From bc577266b9298b3e0be9f318b19ee6cb4ffa778c Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 27 Nov 2025 22:04:20 +0100 Subject: [PATCH] docs: Architecture documentation --- .../webui/docs/high-level-architecture.md | 254 ++++++++++++++++++ tools/server/webui/src/lib/services/chat.ts | 16 ++ .../server/webui/src/lib/services/database.ts | 16 ++ tools/server/webui/src/lib/services/models.ts | 14 +- .../webui/src/lib/services/parameter-sync.ts | 16 ++ tools/server/webui/src/lib/services/props.ts | 4 + .../webui/src/lib/stores/chat.svelte.ts | 202 +++++++------- .../src/lib/stores/conversations.svelte.ts | 58 +++- .../webui/src/lib/stores/models.svelte.ts | 18 +- .../webui/src/lib/stores/server.svelte.ts | 31 ++- .../webui/src/lib/stores/settings.svelte.ts | 116 +++++--- 11 files changed, 569 insertions(+), 176 deletions(-) create mode 100644 tools/server/webui/docs/high-level-architecture.md 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();