docs: Architecture documentation

This commit is contained in:
Aleksander Grygier 2025-11-27 22:04:20 +01:00
parent db479523ec
commit bc577266b9
11 changed files with 569 additions and 176 deletions

View File

@ -0,0 +1,254 @@
```mermaid
flowchart TB
subgraph Routes["📍 Routes"]
R1["/ (+page.svelte)"]
R2["/chat/[id]"]
RL["+layout.svelte"]
end
subgraph Components["🧩 Components"]
direction TB
subgraph LayoutComponents["Layout"]
C_Sidebar["ChatSidebar"]
C_Screen["ChatScreen"]
end
subgraph ChatComponents["Chat"]
C_Form["ChatForm"]
C_Messages["ChatMessages"]
C_Message["ChatMessage"]
end
subgraph ControlComponents["Controls"]
C_SelectorModel["SelectorModel"]
C_Settings["ChatSettings"]
end
end
subgraph Stores["🗄️ Stores"]
direction TB
subgraph S1["chatStore"]
S1State["<b>State:</b><br/>isLoading, currentResponse<br/>errorDialogState<br/>activeProcessingState<br/>chatLoadingStates<br/>chatStreamingStates<br/>abortControllers<br/>processingStates<br/>activeConversationId<br/>isStreamingActive"]
S1LoadState["<b>Loading State:</b><br/>setChatLoading()<br/>isChatLoading()<br/>syncLoadingStateForChat()<br/>clearUIState()<br/>isChatLoadingPublic()<br/>getAllLoadingChats()<br/>getAllStreamingChats()"]
S1ProcState["<b>Processing State:</b><br/>setActiveProcessingConversation()<br/>getProcessingState()<br/>clearProcessingState()<br/>getActiveProcessingState()<br/>updateProcessingStateFromTimings()<br/>getCurrentProcessingStateSync()<br/>restoreProcessingStateFromMessages()"]
S1Stream["<b>Streaming:</b><br/>streamChatCompletion()<br/>startStreaming()<br/>stopStreaming()<br/>stopGeneration()<br/>isStreaming()"]
S1Error["<b>Error Handling:</b><br/>showErrorDialog()<br/>dismissErrorDialog()<br/>isAbortError()"]
S1Msg["<b>Message Operations:</b><br/>addMessage()<br/>sendMessage()<br/>updateMessage()<br/>deleteMessage()<br/>getDeletionInfo()"]
S1Regen["<b>Regeneration:</b><br/>regenerateMessage()<br/>regenerateMessageWithBranching()<br/>continueAssistantMessage()"]
S1Edit["<b>Editing:</b><br/>editAssistantMessage()<br/>editUserMessagePreserveResponses()<br/>editMessageWithBranching()"]
S1Utils["<b>Utilities:</b><br/>getApiOptions()<br/>parseTimingData()<br/>getOrCreateAbortController()<br/>getConversationModel()"]
end
subgraph S2["conversationsStore"]
S2State["<b>State:</b><br/>conversations<br/>activeConversation<br/>activeMessages<br/>usedModalities<br/>isInitialized<br/>titleUpdateConfirmationCallback"]
S2Modal["<b>Modalities:</b><br/>getModalitiesUpToMessage()<br/>calculateModalitiesFromMessages()"]
S2Lifecycle["<b>Lifecycle:</b><br/>initialize()<br/>loadConversations()<br/>clearActiveConversation()"]
S2ConvCRUD["<b>Conversation CRUD:</b><br/>createConversation()<br/>loadConversation()<br/>deleteConversation()<br/>updateConversationName()<br/>updateConversationTitleWithConfirmation()"]
S2MsgMgmt["<b>Message Management:</b><br/>refreshActiveMessages()<br/>addMessageToActive()<br/>updateMessageAtIndex()<br/>findMessageIndex()<br/>sliceActiveMessages()<br/>removeMessageAtIndex()<br/>getConversationMessages()"]
S2Nav["<b>Navigation:</b><br/>navigateToSibling()<br/>updateCurrentNode()<br/>updateConversationTimestamp()"]
S2Export["<b>Import/Export:</b><br/>downloadConversation()<br/>exportAllConversations()<br/>importConversations()<br/>triggerDownload()"]
S2Utils["<b>Utilities:</b><br/>setTitleUpdateConfirmationCallback()"]
end
subgraph S3["modelsStore"]
S3State["<b>State:</b><br/>models, routerModels<br/>selectedModelId<br/>selectedModelName<br/>loading, updating, error<br/>modelUsage<br/>modelLoadingStates<br/>modelPropsCache<br/>modelPropsFetching"]
S3Getters["<b>Computed Getters:</b><br/>selectedModel<br/>loadedModelIds<br/>loadingModelIds"]
S3Modal["<b>Modalities:</b><br/>getModelModalities()<br/>modelSupportsVision()<br/>modelSupportsAudio()<br/>getModelProps()<br/>updateModelModalities()"]
S3Status["<b>Status Queries:</b><br/>isModelLoaded()<br/>isModelOperationInProgress()<br/>getModelStatus()<br/>isModelPropsFetching()"]
S3Fetch["<b>Data Fetching:</b><br/>fetch()<br/>fetchRouterModels()<br/>fetchModelProps()<br/>fetchModalitiesForLoadedModels()"]
S3Select["<b>Model Selection:</b><br/>selectModelById()<br/>selectModelByName()<br/>clearSelection()<br/>findModelByName()<br/>findModelById()<br/>hasModel()"]
S3LoadUnload["<b>Loading/Unloading Models:</b><br/>loadModel()<br/>unloadModel()<br/>ensureModelLoaded()<br/>waitForModelStatus()<br/>pollForModelStatus()"]
S3Usage["<b>Usage Tracking:</b><br/>registerModelUsage()<br/>unregisterModelUsage()<br/>clearConversationUsage()<br/>getModelUsage()<br/>isModelInUse()"]
S3Utils["<b>Utilities:</b><br/>toDisplayName()<br/>clear()"]
end
subgraph S4["serverStore"]
S4State["<b>State:</b><br/>props<br/>loading, error<br/>role<br/>fetchPromise"]
S4Getters["<b>Getters:</b><br/>modelName<br/>supportedModalities<br/>supportsVision<br/>supportsAudio<br/>defaultParams<br/>contextSize<br/>slotsEndpointAvailable<br/>isRouterMode<br/>isModelMode"]
S4Data["<b>Data Handling:</b><br/>fetch()<br/>getErrorMessage()<br/>clear()"]
S4Utils["<b>Utilities:</b><br/>detectRole()"]
end
subgraph S5["settingsStore"]
S5State["<b>State:</b><br/>config<br/>theme<br/>isInitialized<br/>userOverrides"]
S5Lifecycle["<b>Lifecycle:</b><br/>initialize()<br/>loadConfig()<br/>saveConfig()<br/>loadTheme()<br/>saveTheme()"]
S5Update["<b>Config Updates:</b><br/>updateConfig()<br/>updateMultipleConfig()<br/>updateTheme()"]
S5Reset["<b>Reset:</b><br/>resetConfig()<br/>resetTheme()<br/>resetAll()<br/>resetParameterToServerDefault()"]
S5Sync["<b>Server Sync:</b><br/>syncWithServerDefaults()<br/>forceSyncWithServerDefaults()"]
S5Utils["<b>Utilities:</b><br/>getConfig()<br/>getAllConfig()<br/>getParameterInfo()<br/>getParameterDiff()<br/>getServerDefaults()<br/>clearAllUserOverrides()"]
end
subgraph ReactiveExports["⚡ Reactive Exports"]
direction LR
subgraph ChatExports["chatStore"]
RE1["isLoading()"]
RE2["currentResponse()"]
RE3["errorDialog()"]
RE4["activeProcessingState()"]
RE5["isChatStreaming()"]
RE6["isChatLoading()"]
RE7["getChatStreaming()"]
RE8["getAllLoadingChats()"]
RE9["getAllStreamingChats()"]
end
subgraph ConvExports["conversationsStore"]
RE10["conversations()"]
RE11["activeConversation()"]
RE12["activeMessages()"]
RE13["isConversationsInitialized()"]
RE14["usedModalities()"]
end
subgraph ModelsExports["modelsStore"]
RE15["modelOptions()"]
RE16["routerModels()"]
RE17["modelsLoading()"]
RE18["modelsUpdating()"]
RE19["modelsError()"]
RE20["selectedModelId()"]
RE21["selectedModelName()"]
RE22["selectedModelOption()"]
RE23["loadedModelIds()"]
RE24["loadingModelIds()"]
end
subgraph ServerExports["serverStore"]
RE25["serverProps()"]
RE26["serverLoading()"]
RE27["serverError()"]
RE28["serverRole()"]
RE29["modelName()"]
RE30["supportedModalities()"]
RE31["supportsVision()"]
RE32["supportsAudio()"]
RE33["slotsEndpointAvailable()"]
RE34["defaultParams()"]
RE35["contextSize()"]
RE36["isRouterMode()"]
RE37["isModelMode()"]
end
subgraph SettingsExports["settingsStore"]
RE38["config()"]
RE39["theme()"]
RE40["isInitialized()"]
end
end
end
subgraph Services["⚙️ Services"]
direction TB
subgraph SV1["ChatService"]
SV1Msg["<b>Messaging:</b><br/>sendMessage()"]
SV1Stream["<b>Streaming:</b><br/>handleStreamResponse()<br/>parseSSEChunk()"]
SV1Convert["<b>Conversion:</b><br/>convertMessageToChatData()<br/>convertExtraToApiFormat()"]
SV1Utils["<b>Utilities:</b><br/>extractReasoningContent()<br/>getServerProps()<br/>getModels()"]
end
subgraph SV2["ModelsService"]
SV2List["<b>Listing:</b><br/>list()<br/>listRouter()"]
SV2LoadUnload["<b>Load/Unload:</b><br/>load()<br/>unload()"]
SV2Status["<b>Status:</b><br/>getStatus()<br/>isModelLoaded()<br/>isModelLoading()"]
end
subgraph SV3["PropsService"]
SV3Fetch["<b>Fetching:</b><br/>fetch()<br/>fetchForModel()"]
end
subgraph SV4["DatabaseService"]
SV4Conv["<b>Conversations:</b><br/>createConversation()<br/>getConversation()<br/>getAllConversations()<br/>updateConversation()<br/>deleteConversation()"]
SV4Msg["<b>Messages:</b><br/>createMessageBranch()<br/>createRootMessage()<br/>getConversationMessages()<br/>updateMessage()<br/>deleteMessage()<br/>deleteMessageCascading()"]
SV4Node["<b>Navigation:</b><br/>updateCurrentNode()"]
SV4Import["<b>Import:</b><br/>importConversations()"]
end
subgraph SV5["ParameterSyncService"]
SV5Extract["<b>Extraction:</b><br/>extractServerDefaults()"]
SV5Merge["<b>Merging:</b><br/>mergeWithServerDefaults()"]
SV5Info["<b>Info:</b><br/>getParameterInfo()<br/>canSyncParameter()<br/>getSyncableParameterKeys()<br/>validateServerParameter()"]
SV5Diff["<b>Diff:</b><br/>createParameterDiff()"]
end
end
subgraph Storage["💾 Storage"]
ST1["IndexedDB"]
ST2["conversations"]
ST3["messages"]
ST5["LocalStorage"]
ST6["config"]
ST7["userOverrides"]
end
subgraph APIs["🌐 llama-server API"]
API1["/v1/chat/completions"]
API2["/props<br/>/props?model="]
API3["/models<br/>/models/load<br/>/models/unload<br/>/models/status"]
API4["/v1/models"]
end
%% Routes render Components
R1 --> C_Screen
R2 --> C_Screen
RL --> C_Sidebar
%% Component hierarchy
C_Screen --> C_Form & C_Messages & C_Settings
C_Messages --> C_Message
C_Message --> C_SelectorModel
C_Form --> C_SelectorModel
%% Components use Stores
C_Screen --> S1 & S2
C_Messages --> S2
C_Message --> S1 & S2 & S3
C_Form --> S1 & S3
C_Sidebar --> S2
C_SelectorModel --> S3 & S4
C_Settings --> S5
%% Stores export Reactive State
S1 -. exports .-> ChatExports
S2 -. exports .-> ConvExports
S3 -. exports .-> ModelsExports
S4 -. exports .-> ServerExports
S5 -. exports .-> SettingsExports
%% Stores use Services
S1 --> SV1 & SV4
S2 --> SV4
S3 --> SV2 & SV3
S4 --> SV3
S5 --> SV5
%% Services to Storage
SV4 --> ST1
ST1 --> ST2 & ST3
SV5 --> ST5
ST5 --> ST6 & ST7
%% Services to APIs
SV1 --> API1
SV2 --> API3 & API4
SV3 --> API2
%% Styling
classDef routeStyle fill:#e1f5fe,stroke:#01579b,stroke-width:2px
classDef componentStyle fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
classDef componentGroupStyle fill:#e1bee7,stroke:#7b1fa2,stroke-width:1px
classDef storeStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px
classDef stateStyle fill:#ffe0b2,stroke:#e65100,stroke-width:1px
classDef methodStyle fill:#ffecb3,stroke:#e65100,stroke-width:1px
classDef reactiveStyle fill:#fffde7,stroke:#f9a825,stroke-width:1px
classDef serviceStyle fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
classDef serviceMStyle fill:#c8e6c9,stroke:#2e7d32,stroke-width:1px
classDef storageStyle fill:#fce4ec,stroke:#c2185b,stroke-width:2px
classDef apiStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
class R1,R2,RL routeStyle
class C_Sidebar,C_Screen,C_Form,C_Messages,C_Message componentStyle
class C_SelectorModel,C_Settings componentStyle
class LayoutComponents,ChatComponents,ControlComponents componentGroupStyle
class S1,S2,S3,S4,S5 storeStyle
class S1State,S2State,S3State,S4State,S5State stateStyle
class S1Msg,S1Regen,S1Edit,S1Stream,S1LoadState,S1ProcState,S1Error,S1Utils methodStyle
class S2Lifecycle,S2ConvCRUD,S2MsgMgmt,S2Nav,S2Modal,S2Export,S2Utils methodStyle
class S3Getters,S3Modal,S3Status,S3Fetch,S3Select,S3LoadUnload,S3Usage,S3Utils methodStyle
class S4Getters,S4Data,S4Utils methodStyle
class S5Lifecycle,S5Update,S5Reset,S5Sync,S5Utils methodStyle
class ChatExports,ConvExports,ModelsExports,ServerExports,SettingsExports reactiveStyle
class SV1,SV2,SV3,SV4,SV5 serviceStyle
class SV1Msg,SV1Stream,SV1Convert,SV1Utils serviceMStyle
class SV2List,SV2LoadUnload,SV2Status serviceMStyle
class SV3Fetch serviceMStyle
class SV4Conv,SV4Msg,SV4Node,SV4Import serviceMStyle
class SV5Extract,SV5Merge,SV5Info,SV5Diff serviceMStyle
class ST1,ST2,ST3,ST5,ST6,ST7 storageStyle
class API1,API2,API3,API4 apiStyle
```

View File

@ -54,6 +54,10 @@ import type { SettingsChatServiceOptions } from '$lib/types/settings';
* - Request lifecycle management (abort via AbortSignal)
*/
export class ChatService {
// ─────────────────────────────────────────────────────────────────────────────
// Messaging
// ─────────────────────────────────────────────────────────────────────────────
/**
* Sends a chat completion request to the llama.cpp server.
* Supports both streaming and non-streaming responses with comprehensive parameter configuration.
@ -261,6 +265,10 @@ export class ChatService {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Streaming
// ─────────────────────────────────────────────────────────────────────────────
/**
* Handles streaming response from the chat completion API
* @param response - The Response object from the fetch request
@ -571,6 +579,10 @@ export class ChatService {
return result;
}
// ─────────────────────────────────────────────────────────────────────────────
// Conversion
// ─────────────────────────────────────────────────────────────────────────────
/**
* Converts a database message with attachments to API chat message format.
* Processes various attachment types (images, text files, PDFs) and formats them
@ -681,6 +693,10 @@ export class ChatService {
};
}
// ─────────────────────────────────────────────────────────────────────────────
// Utilities
// ─────────────────────────────────────────────────────────────────────────────
/**
* Get server properties - static method for API compatibility (to be refactored)
*/

View File

@ -66,6 +66,10 @@ import { v4 as uuid } from 'uuid';
* `currNode` tracks the currently active branch endpoint.
*/
export class DatabaseService {
// ─────────────────────────────────────────────────────────────────────────────
// Conversations
// ─────────────────────────────────────────────────────────────────────────────
/**
* Creates a new conversation.
*
@ -84,6 +88,10 @@ export class DatabaseService {
return conversation;
}
// ─────────────────────────────────────────────────────────────────────────────
// Messages
// ─────────────────────────────────────────────────────────────────────────────
/**
* Creates a new message branch by adding a message and updating parent/child relationships.
* Also updates the conversation's currNode to point to the new message.
@ -277,6 +285,10 @@ export class DatabaseService {
});
}
// ─────────────────────────────────────────────────────────────────────────────
// Navigation
// ─────────────────────────────────────────────────────────────────────────────
/**
* Updates the conversation's current node (active branch).
* This determines which conversation path is currently being viewed.
@ -304,6 +316,10 @@ export class DatabaseService {
await db.messages.update(id, updates);
}
// ─────────────────────────────────────────────────────────────────────────────
// Import
// ─────────────────────────────────────────────────────────────────────────────
/**
* Imports multiple conversations and their messages.
* Skips conversations that already exist.

View File

@ -27,7 +27,7 @@ import type {
*/
export class ModelsService {
// ─────────────────────────────────────────────────────────────────────────────
// MODEL + ROUTER mode - OpenAI-compatible API
// Listing
// ─────────────────────────────────────────────────────────────────────────────
/**
@ -46,10 +46,6 @@ export class ModelsService {
return response.json() as Promise<ApiModelListResponse>;
}
// ─────────────────────────────────────────────────────────────────────────────
// ROUTER mode only - Model management API
// ─────────────────────────────────────────────────────────────────────────────
/**
* Fetch list of all models with detailed metadata (ROUTER mode)
* Returns models with load status, paths, and other metadata
@ -66,6 +62,10 @@ export class ModelsService {
return response.json() as Promise<ApiRouterModelsListResponse>;
}
// ─────────────────────────────────────────────────────────────────────────────
// Load/Unload
// ─────────────────────────────────────────────────────────────────────────────
/**
* Load a model (ROUTER mode)
* POST /models/load
@ -112,6 +112,10 @@ export class ModelsService {
return response.json() as Promise<ApiRouterModelsUnloadResponse>;
}
// ─────────────────────────────────────────────────────────────────────────────
// Status
// ─────────────────────────────────────────────────────────────────────────────
/**
* Get status of a specific model (ROUTER mode)
* @param modelId - Model identifier to check

View File

@ -60,6 +60,10 @@ export const SYNCABLE_PARAMETERS: SyncableParameter[] = [
];
export class ParameterSyncService {
// ─────────────────────────────────────────────────────────────────────────────
// Extraction
// ─────────────────────────────────────────────────────────────────────────────
/**
* Round floating-point numbers to avoid JavaScript precision issues
*/
@ -95,6 +99,10 @@ export class ParameterSyncService {
return extracted;
}
// ─────────────────────────────────────────────────────────────────────────────
// Merging
// ─────────────────────────────────────────────────────────────────────────────
/**
* Merge server defaults with current user settings
* Returns updated settings that respect user overrides while using server defaults
@ -116,6 +124,10 @@ export class ParameterSyncService {
return merged;
}
// ─────────────────────────────────────────────────────────────────────────────
// Info
// ─────────────────────────────────────────────────────────────────────────────
/**
* Get parameter information including source and values
*/
@ -172,6 +184,10 @@ export class ParameterSyncService {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Diff
// ─────────────────────────────────────────────────────────────────────────────
/**
* Create a diff between current settings and server defaults
*/

View File

@ -15,6 +15,10 @@ import { getAuthHeaders } from '$lib/utils/api-headers';
* - serverStore: Primary consumer for server state management
*/
export class PropsService {
// ─────────────────────────────────────────────────────────────────────────────
// Fetching
// ─────────────────────────────────────────────────────────────────────────────
/**
* Fetches server properties from the /props endpoint
*

View File

@ -55,78 +55,24 @@ import type { DatabaseMessage, DatabaseMessageExtra } from '$lib/types/database'
* - Automatic state sync when switching between conversations
*/
class ChatStore {
// ─────────────────────────────────────────────────────────────────────────────
// State
// ─────────────────────────────────────────────────────────────────────────────
activeProcessingState = $state<ApiProcessingState | null>(null);
currentResponse = $state('');
errorDialogState = $state<{ type: 'timeout' | 'server'; message: string } | null>(null);
isLoading = $state(false);
chatLoadingStates = new SvelteMap<string, boolean>();
chatStreamingStates = new SvelteMap<string, { response: string; messageId: string }>();
// Abort controllers for per-conversation request cancellation
private abortControllers = new SvelteMap<string, AbortController>();
// Processing state tracking - per-conversation timing/context info
private processingStates = new SvelteMap<string, ApiProcessingState | null>();
private activeConversationId = $state<string | null>(null);
private isStreamingActive = $state(false);
// ============ API Options ============
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.custom) apiOptions.custom = currentConfig.custom;
return apiOptions;
}
// ============ Loading State Management ============
// ─────────────────────────────────────────────────────────────────────────────
// Loading State
// ─────────────────────────────────────────────────────────────────────────────
private setChatLoading(convId: string, loading: boolean): void {
if (loading) {
@ -171,28 +117,9 @@ class ChatStore {
this.currentResponse = '';
}
// ============ Processing State Management ============
/**
* 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;
}
// ─────────────────────────────────────────────────────────────────────────────
// Processing State
// ─────────────────────────────────────────────────────────────────────────────
/**
* Set the active conversation for statistics display
@ -301,6 +228,31 @@ class ChatStore {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// 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();
@ -360,8 +312,6 @@ class ChatStore {
};
}
// ============ Model Detection ============
/**
* 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.
@ -380,7 +330,9 @@ class ChatStore {
return null;
}
// ============ Error Handling ============
// ─────────────────────────────────────────────────────────────────────────────
// Error Handling
// ─────────────────────────────────────────────────────────────────────────────
private isAbortError(error: unknown): boolean {
return error instanceof Error && (error.name === 'AbortError' || error instanceof DOMException);
@ -394,7 +346,9 @@ class ChatStore {
this.errorDialogState = null;
}
// ============ Message Creation ============
// ─────────────────────────────────────────────────────────────────────────────
// Message Operations
// ─────────────────────────────────────────────────────────────────────────────
async addMessage(
role: ChatRole,
@ -475,8 +429,6 @@ class ChatStore {
);
}
// ============ Streaming ============
private async streamChatCompletion(
allMessages: DatabaseMessage[],
assistantMessage: DatabaseMessage,
@ -614,8 +566,6 @@ class ChatStore {
);
}
// ============ Send Message ============
async sendMessage(content: string, extras?: DatabaseMessageExtra[]): Promise<void> {
if (!content.trim() && (!extras || extras.length === 0)) return;
const activeConv = conversationsStore.activeConversation;
@ -661,8 +611,6 @@ class ChatStore {
}
}
// ============ Graceful Stop ============
async stopGeneration(): Promise<void> {
const activeConv = conversationsStore.activeConversation;
if (!activeConv) return;
@ -746,8 +694,6 @@ class ChatStore {
}
}
// ============ Message Updates ============
async updateMessage(messageId: string, newContent: string): Promise<void> {
const activeConv = conversationsStore.activeConversation;
if (!activeConv) return;
@ -803,6 +749,10 @@ class ChatStore {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Regeneration
// ─────────────────────────────────────────────────────────────────────────────
async regenerateMessage(messageId: string): Promise<void> {
const activeConv = conversationsStore.activeConversation;
if (!activeConv || this.isLoading) return;
@ -900,7 +850,9 @@ class ChatStore {
}
}
// ============ Branching Operations ============
// ─────────────────────────────────────────────────────────────────────────────
// Editing
// ─────────────────────────────────────────────────────────────────────────────
async editAssistantMessage(
messageId: string,
@ -1268,6 +1220,64 @@ class ChatStore {
public getAllStreamingChats(): string[] {
return Array.from(this.chatStreamingStates.keys());
}
// ─────────────────────────────────────────────────────────────────────────────
// 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.custom) apiOptions.custom = currentConfig.custom;
return apiOptions;
}
}
export const chatStore = new ChatStore();

View File

@ -52,6 +52,10 @@ import type { ModelModalities } from '$lib/types/models';
* - `isInitialized`: Store initialization status
*/
class ConversationsStore {
// ─────────────────────────────────────────────────────────────────────────────
// State
// ─────────────────────────────────────────────────────────────────────────────
/** List of all conversations */
conversations = $state<DatabaseConversation[]>([]);
@ -64,6 +68,13 @@ class ConversationsStore {
/** Whether the store has been initialized */
isInitialized = $state(false);
/** Callback for title update confirmation dialog */
titleUpdateConfirmationCallback?: (currentTitle: string, newTitle: string) => Promise<boolean>;
// ─────────────────────────────────────────────────────────────────────────────
// Modalities
// ─────────────────────────────────────────────────────────────────────────────
/**
* Modalities used in the active conversation.
* Computed from attachments in activeMessages.
@ -113,15 +124,16 @@ class ConversationsStore {
return this.calculateModalitiesFromMessages(messagesBefore);
}
/** Callback for title update confirmation dialog */
titleUpdateConfirmationCallback?: (currentTitle: string, newTitle: string) => Promise<boolean>;
constructor() {
if (browser) {
this.initialize();
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Lifecycle
// ─────────────────────────────────────────────────────────────────────────────
/**
* Initializes the conversations store by loading conversations from the database
*/
@ -141,6 +153,10 @@ class ConversationsStore {
this.conversations = await DatabaseService.getAllConversations();
}
// ─────────────────────────────────────────────────────────────────────────────
// Conversation CRUD
// ─────────────────────────────────────────────────────────────────────────────
/**
* Creates a new conversation and navigates to it
* @param name - Optional name for the conversation
@ -202,6 +218,10 @@ class ConversationsStore {
// Active processing conversation is now managed by chatStore
}
// ─────────────────────────────────────────────────────────────────────────────
// Message Management
// ─────────────────────────────────────────────────────────────────────────────
/**
* Refreshes active messages based on currNode after branch navigation
*/
@ -248,16 +268,6 @@ class ConversationsStore {
}
}
/**
* Sets the callback function for title update confirmations
* @param callback - Function to call when confirmation is needed
*/
setTitleUpdateConfirmationCallback(
callback: (currentTitle: string, newTitle: string) => Promise<boolean>
): void {
this.titleUpdateConfirmationCallback = callback;
}
/**
* Updates conversation title with optional confirmation dialog based on settings
* @param convId - The conversation ID to update
@ -289,6 +299,10 @@ class ConversationsStore {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Navigation
// ─────────────────────────────────────────────────────────────────────────────
/**
* Updates the current node of the active conversation
* @param nodeId - The new current node ID
@ -376,6 +390,10 @@ class ConversationsStore {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Import/Export
// ─────────────────────────────────────────────────────────────────────────────
/**
* Downloads a conversation as JSON file
* @param convId - The conversation ID to download
@ -580,6 +598,20 @@ class ConversationsStore {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// ─────────────────────────────────────────────────────────────────────────────
// Utilities
// ─────────────────────────────────────────────────────────────────────────────
/**
* Sets the callback function for title update confirmations
* @param callback - Function to call when confirmation is needed
*/
setTitleUpdateConfirmationCallback(
callback: (currentTitle: string, newTitle: string) => Promise<boolean>
): void {
this.titleUpdateConfirmationCallback = callback;
}
}
export const conversationsStore = new ConversationsStore();

View File

@ -78,7 +78,7 @@ class ModelsStore {
}
// ─────────────────────────────────────────────────────────────────────────────
// Methods - Model Modalities
// Modalities
// ─────────────────────────────────────────────────────────────────────────────
/**
@ -133,7 +133,7 @@ class ModelsStore {
}
// ─────────────────────────────────────────────────────────────────────────────
// Methods - Model Status
// Status Queries
// ─────────────────────────────────────────────────────────────────────────────
isModelLoaded(modelId: string): boolean {
@ -160,7 +160,7 @@ class ModelsStore {
}
// ─────────────────────────────────────────────────────────────────────────────
// Fetch Models
// Data Fetching
// ─────────────────────────────────────────────────────────────────────────────
/**
@ -328,7 +328,7 @@ class ModelsStore {
}
// ─────────────────────────────────────────────────────────────────────────────
// Select Model
// Model Selection
// ─────────────────────────────────────────────────────────────────────────────
/**
@ -382,7 +382,7 @@ class ModelsStore {
}
// ─────────────────────────────────────────────────────────────────────────────
// Load/Unload Models (ROUTER mode)
// Loading/Unloading Models
// ─────────────────────────────────────────────────────────────────────────────
/**
@ -501,7 +501,7 @@ class ModelsStore {
}
// ─────────────────────────────────────────────────────────────────────────────
// Model Usage Tracking
// Usage Tracking
// ─────────────────────────────────────────────────────────────────────────────
/**
@ -546,7 +546,7 @@ class ModelsStore {
}
// ─────────────────────────────────────────────────────────────────────────────
// Private Helpers
// Utilities
// ─────────────────────────────────────────────────────────────────────────────
private toDisplayName(id: string): string {
@ -556,10 +556,6 @@ class ModelsStore {
return candidate && candidate.trim().length > 0 ? candidate : id;
}
// ─────────────────────────────────────────────────────────────────────────────
// Clear State
// ─────────────────────────────────────────────────────────────────────────────
clear(): void {
this.models = [];
this.routerModels = [];

View File

@ -24,12 +24,20 @@ import { ServerRole, ModelModality } from '$lib/enums';
* apply to MODEL mode (single model).
*/
class ServerStore {
// ─────────────────────────────────────────────────────────────────────────────
// State
// ─────────────────────────────────────────────────────────────────────────────
props = $state<ApiLlamaCppServerProps | null>(null);
loading = $state(false);
error = $state<string | null>(null);
role = $state<ServerRole | null>(null);
private fetchPromise: Promise<void> | null = null;
// ─────────────────────────────────────────────────────────────────────────────
// Getters
// ─────────────────────────────────────────────────────────────────────────────
/**
* Get model name from server props.
* In MODEL mode: extracts from model_path or model_alias
@ -89,13 +97,9 @@ class ServerStore {
return this.role === ServerRole.MODEL;
}
private detectRole(props: ApiLlamaCppServerProps): void {
const newRole = props?.role === ServerRole.ROUTER ? ServerRole.ROUTER : ServerRole.MODEL;
if (this.role !== newRole) {
this.role = newRole;
console.info(`Server running in ${newRole === ServerRole.ROUTER ? 'ROUTER' : 'MODEL'} mode`);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Data Handling
// ─────────────────────────────────────────────────────────────────────────────
async fetch(): Promise<void> {
if (this.fetchPromise) return this.fetchPromise;
@ -147,6 +151,7 @@ class ServerStore {
return 'Failed to connect to server';
}
clear(): void {
this.props = null;
this.error = null;
@ -154,6 +159,18 @@ class ServerStore {
this.role = null;
this.fetchPromise = null;
}
// ─────────────────────────────────────────────────────────────────────────────
// Utilities
// ─────────────────────────────────────────────────────────────────────────────
private detectRole(props: ApiLlamaCppServerProps): void {
const newRole = props?.role === ServerRole.ROUTER ? ServerRole.ROUTER : ServerRole.MODEL;
if (this.role !== newRole) {
this.role = newRole;
console.info(`Server running in ${newRole === ServerRole.ROUTER ? 'ROUTER' : 'MODEL'} mode`);
}
}
}
export const serverStore = new ServerStore();

View File

@ -43,11 +43,19 @@ import {
} from '$lib/constants/localstorage-keys';
class SettingsStore {
// ─────────────────────────────────────────────────────────────────────────────
// State
// ─────────────────────────────────────────────────────────────────────────────
config = $state<SettingsConfigType>({ ...SETTING_CONFIG_DEFAULT });
theme = $state<string>('auto');
isInitialized = $state(false);
userOverrides = $state<Set<string>>(new Set());
// ─────────────────────────────────────────────────────────────────────────────
// Utilities (private helpers)
// ─────────────────────────────────────────────────────────────────────────────
/**
* Helper method to get server defaults with null safety
* Centralizes the pattern of getting and extracting server defaults
@ -63,6 +71,10 @@ class SettingsStore {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Lifecycle
// ─────────────────────────────────────────────────────────────────────────────
/**
* Initialize the settings store by loading from localStorage
*/
@ -113,6 +125,10 @@ class SettingsStore {
this.theme = localStorage.getItem('theme') || 'auto';
}
// ─────────────────────────────────────────────────────────────────────────────
// Config Updates
// ─────────────────────────────────────────────────────────────────────────────
/**
* Update a specific configuration setting
* @param key - The configuration key to update
@ -213,6 +229,10 @@ class SettingsStore {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Reset
// ─────────────────────────────────────────────────────────────────────────────
/**
* Reset configuration to defaults
*/
@ -238,22 +258,32 @@ class SettingsStore {
}
/**
* Get a specific configuration value
* @param key - The configuration key to get
* @returns The configuration value
* Reset a parameter to server default (or webui default if no server default)
*/
getConfig<K extends keyof SettingsConfigType>(key: K): SettingsConfigType[K] {
return this.config[key];
resetParameterToServerDefault(key: string): void {
const serverDefaults = this.getServerDefaults();
if (serverDefaults[key] !== undefined) {
const value = normalizeFloatingPoint(serverDefaults[key]);
this.config[key as keyof SettingsConfigType] =
value as SettingsConfigType[keyof SettingsConfigType];
} else {
if (key in SETTING_CONFIG_DEFAULT) {
const defaultValue = getConfigValue(SETTING_CONFIG_DEFAULT, key);
setConfigValue(this.config, key, defaultValue);
}
}
/**
* Get the entire configuration object
* @returns The complete configuration object
*/
getAllConfig(): SettingsConfigType {
return { ...this.config };
this.userOverrides.delete(key);
this.saveConfig();
}
// ─────────────────────────────────────────────────────────────────────────────
// Server Sync
// ─────────────────────────────────────────────────────────────────────────────
/**
* Initialize settings with props defaults when server properties are first loaded
* This sets up the default values from /props endpoint
@ -287,15 +317,6 @@ class SettingsStore {
console.log('Current user overrides after sync:', Array.from(this.userOverrides));
}
/**
* Clear all user overrides (for debugging)
*/
clearAllUserOverrides(): void {
this.userOverrides.clear();
this.saveConfig();
console.log('Cleared all user overrides');
}
/**
* Reset all parameters to their default values (from props)
* This is used by the "Reset to Default" functionality
@ -324,6 +345,27 @@ class SettingsStore {
this.saveConfig();
}
// ─────────────────────────────────────────────────────────────────────────────
// Utilities
// ─────────────────────────────────────────────────────────────────────────────
/**
* Get a specific configuration value
* @param key - The configuration key to get
* @returns The configuration value
*/
getConfig<K extends keyof SettingsConfigType>(key: K): SettingsConfigType[K] {
return this.config[key];
}
/**
* Get the entire configuration object
* @returns The complete configuration object
*/
getAllConfig(): SettingsConfigType {
return { ...this.config };
}
/**
* Get parameter information including source for a specific parameter
*/
@ -339,29 +381,6 @@ class SettingsStore {
);
}
/**
* Reset a parameter to server default (or webui default if no server default)
*/
resetParameterToServerDefault(key: string): void {
const serverDefaults = this.getServerDefaults();
if (serverDefaults[key] !== undefined) {
const value = normalizeFloatingPoint(serverDefaults[key]);
this.config[key as keyof SettingsConfigType] =
value as SettingsConfigType[keyof SettingsConfigType];
} else {
if (key in SETTING_CONFIG_DEFAULT) {
const defaultValue = getConfigValue(SETTING_CONFIG_DEFAULT, key);
setConfigValue(this.config, key, defaultValue);
}
}
this.userOverrides.delete(key);
this.saveConfig();
}
/**
* Get diff between current settings and server defaults
*/
@ -376,6 +395,15 @@ class SettingsStore {
return ParameterSyncService.createParameterDiff(configAsRecord, serverDefaults);
}
/**
* Clear all user overrides (for debugging)
*/
clearAllUserOverrides(): void {
this.userOverrides.clear();
this.saveConfig();
console.log('Cleared all user overrides');
}
}
export const settingsStore = new SettingsStore();