diff --git a/tools/server/webui/docs/high-level-architecture.md b/tools/server/webui/docs/high-level-architecture.md
new file mode 100644
index 0000000000..724478b3c2
--- /dev/null
+++ b/tools/server/webui/docs/high-level-architecture.md
@@ -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["State:
isLoading, currentResponse
errorDialogState
activeProcessingState
chatLoadingStates
chatStreamingStates
abortControllers
processingStates
activeConversationId
isStreamingActive"]
+ S1LoadState["Loading State:
setChatLoading()
isChatLoading()
syncLoadingStateForChat()
clearUIState()
isChatLoadingPublic()
getAllLoadingChats()
getAllStreamingChats()"]
+ S1ProcState["Processing State:
setActiveProcessingConversation()
getProcessingState()
clearProcessingState()
getActiveProcessingState()
updateProcessingStateFromTimings()
getCurrentProcessingStateSync()
restoreProcessingStateFromMessages()"]
+ S1Stream["Streaming:
streamChatCompletion()
startStreaming()
stopStreaming()
stopGeneration()
isStreaming()"]
+ S1Error["Error Handling:
showErrorDialog()
dismissErrorDialog()
isAbortError()"]
+ S1Msg["Message Operations:
addMessage()
sendMessage()
updateMessage()
deleteMessage()
getDeletionInfo()"]
+ S1Regen["Regeneration:
regenerateMessage()
regenerateMessageWithBranching()
continueAssistantMessage()"]
+ S1Edit["Editing:
editAssistantMessage()
editUserMessagePreserveResponses()
editMessageWithBranching()"]
+ S1Utils["Utilities:
getApiOptions()
parseTimingData()
getOrCreateAbortController()
getConversationModel()"]
+ end
+ subgraph S2["conversationsStore"]
+ S2State["State:
conversations
activeConversation
activeMessages
usedModalities
isInitialized
titleUpdateConfirmationCallback"]
+ S2Modal["Modalities:
getModalitiesUpToMessage()
calculateModalitiesFromMessages()"]
+ S2Lifecycle["Lifecycle:
initialize()
loadConversations()
clearActiveConversation()"]
+ S2ConvCRUD["Conversation CRUD:
createConversation()
loadConversation()
deleteConversation()
updateConversationName()
updateConversationTitleWithConfirmation()"]
+ S2MsgMgmt["Message Management:
refreshActiveMessages()
addMessageToActive()
updateMessageAtIndex()
findMessageIndex()
sliceActiveMessages()
removeMessageAtIndex()
getConversationMessages()"]
+ S2Nav["Navigation:
navigateToSibling()
updateCurrentNode()
updateConversationTimestamp()"]
+ S2Export["Import/Export:
downloadConversation()
exportAllConversations()
importConversations()
triggerDownload()"]
+ S2Utils["Utilities:
setTitleUpdateConfirmationCallback()"]
+ end
+ subgraph S3["modelsStore"]
+ S3State["State:
models, routerModels
selectedModelId
selectedModelName
loading, updating, error
modelUsage
modelLoadingStates
modelPropsCache
modelPropsFetching"]
+ S3Getters["Computed Getters:
selectedModel
loadedModelIds
loadingModelIds"]
+ S3Modal["Modalities:
getModelModalities()
modelSupportsVision()
modelSupportsAudio()
getModelProps()
updateModelModalities()"]
+ S3Status["Status Queries:
isModelLoaded()
isModelOperationInProgress()
getModelStatus()
isModelPropsFetching()"]
+ S3Fetch["Data Fetching:
fetch()
fetchRouterModels()
fetchModelProps()
fetchModalitiesForLoadedModels()"]
+ S3Select["Model Selection:
selectModelById()
selectModelByName()
clearSelection()
findModelByName()
findModelById()
hasModel()"]
+ S3LoadUnload["Loading/Unloading Models:
loadModel()
unloadModel()
ensureModelLoaded()
waitForModelStatus()
pollForModelStatus()"]
+ S3Usage["Usage Tracking:
registerModelUsage()
unregisterModelUsage()
clearConversationUsage()
getModelUsage()
isModelInUse()"]
+ S3Utils["Utilities:
toDisplayName()
clear()"]
+ end
+ subgraph S4["serverStore"]
+ S4State["State:
props
loading, error
role
fetchPromise"]
+ S4Getters["Getters:
modelName
supportedModalities
supportsVision
supportsAudio
defaultParams
contextSize
slotsEndpointAvailable
isRouterMode
isModelMode"]
+ S4Data["Data Handling:
fetch()
getErrorMessage()
clear()"]
+ S4Utils["Utilities:
detectRole()"]
+ end
+ subgraph S5["settingsStore"]
+ S5State["State:
config
theme
isInitialized
userOverrides"]
+ S5Lifecycle["Lifecycle:
initialize()
loadConfig()
saveConfig()
loadTheme()
saveTheme()"]
+ S5Update["Config Updates:
updateConfig()
updateMultipleConfig()
updateTheme()"]
+ S5Reset["Reset:
resetConfig()
resetTheme()
resetAll()
resetParameterToServerDefault()"]
+ S5Sync["Server Sync:
syncWithServerDefaults()
forceSyncWithServerDefaults()"]
+ S5Utils["Utilities:
getConfig()
getAllConfig()
getParameterInfo()
getParameterDiff()
getServerDefaults()
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["Messaging:
sendMessage()"]
+ SV1Stream["Streaming:
handleStreamResponse()
parseSSEChunk()"]
+ SV1Convert["Conversion:
convertMessageToChatData()
convertExtraToApiFormat()"]
+ SV1Utils["Utilities:
extractReasoningContent()
getServerProps()
getModels()"]
+ end
+ subgraph SV2["ModelsService"]
+ SV2List["Listing:
list()
listRouter()"]
+ SV2LoadUnload["Load/Unload:
load()
unload()"]
+ SV2Status["Status:
getStatus()
isModelLoaded()
isModelLoading()"]
+ end
+ subgraph SV3["PropsService"]
+ SV3Fetch["Fetching:
fetch()
fetchForModel()"]
+ end
+ subgraph SV4["DatabaseService"]
+ SV4Conv["Conversations:
createConversation()
getConversation()
getAllConversations()
updateConversation()
deleteConversation()"]
+ SV4Msg["Messages:
createMessageBranch()
createRootMessage()
getConversationMessages()
updateMessage()
deleteMessage()
deleteMessageCascading()"]
+ SV4Node["Navigation:
updateCurrentNode()"]
+ SV4Import["Import:
importConversations()"]
+ end
+ subgraph SV5["ParameterSyncService"]
+ SV5Extract["Extraction:
extractServerDefaults()"]
+ SV5Merge["Merging:
mergeWithServerDefaults()"]
+ SV5Info["Info:
getParameterInfo()
canSyncParameter()
getSyncableParameterKeys()
validateServerParameter()"]
+ SV5Diff["Diff:
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
/props?model="]
+ API3["/models
/models/load
/models/unload
/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
+```
diff --git a/tools/server/webui/src/lib/services/chat.ts b/tools/server/webui/src/lib/services/chat.ts
index 9d034380da..afced890b5 100644
--- a/tools/server/webui/src/lib/services/chat.ts
+++ b/tools/server/webui/src/lib/services/chat.ts
@@ -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)
*/
diff --git a/tools/server/webui/src/lib/services/database.ts b/tools/server/webui/src/lib/services/database.ts
index 77f8c0b2e0..f4a3ffd275 100644
--- a/tools/server/webui/src/lib/services/database.ts
+++ b/tools/server/webui/src/lib/services/database.ts
@@ -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.
diff --git a/tools/server/webui/src/lib/services/models.ts b/tools/server/webui/src/lib/services/models.ts
index 752f885494..5775530931 100644
--- a/tools/server/webui/src/lib/services/models.ts
+++ b/tools/server/webui/src/lib/services/models.ts
@@ -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;
}
- // ─────────────────────────────────────────────────────────────────────────────
- // 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;
}
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Load/Unload
+ // ─────────────────────────────────────────────────────────────────────────────
+
/**
* Load a model (ROUTER mode)
* POST /models/load
@@ -112,6 +112,10 @@ export class ModelsService {
return response.json() as Promise;
}
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Status
+ // ─────────────────────────────────────────────────────────────────────────────
+
/**
* Get status of a specific model (ROUTER mode)
* @param modelId - Model identifier to check
diff --git a/tools/server/webui/src/lib/services/parameter-sync.ts b/tools/server/webui/src/lib/services/parameter-sync.ts
index ee147ae194..0a0eb39263 100644
--- a/tools/server/webui/src/lib/services/parameter-sync.ts
+++ b/tools/server/webui/src/lib/services/parameter-sync.ts
@@ -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
*/
diff --git a/tools/server/webui/src/lib/services/props.ts b/tools/server/webui/src/lib/services/props.ts
index 559676cc6c..0e64350dbd 100644
--- a/tools/server/webui/src/lib/services/props.ts
+++ b/tools/server/webui/src/lib/services/props.ts
@@ -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
*
diff --git a/tools/server/webui/src/lib/stores/chat.svelte.ts b/tools/server/webui/src/lib/stores/chat.svelte.ts
index 08f4728a02..15c5e8e252 100644
--- a/tools/server/webui/src/lib/stores/chat.svelte.ts
+++ b/tools/server/webui/src/lib/stores/chat.svelte.ts
@@ -55,78 +55,24 @@ import type { DatabaseMessage, DatabaseMessageExtra } from '$lib/types/database'
* - Automatic state sync when switching between conversations
*/
class ChatStore {
+ // ─────────────────────────────────────────────────────────────────────────────
+ // State
+ // ─────────────────────────────────────────────────────────────────────────────
+
activeProcessingState = $state(null);
currentResponse = $state('');
errorDialogState = $state<{ type: 'timeout' | 'server'; message: string } | null>(null);
isLoading = $state(false);
chatLoadingStates = new SvelteMap();
chatStreamingStates = new SvelteMap();
-
- // Abort controllers for per-conversation request cancellation
private abortControllers = new SvelteMap();
-
- // Processing state tracking - per-conversation timing/context info
private processingStates = new SvelteMap();
private activeConversationId = $state(null);
private isStreamingActive = $state(false);
- // ============ API Options ============
-
- private getApiOptions(): Record {
- const currentConfig = config();
- const hasValue = (value: unknown): boolean =>
- value !== undefined && value !== null && value !== '';
-
- const apiOptions: Record = { 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 {
if (!content.trim() && (!extras || extras.length === 0)) return;
const activeConv = conversationsStore.activeConversation;
@@ -661,8 +611,6 @@ class ChatStore {
}
}
- // ============ Graceful Stop ============
-
async stopGeneration(): Promise {
const activeConv = conversationsStore.activeConversation;
if (!activeConv) return;
@@ -746,8 +694,6 @@ class ChatStore {
}
}
- // ============ Message Updates ============
-
async updateMessage(messageId: string, newContent: string): Promise {
const activeConv = conversationsStore.activeConversation;
if (!activeConv) return;
@@ -803,6 +749,10 @@ class ChatStore {
}
}
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Regeneration
+ // ─────────────────────────────────────────────────────────────────────────────
+
async regenerateMessage(messageId: string): Promise {
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 {
+ const currentConfig = config();
+ const hasValue = (value: unknown): boolean =>
+ value !== undefined && value !== null && value !== '';
+
+ const apiOptions: Record = { 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();
diff --git a/tools/server/webui/src/lib/stores/conversations.svelte.ts b/tools/server/webui/src/lib/stores/conversations.svelte.ts
index aec5dddf7d..c3a0bff1e8 100644
--- a/tools/server/webui/src/lib/stores/conversations.svelte.ts
+++ b/tools/server/webui/src/lib/stores/conversations.svelte.ts
@@ -52,6 +52,10 @@ import type { ModelModalities } from '$lib/types/models';
* - `isInitialized`: Store initialization status
*/
class ConversationsStore {
+ // ─────────────────────────────────────────────────────────────────────────────
+ // State
+ // ─────────────────────────────────────────────────────────────────────────────
+
/** List of all conversations */
conversations = $state([]);
@@ -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;
+
+ // ─────────────────────────────────────────────────────────────────────────────
+ // 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;
-
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
- ): 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
+ ): void {
+ this.titleUpdateConfirmationCallback = callback;
+ }
}
export const conversationsStore = new ConversationsStore();
diff --git a/tools/server/webui/src/lib/stores/models.svelte.ts b/tools/server/webui/src/lib/stores/models.svelte.ts
index 70c9530218..a30ad6962c 100644
--- a/tools/server/webui/src/lib/stores/models.svelte.ts
+++ b/tools/server/webui/src/lib/stores/models.svelte.ts
@@ -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 = [];
diff --git a/tools/server/webui/src/lib/stores/server.svelte.ts b/tools/server/webui/src/lib/stores/server.svelte.ts
index 4996c356dd..f6ea29183d 100644
--- a/tools/server/webui/src/lib/stores/server.svelte.ts
+++ b/tools/server/webui/src/lib/stores/server.svelte.ts
@@ -24,12 +24,20 @@ import { ServerRole, ModelModality } from '$lib/enums';
* apply to MODEL mode (single model).
*/
class ServerStore {
+ // ─────────────────────────────────────────────────────────────────────────────
+ // State
+ // ─────────────────────────────────────────────────────────────────────────────
+
props = $state(null);
loading = $state(false);
error = $state(null);
role = $state(null);
private fetchPromise: Promise | 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 {
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();
diff --git a/tools/server/webui/src/lib/stores/settings.svelte.ts b/tools/server/webui/src/lib/stores/settings.svelte.ts
index 8b3ccac021..8751183f6d 100644
--- a/tools/server/webui/src/lib/stores/settings.svelte.ts
+++ b/tools/server/webui/src/lib/stores/settings.svelte.ts
@@ -43,11 +43,19 @@ import {
} from '$lib/constants/localstorage-keys';
class SettingsStore {
+ // ─────────────────────────────────────────────────────────────────────────────
+ // State
+ // ─────────────────────────────────────────────────────────────────────────────
+
config = $state({ ...SETTING_CONFIG_DEFAULT });
theme = $state('auto');
isInitialized = $state(false);
userOverrides = $state>(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,21 +258,31 @@ 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(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);
+ }
+ }
+
+ this.userOverrides.delete(key);
+ this.saveConfig();
}
- /**
- * Get the entire configuration object
- * @returns The complete configuration object
- */
- getAllConfig(): SettingsConfigType {
- return { ...this.config };
- }
+ // ─────────────────────────────────────────────────────────────────────────────
+ // Server Sync
+ // ─────────────────────────────────────────────────────────────────────────────
/**
* Initialize settings with props defaults when server properties are first loaded
@@ -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(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();