]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
webui: Add switcher to Chat Message UI to show raw LLM output (#19571)
authorAleksander Grygier <redacted>
Thu, 12 Feb 2026 18:55:51 +0000 (19:55 +0100)
committerGitHub <redacted>
Thu, 12 Feb 2026 18:55:51 +0000 (19:55 +0100)
12 files changed:
tools/server/public/index.html.gz
tools/server/webui/docs/flows/settings-flow.md
tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageActions.svelte
tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte
tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettings.svelte
tools/server/webui/src/lib/constants/settings-config.ts
tools/server/webui/src/lib/services/chat.ts
tools/server/webui/src/lib/services/parameter-sync.ts
tools/server/webui/src/lib/stores/chat.svelte.ts
tools/server/webui/src/lib/types/settings.d.ts
tools/server/webui/tests/stories/ChatMessage.stories.svelte

index ae2c8f77a7c83a47f1a1183ece27be472e03e84f..f4ff57b4c979075cb73117619c5a02ad2bb89e5e 100644 (file)
Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ
index 578e01e6e1d3c4fdcb2f30007c9a3c8de70ea98d..474aef01b09e612b0d4eb9909ee27a5317f19a42 100644 (file)
@@ -139,6 +139,6 @@ sequenceDiagram
 
     Note over settingsStore: UI-only (not synced):
     rect rgb(255, 240, 240)
-        Note over settingsStore: systemMessage, custom (JSON)<br/>showStatistics, enableContinueGeneration<br/>autoMicOnEmpty, disableAutoScroll<br/>apiKey, pdfAsImage, disableReasoningFormat
+        Note over settingsStore: systemMessage, custom (JSON)<br/>showStatistics, enableContinueGeneration<br/>autoMicOnEmpty, disableAutoScroll<br/>apiKey, pdfAsImage, disableReasoningParsing, showRawOutputSwitch
     end
 ```
index 3cb48157d8c0b0d5d81daadbbb78f6d1c985c15f..dbd9b9822852e0b5eca9fc24e018d39bd72bff0a 100644 (file)
@@ -5,6 +5,7 @@
                ChatMessageBranchingControls,
                DialogConfirmation
        } from '$lib/components/app';
+       import { Switch } from '$lib/components/ui/switch';
 
        interface Props {
                role: 'user' | 'assistant';
@@ -26,6 +27,9 @@
                onConfirmDelete: () => void;
                onNavigateToSibling?: (siblingId: string) => void;
                onShowDeleteDialogChange: (show: boolean) => void;
+               showRawOutputSwitch?: boolean;
+               rawOutputEnabled?: boolean;
+               onRawOutputToggle?: (enabled: boolean) => void;
        }
 
        let {
                onRegenerate,
                role,
                siblingInfo = null,
-               showDeleteDialog
+               showDeleteDialog,
+               showRawOutputSwitch = false,
+               rawOutputEnabled = false,
+               onRawOutputToggle
        }: Props = $props();
 
        function handleConfirmDelete() {
@@ -51,9 +58,9 @@
        }
 </script>
 
-<div class="relative {justify === 'start' ? 'mt-2' : ''} flex h-6 items-center justify-{justify}">
+<div class="relative {justify === 'start' ? 'mt-2' : ''} flex h-6 items-center justify-between">
        <div
-               class="absolute top-0 {actionsPosition === 'left'
+               class="{actionsPosition === 'left'
                        ? 'left-0'
                        : 'right-0'} flex items-center gap-2 opacity-100 transition-opacity"
        >
                        <ActionButton icon={Trash2} tooltip="Delete" onclick={onDelete} />
                </div>
        </div>
+
+       {#if showRawOutputSwitch}
+               <div class="flex items-center gap-2">
+                       <span class="text-xs text-muted-foreground">Show raw output</span>
+                       <Switch
+                               checked={rawOutputEnabled}
+                               onCheckedChange={(checked) => onRawOutputToggle?.(checked)}
+                       />
+               </div>
+       {/if}
 </div>
 
 <DialogConfirmation
index 2b34b1c20ae65e5685e77cfafac2023fcf9b4de0..1cb6b274b67f18d434e2c22ba54ce3030db1cefd 100644 (file)
@@ -90,6 +90,9 @@
 
        const processingState = useProcessingState();
 
+       // Local state for raw output toggle (per message)
+       let showRawOutput = $state(false);
+
        let currentConfig = $derived(config());
        let isRouter = $derived(isRouterMode());
        let displayedModel = $derived((): string | null => {
                        </div>
                </div>
        {:else if message.role === 'assistant'}
-               {#if config().disableReasoningFormat}
+               {#if showRawOutput}
                        <pre class="raw-output">{messageContent || ''}</pre>
                {:else}
                        <MarkdownContent content={messageContent || ''} />
                        {onConfirmDelete}
                        {onNavigateToSibling}
                        {onShowDeleteDialogChange}
+                       showRawOutputSwitch={currentConfig.showRawOutputSwitch}
+                       rawOutputEnabled={showRawOutput}
+                       onRawOutputToggle={(enabled) => (showRawOutput = enabled)}
                />
        {/if}
 </div>
index 6e26d510cdb0e2d0da19c6fcac3644e14dec5aa6..a5450e6af894be1dafcff8bff095121820ea8f55 100644 (file)
@@ -21,6 +21,7 @@
                chatStore,
                errorDialog,
                isLoading,
+               isChatStreaming,
                isEditing,
                getAddFilesHandler
        } from '$lib/stores/chat.svelte';
@@ -81,7 +82,7 @@
        let isServerLoading = $derived(serverLoading());
        let hasPropsError = $derived(!!serverError());
 
-       let isCurrentConversationLoading = $derived(isLoading());
+       let isCurrentConversationLoading = $derived(isLoading() || isChatStreaming());
 
        let isRouter = $derived(isRouterMode());
 
index 5a668aa300adad3f8d43f8bc9775e84b97ba22e0..967f19bbce53f1eb82481e94215dbc88b26ba2ff 100644 (file)
                                        type: 'checkbox'
                                },
                                {
-                                       key: 'disableReasoningFormat',
-                                       label: 'Show raw LLM output',
+                                       key: 'disableReasoningParsing',
+                                       label: 'Disable reasoning content parsing',
+                                       type: 'checkbox'
+                               },
+                               {
+                                       key: 'showRawOutputSwitch',
+                                       label: 'Enable raw output toggle',
                                        type: 'checkbox'
                                },
                                {
index cac48a557c724fcf209ff1f6fba855cb93415db2..1b959f3b69f217371562731ae0ed1d8725bd1075 100644 (file)
@@ -7,7 +7,8 @@ export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean> =
        theme: 'system',
        showThoughtInProgress: false,
        showToolCalls: false,
-       disableReasoningFormat: false,
+       disableReasoningParsing: false,
+       showRawOutputSwitch: false,
        keepStatsVisible: false,
        showMessageStats: true,
        askForTitleConfirmation: false,
@@ -92,8 +93,10 @@ export const SETTING_CONFIG_INFO: Record<string, string> = {
        showThoughtInProgress: 'Expand thought process by default when generating messages.',
        showToolCalls:
                'Display tool call labels and payloads from Harmony-compatible delta.tool_calls data below assistant messages.',
-       disableReasoningFormat:
-               'Show raw LLM output without backend parsing and frontend Markdown rendering to inspect streaming across different models.',
+       disableReasoningParsing:
+               'Send reasoning_format=none to prevent server-side extraction of reasoning tokens into separate field',
+       showRawOutputSwitch:
+               'Show toggle button to display messages as plain text instead of Markdown-formatted content',
        keepStatsVisible: 'Keep processing statistics visible after generation finishes.',
        showMessageStats:
                'Display generation statistics (tokens/second, token count, duration) below each assistant message.',
index 02fc6381c002cc944dc86a7e5eb71e1c67361999..55af0ce8160fe90a2a9a8480ba3eb42b01bfc0e3 100644 (file)
@@ -90,7 +90,7 @@ export class ChatService {
                        custom,
                        timings_per_token,
                        // Config options
-                       disableReasoningFormat
+                       disableReasoningParsing
                } = options;
 
                const normalizedMessages: ApiChatMessageData[] = messages
@@ -127,7 +127,7 @@ export class ChatService {
                        requestBody.model = options.model;
                }
 
-               requestBody.reasoning_format = disableReasoningFormat ? 'none' : 'auto';
+               requestBody.reasoning_format = disableReasoningParsing ? 'none' : 'auto';
 
                if (temperature !== undefined) requestBody.temperature = temperature;
                if (max_tokens !== undefined) {
index d124cf5c8daf95a9464fea0789784074984550dd..333260701fb4ab057ee924e1b9ba0d71509baed3 100644 (file)
@@ -70,12 +70,6 @@ export const SYNCABLE_PARAMETERS: SyncableParameter[] = [
                canSync: true
        },
        { key: 'showToolCalls', serverKey: 'showToolCalls', type: 'boolean', canSync: true },
-       {
-               key: 'disableReasoningFormat',
-               serverKey: 'disableReasoningFormat',
-               type: 'boolean',
-               canSync: true
-       },
        { key: 'keepStatsVisible', serverKey: 'keepStatsVisible', type: 'boolean', canSync: true },
        { key: 'showMessageStats', serverKey: 'showMessageStats', type: 'boolean', canSync: true },
        {
index 89de4f080c2d5a6537236b9af5e1115166309551..f00f418b4cb4a47cb5a61a358bc7c6af5d2e6d38 100644 (file)
@@ -118,6 +118,16 @@ class ChatStore {
                this.isLoading = this.isChatLoading(convId);
                const streamingState = this.getChatStreaming(convId);
                this.currentResponse = streamingState?.response || '';
+               this.isStreamingActive = streamingState !== undefined;
+               this.setActiveProcessingConversation(convId);
+
+               // Sync streaming content to activeMessages so UI displays current content
+               if (streamingState?.response && streamingState?.messageId) {
+                       const idx = conversationsStore.findMessageIndex(streamingState.messageId);
+                       if (idx !== -1) {
+                               conversationsStore.updateMessageAtIndex(idx, { content: streamingState.response });
+                       }
+               }
        }
 
        /**
@@ -1639,7 +1649,7 @@ class ChatStore {
 
                // Config options needed by ChatService
                if (currentConfig.systemMessage) apiOptions.systemMessage = currentConfig.systemMessage;
-               if (currentConfig.disableReasoningFormat) apiOptions.disableReasoningFormat = true;
+               if (currentConfig.disableReasoningParsing) apiOptions.disableReasoningParsing = true;
 
                if (hasValue(currentConfig.temperature))
                        apiOptions.temperature = Number(currentConfig.temperature);
index 38b3047dd0a18eb17b868b3e57e9d9a30d3f40b3..d894245ec3b44e83531074800d15e3ba63722a2a 100644 (file)
@@ -18,8 +18,8 @@ export interface SettingsChatServiceOptions {
        model?: string;
        // System message to inject
        systemMessage?: string;
-       // Disable reasoning format (use 'none' instead of 'auto')
-       disableReasoningFormat?: boolean;
+       // Disable reasoning parsing (use 'none' instead of 'auto')
+       disableReasoningParsing?: boolean;
        // Generation parameters
        temperature?: number;
        max_tokens?: number;
index 5f4de7d476f1c0f52b55b2426269e2907cbfde19..a3579cf04ea2237b7fa07a18634b1597d21a7e16 100644 (file)
@@ -93,7 +93,7 @@
        }}
        play={async () => {
                const { settingsStore } = await import('$lib/stores/settings.svelte');
-               settingsStore.updateConfig('disableReasoningFormat', false);
+               settingsStore.updateConfig('showRawOutputSwitch', false);
        }}
 />
 
        }}
        play={async () => {
                const { settingsStore } = await import('$lib/stores/settings.svelte');
-               settingsStore.updateConfig('disableReasoningFormat', false);
+               settingsStore.updateConfig('showRawOutputSwitch', false);
        }}
 />
 
        }}
        play={async () => {
                const { settingsStore } = await import('$lib/stores/settings.svelte');
-               settingsStore.updateConfig('disableReasoningFormat', false);
+               settingsStore.updateConfig('showRawOutputSwitch', false);
        }}
 />
 
        }}
        play={async () => {
                const { settingsStore } = await import('$lib/stores/settings.svelte');
-               settingsStore.updateConfig('disableReasoningFormat', true);
+               settingsStore.updateConfig('showRawOutputSwitch', true);
        }}
 />
 
        asChild
        play={async () => {
                const { settingsStore } = await import('$lib/stores/settings.svelte');
-               settingsStore.updateConfig('disableReasoningFormat', false);
+               settingsStore.updateConfig('showRawOutputSwitch', false);
                // Phase 1: Stream reasoning content in chunks
                let reasoningText =
                        'I need to think about this carefully. Let me break down the problem:\n\n1. The user is asking for help with something complex\n2. I should provide a thorough and helpful response\n3. I need to consider multiple approaches\n4. The best solution would be to explain step by step\n\nThis approach will ensure clarity and understanding.';
        }}
        play={async () => {
                const { settingsStore } = await import('$lib/stores/settings.svelte');
-               settingsStore.updateConfig('disableReasoningFormat', false);
+               settingsStore.updateConfig('showRawOutputSwitch', false);
                // Import the chat store to simulate loading state
                const { chatStore } = await import('$lib/stores/chat.svelte');