From 22eeae962a7d59486dc5f7f3ca00e4984f7a16bd Mon Sep 17 00:00:00 2001 From: Pabloader Date: Tue, 17 Feb 2026 12:25:39 +0000 Subject: [PATCH] Move stores to reducer --- src/common/utils.ts | 13 ++- src/games/ai-story/contexts/state.tsx | 125 +++++++++++++++++--------- 2 files changed, 92 insertions(+), 46 deletions(-) diff --git a/src/common/utils.ts b/src/common/utils.ts index 027232d..f3cd323 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,3 +1,5 @@ +import type { StateUpdater } from "preact/hooks"; + export const nextFrame = async (): Promise => 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 R>(func: F, ms: number, trailing = false): F { + +type F = (this: T, ...args: A) => R; +export const throttle = function (func: F, ms: number, trailing = false): F { 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 (f: StateUpdater, prev: T) => + typeof f === 'function' ? (f as Function)(prev) : f; diff --git a/src/games/ai-story/contexts/state.tsx b/src/games/ai-story/contexts/state.tsx index 973b956..fe4a077 100644 --- a/src/games/ai-story/contexts/state.tsx +++ b/src/games/ai-story/contexts/state.tsx @@ -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; + lore: string; + messages: IMessage[]; +} + +interface StoryActionSetCurrent { + currentStory: StateUpdater; +} + +interface StoryActionSetMessages { + messages: StateUpdater; +} + +interface StoryActionSetLore { + lore: StateUpdater; +} + +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 = { ...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({} 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(loadedContext.currentConnection); const [availableConnections, setAvailableConnections] = useState(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) => { - if (!currentStory) return; + const setMessages = useCallback((messages: StateUpdater) => { + 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) => { + 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));