Skip to content

Commit f9ec885

Browse files
webui: display prompt processing stats (#18146)
* 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 <aleksander.grygier@gmail.com>
1 parent f716588 commit f9ec885

File tree

8 files changed

+153
-16
lines changed

8 files changed

+153
-16
lines changed

tools/server/public/index.html.gz

1.14 KB
Binary file not shown.

tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@
244244

245245
<div class="info my-6 grid gap-4">
246246
{#if displayedModel()}
247-
<span class="inline-flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
247+
<div class="inline-flex flex-wrap items-start gap-2 text-xs text-muted-foreground">
248248
{#if isRouter}
249249
<ModelsSelector
250250
currentModel={displayedModel()}
@@ -258,11 +258,13 @@
258258

259259
{#if currentConfig.showMessageStats && message.timings && message.timings.predicted_n && message.timings.predicted_ms}
260260
<ChatMessageStatistics
261+
promptTokens={message.timings.prompt_n}
262+
promptMs={message.timings.prompt_ms}
261263
predictedTokens={message.timings.predicted_n}
262264
predictedMs={message.timings.predicted_ms}
263265
/>
264266
{/if}
265-
</span>
267+
</div>
266268
{/if}
267269

268270
{#if config().showToolCalls}
Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,122 @@
11
<script lang="ts">
2-
import { Clock, Gauge, WholeWord } from '@lucide/svelte';
2+
import { Clock, Gauge, WholeWord, BookOpenText, Sparkles } from '@lucide/svelte';
33
import { BadgeChatStatistic } from '$lib/components/app';
4+
import * as Tooltip from '$lib/components/ui/tooltip';
5+
import { ChatMessageStatsView } from '$lib/enums';
46
57
interface Props {
68
predictedTokens: number;
79
predictedMs: number;
10+
promptTokens?: number;
11+
promptMs?: number;
812
}
913
10-
let { predictedTokens, predictedMs }: Props = $props();
14+
let { predictedTokens, predictedMs, promptTokens, promptMs }: Props = $props();
15+
16+
let activeView: ChatMessageStatsView = $state(ChatMessageStatsView.GENERATION);
1117
1218
let tokensPerSecond = $derived((predictedTokens / predictedMs) * 1000);
1319
let timeInSeconds = $derived((predictedMs / 1000).toFixed(2));
14-
</script>
1520
16-
<BadgeChatStatistic icon={WholeWord} value="{predictedTokens} tokens" />
21+
let promptTokensPerSecond = $derived(
22+
promptTokens !== undefined && promptMs !== undefined
23+
? (promptTokens / promptMs) * 1000
24+
: undefined
25+
);
26+
27+
let promptTimeInSeconds = $derived(
28+
promptMs !== undefined ? (promptMs / 1000).toFixed(2) : undefined
29+
);
30+
31+
let hasPromptStats = $derived(
32+
promptTokens !== undefined &&
33+
promptMs !== undefined &&
34+
promptTokensPerSecond !== undefined &&
35+
promptTimeInSeconds !== undefined
36+
);
37+
</script>
1738

18-
<BadgeChatStatistic icon={Clock} value="{timeInSeconds}s" />
39+
<div class="inline-flex items-center text-xs text-muted-foreground">
40+
<div class="inline-flex items-center rounded-sm bg-muted-foreground/15 p-0.5">
41+
{#if hasPromptStats}
42+
<Tooltip.Root>
43+
<Tooltip.Trigger>
44+
<button
45+
type="button"
46+
class="inline-flex h-5 w-5 items-center justify-center rounded-sm transition-colors {activeView ===
47+
ChatMessageStatsView.READING
48+
? 'bg-background text-foreground shadow-sm'
49+
: 'hover:text-foreground'}"
50+
onclick={() => (activeView = ChatMessageStatsView.READING)}
51+
>
52+
<BookOpenText class="h-3 w-3" />
53+
<span class="sr-only">Reading</span>
54+
</button>
55+
</Tooltip.Trigger>
56+
<Tooltip.Content>
57+
<p>Reading (prompt processing)</p>
58+
</Tooltip.Content>
59+
</Tooltip.Root>
60+
{/if}
61+
<Tooltip.Root>
62+
<Tooltip.Trigger>
63+
<button
64+
type="button"
65+
class="inline-flex h-5 w-5 items-center justify-center rounded-sm transition-colors {activeView ===
66+
ChatMessageStatsView.GENERATION
67+
? 'bg-background text-foreground shadow-sm'
68+
: 'hover:text-foreground'}"
69+
onclick={() => (activeView = ChatMessageStatsView.GENERATION)}
70+
>
71+
<Sparkles class="h-3 w-3" />
72+
<span class="sr-only">Generation</span>
73+
</button>
74+
</Tooltip.Trigger>
75+
<Tooltip.Content>
76+
<p>Generation (token output)</p>
77+
</Tooltip.Content>
78+
</Tooltip.Root>
79+
</div>
1980

20-
<BadgeChatStatistic icon={Gauge} value="{tokensPerSecond.toFixed(2)} tokens/s" />
81+
<div class="flex items-center gap-1 px-2">
82+
{#if activeView === ChatMessageStatsView.GENERATION}
83+
<BadgeChatStatistic
84+
class="bg-transparent"
85+
icon={WholeWord}
86+
value="{predictedTokens} tokens"
87+
tooltipLabel="Generated tokens"
88+
/>
89+
<BadgeChatStatistic
90+
class="bg-transparent"
91+
icon={Clock}
92+
value="{timeInSeconds}s"
93+
tooltipLabel="Generation time"
94+
/>
95+
<BadgeChatStatistic
96+
class="bg-transparent"
97+
icon={Gauge}
98+
value="{tokensPerSecond.toFixed(2)} tokens/s"
99+
tooltipLabel="Generation speed"
100+
/>
101+
{:else if hasPromptStats}
102+
<BadgeChatStatistic
103+
class="bg-transparent"
104+
icon={WholeWord}
105+
value="{promptTokens} tokens"
106+
tooltipLabel="Prompt tokens"
107+
/>
108+
<BadgeChatStatistic
109+
class="bg-transparent"
110+
icon={Clock}
111+
value="{promptTimeInSeconds}s"
112+
tooltipLabel="Prompt processing time"
113+
/>
114+
<BadgeChatStatistic
115+
class="bg-transparent"
116+
icon={Gauge}
117+
value="{promptTokensPerSecond!.toFixed(2)} tokens/s"
118+
tooltipLabel="Prompt processing speed"
119+
/>
120+
{/if}
121+
</div>
122+
</div>
Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,44 @@
11
<script lang="ts">
22
import { BadgeInfo } from '$lib/components/app';
3+
import * as Tooltip from '$lib/components/ui/tooltip';
34
import { copyToClipboard } from '$lib/utils';
45
import type { Component } from 'svelte';
56
67
interface Props {
78
class?: string;
89
icon: Component;
910
value: string | number;
11+
tooltipLabel?: string;
1012
}
1113
12-
let { class: className = '', icon: Icon, value }: Props = $props();
14+
let { class: className = '', icon: Icon, value, tooltipLabel }: Props = $props();
1315
1416
function handleClick() {
1517
void copyToClipboard(String(value));
1618
}
1719
</script>
1820

19-
<BadgeInfo class={className} onclick={handleClick}>
20-
{#snippet icon()}
21-
<Icon class="h-3 w-3" />
22-
{/snippet}
21+
{#if tooltipLabel}
22+
<Tooltip.Root>
23+
<Tooltip.Trigger>
24+
<BadgeInfo class={className} onclick={handleClick}>
25+
{#snippet icon()}
26+
<Icon class="h-3 w-3" />
27+
{/snippet}
2328

24-
{value}
25-
</BadgeInfo>
29+
{value}
30+
</BadgeInfo>
31+
</Tooltip.Trigger>
32+
<Tooltip.Content>
33+
<p>{tooltipLabel}</p>
34+
</Tooltip.Content>
35+
</Tooltip.Root>
36+
{:else}
37+
<BadgeInfo class={className} onclick={handleClick}>
38+
{#snippet icon()}
39+
<Icon class="h-3 w-3" />
40+
{/snippet}
41+
42+
{value}
43+
</BadgeInfo>
44+
{/if}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum ChatMessageStatsView {
2+
GENERATION = 'generation',
3+
READING = 'reading'
4+
}

tools/server/webui/src/lib/enums/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export { AttachmentType } from './attachment';
22

3+
export { ChatMessageStatsView } from './chat';
4+
35
export {
46
FileTypeCategory,
57
FileTypeImage,

tools/server/webui/src/lib/stores/chat.svelte.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ class ChatStore {
171171
updateProcessingStateFromTimings(
172172
timingData: {
173173
prompt_n: number;
174+
prompt_ms?: number;
174175
predicted_n: number;
175176
predicted_per_second: number;
176177
cache_n: number;
@@ -212,6 +213,7 @@ class ChatStore {
212213
if (message.role === 'assistant' && message.timings) {
213214
const restoredState = this.parseTimingData({
214215
prompt_n: message.timings.prompt_n || 0,
216+
prompt_ms: message.timings.prompt_ms,
215217
predicted_n: message.timings.predicted_n || 0,
216218
predicted_per_second:
217219
message.timings.predicted_n && message.timings.predicted_ms
@@ -282,6 +284,7 @@ class ChatStore {
282284

283285
private parseTimingData(timingData: Record<string, unknown>): ApiProcessingState | null {
284286
const promptTokens = (timingData.prompt_n as number) || 0;
287+
const promptMs = (timingData.prompt_ms as number) || undefined;
285288
const predictedTokens = (timingData.predicted_n as number) || 0;
286289
const tokensPerSecond = (timingData.predicted_per_second as number) || 0;
287290
const cacheTokens = (timingData.cache_n as number) || 0;
@@ -320,6 +323,7 @@ class ChatStore {
320323
speculative: false,
321324
progressPercent,
322325
promptTokens,
326+
promptMs,
323327
cacheTokens
324328
};
325329
}
@@ -536,6 +540,7 @@ class ChatStore {
536540
this.updateProcessingStateFromTimings(
537541
{
538542
prompt_n: timings?.prompt_n || 0,
543+
prompt_ms: timings?.prompt_ms,
539544
predicted_n: timings?.predicted_n || 0,
540545
predicted_per_second: tokensPerSecond,
541546
cache_n: timings?.cache_n || 0,
@@ -768,10 +773,11 @@ class ChatStore {
768773
content: streamingState.response
769774
};
770775
if (lastMessage.thinking?.trim()) updateData.thinking = lastMessage.thinking;
771-
const lastKnownState = this.getCurrentProcessingStateSync();
776+
const lastKnownState = this.getProcessingState(conversationId);
772777
if (lastKnownState) {
773778
updateData.timings = {
774779
prompt_n: lastKnownState.promptTokens || 0,
780+
prompt_ms: lastKnownState.promptMs,
775781
predicted_n: lastKnownState.tokensDecoded || 0,
776782
cache_n: lastKnownState.cacheTokens || 0,
777783
predicted_ms:
@@ -1253,6 +1259,7 @@ class ChatStore {
12531259
this.updateProcessingStateFromTimings(
12541260
{
12551261
prompt_n: timings?.prompt_n || 0,
1262+
prompt_ms: timings?.prompt_ms,
12561263
predicted_n: timings?.predicted_n || 0,
12571264
predicted_per_second: tokensPerSecond,
12581265
cache_n: timings?.cache_n || 0,

tools/server/webui/src/lib/types/api.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ export interface ApiProcessingState {
342342
// Progress information from prompt_progress
343343
progressPercent?: number;
344344
promptTokens?: number;
345+
promptMs?: number;
345346
cacheTokens?: number;
346347
}
347348

0 commit comments

Comments
 (0)