diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz index c1e6841d38d2b..4575db67a1f02 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/utils/thinking.ts b/tools/server/webui/src/lib/utils/thinking.ts index 11ce871231a66..bed13fcecf159 100644 --- a/tools/server/webui/src/lib/utils/thinking.ts +++ b/tools/server/webui/src/lib/utils/thinking.ts @@ -1,7 +1,8 @@ /** - * Parses thinking content from a message that may contain tags + * Parses thinking content from a message that may contain tags or [THINK] tags * Returns an object with thinking content and cleaned message content - * Handles both complete ... blocks and incomplete blocks (streaming) + * Handles both complete blocks and incomplete blocks (streaming) + * Supports formats: ... and [THINK]...[/THINK] * @param content - The message content to parse * @returns An object containing the extracted thinking content and the cleaned message content */ @@ -9,12 +10,11 @@ export function parseThinkingContent(content: string): { thinking: string | null; cleanContent: string; } { - const incompleteMatch = content.includes('') && !content.includes(''); + const incompleteThinkMatch = content.includes('') && !content.includes(''); + const incompleteThinkBracketMatch = content.includes('[THINK]') && !content.includes('[/THINK]'); - if (incompleteMatch) { - // Remove the entire ... part from clean content + if (incompleteThinkMatch) { const cleanContent = content.split('')?.[1]?.trim(); - // Extract everything after as thinking content const thinkingContent = content.split('')?.[1]?.trim(); return { @@ -23,12 +23,40 @@ export function parseThinkingContent(content: string): { }; } - const completeMatch = content.includes(''); + if (incompleteThinkBracketMatch) { + const cleanContent = content.split('[/THINK]')?.[1]?.trim(); + const thinkingContent = content.split('[THINK]')?.[1]?.trim(); - if (completeMatch) { return { - thinking: content.split('')?.[0]?.trim(), - cleanContent: content.split('')?.[1]?.trim() + cleanContent, + thinking: thinkingContent + }; + } + + const completeThinkMatch = content.match(/([\s\S]*?)<\/think>/); + const completeThinkBracketMatch = content.match(/\[THINK\]([\s\S]*?)\[\/THINK\]/); + + if (completeThinkMatch) { + const thinkingContent = completeThinkMatch[1]?.trim() ?? ''; + const cleanContent = `${content.slice(0, completeThinkMatch.index ?? 0)}${content.slice( + (completeThinkMatch.index ?? 0) + completeThinkMatch[0].length + )}`.trim(); + + return { + thinking: thinkingContent, + cleanContent + }; + } + + if (completeThinkBracketMatch) { + const thinkingContent = completeThinkBracketMatch[1]?.trim() ?? ''; + const cleanContent = `${content.slice(0, completeThinkBracketMatch.index ?? 0)}${content.slice( + (completeThinkBracketMatch.index ?? 0) + completeThinkBracketMatch[0].length + )}`.trim(); + + return { + thinking: thinkingContent, + cleanContent }; } @@ -39,26 +67,33 @@ export function parseThinkingContent(content: string): { } /** - * Checks if content contains an opening tag (for streaming) + * Checks if content contains an opening thinking tag (for streaming) + * Supports both and [THINK] formats * @param content - The message content to check - * @returns True if the content contains an opening tag + * @returns True if the content contains an opening thinking tag */ export function hasThinkingStart(content: string): boolean { - return content.includes('') || content.includes('<|channel|>analysis'); + return ( + content.includes('') || + content.includes('[THINK]') || + content.includes('<|channel|>analysis') + ); } /** - * Checks if content contains a closing tag (for streaming) + * Checks if content contains a closing thinking tag (for streaming) + * Supports both and [/THINK] formats * @param content - The message content to check - * @returns True if the content contains a closing tag + * @returns True if the content contains a closing thinking tag */ export function hasThinkingEnd(content: string): boolean { - return content.includes(''); + return content.includes('') || content.includes('[/THINK]'); } /** * Extracts partial thinking content during streaming - * Used when we have but not yet + * Supports both and [THINK] formats + * Used when we have opening tag but not yet closing tag * @param content - The message content to extract partial thinking from * @returns An object containing the extracted partial thinking content and the remaining content */ @@ -66,23 +101,41 @@ export function extractPartialThinking(content: string): { thinking: string | null; remainingContent: string; } { - const startIndex = content.indexOf(''); - if (startIndex === -1) { - return { thinking: null, remainingContent: content }; - } + const thinkStartIndex = content.indexOf(''); + const thinkEndIndex = content.indexOf(''); - const endIndex = content.indexOf(''); - if (endIndex === -1) { - // Still streaming thinking content - const thinkingStart = startIndex + ''.length; - return { - thinking: content.substring(thinkingStart), - remainingContent: content.substring(0, startIndex) - }; + const bracketStartIndex = content.indexOf('[THINK]'); + const bracketEndIndex = content.indexOf('[/THINK]'); + + const useThinkFormat = + thinkStartIndex !== -1 && (bracketStartIndex === -1 || thinkStartIndex < bracketStartIndex); + const useBracketFormat = + bracketStartIndex !== -1 && (thinkStartIndex === -1 || bracketStartIndex < thinkStartIndex); + + if (useThinkFormat) { + if (thinkEndIndex === -1) { + const thinkingStart = thinkStartIndex + ''.length; + + return { + thinking: content.substring(thinkingStart), + remainingContent: content.substring(0, thinkStartIndex) + }; + } + } else if (useBracketFormat) { + if (bracketEndIndex === -1) { + const thinkingStart = bracketStartIndex + '[THINK]'.length; + + return { + thinking: content.substring(thinkingStart), + remainingContent: content.substring(0, bracketStartIndex) + }; + } + } else { + return { thinking: null, remainingContent: content }; } - // Complete thinking block found const parsed = parseThinkingContent(content); + return { thinking: parsed.thinking, remainingContent: parsed.cleanContent diff --git a/tools/server/webui/src/stories/ChatMessage.stories.svelte b/tools/server/webui/src/stories/ChatMessage.stories.svelte index f9d7d5358cc7e..c6377e23cb6fd 100644 --- a/tools/server/webui/src/stories/ChatMessage.stories.svelte +++ b/tools/server/webui/src/stories/ChatMessage.stories.svelte @@ -59,6 +59,60 @@ thinking: '', children: [] }); + + // Message with format thinking content + const thinkTagMessage: DatabaseMessage = { + id: '6', + convId: 'conv-1', + type: 'message', + timestamp: Date.now() - 1000 * 60 * 2, + role: 'assistant', + content: + "\nLet me analyze this step by step:\n\n1. The user is asking about thinking formats\n2. I need to demonstrate the <think> tag format\n3. This content should be displayed in the thinking section\n4. The main response should be separate\n\nThis is a good example of reasoning content.\n\n\nHere's my response after thinking through the problem. The thinking content above should be displayed separately from this main response content.", + parent: '1', + thinking: '', + children: [] + }; + + // Message with [THINK] format thinking content + const thinkBracketMessage: DatabaseMessage = { + id: '7', + convId: 'conv-1', + type: 'message', + timestamp: Date.now() - 1000 * 60 * 1, + role: 'assistant', + content: + '[THINK]\nThis is the DeepSeek-style thinking format:\n\n- Using square brackets instead of angle brackets\n- Should work identically to the <think> format\n- Content parsing should extract this reasoning\n- Display should be the same as <think> format\n\nBoth formats should be supported seamlessly.\n[/THINK]\n\nThis is the main response content that comes after the [THINK] block. The reasoning above should be parsed and displayed in the thinking section.', + parent: '1', + thinking: '', + children: [] + }; + + // Streaming message for format + let streamingThinkMessage = $state({ + id: '8', + convId: 'conv-1', + type: 'message', + timestamp: 0, // No timestamp = streaming + role: 'assistant', + content: '', + parent: '1', + thinking: '', + children: [] + }); + + // Streaming message for [THINK] format + let streamingBracketMessage = $state({ + id: '9', + convId: 'conv-1', + type: 'message', + timestamp: 0, // No timestamp = streaming + role: 'assistant', + content: '', + parent: '1', + thinking: '', + children: [] + }); setTimeout(resolve, 100)); }} /> + + + + + + { + // Phase 1: Stream reasoning content + const thinkingContent = + 'Let me work through this problem systematically:\n\n1. First, I need to understand what the user is asking\n2. Then I should consider different approaches\n3. I need to evaluate the pros and cons\n4. Finally, I should provide a clear recommendation\n\nThis step-by-step approach will ensure accuracy.'; + + let currentContent = '\n'; + streamingThinkMessage.content = currentContent; + + for (let i = 0; i < thinkingContent.length; i++) { + currentContent += thinkingContent[i]; + streamingThinkMessage.content = currentContent; + await new Promise((resolve) => setTimeout(resolve, 5)); + } + + // Close the thinking block + currentContent += '\n\n\n'; + streamingThinkMessage.content = currentContent; + await new Promise((resolve) => setTimeout(resolve, 200)); + + // Phase 2: Stream main response content + const responseContent = + "Based on my analysis above, here's the solution:\n\n**Key Points:**\n- The approach should be systematic\n- We need to consider all factors\n- Implementation should be step-by-step\n\nThis ensures the best possible outcome."; + + for (let i = 0; i < responseContent.length; i++) { + currentContent += responseContent[i]; + streamingThinkMessage.content = currentContent; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + + streamingThinkMessage.timestamp = Date.now(); + }} +> +
+ +
+
+ + { + // Phase 1: Stream [THINK] reasoning content + const thinkingContent = + 'Using the DeepSeek format now:\n\n- This demonstrates the [THINK] bracket format\n- Should parse identically to <think> tags\n- The UI should display this in the thinking section\n- Main content should be separate\n\nBoth formats provide the same functionality.'; + + let currentContent = '[THINK]\n'; + streamingBracketMessage.content = currentContent; + + for (let i = 0; i < thinkingContent.length; i++) { + currentContent += thinkingContent[i]; + streamingBracketMessage.content = currentContent; + await new Promise((resolve) => setTimeout(resolve, 5)); + } + + // Close the thinking block + currentContent += '\n[/THINK]\n\n'; + streamingBracketMessage.content = currentContent; + await new Promise((resolve) => setTimeout(resolve, 200)); + + // Phase 2: Stream main response content + const responseContent = + "Here's my response after using the [THINK] format:\n\n**Observations:**\n- Both <think> and [THINK] formats work seamlessly\n- The parsing logic handles both cases\n- UI display is consistent across formats\n\nThis demonstrates the enhanced thinking content support."; + + for (let i = 0; i < responseContent.length; i++) { + currentContent += responseContent[i]; + streamingBracketMessage.content = currentContent; + await new Promise((resolve) => setTimeout(resolve, 10)); + } + + streamingBracketMessage.timestamp = Date.now(); + }} +> +
+ +
+