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();