webui: Add switcher to Chat Message UI to show raw LLM output (#19571)

This commit is contained in:
Aleksander Grygier
2026-02-12 19:55:51 +01:00
committed by GitHub
parent 4b385bfcf8
commit 4c61875bf8
12 changed files with 64 additions and 28 deletions
Binary file not shown.
@@ -139,6 +139,6 @@ sequenceDiagram
Note over settingsStore: UI-only (not synced): Note over settingsStore: UI-only (not synced):
rect rgb(255, 240, 240) 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 end
``` ```
@@ -5,6 +5,7 @@
ChatMessageBranchingControls, ChatMessageBranchingControls,
DialogConfirmation DialogConfirmation
} from '$lib/components/app'; } from '$lib/components/app';
import { Switch } from '$lib/components/ui/switch';
interface Props { interface Props {
role: 'user' | 'assistant'; role: 'user' | 'assistant';
@@ -26,6 +27,9 @@
onConfirmDelete: () => void; onConfirmDelete: () => void;
onNavigateToSibling?: (siblingId: string) => void; onNavigateToSibling?: (siblingId: string) => void;
onShowDeleteDialogChange: (show: boolean) => void; onShowDeleteDialogChange: (show: boolean) => void;
showRawOutputSwitch?: boolean;
rawOutputEnabled?: boolean;
onRawOutputToggle?: (enabled: boolean) => void;
} }
let { let {
@@ -42,7 +46,10 @@
onRegenerate, onRegenerate,
role, role,
siblingInfo = null, siblingInfo = null,
showDeleteDialog showDeleteDialog,
showRawOutputSwitch = false,
rawOutputEnabled = false,
onRawOutputToggle
}: Props = $props(); }: Props = $props();
function handleConfirmDelete() { function handleConfirmDelete() {
@@ -51,9 +58,9 @@
} }
</script> </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 <div
class="absolute top-0 {actionsPosition === 'left' class="{actionsPosition === 'left'
? 'left-0' ? 'left-0'
: 'right-0'} flex items-center gap-2 opacity-100 transition-opacity" : 'right-0'} flex items-center gap-2 opacity-100 transition-opacity"
> >
@@ -81,6 +88,16 @@
<ActionButton icon={Trash2} tooltip="Delete" onclick={onDelete} /> <ActionButton icon={Trash2} tooltip="Delete" onclick={onDelete} />
</div> </div>
</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> </div>
<DialogConfirmation <DialogConfirmation
@@ -90,6 +90,9 @@
const processingState = useProcessingState(); const processingState = useProcessingState();
// Local state for raw output toggle (per message)
let showRawOutput = $state(false);
let currentConfig = $derived(config()); let currentConfig = $derived(config());
let isRouter = $derived(isRouterMode()); let isRouter = $derived(isRouterMode());
let displayedModel = $derived((): string | null => { let displayedModel = $derived((): string | null => {
@@ -238,7 +241,7 @@
</div> </div>
</div> </div>
{:else if message.role === 'assistant'} {:else if message.role === 'assistant'}
{#if config().disableReasoningFormat} {#if showRawOutput}
<pre class="raw-output">{messageContent || ''}</pre> <pre class="raw-output">{messageContent || ''}</pre>
{:else} {:else}
<MarkdownContent content={messageContent || ''} /> <MarkdownContent content={messageContent || ''} />
@@ -352,6 +355,9 @@
{onConfirmDelete} {onConfirmDelete}
{onNavigateToSibling} {onNavigateToSibling}
{onShowDeleteDialogChange} {onShowDeleteDialogChange}
showRawOutputSwitch={currentConfig.showRawOutputSwitch}
rawOutputEnabled={showRawOutput}
onRawOutputToggle={(enabled) => (showRawOutput = enabled)}
/> />
{/if} {/if}
</div> </div>
@@ -21,6 +21,7 @@
chatStore, chatStore,
errorDialog, errorDialog,
isLoading, isLoading,
isChatStreaming,
isEditing, isEditing,
getAddFilesHandler getAddFilesHandler
} from '$lib/stores/chat.svelte'; } from '$lib/stores/chat.svelte';
@@ -81,7 +82,7 @@
let isServerLoading = $derived(serverLoading()); let isServerLoading = $derived(serverLoading());
let hasPropsError = $derived(!!serverError()); let hasPropsError = $derived(!!serverError());
let isCurrentConversationLoading = $derived(isLoading()); let isCurrentConversationLoading = $derived(isLoading() || isChatStreaming());
let isRouter = $derived(isRouterMode()); let isRouter = $derived(isRouterMode());
@@ -254,8 +254,13 @@
type: 'checkbox' type: 'checkbox'
}, },
{ {
key: 'disableReasoningFormat', key: 'disableReasoningParsing',
label: 'Show raw LLM output', label: 'Disable reasoning content parsing',
type: 'checkbox'
},
{
key: 'showRawOutputSwitch',
label: 'Enable raw output toggle',
type: 'checkbox' type: 'checkbox'
}, },
{ {
@@ -7,7 +7,8 @@ export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean> =
theme: 'system', theme: 'system',
showThoughtInProgress: false, showThoughtInProgress: false,
showToolCalls: false, showToolCalls: false,
disableReasoningFormat: false, disableReasoningParsing: false,
showRawOutputSwitch: false,
keepStatsVisible: false, keepStatsVisible: false,
showMessageStats: true, showMessageStats: true,
askForTitleConfirmation: false, askForTitleConfirmation: false,
@@ -92,8 +93,10 @@ export const SETTING_CONFIG_INFO: Record<string, string> = {
showThoughtInProgress: 'Expand thought process by default when generating messages.', showThoughtInProgress: 'Expand thought process by default when generating messages.',
showToolCalls: showToolCalls:
'Display tool call labels and payloads from Harmony-compatible delta.tool_calls data below assistant messages.', 'Display tool call labels and payloads from Harmony-compatible delta.tool_calls data below assistant messages.',
disableReasoningFormat: disableReasoningParsing:
'Show raw LLM output without backend parsing and frontend Markdown rendering to inspect streaming across different models.', '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.', keepStatsVisible: 'Keep processing statistics visible after generation finishes.',
showMessageStats: showMessageStats:
'Display generation statistics (tokens/second, token count, duration) below each assistant message.', 'Display generation statistics (tokens/second, token count, duration) below each assistant message.',
+2 -2
View File
@@ -90,7 +90,7 @@ export class ChatService {
custom, custom,
timings_per_token, timings_per_token,
// Config options // Config options
disableReasoningFormat disableReasoningParsing
} = options; } = options;
const normalizedMessages: ApiChatMessageData[] = messages const normalizedMessages: ApiChatMessageData[] = messages
@@ -127,7 +127,7 @@ export class ChatService {
requestBody.model = options.model; requestBody.model = options.model;
} }
requestBody.reasoning_format = disableReasoningFormat ? 'none' : 'auto'; requestBody.reasoning_format = disableReasoningParsing ? 'none' : 'auto';
if (temperature !== undefined) requestBody.temperature = temperature; if (temperature !== undefined) requestBody.temperature = temperature;
if (max_tokens !== undefined) { if (max_tokens !== undefined) {
@@ -70,12 +70,6 @@ export const SYNCABLE_PARAMETERS: SyncableParameter[] = [
canSync: true canSync: true
}, },
{ key: 'showToolCalls', serverKey: 'showToolCalls', type: 'boolean', 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: 'keepStatsVisible', serverKey: 'keepStatsVisible', type: 'boolean', canSync: true },
{ key: 'showMessageStats', serverKey: 'showMessageStats', type: 'boolean', canSync: true }, { key: 'showMessageStats', serverKey: 'showMessageStats', type: 'boolean', canSync: true },
{ {
@@ -118,6 +118,16 @@ class ChatStore {
this.isLoading = this.isChatLoading(convId); this.isLoading = this.isChatLoading(convId);
const streamingState = this.getChatStreaming(convId); const streamingState = this.getChatStreaming(convId);
this.currentResponse = streamingState?.response || ''; 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 // Config options needed by ChatService
if (currentConfig.systemMessage) apiOptions.systemMessage = currentConfig.systemMessage; if (currentConfig.systemMessage) apiOptions.systemMessage = currentConfig.systemMessage;
if (currentConfig.disableReasoningFormat) apiOptions.disableReasoningFormat = true; if (currentConfig.disableReasoningParsing) apiOptions.disableReasoningParsing = true;
if (hasValue(currentConfig.temperature)) if (hasValue(currentConfig.temperature))
apiOptions.temperature = Number(currentConfig.temperature); apiOptions.temperature = Number(currentConfig.temperature);
+2 -2
View File
@@ -18,8 +18,8 @@ export interface SettingsChatServiceOptions {
model?: string; model?: string;
// System message to inject // System message to inject
systemMessage?: string; systemMessage?: string;
// Disable reasoning format (use 'none' instead of 'auto') // Disable reasoning parsing (use 'none' instead of 'auto')
disableReasoningFormat?: boolean; disableReasoningParsing?: boolean;
// Generation parameters // Generation parameters
temperature?: number; temperature?: number;
max_tokens?: number; max_tokens?: number;
@@ -93,7 +93,7 @@
}} }}
play={async () => { play={async () => {
const { settingsStore } = await import('$lib/stores/settings.svelte'); const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false); settingsStore.updateConfig('showRawOutputSwitch', false);
}} }}
/> />
@@ -105,7 +105,7 @@
}} }}
play={async () => { play={async () => {
const { settingsStore } = await import('$lib/stores/settings.svelte'); const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false); settingsStore.updateConfig('showRawOutputSwitch', false);
}} }}
/> />
@@ -117,7 +117,7 @@
}} }}
play={async () => { play={async () => {
const { settingsStore } = await import('$lib/stores/settings.svelte'); const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false); settingsStore.updateConfig('showRawOutputSwitch', false);
}} }}
/> />
@@ -129,7 +129,7 @@
}} }}
play={async () => { play={async () => {
const { settingsStore } = await import('$lib/stores/settings.svelte'); const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', true); settingsStore.updateConfig('showRawOutputSwitch', true);
}} }}
/> />
@@ -141,7 +141,7 @@
asChild asChild
play={async () => { play={async () => {
const { settingsStore } = await import('$lib/stores/settings.svelte'); const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false); settingsStore.updateConfig('showRawOutputSwitch', false);
// Phase 1: Stream reasoning content in chunks // Phase 1: Stream reasoning content in chunks
let reasoningText = 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.'; '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.';
@@ -193,7 +193,7 @@
}} }}
play={async () => { play={async () => {
const { settingsStore } = await import('$lib/stores/settings.svelte'); const { settingsStore } = await import('$lib/stores/settings.svelte');
settingsStore.updateConfig('disableReasoningFormat', false); settingsStore.updateConfig('showRawOutputSwitch', false);
// Import the chat store to simulate loading state // Import the chat store to simulate loading state
const { chatStore } = await import('$lib/stores/chat.svelte'); const { chatStore } = await import('$lib/stores/chat.svelte');