From eaa79c6c49c9890d9444cd71caa16775bc670068 Mon Sep 17 00:00:00 2001 From: Pabloader Date: Fri, 10 Apr 2026 16:01:21 +0000 Subject: [PATCH] Continue mode for chat --- .../assets/chat-sidebar.module.css | 12 -- .../storywriter/components/chat-sidebar.tsx | 125 ++++++++++-------- src/games/storywriter/utils/llm.ts | 1 + src/games/storywriter/utils/prompt.ts | 15 ++- 4 files changed, 85 insertions(+), 68 deletions(-) diff --git a/src/games/storywriter/assets/chat-sidebar.module.css b/src/games/storywriter/assets/chat-sidebar.module.css index e9514d7..7f82e41 100644 --- a/src/games/storywriter/assets/chat-sidebar.module.css +++ b/src/games/storywriter/assets/chat-sidebar.module.css @@ -43,18 +43,6 @@ .messageActions { display: flex; gap: 4px; - opacity: 0; - transition: opacity 0.15s ease; -} - -.message:hover .messageActions { - opacity: 1; -} - -@media (max-width: 1000px) { - .messageActions { - opacity: 1; - } } .iconButton { diff --git a/src/games/storywriter/components/chat-sidebar.tsx b/src/games/storywriter/components/chat-sidebar.tsx index 5f96080..96e9855 100644 --- a/src/games/storywriter/components/chat-sidebar.tsx +++ b/src/games/storywriter/components/chat-sidebar.tsx @@ -115,17 +115,17 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => { const countTokens = async () => { try { - const messages: ChatMessage[] = []; + const newMessages: ChatMessage[] = []; if (input.trim()) { - messages.push({ + newMessages.push({ id: crypto.randomUUID(), role: 'user', content: input.trim(), }); } - const chatRequest = Prompt.compilePrompt(appStateRef.current, messages); + const chatRequest = Prompt.compilePrompt(appStateRef.current, { newMessages }); const countRequest: LLM.CountTokensRequest = { model: model.id, input: chatRequest?.messages ?? [], @@ -148,14 +148,14 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => { return () => clearTimeout(timeoutId); }, [currentStory, connection, model, input, currentStory?.chatMessages.length]); - const sendMessage = useCallback(async ( - newMessages: Iterable, - excludedMessageIds: string[] = [], - ) => { + const sendMessage = useCallback(async (config: Prompt.CompileConfig = {}) => { if (!currentStory || !currentWorld || !connection || !model) return; + const { newMessages = [], excludedMessageIds = [] } = config; + const excludedSet = new Set(excludedMessageIds); + for (const message of newMessages) { - if (excludedMessageIds.includes(message.id)) continue; + if (excludedSet.has(message.id)) continue; dispatch({ type: 'ADD_CHAT_MESSAGE', worldId: currentWorld.id, @@ -164,20 +164,25 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => { }); } - const assistantMessageId = crypto.randomUUID(); - dispatch({ - type: 'ADD_CHAT_MESSAGE', - worldId: currentWorld.id, - storyId: currentStory.id, - message: { - id: assistantMessageId, - role: 'assistant', - content: '', - reasoning_content: 'Generating...', - }, - }); + const continuedMessage = config.continueLast ? currentStory.chatMessages.at(-1) : null; + const targetMessageId = continuedMessage?.id ?? crypto.randomUUID(); + const targetRole = continuedMessage?.role ?? 'assistant'; - const request = Prompt.compilePrompt(appStateRef.current, newMessages, excludedMessageIds); + if (!continuedMessage) { + dispatch({ + type: 'ADD_CHAT_MESSAGE', + worldId: currentWorld.id, + storyId: currentStory.id, + message: { + id: targetMessageId, + role: 'assistant', + content: '', + reasoning_content: 'Generating...', + }, + }); + } + + const request = Prompt.compilePrompt(appStateRef.current, config); if (!request) { setError('Failed to compile prompt'); @@ -188,8 +193,8 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => { try { const charName = currentWorld.title ?? 'Assistant'; const prefix = `${charName}: `; - let accumulatedContent = ''; - let accumulatedReasoning = ''; + let accumulatedContent = continuedMessage?.content ?? ''; + let accumulatedReasoning = continuedMessage?.role === 'assistant' ? continuedMessage.reasoning_content ?? '' : ''; let tool_calls: LLM.ToolCall[] | undefined; for await (const chunk of LLM.generateStream(connection, request)) { @@ -219,8 +224,8 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => { worldId: currentWorld.id, storyId: currentStory.id, message: { - id: assistantMessageId, - role: 'assistant', + id: targetMessageId, + role: targetRole, content: accumulatedContent, reasoning_content: accumulatedReasoning, tool_calls, @@ -228,9 +233,9 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => { }); } } - const assistantMessage: ChatMessage = { - id: assistantMessageId, - role: 'assistant', + const finalMessage: ChatMessage = { + id: targetMessageId, + role: targetRole, content: accumulatedContent, reasoning_content: accumulatedReasoning, tool_calls, @@ -239,7 +244,7 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => { type: 'ADD_CHAT_MESSAGE', worldId: currentWorld.id, storyId: currentStory.id, - message: assistantMessage, + message: finalMessage, }); if (tool_calls) { @@ -265,7 +270,13 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => { } if (!abortControllerRef.current?.signal.aborted) { - return sendMessage([...newMessages, assistantMessage, ...toolMessages]); + return sendMessage({ + newMessages: [ + ...newMessages, + finalMessage, + ...toolMessages, + ] + }); } } } catch (err) { @@ -288,7 +299,7 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => { setError(null); abortControllerRef.current = new AbortController(); - const excludedMessages: string[] = []; + const excludedMessageIds = new Set(); try { if (isAssistant) { // Delete the last assistant message and regenerate @@ -298,9 +309,9 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => { storyId: currentStory.id, messageId: lastMessage.id, }); - excludedMessages.push(lastMessage.id); + excludedMessageIds.add(lastMessage.id); } - await sendMessage([], excludedMessages); + await sendMessage({ excludedMessageIds }); } finally { setIsLoading(false); } @@ -322,11 +333,13 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => { abortControllerRef.current = new AbortController(); try { - await sendMessage([{ - id: crypto.randomUUID(), - role: 'user' as const, - content: input.trim(), - }]); + await sendMessage({ + newMessages: [{ + id: crypto.randomUUID(), + role: 'user' as const, + content: input.trim(), + }] + }); } finally { setIsLoading(false); } @@ -341,15 +354,21 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => { abortControllerRef.current = new AbortController(); try { - await sendMessage([{ - id: crypto.randomUUID(), - role: 'user' as const, - content: (continuePrompt + '\n\n' + input).trim(), - }]); + if (currentWorld?.chatOnly) { + await sendMessage({ continueLast: true }); + } else { + await sendMessage({ + newMessages: [{ + id: crypto.randomUUID(), + role: 'user' as const, + content: (continuePrompt + '\n\n' + input).trim(), + }] + }); + } } finally { setIsLoading(false); } - }, [currentStory, input, connection, model, isLoading, sendMessage]); + }, [currentStory, currentWorld, input, connection, model, isLoading, sendMessage]); const handleStopGeneration = useCallback(() => { abortControllerRef.current?.abort(); @@ -599,16 +618,14 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => { )} - {!currentWorld?.chatOnly && ( - - )} +