1
0
Fork 0

Move stores to reducer

This commit is contained in:
Pabloader 2026-02-17 12:25:39 +00:00
parent 77802634ae
commit 22eeae962a
2 changed files with 92 additions and 46 deletions

View File

@ -1,3 +1,5 @@
import type { StateUpdater } from "preact/hooks";
export const nextFrame = async (): Promise<number> => new Promise((resolve) => requestAnimationFrame(resolve));
export const delay = async (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
@ -74,13 +76,15 @@ export const intHash = (seed: number, ...parts: number[]) => {
return h1;
};
export const sinHash = (...data: number[]) => data.reduce((hash, n) => Math.sin((hash * 123.12 + n) * 756.12), 0) / 2 + 0.5;
export const throttle = function <T, A extends unknown[], R, F extends (this: T, ...args: A) => R>(func: F, ms: number, trailing = false): F {
type F<T, A extends unknown[], R> = (this: T, ...args: A) => R;
export const throttle = function <T, A extends unknown[], R>(func: F<T, A, R>, ms: number, trailing = false): F<T, A, R> {
let isThrottled = false;
let savedResult: R;
let savedThis: T;
let savedArgs: A | undefined;
const wrapper: F = function (...args: A) {
const wrapper = function (this: T, ...args: A): R {
if (isThrottled) {
savedThis = this;
savedArgs = args;
@ -99,7 +103,10 @@ export const throttle = function <T, A extends unknown[], R, F extends (this: T,
}
return savedResult;
} as F;
};
return wrapper;
}
export const callUpdater = <T>(f: StateUpdater<T>, prev: T) =>
typeof f === 'function' ? (f as Function)(prev) : f;

View File

@ -1,16 +1,43 @@
import { createContext } from "preact";
import { useCallback, useEffect, useMemo, useState, type Dispatch, type StateUpdater } from "preact/hooks";
import { useCallback, useEffect, useMemo, useReducer, useState, type Dispatch, type StateUpdater } from "preact/hooks";
import { MessageTools, type IMessage } from "../tools/messages";
import { useInputState } from "@common/hooks/useInputState";
import { type IConnection } from "../tools/connection";
import { loadObject, saveObject } from "../tools/storage";
import { useInputCallback } from "@common/hooks/useInputCallback";
import { callUpdater, throttle } from "@common/utils";
interface IStory {
lore: string;
messages: IMessage[];
}
interface StoriesState {
currentStory: string;
stories: Record<string, IStory>;
lore: string;
messages: IMessage[];
}
interface StoryActionSetCurrent {
currentStory: StateUpdater<string>;
}
interface StoryActionSetMessages {
messages: StateUpdater<IMessage[]>;
}
interface StoryActionSetLore {
lore: StateUpdater<string>;
}
interface StoryActionWithId {
action: 'create' | 'delete';
id: string;
}
type StoryAction = StoryActionSetCurrent | StoryActionSetLore | StoryActionSetMessages | StoryActionWithId;
interface IContext {
currentConnection: number;
availableConnections: IConnection[];
@ -118,7 +145,7 @@ const EMPTY_STORY: IStory = {
messages: [],
};
const saveContext = async (context: IContext & IComputableContext) => {
const saveContext = throttle(async (context: IContext & IComputableContext) => {
const contextToSave: Partial<IContext & IComputableContext> = { ...context };
delete contextToSave.connection;
delete contextToSave.triggerNext;
@ -127,7 +154,7 @@ const saveContext = async (context: IContext & IComputableContext) => {
delete contextToSave.messages;
return saveObject(SAVE_KEY, contextToSave);
}
}, 1000, true);
export type IStateContext = IContext & IActions & IComputableContext;
@ -135,12 +162,43 @@ export const StateContext = createContext<IStateContext>({} as IStateContext);
const loadedContext = await loadObject(SAVE_KEY, DEFAULT_CONTEXT);
const storyReducer = (state: StoriesState, action: StoryAction): StoriesState => {
let { currentStory, stories } = state;
if ('id' in action) {
switch (action.action) {
case 'create':
stories[action.id] = EMPTY_STORY;
break;
case 'delete':
if (action.id !== DEFAULT_STORY) {
delete stories[action.id];
if (action.id === currentStory) {
currentStory = DEFAULT_STORY;
}
}
break;
}
} else if ('currentStory' in action) {
currentStory = callUpdater(action.currentStory, currentStory);
} else if ('messages' in action) {
stories[currentStory].messages = callUpdater(action.messages, stories[currentStory]?.messages ?? []);
} else if ('lore' in action) {
stories[currentStory].lore = callUpdater(action.lore, stories[currentStory]?.lore ?? '');
}
return {
currentStory,
stories,
lore: stories[currentStory].lore ?? '',
messages: stories[currentStory].messages ?? [],
};
};
export const StateContextProvider = ({ children }: { children?: any }) => {
const [currentConnection, setCurrentConnection] = useState<number>(loadedContext.currentConnection);
const [availableConnections, setAvailableConnections] = useState<IConnection[]>(loadedContext.availableConnections);
const [input, setInput] = useInputState(loadedContext.input);
const [stories, setStories] = useState(loadedContext.stories);
const [currentStory, setCurrentStory] = useState(loadedContext.currentStory);
const [systemPrompt, setSystemPrompt] = useInputState(loadedContext.systemPrompt);
const [userPrompt, setUserPrompt] = useInputState(loadedContext.userPrompt);
const [summarizePrompt, setSummarizePrompt] = useInputState(loadedContext.summarizePrompt);
@ -148,6 +206,13 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
const [summaryEnabled, setSummaryEnabled] = useState(loadedContext.summaryEnabled);
const [totalSpentKudos, setTotalSpentKudos] = useState(loadedContext.totalSpentKudos);
const [storiesState, storyDispatch] = useReducer(storyReducer, {
stories: loadedContext.stories,
currentStory: loadedContext.currentStory,
lore: loadedContext.stories[loadedContext.currentStory]?.lore ?? '',
messages: loadedContext.stories[loadedContext.currentStory]?.messages ?? [],
});
const connection = availableConnections[currentConnection] ?? DEFAULT_CONTEXT.availableConnections[0];
const [triggerNext, setTriggerNext] = useState(false);
@ -167,33 +232,16 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
useEffect(() => setConnection({ ...connection, instruct }), [instruct]);
const setLore = useInputCallback((lore) => {
if (!currentStory) return;
setStories(ss => ({
...ss,
[currentStory]: {
...EMPTY_STORY,
...stories[currentStory],
lore,
}
}));
}, [currentStory]);
storyDispatch({ lore });
}, []);
const setMessages = useCallback((msg: StateUpdater<IMessage[]>) => {
if (!currentStory) return;
const setMessages = useCallback((messages: StateUpdater<IMessage[]>) => {
storyDispatch({ messages });
}, []);
let messages = (typeof msg === 'function')
? msg(stories[currentStory]?.messages ?? EMPTY_STORY.messages)
: msg;
setStories(ss => ({
...ss,
[currentStory]: {
...EMPTY_STORY,
...stories[currentStory],
messages,
}
}));
}, [currentStory]);
const setCurrentStory = useCallback((currentStory: StateUpdater<string>) => {
storyDispatch({ currentStory });
}, []);
const actions: IActions = useMemo(() => ({
setConnection,
@ -285,18 +333,12 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
setContinueLast(c);
},
createStory: (id: string) => {
setStories(ss => ({
...ss,
[id]: { ...EMPTY_STORY }
}))
storyDispatch({ id, action: 'create' });
},
deleteStory: (id: string) => {
if (id === DEFAULT_STORY) return;
setStories(ss => Object.fromEntries(Object.entries(ss).filter(([k]) => k !== id)));
setCurrentStory(cs => cs === id ? DEFAULT_STORY : cs);
storyDispatch({ id, action: 'delete' });
}
}), [setLore, setMessages]);
}), []);
const rawContext: IContext & IComputableContext = {
connection,
@ -309,13 +351,10 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
summaryEnabled,
bannedWords,
totalSpentKudos,
stories,
currentStory,
...storiesState,
//
triggerNext,
continueLast,
lore: stories[currentStory]?.lore ?? '',
messages: stories[currentStory]?.messages ?? [],
};
const context = useMemo(() => rawContext, Object.values(rawContext));