refactor: Services/Stores syntax + logic improvements

Refactors components to access stores directly instead of using exported getter functions.

This change centralizes store access and logic, simplifying component code and improving maintainability by reducing the number of exported functions and promoting direct store interaction.

Removes exported getter functions from `chat.svelte.ts`, `conversations.svelte.ts`, `models.svelte.ts` and `settings.svelte.ts`.
This commit is contained in:
Aleksander Grygier 2025-11-27 13:44:49 +01:00
parent 69065ddc56
commit 6a3d6e79d2
29 changed files with 188 additions and 415 deletions

View File

@ -9,16 +9,9 @@
} from '$lib/components/app';
import { INPUT_CLASSES } from '$lib/constants/input-classes';
import { config } from '$lib/stores/settings.svelte';
import {
modelOptions,
selectedModelId,
isRouterMode,
fetchModelProps,
getModelProps,
modelSupportsVision,
modelSupportsAudio
} from '$lib/stores/models.svelte';
import { getConversationModel } from '$lib/stores/chat.svelte';
import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
import { chatStore } from '$lib/stores/chat.svelte';
import { activeMessages } from '$lib/stores/conversations.svelte';
import {
FileTypeCategory,
@ -77,7 +70,9 @@
let textareaRef: ChatFormTextarea | undefined = $state(undefined);
// Check if model is selected (in ROUTER mode)
let conversationModel = $derived(getConversationModel(activeMessages() as DatabaseMessage[]));
let conversationModel = $derived(
chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
);
let isRouter = $derived(isRouterMode());
let hasModelSelected = $derived(!isRouter || !!conversationModel || !!selectedModelId());
@ -109,9 +104,9 @@
// Fetch model props when active model changes
$effect(() => {
if (isRouter && activeModelId) {
const cached = getModelProps(activeModelId);
const cached = modelsStore.getModelProps(activeModelId);
if (!cached) {
fetchModelProps(activeModelId).then(() => {
modelsStore.fetchModelProps(activeModelId).then(() => {
modelPropsVersion++;
});
}
@ -122,7 +117,7 @@
let hasAudioModality = $derived.by(() => {
if (activeModelId) {
void modelPropsVersion; // Trigger reactivity on props fetch
return modelSupportsAudio(activeModelId);
return modelsStore.modelSupportsAudio(activeModelId);
}
return false;
});
@ -130,7 +125,7 @@
let hasVisionModality = $derived.by(() => {
if (activeModelId) {
void modelPropsVersion; // Trigger reactivity on props fetch
return modelSupportsVision(activeModelId);
return modelsStore.modelSupportsVision(activeModelId);
}
return false;
});

View File

@ -10,17 +10,9 @@
import { FileTypeCategory } from '$lib/enums';
import { getFileTypeCategory } from '$lib/utils/file-type';
import { config } from '$lib/stores/settings.svelte';
import {
modelOptions,
selectedModelId,
selectModelByName,
isRouterMode,
fetchModelProps,
getModelProps,
modelSupportsVision,
modelSupportsAudio
} from '$lib/stores/models.svelte';
import { getConversationModel } from '$lib/stores/chat.svelte';
import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
import { chatStore } from '$lib/stores/chat.svelte';
import { activeMessages } from '$lib/stores/conversations.svelte';
import type { ChatUploadedFile } from '$lib/types/chat';
@ -53,14 +45,16 @@
let currentConfig = $derived(config());
let isRouter = $derived(isRouterMode());
let conversationModel = $derived(getConversationModel(activeMessages() as DatabaseMessage[]));
let conversationModel = $derived(
chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
);
let previousConversationModel: string | null = null;
$effect(() => {
if (conversationModel && conversationModel !== previousConversationModel) {
previousConversationModel = conversationModel;
selectModelByName(conversationModel);
modelsStore.selectModelByName(conversationModel);
}
});
@ -92,10 +86,10 @@
$effect(() => {
if (isRouter && activeModelId) {
// Check if we already have cached props
const cached = getModelProps(activeModelId);
const cached = modelsStore.getModelProps(activeModelId);
if (!cached) {
// Fetch props for this model
fetchModelProps(activeModelId).then(() => {
modelsStore.fetchModelProps(activeModelId).then(() => {
// Trigger reactivity update
modelPropsVersion++;
});
@ -107,7 +101,7 @@
let hasAudioModality = $derived.by(() => {
if (activeModelId) {
void modelPropsVersion; // Trigger reactivity on props fetch
return modelSupportsAudio(activeModelId);
return modelsStore.modelSupportsAudio(activeModelId);
}
return false;
});
@ -115,7 +109,7 @@
let hasVisionModality = $derived.by(() => {
if (activeModelId) {
void modelPropsVersion; // Trigger reactivity on props fetch
return modelSupportsVision(activeModelId);
return modelsStore.modelSupportsVision(activeModelId);
}
return false;
});

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { getDeletionInfo } from '$lib/stores/chat.svelte';
import { chatStore } from '$lib/stores/chat.svelte';
import { copyToClipboard } from '$lib/utils/copy';
import { isIMEComposing } from '$lib/utils/is-ime-composing';
import type { ApiChatCompletionToolCall } from '$lib/types/api';
@ -98,7 +98,7 @@
}
async function handleDelete() {
deletionInfo = await getDeletionInfo(message.id);
deletionInfo = await chatStore.getDeletionInfo(message.id);
showDeleteDialog = true;
}

View File

@ -19,7 +19,7 @@
import Label from '$lib/components/ui/label/label.svelte';
import { config } from '$lib/stores/settings.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
import { selectModel } from '$lib/stores/models.svelte';
import { modelsStore } from '$lib/stores/models.svelte';
import { copyToClipboard } from '$lib/utils/copy';
import type { ApiChatCompletionToolCall } from '$lib/types/api';
@ -103,7 +103,7 @@
async function handleModelChange(modelId: string, modelName: string) {
try {
await selectModel(modelId);
await modelsStore.selectModelById(modelId);
// Pass the selected model name for regeneration
onRegenerate(modelName);

View File

@ -1,15 +1,8 @@
<script lang="ts">
import { ChatMessage } from '$lib/components/app';
import { DatabaseService } from '$lib/services/database';
import {
continueAssistantMessage,
deleteMessage,
editAssistantMessage,
editMessageWithBranching,
editUserMessagePreserveResponses,
regenerateMessageWithBranching
} from '$lib/stores/chat.svelte';
import { activeConversation, navigateToSibling } from '$lib/stores/conversations.svelte';
import { chatStore } from '$lib/stores/chat.svelte';
import { conversationsStore, activeConversation } from '$lib/stores/conversations.svelte';
import { getMessageSiblings } from '$lib/utils/branching';
interface Props {
@ -64,13 +57,13 @@
});
async function handleNavigateToSibling(siblingId: string) {
await navigateToSibling(siblingId);
await conversationsStore.navigateToSibling(siblingId);
}
async function handleEditWithBranching(message: DatabaseMessage, newContent: string) {
onUserAction?.();
await editMessageWithBranching(message.id, newContent);
await chatStore.editMessageWithBranching(message.id, newContent);
refreshAllMessages();
}
@ -82,7 +75,7 @@
) {
onUserAction?.();
await editAssistantMessage(message.id, newContent, shouldBranch);
await chatStore.editAssistantMessage(message.id, newContent, shouldBranch);
refreshAllMessages();
}
@ -90,7 +83,7 @@
async function handleRegenerateWithBranching(message: DatabaseMessage, modelOverride?: string) {
onUserAction?.();
await regenerateMessageWithBranching(message.id, modelOverride);
await chatStore.regenerateMessageWithBranching(message.id, modelOverride);
refreshAllMessages();
}
@ -98,7 +91,7 @@
async function handleContinueAssistantMessage(message: DatabaseMessage) {
onUserAction?.();
await continueAssistantMessage(message.id);
await chatStore.continueAssistantMessage(message.id);
refreshAllMessages();
}
@ -109,13 +102,13 @@
) {
onUserAction?.();
await editUserMessagePreserveResponses(message.id, newContent);
await chatStore.editUserMessagePreserveResponses(message.id, newContent);
refreshAllMessages();
}
async function handleDeleteMessage(message: DatabaseMessage) {
await deleteMessage(message.id);
await chatStore.deleteMessage(message.id);
refreshAllMessages();
}

View File

@ -16,30 +16,15 @@
AUTO_SCROLL_INTERVAL,
INITIAL_SCROLL_DELAY
} from '$lib/constants/auto-scroll';
import { chatStore, errorDialog, isLoading } from '$lib/stores/chat.svelte';
import {
dismissErrorDialog,
errorDialog,
isLoading,
sendMessage,
stopGeneration
} from '$lib/stores/chat.svelte';
import {
conversationsStore,
activeMessages,
activeConversation,
deleteConversation
activeConversation
} from '$lib/stores/conversations.svelte';
import { config } from '$lib/stores/settings.svelte';
import { serverLoading, serverError, serverStore } from '$lib/stores/server.svelte';
import {
modelOptions,
selectedModelId,
isRouterMode,
fetchModelProps,
getModelProps,
modelSupportsVision,
modelSupportsAudio
} from '$lib/stores/models.svelte';
import { getConversationModel } from '$lib/stores/chat.svelte';
import { serverLoading, serverError, serverStore, isRouterMode } from '$lib/stores/server.svelte';
import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
import { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra';
import { isFileTypeSupported } from '$lib/utils/file-type';
import { filterFilesByModalities } from '$lib/utils/modality-file-validation';
@ -93,7 +78,9 @@
// Model-specific capability detection (same logic as ChatFormActions)
let isRouter = $derived(isRouterMode());
let conversationModel = $derived(getConversationModel(activeMessages() as DatabaseMessage[]));
let conversationModel = $derived(
chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
);
// Get active model ID for fetching props
let activeModelId = $derived.by(() => {
@ -123,9 +110,9 @@
// Fetch model props when active model changes
$effect(() => {
if (isRouter && activeModelId) {
const cached = getModelProps(activeModelId);
const cached = modelsStore.getModelProps(activeModelId);
if (!cached) {
fetchModelProps(activeModelId).then(() => {
modelsStore.fetchModelProps(activeModelId).then(() => {
modelPropsVersion++;
});
}
@ -136,7 +123,7 @@
let hasAudioModality = $derived.by(() => {
if (activeModelId) {
void modelPropsVersion; // Trigger reactivity on props fetch
return modelSupportsAudio(activeModelId);
return modelsStore.modelSupportsAudio(activeModelId);
}
return false;
});
@ -144,7 +131,7 @@
let hasVisionModality = $derived.by(() => {
if (activeModelId) {
void modelPropsVersion; // Trigger reactivity on props fetch
return modelSupportsVision(activeModelId);
return modelsStore.modelSupportsVision(activeModelId);
}
return false;
});
@ -152,7 +139,7 @@
async function handleDeleteConfirm() {
const conversation = activeConversation();
if (conversation) {
await deleteConversation(conversation.id);
await conversationsStore.deleteConversation(conversation.id);
}
showDeleteDialog = false;
}
@ -175,7 +162,7 @@
function handleErrorDialogOpenChange(open: boolean) {
if (!open) {
dismissErrorDialog();
chatStore.dismissErrorDialog();
}
}
@ -262,7 +249,7 @@
userScrolledUp = false;
autoScrollEnabled = true;
}
await sendMessage(message, extras);
await chatStore.sendMessage(message, extras);
scrollChatToBottom();
return true;
@ -420,7 +407,7 @@
onFileRemove={handleFileRemove}
onFileUpload={handleFileUpload}
onSend={handleSendMessage}
onStop={() => stopGeneration()}
onStop={() => chatStore.stopGeneration()}
showHelperText={false}
bind:uploadedFiles
/>
@ -480,7 +467,7 @@
onFileRemove={handleFileRemove}
onFileUpload={handleFileUpload}
onSend={handleSendMessage}
onStop={() => stopGeneration()}
onStop={() => chatStore.stopGeneration()}
showHelperText={true}
bind:uploadedFiles
/>

View File

@ -2,13 +2,7 @@
import { untrack } from 'svelte';
import { PROCESSING_INFO_TIMEOUT } from '$lib/constants/processing-info';
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
import {
isLoading,
isChatStreaming,
clearProcessingState,
setActiveProcessingConversation,
restoreProcessingStateFromMessages
} from '$lib/stores/chat.svelte';
import { chatStore, isLoading, isChatStreaming } from '$lib/stores/chat.svelte';
import { activeMessages, activeConversation } from '$lib/stores/conversations.svelte';
import { config } from '$lib/stores/settings.svelte';
@ -26,7 +20,7 @@
$effect(() => {
const conversation = activeConversation();
untrack(() => setActiveProcessingConversation(conversation?.id ?? null));
untrack(() => chatStore.setActiveProcessingConversation(conversation?.id ?? null));
});
$effect(() => {
@ -55,12 +49,12 @@
if (keepStatsVisible && conversation) {
if (messages.length === 0) {
untrack(() => clearProcessingState(conversation.id));
untrack(() => chatStore.clearProcessingState(conversation.id));
return;
}
if (!isCurrentConversationLoading && !isStreaming) {
untrack(() => restoreProcessingStateFromMessages(messages, conversation.id));
untrack(() => chatStore.restoreProcessingStateFromMessages(messages, conversation.id));
}
}
});

View File

@ -17,7 +17,7 @@
ChatSettingsFields
} from '$lib/components/app';
import { ScrollArea } from '$lib/components/ui/scroll-area';
import { config, updateMultipleConfig } from '$lib/stores/settings.svelte';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { setMode } from 'mode-watcher';
import type { Component } from 'svelte';
@ -333,7 +333,7 @@
}
}
updateMultipleConfig(processedConfig);
settingsStore.updateMultipleConfig(processedConfig);
onSave?.();
}

View File

@ -7,7 +7,7 @@
import { Textarea } from '$lib/components/ui/textarea';
import { SETTING_CONFIG_DEFAULT, SETTING_CONFIG_INFO } from '$lib/constants/settings-config';
import { supportsVision } from '$lib/stores/server.svelte';
import { getParameterInfo, resetParameterToServerDefault } from '$lib/stores/settings.svelte';
import { settingsStore } from '$lib/stores/settings.svelte';
import { ParameterSyncService } from '$lib/services/parameter-sync';
import { ChatSettingsParameterSourceIndicator } from '$lib/components/app';
import type { Component } from 'svelte';
@ -27,7 +27,7 @@
return null;
}
return getParameterInfo(key);
return settingsStore.getParameterInfo(key);
}
</script>
@ -82,7 +82,7 @@
<button
type="button"
onclick={() => {
resetParameterToServerDefault(field.key);
settingsStore.resetParameterToServerDefault(field.key);
// Trigger UI update by calling onConfigChange with the default value
const defaultValue = propsDefault ?? SETTING_CONFIG_DEFAULT[field.key];
onConfigChange(field.key, String(defaultValue));
@ -175,7 +175,7 @@
<button
type="button"
onclick={() => {
resetParameterToServerDefault(field.key);
settingsStore.resetParameterToServerDefault(field.key);
// Trigger UI update by calling onConfigChange with the default value
const defaultValue = propsDefault ?? SETTING_CONFIG_DEFAULT[field.key];
onConfigChange(field.key, String(defaultValue));

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { forceSyncWithServerDefaults } from '$lib/stores/settings.svelte';
import { settingsStore } from '$lib/stores/settings.svelte';
import { RotateCcw } from '@lucide/svelte';
interface Props {
@ -18,7 +18,7 @@
}
function handleConfirmReset() {
forceSyncWithServerDefaults();
settingsStore.forceSyncWithServerDefaults();
onReset?.();
showResetDialog = false;

View File

@ -7,11 +7,7 @@
import * as Sidebar from '$lib/components/ui/sidebar';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import Input from '$lib/components/ui/input/input.svelte';
import {
conversations,
deleteConversation,
updateConversationName
} from '$lib/stores/conversations.svelte';
import { conversationsStore, conversations } from '$lib/stores/conversations.svelte';
import ChatSidebarActions from './ChatSidebarActions.svelte';
const sidebar = Sidebar.useSidebar();
@ -56,7 +52,7 @@
showDeleteDialog = false;
setTimeout(() => {
deleteConversation(selectedConversation.id);
conversationsStore.deleteConversation(selectedConversation.id);
selectedConversation = null;
}, 100); // Wait for animation to finish
}
@ -67,7 +63,7 @@
showEditDialog = false;
updateConversationName(selectedConversation.id, editedName);
conversationsStore.updateConversationName(selectedConversation.id, editedName);
selectedConversation = null;
}

View File

@ -2,7 +2,7 @@
import { Trash2, Pencil, MoreHorizontal, Download, Loader2 } from '@lucide/svelte';
import { ActionDropdown } from '$lib/components/app';
import { getAllLoadingChats } from '$lib/stores/chat.svelte';
import { downloadConversation } from '$lib/stores/conversations.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { onMount } from 'svelte';
interface Props {
@ -115,7 +115,7 @@
label: 'Export',
onclick: (e) => {
e.stopPropagation();
downloadConversation(conversation.id);
conversationsStore.downloadConversation(conversation.id);
},
shortcut: ['shift', 'cmd', 's']
},

View File

@ -4,15 +4,12 @@
import { cn } from '$lib/components/ui/utils';
import { portalToBody } from '$lib/utils/portal-to-body';
import {
fetchModels,
modelsStore,
modelOptions,
modelsLoading,
modelsUpdating,
selectModel,
selectedModelId,
unloadModel,
routerModels,
loadModel
routerModels
} from '$lib/stores/models.svelte';
import { ServerModelStatus } from '$lib/enums';
import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
@ -89,7 +86,7 @@
onMount(async () => {
try {
await fetchModels();
await modelsStore.fetch();
} catch (error) {
console.error('Unable to load models:', error);
}
@ -261,13 +258,13 @@
onModelChange(option.id, option.model);
} else {
// Update global selection
await selectModel(option.id);
await modelsStore.selectModelById(option.id);
}
// Load the model if not already loaded (router mode)
if (isRouter && getModelStatus(option.model) !== ServerModelStatus.LOADED) {
try {
await loadModel(option.model);
await modelsStore.loadModel(option.model);
} catch (error) {
console.error('Failed to load model:', error);
}
@ -438,7 +435,7 @@
class="relative ml-2 flex h-4 w-4 shrink-0 items-center justify-center"
onclick={(e) => {
e.stopPropagation();
unloadModel(option.model);
modelsStore.unloadModel(option.model);
}}
title="Unload model"
>

View File

@ -5,7 +5,7 @@
import { Input } from '$lib/components/ui/input';
import Label from '$lib/components/ui/label/label.svelte';
import { serverStore, serverLoading } from '$lib/stores/server.svelte';
import { config, updateConfig } from '$lib/stores/settings.svelte';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { fade, fly, scale } from 'svelte/transition';
interface Props {
@ -61,7 +61,7 @@
try {
// Update the API key in settings first
updateConfig('apiKey', apiKeyInput.trim());
settingsStore.updateConfig('apiKey', apiKeyInput.trim());
// Test the API key by making a real request to the server
const response = await fetch('./props', {

View File

@ -14,7 +14,7 @@ export interface UseProcessingStateReturn {
* useProcessingState - Reactive processing state hook
*
* This hook provides reactive access to the processing state of the server.
* It directly reads from ChatStore's reactive state and provides
* It directly reads from chatStore's reactive state and provides
* formatted processing details for UI display.
*
* **Features:**
@ -30,7 +30,7 @@ export function useProcessingState(): UseProcessingStateReturn {
let isMonitoring = $state(false);
let lastKnownState = $state<ApiProcessingState | null>(null);
// Derive processing state reactively from ChatStore's direct state
// Derive processing state reactively from chatStore's direct state
const processingState = $derived.by(() => {
if (!isMonitoring) {
return lastKnownState;

View File

@ -46,8 +46,8 @@ import type { SettingsChatServiceOptions } from '$lib/types/settings';
* - Converts database messages to API format
* - Handles error translation for server responses
*
* - **ChatStore**: Uses ChatService for all AI model communication
* - **ConversationsStore**: Provides message context for API requests
* - **chatStore**: Uses ChatService for all AI model communication
* - **conversationsStore**: Provides message context for API requests
*
* **Key Responsibilities:**
* - Message format conversion (DatabaseMessage API format)
@ -67,7 +67,7 @@ export class ChatService {
* @returns {Promise<string | void>} that resolves to the complete response string (non-streaming) or void (streaming)
* @throws {Error} if the request fails or is aborted
*/
async sendMessage(
static async sendMessage(
messages: ApiChatMessageData[] | (DatabaseMessage & { extra?: DatabaseMessageExtra[] })[],
options: SettingsChatServiceOptions = {},
conversationId?: string,
@ -130,7 +130,7 @@ export class ChatService {
return true;
});
const processedMessages = this.injectSystemMessage(normalizedMessages);
const processedMessages = ChatService.injectSystemMessage(normalizedMessages);
const requestBody: ApiChatCompletionRequest = {
messages: processedMessages.map((msg: ApiChatMessageData) => ({
@ -200,7 +200,7 @@ export class ChatService {
});
if (!response.ok) {
const error = await this.parseErrorResponse(response);
const error = await ChatService.parseErrorResponse(response);
if (onError) {
onError(error);
}
@ -208,7 +208,7 @@ export class ChatService {
}
if (stream) {
await this.handleStreamResponse(
await ChatService.handleStreamResponse(
response,
onChunk,
onComplete,
@ -222,7 +222,7 @@ export class ChatService {
);
return;
} else {
return this.handleNonStreamResponse(
return ChatService.handleNonStreamResponse(
response,
onComplete,
onError,
@ -276,7 +276,7 @@ export class ChatService {
* @returns {Promise<void>} Promise that resolves when streaming is complete
* @throws {Error} if the stream cannot be read or parsed
*/
private async handleStreamResponse(
private static async handleStreamResponse(
response: Response,
onChunk?: (chunk: string) => void,
onComplete?: (
@ -323,7 +323,7 @@ export class ChatService {
return;
}
aggregatedToolCalls = this.mergeToolCallDeltas(
aggregatedToolCalls = ChatService.mergeToolCallDeltas(
aggregatedToolCalls,
toolCalls,
toolCallIndexOffset
@ -378,14 +378,14 @@ export class ChatService {
const timings = parsed.timings;
const promptProgress = parsed.prompt_progress;
const chunkModel = this.extractModelName(parsed);
const chunkModel = ChatService.extractModelName(parsed);
if (chunkModel && !modelEmitted) {
modelEmitted = true;
onModel?.(chunkModel);
}
if (timings || promptProgress) {
this.notifyTimings(timings, promptProgress, onTimings);
ChatService.notifyTimings(timings, promptProgress, onTimings);
if (timings) {
lastTimings = timings;
}
@ -453,7 +453,7 @@ export class ChatService {
* @returns {Promise<string>} Promise that resolves to the generated content string
* @throws {Error} if the response cannot be parsed or is malformed
*/
private async handleNonStreamResponse(
private static async handleNonStreamResponse(
response: Response,
onComplete?: (
response: string,
@ -475,7 +475,7 @@ export class ChatService {
const data: ApiChatCompletionResponse = JSON.parse(responseText);
const responseModel = this.extractModelName(data);
const responseModel = ChatService.extractModelName(data);
if (responseModel) {
onModel?.(responseModel);
}
@ -491,7 +491,7 @@ export class ChatService {
let serializedToolCalls: string | undefined;
if (toolCalls && toolCalls.length > 0) {
const mergedToolCalls = this.mergeToolCallDeltas([], toolCalls);
const mergedToolCalls = ChatService.mergeToolCallDeltas([], toolCalls);
if (mergedToolCalls.length > 0) {
serializedToolCalls = JSON.stringify(mergedToolCalls);
@ -527,7 +527,7 @@ export class ChatService {
* @param indexOffset - Optional offset to apply to the index of new tool calls
* @returns {ApiChatCompletionToolCall[]} The merged array of tool calls
*/
private mergeToolCallDeltas(
private static mergeToolCallDeltas(
existing: ApiChatCompletionToolCall[],
deltas: ApiChatCompletionToolCallDelta[],
indexOffset = 0
@ -736,7 +736,7 @@ export class ChatService {
* @returns Array of messages with system message injected at the beginning if configured
* @private
*/
private injectSystemMessage(messages: ApiChatMessageData[]): ApiChatMessageData[] {
private static injectSystemMessage(messages: ApiChatMessageData[]): ApiChatMessageData[] {
const currentConfig = config();
const systemMessage = currentConfig.systemMessage?.toString().trim();
@ -770,7 +770,7 @@ export class ChatService {
* @param response - HTTP response object
* @returns Promise<Error> - Parsed error with context info if available
*/
private async parseErrorResponse(response: Response): Promise<Error> {
private static async parseErrorResponse(response: Response): Promise<Error> {
try {
const errorText = await response.text();
const errorData: ApiErrorResponse = JSON.parse(errorText);
@ -798,7 +798,7 @@ export class ChatService {
* @returns Model name string if found, undefined otherwise
* @private
*/
private extractModelName(data: unknown): string | undefined {
private static extractModelName(data: unknown): string | undefined {
// WORKAROUND: In single model mode, use model name from props instead of API response
// because llama-server returns `gpt-3.5-turbo` value in the `model` field
const isRouter = isRouterMode();
@ -849,7 +849,7 @@ export class ChatService {
* @param onTimingsCallback - Callback function to invoke with timing data
* @private
*/
private notifyTimings(
private static notifyTimings(
timings: ChatMessageTimings | undefined,
promptProgress: ChatMessagePromptProgress | undefined,
onTimingsCallback:
@ -860,5 +860,3 @@ export class ChatService {
onTimingsCallback(timings, promptProgress);
}
}
export const chatService = new ChatService();

View File

@ -42,12 +42,12 @@ import { v4 as uuid } from 'uuid';
* - Uses DatabaseService for all persistence operations
* - Adds import/export, navigation, and higher-level operations
*
* - **ConversationsStore**: Reactive state management for conversations
* - **conversationsStore**: Reactive state management for conversations
* - Uses ConversationsService for database operations
* - Manages conversation list, active conversation, and messages in memory
*
* - **ChatStore**: Active AI interaction management
* - Uses ConversationsStore for conversation context
* - **chatStore**: Active AI interaction management
* - Uses conversationsStore for conversation context
* - Directly uses DatabaseService for message CRUD during streaming
*
* **Key Features:**

View File

@ -1,2 +1,5 @@
export { chatService } from './chat';
export { ChatService } from './chat';
export { DatabaseService } from './database';
export { ModelsService } from './models';
export { PropsService } from './props';
export { ParameterSyncService } from './parameter-sync';

View File

@ -23,7 +23,7 @@ import type {
* - Check model status (ROUTER mode)
*
* **Used by:**
* - ModelsStore: Primary consumer for model state management
* - modelsStore: Primary consumer for model state management
*/
export class ModelsService {
// ─────────────────────────────────────────────────────────────────────────────

View File

@ -12,7 +12,7 @@ import { getAuthHeaders } from '$lib/utils/api-headers';
* - Parse and validate server response
*
* **Used by:**
* - ServerStore: Primary consumer for server state management
* - serverStore: Primary consumer for server state management
*/
export class PropsService {
/**

View File

@ -1,5 +1,4 @@
import { DatabaseService } from '$lib/services/database';
import { chatService } from '$lib/services';
import { DatabaseService, ChatService } from '$lib/services';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { config } from '$lib/stores/settings.svelte';
import { contextSize } from '$lib/stores/server.svelte';
@ -16,27 +15,27 @@ import type {
import type { DatabaseMessage, DatabaseMessageExtra } from '$lib/types/database';
/**
* ChatStore - Active AI interaction and streaming state management
* 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.
* 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
* - **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
* - Coordinates with conversationsStore for persistence
*
* - **ConversationsStore**: Provides conversation data and message arrays for chat context
* - **conversationsStore**: Provides conversation data and message arrays for chat context
* - **ChatService**: Low-level API communication with llama.cpp server
* - **DatabaseService**: Message persistence and retrieval
*
@ -501,7 +500,7 @@ class ChatStore {
const abortController = this.getOrCreateAbortController(assistantMessage.convId);
await chatService.sendMessage(
await ChatService.sendMessage(
allMessages,
{
...this.getApiOptions(),
@ -1140,7 +1139,7 @@ class ChatStore {
const abortController = this.getOrCreateAbortController(msg.convId);
await chatService.sendMessage(
await ChatService.sendMessage(
contextWithContinue,
{
...this.getApiOptions(),
@ -1244,8 +1243,6 @@ class ChatStore {
}
}
// ============ Public Methods for Per-Conversation State ============
public isChatLoadingPublic(convId: string): boolean {
return this.isChatLoading(convId);
}
@ -1264,57 +1261,13 @@ class ChatStore {
export const chatStore = new ChatStore();
// ChatStore state exports
export const isLoading = () => chatStore.isLoading;
export const currentResponse = () => chatStore.currentResponse;
export const errorDialog = () => chatStore.errorDialogState;
export const activeProcessingState = () => chatStore.activeProcessingState;
export const isChatStreaming = () => chatStore.isStreaming();
// ChatStore method exports
export const sendMessage = chatStore.sendMessage.bind(chatStore);
export const dismissErrorDialog = chatStore.dismissErrorDialog.bind(chatStore);
export function stopGeneration() {
chatStore.stopGeneration();
}
// Message operations
export const updateMessage = chatStore.updateMessage.bind(chatStore);
export const regenerateMessage = chatStore.regenerateMessage.bind(chatStore);
export const deleteMessage = chatStore.deleteMessage.bind(chatStore);
export const getDeletionInfo = chatStore.getDeletionInfo.bind(chatStore);
// Branching operations
export const editAssistantMessage = chatStore.editAssistantMessage.bind(chatStore);
export const editUserMessagePreserveResponses =
chatStore.editUserMessagePreserveResponses.bind(chatStore);
export const editMessageWithBranching = chatStore.editMessageWithBranching.bind(chatStore);
export const regenerateMessageWithBranching =
chatStore.regenerateMessageWithBranching.bind(chatStore);
export const continueAssistantMessage = chatStore.continueAssistantMessage.bind(chatStore);
// Per-conversation state access
export const isChatLoading = (convId: string) => chatStore.isChatLoadingPublic(convId);
export const getChatStreaming = (convId: string) => chatStore.getChatStreamingPublic(convId);
export const getAllLoadingChats = () => chatStore.getAllLoadingChats();
export const getAllStreamingChats = () => chatStore.getAllStreamingChats();
// Sync/clear UI state when switching conversations
export const syncLoadingStateForChat = chatStore.syncLoadingStateForChat.bind(chatStore);
export const clearUIState = chatStore.clearUIState.bind(chatStore);
// Processing state (timing/context info)
export const getProcessingState = chatStore.getProcessingState.bind(chatStore);
export const getActiveProcessingState = chatStore.getActiveProcessingState.bind(chatStore);
export const activeProcessingState = () => chatStore.activeProcessingState;
export const getCurrentProcessingStateSync =
chatStore.getCurrentProcessingStateSync.bind(chatStore);
export const restoreProcessingStateFromMessages =
chatStore.restoreProcessingStateFromMessages.bind(chatStore);
export const clearProcessingState = chatStore.clearProcessingState.bind(chatStore);
export const updateProcessingStateFromTimings =
chatStore.updateProcessingStateFromTimings.bind(chatStore);
export const setActiveProcessingConversation =
chatStore.setActiveProcessingConversation.bind(chatStore);
export const isChatStreaming = () => chatStore.isStreaming();
// Model detection
export const getConversationModel = chatStore.getConversationModel.bind(chatStore);

View File

@ -11,12 +11,12 @@ import type {
} from '$lib/types/database';
/**
* ConversationsStore - Persistent conversation data and lifecycle management
* conversationsStore - Persistent conversation data and lifecycle 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.
* Managed by ChatStore, a "chat" is ephemeral and exists during active AI interactions.
* Managed by chatStore, a "chat" is ephemeral and exists during active AI interactions.
* - **Conversation**: The persistent database entity storing all messages and metadata.
* A "conversation" survives across sessions, page reloads, and browser restarts.
* It contains the complete message history, branching structure, and conversation metadata.
@ -26,13 +26,13 @@ import type {
* conversation with its message history, providing reactive state for UI components.
*
* **Architecture & Relationships:**
* - **ConversationsStore** (this class): Persistent conversation data management
* - **conversationsStore** (this class): Persistent conversation data management
* - Manages conversation list and active conversation state
* - Handles conversation CRUD operations via DatabaseService
* - Maintains active message array for current conversation
* - Coordinates branching navigation (currNode tracking)
*
* - **ChatStore**: Uses conversation data as context for active AI streaming
* - **chatStore**: Uses conversation data as context for active AI streaming
* - **DatabaseService**: Low-level IndexedDB storage for conversations and messages
*
* **Key Features:**
@ -148,7 +148,7 @@ class ConversationsStore {
clearActiveConversation(): void {
this.activeConversation = null;
this.activeMessages = [];
// Active processing conversation is now managed by ChatStore
// Active processing conversation is now managed by chatStore
}
/**
@ -448,7 +448,7 @@ class ConversationsStore {
/**
* Adds a message to the active messages array
* Used by ChatStore when creating new messages
* Used by chatStore when creating new messages
* @param message - The message to add
*/
addMessageToActive(message: DatabaseMessage): void {
@ -499,6 +499,8 @@ class ConversationsStore {
/**
* Triggers file download in browser
* @param data - The data to download
* @param filename - Optional filename for the download
*/
private triggerDownload(data: ExportedConversations, filename?: string): void {
const conversation =
@ -531,27 +533,7 @@ class ConversationsStore {
export const conversationsStore = new ConversationsStore();
// Export getter functions for reactive access
export const conversations = () => conversationsStore.conversations;
export const activeConversation = () => conversationsStore.activeConversation;
export const activeMessages = () => conversationsStore.activeMessages;
export const isConversationsInitialized = () => conversationsStore.isInitialized;
// Export conversation operations
export const createConversation = conversationsStore.createConversation.bind(conversationsStore);
export const loadConversation = conversationsStore.loadConversation.bind(conversationsStore);
export const deleteConversation = conversationsStore.deleteConversation.bind(conversationsStore);
export const clearActiveConversation =
conversationsStore.clearActiveConversation.bind(conversationsStore);
export const updateConversationName =
conversationsStore.updateConversationName.bind(conversationsStore);
export const downloadConversation =
conversationsStore.downloadConversation.bind(conversationsStore);
export const exportAllConversations =
conversationsStore.exportAllConversations.bind(conversationsStore);
export const importConversations = conversationsStore.importConversations.bind(conversationsStore);
export const navigateToSibling = conversationsStore.navigateToSibling.bind(conversationsStore);
export const refreshActiveMessages =
conversationsStore.refreshActiveMessages.bind(conversationsStore);
export const setTitleUpdateConfirmationCallback =
conversationsStore.setTitleUpdateConfirmationCallback.bind(conversationsStore);

View File

@ -1,12 +1,13 @@
import { SvelteSet } from 'svelte/reactivity';
import { ModelsService } from '$lib/services/models';
import { PropsService } from '$lib/services/props';
import { ServerModelStatus, ServerRole } from '$lib/enums';
import { ServerModelStatus } from '$lib/enums';
import { serverStore } from '$lib/stores/server.svelte';
import type { ModelOption, ModelModalities } from '$lib/types/models';
import type { ApiModelDataEntry } from '$lib/types/api';
/**
* ModelsStore - Reactive store for model management in both MODEL and ROUTER modes
* modelsStore - Reactive store for model management in both MODEL and ROUTER modes
*
* This store manages:
* - Available models list
@ -18,8 +19,8 @@ import type { ApiModelDataEntry } from '$lib/types/api';
* **Architecture & Relationships:**
* - **ModelsService**: Stateless service for model API communication
* - **PropsService**: Stateless service for props/modalities fetching
* - **ModelsStore** (this class): Reactive store for model state
* - **ConversationsStore**: Tracks which conversations use which models
* - **modelsStore** (this class): Reactive store for model state
* - **conversationsStore**: Tracks which conversations use which models
*
* **API Inconsistency Workaround:**
* In MODEL mode, `/props` returns modalities for the single model.
@ -48,13 +49,6 @@ class ModelsStore {
private modelUsage = $state<Map<string, SvelteSet<string>>>(new Map());
private modelLoadingStates = $state<Map<string, boolean>>(new Map());
/**
* Server role detection - determines API behavior
* In ROUTER mode, modalities come from /props?model=<id>
* In MODEL mode, modalities come from /props (single model)
*/
serverRole = $state<ServerRole | null>(null);
/**
* Model-specific props cache
* Key: modelId, Value: props data including modalities
@ -83,14 +77,6 @@ class ModelsStore {
.map(([id]) => id);
}
get isRouterMode(): boolean {
return this.serverRole === ServerRole.ROUTER;
}
get isModelMode(): boolean {
return this.serverRole === ServerRole.MODEL;
}
// ─────────────────────────────────────────────────────────────────────────────
// Methods - Model Modalities
// ─────────────────────────────────────────────────────────────────────────────
@ -189,10 +175,10 @@ class ModelsStore {
this.error = null;
try {
// Fetch server props to detect role and get modalities for MODEL mode
const serverProps = await PropsService.fetch();
this.serverRole =
serverProps.role === ServerRole.ROUTER ? ServerRole.ROUTER : ServerRole.MODEL;
// Ensure server props are loaded (for role detection and MODEL mode modalities)
if (!serverStore.props) {
await serverStore.fetch();
}
const response = await ModelsService.list();
@ -216,10 +202,11 @@ class ModelsStore {
this.models = models;
// In MODEL mode, populate modalities from /props (single model)
// In MODEL mode, populate modalities from serverStore.props (single model)
// WORKAROUND: In MODEL mode, /props returns modalities for the single model,
// but /v1/models doesn't include modalities. We bridge this gap here.
if (this.isModelMode && this.models.length > 0 && serverProps.modalities) {
const serverProps = serverStore.props;
if (serverStore.isModelMode && this.models.length > 0 && serverProps?.modalities) {
const modalities: ModelModalities = {
vision: serverProps.modalities.vision ?? false,
audio: serverProps.modalities.audio ?? false
@ -347,7 +334,7 @@ class ModelsStore {
/**
* Select a model for new conversations
*/
async select(modelId: string): Promise<void> {
async selectModelById(modelId: string): Promise<void> {
if (!modelId || this.updating) return;
if (this.selectedModelId === modelId) return;
@ -581,7 +568,6 @@ class ModelsStore {
this.error = null;
this.selectedModelId = null;
this.selectedModelName = null;
this.serverRole = null;
this.modelUsage.clear();
this.modelLoadingStates.clear();
this.modelPropsCache.clear();
@ -591,10 +577,6 @@ class ModelsStore {
export const modelsStore = new ModelsStore();
// ─────────────────────────────────────────────────────────────────────────────
// Reactive Getters
// ─────────────────────────────────────────────────────────────────────────────
export const modelOptions = () => modelsStore.models;
export const routerModels = () => modelsStore.routerModels;
export const modelsLoading = () => modelsStore.loading;
@ -605,34 +587,3 @@ export const selectedModelName = () => modelsStore.selectedModelName;
export const selectedModelOption = () => modelsStore.selectedModel;
export const loadedModelIds = () => modelsStore.loadedModelIds;
export const loadingModelIds = () => modelsStore.loadingModelIds;
export const isRouterMode = () => modelsStore.isRouterMode;
export const isModelMode = () => modelsStore.isModelMode;
// ─────────────────────────────────────────────────────────────────────────────
// Actions
// ─────────────────────────────────────────────────────────────────────────────
export const fetchModels = modelsStore.fetch.bind(modelsStore);
export const fetchRouterModels = modelsStore.fetchRouterModels.bind(modelsStore);
export const fetchModalitiesForLoadedModels =
modelsStore.fetchModalitiesForLoadedModels.bind(modelsStore);
export const updateModelModalities = modelsStore.updateModelModalities.bind(modelsStore);
export const selectModel = modelsStore.select.bind(modelsStore);
export const loadModel = modelsStore.loadModel.bind(modelsStore);
export const unloadModel = modelsStore.unloadModel.bind(modelsStore);
export const ensureModelLoaded = modelsStore.ensureModelLoaded.bind(modelsStore);
export const registerModelUsage = modelsStore.registerModelUsage.bind(modelsStore);
export const unregisterModelUsage = modelsStore.unregisterModelUsage.bind(modelsStore);
export const clearConversationUsage = modelsStore.clearConversationUsage.bind(modelsStore);
export const selectModelByName = modelsStore.selectModelByName.bind(modelsStore);
export const clearModelSelection = modelsStore.clearSelection.bind(modelsStore);
export const findModelByName = modelsStore.findModelByName.bind(modelsStore);
export const findModelById = modelsStore.findModelById.bind(modelsStore);
export const hasModel = modelsStore.hasModel.bind(modelsStore);
// Model modalities
export const getModelModalities = modelsStore.getModelModalities.bind(modelsStore);
export const modelSupportsVision = modelsStore.modelSupportsVision.bind(modelsStore);
export const modelSupportsAudio = modelsStore.modelSupportsAudio.bind(modelsStore);
export const fetchModelProps = modelsStore.fetchModelProps.bind(modelsStore);
export const getModelProps = modelsStore.getModelProps.bind(modelsStore);

View File

@ -2,15 +2,15 @@ import { PropsService } from '$lib/services/props';
import { ServerRole, ModelModality } from '$lib/enums';
/**
* ServerStore - Server connection state, configuration, and role detection
* serverStore - Server connection state, configuration, and role detection
*
* This store manages the server connection state and properties fetched from `/props`.
* It provides reactive state for server configuration and role detection.
*
* **Architecture & Relationships:**
* - **PropsService**: Stateless service for fetching `/props` data
* - **ServerStore** (this class): Reactive store for server state
* - **ModelsStore**: Independent store for model management (uses PropsService directly)
* - **serverStore** (this class): Reactive store for server state
* - **modelsStore**: Independent store for model management (uses PropsService directly)
*
* **Key Features:**
* - **Server State**: Connection status, loading, error handling
@ -18,7 +18,7 @@ import { ServerRole, ModelModality } from '$lib/enums';
* - **Default Params**: Server-wide generation defaults
*
* **Note on Modalities:**
* Model-specific modalities (vision, audio) are now managed by ModelsStore.
* Model-specific modalities (vision, audio) are now managed by modelsStore.
* Use `modelsStore.getModelModalities(modelId)` for per-model modality info.
* The `supportsVision`/`supportsAudio` getters here are deprecated and only
* apply to MODEL mode (single model).
@ -30,10 +30,6 @@ class ServerStore {
role = $state<ServerRole | null>(null);
private fetchPromise: Promise<void> | null = null;
// ─────────────────────────────────────────────────────────────────────────────
// Computed Getters
// ─────────────────────────────────────────────────────────────────────────────
/**
* Get model name from server props.
* In MODEL mode: extracts from model_path or model_alias
@ -93,10 +89,6 @@ class ServerStore {
return this.role === ServerRole.MODEL;
}
// ─────────────────────────────────────────────────────────────────────────────
// Server Role Detection
// ─────────────────────────────────────────────────────────────────────────────
private detectRole(props: ApiLlamaCppServerProps): void {
const newRole = props?.role === ServerRole.ROUTER ? ServerRole.ROUTER : ServerRole.MODEL;
if (this.role !== newRole) {
@ -105,10 +97,6 @@ class ServerStore {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Fetch Server Properties
// ─────────────────────────────────────────────────────────────────────────────
async fetch(): Promise<void> {
if (this.fetchPromise) return this.fetchPromise;
@ -134,10 +122,6 @@ class ServerStore {
await fetchPromise;
}
// ─────────────────────────────────────────────────────────────────────────────
// Error Handling
// ─────────────────────────────────────────────────────────────────────────────
private getErrorMessage(error: unknown): string {
if (error instanceof Error) {
const message = error.message || '';
@ -163,11 +147,6 @@ class ServerStore {
return 'Failed to connect to server';
}
// ─────────────────────────────────────────────────────────────────────────────
// Clear State
// ─────────────────────────────────────────────────────────────────────────────
clear(): void {
this.props = null;
this.error = null;
@ -179,10 +158,6 @@ class ServerStore {
export const serverStore = new ServerStore();
// ─────────────────────────────────────────────────────────────────────────────
// Reactive Getters (for use in components)
// ─────────────────────────────────────────────────────────────────────────────
export const serverProps = () => serverStore.props;
export const serverLoading = () => serverStore.loading;
export const serverError = () => serverStore.error;
@ -196,6 +171,3 @@ export const defaultParams = () => serverStore.defaultParams;
export const contextSize = () => serverStore.contextSize;
export const isRouterMode = () => serverStore.isRouterMode;
export const isModelMode = () => serverStore.isModelMode;
// Actions
export const fetchServerProps = serverStore.fetch.bind(serverStore);

View File

@ -1,12 +1,12 @@
/**
* SettingsStore - Application configuration and theme management
* settingsStore - Application configuration and theme management
*
* This store manages all application settings including AI model parameters, UI preferences,
* and theme configuration. It provides persistent storage through localStorage with reactive
* state management using Svelte 5 runes.
*
* **Architecture & Relationships:**
* - **SettingsStore** (this class): Configuration state management
* - **settingsStore** (this class): Configuration state management
* - Manages AI model parameters (temperature, max tokens, etc.)
* - Handles theme switching and persistence
* - Provides localStorage synchronization
@ -378,28 +378,8 @@ class SettingsStore {
}
}
// Create and export the settings store instance
export const settingsStore = new SettingsStore();
// Export reactive getters for easy access in components
export const config = () => settingsStore.config;
export const theme = () => settingsStore.theme;
export const isInitialized = () => settingsStore.isInitialized;
// Export bound methods for easy access
export const updateConfig = settingsStore.updateConfig.bind(settingsStore);
export const updateMultipleConfig = settingsStore.updateMultipleConfig.bind(settingsStore);
export const updateTheme = settingsStore.updateTheme.bind(settingsStore);
export const resetConfig = settingsStore.resetConfig.bind(settingsStore);
export const resetTheme = settingsStore.resetTheme.bind(settingsStore);
export const resetAll = settingsStore.resetAll.bind(settingsStore);
export const getConfig = settingsStore.getConfig.bind(settingsStore);
export const getAllConfig = settingsStore.getAllConfig.bind(settingsStore);
export const syncWithServerDefaults = settingsStore.syncWithServerDefaults.bind(settingsStore);
export const forceSyncWithServerDefaults =
settingsStore.forceSyncWithServerDefaults.bind(settingsStore);
export const getParameterInfo = settingsStore.getParameterInfo.bind(settingsStore);
export const resetParameterToServerDefault =
settingsStore.resetParameterToServerDefault.bind(settingsStore);
export const getParameterDiff = settingsStore.getParameterDiff.bind(settingsStore);
export const clearAllUserOverrides = settingsStore.clearAllUserOverrides.bind(settingsStore);

View File

@ -4,10 +4,7 @@
import { untrack } from 'svelte';
import { ChatSidebar, DialogConversationTitleUpdate } from '$lib/components/app';
import { isLoading } from '$lib/stores/chat.svelte';
import {
activeMessages,
setTitleUpdateConfirmationCallback
} from '$lib/stores/conversations.svelte';
import { conversationsStore, activeMessages } from '$lib/stores/conversations.svelte';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
import { isRouterMode, serverStore } from '$lib/stores/server.svelte';
import { config, settingsStore } from '$lib/stores/settings.svelte';
@ -158,14 +155,16 @@
// Set up title update confirmation callback
$effect(() => {
setTitleUpdateConfirmationCallback(async (currentTitle: string, newTitle: string) => {
return new Promise<boolean>((resolve) => {
titleUpdateCurrentTitle = currentTitle;
titleUpdateNewTitle = newTitle;
titleUpdateResolve = resolve;
titleUpdateDialogOpen = true;
});
});
conversationsStore.setTitleUpdateConfirmationCallback(
async (currentTitle: string, newTitle: string) => {
return new Promise<boolean>((resolve) => {
titleUpdateCurrentTitle = currentTitle;
titleUpdateNewTitle = newTitle;
titleUpdateResolve = resolve;
titleUpdateDialogOpen = true;
});
}
);
});
</script>

View File

@ -1,18 +1,8 @@
<script lang="ts">
import { ChatScreen, DialogModelNotAvailable } from '$lib/components/app';
import { sendMessage, clearUIState } from '$lib/stores/chat.svelte';
import {
conversationsStore,
isConversationsInitialized,
clearActiveConversation,
createConversation
} from '$lib/stores/conversations.svelte';
import {
fetchModels,
modelOptions,
selectModel,
findModelByName
} from '$lib/stores/models.svelte';
import { chatStore } from '$lib/stores/chat.svelte';
import { conversationsStore, isConversationsInitialized } from '$lib/stores/conversations.svelte';
import { modelsStore, modelOptions } from '$lib/stores/models.svelte';
import { onMount } from 'svelte';
import { page } from '$app/state';
import { replaceState } from '$app/navigation';
@ -39,14 +29,14 @@
async function handleUrlParams() {
// Ensure models are loaded first
await fetchModels();
await modelsStore.fetch();
// Handle model parameter - select model if provided
if (modelParam) {
const model = findModelByName(modelParam);
const model = modelsStore.findModelByName(modelParam);
if (model) {
try {
await selectModel(model.id);
await modelsStore.selectModelById(model.id);
} catch (error) {
console.error('Failed to select model:', error);
requestedModelName = modelParam;
@ -63,8 +53,8 @@
// Handle ?q= parameter - create new conversation and send message
if (qParam !== null) {
await createConversation();
await sendMessage(qParam);
await conversationsStore.createConversation();
await chatStore.sendMessage(qParam);
// Clear URL params after message is sent
clearUrlParams();
} else if (modelParam || newChatParam === 'true') {
@ -78,8 +68,8 @@
await conversationsStore.initialize();
}
clearActiveConversation();
clearUIState();
conversationsStore.clearActiveConversation();
chatStore.clearUIState();
// Handle URL params only if we have ?q= or ?model= or ?new_chat=true
if (qParam !== null || modelParam !== null || newChatParam === 'true') {

View File

@ -3,24 +3,13 @@
import { page } from '$app/state';
import { afterNavigate } from '$app/navigation';
import { ChatScreen, DialogModelNotAvailable } from '$lib/components/app';
import { chatStore, isLoading } from '$lib/stores/chat.svelte';
import {
isLoading,
stopGeneration,
syncLoadingStateForChat,
sendMessage
} from '$lib/stores/chat.svelte';
import {
conversationsStore,
activeConversation,
activeMessages,
loadConversation
activeMessages
} from '$lib/stores/conversations.svelte';
import {
selectModel,
modelOptions,
selectedModelId,
fetchModels,
findModelByName
} from '$lib/stores/models.svelte';
import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
let chatId = $derived(page.params.id);
let currentChatId: string | undefined = undefined;
@ -49,14 +38,14 @@
async function handleUrlParams() {
// Ensure models are loaded first
await fetchModels();
await modelsStore.fetch();
// Handle model parameter - select model if provided
if (modelParam) {
const model = findModelByName(modelParam);
const model = modelsStore.findModelByName(modelParam);
if (model) {
try {
await selectModel(model.id);
await modelsStore.selectModelById(model.id);
} catch (error) {
console.error('Failed to select model:', error);
requestedModelName = modelParam;
@ -73,7 +62,7 @@
// Handle ?q= parameter - send message in current conversation
if (qParam !== null) {
await sendMessage(qParam);
await chatStore.sendMessage(qParam);
// Clear URL params after message is sent
clearUrlParams();
} else if (modelParam) {
@ -112,7 +101,7 @@
if (matchingModel) {
try {
await selectModel(matchingModel.id);
await modelsStore.selectModelById(matchingModel.id);
console.log(`Automatically loaded model: ${lastMessageWithModel.model} from last message`);
} catch (error) {
console.warn('Failed to automatically select model from last message:', error);
@ -141,9 +130,9 @@
}
(async () => {
const success = await loadConversation(chatId);
const success = await conversationsStore.loadConversation(chatId);
if (success) {
syncLoadingStateForChat(chatId);
chatStore.syncLoadingStateForChat(chatId);
// Handle URL params after conversation is loaded
if ((qParam !== null || modelParam !== null) && !urlParamsProcessed) {
@ -161,7 +150,7 @@
const handleBeforeUnload = () => {
if (isLoading()) {
console.log('Page unload detected while streaming - aborting stream');
stopGeneration();
chatStore.stopGeneration();
}
};

View File

@ -92,8 +92,8 @@
message: userMessage
}}
play={async () => {
const { updateConfig } = await import('$lib/stores/settings.svelte');
updateConfig('disableReasoningFormat', false);
const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false);
}}
/>
@ -104,8 +104,8 @@
message: assistantMessage
}}
play={async () => {
const { updateConfig } = await import('$lib/stores/settings.svelte');
updateConfig('disableReasoningFormat', false);
const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false);
}}
/>
@ -116,8 +116,8 @@
message: assistantWithReasoning
}}
play={async () => {
const { updateConfig } = await import('$lib/stores/settings.svelte');
updateConfig('disableReasoningFormat', false);
const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false);
}}
/>
@ -128,8 +128,8 @@
message: rawOutputMessage
}}
play={async () => {
const { updateConfig } = await import('$lib/stores/settings.svelte');
updateConfig('disableReasoningFormat', true);
const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', true);
}}
/>
@ -140,8 +140,8 @@
}}
asChild
play={async () => {
const { updateConfig } = await import('$lib/stores/settings.svelte');
updateConfig('disableReasoningFormat', false);
const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false);
// Phase 1: Stream reasoning content in chunks
let reasoningText =
'I need to think about this carefully. Let me break down the problem:\n\n1. The user is asking for help with something complex\n2. I should provide a thorough and helpful response\n3. I need to consider multiple approaches\n4. The best solution would be to explain step by step\n\nThis approach will ensure clarity and understanding.';
@ -192,8 +192,8 @@
message: processingMessage
}}
play={async () => {
const { updateConfig } = await import('$lib/stores/settings.svelte');
updateConfig('disableReasoningFormat', false);
const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false);
// Import the chat store to simulate loading state
const { chatStore } = await import('$lib/stores/chat.svelte');