From 4bf82a10f190b82b83bb4b545664bfe5b71c426e Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Fri, 21 Nov 2025 00:05:43 +0100 Subject: [PATCH] feat: Improved UX for model information, modality interactions etc --- .../app/chat/ChatForm/ChatForm.svelte | 2 + .../ChatFormActionRecord.svelte | 10 +- .../ChatFormActions/ChatFormActions.svelte | 65 +++-- .../ChatMessages/ChatMessageAssistant.svelte | 4 + .../app/chat/ChatScreen/ChatScreen.svelte | 16 +- .../app/chat/ChatSidebar/ChatSidebar.svelte | 2 +- .../app/dialogs/DialogModelInformation.svelte | 245 ++++++++++++++++++ .../webui/src/lib/components/app/index.ts | 4 +- .../components/app/misc/BadgeModality.svelte | 51 ++++ .../components/app/misc/BadgeModelName.svelte | 53 ++++ .../components/app/server/ServerInfo.svelte | 43 --- .../src/lib/components/ui/table/index.ts | 28 ++ .../lib/components/ui/table/table-body.svelte | 20 ++ .../components/ui/table/table-caption.svelte | 20 ++ .../lib/components/ui/table/table-cell.svelte | 23 ++ .../components/ui/table/table-footer.svelte | 20 ++ .../lib/components/ui/table/table-head.svelte | 23 ++ .../components/ui/table/table-header.svelte | 20 ++ .../lib/components/ui/table/table-row.svelte | 23 ++ .../src/lib/components/ui/table/table.svelte | 22 ++ tools/server/webui/src/lib/services/chat.ts | 13 - .../webui/src/lib/stores/chat.svelte.ts | 52 +--- .../webui/src/lib/stores/server.svelte.ts | 4 +- .../server/webui/src/lib/utils/model-names.ts | 29 ++- 24 files changed, 649 insertions(+), 143 deletions(-) create mode 100644 tools/server/webui/src/lib/components/app/dialogs/DialogModelInformation.svelte create mode 100644 tools/server/webui/src/lib/components/app/misc/BadgeModality.svelte create mode 100644 tools/server/webui/src/lib/components/app/misc/BadgeModelName.svelte delete mode 100644 tools/server/webui/src/lib/components/app/server/ServerInfo.svelte create mode 100644 tools/server/webui/src/lib/components/ui/table/index.ts create mode 100644 tools/server/webui/src/lib/components/ui/table/table-body.svelte create mode 100644 tools/server/webui/src/lib/components/ui/table/table-caption.svelte create mode 100644 tools/server/webui/src/lib/components/ui/table/table-cell.svelte create mode 100644 tools/server/webui/src/lib/components/ui/table/table-footer.svelte create mode 100644 tools/server/webui/src/lib/components/ui/table/table-head.svelte create mode 100644 tools/server/webui/src/lib/components/ui/table/table-header.svelte create mode 100644 tools/server/webui/src/lib/components/ui/table/table-row.svelte create mode 100644 tools/server/webui/src/lib/components/ui/table/table.svelte diff --git a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte index 6c9a11849c..15080a5ee2 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatForm.svelte @@ -253,9 +253,11 @@ 0 || uploadedFiles.length > 0} + hasText={message.trim().length > 0} {disabled} {isLoading} {isRecording} + {uploadedFiles} onFileUpload={handleFileUpload} onMicClick={handleMicClick} onStop={handleStop} diff --git a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionRecord.svelte b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionRecord.svelte index d08a6972db..d9e3697203 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionRecord.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionRecord.svelte @@ -1,5 +1,5 @@ -
+
- {#if currentConfig.modelSelectorEnabled} + {#if !inRouterMode} + (showModelInfoDialog = true)} + showTooltip + /> + {:else} {/if} @@ -51,15 +73,24 @@ {:else} - + {#if shouldShowRecordButton} + + {/if} - + {#if shouldShowSubmitButton} + + {/if} {/if}
+ + (showModelInfoDialog = open)} +/> diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte index 865d81ba44..91beff6925 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte @@ -101,6 +101,10 @@ return message.model; } + if (!serverModel || serverModel === 'none' || serverModel === 'llama-server') { + return null; + } + return serverModel; }); diff --git a/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte b/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte index e891f7efdc..91d4309019 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte @@ -9,7 +9,6 @@ DialogEmptyFileAlert, DialogChatError, ServerErrorSplash, - ServerInfo, ServerLoadingSplash, DialogConfirmation } from '$lib/components/app'; @@ -44,6 +43,7 @@ import { fade, fly, slide } from 'svelte/transition'; import { Trash2 } from '@lucide/svelte'; import ChatScreenDragOverlay from './ChatScreenDragOverlay.svelte'; + import { ModelModality } from '$lib/enums/model'; let { showCenteredEmpty = false } = $props(); @@ -334,14 +334,14 @@ role="main" >
-
-

llama.cpp

+
+

llama.cpp

-

How can I help you today?

-
- -
- +

+ {serverStore.supportedModalities.includes(ModelModality.AUDIO) + ? 'Record audio, type a message ' + : 'Type a message'} or upload files to get started +

{#if serverWarning()} diff --git a/tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebar.svelte b/tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebar.svelte index 34f3da53ea..9663045f82 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebar.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatSidebar/ChatSidebar.svelte @@ -105,7 +105,7 @@ - +

llama.cpp

diff --git a/tools/server/webui/src/lib/components/app/dialogs/DialogModelInformation.svelte b/tools/server/webui/src/lib/components/app/dialogs/DialogModelInformation.svelte new file mode 100644 index 0000000000..c8ace168bb --- /dev/null +++ b/tools/server/webui/src/lib/components/app/dialogs/DialogModelInformation.svelte @@ -0,0 +1,245 @@ + + + + + + + + Model Information + Current model details and capabilities + + +
+ {#if isLoadingModels} +
+
Loading model information...
+
+ {:else if modelsData && modelsData.data.length > 0} + {@const modelMeta = modelsData.data[0].meta} + + {#if serverProps} + + + + Model + + + + {serverStore.modelName} + + + + + + + + File Path + + + + {serverProps.model_path} + + + copyToClipboard(serverProps.model_path)} + /> + + + + + + Context Size + {formatNumber(serverProps.default_generation_settings.n_ctx)} tokens + + + + {#if modelMeta?.n_ctx_train} + + Training Context + {formatNumber(modelMeta.n_ctx_train)} tokens + + {/if} + + + {#if modelMeta?.size} + + Model Size + {formatSize(modelMeta.size)} + + {/if} + + + {#if modelMeta?.n_params} + + Parameters + {formatParameters(modelMeta.n_params)} + + {/if} + + + {#if modelMeta?.n_embd} + + Embedding Size + {formatNumber(modelMeta.n_embd)} + + {/if} + + + {#if modelMeta?.n_vocab} + + Vocabulary Size + {formatNumber(modelMeta.n_vocab)} tokens + + {/if} + + + {#if modelMeta?.vocab_type} + + Vocabulary Type + {modelMeta.vocab_type} + + {/if} + + + + Parallel Slots + {serverProps.total_slots} + + + + {#if modalities.length > 0} + + Modalities + +
+ +
+
+
+ {/if} + + + + Build Info + {serverProps.build_info} + + + + {#if serverProps.chat_template} + + Chat Template + +
+
{serverProps.chat_template}
+
+
+
+ {/if} +
+
+ {/if} + {:else if !isLoadingModels} +
+
No model information available
+
+ {/if} +
+
+
diff --git a/tools/server/webui/src/lib/components/app/index.ts b/tools/server/webui/src/lib/components/app/index.ts index 54bd8d5aa3..0312322730 100644 --- a/tools/server/webui/src/lib/components/app/index.ts +++ b/tools/server/webui/src/lib/components/app/index.ts @@ -45,11 +45,14 @@ export { default as DialogConfirmation } from './dialogs/DialogConfirmation.svel export { default as DialogConversationSelection } from './dialogs/DialogConversationSelection.svelte'; export { default as DialogConversationTitleUpdate } from './dialogs/DialogConversationTitleUpdate.svelte'; export { default as DialogEmptyFileAlert } from './dialogs/DialogEmptyFileAlert.svelte'; +export { default as DialogModelInformation } from './dialogs/DialogModelInformation.svelte'; // Miscellanous export { default as ActionButton } from './misc/ActionButton.svelte'; export { default as ActionDropdown } from './misc/ActionDropdown.svelte'; +export { default as BadgeModelName } from './misc/BadgeModelName.svelte'; +export { default as BadgeModality } from './misc/BadgeModality.svelte'; export { default as ConversationSelection } from './misc/ConversationSelection.svelte'; export { default as KeyboardShortcutInfo } from './misc/KeyboardShortcutInfo.svelte'; export { default as MarkdownContent } from './misc/MarkdownContent.svelte'; @@ -60,4 +63,3 @@ export { default as RemoveButton } from './misc/RemoveButton.svelte'; export { default as ServerStatus } from './server/ServerStatus.svelte'; export { default as ServerErrorSplash } from './server/ServerErrorSplash.svelte'; export { default as ServerLoadingSplash } from './server/ServerLoadingSplash.svelte'; -export { default as ServerInfo } from './server/ServerInfo.svelte'; diff --git a/tools/server/webui/src/lib/components/app/misc/BadgeModality.svelte b/tools/server/webui/src/lib/components/app/misc/BadgeModality.svelte new file mode 100644 index 0000000000..b9413cb853 --- /dev/null +++ b/tools/server/webui/src/lib/components/app/misc/BadgeModality.svelte @@ -0,0 +1,51 @@ + + +{#each modalities as modality, index (index)} + {@const IconComponent = getModalityIcon(modality)} + + + {#if IconComponent} + + {/if} + + {getModalityLabel(modality)} + +{/each} diff --git a/tools/server/webui/src/lib/components/app/misc/BadgeModelName.svelte b/tools/server/webui/src/lib/components/app/misc/BadgeModelName.svelte new file mode 100644 index 0000000000..babdcd90b0 --- /dev/null +++ b/tools/server/webui/src/lib/components/app/misc/BadgeModelName.svelte @@ -0,0 +1,53 @@ + + +{#snippet badge()} + +
+ +
+ + {model} +
+{/snippet} + +{#if model && isModelMode} + {#if showTooltip} + + + {@render badge()} + + + + {onclick ? 'Click for model details' : 'Model name'} + + + {:else} + {@render badge()} + {/if} +{/if} diff --git a/tools/server/webui/src/lib/components/app/server/ServerInfo.svelte b/tools/server/webui/src/lib/components/app/server/ServerInfo.svelte deleted file mode 100644 index 9a43e333c4..0000000000 --- a/tools/server/webui/src/lib/components/app/server/ServerInfo.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - -{#if props} -
- {#if model} - - - - {model} - - {/if} - -
- {#if props.default_generation_settings.n_ctx} - - ctx: {props.default_generation_settings.n_ctx.toLocaleString()} - - {/if} - - {#if modalities.length > 0} - {#each modalities as modality (modality)} - - {#if modality === 'vision'} - - {:else if modality === 'audio'} - - {/if} - - {modality} - - {/each} - {/if} -
-
-{/if} diff --git a/tools/server/webui/src/lib/components/ui/table/index.ts b/tools/server/webui/src/lib/components/ui/table/index.ts new file mode 100644 index 0000000000..99239aeead --- /dev/null +++ b/tools/server/webui/src/lib/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Root from './table.svelte'; +import Body from './table-body.svelte'; +import Caption from './table-caption.svelte'; +import Cell from './table-cell.svelte'; +import Footer from './table-footer.svelte'; +import Head from './table-head.svelte'; +import Header from './table-header.svelte'; +import Row from './table-row.svelte'; + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow +}; diff --git a/tools/server/webui/src/lib/components/ui/table/table-body.svelte b/tools/server/webui/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 0000000000..f8df65cf68 --- /dev/null +++ b/tools/server/webui/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/tools/server/webui/src/lib/components/ui/table/table-caption.svelte b/tools/server/webui/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 0000000000..0fdcc6439c --- /dev/null +++ b/tools/server/webui/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/tools/server/webui/src/lib/components/ui/table/table-cell.svelte b/tools/server/webui/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 0000000000..4506fdfc5b --- /dev/null +++ b/tools/server/webui/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/tools/server/webui/src/lib/components/ui/table/table-footer.svelte b/tools/server/webui/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 0000000000..77e4a64c08 --- /dev/null +++ b/tools/server/webui/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,20 @@ + + +tr]:last:border-b-0', className)} + {...restProps} +> + {@render children?.()} + diff --git a/tools/server/webui/src/lib/components/ui/table/table-head.svelte b/tools/server/webui/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 0000000000..c1c57ad443 --- /dev/null +++ b/tools/server/webui/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/tools/server/webui/src/lib/components/ui/table/table-header.svelte b/tools/server/webui/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 0000000000..eb366739b3 --- /dev/null +++ b/tools/server/webui/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/tools/server/webui/src/lib/components/ui/table/table-row.svelte b/tools/server/webui/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 0000000000..4131d3660a --- /dev/null +++ b/tools/server/webui/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,23 @@ + + +svelte-css-wrapper]:[&>th,td]:bg-muted/50', + className + )} + {...restProps} +> + {@render children?.()} + diff --git a/tools/server/webui/src/lib/components/ui/table/table.svelte b/tools/server/webui/src/lib/components/ui/table/table.svelte new file mode 100644 index 0000000000..c11a6a6c4b --- /dev/null +++ b/tools/server/webui/src/lib/components/ui/table/table.svelte @@ -0,0 +1,22 @@ + + +
+ + {@render children?.()} +
+
diff --git a/tools/server/webui/src/lib/services/chat.ts b/tools/server/webui/src/lib/services/chat.ts index bc5cf680ae..a0fc4a0f5d 100644 --- a/tools/server/webui/src/lib/services/chat.ts +++ b/tools/server/webui/src/lib/services/chat.ts @@ -76,7 +76,6 @@ export class ChatService { onReasoningChunk, onToolCallChunk, onModel, - onFirstValidChunk, // Generation parameters temperature, max_tokens, @@ -225,7 +224,6 @@ export class ChatService { onReasoningChunk, onToolCallChunk, onModel, - onFirstValidChunk, conversationId, abortController.signal ); @@ -300,7 +298,6 @@ export class ChatService { onReasoningChunk?: (chunk: string) => void, onToolCallChunk?: (chunk: string) => void, onModel?: (model: string) => void, - onFirstValidChunk?: () => void, conversationId?: string, abortSignal?: AbortSignal ): Promise { @@ -317,7 +314,6 @@ export class ChatService { let lastTimings: ChatMessageTimings | undefined; let streamFinished = false; let modelEmitted = false; - let firstValidChunkEmitted = false; let toolCallIndexOffset = 0; let hasOpenToolCallBatch = false; @@ -384,15 +380,6 @@ export class ChatService { try { const parsed: ApiChatCompletionStreamChunk = JSON.parse(data); - - if (!firstValidChunkEmitted && parsed.object === 'chat.completion.chunk') { - firstValidChunkEmitted = true; - - if (!abortSignal?.aborted) { - onFirstValidChunk?.(); - } - } - const content = parsed.choices[0]?.delta?.content; const reasoningContent = parsed.choices[0]?.delta?.reasoning_content; const toolCalls = parsed.choices[0]?.delta?.tool_calls; diff --git a/tools/server/webui/src/lib/stores/chat.svelte.ts b/tools/server/webui/src/lib/stores/chat.svelte.ts index c70b9580cb..028ada3287 100644 --- a/tools/server/webui/src/lib/stores/chat.svelte.ts +++ b/tools/server/webui/src/lib/stores/chat.svelte.ts @@ -1,7 +1,6 @@ import { DatabaseStore } from '$lib/stores/database'; import { chatService, slotsService } from '$lib/services'; import { config } from '$lib/stores/settings.svelte'; -import { serverStore } from '$lib/stores/server.svelte'; import { normalizeModelName } from '$lib/utils/model-names'; import { filterByLeafNodeId, findLeafNode, findDescendantMessages } from '$lib/utils/branching'; import { browser } from '$app/environment'; @@ -365,41 +364,15 @@ class ChatStore { let resolvedModel: string | null = null; let modelPersisted = false; - const currentConfig = config(); - const preferServerPropsModel = !currentConfig.modelSelectorEnabled; - let serverPropsRefreshed = false; - let updateModelFromServerProps: ((persistImmediately?: boolean) => void) | null = null; - - const refreshServerPropsOnce = () => { - if (serverPropsRefreshed) { - return; - } - - serverPropsRefreshed = true; - - const hasExistingProps = serverStore.serverProps !== null; - - serverStore - .fetchServerProps({ silent: hasExistingProps }) - .then(() => { - updateModelFromServerProps?.(true); - }) - .catch((error) => { - console.warn('Failed to refresh server props after streaming started:', error); - }); - }; const recordModel = (modelName: string | null | undefined, persistImmediately = true): void => { - const serverModelName = serverStore.modelName; - const preferredModelSource = preferServerPropsModel - ? (serverModelName ?? modelName ?? null) - : (modelName ?? serverModelName ?? null); - - if (!preferredModelSource) { + if (!modelName) { return; } - const normalizedModel = normalizeModelName(preferredModelSource); + const normalizedModel = normalizeModelName(modelName); + + console.log('Resolved model:', normalizedModel); if (!normalizedModel || normalizedModel === resolvedModel) { return; @@ -423,20 +396,6 @@ class ChatStore { } }; - if (preferServerPropsModel) { - updateModelFromServerProps = (persistImmediately = true) => { - const currentServerModel = serverStore.modelName; - - if (!currentServerModel) { - return; - } - - recordModel(currentServerModel, persistImmediately); - }; - - updateModelFromServerProps(false); - } - slotsService.startStreaming(); slotsService.setActiveConversation(assistantMessage.convId); @@ -445,9 +404,6 @@ class ChatStore { { ...this.getApiOptions(), - onFirstValidChunk: () => { - refreshServerPropsOnce(); - }, onChunk: (chunk: string) => { streamedContent += chunk; this.setConversationStreaming( diff --git a/tools/server/webui/src/lib/stores/server.svelte.ts b/tools/server/webui/src/lib/stores/server.svelte.ts index 0620ac3b04..b8d47f295f 100644 --- a/tools/server/webui/src/lib/stores/server.svelte.ts +++ b/tools/server/webui/src/lib/stores/server.svelte.ts @@ -113,8 +113,8 @@ class ServerStore { return this._serverProps.model_path.split(/(\\|\/)/).pop() || null; } - get supportedModalities(): string[] { - const modalities: string[] = []; + get supportedModalities(): ModelModality[] { + const modalities: ModelModality[] = []; if (this._serverProps?.modalities?.audio) { modalities.push(ModelModality.AUDIO); } diff --git a/tools/server/webui/src/lib/utils/model-names.ts b/tools/server/webui/src/lib/utils/model-names.ts index b1ea9d9536..c0a1e1c578 100644 --- a/tools/server/webui/src/lib/utils/model-names.ts +++ b/tools/server/webui/src/lib/utils/model-names.ts @@ -1,16 +1,19 @@ /** - * Normalizes a model name by extracting the filename from a path. + * Normalizes a model name by extracting the filename from a path, but preserves Hugging Face repository format. * * Handles both forward slashes (/) and backslashes (\) as path separators. - * If the model name is just a filename (no path), returns it as-is. + * - If the model name has exactly one slash (org/model format), preserves the full "org/model" name + * - If the model name has no slash or multiple slashes, extracts just the filename + * - If the model name is just a filename (no path), returns it as-is. * * @param modelName - The model name or path to normalize - * @returns The normalized model name (filename only) + * @returns The normalized model name * * @example - * normalizeModelName('models/llama-3.1-8b') // Returns: 'llama-3.1-8b' - * normalizeModelName('C:\\Models\\gpt-4') // Returns: 'gpt-4' - * normalizeModelName('simple-model') // Returns: 'simple-model' + * normalizeModelName('models/llama-3.1-8b') // Returns: 'llama-3.1-8b' (multiple slashes -> filename) + * normalizeModelName('C:\\Models\\gpt-4') // Returns: 'gpt-4' (multiple slashes -> filename) + * normalizeModelName('meta-llama/Llama-3.1-8B') // Returns: 'meta-llama/Llama-3.1-8B' (Hugging Face format) + * normalizeModelName('simple-model') // Returns: 'simple-model' (no slash) * normalizeModelName(' spaced ') // Returns: 'spaced' * normalizeModelName('') // Returns: '' */ @@ -22,6 +25,20 @@ export function normalizeModelName(modelName: string): string { } const segments = trimmed.split(/[\\/]/); + + // If we have exactly 2 segments (one slash), treat it as Hugging Face repo format + // and preserve the full "org/model" format + if (segments.length === 2) { + const [org, model] = segments; + const trimmedOrg = org?.trim(); + const trimmedModel = model?.trim(); + + if (trimmedOrg && trimmedModel) { + return `${trimmedOrg}/${trimmedModel}`; + } + } + + // For other cases (no slash, or multiple slashes), extract just the filename const candidate = segments.pop(); const normalized = candidate?.trim();