1<script lang="ts">
2 import { Clock, Gauge, WholeWord, BookOpenText, Sparkles } from '@lucide/svelte';
3 import { BadgeChatStatistic } from '$lib/components/app';
4 import * as Tooltip from '$lib/components/ui/tooltip';
5 import { ChatMessageStatsView } from '$lib/enums';
6
7 interface Props {
8 predictedTokens?: number;
9 predictedMs?: number;
10 promptTokens?: number;
11 promptMs?: number;
12 // Live mode: when true, shows stats during streaming
13 isLive?: boolean;
14 // Whether prompt processing is still in progress
15 isProcessingPrompt?: boolean;
16 // Initial view to show (defaults to READING in live mode)
17 initialView?: ChatMessageStatsView;
18 }
19
20 let {
21 predictedTokens,
22 predictedMs,
23 promptTokens,
24 promptMs,
25 isLive = false,
26 isProcessingPrompt = false,
27 initialView = ChatMessageStatsView.GENERATION
28 }: Props = $props();
29
30 let activeView: ChatMessageStatsView = $state(initialView);
31 let hasAutoSwitchedToGeneration = $state(false);
32
33 // In live mode: auto-switch to GENERATION tab when prompt processing completes
34 $effect(() => {
35 if (isLive) {
36 // Auto-switch to generation tab only when prompt processing is done (once)
37 if (
38 !hasAutoSwitchedToGeneration &&
39 !isProcessingPrompt &&
40 predictedTokens &&
41 predictedTokens > 0
42 ) {
43 activeView = ChatMessageStatsView.GENERATION;
44 hasAutoSwitchedToGeneration = true;
45 } else if (!hasAutoSwitchedToGeneration) {
46 // Stay on READING while prompt is still being processed
47 activeView = ChatMessageStatsView.READING;
48 }
49 }
50 });
51
52 let hasGenerationStats = $derived(
53 predictedTokens !== undefined &&
54 predictedTokens > 0 &&
55 predictedMs !== undefined &&
56 predictedMs > 0
57 );
58
59 let tokensPerSecond = $derived(hasGenerationStats ? (predictedTokens! / predictedMs!) * 1000 : 0);
60 let timeInSeconds = $derived(
61 predictedMs !== undefined ? (predictedMs / 1000).toFixed(2) : '0.00'
62 );
63
64 let promptTokensPerSecond = $derived(
65 promptTokens !== undefined && promptMs !== undefined && promptMs > 0
66 ? (promptTokens / promptMs) * 1000
67 : undefined
68 );
69
70 let promptTimeInSeconds = $derived(
71 promptMs !== undefined ? (promptMs / 1000).toFixed(2) : undefined
72 );
73
74 let hasPromptStats = $derived(
75 promptTokens !== undefined &&
76 promptMs !== undefined &&
77 promptTokensPerSecond !== undefined &&
78 promptTimeInSeconds !== undefined
79 );
80
81 // In live mode, generation tab is disabled until we have generation stats
82 let isGenerationDisabled = $derived(isLive && !hasGenerationStats);
83</script>
84
85<div class="inline-flex items-center text-xs text-muted-foreground">
86 <div class="inline-flex items-center rounded-sm bg-muted-foreground/15 p-0.5">
87 {#if hasPromptStats || isLive}
88 <Tooltip.Root>
89 <Tooltip.Trigger>
90 <button
91 type="button"
92 class="inline-flex h-5 w-5 items-center justify-center rounded-sm transition-colors {activeView ===
93 ChatMessageStatsView.READING
94 ? 'bg-background text-foreground shadow-sm'
95 : 'hover:text-foreground'}"
96 onclick={() => (activeView = ChatMessageStatsView.READING)}
97 >
98 <BookOpenText class="h-3 w-3" />
99 <span class="sr-only">Reading</span>
100 </button>
101 </Tooltip.Trigger>
102 <Tooltip.Content>
103 <p>Reading (prompt processing)</p>
104 </Tooltip.Content>
105 </Tooltip.Root>
106 {/if}
107 <Tooltip.Root>
108 <Tooltip.Trigger>
109 <button
110 type="button"
111 class="inline-flex h-5 w-5 items-center justify-center rounded-sm transition-colors {activeView ===
112 ChatMessageStatsView.GENERATION
113 ? 'bg-background text-foreground shadow-sm'
114 : isGenerationDisabled
115 ? 'cursor-not-allowed opacity-40'
116 : 'hover:text-foreground'}"
117 onclick={() => !isGenerationDisabled && (activeView = ChatMessageStatsView.GENERATION)}
118 disabled={isGenerationDisabled}
119 >
120 <Sparkles class="h-3 w-3" />
121 <span class="sr-only">Generation</span>
122 </button>
123 </Tooltip.Trigger>
124 <Tooltip.Content>
125 <p>
126 {isGenerationDisabled
127 ? 'Generation (waiting for tokens...)'
128 : 'Generation (token output)'}
129 </p>
130 </Tooltip.Content>
131 </Tooltip.Root>
132 </div>
133
134 <div class="flex items-center gap-1 px-2">
135 {#if activeView === ChatMessageStatsView.GENERATION && hasGenerationStats}
136 <BadgeChatStatistic
137 class="bg-transparent"
138 icon={WholeWord}
139 value="{predictedTokens?.toLocaleString()} tokens"
140 tooltipLabel="Generated tokens"
141 />
142 <BadgeChatStatistic
143 class="bg-transparent"
144 icon={Clock}
145 value="{timeInSeconds}s"
146 tooltipLabel="Generation time"
147 />
148 <BadgeChatStatistic
149 class="bg-transparent"
150 icon={Gauge}
151 value="{tokensPerSecond.toFixed(2)} tokens/s"
152 tooltipLabel="Generation speed"
153 />
154 {:else if hasPromptStats}
155 <BadgeChatStatistic
156 class="bg-transparent"
157 icon={WholeWord}
158 value="{promptTokens} tokens"
159 tooltipLabel="Prompt tokens"
160 />
161 <BadgeChatStatistic
162 class="bg-transparent"
163 icon={Clock}
164 value="{promptTimeInSeconds}s"
165 tooltipLabel="Prompt processing time"
166 />
167 <BadgeChatStatistic
168 class="bg-transparent"
169 icon={Gauge}
170 value="{promptTokensPerSecond!.toFixed(2)} tokens/s"
171 tooltipLabel="Prompt processing speed"
172 />
173 {/if}
174 </div>
175</div>