diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz index e59d8a2470..3089156aad 100644 Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ diff --git a/tools/server/webui/src/lib/components/app/notebook/NotebookScreen.svelte b/tools/server/webui/src/lib/components/app/notebook/NotebookScreen.svelte index 4b0b32d9f2..0c9b388ab2 100644 --- a/tools/server/webui/src/lib/components/app/notebook/NotebookScreen.svelte +++ b/tools/server/webui/src/lib/components/app/notebook/NotebookScreen.svelte @@ -2,7 +2,7 @@ import { notebookStore } from '$lib/stores/notebook.svelte'; import Button from '$lib/components/ui/button/button.svelte'; import Textarea from '$lib/components/ui/textarea/textarea.svelte'; - import { Play, Square, Settings, Undo, Redo } from '@lucide/svelte'; + import { Play, Square, Settings, Undo, Redo, RulerDimensionLine } from '@lucide/svelte'; import { config } from '$lib/stores/settings.svelte'; import DialogChatSettings from '$lib/components/app/dialogs/DialogChatSettings.svelte'; import { ChatMessageStatistics, DialogChatError, KeyboardShortcutInfo, ModelsSelector } from '$lib/components/app'; @@ -41,6 +41,7 @@ // Sync local input with store content $effect(() => { inputContent = notebookStore.content; + notebookStore.updateTokenCount(); }); function handleInput(e: Event) { @@ -335,16 +336,32 @@ /> - {#if showMessageStats && (notebookStore.promptTokens > 0 || notebookStore.predictedTokens > 0)} -
- + {#if showMessageStats} +
+ {#if notebookStore.totalTokens > 0} + + +
+ + {notebookStore.totalTokens} tokens +
+
+ +

Total tokens

+
+
+ {/if} + + {#if notebookStore.promptTokens > 0 || notebookStore.predictedTokens > 0} + + {/if}
{/if}
diff --git a/tools/server/webui/src/lib/services/chat.ts b/tools/server/webui/src/lib/services/chat.ts index 448d51f27e..d2d482024d 100644 --- a/tools/server/webui/src/lib/services/chat.ts +++ b/tools/server/webui/src/lib/services/chat.ts @@ -1,5 +1,17 @@ import { getJsonHeaders } from '$lib/utils'; import { AttachmentType } from '$lib/enums'; +import type { + ApiChatCompletionRequest, + ApiChatCompletionResponse, + ApiChatCompletionStreamChunk, + ApiChatCompletionToolCall, + ApiChatCompletionToolCallDelta, + ApiChatMessageContentPart, + ApiChatMessageData, + ApiCompletionRequest, + ApiCompletionResponse, + ApiCompletionStreamChunk +} from '$lib/types/api'; /** * ChatService - Low-level API communication layer for Chat Completions @@ -1116,4 +1128,31 @@ export class ChatService { throw err; } } + /** + * Tokenizes the provided text using the server's tokenizer. + * + * @param content - The text content to tokenize + * @param signal - Optional AbortSignal + * @returns {Promise} Promise that resolves to an array of token IDs + */ + static async tokenize(content: string, signal?: AbortSignal): Promise { + try { + const response = await fetch('./tokenize', { + method: 'POST', + headers: getJsonHeaders(), + body: JSON.stringify({ content }), + signal + }); + + if (!response.ok) { + throw new Error(`Tokenize failed: ${response.statusText}`); + } + + const data = await response.json(); + return data.tokens; + } catch (error) { + console.error('Tokenize error:', error); + return []; + } + } } diff --git a/tools/server/webui/src/lib/stores/notebook.svelte.ts b/tools/server/webui/src/lib/stores/notebook.svelte.ts index d482085f85..cd3b3391a6 100644 --- a/tools/server/webui/src/lib/stores/notebook.svelte.ts +++ b/tools/server/webui/src/lib/stores/notebook.svelte.ts @@ -11,6 +11,9 @@ export class NotebookStore { promptMs = $state(0); predictedTokens = $state(0); predictedMs = $state(0); + totalTokens = $state(0); + generationStartTokens = $state(0); + tokenizeTimeout: ReturnType | undefined; error = $state<{ message: string; @@ -36,6 +39,9 @@ export class NotebookStore { this.predictedTokens = 0; this.predictedMs = 0; + // Snapshot the current total tokens as the baseline for this generation + this.generationStartTokens = this.totalTokens; + try { const currentConfig = config(); await ChatService.sendCompletion( @@ -62,9 +68,13 @@ export class NotebookStore { if (processed > 0) this.promptTokens = processed; if (time_ms > 0) this.promptMs = time_ms; } + + // Update totalTokens live + this.totalTokens = this.generationStartTokens + this.predictedTokens; }, onComplete: () => { this.isGenerating = false; + this.totalTokens = this.generationStartTokens + this.predictedTokens; }, onError: (error: unknown) => { if (error instanceof Error && error.name === 'AbortError') { @@ -123,6 +133,17 @@ export class NotebookStore { } this.isGenerating = false; } + + updateTokenCount() { + if (this.tokenizeTimeout) { + clearTimeout(this.tokenizeTimeout); + } + + this.tokenizeTimeout = setTimeout(async () => { + const tokens = await ChatService.tokenize(this.content); + this.totalTokens = tokens.length; + }, 500); + } } export const notebookStore = new NotebookStore();