Add generation statistics to notebook page

This commit is contained in:
Leszek Hanusz 2026-02-02 18:39:46 +01:00
parent 8a71126e5b
commit 301c3fec7e
5 changed files with 37 additions and 10 deletions

Binary file not shown.

View File

@ -5,7 +5,7 @@
import { Play, Square, Settings } from '@lucide/svelte';
import { config } from '$lib/stores/settings.svelte';
import DialogChatSettings from '$lib/components/app/dialogs/DialogChatSettings.svelte';
import { ModelsSelector } from '$lib/components/app';
import { ModelsSelector, ChatMessageStatistics } from '$lib/components/app';
import { useModelChangeValidation } from '$lib/hooks/use-model-change-validation.svelte';
import { modelsStore, modelOptions, selectedModelId } from '$lib/stores/models.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
@ -24,6 +24,7 @@
import { onMount } from 'svelte';
let disableAutoScroll = $derived(Boolean(config().disableAutoScroll));
let showMessageStats = $derived(config().showMessageStats);
let autoScrollEnabled = $state(true);
let scrollContainer: HTMLTextAreaElement | null = $state(null);
let lastScrollTop = $state(0);
@ -190,19 +191,19 @@
</Button>
</header>
<div class="flex-1 overflow-y-auto p-4 md:p-6">
<div class="flex-1 overflow-y-auto p-2 md:p-4">
<Textarea
bind:ref={scrollContainer}
onscroll={handleScroll}
value={inputContent}
oninput={handleInput}
class="h-full min-h-[500px] w-full resize-none rounded-xl border-none bg-muted p-4 text-base focus-visible:ring-0 md:p-6"
class="h-full min-h-[100px] w-full resize-none rounded-xl border-none bg-muted p-4 text-base focus-visible:ring-0 md:p-6"
placeholder="Enter your text here..."
/>
</div>
<div class="border-t border-border/40 bg-background p-4 md:px-6 md:py-4">
<div class="flex items-center justify-between gap-4">
<div class="bg-background p-2 md:p-4">
<div class="flex flex-col-reverse gap-4 md:flex-row md:items-center md:justify-between">
<div class="flex items-center gap-2">
{#snippet generateButton(props = {})}
<Button
@ -244,6 +245,19 @@
disabled={notebookStore.isGenerating}
/>
</div>
{#if showMessageStats && (notebookStore.promptTokens > 0 || notebookStore.predictedTokens > 0)}
<div class="flex w-full justify-end md:w-auto">
<ChatMessageStatistics
promptTokens={notebookStore.promptTokens}
promptMs={notebookStore.promptMs}
predictedTokens={notebookStore.predictedTokens}
predictedMs={notebookStore.predictedMs}
isLive={notebookStore.isGenerating}
isProcessingPrompt={notebookStore.isGenerating && notebookStore.predictedTokens === 0}
/>
</div>
{/if}
</div>
</div>

View File

@ -96,7 +96,7 @@ export const SETTING_CONFIG_INFO: Record<string, string> = {
'Show raw LLM output without backend parsing and frontend Markdown rendering to inspect streaming across different models.',
keepStatsVisible: 'Keep processing statistics visible after generation finishes.',
showMessageStats:
'Display generation statistics (tokens/second, token count, duration) below each assistant message.',
'Display generation statistics (tokens/second, token count, duration).',
askForTitleConfirmation:
'Ask for confirmation before automatically changing conversation title when editing the first message.',
pdfAsImage:

View File

@ -1019,6 +1019,7 @@ export class ChatService {
const content = parsed.content;
const timings = parsed.timings;
const model = parsed.model;
const promptProgress = parsed.prompt_progress;
if (parsed.stop) {
streamFinished = true;
@ -1029,8 +1030,12 @@ export class ChatService {
onModel?.(model);
}
if (promptProgress) {
ChatService.notifyTimings(undefined, promptProgress, onTimings);
}
if (timings) {
ChatService.notifyTimings(timings, undefined, onTimings);
ChatService.notifyTimings(timings, promptProgress, onTimings);
lastTimings = timings;
}

View File

@ -32,21 +32,29 @@ export class NotebookStore {
...currentConfig,
model: model ?? currentConfig.model,
stream: true,
onChunk: (chunk) => {
timings_per_token: true,
onChunk: (chunk: string) => {
this.content += chunk;
},
onTimings: (timings) => {
onTimings: (timings: ChatMessageTimings, promptProgress: ChatMessagePromptProgress) => {
if (timings) {
if (timings.prompt_n) this.promptTokens = timings.prompt_n;
if (timings.prompt_ms) this.promptMs = timings.prompt_ms;
if (timings.predicted_n) this.predictedTokens = timings.predicted_n;
if (timings.predicted_ms) this.predictedMs = timings.predicted_ms;
}
if (promptProgress) {
// Update prompt stats from progress
const { processed, time_ms } = promptProgress;
if (processed > 0) this.promptTokens = processed;
if (time_ms > 0) this.promptMs = time_ms;
}
},
onComplete: () => {
this.isGenerating = false;
},
onError: (error) => {
onError: (error: unknown) => {
if (error instanceof Error && error.name === 'AbortError') {
// aborted by user
} else {