summaryrefslogtreecommitdiff
path: root/llama.cpp/tools/server/webui/src/lib/stores/chat.svelte.ts
diff options
context:
space:
mode:
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.ts1487
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 @@
+import { DatabaseService, ChatService } from '$lib/services';
+import { conversationsStore } from '$lib/stores/conversations.svelte';
+import { config } from '$lib/stores/settings.svelte';
+import { contextSize, isRouterMode } from '$lib/stores/server.svelte';
+import {
+ selectedModelName,
+ modelsStore,
+ selectedModelContextSize
+} from '$lib/stores/models.svelte';
+import {
+ normalizeModelName,
+ filterByLeafNodeId,
+ findDescendantMessages,
+ findLeafNode
+} from '$lib/utils';
+import { SvelteMap } from 'svelte/reactivity';
+import { DEFAULT_CONTEXT } from '$lib/constants/default-context';
+
+/**
+ * chatStore - Active AI interaction and streaming state management
+ *
+ * **Terminology - Chat vs Conversation:**
+ * - **Chat**: The active interaction space with the Chat Completions API. Represents the
+ * real-time streaming session, loading states, and UI visualization of AI communication.
+ * A "chat" is ephemeral - it exists only while the user is actively interacting with the AI.
+ * - **Conversation**: The persistent database entity storing all messages and metadata.
+ * Managed by conversationsStore, conversations persist across sessions and page reloads.
+ *
+ * This store manages all active AI interactions including real-time streaming, response
+ * generation, and per-chat loading states. It handles the runtime layer between UI and
+ * AI backend, supporting concurrent streaming across multiple conversations.
+ *
+ * **Architecture & Relationships:**
+ * - **chatStore** (this class): Active AI session and streaming management
+ * - Manages real-time AI response streaming via ChatService
+ * - Tracks per-chat loading and streaming states for concurrent sessions
+ * - Handles message operations (send, edit, regenerate, branch)
+ * - Coordinates with conversationsStore for persistence
+ *
+ * - **conversationsStore**: Provides conversation data and message arrays for chat context
+ * - **ChatService**: Low-level API communication with llama.cpp server
+ * - **DatabaseService**: Message persistence and retrieval
+ *
+ * **Key Features:**
+ * - **AI Streaming**: Real-time token streaming with abort support
+ * - **Concurrent Chats**: Independent loading/streaming states per conversation
+ * - **Message Branching**: Edit, regenerate, and branch conversation trees
+ * - **Error Handling**: Timeout and server error recovery with user feedback
+ * - **Graceful Stop**: Save partial responses when stopping generation
+ *
+ * **State Management:**
+ * - Global `isLoading` and `currentResponse` for active chat UI
+ * - `chatLoadingStates` Map for per-conversation streaming tracking
+ * - `chatStreamingStates` Map for per-conversation streaming content
+ * - `processingStates` Map for per-conversation processing state (timing/context info)
+ * - Automatic state sync when switching between conversations
+ */
+class ChatStore {
+ // ─────────────────────────────────────────────────────────────────────────────
+ // State
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ activeProcessingState = $state<ApiProcessingState | null>(null);
+ currentResponse = $state('');
+ errorDialogState = $state<{
+ type: 'timeout' | 'server';
+ message: string;
+ contextInfo?: { n_prompt_tokens: number; n_ctx: number };
+ } | null>(null);
+ isLoading = $state(false);
+ chatLoadingStates = new SvelteMap<string, boolean>();
+ chatStreamingStates = new SvelteMap<string, { response: string; messageId: string }>();
+ private abortControllers = new SvelteMap<string, AbortController>();
+ private processingStates = new SvelteMap<string, ApiProcessingState | null>();
+ private activeConversationId = $state<string | null>(null);
+ private isStreamingActive = $state(false);
+ private isEditModeActive = $state(false);
+ private addFilesHandler: ((files: File[]) => void) | null = $state(null);
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Loading State
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ private setChatLoading(convId: string, loading: boolean): void {
+ if (loading) {
+ this.chatLoadingStates.set(convId, true);
+ if (conversationsStore.activeConversation?.id === convId) this.isLoading = true;
+ } else {
+ this.chatLoadingStates.delete(convId);
+ if (conversationsStore.activeConversation?.id === convId) this.isLoading = false;
+ }
+ }
+
+ private isChatLoading(convId: string): boolean {
+ return this.chatLoadingStates.get(convId) || false;
+ }
+
+ private setChatStreaming(convId: string, response: string, messageId: string): void {
+ this.chatStreamingStates.set(convId, { response, messageId });
+ if (conversationsStore.activeConversation?.id === convId) this.currentResponse = response;
+ }
+
+ private clearChatStreaming(convId: string): void {
+ this.chatStreamingStates.delete(convId);
+ if (conversationsStore.activeConversation?.id === convId) this.currentResponse = '';
+ }
+
+ private getChatStreaming(convId: string): { response: string; messageId: string } | undefined {
+ return this.chatStreamingStates.get(convId);
+ }
+
+ syncLoadingStateForChat(convId: string): void {
+ this.isLoading = this.isChatLoading(convId);
+ const streamingState = this.getChatStreaming(convId);
+ this.currentResponse = streamingState?.response || '';
+ }
+
+ /**
+ * Clears global UI state without affecting background streaming.
+ * Used when navigating to empty/new chat while other chats stream in background.
+ */
+ clearUIState(): void {
+ this.isLoading = false;
+ this.currentResponse = '';
+ }
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Processing State
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ /**
+ * Set the active conversation for statistics display
+ */
+ setActiveProcessingConversation(conversationId: string | null): void {
+ this.activeConversationId = conversationId;
+
+ if (conversationId) {
+ this.activeProcessingState = this.processingStates.get(conversationId) || null;
+ } else {
+ this.activeProcessingState = null;
+ }
+ }
+
+ /**
+ * Get processing state for a specific conversation
+ */
+ getProcessingState(conversationId: string): ApiProcessingState | null {
+ return this.processingStates.get(conversationId) || null;
+ }
+
+ /**
+ * Clear processing state for a specific conversation
+ */
+ clearProcessingState(conversationId: string): void {
+ this.processingStates.delete(conversationId);
+
+ if (conversationId === this.activeConversationId) {
+ this.activeProcessingState = null;
+ }
+ }
+
+ /**
+ * Get the current processing state for the active conversation (reactive)
+ * Returns the direct reactive state for UI binding
+ */
+ getActiveProcessingState(): ApiProcessingState | null {
+ return this.activeProcessingState;
+ }
+
+ /**
+ * Updates processing state with timing data from streaming response
+ */
+ updateProcessingStateFromTimings(
+ timingData: {
+ prompt_n: number;
+ prompt_ms?: number;
+ predicted_n: number;
+ predicted_per_second: number;
+ cache_n: number;
+ prompt_progress?: ChatMessagePromptProgress;
+ },
+ conversationId?: string
+ ): void {
+ const processingState = this.parseTimingData(timingData);
+
+ if (processingState === null) {
+ console.warn('Failed to parse timing data - skipping update');
+ return;
+ }
+
+ const targetId = conversationId || this.activeConversationId;
+ if (targetId) {
+ this.processingStates.set(targetId, processingState);
+
+ if (targetId === this.activeConversationId) {
+ this.activeProcessingState = processingState;
+ }
+ }
+ }
+
+ /**
+ * Get current processing state (sync version for reactive access)
+ */
+ getCurrentProcessingStateSync(): ApiProcessingState | null {
+ return this.activeProcessingState;
+ }
+
+ /**
+ * Restore processing state from last assistant message timings
+ * Call this when keepStatsVisible is enabled and we need to show last known stats
+ */
+ restoreProcessingStateFromMessages(messages: DatabaseMessage[], conversationId: string): void {
+ for (let i = messages.length - 1; i >= 0; i--) {
+ const message = messages[i];
+ if (message.role === 'assistant' && message.timings) {
+ const restoredState = this.parseTimingData({
+ prompt_n: message.timings.prompt_n || 0,
+ prompt_ms: message.timings.prompt_ms,
+ predicted_n: message.timings.predicted_n || 0,
+ predicted_per_second:
+ message.timings.predicted_n && message.timings.predicted_ms
+ ? (message.timings.predicted_n / message.timings.predicted_ms) * 1000
+ : 0,
+ cache_n: message.timings.cache_n || 0
+ });
+
+ if (restoredState) {
+ this.processingStates.set(conversationId, restoredState);
+
+ if (conversationId === this.activeConversationId) {
+ this.activeProcessingState = restoredState;
+ }
+
+ return;
+ }
+ }
+ }
+ }
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Streaming
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ /**
+ * Start streaming session tracking
+ */
+ startStreaming(): void {
+ this.isStreamingActive = true;
+ }
+
+ /**
+ * Stop streaming session tracking
+ */
+ stopStreaming(): void {
+ this.isStreamingActive = false;
+ }
+
+ /**
+ * Check if currently in a streaming session
+ */
+ isStreaming(): boolean {
+ return this.isStreamingActive;
+ }
+
+ private getContextTotal(): number {
+ const activeState = this.getActiveProcessingState();
+
+ if (activeState && activeState.contextTotal > 0) {
+ return activeState.contextTotal;
+ }
+
+ if (isRouterMode()) {
+ const modelContextSize = selectedModelContextSize();
+ if (modelContextSize && modelContextSize > 0) {
+ return modelContextSize;
+ }
+ }
+
+ const propsContextSize = contextSize();
+ if (propsContextSize && propsContextSize > 0) {
+ return propsContextSize;
+ }
+
+ return DEFAULT_CONTEXT;
+ }
+
+ private parseTimingData(timingData: Record<string, unknown>): ApiProcessingState | null {
+ const promptTokens = (timingData.prompt_n as number) || 0;
+ const promptMs = (timingData.prompt_ms as number) || undefined;
+ const predictedTokens = (timingData.predicted_n as number) || 0;
+ const tokensPerSecond = (timingData.predicted_per_second as number) || 0;
+ const cacheTokens = (timingData.cache_n as number) || 0;
+ const promptProgress = timingData.prompt_progress as
+ | {
+ total: number;
+ cache: number;
+ processed: number;
+ time_ms: number;
+ }
+ | undefined;
+
+ const contextTotal = this.getContextTotal();
+ const currentConfig = config();
+ const outputTokensMax = currentConfig.max_tokens || -1;
+
+ // Note: for timings data, the n_prompt does NOT include cache tokens
+ const contextUsed = promptTokens + cacheTokens + predictedTokens;
+ const outputTokensUsed = predictedTokens;
+
+ // Note: for prompt progress, the "processed" DOES include cache tokens
+ // we need to exclude them to get the real prompt tokens processed count
+ const progressCache = promptProgress?.cache || 0;
+ const progressActualDone = (promptProgress?.processed ?? 0) - progressCache;
+ const progressActualTotal = (promptProgress?.total ?? 0) - progressCache;
+ const progressPercent = promptProgress
+ ? Math.round((progressActualDone / progressActualTotal) * 100)
+ : undefined;
+
+ return {
+ status: predictedTokens > 0 ? 'generating' : promptProgress ? 'preparing' : 'idle',
+ tokensDecoded: predictedTokens,
+ tokensRemaining: outputTokensMax - predictedTokens,
+ contextUsed,
+ contextTotal,
+ outputTokensUsed,
+ outputTokensMax,
+ hasNextToken: predictedTokens > 0,
+ tokensPerSecond,
+ temperature: currentConfig.temperature ?? 0.8,
+ topP: currentConfig.top_p ?? 0.95,
+ speculative: false,
+ progressPercent,
+ promptProgress,
+ promptTokens,
+ promptMs,
+ cacheTokens
+ };
+ }
+
+ /**
+ * Gets the model used in a conversation based on the latest assistant message.
+ * Returns the model from the most recent assistant message that has a model field set.
+ *
+ * @param messages - Array of messages to search through
+ * @returns The model name or null if no model found
+ */
+ getConversationModel(messages: DatabaseMessage[]): string | null {
+ // Search backwards through messages to find most recent assistant message with model
+ for (let i = messages.length - 1; i >= 0; i--) {
+ const message = messages[i];
+ if (message.role === 'assistant' && message.model) {
+ return message.model;
+ }
+ }
+ return null;
+ }
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Error Handling
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ private isAbortError(error: unknown): boolean {
+ return error instanceof Error && (error.name === 'AbortError' || error instanceof DOMException);
+ }
+
+ private showErrorDialog(
+ type: 'timeout' | 'server',
+ message: string,
+ contextInfo?: { n_prompt_tokens: number; n_ctx: number }
+ ): void {
+ this.errorDialogState = { type, message, contextInfo };
+ }
+
+ dismissErrorDialog(): void {
+ this.errorDialogState = null;
+ }
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Message Operations
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ /**
+ * Finds a message by ID and optionally validates its role.
+ * Returns message and index, or null if not found or role doesn't match.
+ */
+ private getMessageByIdWithRole(
+ messageId: string,
+ expectedRole?: ChatRole
+ ): { message: DatabaseMessage; index: number } | null {
+ const index = conversationsStore.findMessageIndex(messageId);
+ if (index === -1) return null;
+
+ const message = conversationsStore.activeMessages[index];
+ if (expectedRole && message.role !== expectedRole) return null;
+
+ return { message, index };
+ }
+
+ async addMessage(
+ role: ChatRole,
+ content: string,
+ type: ChatMessageType = 'text',
+ parent: string = '-1',
+ extras?: DatabaseMessageExtra[]
+ ): Promise<DatabaseMessage | null> {
+ const activeConv = conversationsStore.activeConversation;
+ if (!activeConv) {
+ console.error('No active conversation when trying to add message');
+ return null;
+ }
+
+ try {
+ let parentId: string | null = null;
+
+ if (parent === '-1') {
+ const activeMessages = conversationsStore.activeMessages;
+ if (activeMessages.length > 0) {
+ parentId = activeMessages[activeMessages.length - 1].id;
+ } else {
+ const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
+ const rootMessage = allMessages.find((m) => m.parent === null && m.type === 'root');
+ if (!rootMessage) {
+ parentId = await DatabaseService.createRootMessage(activeConv.id);
+ } else {
+ parentId = rootMessage.id;
+ }
+ }
+ } else {
+ parentId = parent;
+ }
+
+ const message = await DatabaseService.createMessageBranch(
+ {
+ convId: activeConv.id,
+ role,
+ content,
+ type,
+ timestamp: Date.now(),
+ thinking: '',
+ toolCalls: '',
+ children: [],
+ extra: extras
+ },
+ parentId
+ );
+
+ conversationsStore.addMessageToActive(message);
+ await conversationsStore.updateCurrentNode(message.id);
+ conversationsStore.updateConversationTimestamp();
+
+ return message;
+ } catch (error) {
+ console.error('Failed to add message:', error);
+ return null;
+ }
+ }
+
+ private async createAssistantMessage(parentId?: string): Promise<DatabaseMessage | null> {
+ const activeConv = conversationsStore.activeConversation;
+ if (!activeConv) return null;
+
+ return await DatabaseService.createMessageBranch(
+ {
+ convId: activeConv.id,
+ type: 'text',
+ role: 'assistant',
+ content: '',
+ timestamp: Date.now(),
+ thinking: '',
+ toolCalls: '',
+ children: [],
+ model: null
+ },
+ parentId || null
+ );
+ }
+
+ private async streamChatCompletion(
+ allMessages: DatabaseMessage[],
+ assistantMessage: DatabaseMessage,
+ onComplete?: (content: string) => Promise<void>,
+ onError?: (error: Error) => void,
+ modelOverride?: string | null
+ ): Promise<void> {
+ // Ensure model props are cached before streaming (for correct n_ctx in processing info)
+ if (isRouterMode()) {
+ const modelName = modelOverride || selectedModelName();
+ if (modelName && !modelsStore.getModelProps(modelName)) {
+ await modelsStore.fetchModelProps(modelName);
+ }
+ }
+
+ let streamedContent = '';
+ let streamedReasoningContent = '';
+ let streamedToolCallContent = '';
+ let resolvedModel: string | null = null;
+ let modelPersisted = false;
+
+ const recordModel = (modelName: string | null | undefined, persistImmediately = true): void => {
+ if (!modelName) return;
+ const normalizedModel = normalizeModelName(modelName);
+ if (!normalizedModel || normalizedModel === resolvedModel) return;
+ resolvedModel = normalizedModel;
+ const messageIndex = conversationsStore.findMessageIndex(assistantMessage.id);
+ conversationsStore.updateMessageAtIndex(messageIndex, { model: normalizedModel });
+ if (persistImmediately && !modelPersisted) {
+ modelPersisted = true;
+ DatabaseService.updateMessage(assistantMessage.id, { model: normalizedModel }).catch(() => {
+ modelPersisted = false;
+ resolvedModel = null;
+ });
+ }
+ };
+
+ this.startStreaming();
+ this.setActiveProcessingConversation(assistantMessage.convId);
+
+ const abortController = this.getOrCreateAbortController(assistantMessage.convId);
+
+ await ChatService.sendMessage(
+ allMessages,
+ {
+ ...this.getApiOptions(),
+ ...(modelOverride ? { model: modelOverride } : {}),
+ onChunk: (chunk: string) => {
+ streamedContent += chunk;
+ this.setChatStreaming(assistantMessage.convId, streamedContent, assistantMessage.id);
+ const idx = conversationsStore.findMessageIndex(assistantMessage.id);
+ conversationsStore.updateMessageAtIndex(idx, { content: streamedContent });
+ },
+ onReasoningChunk: (reasoningChunk: string) => {
+ streamedReasoningContent += reasoningChunk;
+ const idx = conversationsStore.findMessageIndex(assistantMessage.id);
+ conversationsStore.updateMessageAtIndex(idx, { thinking: streamedReasoningContent });
+ },
+ onToolCallChunk: (toolCallChunk: string) => {
+ const chunk = toolCallChunk.trim();
+ if (!chunk) return;
+ streamedToolCallContent = chunk;
+ const idx = conversationsStore.findMessageIndex(assistantMessage.id);
+ conversationsStore.updateMessageAtIndex(idx, { toolCalls: streamedToolCallContent });
+ },
+ onModel: (modelName: string) => recordModel(modelName),
+ 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
+ },
+ assistantMessage.convId
+ );
+ },
+ onComplete: async (
+ finalContent?: string,
+ reasoningContent?: string,
+ timings?: ChatMessageTimings,
+ toolCallContent?: string
+ ) => {
+ this.stopStreaming();
+
+ const updateData: Record<string, unknown> = {
+ content: finalContent || streamedContent,
+ thinking: reasoningContent || streamedReasoningContent,
+ toolCalls: toolCallContent || streamedToolCallContent,
+ timings
+ };
+ if (resolvedModel && !modelPersisted) {
+ updateData.model = resolvedModel;
+ }
+ await DatabaseService.updateMessage(assistantMessage.id, updateData);
+
+ const idx = conversationsStore.findMessageIndex(assistantMessage.id);
+ const uiUpdate: Partial<DatabaseMessage> = {
+ content: updateData.content as string,
+ toolCalls: updateData.toolCalls as string
+ };
+ if (timings) uiUpdate.timings = timings;
+ if (resolvedModel) uiUpdate.model = resolvedModel;
+
+ conversationsStore.updateMessageAtIndex(idx, uiUpdate);
+ await conversationsStore.updateCurrentNode(assistantMessage.id);
+
+ if (onComplete) await onComplete(streamedContent);
+ this.setChatLoading(assistantMessage.convId, false);
+ this.clearChatStreaming(assistantMessage.convId);
+ this.clearProcessingState(assistantMessage.convId);
+
+ if (isRouterMode()) {
+ modelsStore.fetchRouterModels().catch(console.error);
+ }
+ },
+ onError: (error: Error) => {
+ this.stopStreaming();
+
+ if (this.isAbortError(error)) {
+ this.setChatLoading(assistantMessage.convId, false);
+ this.clearChatStreaming(assistantMessage.convId);
+ this.clearProcessingState(assistantMessage.convId);
+
+ return;
+ }
+
+ console.error('Streaming error:', error);
+
+ this.setChatLoading(assistantMessage.convId, false);
+ this.clearChatStreaming(assistantMessage.convId);
+ this.clearProcessingState(assistantMessage.convId);
+
+ const idx = conversationsStore.findMessageIndex(assistantMessage.id);
+
+ if (idx !== -1) {
+ const failedMessage = conversationsStore.removeMessageAtIndex(idx);
+ if (failedMessage) DatabaseService.deleteMessage(failedMessage.id).catch(console.error);
+ }
+
+ const contextInfo = (
+ error as Error & { contextInfo?: { n_prompt_tokens: number; n_ctx: number } }
+ ).contextInfo;
+
+ this.showErrorDialog(
+ error.name === 'TimeoutError' ? 'timeout' : 'server',
+ error.message,
+ contextInfo
+ );
+
+ if (onError) onError(error);
+ }
+ },
+ assistantMessage.convId,
+ abortController.signal
+ );
+ }
+
+ async sendMessage(content: string, extras?: DatabaseMessageExtra[]): Promise<void> {
+ if (!content.trim() && (!extras || extras.length === 0)) return;
+ const activeConv = conversationsStore.activeConversation;
+ if (activeConv && this.isChatLoading(activeConv.id)) return;
+
+ let isNewConversation = false;
+ if (!activeConv) {
+ await conversationsStore.createConversation();
+ isNewConversation = true;
+ }
+ const currentConv = conversationsStore.activeConversation;
+ if (!currentConv) return;
+
+ this.errorDialogState = null;
+ this.setChatLoading(currentConv.id, true);
+ this.clearChatStreaming(currentConv.id);
+
+ try {
+ if (isNewConversation) {
+ const rootId = await DatabaseService.createRootMessage(currentConv.id);
+ const currentConfig = config();
+ const systemPrompt = currentConfig.systemMessage?.toString().trim();
+
+ if (systemPrompt) {
+ const systemMessage = await DatabaseService.createSystemMessage(
+ currentConv.id,
+ systemPrompt,
+ rootId
+ );
+
+ conversationsStore.addMessageToActive(systemMessage);
+ }
+ }
+
+ const userMessage = await this.addMessage('user', content, 'text', '-1', extras);
+ if (!userMessage) throw new Error('Failed to add user message');
+ if (isNewConversation && content)
+ await conversationsStore.updateConversationName(currentConv.id, content.trim());
+
+ const assistantMessage = await this.createAssistantMessage(userMessage.id);
+
+ if (!assistantMessage) throw new Error('Failed to create assistant message');
+
+ conversationsStore.addMessageToActive(assistantMessage);
+ await this.streamChatCompletion(
+ conversationsStore.activeMessages.slice(0, -1),
+ assistantMessage
+ );
+ } catch (error) {
+ if (this.isAbortError(error)) {
+ this.setChatLoading(currentConv.id, false);
+ return;
+ }
+ console.error('Failed to send message:', error);
+ this.setChatLoading(currentConv.id, false);
+ if (!this.errorDialogState) {
+ const dialogType =
+ error instanceof Error && error.name === 'TimeoutError' ? 'timeout' : 'server';
+ const contextInfo = (
+ error as Error & { contextInfo?: { n_prompt_tokens: number; n_ctx: number } }
+ ).contextInfo;
+
+ this.showErrorDialog(
+ dialogType,
+ error instanceof Error ? error.message : 'Unknown error',
+ contextInfo
+ );
+ }
+ }
+ }
+
+ async stopGeneration(): Promise<void> {
+ const activeConv = conversationsStore.activeConversation;
+
+ if (!activeConv) return;
+
+ await this.stopGenerationForChat(activeConv.id);
+ }
+
+ async stopGenerationForChat(convId: string): Promise<void> {
+ await this.savePartialResponseIfNeeded(convId);
+
+ this.stopStreaming();
+ this.abortRequest(convId);
+ this.setChatLoading(convId, false);
+ this.clearChatStreaming(convId);
+ this.clearProcessingState(convId);
+ }
+
+ /**
+ * Gets or creates an AbortController for a conversation
+ */
+ private getOrCreateAbortController(convId: string): AbortController {
+ let controller = this.abortControllers.get(convId);
+ if (!controller || controller.signal.aborted) {
+ controller = new AbortController();
+ this.abortControllers.set(convId, controller);
+ }
+ return controller;
+ }
+
+ /**
+ * Aborts any ongoing request for a conversation
+ */
+ private abortRequest(convId?: string): void {
+ if (convId) {
+ const controller = this.abortControllers.get(convId);
+ if (controller) {
+ controller.abort();
+ this.abortControllers.delete(convId);
+ }
+ } else {
+ for (const controller of this.abortControllers.values()) {
+ controller.abort();
+ }
+ this.abortControllers.clear();
+ }
+ }
+
+ private async savePartialResponseIfNeeded(convId?: string): Promise<void> {
+ const conversationId = convId || conversationsStore.activeConversation?.id;
+
+ if (!conversationId) return;
+
+ const streamingState = this.chatStreamingStates.get(conversationId);
+
+ if (!streamingState || !streamingState.response.trim()) return;
+
+ const messages =
+ conversationId === conversationsStore.activeConversation?.id
+ ? conversationsStore.activeMessages
+ : await conversationsStore.getConversationMessages(conversationId);
+
+ if (!messages.length) return;
+
+ const lastMessage = messages[messages.length - 1];
+
+ if (lastMessage?.role === 'assistant') {
+ try {
+ const updateData: { content: string; thinking?: string; timings?: ChatMessageTimings } = {
+ content: streamingState.response
+ };
+ if (lastMessage.thinking?.trim()) updateData.thinking = lastMessage.thinking;
+ const lastKnownState = this.getProcessingState(conversationId);
+ if (lastKnownState) {
+ updateData.timings = {
+ prompt_n: lastKnownState.promptTokens || 0,
+ prompt_ms: lastKnownState.promptMs,
+ predicted_n: lastKnownState.tokensDecoded || 0,
+ cache_n: lastKnownState.cacheTokens || 0,
+ predicted_ms:
+ lastKnownState.tokensPerSecond && lastKnownState.tokensDecoded
+ ? (lastKnownState.tokensDecoded / lastKnownState.tokensPerSecond) * 1000
+ : undefined
+ };
+ }
+
+ await DatabaseService.updateMessage(lastMessage.id, updateData);
+
+ lastMessage.content = this.currentResponse;
+
+ if (updateData.thinking) lastMessage.thinking = updateData.thinking;
+
+ if (updateData.timings) lastMessage.timings = updateData.timings;
+ } catch (error) {
+ lastMessage.content = this.currentResponse;
+ console.error('Failed to save partial response:', error);
+ }
+ }
+ }
+
+ async updateMessage(messageId: string, newContent: string): Promise<void> {
+ const activeConv = conversationsStore.activeConversation;
+ if (!activeConv) return;
+ if (this.isLoading) this.stopGeneration();
+
+ const result = this.getMessageByIdWithRole(messageId, 'user');
+ if (!result) return;
+ const { message: messageToUpdate, index: messageIndex } = result;
+ const originalContent = messageToUpdate.content;
+
+ try {
+ const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
+ const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
+ const isFirstUserMessage = rootMessage && messageToUpdate.parent === rootMessage.id;
+
+ conversationsStore.updateMessageAtIndex(messageIndex, { content: newContent });
+ await DatabaseService.updateMessage(messageId, { content: newContent });
+
+ if (isFirstUserMessage && newContent.trim()) {
+ await conversationsStore.updateConversationTitleWithConfirmation(
+ activeConv.id,
+ newContent.trim(),
+ conversationsStore.titleUpdateConfirmationCallback
+ );
+ }
+
+ const messagesToRemove = conversationsStore.activeMessages.slice(messageIndex + 1);
+
+ for (const message of messagesToRemove) await DatabaseService.deleteMessage(message.id);
+
+ conversationsStore.sliceActiveMessages(messageIndex + 1);
+ conversationsStore.updateConversationTimestamp();
+
+ this.setChatLoading(activeConv.id, true);
+ this.clearChatStreaming(activeConv.id);
+
+ const assistantMessage = await this.createAssistantMessage();
+
+ if (!assistantMessage) throw new Error('Failed to create assistant message');
+
+ conversationsStore.addMessageToActive(assistantMessage);
+
+ await conversationsStore.updateCurrentNode(assistantMessage.id);
+ await this.streamChatCompletion(
+ conversationsStore.activeMessages.slice(0, -1),
+ assistantMessage,
+ undefined,
+ () => {
+ conversationsStore.updateMessageAtIndex(conversationsStore.findMessageIndex(messageId), {
+ content: originalContent
+ });
+ }
+ );
+ } catch (error) {
+ if (!this.isAbortError(error)) console.error('Failed to update message:', error);
+ }
+ }
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Regeneration
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ async regenerateMessage(messageId: string): Promise<void> {
+ const activeConv = conversationsStore.activeConversation;
+ if (!activeConv || this.isLoading) return;
+
+ const result = this.getMessageByIdWithRole(messageId, 'assistant');
+ if (!result) return;
+ const { index: messageIndex } = result;
+
+ try {
+ const messagesToRemove = conversationsStore.activeMessages.slice(messageIndex);
+ for (const message of messagesToRemove) await DatabaseService.deleteMessage(message.id);
+ conversationsStore.sliceActiveMessages(messageIndex);
+ conversationsStore.updateConversationTimestamp();
+
+ this.setChatLoading(activeConv.id, true);
+ this.clearChatStreaming(activeConv.id);
+
+ const parentMessageId =
+ conversationsStore.activeMessages.length > 0
+ ? conversationsStore.activeMessages[conversationsStore.activeMessages.length - 1].id
+ : undefined;
+ const assistantMessage = await this.createAssistantMessage(parentMessageId);
+ if (!assistantMessage) throw new Error('Failed to create assistant message');
+ conversationsStore.addMessageToActive(assistantMessage);
+ await this.streamChatCompletion(
+ conversationsStore.activeMessages.slice(0, -1),
+ assistantMessage
+ );
+ } catch (error) {
+ if (!this.isAbortError(error)) console.error('Failed to regenerate message:', error);
+ this.setChatLoading(activeConv?.id || '', false);
+ }
+ }
+
+ async getDeletionInfo(messageId: string): Promise<{
+ totalCount: number;
+ userMessages: number;
+ assistantMessages: number;
+ messageTypes: string[];
+ }> {
+ const activeConv = conversationsStore.activeConversation;
+ if (!activeConv)
+ return { totalCount: 0, userMessages: 0, assistantMessages: 0, messageTypes: [] };
+ const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
+ const descendants = findDescendantMessages(allMessages, messageId);
+ const allToDelete = [messageId, ...descendants];
+ const messagesToDelete = allMessages.filter((m) => allToDelete.includes(m.id));
+ let userMessages = 0,
+ assistantMessages = 0;
+ const messageTypes: string[] = [];
+ for (const msg of messagesToDelete) {
+ if (msg.role === 'user') {
+ userMessages++;
+ if (!messageTypes.includes('user message')) messageTypes.push('user message');
+ } else if (msg.role === 'assistant') {
+ assistantMessages++;
+ if (!messageTypes.includes('assistant response')) messageTypes.push('assistant response');
+ }
+ }
+ return { totalCount: allToDelete.length, userMessages, assistantMessages, messageTypes };
+ }
+
+ async deleteMessage(messageId: string): Promise<void> {
+ const activeConv = conversationsStore.activeConversation;
+ if (!activeConv) return;
+ try {
+ const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
+ const messageToDelete = allMessages.find((m) => m.id === messageId);
+ if (!messageToDelete) return;
+
+ const currentPath = filterByLeafNodeId(allMessages, activeConv.currNode || '', false);
+ const isInCurrentPath = currentPath.some((m) => m.id === messageId);
+
+ if (isInCurrentPath && messageToDelete.parent) {
+ const siblings = allMessages.filter(
+ (m) => m.parent === messageToDelete.parent && m.id !== messageId
+ );
+
+ if (siblings.length > 0) {
+ const latestSibling = siblings.reduce((latest, sibling) =>
+ sibling.timestamp > latest.timestamp ? sibling : latest
+ );
+ await conversationsStore.updateCurrentNode(findLeafNode(allMessages, latestSibling.id));
+ } else if (messageToDelete.parent) {
+ await conversationsStore.updateCurrentNode(
+ findLeafNode(allMessages, messageToDelete.parent)
+ );
+ }
+ }
+ await DatabaseService.deleteMessageCascading(activeConv.id, messageId);
+ await conversationsStore.refreshActiveMessages();
+
+ conversationsStore.updateConversationTimestamp();
+ } catch (error) {
+ console.error('Failed to delete message:', error);
+ }
+ }
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Editing
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ clearEditMode(): void {
+ this.isEditModeActive = false;
+ this.addFilesHandler = null;
+ }
+
+ async continueAssistantMessage(messageId: string): Promise<void> {
+ 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,
+ shouldBranch: boolean
+ ): Promise<void> {
+ const activeConv = conversationsStore.activeConversation;
+ if (!activeConv || this.isLoading) return;
+
+ const result = this.getMessageByIdWithRole(messageId, 'assistant');
+ if (!result) return;
+ const { message: msg, index: idx } = result;
+
+ try {
+ if (shouldBranch) {
+ const newMessage = await DatabaseService.createMessageBranch(
+ {
+ convId: msg.convId,
+ type: msg.type,
+ timestamp: Date.now(),
+ role: msg.role,
+ content: newContent,
+ thinking: msg.thinking || '',
+ toolCalls: msg.toolCalls || '',
+ children: [],
+ model: msg.model
+ },
+ msg.parent!
+ );
+ await conversationsStore.updateCurrentNode(newMessage.id);
+ } else {
+ await DatabaseService.updateMessage(msg.id, { content: newContent });
+ await conversationsStore.updateCurrentNode(msg.id);
+ conversationsStore.updateMessageAtIndex(idx, {
+ content: newContent
+ });
+ }
+ conversationsStore.updateConversationTimestamp();
+ await conversationsStore.refreshActiveMessages();
+ } catch (error) {
+ console.error('Failed to edit assistant message:', error);
+ }
+ }
+
+ async editUserMessagePreserveResponses(
+ messageId: string,
+ newContent: string,
+ newExtras?: DatabaseMessageExtra[]
+ ): Promise<void> {
+ const activeConv = conversationsStore.activeConversation;
+ if (!activeConv) return;
+
+ const result = this.getMessageByIdWithRole(messageId, 'user');
+ if (!result) return;
+ const { message: msg, index: idx } = result;
+
+ try {
+ const updateData: Partial<DatabaseMessage> = {
+ 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);
+
+ if (rootMessage && msg.parent === rootMessage.id && newContent.trim()) {
+ await conversationsStore.updateConversationTitleWithConfirmation(
+ activeConv.id,
+ newContent.trim(),
+ conversationsStore.titleUpdateConfirmationCallback
+ );
+ }
+ conversationsStore.updateConversationTimestamp();
+ } catch (error) {
+ console.error('Failed to edit user message:', error);
+ }
+ }
+
+ async editMessageWithBranching(
+ messageId: string,
+ newContent: string,
+ newExtras?: DatabaseMessageExtra[]
+ ): Promise<void> {
+ const activeConv = conversationsStore.activeConversation;
+ if (!activeConv || this.isLoading) return;
+
+ let result = this.getMessageByIdWithRole(messageId, 'user');
+
+ if (!result) {
+ result = this.getMessageByIdWithRole(messageId, 'system');
+ }
+
+ if (!result) return;
+ const { message: msg } = result;
+
+ try {
+ const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
+ const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
+ const isFirstUserMessage =
+ msg.role === 'user' && rootMessage && msg.parent === rootMessage.id;
+
+ 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,
+ type: msg.type,
+ timestamp: Date.now(),
+ role: msg.role,
+ content: newContent,
+ thinking: msg.thinking || '',
+ toolCalls: msg.toolCalls || '',
+ children: [],
+ extra: extrasToUse,
+ model: msg.model
+ },
+ parentId
+ );
+ await conversationsStore.updateCurrentNode(newMessage.id);
+ conversationsStore.updateConversationTimestamp();
+
+ if (isFirstUserMessage && newContent.trim()) {
+ await conversationsStore.updateConversationTitleWithConfirmation(
+ activeConv.id,
+ newContent.trim(),
+ conversationsStore.titleUpdateConfirmationCallback
+ );
+ }
+ await conversationsStore.refreshActiveMessages();
+
+ if (msg.role === 'user') {
+ await this.generateResponseForMessage(newMessage.id);
+ }
+ } catch (error) {
+ console.error('Failed to edit message with branching:', error);
+ }
+ }
+
+ async regenerateMessageWithBranching(messageId: string, modelOverride?: string): Promise<void> {
+ const activeConv = conversationsStore.activeConversation;
+ if (!activeConv || this.isLoading) return;
+ try {
+ const idx = conversationsStore.findMessageIndex(messageId);
+ if (idx === -1) return;
+ const msg = conversationsStore.activeMessages[idx];
+ if (msg.role !== 'assistant') return;
+
+ const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
+ const parentMessage = allMessages.find((m) => m.id === msg.parent);
+ if (!parentMessage) return;
+
+ this.setChatLoading(activeConv.id, true);
+ this.clearChatStreaming(activeConv.id);
+
+ const newAssistantMessage = await DatabaseService.createMessageBranch(
+ {
+ convId: activeConv.id,
+ type: 'text',
+ timestamp: Date.now(),
+ role: 'assistant',
+ content: '',
+ thinking: '',
+ toolCalls: '',
+ children: [],
+ model: null
+ },
+ parentMessage.id
+ );
+ await conversationsStore.updateCurrentNode(newAssistantMessage.id);
+ conversationsStore.updateConversationTimestamp();
+ await conversationsStore.refreshActiveMessages();
+
+ const conversationPath = filterByLeafNodeId(
+ allMessages,
+ parentMessage.id,
+ false
+ ) as DatabaseMessage[];
+ // Use modelOverride if provided, otherwise use the original message's model
+ // If neither is available, don't pass model (will use global selection)
+ const modelToUse = modelOverride || msg.model || undefined;
+ await this.streamChatCompletion(
+ conversationPath,
+ newAssistantMessage,
+ undefined,
+ undefined,
+ modelToUse
+ );
+ } catch (error) {
+ if (!this.isAbortError(error))
+ console.error('Failed to regenerate message with branching:', error);
+ this.setChatLoading(activeConv?.id || '', false);
+ }
+ }
+
+ private async generateResponseForMessage(userMessageId: string): Promise<void> {
+ const activeConv = conversationsStore.activeConversation;
+
+ if (!activeConv) return;
+
+ this.errorDialogState = null;
+ this.setChatLoading(activeConv.id, true);
+ this.clearChatStreaming(activeConv.id);
+
+ try {
+ const allMessages = await conversationsStore.getConversationMessages(activeConv.id);
+ const conversationPath = filterByLeafNodeId(
+ allMessages,
+ userMessageId,
+ false
+ ) as DatabaseMessage[];
+ const assistantMessage = await DatabaseService.createMessageBranch(
+ {
+ convId: activeConv.id,
+ type: 'text',
+ timestamp: Date.now(),
+ role: 'assistant',
+ content: '',
+ thinking: '',
+ toolCalls: '',
+ children: [],
+ model: null
+ },
+ userMessageId
+ );
+ conversationsStore.addMessageToActive(assistantMessage);
+ await this.streamChatCompletion(conversationPath, assistantMessage);
+ } catch (error) {
+ console.error('Failed to generate response:', error);
+ this.setChatLoading(activeConv.id, false);
+ }
+ }
+
+ getAddFilesHandler(): ((files: File[]) => void) | null {
+ return this.addFilesHandler;
+ }
+
+ public getAllLoadingChats(): string[] {
+ return Array.from(this.chatLoadingStates.keys());
+ }
+
+ public getAllStreamingChats(): string[] {
+ return Array.from(this.chatStreamingStates.keys());
+ }
+
+ public getChatStreamingPublic(
+ convId: string
+ ): { response: string; messageId: string } | undefined {
+ return this.getChatStreaming(convId);
+ }
+
+ public isChatLoadingPublic(convId: string): boolean {
+ return this.isChatLoading(convId);
+ }
+
+ isEditing(): boolean {
+ return this.isEditModeActive;
+ }
+
+ setEditModeActive(handler: (files: File[]) => void): void {
+ this.isEditModeActive = true;
+ this.addFilesHandler = handler;
+ }
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Utilities
+ // ─────────────────────────────────────────────────────────────────────────────
+
+ private getApiOptions(): Record<string, unknown> {
+ const currentConfig = config();
+ const hasValue = (value: unknown): boolean =>
+ value !== undefined && value !== null && value !== '';
+
+ const apiOptions: Record<string, unknown> = { stream: true, timings_per_token: true };
+
+ // Model selection (required in ROUTER mode)
+ if (isRouterMode()) {
+ const modelName = selectedModelName();
+ if (modelName) apiOptions.model = modelName;
+ }
+
+ // Config options needed by ChatService
+ if (currentConfig.systemMessage) apiOptions.systemMessage = currentConfig.systemMessage;
+ if (currentConfig.disableReasoningFormat) apiOptions.disableReasoningFormat = true;
+
+ if (hasValue(currentConfig.temperature))
+ apiOptions.temperature = Number(currentConfig.temperature);
+ if (hasValue(currentConfig.max_tokens))
+ apiOptions.max_tokens = Number(currentConfig.max_tokens);
+ if (hasValue(currentConfig.dynatemp_range))
+ apiOptions.dynatemp_range = Number(currentConfig.dynatemp_range);
+ if (hasValue(currentConfig.dynatemp_exponent))
+ apiOptions.dynatemp_exponent = Number(currentConfig.dynatemp_exponent);
+ if (hasValue(currentConfig.top_k)) apiOptions.top_k = Number(currentConfig.top_k);
+ if (hasValue(currentConfig.top_p)) apiOptions.top_p = Number(currentConfig.top_p);
+ if (hasValue(currentConfig.min_p)) apiOptions.min_p = Number(currentConfig.min_p);
+ if (hasValue(currentConfig.xtc_probability))
+ apiOptions.xtc_probability = Number(currentConfig.xtc_probability);
+ if (hasValue(currentConfig.xtc_threshold))
+ apiOptions.xtc_threshold = Number(currentConfig.xtc_threshold);
+ if (hasValue(currentConfig.typ_p)) apiOptions.typ_p = Number(currentConfig.typ_p);
+ if (hasValue(currentConfig.repeat_last_n))
+ apiOptions.repeat_last_n = Number(currentConfig.repeat_last_n);
+ if (hasValue(currentConfig.repeat_penalty))
+ apiOptions.repeat_penalty = Number(currentConfig.repeat_penalty);
+ if (hasValue(currentConfig.presence_penalty))
+ apiOptions.presence_penalty = Number(currentConfig.presence_penalty);
+ if (hasValue(currentConfig.frequency_penalty))
+ apiOptions.frequency_penalty = Number(currentConfig.frequency_penalty);
+ if (hasValue(currentConfig.dry_multiplier))
+ apiOptions.dry_multiplier = Number(currentConfig.dry_multiplier);
+ if (hasValue(currentConfig.dry_base)) apiOptions.dry_base = Number(currentConfig.dry_base);
+ if (hasValue(currentConfig.dry_allowed_length))
+ apiOptions.dry_allowed_length = Number(currentConfig.dry_allowed_length);
+ if (hasValue(currentConfig.dry_penalty_last_n))
+ apiOptions.dry_penalty_last_n = Number(currentConfig.dry_penalty_last_n);
+ if (currentConfig.samplers) apiOptions.samplers = currentConfig.samplers;
+ if (currentConfig.backend_sampling)
+ apiOptions.backend_sampling = currentConfig.backend_sampling;
+ if (currentConfig.custom) apiOptions.custom = currentConfig.custom;
+
+ return apiOptions;
+ }
+}
+
+export const chatStore = new ChatStore();
+
+export const activeProcessingState = () => chatStore.activeProcessingState;
+export const clearEditMode = () => chatStore.clearEditMode();
+export const currentResponse = () => chatStore.currentResponse;
+export const errorDialog = () => chatStore.errorDialogState;
+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);