diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz index 9e44f03260a..b5266edee70 100644 Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ diff --git a/tools/server/webui/docs/architecture/high-level-architecture-simplified.md b/tools/server/webui/docs/architecture/high-level-architecture-simplified.md index 50f2e1df0a0..a6cb1e9c394 100644 --- a/tools/server/webui/docs/architecture/high-level-architecture-simplified.md +++ b/tools/server/webui/docs/architecture/high-level-architecture-simplified.md @@ -11,6 +11,8 @@ flowchart TB C_Screen["ChatScreen"] C_Form["ChatForm"] C_Messages["ChatMessages"] + C_Message["ChatMessage"] + C_MessageEditForm["ChatMessageEditForm"] C_ModelsSelector["ModelsSelector"] C_Settings["ChatSettings"] end @@ -54,7 +56,9 @@ flowchart TB %% Component hierarchy C_Screen --> C_Form & C_Messages & C_Settings - C_Form & C_Messages --> C_ModelsSelector + C_Messages --> C_Message + C_Message --> C_MessageEditForm + C_Form & C_MessageEditForm --> C_ModelsSelector %% Components → Hooks → Stores C_Form & C_Messages --> H1 & H2 @@ -93,7 +97,7 @@ flowchart TB classDef apiStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px class R1,R2,RL routeStyle - class C_Sidebar,C_Screen,C_Form,C_Messages,C_ModelsSelector,C_Settings componentStyle + class C_Sidebar,C_Screen,C_Form,C_Messages,C_Message,C_MessageEditForm,C_ModelsSelector,C_Settings componentStyle class H1,H2 hookStyle class S1,S2,S3,S4,S5 storeStyle class SV1,SV2,SV3,SV4,SV5 serviceStyle diff --git a/tools/server/webui/docs/architecture/high-level-architecture.md b/tools/server/webui/docs/architecture/high-level-architecture.md index 730da10a59e..c5ec4d69095 100644 --- a/tools/server/webui/docs/architecture/high-level-architecture.md +++ b/tools/server/webui/docs/architecture/high-level-architecture.md @@ -16,6 +16,8 @@ end C_Form["ChatForm"] C_Messages["ChatMessages"] C_Message["ChatMessage"] + C_MessageUser["ChatMessageUser"] + C_MessageEditForm["ChatMessageEditForm"] C_Attach["ChatAttachments"] C_ModelsSelector["ModelsSelector"] C_Settings["ChatSettings"] @@ -38,7 +40,7 @@ end S1Error["Error Handling:showErrorDialog()dismissErrorDialog()isAbortError()"] S1Msg["Message Operations:addMessage()sendMessage()updateMessage()deleteMessage()getDeletionInfo()"] S1Regen["Regeneration:regenerateMessage()regenerateMessageWithBranching()continueAssistantMessage()"] - S1Edit["Editing:editAssistantMessage()editUserMessagePreserveResponses()editMessageWithBranching()"] + S1Edit["Editing:editAssistantMessage()editUserMessagePreserveResponses()editMessageWithBranching()clearEditMode()isEditModeActive()getAddFilesHandler()setEditModeActive()"] S1Utils["Utilities:getApiOptions()parseTimingData()getOrCreateAbortController()getConversationModel()"] end subgraph S2["conversationsStore"] @@ -88,6 +90,10 @@ end RE7["getChatStreaming()"] RE8["getAllLoadingChats()"] RE9["getAllStreamingChats()"] + RE9a["isEditModeActive()"] + RE9b["getAddFilesHandler()"] + RE9c["setEditModeActive()"] + RE9d["clearEditMode()"] end subgraph ConvExports["conversationsStore"] RE10["conversations()"] @@ -182,7 +188,10 @@ end %% Component hierarchy C_Screen --> C_Form & C_Messages & C_Settings C_Messages --> C_Message - C_Message --> C_ModelsSelector + C_Message --> C_MessageUser + C_MessageUser --> C_MessageEditForm + C_MessageEditForm --> C_ModelsSelector + C_MessageEditForm --> C_Attach C_Form --> C_ModelsSelector C_Form --> C_Attach C_Message --> C_Attach @@ -190,6 +199,7 @@ end %% Components use Hooks C_Form --> H1 C_Message --> H1 & H2 + C_MessageEditForm --> H1 C_Screen --> H2 %% Hooks use Stores @@ -244,7 +254,7 @@ end classDef apiStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px class R1,R2,RL routeStyle - class C_Sidebar,C_Screen,C_Form,C_Messages,C_Message componentStyle + class C_Sidebar,C_Screen,C_Form,C_Messages,C_Message,C_MessageUser,C_MessageEditForm componentStyle class C_ModelsSelector,C_Settings componentStyle class C_Attach componentStyle class H1,H2,H3 methodStyle diff --git a/tools/server/webui/package-lock.json b/tools/server/webui/package-lock.json index 0d1a03aca34..6fa9d39c719 100644 --- a/tools/server/webui/package-lock.json +++ b/tools/server/webui/package-lock.json @@ -25,7 +25,7 @@ "@chromatic-com/storybook": "^4.1.2", "@eslint/compat": "^1.2.5", "@eslint/js": "^9.18.0", - "@internationalized/date": "^3.8.2", + "@internationalized/date": "^3.10.1", "@lucide/svelte": "^0.515.0", "@playwright/test": "^1.49.1", "@storybook/addon-a11y": "^10.0.7", @@ -862,9 +862,9 @@ } }, "node_modules/@internationalized/date": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.8.2.tgz", - "integrity": "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.1.tgz", + "integrity": "sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA==", "dev": true, "license": "Apache-2.0", "dependencies": { diff --git a/tools/server/webui/package.json b/tools/server/webui/package.json index 1c970ae7a89..1a8c2737496 100644 --- a/tools/server/webui/package.json +++ b/tools/server/webui/package.json @@ -26,7 +26,7 @@ "@chromatic-com/storybook": "^4.1.2", "@eslint/compat": "^1.2.5", "@eslint/js": "^9.18.0", - "@internationalized/date": "^3.8.2", + "@internationalized/date": "^3.10.1", "@lucide/svelte": "^0.515.0", "@playwright/test": "^1.49.1", "@storybook/addon-a11y": "^10.0.7", diff --git a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte index 3ad14ed3ab0..fd2f7f60e57 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte @@ -8,6 +8,7 @@ ChatFormTextarea } from '$lib/components/app'; import { INPUT_CLASSES } from '$lib/constants/input-classes'; + import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config'; import { config } from '$lib/stores/settings.svelte'; import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte'; import { isRouterMode } from '$lib/stores/server.svelte'; @@ -66,7 +67,7 @@ let message = $state(''); let pasteLongTextToFileLength = $derived.by(() => { const n = Number(currentConfig.pasteLongTextToFileLen); - return Number.isNaN(n) ? 2500 : n; + return Number.isNaN(n) ? Number(SETTING_CONFIG_DEFAULT.pasteLongTextToFileLen) : n; }); let previousIsLoading = $state(isLoading); let recordingSupported = $state(false); diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte index 0969a937ed2..220276fc9e3 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessage.svelte @@ -12,13 +12,21 @@ onCopy?: (message: DatabaseMessage) => void; onContinueAssistantMessage?: (message: DatabaseMessage) => void; onDelete?: (message: DatabaseMessage) => void; - onEditWithBranching?: (message: DatabaseMessage, newContent: string) => void; + onEditWithBranching?: ( + message: DatabaseMessage, + newContent: string, + newExtras?: DatabaseMessageExtra[] + ) => void; onEditWithReplacement?: ( message: DatabaseMessage, newContent: string, shouldBranch: boolean ) => void; - onEditUserMessagePreserveResponses?: (message: DatabaseMessage, newContent: string) => void; + onEditUserMessagePreserveResponses?: ( + message: DatabaseMessage, + newContent: string, + newExtras?: DatabaseMessageExtra[] + ) => void; onNavigateToSibling?: (siblingId: string) => void; onRegenerateWithBranching?: (message: DatabaseMessage, modelOverride?: string) => void; siblingInfo?: ChatMessageSiblingInfo | null; @@ -45,6 +53,8 @@ messageTypes: string[]; } | null>(null); let editedContent = $state(message.content); + let editedExtras = $state(message.extra ? [...message.extra] : []); + let editedUploadedFiles = $state([]); let isEditing = $state(false); let showDeleteDialog = $state(false); let shouldBranchAfterEdit = $state(false); @@ -85,6 +95,16 @@ function handleCancelEdit() { isEditing = false; editedContent = message.content; + editedExtras = message.extra ? [...message.extra] : []; + editedUploadedFiles = []; + } + + function handleEditedExtrasChange(extras: DatabaseMessageExtra[]) { + editedExtras = extras; + } + + function handleEditedUploadedFilesChange(files: ChatUploadedFile[]) { + editedUploadedFiles = files; } async function handleCopy() { @@ -107,6 +127,8 @@ function handleEdit() { isEditing = true; editedContent = message.content; + editedExtras = message.extra ? [...message.extra] : []; + editedUploadedFiles = []; setTimeout(() => { if (textareaElement) { @@ -143,9 +165,10 @@ onContinueAssistantMessage?.(message); } - function handleSaveEdit() { + async function handleSaveEdit() { if (message.role === 'user' || message.role === 'system') { - onEditWithBranching?.(message, editedContent.trim()); + const finalExtras = await getMergedExtras(); + onEditWithBranching?.(message, editedContent.trim(), finalExtras); } else { // For assistant messages, preserve exact content including trailing whitespace // This is important for the Continue feature to work properly @@ -154,15 +177,30 @@ isEditing = false; shouldBranchAfterEdit = false; + editedUploadedFiles = []; } - function handleSaveEditOnly() { + async function handleSaveEditOnly() { if (message.role === 'user') { // For user messages, trim to avoid accidental whitespace - onEditUserMessagePreserveResponses?.(message, editedContent.trim()); + const finalExtras = await getMergedExtras(); + onEditUserMessagePreserveResponses?.(message, editedContent.trim(), finalExtras); } isEditing = false; + editedUploadedFiles = []; + } + + async function getMergedExtras(): Promise { + if (editedUploadedFiles.length === 0) { + return editedExtras; + } + + const { parseFilesToMessageExtras } = await import('$lib/utils/browser-only'); + const result = await parseFilesToMessageExtras(editedUploadedFiles); + const newExtras = result?.extras || []; + + return [...editedExtras, ...newExtras]; } function handleShowDeleteDialogChange(show: boolean) { @@ -197,6 +235,8 @@ class={className} {deletionInfo} {editedContent} + {editedExtras} + {editedUploadedFiles} {isEditing} {message} onCancelEdit={handleCancelEdit} @@ -206,6 +246,8 @@ onEdit={handleEdit} onEditKeydown={handleEditKeydown} onEditedContentChange={handleEditedContentChange} + onEditedExtrasChange={handleEditedExtrasChange} + onEditedUploadedFilesChange={handleEditedUploadedFilesChange} {onNavigateToSibling} onSaveEdit={handleSaveEdit} onSaveEditOnly={handleSaveEditOnly} diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte new file mode 100644 index 00000000000..f812ea2fd9d --- /dev/null +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte @@ -0,0 +1,391 @@ + + + + + + + + { + if (fileId.startsWith('attachment-')) { + const index = parseInt(fileId.replace('attachment-', ''), 10); + if (!isNaN(index) && index >= 0 && index < editedExtras.length) { + handleRemoveExistingAttachment(index); + } + } else { + handleRemoveUploadedFile(fileId); + } + }} + limitToSingleRow + class="py-5" + style="scroll-padding: 1rem;" + /> + + + { + autoResizeTextarea(e.currentTarget); + onEditedContentChange(e.currentTarget.value); + }} + onpaste={handlePaste} + placeholder="Edit your message..." + > + + + fileInputElement?.click()} + type="button" + title="Add attachment" + > + Attach files + + + + + + + {#if isRouter} + + {/if} + + + {saveWithoutRegenerate ? 'Save' : 'Send'} + + + + + + + + + {#if showSaveOnlyOption && onSaveEditOnly} + + + + + Update without re-sending + + + {:else} + + {/if} + + + + + Cancel + + + + (showDiscardDialog = false)} +/> diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageUser.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageUser.svelte index 3d2b8dd35b4..041c6bd2513 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageUser.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageUser.svelte @@ -1,18 +1,17 @@ + + + + diff --git a/tools/server/webui/src/lib/stores/chat.svelte.ts b/tools/server/webui/src/lib/stores/chat.svelte.ts index e0431ee6438..01088945249 100644 --- a/tools/server/webui/src/lib/stores/chat.svelte.ts +++ b/tools/server/webui/src/lib/stores/chat.svelte.ts @@ -74,6 +74,8 @@ class ChatStore { private processingStates = new SvelteMap(); private activeConversationId = $state(null); private isStreamingActive = $state(false); + private isEditModeActive = $state(false); + private addFilesHandler: ((files: File[]) => void) | null = $state(null); // ───────────────────────────────────────────────────────────────────────────── // Loading State @@ -965,6 +967,160 @@ class ChatStore { // Editing // ───────────────────────────────────────────────────────────────────────────── + clearEditMode(): void { + this.isEditModeActive = false; + this.addFilesHandler = null; + } + + async continueAssistantMessage(messageId: string): Promise { + const activeConv = conversationsStore.activeConversation; + if (!activeConv || this.isLoading) return; + + const result = this.getMessageByIdWithRole(messageId, 'assistant'); + if (!result) return; + const { message: msg, index: idx } = result; + + if (this.isChatLoading(activeConv.id)) return; + + try { + this.errorDialogState = null; + this.setChatLoading(activeConv.id, true); + this.clearChatStreaming(activeConv.id); + + const allMessages = await conversationsStore.getConversationMessages(activeConv.id); + const dbMessage = allMessages.find((m) => m.id === messageId); + + if (!dbMessage) { + this.setChatLoading(activeConv.id, false); + + return; + } + + const originalContent = dbMessage.content; + const originalThinking = dbMessage.thinking || ''; + + const conversationContext = conversationsStore.activeMessages.slice(0, idx); + const contextWithContinue = [ + ...conversationContext, + { role: 'assistant' as const, content: originalContent } + ]; + + let appendedContent = '', + appendedThinking = '', + hasReceivedContent = false; + + const abortController = this.getOrCreateAbortController(msg.convId); + + await ChatService.sendMessage( + contextWithContinue, + { + ...this.getApiOptions(), + + onChunk: (chunk: string) => { + hasReceivedContent = true; + appendedContent += chunk; + const fullContent = originalContent + appendedContent; + this.setChatStreaming(msg.convId, fullContent, msg.id); + conversationsStore.updateMessageAtIndex(idx, { content: fullContent }); + }, + + onReasoningChunk: (reasoningChunk: string) => { + hasReceivedContent = true; + appendedThinking += reasoningChunk; + conversationsStore.updateMessageAtIndex(idx, { + thinking: originalThinking + appendedThinking + }); + }, + + onTimings: (timings: ChatMessageTimings, promptProgress?: ChatMessagePromptProgress) => { + const tokensPerSecond = + timings?.predicted_ms && timings?.predicted_n + ? (timings.predicted_n / timings.predicted_ms) * 1000 + : 0; + this.updateProcessingStateFromTimings( + { + prompt_n: timings?.prompt_n || 0, + prompt_ms: timings?.prompt_ms, + predicted_n: timings?.predicted_n || 0, + predicted_per_second: tokensPerSecond, + cache_n: timings?.cache_n || 0, + prompt_progress: promptProgress + }, + msg.convId + ); + }, + + onComplete: async ( + finalContent?: string, + reasoningContent?: string, + timings?: ChatMessageTimings + ) => { + const fullContent = originalContent + (finalContent || appendedContent); + const fullThinking = originalThinking + (reasoningContent || appendedThinking); + await DatabaseService.updateMessage(msg.id, { + content: fullContent, + thinking: fullThinking, + timestamp: Date.now(), + timings + }); + conversationsStore.updateMessageAtIndex(idx, { + content: fullContent, + thinking: fullThinking, + timestamp: Date.now(), + timings + }); + conversationsStore.updateConversationTimestamp(); + this.setChatLoading(msg.convId, false); + this.clearChatStreaming(msg.convId); + this.clearProcessingState(msg.convId); + }, + + onError: async (error: Error) => { + if (this.isAbortError(error)) { + if (hasReceivedContent && appendedContent) { + await DatabaseService.updateMessage(msg.id, { + content: originalContent + appendedContent, + thinking: originalThinking + appendedThinking, + timestamp: Date.now() + }); + conversationsStore.updateMessageAtIndex(idx, { + content: originalContent + appendedContent, + thinking: originalThinking + appendedThinking, + timestamp: Date.now() + }); + } + this.setChatLoading(msg.convId, false); + this.clearChatStreaming(msg.convId); + this.clearProcessingState(msg.convId); + return; + } + console.error('Continue generation error:', error); + conversationsStore.updateMessageAtIndex(idx, { + content: originalContent, + thinking: originalThinking + }); + await DatabaseService.updateMessage(msg.id, { + content: originalContent, + thinking: originalThinking + }); + this.setChatLoading(msg.convId, false); + this.clearChatStreaming(msg.convId); + this.clearProcessingState(msg.convId); + this.showErrorDialog( + error.name === 'TimeoutError' ? 'timeout' : 'server', + error.message + ); + } + }, + msg.convId, + abortController.signal + ); + } catch (error) { + if (!this.isAbortError(error)) console.error('Failed to continue message:', error); + if (activeConv) this.setChatLoading(activeConv.id, false); + } + } + async editAssistantMessage( messageId: string, newContent: string, @@ -995,11 +1151,10 @@ class ChatStore { ); await conversationsStore.updateCurrentNode(newMessage.id); } else { - await DatabaseService.updateMessage(msg.id, { content: newContent, timestamp: Date.now() }); + await DatabaseService.updateMessage(msg.id, { content: newContent }); await conversationsStore.updateCurrentNode(msg.id); conversationsStore.updateMessageAtIndex(idx, { - content: newContent, - timestamp: Date.now() + content: newContent }); } conversationsStore.updateConversationTimestamp(); @@ -1009,7 +1164,11 @@ class ChatStore { } } - async editUserMessagePreserveResponses(messageId: string, newContent: string): Promise { + async editUserMessagePreserveResponses( + messageId: string, + newContent: string, + newExtras?: DatabaseMessageExtra[] + ): Promise { const activeConv = conversationsStore.activeConversation; if (!activeConv) return; @@ -1018,11 +1177,18 @@ class ChatStore { const { message: msg, index: idx } = result; try { - await DatabaseService.updateMessage(messageId, { - content: newContent, - timestamp: Date.now() - }); - conversationsStore.updateMessageAtIndex(idx, { content: newContent, timestamp: Date.now() }); + const updateData: Partial = { + content: newContent + }; + + // Update extras if provided (including empty array to clear attachments) + // Deep clone to avoid Proxy objects from Svelte reactivity + if (newExtras !== undefined) { + updateData.extra = JSON.parse(JSON.stringify(newExtras)); + } + + await DatabaseService.updateMessage(messageId, updateData); + conversationsStore.updateMessageAtIndex(idx, updateData); const allMessages = await conversationsStore.getConversationMessages(activeConv.id); const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null); @@ -1040,7 +1206,11 @@ class ChatStore { } } - async editMessageWithBranching(messageId: string, newContent: string): Promise { + async editMessageWithBranching( + messageId: string, + newContent: string, + newExtras?: DatabaseMessageExtra[] + ): Promise { const activeConv = conversationsStore.activeConversation; if (!activeConv || this.isLoading) return; @@ -1062,6 +1232,15 @@ class ChatStore { const parentId = msg.parent || rootMessage?.id; if (!parentId) return; + // Use newExtras if provided, otherwise copy existing extras + // Deep clone to avoid Proxy objects from Svelte reactivity + const extrasToUse = + newExtras !== undefined + ? JSON.parse(JSON.stringify(newExtras)) + : msg.extra + ? JSON.parse(JSON.stringify(msg.extra)) + : undefined; + const newMessage = await DatabaseService.createMessageBranch( { convId: msg.convId, @@ -1072,7 +1251,7 @@ class ChatStore { thinking: msg.thinking || '', toolCalls: msg.toolCalls || '', children: [], - extra: msg.extra ? JSON.parse(JSON.stringify(msg.extra)) : undefined, + extra: extrasToUse, model: msg.model }, parentId @@ -1191,168 +1370,35 @@ class ChatStore { } } - async continueAssistantMessage(messageId: string): Promise { - const activeConv = conversationsStore.activeConversation; - if (!activeConv || this.isLoading) return; - - const result = this.getMessageByIdWithRole(messageId, 'assistant'); - if (!result) return; - const { message: msg, index: idx } = result; - - if (this.isChatLoading(activeConv.id)) return; - - try { - this.errorDialogState = null; - this.setChatLoading(activeConv.id, true); - this.clearChatStreaming(activeConv.id); - - const allMessages = await conversationsStore.getConversationMessages(activeConv.id); - const dbMessage = allMessages.find((m) => m.id === messageId); - - if (!dbMessage) { - this.setChatLoading(activeConv.id, false); - - return; - } - - const originalContent = dbMessage.content; - const originalThinking = dbMessage.thinking || ''; - - const conversationContext = conversationsStore.activeMessages.slice(0, idx); - const contextWithContinue = [ - ...conversationContext, - { role: 'assistant' as const, content: originalContent } - ]; - - let appendedContent = '', - appendedThinking = '', - hasReceivedContent = false; - - const abortController = this.getOrCreateAbortController(msg.convId); - - await ChatService.sendMessage( - contextWithContinue, - { - ...this.getApiOptions(), - - onChunk: (chunk: string) => { - hasReceivedContent = true; - appendedContent += chunk; - const fullContent = originalContent + appendedContent; - this.setChatStreaming(msg.convId, fullContent, msg.id); - conversationsStore.updateMessageAtIndex(idx, { content: fullContent }); - }, - - onReasoningChunk: (reasoningChunk: string) => { - hasReceivedContent = true; - appendedThinking += reasoningChunk; - conversationsStore.updateMessageAtIndex(idx, { - thinking: originalThinking + appendedThinking - }); - }, - - onTimings: (timings: ChatMessageTimings, promptProgress?: ChatMessagePromptProgress) => { - const tokensPerSecond = - timings?.predicted_ms && timings?.predicted_n - ? (timings.predicted_n / timings.predicted_ms) * 1000 - : 0; - this.updateProcessingStateFromTimings( - { - prompt_n: timings?.prompt_n || 0, - prompt_ms: timings?.prompt_ms, - predicted_n: timings?.predicted_n || 0, - predicted_per_second: tokensPerSecond, - cache_n: timings?.cache_n || 0, - prompt_progress: promptProgress - }, - msg.convId - ); - }, - - onComplete: async ( - finalContent?: string, - reasoningContent?: string, - timings?: ChatMessageTimings - ) => { - const fullContent = originalContent + (finalContent || appendedContent); - const fullThinking = originalThinking + (reasoningContent || appendedThinking); - await DatabaseService.updateMessage(msg.id, { - content: fullContent, - thinking: fullThinking, - timestamp: Date.now(), - timings - }); - conversationsStore.updateMessageAtIndex(idx, { - content: fullContent, - thinking: fullThinking, - timestamp: Date.now(), - timings - }); - conversationsStore.updateConversationTimestamp(); - this.setChatLoading(msg.convId, false); - this.clearChatStreaming(msg.convId); - this.clearProcessingState(msg.convId); - }, + getAddFilesHandler(): ((files: File[]) => void) | null { + return this.addFilesHandler; + } - onError: async (error: Error) => { - if (this.isAbortError(error)) { - if (hasReceivedContent && appendedContent) { - await DatabaseService.updateMessage(msg.id, { - content: originalContent + appendedContent, - thinking: originalThinking + appendedThinking, - timestamp: Date.now() - }); - conversationsStore.updateMessageAtIndex(idx, { - content: originalContent + appendedContent, - thinking: originalThinking + appendedThinking, - timestamp: Date.now() - }); - } - this.setChatLoading(msg.convId, false); - this.clearChatStreaming(msg.convId); - this.clearProcessingState(msg.convId); - return; - } - console.error('Continue generation error:', error); - conversationsStore.updateMessageAtIndex(idx, { - content: originalContent, - thinking: originalThinking - }); - await DatabaseService.updateMessage(msg.id, { - content: originalContent, - thinking: originalThinking - }); - this.setChatLoading(msg.convId, false); - this.clearChatStreaming(msg.convId); - this.clearProcessingState(msg.convId); - this.showErrorDialog( - error.name === 'TimeoutError' ? 'timeout' : 'server', - error.message - ); - } - }, - msg.convId, - abortController.signal - ); - } catch (error) { - if (!this.isAbortError(error)) console.error('Failed to continue message:', error); - if (activeConv) this.setChatLoading(activeConv.id, false); - } + public getAllLoadingChats(): string[] { + return Array.from(this.chatLoadingStates.keys()); } - public isChatLoadingPublic(convId: string): boolean { - return this.isChatLoading(convId); + public getAllStreamingChats(): string[] { + return Array.from(this.chatStreamingStates.keys()); } + public getChatStreamingPublic( convId: string ): { response: string; messageId: string } | undefined { return this.getChatStreaming(convId); } - public getAllLoadingChats(): string[] { - return Array.from(this.chatLoadingStates.keys()); + + public isChatLoadingPublic(convId: string): boolean { + return this.isChatLoading(convId); } - public getAllStreamingChats(): string[] { - return Array.from(this.chatStreamingStates.keys()); + + isEditing(): boolean { + return this.isEditModeActive; + } + + setEditModeActive(handler: (files: File[]) => void): void { + this.isEditModeActive = true; + this.addFilesHandler = handler; } // ───────────────────────────────────────────────────────────────────────────── @@ -1416,13 +1462,17 @@ class ChatStore { export const chatStore = new ChatStore(); -export const isLoading = () => chatStore.isLoading; +export const activeProcessingState = () => chatStore.activeProcessingState; +export const clearEditMode = () => chatStore.clearEditMode(); export const currentResponse = () => chatStore.currentResponse; export const errorDialog = () => chatStore.errorDialogState; -export const activeProcessingState = () => chatStore.activeProcessingState; -export const isChatStreaming = () => chatStore.isStreaming(); - -export const isChatLoading = (convId: string) => chatStore.isChatLoadingPublic(convId); -export const getChatStreaming = (convId: string) => chatStore.getChatStreamingPublic(convId); +export const getAddFilesHandler = () => chatStore.getAddFilesHandler(); export const getAllLoadingChats = () => chatStore.getAllLoadingChats(); export const getAllStreamingChats = () => chatStore.getAllStreamingChats(); +export const getChatStreaming = (convId: string) => chatStore.getChatStreamingPublic(convId); +export const isChatLoading = (convId: string) => chatStore.isChatLoadingPublic(convId); +export const isChatStreaming = () => chatStore.isStreaming(); +export const isEditing = () => chatStore.isEditing(); +export const isLoading = () => chatStore.isLoading; +export const setEditModeActive = (handler: (files: File[]) => void) => + chatStore.setEditModeActive(handler);