2 import { Clock, Gauge, WholeWord, BookOpenText, Sparkles, Wrench, Layers } from '@lucide/svelte';
3 import { ChatMessageStatisticsBadge } from '$lib/components/app';
4 import * as Tooltip from '$lib/components/ui/tooltip';
5 import { ChatMessageStatsView, ChatMessageStatisticsMode } from '$lib/enums';
6 import type { ChatMessageAgenticTimings } from '$lib/types/chat';
7 import { formatPerformanceTime } from '$lib/utils';
8 import { MS_PER_SECOND, DEFAULT_PERFORMANCE_TIME } from '$lib/constants';
9 import type { Component } from 'svelte';
12 predictedTokens?: number;
14 promptTokens?: number;
17 isProcessingPrompt?: boolean;
18 initialView?: ChatMessageStatsView;
19 agenticTimings?: ChatMessageAgenticTimings;
20 onActiveViewChange?: (view: ChatMessageStatsView) => void;
21 hideSummary?: boolean;
22 mode?: ChatMessageStatisticsMode;
31 isProcessingPrompt = false,
32 initialView = ChatMessageStatsView.GENERATION,
36 mode = ChatMessageStatisticsMode.SWITCHABLE
39 let isSwitchable = $derived(mode === ChatMessageStatisticsMode.SWITCHABLE);
41 let activeView: ChatMessageStatsView = $derived(
42 mode === ChatMessageStatisticsMode.READING
43 ? ChatMessageStatsView.READING
44 : mode === ChatMessageStatisticsMode.GENERATION
45 ? ChatMessageStatsView.GENERATION
48 let hasAutoSwitchedToGeneration = $state(false);
52 onActiveViewChange?.(activeView);
56 // In live mode: auto-switch to GENERATION tab when prompt processing completes
58 if (isLive && isSwitchable) {
59 // Auto-switch to generation tab only when prompt processing is done (once)
61 !hasAutoSwitchedToGeneration &&
62 !isProcessingPrompt &&
66 activeView = ChatMessageStatsView.GENERATION;
67 hasAutoSwitchedToGeneration = true;
68 } else if (!hasAutoSwitchedToGeneration) {
69 // Stay on READING while prompt is still being processed
70 activeView = ChatMessageStatsView.READING;
75 let hasGenerationStats = $derived(
76 predictedTokens !== undefined &&
77 predictedTokens > 0 &&
78 predictedMs !== undefined &&
82 let tokensPerSecond = $derived(
83 hasGenerationStats ? (predictedTokens! / predictedMs!) * MS_PER_SECOND : 0
85 let formattedTime = $derived(
86 predictedMs !== undefined ? formatPerformanceTime(predictedMs) : DEFAULT_PERFORMANCE_TIME
89 let promptTokensPerSecond = $derived(
90 promptTokens !== undefined && promptMs !== undefined && promptMs > 0
91 ? (promptTokens / promptMs) * MS_PER_SECOND
95 let formattedPromptTime = $derived(
96 promptMs !== undefined ? formatPerformanceTime(promptMs) : undefined
99 let hasPromptStats = $derived(
100 promptTokens !== undefined &&
101 promptMs !== undefined &&
102 promptTokensPerSecond !== undefined &&
103 formattedPromptTime !== undefined
106 let isGenerationDisabled = $derived(isLive && isSwitchable && !hasGenerationStats);
108 let hasAgenticStats = $derived(agenticTimings !== undefined && agenticTimings.toolCallsCount > 0);
110 let agenticToolsPerSecond = $derived(
111 hasAgenticStats && agenticTimings!.toolsMs > 0
112 ? (agenticTimings!.toolCallsCount / agenticTimings!.toolsMs) * MS_PER_SECOND
116 let formattedAgenticToolsTime = $derived(
117 hasAgenticStats ? formatPerformanceTime(agenticTimings!.toolsMs) : DEFAULT_PERFORMANCE_TIME
120 let agenticTotalTimeMs = $derived(
122 ? agenticTimings!.toolsMs + agenticTimings!.llm.predicted_ms + agenticTimings!.llm.prompt_ms
126 let formattedAgenticTotalTime = $derived(formatPerformanceTime(agenticTotalTimeMs));
129 {#snippet viewButton(opts: {
130 view: ChatMessageStatsView;
136 {@const IconComponent = opts.icon}
139 <!-- prevent another nested button element -->
140 {#snippet child({ props })}
144 class="inline-flex h-5 w-5 items-center justify-center rounded-sm transition-colors {activeView ===
146 ? 'bg-background text-foreground shadow-sm'
148 ? 'cursor-not-allowed opacity-40'
149 : 'hover:text-foreground'}"
150 onclick={() => !opts.disabled && (activeView = opts.view)}
151 disabled={opts.disabled}
153 <IconComponent class="h-3 w-3" />
155 <span class="sr-only">{opts.label}</span>
161 <p>{opts.tooltipText}</p>
166 <div class="inline-flex items-center text-xs text-muted-foreground">
168 <div class="inline-flex items-center rounded-sm bg-muted-foreground/15 p-0.5">
169 {#if hasPromptStats || isLive}
170 {@render viewButton({
171 view: ChatMessageStatsView.READING,
174 tooltipText: 'Processing'
178 {@render viewButton({
179 view: ChatMessageStatsView.GENERATION,
182 tooltipText: isGenerationDisabled ? 'Waiting for tokens...' : 'Generation',
183 disabled: isGenerationDisabled
186 {#if hasAgenticStats}
187 {@render viewButton({
188 view: ChatMessageStatsView.TOOLS,
191 tooltipText: 'Tool calls'
195 {@render viewButton({
196 view: ChatMessageStatsView.SUMMARY,
199 tooltipText: 'Agentic summary'
206 <div class="flex items-center gap-1 px-2">
207 {#if activeView === ChatMessageStatsView.GENERATION && hasGenerationStats}
208 <ChatMessageStatisticsBadge
209 class="bg-transparent"
211 value="{predictedTokens?.toLocaleString()} tokens"
212 tooltipLabel="Generated tokens"
215 <ChatMessageStatisticsBadge
216 class="bg-transparent"
218 value={formattedTime}
219 tooltipLabel="Generation time"
222 <ChatMessageStatisticsBadge
223 class="bg-transparent"
225 value="{tokensPerSecond.toFixed(2)} t/s"
226 tooltipLabel="Generation speed"
228 {:else if activeView === ChatMessageStatsView.TOOLS && hasAgenticStats}
229 <ChatMessageStatisticsBadge
230 class="bg-transparent"
232 value="{agenticTimings!.toolCallsCount} calls"
233 tooltipLabel="Tool calls executed"
236 <ChatMessageStatisticsBadge
237 class="bg-transparent"
239 value={formattedAgenticToolsTime}
240 tooltipLabel="Tool execution time"
243 <ChatMessageStatisticsBadge
244 class="bg-transparent"
246 value="{agenticToolsPerSecond.toFixed(2)} calls/s"
247 tooltipLabel="Tool execution rate"
249 {:else if activeView === ChatMessageStatsView.SUMMARY && hasAgenticStats}
250 <ChatMessageStatisticsBadge
251 class="bg-transparent"
253 value="{agenticTimings!.turns} turns"
254 tooltipLabel="Agentic turns (LLM calls)"
257 <ChatMessageStatisticsBadge
258 class="bg-transparent"
260 value="{agenticTimings!.llm.predicted_n.toLocaleString()} tokens"
261 tooltipLabel="Total tokens generated"
264 <ChatMessageStatisticsBadge
265 class="bg-transparent"
267 value={formattedAgenticTotalTime}
268 tooltipLabel="Total time (LLM + tools)"
270 {:else if hasPromptStats && (mode === ChatMessageStatisticsMode.READING || isSwitchable)}
271 <ChatMessageStatisticsBadge
272 class="bg-transparent"
274 value="{promptTokens} tokens"
275 tooltipLabel="Prompt tokens"
278 <ChatMessageStatisticsBadge
279 class="bg-transparent"
281 value={formattedPromptTime ?? '0s'}
282 tooltipLabel="Prompt processing time"
285 <ChatMessageStatisticsBadge
286 class="bg-transparent"
288 value="{promptTokensPerSecond!.toFixed(2)} tokens/s"
289 tooltipLabel="Prompt processing speed"