]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
webui: display prompt processing stats (#18146)
authorPascal <redacted>
Thu, 18 Dec 2025 16:55:03 +0000 (17:55 +0100)
committerGitHub <redacted>
Thu, 18 Dec 2025 16:55:03 +0000 (17:55 +0100)
* webui: display prompt processing stats

* feat: Improve UI of Chat Message Statistics

* chore: update webui build output

* refactor: Post-review improvements

* chore: update webui build output

---------

Co-authored-by: Aleksander Grygier <redacted>
tools/server/public/index.html.gz
tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte
tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageStatistics.svelte
tools/server/webui/src/lib/components/app/misc/BadgeChatStatistic.svelte
tools/server/webui/src/lib/enums/chat.ts [new file with mode: 0644]
tools/server/webui/src/lib/enums/index.ts
tools/server/webui/src/lib/stores/chat.svelte.ts
tools/server/webui/src/lib/types/api.d.ts

index a1d62273b285a65cffab0d9053f5bb00559e2b6b..9e44f03260aa9d5afd793cf361aa64faef60fe11 100644 (file)
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
index 2c9a012eff6fe83b8d3d988efc45fd31f9f0c7c1..8997963f1622aa7914805537e5b408fe1a7a678a 100644 (file)
 
        <div class="info my-6 grid gap-4">
                {#if displayedModel()}
-                       <span class="inline-flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
+                       <div class="inline-flex flex-wrap items-start gap-2 text-xs text-muted-foreground">
                                {#if isRouter}
                                        <ModelsSelector
                                                currentModel={displayedModel()}
 
                                {#if currentConfig.showMessageStats && message.timings && message.timings.predicted_n && message.timings.predicted_ms}
                                        <ChatMessageStatistics
+                                               promptTokens={message.timings.prompt_n}
+                                               promptMs={message.timings.prompt_ms}
                                                predictedTokens={message.timings.predicted_n}
                                                predictedMs={message.timings.predicted_ms}
                                        />
                                {/if}
-                       </span>
+                       </div>
                {/if}
 
                {#if config().showToolCalls}
index a453a310104ce78f87d15ba25b39901bb8d798a2..a39acb1d758689a02f0d97fec89a46cb079ba124 100644 (file)
 <script lang="ts">
-       import { Clock, Gauge, WholeWord } from '@lucide/svelte';
+       import { Clock, Gauge, WholeWord, BookOpenText, Sparkles } from '@lucide/svelte';
        import { BadgeChatStatistic } from '$lib/components/app';
+       import * as Tooltip from '$lib/components/ui/tooltip';
+       import { ChatMessageStatsView } from '$lib/enums';
 
        interface Props {
                predictedTokens: number;
                predictedMs: number;
+               promptTokens?: number;
+               promptMs?: number;
        }
 
-       let { predictedTokens, predictedMs }: Props = $props();
+       let { predictedTokens, predictedMs, promptTokens, promptMs }: Props = $props();
+
+       let activeView: ChatMessageStatsView = $state(ChatMessageStatsView.GENERATION);
 
        let tokensPerSecond = $derived((predictedTokens / predictedMs) * 1000);
        let timeInSeconds = $derived((predictedMs / 1000).toFixed(2));
-</script>
 
-<BadgeChatStatistic icon={WholeWord} value="{predictedTokens} tokens" />
+       let promptTokensPerSecond = $derived(
+               promptTokens !== undefined && promptMs !== undefined
+                       ? (promptTokens / promptMs) * 1000
+                       : undefined
+       );
+
+       let promptTimeInSeconds = $derived(
+               promptMs !== undefined ? (promptMs / 1000).toFixed(2) : undefined
+       );
+
+       let hasPromptStats = $derived(
+               promptTokens !== undefined &&
+                       promptMs !== undefined &&
+                       promptTokensPerSecond !== undefined &&
+                       promptTimeInSeconds !== undefined
+       );
+</script>
 
-<BadgeChatStatistic icon={Clock} value="{timeInSeconds}s" />
+<div class="inline-flex items-center text-xs text-muted-foreground">
+       <div class="inline-flex items-center rounded-sm bg-muted-foreground/15 p-0.5">
+               {#if hasPromptStats}
+                       <Tooltip.Root>
+                               <Tooltip.Trigger>
+                                       <button
+                                               type="button"
+                                               class="inline-flex h-5 w-5 items-center justify-center rounded-sm transition-colors {activeView ===
+                                               ChatMessageStatsView.READING
+                                                       ? 'bg-background text-foreground shadow-sm'
+                                                       : 'hover:text-foreground'}"
+                                               onclick={() => (activeView = ChatMessageStatsView.READING)}
+                                       >
+                                               <BookOpenText class="h-3 w-3" />
+                                               <span class="sr-only">Reading</span>
+                                       </button>
+                               </Tooltip.Trigger>
+                               <Tooltip.Content>
+                                       <p>Reading (prompt processing)</p>
+                               </Tooltip.Content>
+                       </Tooltip.Root>
+               {/if}
+               <Tooltip.Root>
+                       <Tooltip.Trigger>
+                               <button
+                                       type="button"
+                                       class="inline-flex h-5 w-5 items-center justify-center rounded-sm transition-colors {activeView ===
+                                       ChatMessageStatsView.GENERATION
+                                               ? 'bg-background text-foreground shadow-sm'
+                                               : 'hover:text-foreground'}"
+                                       onclick={() => (activeView = ChatMessageStatsView.GENERATION)}
+                               >
+                                       <Sparkles class="h-3 w-3" />
+                                       <span class="sr-only">Generation</span>
+                               </button>
+                       </Tooltip.Trigger>
+                       <Tooltip.Content>
+                               <p>Generation (token output)</p>
+                       </Tooltip.Content>
+               </Tooltip.Root>
+       </div>
 
-<BadgeChatStatistic icon={Gauge} value="{tokensPerSecond.toFixed(2)} tokens/s" />
+       <div class="flex items-center gap-1 px-2">
+               {#if activeView === ChatMessageStatsView.GENERATION}
+                       <BadgeChatStatistic
+                               class="bg-transparent"
+                               icon={WholeWord}
+                               value="{predictedTokens} tokens"
+                               tooltipLabel="Generated tokens"
+                       />
+                       <BadgeChatStatistic
+                               class="bg-transparent"
+                               icon={Clock}
+                               value="{timeInSeconds}s"
+                               tooltipLabel="Generation time"
+                       />
+                       <BadgeChatStatistic
+                               class="bg-transparent"
+                               icon={Gauge}
+                               value="{tokensPerSecond.toFixed(2)} tokens/s"
+                               tooltipLabel="Generation speed"
+                       />
+               {:else if hasPromptStats}
+                       <BadgeChatStatistic
+                               class="bg-transparent"
+                               icon={WholeWord}
+                               value="{promptTokens} tokens"
+                               tooltipLabel="Prompt tokens"
+                       />
+                       <BadgeChatStatistic
+                               class="bg-transparent"
+                               icon={Clock}
+                               value="{promptTimeInSeconds}s"
+                               tooltipLabel="Prompt processing time"
+                       />
+                       <BadgeChatStatistic
+                               class="bg-transparent"
+                               icon={Gauge}
+                               value="{promptTokensPerSecond!.toFixed(2)} tokens/s"
+                               tooltipLabel="Prompt processing speed"
+                       />
+               {/if}
+       </div>
+</div>
index 9e5339cab52c1e3900486a972dd58c0c9503e62d..a2b28d2057755952bbd4f701fe79d50703d73e8a 100644 (file)
@@ -1,5 +1,6 @@
 <script lang="ts">
        import { BadgeInfo } from '$lib/components/app';
+       import * as Tooltip from '$lib/components/ui/tooltip';
        import { copyToClipboard } from '$lib/utils';
        import type { Component } from 'svelte';
 
@@ -7,19 +8,37 @@
                class?: string;
                icon: Component;
                value: string | number;
+               tooltipLabel?: string;
        }
 
-       let { class: className = '', icon: Icon, value }: Props = $props();
+       let { class: className = '', icon: Icon, value, tooltipLabel }: Props = $props();
 
        function handleClick() {
                void copyToClipboard(String(value));
        }
 </script>
 
-<BadgeInfo class={className} onclick={handleClick}>
-       {#snippet icon()}
-               <Icon class="h-3 w-3" />
-       {/snippet}
+{#if tooltipLabel}
+       <Tooltip.Root>
+               <Tooltip.Trigger>
+                       <BadgeInfo class={className} onclick={handleClick}>
+                               {#snippet icon()}
+                                       <Icon class="h-3 w-3" />
+                               {/snippet}
 
-       {value}
-</BadgeInfo>
+                               {value}
+                       </BadgeInfo>
+               </Tooltip.Trigger>
+               <Tooltip.Content>
+                       <p>{tooltipLabel}</p>
+               </Tooltip.Content>
+       </Tooltip.Root>
+{:else}
+       <BadgeInfo class={className} onclick={handleClick}>
+               {#snippet icon()}
+                       <Icon class="h-3 w-3" />
+               {/snippet}
+
+               {value}
+       </BadgeInfo>
+{/if}
diff --git a/tools/server/webui/src/lib/enums/chat.ts b/tools/server/webui/src/lib/enums/chat.ts
new file mode 100644 (file)
index 0000000..2b9eb7b
--- /dev/null
@@ -0,0 +1,4 @@
+export enum ChatMessageStatsView {
+       GENERATION = 'generation',
+       READING = 'reading'
+}
index d9e90014705d4c94ffa36c5856b9824dd74e2c41..83c86caf66928c06a024b446c7dc11a8344e4cae 100644 (file)
@@ -1,5 +1,7 @@
 export { AttachmentType } from './attachment';
 
+export { ChatMessageStatsView } from './chat';
+
 export {
        FileTypeCategory,
        FileTypeImage,
index 4f78840a57d2aa208ebb49f610d6c470865be4fa..e0431ee643883e5e0052d3f9d60f3d25dacf73db 100644 (file)
@@ -171,6 +171,7 @@ class ChatStore {
        updateProcessingStateFromTimings(
                timingData: {
                        prompt_n: number;
+                       prompt_ms?: number;
                        predicted_n: number;
                        predicted_per_second: number;
                        cache_n: number;
@@ -212,6 +213,7 @@ class ChatStore {
                        if (message.role === 'assistant' && message.timings) {
                                const restoredState = this.parseTimingData({
                                        prompt_n: message.timings.prompt_n || 0,
+                                       prompt_ms: message.timings.prompt_ms,
                                        predicted_n: message.timings.predicted_n || 0,
                                        predicted_per_second:
                                                message.timings.predicted_n && message.timings.predicted_ms
@@ -282,6 +284,7 @@ class ChatStore {
 
        private parseTimingData(timingData: Record<string, unknown>): ApiProcessingState | null {
                const promptTokens = (timingData.prompt_n as number) || 0;
+               const promptMs = (timingData.prompt_ms as number) || undefined;
                const predictedTokens = (timingData.predicted_n as number) || 0;
                const tokensPerSecond = (timingData.predicted_per_second as number) || 0;
                const cacheTokens = (timingData.cache_n as number) || 0;
@@ -320,6 +323,7 @@ class ChatStore {
                        speculative: false,
                        progressPercent,
                        promptTokens,
+                       promptMs,
                        cacheTokens
                };
        }
@@ -536,6 +540,7 @@ class ChatStore {
                                        this.updateProcessingStateFromTimings(
                                                {
                                                        prompt_n: timings?.prompt_n || 0,
+                                                       prompt_ms: timings?.prompt_ms,
                                                        predicted_n: timings?.predicted_n || 0,
                                                        predicted_per_second: tokensPerSecond,
                                                        cache_n: timings?.cache_n || 0,
@@ -768,10 +773,11 @@ class ChatStore {
                                        content: streamingState.response
                                };
                                if (lastMessage.thinking?.trim()) updateData.thinking = lastMessage.thinking;
-                               const lastKnownState = this.getCurrentProcessingStateSync();
+                               const lastKnownState = this.getProcessingState(conversationId);
                                if (lastKnownState) {
                                        updateData.timings = {
                                                prompt_n: lastKnownState.promptTokens || 0,
+                                               prompt_ms: lastKnownState.promptMs,
                                                predicted_n: lastKnownState.tokensDecoded || 0,
                                                cache_n: lastKnownState.cacheTokens || 0,
                                                predicted_ms:
@@ -1253,6 +1259,7 @@ class ChatStore {
                                                this.updateProcessingStateFromTimings(
                                                        {
                                                                prompt_n: timings?.prompt_n || 0,
+                                                               prompt_ms: timings?.prompt_ms,
                                                                predicted_n: timings?.predicted_n || 0,
                                                                predicted_per_second: tokensPerSecond,
                                                                cache_n: timings?.cache_n || 0,
index c3f47077f5f2f828a8215b47e598c8645d227f3b..e5fde24c75d3d4ce7ecf583bbb847a528f5488d2 100644 (file)
@@ -342,6 +342,7 @@ export interface ApiProcessingState {
        // Progress information from prompt_progress
        progressPercent?: number;
        promptTokens?: number;
+       promptMs?: number;
        cacheTokens?: number;
 }