diff options
| author | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-02-12 20:57:17 +0100 |
|---|---|---|
| committer | Mitja Felicijan <mitja.felicijan@gmail.com> | 2026-02-12 20:57:17 +0100 |
| commit | b333b06772c89d96aacb5490d6a219fba7c09cc6 (patch) | |
| tree | 211df60083a5946baa2ed61d33d8121b7e251b06 /llama.cpp/tools/server/webui/src/lib/stores/chat.svelte.ts | |
| download | llmnpc-b333b06772c89d96aacb5490d6a219fba7c09cc6.tar.gz | |
Engage!
Diffstat (limited to 'llama.cpp/tools/server/webui/src/lib/stores/chat.svelte.ts')
| -rw-r--r-- | llama.cpp/tools/server/webui/src/lib/stores/chat.svelte.ts | 1487 |
1 files changed, 1487 insertions, 0 deletions
diff --git a/llama.cpp/tools/server/webui/src/lib/stores/chat.svelte.ts b/llama.cpp/tools/server/webui/src/lib/stores/chat.svelte.ts new file mode 100644 index 0000000..879b2f3 --- /dev/null +++ b/llama.cpp/tools/server/webui/src/lib/stores/chat.svelte.ts | |||
| @@ -0,0 +1,1487 @@ | |||
| 1 | import { DatabaseService, ChatService } from '$lib/services'; | ||
| 2 | import { conversationsStore } from '$lib/stores/conversations.svelte'; | ||
| 3 | import { config } from '$lib/stores/settings.svelte'; | ||
| 4 | import { contextSize, isRouterMode } from '$lib/stores/server.svelte'; | ||
| 5 | import { | ||
| 6 | selectedModelName, | ||
| 7 | modelsStore, | ||
| 8 | selectedModelContextSize | ||
| 9 | } from '$lib/stores/models.svelte'; | ||
| 10 | import { | ||
| 11 | normalizeModelName, | ||
| 12 | filterByLeafNodeId, | ||
| 13 | findDescendantMessages, | ||
| 14 | findLeafNode | ||
| 15 | } from '$lib/utils'; | ||
| 16 | import { SvelteMap } from 'svelte/reactivity'; | ||
| 17 | import { DEFAULT_CONTEXT } from '$lib/constants/default-context'; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * chatStore - Active AI interaction and streaming state management | ||
| 21 | * | ||
| 22 | * **Terminology - Chat vs Conversation:** | ||
| 23 | * - **Chat**: The active interaction space with the Chat Completions API. Represents the | ||
| 24 | * real-time streaming session, loading states, and UI visualization of AI communication. | ||
| 25 | * A "chat" is ephemeral - it exists only while the user is actively interacting with the AI. | ||
| 26 | * - **Conversation**: The persistent database entity storing all messages and metadata. | ||
| 27 | * Managed by conversationsStore, conversations persist across sessions and page reloads. | ||
| 28 | * | ||
| 29 | * This store manages all active AI interactions including real-time streaming, response | ||
| 30 | * generation, and per-chat loading states. It handles the runtime layer between UI and | ||
| 31 | * AI backend, supporting concurrent streaming across multiple conversations. | ||
| 32 | * | ||
| 33 | * **Architecture & Relationships:** | ||
| 34 | * - **chatStore** (this class): Active AI session and streaming management | ||
| 35 | * - Manages real-time AI response streaming via ChatService | ||
| 36 | * - Tracks per-chat loading and streaming states for concurrent sessions | ||
| 37 | * - Handles message operations (send, edit, regenerate, branch) | ||
| 38 | * - Coordinates with conversationsStore for persistence | ||
| 39 | * | ||
| 40 | * - **conversationsStore**: Provides conversation data and message arrays for chat context | ||
| 41 | * - **ChatService**: Low-level API communication with llama.cpp server | ||
| 42 | * - **DatabaseService**: Message persistence and retrieval | ||
| 43 | * | ||
| 44 | * **Key Features:** | ||
| 45 | * - **AI Streaming**: Real-time token streaming with abort support | ||
| 46 | * - **Concurrent Chats**: Independent loading/streaming states per conversation | ||
| 47 | * - **Message Branching**: Edit, regenerate, and branch conversation trees | ||
| 48 | * - **Error Handling**: Timeout and server error recovery with user feedback | ||
| 49 | * - **Graceful Stop**: Save partial responses when stopping generation | ||
| 50 | * | ||
| 51 | * **State Management:** | ||
| 52 | * - Global `isLoading` and `currentResponse` for active chat UI | ||
| 53 | * - `chatLoadingStates` Map for per-conversation streaming tracking | ||
| 54 | * - `chatStreamingStates` Map for per-conversation streaming content | ||
| 55 | * - `processingStates` Map for per-conversation processing state (timing/context info) | ||
| 56 | * - Automatic state sync when switching between conversations | ||
| 57 | */ | ||
| 58 | class ChatStore { | ||
| 59 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 60 | // State | ||
| 61 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 62 | |||
| 63 | activeProcessingState = $state<ApiProcessingState | null>(null); | ||
| 64 | currentResponse = $state(''); | ||
| 65 | errorDialogState = $state<{ | ||
| 66 | type: 'timeout' | 'server'; | ||
| 67 | message: string; | ||
| 68 | contextInfo?: { n_prompt_tokens: number; n_ctx: number }; | ||
| 69 | } | null>(null); | ||
| 70 | isLoading = $state(false); | ||
| 71 | chatLoadingStates = new SvelteMap<string, boolean>(); | ||
| 72 | chatStreamingStates = new SvelteMap<string, { response: string; messageId: string }>(); | ||
| 73 | private abortControllers = new SvelteMap<string, AbortController>(); | ||
| 74 | private processingStates = new SvelteMap<string, ApiProcessingState | null>(); | ||
| 75 | private activeConversationId = $state<string | null>(null); | ||
| 76 | private isStreamingActive = $state(false); | ||
| 77 | private isEditModeActive = $state(false); | ||
| 78 | private addFilesHandler: ((files: File[]) => void) | null = $state(null); | ||
| 79 | |||
| 80 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 81 | // Loading State | ||
| 82 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 83 | |||
| 84 | private setChatLoading(convId: string, loading: boolean): void { | ||
| 85 | if (loading) { | ||
| 86 | this.chatLoadingStates.set(convId, true); | ||
| 87 | if (conversationsStore.activeConversation?.id === convId) this.isLoading = true; | ||
| 88 | } else { | ||
| 89 | this.chatLoadingStates.delete(convId); | ||
| 90 | if (conversationsStore.activeConversation?.id === convId) this.isLoading = false; | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | private isChatLoading(convId: string): boolean { | ||
| 95 | return this.chatLoadingStates.get(convId) || false; | ||
| 96 | } | ||
| 97 | |||
| 98 | private setChatStreaming(convId: string, response: string, messageId: string): void { | ||
| 99 | this.chatStreamingStates.set(convId, { response, messageId }); | ||
| 100 | if (conversationsStore.activeConversation?.id === convId) this.currentResponse = response; | ||
| 101 | } | ||
| 102 | |||
| 103 | private clearChatStreaming(convId: string): void { | ||
| 104 | this.chatStreamingStates.delete(convId); | ||
| 105 | if (conversationsStore.activeConversation?.id === convId) this.currentResponse = ''; | ||
| 106 | } | ||
| 107 | |||
| 108 | private getChatStreaming(convId: string): { response: string; messageId: string } | undefined { | ||
| 109 | return this.chatStreamingStates.get(convId); | ||
| 110 | } | ||
| 111 | |||
| 112 | syncLoadingStateForChat(convId: string): void { | ||
| 113 | this.isLoading = this.isChatLoading(convId); | ||
| 114 | const streamingState = this.getChatStreaming(convId); | ||
| 115 | this.currentResponse = streamingState?.response || ''; | ||
| 116 | } | ||
| 117 | |||
| 118 | /** | ||
| 119 | * Clears global UI state without affecting background streaming. | ||
| 120 | * Used when navigating to empty/new chat while other chats stream in background. | ||
| 121 | */ | ||
| 122 | clearUIState(): void { | ||
| 123 | this.isLoading = false; | ||
| 124 | this.currentResponse = ''; | ||
| 125 | } | ||
| 126 | |||
| 127 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 128 | // Processing State | ||
| 129 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 130 | |||
| 131 | /** | ||
| 132 | * Set the active conversation for statistics display | ||
| 133 | */ | ||
| 134 | setActiveProcessingConversation(conversationId: string | null): void { | ||
| 135 | this.activeConversationId = conversationId; | ||
| 136 | |||
| 137 | if (conversationId) { | ||
| 138 | this.activeProcessingState = this.processingStates.get(conversationId) || null; | ||
| 139 | } else { | ||
| 140 | this.activeProcessingState = null; | ||
| 141 | } | ||
| 142 | } | ||
| 143 | |||
| 144 | /** | ||
| 145 | * Get processing state for a specific conversation | ||
| 146 | */ | ||
| 147 | getProcessingState(conversationId: string): ApiProcessingState | null { | ||
| 148 | return this.processingStates.get(conversationId) || null; | ||
| 149 | } | ||
| 150 | |||
| 151 | /** | ||
| 152 | * Clear processing state for a specific conversation | ||
| 153 | */ | ||
| 154 | clearProcessingState(conversationId: string): void { | ||
| 155 | this.processingStates.delete(conversationId); | ||
| 156 | |||
| 157 | if (conversationId === this.activeConversationId) { | ||
| 158 | this.activeProcessingState = null; | ||
| 159 | } | ||
| 160 | } | ||
| 161 | |||
| 162 | /** | ||
| 163 | * Get the current processing state for the active conversation (reactive) | ||
| 164 | * Returns the direct reactive state for UI binding | ||
| 165 | */ | ||
| 166 | getActiveProcessingState(): ApiProcessingState | null { | ||
| 167 | return this.activeProcessingState; | ||
| 168 | } | ||
| 169 | |||
| 170 | /** | ||
| 171 | * Updates processing state with timing data from streaming response | ||
| 172 | */ | ||
| 173 | updateProcessingStateFromTimings( | ||
| 174 | timingData: { | ||
| 175 | prompt_n: number; | ||
| 176 | prompt_ms?: number; | ||
| 177 | predicted_n: number; | ||
| 178 | predicted_per_second: number; | ||
| 179 | cache_n: number; | ||
| 180 | prompt_progress?: ChatMessagePromptProgress; | ||
| 181 | }, | ||
| 182 | conversationId?: string | ||
| 183 | ): void { | ||
| 184 | const processingState = this.parseTimingData(timingData); | ||
| 185 | |||
| 186 | if (processingState === null) { | ||
| 187 | console.warn('Failed to parse timing data - skipping update'); | ||
| 188 | return; | ||
| 189 | } | ||
| 190 | |||
| 191 | const targetId = conversationId || this.activeConversationId; | ||
| 192 | if (targetId) { | ||
| 193 | this.processingStates.set(targetId, processingState); | ||
| 194 | |||
| 195 | if (targetId === this.activeConversationId) { | ||
| 196 | this.activeProcessingState = processingState; | ||
| 197 | } | ||
| 198 | } | ||
| 199 | } | ||
| 200 | |||
| 201 | /** | ||
| 202 | * Get current processing state (sync version for reactive access) | ||
| 203 | */ | ||
| 204 | getCurrentProcessingStateSync(): ApiProcessingState | null { | ||
| 205 | return this.activeProcessingState; | ||
| 206 | } | ||
| 207 | |||
| 208 | /** | ||
| 209 | * Restore processing state from last assistant message timings | ||
| 210 | * Call this when keepStatsVisible is enabled and we need to show last known stats | ||
| 211 | */ | ||
| 212 | restoreProcessingStateFromMessages(messages: DatabaseMessage[], conversationId: string): void { | ||
| 213 | for (let i = messages.length - 1; i >= 0; i--) { | ||
| 214 | const message = messages[i]; | ||
| 215 | if (message.role === 'assistant' && message.timings) { | ||
| 216 | const restoredState = this.parseTimingData({ | ||
| 217 | prompt_n: message.timings.prompt_n || 0, | ||
| 218 | prompt_ms: message.timings.prompt_ms, | ||
| 219 | predicted_n: message.timings.predicted_n || 0, | ||
| 220 | predicted_per_second: | ||
| 221 | message.timings.predicted_n && message.timings.predicted_ms | ||
| 222 | ? (message.timings.predicted_n / message.timings.predicted_ms) * 1000 | ||
| 223 | : 0, | ||
| 224 | cache_n: message.timings.cache_n || 0 | ||
| 225 | }); | ||
| 226 | |||
| 227 | if (restoredState) { | ||
| 228 | this.processingStates.set(conversationId, restoredState); | ||
| 229 | |||
| 230 | if (conversationId === this.activeConversationId) { | ||
| 231 | this.activeProcessingState = restoredState; | ||
| 232 | } | ||
| 233 | |||
| 234 | return; | ||
| 235 | } | ||
| 236 | } | ||
| 237 | } | ||
| 238 | } | ||
| 239 | |||
| 240 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 241 | // Streaming | ||
| 242 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 243 | |||
| 244 | /** | ||
| 245 | * Start streaming session tracking | ||
| 246 | */ | ||
| 247 | startStreaming(): void { | ||
| 248 | this.isStreamingActive = true; | ||
| 249 | } | ||
| 250 | |||
| 251 | /** | ||
| 252 | * Stop streaming session tracking | ||
| 253 | */ | ||
| 254 | stopStreaming(): void { | ||
| 255 | this.isStreamingActive = false; | ||
| 256 | } | ||
| 257 | |||
| 258 | /** | ||
| 259 | * Check if currently in a streaming session | ||
| 260 | */ | ||
| 261 | isStreaming(): boolean { | ||
| 262 | return this.isStreamingActive; | ||
| 263 | } | ||
| 264 | |||
| 265 | private getContextTotal(): number { | ||
| 266 | const activeState = this.getActiveProcessingState(); | ||
| 267 | |||
| 268 | if (activeState && activeState.contextTotal > 0) { | ||
| 269 | return activeState.contextTotal; | ||
| 270 | } | ||
| 271 | |||
| 272 | if (isRouterMode()) { | ||
| 273 | const modelContextSize = selectedModelContextSize(); | ||
| 274 | if (modelContextSize && modelContextSize > 0) { | ||
| 275 | return modelContextSize; | ||
| 276 | } | ||
| 277 | } | ||
| 278 | |||
| 279 | const propsContextSize = contextSize(); | ||
| 280 | if (propsContextSize && propsContextSize > 0) { | ||
| 281 | return propsContextSize; | ||
| 282 | } | ||
| 283 | |||
| 284 | return DEFAULT_CONTEXT; | ||
| 285 | } | ||
| 286 | |||
| 287 | private parseTimingData(timingData: Record<string, unknown>): ApiProcessingState | null { | ||
| 288 | const promptTokens = (timingData.prompt_n as number) || 0; | ||
| 289 | const promptMs = (timingData.prompt_ms as number) || undefined; | ||
| 290 | const predictedTokens = (timingData.predicted_n as number) || 0; | ||
| 291 | const tokensPerSecond = (timingData.predicted_per_second as number) || 0; | ||
| 292 | const cacheTokens = (timingData.cache_n as number) || 0; | ||
| 293 | const promptProgress = timingData.prompt_progress as | ||
| 294 | | { | ||
| 295 | total: number; | ||
| 296 | cache: number; | ||
| 297 | processed: number; | ||
| 298 | time_ms: number; | ||
| 299 | } | ||
| 300 | | undefined; | ||
| 301 | |||
| 302 | const contextTotal = this.getContextTotal(); | ||
| 303 | const currentConfig = config(); | ||
| 304 | const outputTokensMax = currentConfig.max_tokens || -1; | ||
| 305 | |||
| 306 | // Note: for timings data, the n_prompt does NOT include cache tokens | ||
| 307 | const contextUsed = promptTokens + cacheTokens + predictedTokens; | ||
| 308 | const outputTokensUsed = predictedTokens; | ||
| 309 | |||
| 310 | // Note: for prompt progress, the "processed" DOES include cache tokens | ||
| 311 | // we need to exclude them to get the real prompt tokens processed count | ||
| 312 | const progressCache = promptProgress?.cache || 0; | ||
| 313 | const progressActualDone = (promptProgress?.processed ?? 0) - progressCache; | ||
| 314 | const progressActualTotal = (promptProgress?.total ?? 0) - progressCache; | ||
| 315 | const progressPercent = promptProgress | ||
| 316 | ? Math.round((progressActualDone / progressActualTotal) * 100) | ||
| 317 | : undefined; | ||
| 318 | |||
| 319 | return { | ||
| 320 | status: predictedTokens > 0 ? 'generating' : promptProgress ? 'preparing' : 'idle', | ||
| 321 | tokensDecoded: predictedTokens, | ||
| 322 | tokensRemaining: outputTokensMax - predictedTokens, | ||
| 323 | contextUsed, | ||
| 324 | contextTotal, | ||
| 325 | outputTokensUsed, | ||
| 326 | outputTokensMax, | ||
| 327 | hasNextToken: predictedTokens > 0, | ||
| 328 | tokensPerSecond, | ||
| 329 | temperature: currentConfig.temperature ?? 0.8, | ||
| 330 | topP: currentConfig.top_p ?? 0.95, | ||
| 331 | speculative: false, | ||
| 332 | progressPercent, | ||
| 333 | promptProgress, | ||
| 334 | promptTokens, | ||
| 335 | promptMs, | ||
| 336 | cacheTokens | ||
| 337 | }; | ||
| 338 | } | ||
| 339 | |||
| 340 | /** | ||
| 341 | * Gets the model used in a conversation based on the latest assistant message. | ||
| 342 | * Returns the model from the most recent assistant message that has a model field set. | ||
| 343 | * | ||
| 344 | * @param messages - Array of messages to search through | ||
| 345 | * @returns The model name or null if no model found | ||
| 346 | */ | ||
| 347 | getConversationModel(messages: DatabaseMessage[]): string | null { | ||
| 348 | // Search backwards through messages to find most recent assistant message with model | ||
| 349 | for (let i = messages.length - 1; i >= 0; i--) { | ||
| 350 | const message = messages[i]; | ||
| 351 | if (message.role === 'assistant' && message.model) { | ||
| 352 | return message.model; | ||
| 353 | } | ||
| 354 | } | ||
| 355 | return null; | ||
| 356 | } | ||
| 357 | |||
| 358 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 359 | // Error Handling | ||
| 360 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 361 | |||
| 362 | private isAbortError(error: unknown): boolean { | ||
| 363 | return error instanceof Error && (error.name === 'AbortError' || error instanceof DOMException); | ||
| 364 | } | ||
| 365 | |||
| 366 | private showErrorDialog( | ||
| 367 | type: 'timeout' | 'server', | ||
| 368 | message: string, | ||
| 369 | contextInfo?: { n_prompt_tokens: number; n_ctx: number } | ||
| 370 | ): void { | ||
| 371 | this.errorDialogState = { type, message, contextInfo }; | ||
| 372 | } | ||
| 373 | |||
| 374 | dismissErrorDialog(): void { | ||
| 375 | this.errorDialogState = null; | ||
| 376 | } | ||
| 377 | |||
| 378 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 379 | // Message Operations | ||
| 380 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 381 | |||
| 382 | /** | ||
| 383 | * Finds a message by ID and optionally validates its role. | ||
| 384 | * Returns message and index, or null if not found or role doesn't match. | ||
| 385 | */ | ||
| 386 | private getMessageByIdWithRole( | ||
| 387 | messageId: string, | ||
| 388 | expectedRole?: ChatRole | ||
| 389 | ): { message: DatabaseMessage; index: number } | null { | ||
| 390 | const index = conversationsStore.findMessageIndex(messageId); | ||
| 391 | if (index === -1) return null; | ||
| 392 | |||
| 393 | const message = conversationsStore.activeMessages[index]; | ||
| 394 | if (expectedRole && message.role !== expectedRole) return null; | ||
| 395 | |||
| 396 | return { message, index }; | ||
| 397 | } | ||
| 398 | |||
| 399 | async addMessage( | ||
| 400 | role: ChatRole, | ||
| 401 | content: string, | ||
| 402 | type: ChatMessageType = 'text', | ||
| 403 | parent: string = '-1', | ||
| 404 | extras?: DatabaseMessageExtra[] | ||
| 405 | ): Promise<DatabaseMessage | null> { | ||
| 406 | const activeConv = conversationsStore.activeConversation; | ||
| 407 | if (!activeConv) { | ||
| 408 | console.error('No active conversation when trying to add message'); | ||
| 409 | return null; | ||
| 410 | } | ||
| 411 | |||
| 412 | try { | ||
| 413 | let parentId: string | null = null; | ||
| 414 | |||
| 415 | if (parent === '-1') { | ||
| 416 | const activeMessages = conversationsStore.activeMessages; | ||
| 417 | if (activeMessages.length > 0) { | ||
| 418 | parentId = activeMessages[activeMessages.length - 1].id; | ||
| 419 | } else { | ||
| 420 | const allMessages = await conversationsStore.getConversationMessages(activeConv.id); | ||
| 421 | const rootMessage = allMessages.find((m) => m.parent === null && m.type === 'root'); | ||
| 422 | if (!rootMessage) { | ||
| 423 | parentId = await DatabaseService.createRootMessage(activeConv.id); | ||
| 424 | } else { | ||
| 425 | parentId = rootMessage.id; | ||
| 426 | } | ||
| 427 | } | ||
| 428 | } else { | ||
| 429 | parentId = parent; | ||
| 430 | } | ||
| 431 | |||
| 432 | const message = await DatabaseService.createMessageBranch( | ||
| 433 | { | ||
| 434 | convId: activeConv.id, | ||
| 435 | role, | ||
| 436 | content, | ||
| 437 | type, | ||
| 438 | timestamp: Date.now(), | ||
| 439 | thinking: '', | ||
| 440 | toolCalls: '', | ||
| 441 | children: [], | ||
| 442 | extra: extras | ||
| 443 | }, | ||
| 444 | parentId | ||
| 445 | ); | ||
| 446 | |||
| 447 | conversationsStore.addMessageToActive(message); | ||
| 448 | await conversationsStore.updateCurrentNode(message.id); | ||
| 449 | conversationsStore.updateConversationTimestamp(); | ||
| 450 | |||
| 451 | return message; | ||
| 452 | } catch (error) { | ||
| 453 | console.error('Failed to add message:', error); | ||
| 454 | return null; | ||
| 455 | } | ||
| 456 | } | ||
| 457 | |||
| 458 | private async createAssistantMessage(parentId?: string): Promise<DatabaseMessage | null> { | ||
| 459 | const activeConv = conversationsStore.activeConversation; | ||
| 460 | if (!activeConv) return null; | ||
| 461 | |||
| 462 | return await DatabaseService.createMessageBranch( | ||
| 463 | { | ||
| 464 | convId: activeConv.id, | ||
| 465 | type: 'text', | ||
| 466 | role: 'assistant', | ||
| 467 | content: '', | ||
| 468 | timestamp: Date.now(), | ||
| 469 | thinking: '', | ||
| 470 | toolCalls: '', | ||
| 471 | children: [], | ||
| 472 | model: null | ||
| 473 | }, | ||
| 474 | parentId || null | ||
| 475 | ); | ||
| 476 | } | ||
| 477 | |||
| 478 | private async streamChatCompletion( | ||
| 479 | allMessages: DatabaseMessage[], | ||
| 480 | assistantMessage: DatabaseMessage, | ||
| 481 | onComplete?: (content: string) => Promise<void>, | ||
| 482 | onError?: (error: Error) => void, | ||
| 483 | modelOverride?: string | null | ||
| 484 | ): Promise<void> { | ||
| 485 | // Ensure model props are cached before streaming (for correct n_ctx in processing info) | ||
| 486 | if (isRouterMode()) { | ||
| 487 | const modelName = modelOverride || selectedModelName(); | ||
| 488 | if (modelName && !modelsStore.getModelProps(modelName)) { | ||
| 489 | await modelsStore.fetchModelProps(modelName); | ||
| 490 | } | ||
| 491 | } | ||
| 492 | |||
| 493 | let streamedContent = ''; | ||
| 494 | let streamedReasoningContent = ''; | ||
| 495 | let streamedToolCallContent = ''; | ||
| 496 | let resolvedModel: string | null = null; | ||
| 497 | let modelPersisted = false; | ||
| 498 | |||
| 499 | const recordModel = (modelName: string | null | undefined, persistImmediately = true): void => { | ||
| 500 | if (!modelName) return; | ||
| 501 | const normalizedModel = normalizeModelName(modelName); | ||
| 502 | if (!normalizedModel || normalizedModel === resolvedModel) return; | ||
| 503 | resolvedModel = normalizedModel; | ||
| 504 | const messageIndex = conversationsStore.findMessageIndex(assistantMessage.id); | ||
| 505 | conversationsStore.updateMessageAtIndex(messageIndex, { model: normalizedModel }); | ||
| 506 | if (persistImmediately && !modelPersisted) { | ||
| 507 | modelPersisted = true; | ||
| 508 | DatabaseService.updateMessage(assistantMessage.id, { model: normalizedModel }).catch(() => { | ||
| 509 | modelPersisted = false; | ||
| 510 | resolvedModel = null; | ||
| 511 | }); | ||
| 512 | } | ||
| 513 | }; | ||
| 514 | |||
| 515 | this.startStreaming(); | ||
| 516 | this.setActiveProcessingConversation(assistantMessage.convId); | ||
| 517 | |||
| 518 | const abortController = this.getOrCreateAbortController(assistantMessage.convId); | ||
| 519 | |||
| 520 | await ChatService.sendMessage( | ||
| 521 | allMessages, | ||
| 522 | { | ||
| 523 | ...this.getApiOptions(), | ||
| 524 | ...(modelOverride ? { model: modelOverride } : {}), | ||
| 525 | onChunk: (chunk: string) => { | ||
| 526 | streamedContent += chunk; | ||
| 527 | this.setChatStreaming(assistantMessage.convId, streamedContent, assistantMessage.id); | ||
| 528 | const idx = conversationsStore.findMessageIndex(assistantMessage.id); | ||
| 529 | conversationsStore.updateMessageAtIndex(idx, { content: streamedContent }); | ||
| 530 | }, | ||
| 531 | onReasoningChunk: (reasoningChunk: string) => { | ||
| 532 | streamedReasoningContent += reasoningChunk; | ||
| 533 | const idx = conversationsStore.findMessageIndex(assistantMessage.id); | ||
| 534 | conversationsStore.updateMessageAtIndex(idx, { thinking: streamedReasoningContent }); | ||
| 535 | }, | ||
| 536 | onToolCallChunk: (toolCallChunk: string) => { | ||
| 537 | const chunk = toolCallChunk.trim(); | ||
| 538 | if (!chunk) return; | ||
| 539 | streamedToolCallContent = chunk; | ||
| 540 | const idx = conversationsStore.findMessageIndex(assistantMessage.id); | ||
| 541 | conversationsStore.updateMessageAtIndex(idx, { toolCalls: streamedToolCallContent }); | ||
| 542 | }, | ||
| 543 | onModel: (modelName: string) => recordModel(modelName), | ||
| 544 | onTimings: (timings?: ChatMessageTimings, promptProgress?: ChatMessagePromptProgress) => { | ||
| 545 | const tokensPerSecond = | ||
| 546 | timings?.predicted_ms && timings?.predicted_n | ||
| 547 | ? (timings.predicted_n / timings.predicted_ms) * 1000 | ||
| 548 | : 0; | ||
| 549 | this.updateProcessingStateFromTimings( | ||
| 550 | { | ||
| 551 | prompt_n: timings?.prompt_n || 0, | ||
| 552 | prompt_ms: timings?.prompt_ms, | ||
| 553 | predicted_n: timings?.predicted_n || 0, | ||
| 554 | predicted_per_second: tokensPerSecond, | ||
| 555 | cache_n: timings?.cache_n || 0, | ||
| 556 | prompt_progress: promptProgress | ||
| 557 | }, | ||
| 558 | assistantMessage.convId | ||
| 559 | ); | ||
| 560 | }, | ||
| 561 | onComplete: async ( | ||
| 562 | finalContent?: string, | ||
| 563 | reasoningContent?: string, | ||
| 564 | timings?: ChatMessageTimings, | ||
| 565 | toolCallContent?: string | ||
| 566 | ) => { | ||
| 567 | this.stopStreaming(); | ||
| 568 | |||
| 569 | const updateData: Record<string, unknown> = { | ||
| 570 | content: finalContent || streamedContent, | ||
| 571 | thinking: reasoningContent || streamedReasoningContent, | ||
| 572 | toolCalls: toolCallContent || streamedToolCallContent, | ||
| 573 | timings | ||
| 574 | }; | ||
| 575 | if (resolvedModel && !modelPersisted) { | ||
| 576 | updateData.model = resolvedModel; | ||
| 577 | } | ||
| 578 | await DatabaseService.updateMessage(assistantMessage.id, updateData); | ||
| 579 | |||
| 580 | const idx = conversationsStore.findMessageIndex(assistantMessage.id); | ||
| 581 | const uiUpdate: Partial<DatabaseMessage> = { | ||
| 582 | content: updateData.content as string, | ||
| 583 | toolCalls: updateData.toolCalls as string | ||
| 584 | }; | ||
| 585 | if (timings) uiUpdate.timings = timings; | ||
| 586 | if (resolvedModel) uiUpdate.model = resolvedModel; | ||
| 587 | |||
| 588 | conversationsStore.updateMessageAtIndex(idx, uiUpdate); | ||
| 589 | await conversationsStore.updateCurrentNode(assistantMessage.id); | ||
| 590 | |||
| 591 | if (onComplete) await onComplete(streamedContent); | ||
| 592 | this.setChatLoading(assistantMessage.convId, false); | ||
| 593 | this.clearChatStreaming(assistantMessage.convId); | ||
| 594 | this.clearProcessingState(assistantMessage.convId); | ||
| 595 | |||
| 596 | if (isRouterMode()) { | ||
| 597 | modelsStore.fetchRouterModels().catch(console.error); | ||
| 598 | } | ||
| 599 | }, | ||
| 600 | onError: (error: Error) => { | ||
| 601 | this.stopStreaming(); | ||
| 602 | |||
| 603 | if (this.isAbortError(error)) { | ||
| 604 | this.setChatLoading(assistantMessage.convId, false); | ||
| 605 | this.clearChatStreaming(assistantMessage.convId); | ||
| 606 | this.clearProcessingState(assistantMessage.convId); | ||
| 607 | |||
| 608 | return; | ||
| 609 | } | ||
| 610 | |||
| 611 | console.error('Streaming error:', error); | ||
| 612 | |||
| 613 | this.setChatLoading(assistantMessage.convId, false); | ||
| 614 | this.clearChatStreaming(assistantMessage.convId); | ||
| 615 | this.clearProcessingState(assistantMessage.convId); | ||
| 616 | |||
| 617 | const idx = conversationsStore.findMessageIndex(assistantMessage.id); | ||
| 618 | |||
| 619 | if (idx !== -1) { | ||
| 620 | const failedMessage = conversationsStore.removeMessageAtIndex(idx); | ||
| 621 | if (failedMessage) DatabaseService.deleteMessage(failedMessage.id).catch(console.error); | ||
| 622 | } | ||
| 623 | |||
| 624 | const contextInfo = ( | ||
| 625 | error as Error & { contextInfo?: { n_prompt_tokens: number; n_ctx: number } } | ||
| 626 | ).contextInfo; | ||
| 627 | |||
| 628 | this.showErrorDialog( | ||
| 629 | error.name === 'TimeoutError' ? 'timeout' : 'server', | ||
| 630 | error.message, | ||
| 631 | contextInfo | ||
| 632 | ); | ||
| 633 | |||
| 634 | if (onError) onError(error); | ||
| 635 | } | ||
| 636 | }, | ||
| 637 | assistantMessage.convId, | ||
| 638 | abortController.signal | ||
| 639 | ); | ||
| 640 | } | ||
| 641 | |||
| 642 | async sendMessage(content: string, extras?: DatabaseMessageExtra[]): Promise<void> { | ||
| 643 | if (!content.trim() && (!extras || extras.length === 0)) return; | ||
| 644 | const activeConv = conversationsStore.activeConversation; | ||
| 645 | if (activeConv && this.isChatLoading(activeConv.id)) return; | ||
| 646 | |||
| 647 | let isNewConversation = false; | ||
| 648 | if (!activeConv) { | ||
| 649 | await conversationsStore.createConversation(); | ||
| 650 | isNewConversation = true; | ||
| 651 | } | ||
| 652 | const currentConv = conversationsStore.activeConversation; | ||
| 653 | if (!currentConv) return; | ||
| 654 | |||
| 655 | this.errorDialogState = null; | ||
| 656 | this.setChatLoading(currentConv.id, true); | ||
| 657 | this.clearChatStreaming(currentConv.id); | ||
| 658 | |||
| 659 | try { | ||
| 660 | if (isNewConversation) { | ||
| 661 | const rootId = await DatabaseService.createRootMessage(currentConv.id); | ||
| 662 | const currentConfig = config(); | ||
| 663 | const systemPrompt = currentConfig.systemMessage?.toString().trim(); | ||
| 664 | |||
| 665 | if (systemPrompt) { | ||
| 666 | const systemMessage = await DatabaseService.createSystemMessage( | ||
| 667 | currentConv.id, | ||
| 668 | systemPrompt, | ||
| 669 | rootId | ||
| 670 | ); | ||
| 671 | |||
| 672 | conversationsStore.addMessageToActive(systemMessage); | ||
| 673 | } | ||
| 674 | } | ||
| 675 | |||
| 676 | const userMessage = await this.addMessage('user', content, 'text', '-1', extras); | ||
| 677 | if (!userMessage) throw new Error('Failed to add user message'); | ||
| 678 | if (isNewConversation && content) | ||
| 679 | await conversationsStore.updateConversationName(currentConv.id, content.trim()); | ||
| 680 | |||
| 681 | const assistantMessage = await this.createAssistantMessage(userMessage.id); | ||
| 682 | |||
| 683 | if (!assistantMessage) throw new Error('Failed to create assistant message'); | ||
| 684 | |||
| 685 | conversationsStore.addMessageToActive(assistantMessage); | ||
| 686 | await this.streamChatCompletion( | ||
| 687 | conversationsStore.activeMessages.slice(0, -1), | ||
| 688 | assistantMessage | ||
| 689 | ); | ||
| 690 | } catch (error) { | ||
| 691 | if (this.isAbortError(error)) { | ||
| 692 | this.setChatLoading(currentConv.id, false); | ||
| 693 | return; | ||
| 694 | } | ||
| 695 | console.error('Failed to send message:', error); | ||
| 696 | this.setChatLoading(currentConv.id, false); | ||
| 697 | if (!this.errorDialogState) { | ||
| 698 | const dialogType = | ||
| 699 | error instanceof Error && error.name === 'TimeoutError' ? 'timeout' : 'server'; | ||
| 700 | const contextInfo = ( | ||
| 701 | error as Error & { contextInfo?: { n_prompt_tokens: number; n_ctx: number } } | ||
| 702 | ).contextInfo; | ||
| 703 | |||
| 704 | this.showErrorDialog( | ||
| 705 | dialogType, | ||
| 706 | error instanceof Error ? error.message : 'Unknown error', | ||
| 707 | contextInfo | ||
| 708 | ); | ||
| 709 | } | ||
| 710 | } | ||
| 711 | } | ||
| 712 | |||
| 713 | async stopGeneration(): Promise<void> { | ||
| 714 | const activeConv = conversationsStore.activeConversation; | ||
| 715 | |||
| 716 | if (!activeConv) return; | ||
| 717 | |||
| 718 | await this.stopGenerationForChat(activeConv.id); | ||
| 719 | } | ||
| 720 | |||
| 721 | async stopGenerationForChat(convId: string): Promise<void> { | ||
| 722 | await this.savePartialResponseIfNeeded(convId); | ||
| 723 | |||
| 724 | this.stopStreaming(); | ||
| 725 | this.abortRequest(convId); | ||
| 726 | this.setChatLoading(convId, false); | ||
| 727 | this.clearChatStreaming(convId); | ||
| 728 | this.clearProcessingState(convId); | ||
| 729 | } | ||
| 730 | |||
| 731 | /** | ||
| 732 | * Gets or creates an AbortController for a conversation | ||
| 733 | */ | ||
| 734 | private getOrCreateAbortController(convId: string): AbortController { | ||
| 735 | let controller = this.abortControllers.get(convId); | ||
| 736 | if (!controller || controller.signal.aborted) { | ||
| 737 | controller = new AbortController(); | ||
| 738 | this.abortControllers.set(convId, controller); | ||
| 739 | } | ||
| 740 | return controller; | ||
| 741 | } | ||
| 742 | |||
| 743 | /** | ||
| 744 | * Aborts any ongoing request for a conversation | ||
| 745 | */ | ||
| 746 | private abortRequest(convId?: string): void { | ||
| 747 | if (convId) { | ||
| 748 | const controller = this.abortControllers.get(convId); | ||
| 749 | if (controller) { | ||
| 750 | controller.abort(); | ||
| 751 | this.abortControllers.delete(convId); | ||
| 752 | } | ||
| 753 | } else { | ||
| 754 | for (const controller of this.abortControllers.values()) { | ||
| 755 | controller.abort(); | ||
| 756 | } | ||
| 757 | this.abortControllers.clear(); | ||
| 758 | } | ||
| 759 | } | ||
| 760 | |||
| 761 | private async savePartialResponseIfNeeded(convId?: string): Promise<void> { | ||
| 762 | const conversationId = convId || conversationsStore.activeConversation?.id; | ||
| 763 | |||
| 764 | if (!conversationId) return; | ||
| 765 | |||
| 766 | const streamingState = this.chatStreamingStates.get(conversationId); | ||
| 767 | |||
| 768 | if (!streamingState || !streamingState.response.trim()) return; | ||
| 769 | |||
| 770 | const messages = | ||
| 771 | conversationId === conversationsStore.activeConversation?.id | ||
| 772 | ? conversationsStore.activeMessages | ||
| 773 | : await conversationsStore.getConversationMessages(conversationId); | ||
| 774 | |||
| 775 | if (!messages.length) return; | ||
| 776 | |||
| 777 | const lastMessage = messages[messages.length - 1]; | ||
| 778 | |||
| 779 | if (lastMessage?.role === 'assistant') { | ||
| 780 | try { | ||
| 781 | const updateData: { content: string; thinking?: string; timings?: ChatMessageTimings } = { | ||
| 782 | content: streamingState.response | ||
| 783 | }; | ||
| 784 | if (lastMessage.thinking?.trim()) updateData.thinking = lastMessage.thinking; | ||
| 785 | const lastKnownState = this.getProcessingState(conversationId); | ||
| 786 | if (lastKnownState) { | ||
| 787 | updateData.timings = { | ||
| 788 | prompt_n: lastKnownState.promptTokens || 0, | ||
| 789 | prompt_ms: lastKnownState.promptMs, | ||
| 790 | predicted_n: lastKnownState.tokensDecoded || 0, | ||
| 791 | cache_n: lastKnownState.cacheTokens || 0, | ||
| 792 | predicted_ms: | ||
| 793 | lastKnownState.tokensPerSecond && lastKnownState.tokensDecoded | ||
| 794 | ? (lastKnownState.tokensDecoded / lastKnownState.tokensPerSecond) * 1000 | ||
| 795 | : undefined | ||
| 796 | }; | ||
| 797 | } | ||
| 798 | |||
| 799 | await DatabaseService.updateMessage(lastMessage.id, updateData); | ||
| 800 | |||
| 801 | lastMessage.content = this.currentResponse; | ||
| 802 | |||
| 803 | if (updateData.thinking) lastMessage.thinking = updateData.thinking; | ||
| 804 | |||
| 805 | if (updateData.timings) lastMessage.timings = updateData.timings; | ||
| 806 | } catch (error) { | ||
| 807 | lastMessage.content = this.currentResponse; | ||
| 808 | console.error('Failed to save partial response:', error); | ||
| 809 | } | ||
| 810 | } | ||
| 811 | } | ||
| 812 | |||
| 813 | async updateMessage(messageId: string, newContent: string): Promise<void> { | ||
| 814 | const activeConv = conversationsStore.activeConversation; | ||
| 815 | if (!activeConv) return; | ||
| 816 | if (this.isLoading) this.stopGeneration(); | ||
| 817 | |||
| 818 | const result = this.getMessageByIdWithRole(messageId, 'user'); | ||
| 819 | if (!result) return; | ||
| 820 | const { message: messageToUpdate, index: messageIndex } = result; | ||
| 821 | const originalContent = messageToUpdate.content; | ||
| 822 | |||
| 823 | try { | ||
| 824 | const allMessages = await conversationsStore.getConversationMessages(activeConv.id); | ||
| 825 | const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null); | ||
| 826 | const isFirstUserMessage = rootMessage && messageToUpdate.parent === rootMessage.id; | ||
| 827 | |||
| 828 | conversationsStore.updateMessageAtIndex(messageIndex, { content: newContent }); | ||
| 829 | await DatabaseService.updateMessage(messageId, { content: newContent }); | ||
| 830 | |||
| 831 | if (isFirstUserMessage && newContent.trim()) { | ||
| 832 | await conversationsStore.updateConversationTitleWithConfirmation( | ||
| 833 | activeConv.id, | ||
| 834 | newContent.trim(), | ||
| 835 | conversationsStore.titleUpdateConfirmationCallback | ||
| 836 | ); | ||
| 837 | } | ||
| 838 | |||
| 839 | const messagesToRemove = conversationsStore.activeMessages.slice(messageIndex + 1); | ||
| 840 | |||
| 841 | for (const message of messagesToRemove) await DatabaseService.deleteMessage(message.id); | ||
| 842 | |||
| 843 | conversationsStore.sliceActiveMessages(messageIndex + 1); | ||
| 844 | conversationsStore.updateConversationTimestamp(); | ||
| 845 | |||
| 846 | this.setChatLoading(activeConv.id, true); | ||
| 847 | this.clearChatStreaming(activeConv.id); | ||
| 848 | |||
| 849 | const assistantMessage = await this.createAssistantMessage(); | ||
| 850 | |||
| 851 | if (!assistantMessage) throw new Error('Failed to create assistant message'); | ||
| 852 | |||
| 853 | conversationsStore.addMessageToActive(assistantMessage); | ||
| 854 | |||
| 855 | await conversationsStore.updateCurrentNode(assistantMessage.id); | ||
| 856 | await this.streamChatCompletion( | ||
| 857 | conversationsStore.activeMessages.slice(0, -1), | ||
| 858 | assistantMessage, | ||
| 859 | undefined, | ||
| 860 | () => { | ||
| 861 | conversationsStore.updateMessageAtIndex(conversationsStore.findMessageIndex(messageId), { | ||
| 862 | content: originalContent | ||
| 863 | }); | ||
| 864 | } | ||
| 865 | ); | ||
| 866 | } catch (error) { | ||
| 867 | if (!this.isAbortError(error)) console.error('Failed to update message:', error); | ||
| 868 | } | ||
| 869 | } | ||
| 870 | |||
| 871 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 872 | // Regeneration | ||
| 873 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 874 | |||
| 875 | async regenerateMessage(messageId: string): Promise<void> { | ||
| 876 | const activeConv = conversationsStore.activeConversation; | ||
| 877 | if (!activeConv || this.isLoading) return; | ||
| 878 | |||
| 879 | const result = this.getMessageByIdWithRole(messageId, 'assistant'); | ||
| 880 | if (!result) return; | ||
| 881 | const { index: messageIndex } = result; | ||
| 882 | |||
| 883 | try { | ||
| 884 | const messagesToRemove = conversationsStore.activeMessages.slice(messageIndex); | ||
| 885 | for (const message of messagesToRemove) await DatabaseService.deleteMessage(message.id); | ||
| 886 | conversationsStore.sliceActiveMessages(messageIndex); | ||
| 887 | conversationsStore.updateConversationTimestamp(); | ||
| 888 | |||
| 889 | this.setChatLoading(activeConv.id, true); | ||
| 890 | this.clearChatStreaming(activeConv.id); | ||
| 891 | |||
| 892 | const parentMessageId = | ||
| 893 | conversationsStore.activeMessages.length > 0 | ||
| 894 | ? conversationsStore.activeMessages[conversationsStore.activeMessages.length - 1].id | ||
| 895 | : undefined; | ||
| 896 | const assistantMessage = await this.createAssistantMessage(parentMessageId); | ||
| 897 | if (!assistantMessage) throw new Error('Failed to create assistant message'); | ||
| 898 | conversationsStore.addMessageToActive(assistantMessage); | ||
| 899 | await this.streamChatCompletion( | ||
| 900 | conversationsStore.activeMessages.slice(0, -1), | ||
| 901 | assistantMessage | ||
| 902 | ); | ||
| 903 | } catch (error) { | ||
| 904 | if (!this.isAbortError(error)) console.error('Failed to regenerate message:', error); | ||
| 905 | this.setChatLoading(activeConv?.id || '', false); | ||
| 906 | } | ||
| 907 | } | ||
| 908 | |||
| 909 | async getDeletionInfo(messageId: string): Promise<{ | ||
| 910 | totalCount: number; | ||
| 911 | userMessages: number; | ||
| 912 | assistantMessages: number; | ||
| 913 | messageTypes: string[]; | ||
| 914 | }> { | ||
| 915 | const activeConv = conversationsStore.activeConversation; | ||
| 916 | if (!activeConv) | ||
| 917 | return { totalCount: 0, userMessages: 0, assistantMessages: 0, messageTypes: [] }; | ||
| 918 | const allMessages = await conversationsStore.getConversationMessages(activeConv.id); | ||
| 919 | const descendants = findDescendantMessages(allMessages, messageId); | ||
| 920 | const allToDelete = [messageId, ...descendants]; | ||
| 921 | const messagesToDelete = allMessages.filter((m) => allToDelete.includes(m.id)); | ||
| 922 | let userMessages = 0, | ||
| 923 | assistantMessages = 0; | ||
| 924 | const messageTypes: string[] = []; | ||
| 925 | for (const msg of messagesToDelete) { | ||
| 926 | if (msg.role === 'user') { | ||
| 927 | userMessages++; | ||
| 928 | if (!messageTypes.includes('user message')) messageTypes.push('user message'); | ||
| 929 | } else if (msg.role === 'assistant') { | ||
| 930 | assistantMessages++; | ||
| 931 | if (!messageTypes.includes('assistant response')) messageTypes.push('assistant response'); | ||
| 932 | } | ||
| 933 | } | ||
| 934 | return { totalCount: allToDelete.length, userMessages, assistantMessages, messageTypes }; | ||
| 935 | } | ||
| 936 | |||
| 937 | async deleteMessage(messageId: string): Promise<void> { | ||
| 938 | const activeConv = conversationsStore.activeConversation; | ||
| 939 | if (!activeConv) return; | ||
| 940 | try { | ||
| 941 | const allMessages = await conversationsStore.getConversationMessages(activeConv.id); | ||
| 942 | const messageToDelete = allMessages.find((m) => m.id === messageId); | ||
| 943 | if (!messageToDelete) return; | ||
| 944 | |||
| 945 | const currentPath = filterByLeafNodeId(allMessages, activeConv.currNode || '', false); | ||
| 946 | const isInCurrentPath = currentPath.some((m) => m.id === messageId); | ||
| 947 | |||
| 948 | if (isInCurrentPath && messageToDelete.parent) { | ||
| 949 | const siblings = allMessages.filter( | ||
| 950 | (m) => m.parent === messageToDelete.parent && m.id !== messageId | ||
| 951 | ); | ||
| 952 | |||
| 953 | if (siblings.length > 0) { | ||
| 954 | const latestSibling = siblings.reduce((latest, sibling) => | ||
| 955 | sibling.timestamp > latest.timestamp ? sibling : latest | ||
| 956 | ); | ||
| 957 | await conversationsStore.updateCurrentNode(findLeafNode(allMessages, latestSibling.id)); | ||
| 958 | } else if (messageToDelete.parent) { | ||
| 959 | await conversationsStore.updateCurrentNode( | ||
| 960 | findLeafNode(allMessages, messageToDelete.parent) | ||
| 961 | ); | ||
| 962 | } | ||
| 963 | } | ||
| 964 | await DatabaseService.deleteMessageCascading(activeConv.id, messageId); | ||
| 965 | await conversationsStore.refreshActiveMessages(); | ||
| 966 | |||
| 967 | conversationsStore.updateConversationTimestamp(); | ||
| 968 | } catch (error) { | ||
| 969 | console.error('Failed to delete message:', error); | ||
| 970 | } | ||
| 971 | } | ||
| 972 | |||
| 973 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 974 | // Editing | ||
| 975 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 976 | |||
| 977 | clearEditMode(): void { | ||
| 978 | this.isEditModeActive = false; | ||
| 979 | this.addFilesHandler = null; | ||
| 980 | } | ||
| 981 | |||
| 982 | async continueAssistantMessage(messageId: string): Promise<void> { | ||
| 983 | const activeConv = conversationsStore.activeConversation; | ||
| 984 | if (!activeConv || this.isLoading) return; | ||
| 985 | |||
| 986 | const result = this.getMessageByIdWithRole(messageId, 'assistant'); | ||
| 987 | if (!result) return; | ||
| 988 | const { message: msg, index: idx } = result; | ||
| 989 | |||
| 990 | if (this.isChatLoading(activeConv.id)) return; | ||
| 991 | |||
| 992 | try { | ||
| 993 | this.errorDialogState = null; | ||
| 994 | this.setChatLoading(activeConv.id, true); | ||
| 995 | this.clearChatStreaming(activeConv.id); | ||
| 996 | |||
| 997 | const allMessages = await conversationsStore.getConversationMessages(activeConv.id); | ||
| 998 | const dbMessage = allMessages.find((m) => m.id === messageId); | ||
| 999 | |||
| 1000 | if (!dbMessage) { | ||
| 1001 | this.setChatLoading(activeConv.id, false); | ||
| 1002 | |||
| 1003 | return; | ||
| 1004 | } | ||
| 1005 | |||
| 1006 | const originalContent = dbMessage.content; | ||
| 1007 | const originalThinking = dbMessage.thinking || ''; | ||
| 1008 | |||
| 1009 | const conversationContext = conversationsStore.activeMessages.slice(0, idx); | ||
| 1010 | const contextWithContinue = [ | ||
| 1011 | ...conversationContext, | ||
| 1012 | { role: 'assistant' as const, content: originalContent } | ||
| 1013 | ]; | ||
| 1014 | |||
| 1015 | let appendedContent = '', | ||
| 1016 | appendedThinking = '', | ||
| 1017 | hasReceivedContent = false; | ||
| 1018 | |||
| 1019 | const abortController = this.getOrCreateAbortController(msg.convId); | ||
| 1020 | |||
| 1021 | await ChatService.sendMessage( | ||
| 1022 | contextWithContinue, | ||
| 1023 | { | ||
| 1024 | ...this.getApiOptions(), | ||
| 1025 | |||
| 1026 | onChunk: (chunk: string) => { | ||
| 1027 | hasReceivedContent = true; | ||
| 1028 | appendedContent += chunk; | ||
| 1029 | const fullContent = originalContent + appendedContent; | ||
| 1030 | this.setChatStreaming(msg.convId, fullContent, msg.id); | ||
| 1031 | conversationsStore.updateMessageAtIndex(idx, { content: fullContent }); | ||
| 1032 | }, | ||
| 1033 | |||
| 1034 | onReasoningChunk: (reasoningChunk: string) => { | ||
| 1035 | hasReceivedContent = true; | ||
| 1036 | appendedThinking += reasoningChunk; | ||
| 1037 | conversationsStore.updateMessageAtIndex(idx, { | ||
| 1038 | thinking: originalThinking + appendedThinking | ||
| 1039 | }); | ||
| 1040 | }, | ||
| 1041 | |||
| 1042 | onTimings: (timings?: ChatMessageTimings, promptProgress?: ChatMessagePromptProgress) => { | ||
| 1043 | const tokensPerSecond = | ||
| 1044 | timings?.predicted_ms && timings?.predicted_n | ||
| 1045 | ? (timings.predicted_n / timings.predicted_ms) * 1000 | ||
| 1046 | : 0; | ||
| 1047 | this.updateProcessingStateFromTimings( | ||
| 1048 | { | ||
| 1049 | prompt_n: timings?.prompt_n || 0, | ||
| 1050 | prompt_ms: timings?.prompt_ms, | ||
| 1051 | predicted_n: timings?.predicted_n || 0, | ||
| 1052 | predicted_per_second: tokensPerSecond, | ||
| 1053 | cache_n: timings?.cache_n || 0, | ||
| 1054 | prompt_progress: promptProgress | ||
| 1055 | }, | ||
| 1056 | msg.convId | ||
| 1057 | ); | ||
| 1058 | }, | ||
| 1059 | |||
| 1060 | onComplete: async ( | ||
| 1061 | finalContent?: string, | ||
| 1062 | reasoningContent?: string, | ||
| 1063 | timings?: ChatMessageTimings | ||
| 1064 | ) => { | ||
| 1065 | const fullContent = originalContent + (finalContent || appendedContent); | ||
| 1066 | const fullThinking = originalThinking + (reasoningContent || appendedThinking); | ||
| 1067 | await DatabaseService.updateMessage(msg.id, { | ||
| 1068 | content: fullContent, | ||
| 1069 | thinking: fullThinking, | ||
| 1070 | timestamp: Date.now(), | ||
| 1071 | timings | ||
| 1072 | }); | ||
| 1073 | conversationsStore.updateMessageAtIndex(idx, { | ||
| 1074 | content: fullContent, | ||
| 1075 | thinking: fullThinking, | ||
| 1076 | timestamp: Date.now(), | ||
| 1077 | timings | ||
| 1078 | }); | ||
| 1079 | conversationsStore.updateConversationTimestamp(); | ||
| 1080 | this.setChatLoading(msg.convId, false); | ||
| 1081 | this.clearChatStreaming(msg.convId); | ||
| 1082 | this.clearProcessingState(msg.convId); | ||
| 1083 | }, | ||
| 1084 | |||
| 1085 | onError: async (error: Error) => { | ||
| 1086 | if (this.isAbortError(error)) { | ||
| 1087 | if (hasReceivedContent && appendedContent) { | ||
| 1088 | await DatabaseService.updateMessage(msg.id, { | ||
| 1089 | content: originalContent + appendedContent, | ||
| 1090 | thinking: originalThinking + appendedThinking, | ||
| 1091 | timestamp: Date.now() | ||
| 1092 | }); | ||
| 1093 | conversationsStore.updateMessageAtIndex(idx, { | ||
| 1094 | content: originalContent + appendedContent, | ||
| 1095 | thinking: originalThinking + appendedThinking, | ||
| 1096 | timestamp: Date.now() | ||
| 1097 | }); | ||
| 1098 | } | ||
| 1099 | this.setChatLoading(msg.convId, false); | ||
| 1100 | this.clearChatStreaming(msg.convId); | ||
| 1101 | this.clearProcessingState(msg.convId); | ||
| 1102 | return; | ||
| 1103 | } | ||
| 1104 | console.error('Continue generation error:', error); | ||
| 1105 | conversationsStore.updateMessageAtIndex(idx, { | ||
| 1106 | content: originalContent, | ||
| 1107 | thinking: originalThinking | ||
| 1108 | }); | ||
| 1109 | await DatabaseService.updateMessage(msg.id, { | ||
| 1110 | content: originalContent, | ||
| 1111 | thinking: originalThinking | ||
| 1112 | }); | ||
| 1113 | this.setChatLoading(msg.convId, false); | ||
| 1114 | this.clearChatStreaming(msg.convId); | ||
| 1115 | this.clearProcessingState(msg.convId); | ||
| 1116 | this.showErrorDialog( | ||
| 1117 | error.name === 'TimeoutError' ? 'timeout' : 'server', | ||
| 1118 | error.message | ||
| 1119 | ); | ||
| 1120 | } | ||
| 1121 | }, | ||
| 1122 | msg.convId, | ||
| 1123 | abortController.signal | ||
| 1124 | ); | ||
| 1125 | } catch (error) { | ||
| 1126 | if (!this.isAbortError(error)) console.error('Failed to continue message:', error); | ||
| 1127 | if (activeConv) this.setChatLoading(activeConv.id, false); | ||
| 1128 | } | ||
| 1129 | } | ||
| 1130 | |||
| 1131 | async editAssistantMessage( | ||
| 1132 | messageId: string, | ||
| 1133 | newContent: string, | ||
| 1134 | shouldBranch: boolean | ||
| 1135 | ): Promise<void> { | ||
| 1136 | const activeConv = conversationsStore.activeConversation; | ||
| 1137 | if (!activeConv || this.isLoading) return; | ||
| 1138 | |||
| 1139 | const result = this.getMessageByIdWithRole(messageId, 'assistant'); | ||
| 1140 | if (!result) return; | ||
| 1141 | const { message: msg, index: idx } = result; | ||
| 1142 | |||
| 1143 | try { | ||
| 1144 | if (shouldBranch) { | ||
| 1145 | const newMessage = await DatabaseService.createMessageBranch( | ||
| 1146 | { | ||
| 1147 | convId: msg.convId, | ||
| 1148 | type: msg.type, | ||
| 1149 | timestamp: Date.now(), | ||
| 1150 | role: msg.role, | ||
| 1151 | content: newContent, | ||
| 1152 | thinking: msg.thinking || '', | ||
| 1153 | toolCalls: msg.toolCalls || '', | ||
| 1154 | children: [], | ||
| 1155 | model: msg.model | ||
| 1156 | }, | ||
| 1157 | msg.parent! | ||
| 1158 | ); | ||
| 1159 | await conversationsStore.updateCurrentNode(newMessage.id); | ||
| 1160 | } else { | ||
| 1161 | await DatabaseService.updateMessage(msg.id, { content: newContent }); | ||
| 1162 | await conversationsStore.updateCurrentNode(msg.id); | ||
| 1163 | conversationsStore.updateMessageAtIndex(idx, { | ||
| 1164 | content: newContent | ||
| 1165 | }); | ||
| 1166 | } | ||
| 1167 | conversationsStore.updateConversationTimestamp(); | ||
| 1168 | await conversationsStore.refreshActiveMessages(); | ||
| 1169 | } catch (error) { | ||
| 1170 | console.error('Failed to edit assistant message:', error); | ||
| 1171 | } | ||
| 1172 | } | ||
| 1173 | |||
| 1174 | async editUserMessagePreserveResponses( | ||
| 1175 | messageId: string, | ||
| 1176 | newContent: string, | ||
| 1177 | newExtras?: DatabaseMessageExtra[] | ||
| 1178 | ): Promise<void> { | ||
| 1179 | const activeConv = conversationsStore.activeConversation; | ||
| 1180 | if (!activeConv) return; | ||
| 1181 | |||
| 1182 | const result = this.getMessageByIdWithRole(messageId, 'user'); | ||
| 1183 | if (!result) return; | ||
| 1184 | const { message: msg, index: idx } = result; | ||
| 1185 | |||
| 1186 | try { | ||
| 1187 | const updateData: Partial<DatabaseMessage> = { | ||
| 1188 | content: newContent | ||
| 1189 | }; | ||
| 1190 | |||
| 1191 | // Update extras if provided (including empty array to clear attachments) | ||
| 1192 | // Deep clone to avoid Proxy objects from Svelte reactivity | ||
| 1193 | if (newExtras !== undefined) { | ||
| 1194 | updateData.extra = JSON.parse(JSON.stringify(newExtras)); | ||
| 1195 | } | ||
| 1196 | |||
| 1197 | await DatabaseService.updateMessage(messageId, updateData); | ||
| 1198 | conversationsStore.updateMessageAtIndex(idx, updateData); | ||
| 1199 | |||
| 1200 | const allMessages = await conversationsStore.getConversationMessages(activeConv.id); | ||
| 1201 | const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null); | ||
| 1202 | |||
| 1203 | if (rootMessage && msg.parent === rootMessage.id && newContent.trim()) { | ||
| 1204 | await conversationsStore.updateConversationTitleWithConfirmation( | ||
| 1205 | activeConv.id, | ||
| 1206 | newContent.trim(), | ||
| 1207 | conversationsStore.titleUpdateConfirmationCallback | ||
| 1208 | ); | ||
| 1209 | } | ||
| 1210 | conversationsStore.updateConversationTimestamp(); | ||
| 1211 | } catch (error) { | ||
| 1212 | console.error('Failed to edit user message:', error); | ||
| 1213 | } | ||
| 1214 | } | ||
| 1215 | |||
| 1216 | async editMessageWithBranching( | ||
| 1217 | messageId: string, | ||
| 1218 | newContent: string, | ||
| 1219 | newExtras?: DatabaseMessageExtra[] | ||
| 1220 | ): Promise<void> { | ||
| 1221 | const activeConv = conversationsStore.activeConversation; | ||
| 1222 | if (!activeConv || this.isLoading) return; | ||
| 1223 | |||
| 1224 | let result = this.getMessageByIdWithRole(messageId, 'user'); | ||
| 1225 | |||
| 1226 | if (!result) { | ||
| 1227 | result = this.getMessageByIdWithRole(messageId, 'system'); | ||
| 1228 | } | ||
| 1229 | |||
| 1230 | if (!result) return; | ||
| 1231 | const { message: msg } = result; | ||
| 1232 | |||
| 1233 | try { | ||
| 1234 | const allMessages = await conversationsStore.getConversationMessages(activeConv.id); | ||
| 1235 | const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null); | ||
| 1236 | const isFirstUserMessage = | ||
| 1237 | msg.role === 'user' && rootMessage && msg.parent === rootMessage.id; | ||
| 1238 | |||
| 1239 | const parentId = msg.parent || rootMessage?.id; | ||
| 1240 | if (!parentId) return; | ||
| 1241 | |||
| 1242 | // Use newExtras if provided, otherwise copy existing extras | ||
| 1243 | // Deep clone to avoid Proxy objects from Svelte reactivity | ||
| 1244 | const extrasToUse = | ||
| 1245 | newExtras !== undefined | ||
| 1246 | ? JSON.parse(JSON.stringify(newExtras)) | ||
| 1247 | : msg.extra | ||
| 1248 | ? JSON.parse(JSON.stringify(msg.extra)) | ||
| 1249 | : undefined; | ||
| 1250 | |||
| 1251 | const newMessage = await DatabaseService.createMessageBranch( | ||
| 1252 | { | ||
| 1253 | convId: msg.convId, | ||
| 1254 | type: msg.type, | ||
| 1255 | timestamp: Date.now(), | ||
| 1256 | role: msg.role, | ||
| 1257 | content: newContent, | ||
| 1258 | thinking: msg.thinking || '', | ||
| 1259 | toolCalls: msg.toolCalls || '', | ||
| 1260 | children: [], | ||
| 1261 | extra: extrasToUse, | ||
| 1262 | model: msg.model | ||
| 1263 | }, | ||
| 1264 | parentId | ||
| 1265 | ); | ||
| 1266 | await conversationsStore.updateCurrentNode(newMessage.id); | ||
| 1267 | conversationsStore.updateConversationTimestamp(); | ||
| 1268 | |||
| 1269 | if (isFirstUserMessage && newContent.trim()) { | ||
| 1270 | await conversationsStore.updateConversationTitleWithConfirmation( | ||
| 1271 | activeConv.id, | ||
| 1272 | newContent.trim(), | ||
| 1273 | conversationsStore.titleUpdateConfirmationCallback | ||
| 1274 | ); | ||
| 1275 | } | ||
| 1276 | await conversationsStore.refreshActiveMessages(); | ||
| 1277 | |||
| 1278 | if (msg.role === 'user') { | ||
| 1279 | await this.generateResponseForMessage(newMessage.id); | ||
| 1280 | } | ||
| 1281 | } catch (error) { | ||
| 1282 | console.error('Failed to edit message with branching:', error); | ||
| 1283 | } | ||
| 1284 | } | ||
| 1285 | |||
| 1286 | async regenerateMessageWithBranching(messageId: string, modelOverride?: string): Promise<void> { | ||
| 1287 | const activeConv = conversationsStore.activeConversation; | ||
| 1288 | if (!activeConv || this.isLoading) return; | ||
| 1289 | try { | ||
| 1290 | const idx = conversationsStore.findMessageIndex(messageId); | ||
| 1291 | if (idx === -1) return; | ||
| 1292 | const msg = conversationsStore.activeMessages[idx]; | ||
| 1293 | if (msg.role !== 'assistant') return; | ||
| 1294 | |||
| 1295 | const allMessages = await conversationsStore.getConversationMessages(activeConv.id); | ||
| 1296 | const parentMessage = allMessages.find((m) => m.id === msg.parent); | ||
| 1297 | if (!parentMessage) return; | ||
| 1298 | |||
| 1299 | this.setChatLoading(activeConv.id, true); | ||
| 1300 | this.clearChatStreaming(activeConv.id); | ||
| 1301 | |||
| 1302 | const newAssistantMessage = await DatabaseService.createMessageBranch( | ||
| 1303 | { | ||
| 1304 | convId: activeConv.id, | ||
| 1305 | type: 'text', | ||
| 1306 | timestamp: Date.now(), | ||
| 1307 | role: 'assistant', | ||
| 1308 | content: '', | ||
| 1309 | thinking: '', | ||
| 1310 | toolCalls: '', | ||
| 1311 | children: [], | ||
| 1312 | model: null | ||
| 1313 | }, | ||
| 1314 | parentMessage.id | ||
| 1315 | ); | ||
| 1316 | await conversationsStore.updateCurrentNode(newAssistantMessage.id); | ||
| 1317 | conversationsStore.updateConversationTimestamp(); | ||
| 1318 | await conversationsStore.refreshActiveMessages(); | ||
| 1319 | |||
| 1320 | const conversationPath = filterByLeafNodeId( | ||
| 1321 | allMessages, | ||
| 1322 | parentMessage.id, | ||
| 1323 | false | ||
| 1324 | ) as DatabaseMessage[]; | ||
| 1325 | // Use modelOverride if provided, otherwise use the original message's model | ||
| 1326 | // If neither is available, don't pass model (will use global selection) | ||
| 1327 | const modelToUse = modelOverride || msg.model || undefined; | ||
| 1328 | await this.streamChatCompletion( | ||
| 1329 | conversationPath, | ||
| 1330 | newAssistantMessage, | ||
| 1331 | undefined, | ||
| 1332 | undefined, | ||
| 1333 | modelToUse | ||
| 1334 | ); | ||
| 1335 | } catch (error) { | ||
| 1336 | if (!this.isAbortError(error)) | ||
| 1337 | console.error('Failed to regenerate message with branching:', error); | ||
| 1338 | this.setChatLoading(activeConv?.id || '', false); | ||
| 1339 | } | ||
| 1340 | } | ||
| 1341 | |||
| 1342 | private async generateResponseForMessage(userMessageId: string): Promise<void> { | ||
| 1343 | const activeConv = conversationsStore.activeConversation; | ||
| 1344 | |||
| 1345 | if (!activeConv) return; | ||
| 1346 | |||
| 1347 | this.errorDialogState = null; | ||
| 1348 | this.setChatLoading(activeConv.id, true); | ||
| 1349 | this.clearChatStreaming(activeConv.id); | ||
| 1350 | |||
| 1351 | try { | ||
| 1352 | const allMessages = await conversationsStore.getConversationMessages(activeConv.id); | ||
| 1353 | const conversationPath = filterByLeafNodeId( | ||
| 1354 | allMessages, | ||
| 1355 | userMessageId, | ||
| 1356 | false | ||
| 1357 | ) as DatabaseMessage[]; | ||
| 1358 | const assistantMessage = await DatabaseService.createMessageBranch( | ||
| 1359 | { | ||
| 1360 | convId: activeConv.id, | ||
| 1361 | type: 'text', | ||
| 1362 | timestamp: Date.now(), | ||
| 1363 | role: 'assistant', | ||
| 1364 | content: '', | ||
| 1365 | thinking: '', | ||
| 1366 | toolCalls: '', | ||
| 1367 | children: [], | ||
| 1368 | model: null | ||
| 1369 | }, | ||
| 1370 | userMessageId | ||
| 1371 | ); | ||
| 1372 | conversationsStore.addMessageToActive(assistantMessage); | ||
| 1373 | await this.streamChatCompletion(conversationPath, assistantMessage); | ||
| 1374 | } catch (error) { | ||
| 1375 | console.error('Failed to generate response:', error); | ||
| 1376 | this.setChatLoading(activeConv.id, false); | ||
| 1377 | } | ||
| 1378 | } | ||
| 1379 | |||
| 1380 | getAddFilesHandler(): ((files: File[]) => void) | null { | ||
| 1381 | return this.addFilesHandler; | ||
| 1382 | } | ||
| 1383 | |||
| 1384 | public getAllLoadingChats(): string[] { | ||
| 1385 | return Array.from(this.chatLoadingStates.keys()); | ||
| 1386 | } | ||
| 1387 | |||
| 1388 | public getAllStreamingChats(): string[] { | ||
| 1389 | return Array.from(this.chatStreamingStates.keys()); | ||
| 1390 | } | ||
| 1391 | |||
| 1392 | public getChatStreamingPublic( | ||
| 1393 | convId: string | ||
| 1394 | ): { response: string; messageId: string } | undefined { | ||
| 1395 | return this.getChatStreaming(convId); | ||
| 1396 | } | ||
| 1397 | |||
| 1398 | public isChatLoadingPublic(convId: string): boolean { | ||
| 1399 | return this.isChatLoading(convId); | ||
| 1400 | } | ||
| 1401 | |||
| 1402 | isEditing(): boolean { | ||
| 1403 | return this.isEditModeActive; | ||
| 1404 | } | ||
| 1405 | |||
| 1406 | setEditModeActive(handler: (files: File[]) => void): void { | ||
| 1407 | this.isEditModeActive = true; | ||
| 1408 | this.addFilesHandler = handler; | ||
| 1409 | } | ||
| 1410 | |||
| 1411 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 1412 | // Utilities | ||
| 1413 | // ───────────────────────────────────────────────────────────────────────────── | ||
| 1414 | |||
| 1415 | private getApiOptions(): Record<string, unknown> { | ||
| 1416 | const currentConfig = config(); | ||
| 1417 | const hasValue = (value: unknown): boolean => | ||
| 1418 | value !== undefined && value !== null && value !== ''; | ||
| 1419 | |||
| 1420 | const apiOptions: Record<string, unknown> = { stream: true, timings_per_token: true }; | ||
| 1421 | |||
| 1422 | // Model selection (required in ROUTER mode) | ||
| 1423 | if (isRouterMode()) { | ||
| 1424 | const modelName = selectedModelName(); | ||
| 1425 | if (modelName) apiOptions.model = modelName; | ||
| 1426 | } | ||
| 1427 | |||
| 1428 | // Config options needed by ChatService | ||
| 1429 | if (currentConfig.systemMessage) apiOptions.systemMessage = currentConfig.systemMessage; | ||
| 1430 | if (currentConfig.disableReasoningFormat) apiOptions.disableReasoningFormat = true; | ||
| 1431 | |||
| 1432 | if (hasValue(currentConfig.temperature)) | ||
| 1433 | apiOptions.temperature = Number(currentConfig.temperature); | ||
| 1434 | if (hasValue(currentConfig.max_tokens)) | ||
| 1435 | apiOptions.max_tokens = Number(currentConfig.max_tokens); | ||
| 1436 | if (hasValue(currentConfig.dynatemp_range)) | ||
| 1437 | apiOptions.dynatemp_range = Number(currentConfig.dynatemp_range); | ||
| 1438 | if (hasValue(currentConfig.dynatemp_exponent)) | ||
| 1439 | apiOptions.dynatemp_exponent = Number(currentConfig.dynatemp_exponent); | ||
| 1440 | if (hasValue(currentConfig.top_k)) apiOptions.top_k = Number(currentConfig.top_k); | ||
| 1441 | if (hasValue(currentConfig.top_p)) apiOptions.top_p = Number(currentConfig.top_p); | ||
| 1442 | if (hasValue(currentConfig.min_p)) apiOptions.min_p = Number(currentConfig.min_p); | ||
| 1443 | if (hasValue(currentConfig.xtc_probability)) | ||
| 1444 | apiOptions.xtc_probability = Number(currentConfig.xtc_probability); | ||
| 1445 | if (hasValue(currentConfig.xtc_threshold)) | ||
| 1446 | apiOptions.xtc_threshold = Number(currentConfig.xtc_threshold); | ||
| 1447 | if (hasValue(currentConfig.typ_p)) apiOptions.typ_p = Number(currentConfig.typ_p); | ||
| 1448 | if (hasValue(currentConfig.repeat_last_n)) | ||
| 1449 | apiOptions.repeat_last_n = Number(currentConfig.repeat_last_n); | ||
| 1450 | if (hasValue(currentConfig.repeat_penalty)) | ||
| 1451 | apiOptions.repeat_penalty = Number(currentConfig.repeat_penalty); | ||
| 1452 | if (hasValue(currentConfig.presence_penalty)) | ||
| 1453 | apiOptions.presence_penalty = Number(currentConfig.presence_penalty); | ||
| 1454 | if (hasValue(currentConfig.frequency_penalty)) | ||
| 1455 | apiOptions.frequency_penalty = Number(currentConfig.frequency_penalty); | ||
| 1456 | if (hasValue(currentConfig.dry_multiplier)) | ||
| 1457 | apiOptions.dry_multiplier = Number(currentConfig.dry_multiplier); | ||
| 1458 | if (hasValue(currentConfig.dry_base)) apiOptions.dry_base = Number(currentConfig.dry_base); | ||
| 1459 | if (hasValue(currentConfig.dry_allowed_length)) | ||
| 1460 | apiOptions.dry_allowed_length = Number(currentConfig.dry_allowed_length); | ||
| 1461 | if (hasValue(currentConfig.dry_penalty_last_n)) | ||
| 1462 | apiOptions.dry_penalty_last_n = Number(currentConfig.dry_penalty_last_n); | ||
| 1463 | if (currentConfig.samplers) apiOptions.samplers = currentConfig.samplers; | ||
| 1464 | if (currentConfig.backend_sampling) | ||
| 1465 | apiOptions.backend_sampling = currentConfig.backend_sampling; | ||
| 1466 | if (currentConfig.custom) apiOptions.custom = currentConfig.custom; | ||
| 1467 | |||
| 1468 | return apiOptions; | ||
| 1469 | } | ||
| 1470 | } | ||
| 1471 | |||
| 1472 | export const chatStore = new ChatStore(); | ||
| 1473 | |||
| 1474 | export const activeProcessingState = () => chatStore.activeProcessingState; | ||
| 1475 | export const clearEditMode = () => chatStore.clearEditMode(); | ||
| 1476 | export const currentResponse = () => chatStore.currentResponse; | ||
| 1477 | export const errorDialog = () => chatStore.errorDialogState; | ||
| 1478 | export const getAddFilesHandler = () => chatStore.getAddFilesHandler(); | ||
| 1479 | export const getAllLoadingChats = () => chatStore.getAllLoadingChats(); | ||
| 1480 | export const getAllStreamingChats = () => chatStore.getAllStreamingChats(); | ||
| 1481 | export const getChatStreaming = (convId: string) => chatStore.getChatStreamingPublic(convId); | ||
| 1482 | export const isChatLoading = (convId: string) => chatStore.isChatLoadingPublic(convId); | ||
| 1483 | export const isChatStreaming = () => chatStore.isStreaming(); | ||
| 1484 | export const isEditing = () => chatStore.isEditing(); | ||
| 1485 | export const isLoading = () => chatStore.isLoading; | ||
| 1486 | export const setEditModeActive = (handler: (files: File[]) => void) => | ||
| 1487 | chatStore.setEditModeActive(handler); | ||
