From 1bcb6deb6f45d97370c23b0713a258bbf71ce50b Mon Sep 17 00:00:00 2001 From: Tim Wang <38489718+wanghualoong@users.noreply.github.com> Date: Fri, 8 May 2026 14:40:00 +0800 Subject: [PATCH] Fix: collapsible thinking display and separate deep research retrieval tag (#14613) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - **Collapsible thinking**: Replace `
` with `
` for `` content, so model thinking output is collapsed by default (click to expand). Works for all models that output `` tags (Qwen3, DeepSeek, Gemini, Claude, etc.). - **Fix double thinking tags**: When reasoning/deep research mode is enabled in knowledge base chat, both the retrieval progress and model thinking were wrapped in `` tags, producing two "Thinking..." blocks. Now retrieval progress uses a dedicated `` tag rendered as a separate "Retrieving..." collapsible with a distinct green accent. ### Before - Thinking content displayed as flat gray-bordered `
`, occupying significant screen space - Deep research + model thinking both use `` → two identical "Thinking..." blocks ### After - Thinking content collapsed by default in a `
` element, click "Thinking..." to expand - Deep research shows "Retrieving..." (green border), model thinking shows "Thinking..." (gray border) ## Changes **Backend (`api/db/services/dialog_service.py`)** - Deep research callback: replace `start_to_think`/`end_to_think` marker flags with direct ``/`` answer text **Frontend** - `web/src/utils/chat.ts`: `replaceThinkToSection()` now uses `
` instead of `
`; add new `replaceRetrievingToSection()` - 4 tsx files: import and pipe `replaceRetrievingToSection`, whitelist `details`, `summary`, `retrieving` in DOMPurify `ADD_TAGS` - 4 less files: `section.think` → `details.think` with `` styles; add `details.retrieving` with green accent; dark mode and RTL variants ## Test plan - [ ] Open a chat WITHOUT knowledge base, ask a question to a model with thinking (e.g. Qwen3) → thinking content should be collapsed by default, click "Thinking..." to expand - [ ] Open a chat WITH knowledge base and reasoning enabled, ask a question → "Retrieving..." (green) shows retrieval progress, "Thinking..." (gray) shows model thinking, each independently collapsible - [ ] Verify dark mode renders correctly for both collapsible blocks - [ ] Verify RTL layout renders correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: wanghualoong Co-authored-by: Claude Opus 4.6 --- api/db/services/dialog_service.py | 4 +-- .../floating-chat-widget-markdown.tsx | 3 +- .../markdown-content/index.module.less | 29 ++++++++++++++++- web/src/components/markdown-content/index.tsx | 5 +-- .../components/message-item/index.module.less | 20 +++++++++++- .../next-markdown-content/index.module.less | 29 ++++++++++++++++- .../next-markdown-content/index.tsx | 5 +-- .../next-message-item/index.module.less | 31 +++++++++++++++++-- .../next-search/markdown-content/index.tsx | 5 +-- web/src/utils/chat.ts | 10 +++++- 10 files changed, 126 insertions(+), 15 deletions(-) diff --git a/api/db/services/dialog_service.py b/api/db/services/dialog_service.py index 04e9c691b3..c1d90ebe4c 100644 --- a/api/db/services/dialog_service.py +++ b/api/db/services/dialog_service.py @@ -665,9 +665,9 @@ async def async_chat(dialog, messages, stream=True, **kwargs): while True: msg = await queue.get() if msg.find("") == 0: - yield {"answer": "", "reference": {}, "audio_binary": None, "final": False, "start_to_think": True} + yield {"answer": "", "reference": {}, "audio_binary": None, "final": False} elif msg.find("") == 0: - yield {"answer": "", "reference": {}, "audio_binary": None, "final": False, "end_to_think": True} + yield {"answer": "", "reference": {}, "audio_binary": None, "final": False} break else: yield {"answer": msg, "reference": {}, "audio_binary": None, "final": False} diff --git a/web/src/components/floating-chat-widget-markdown.tsx b/web/src/components/floating-chat-widget-markdown.tsx index bf828c4680..3a4e4942c6 100644 --- a/web/src/components/floating-chat-widget-markdown.tsx +++ b/web/src/components/floating-chat-widget-markdown.tsx @@ -10,6 +10,7 @@ import { currentReg, parseCitationIndex, preprocessLaTeX, + replaceRetrievingToSection, replaceTextByOldReg, replaceThinkToSection, showImage, @@ -66,7 +67,7 @@ const FloatingChatWidgetMarkdown = ({ const contentWithCursor = useMemo(() => { const text = content === '' ? t('chat.searching') : content; const nextText = replaceTextByOldReg(text); - return pipe(replaceThinkToSection, preprocessLaTeX)(nextText); + return pipe(replaceThinkToSection, replaceRetrievingToSection, preprocessLaTeX)(nextText); }, [content, t]); useEffect(() => { diff --git a/web/src/components/markdown-content/index.module.less b/web/src/components/markdown-content/index.module.less index 59a8b4771a..d07097b28d 100644 --- a/web/src/components/markdown-content/index.module.less +++ b/web/src/components/markdown-content/index.module.less @@ -1,10 +1,37 @@ .markdownContentWrapper { - :global(section.think) { + :global(details.think) { padding-inline-start: 10px; color: #8b8b8b; border-inline-start: 2px solid #d5d3d3; margin-bottom: 10px; font-size: 12px; + + summary { + cursor: pointer; + font-weight: 500; + color: #999; + user-select: none; + &:hover { + color: #666; + } + } + } + :global(details.retrieving) { + padding-inline-start: 10px; + color: #8b8b8b; + border-inline-start: 2px solid #a3d5c9; + margin-bottom: 10px; + font-size: 12px; + + summary { + cursor: pointer; + font-weight: 500; + color: #6ba89a; + user-select: none; + &:hover { + color: #4a8a7c; + } + } } :global(blockquote) { padding-inline-start: 10px; diff --git a/web/src/components/markdown-content/index.tsx b/web/src/components/markdown-content/index.tsx index 846d25d5a5..9dc1544404 100644 --- a/web/src/components/markdown-content/index.tsx +++ b/web/src/components/markdown-content/index.tsx @@ -23,6 +23,7 @@ import { currentReg, parseCitationIndex, preprocessLaTeX, + replaceRetrievingToSection, replaceTextByOldReg, replaceThinkToSection, } from '@/utils/chat'; @@ -63,7 +64,7 @@ const MarkdownContent = ({ useFetchDocumentThumbnailsByIds(); const contentWithCursor = useMemo(() => { let text = DOMPurify.sanitize(content, { - ADD_TAGS: ['think', 'section'], + ADD_TAGS: ['think', 'section', 'details', 'summary', 'retrieving'], ADD_ATTR: ['class'], }); @@ -72,7 +73,7 @@ const MarkdownContent = ({ text = t('chat.searching'); } const nextText = replaceTextByOldReg(text); - return pipe(replaceThinkToSection, preprocessLaTeX)(nextText); + return pipe(replaceThinkToSection, replaceRetrievingToSection, preprocessLaTeX)(nextText); }, [content, t]); useEffect(() => { diff --git a/web/src/components/message-item/index.module.less b/web/src/components/message-item/index.module.less index fbf35b8de0..e0f55c2a1b 100644 --- a/web/src/components/message-item/index.module.less +++ b/web/src/components/message-item/index.module.less @@ -34,9 +34,27 @@ .messageTextBase(); padding: 0; word-break: break-word; - :global(section.think) { + :global(details.think) { color: rgb(166, 166, 166); border-inline-start-color: rgb(78, 78, 86); + + summary { + color: rgb(140, 140, 140); + &:hover { + color: rgb(180, 180, 180); + } + } + } + :global(details.retrieving) { + color: rgb(166, 166, 166); + border-inline-start-color: rgb(60, 100, 90); + + summary { + color: rgb(120, 170, 155); + &:hover { + color: rgb(150, 200, 185); + } + } } } diff --git a/web/src/components/next-markdown-content/index.module.less b/web/src/components/next-markdown-content/index.module.less index 71de7f615c..aa20ab010c 100644 --- a/web/src/components/next-markdown-content/index.module.less +++ b/web/src/components/next-markdown-content/index.module.less @@ -1,10 +1,37 @@ .markdownContentWrapper { - :global(section.think) { + :global(details.think) { padding-inline-start: 10px; color: #8b8b8b; border-inline-start: 2px solid #d5d3d3; margin-bottom: 10px; font-size: 12px; + + summary { + cursor: pointer; + font-weight: 500; + color: #999; + user-select: none; + &:hover { + color: #666; + } + } + } + :global(details.retrieving) { + padding-inline-start: 10px; + color: #8b8b8b; + border-inline-start: 2px solid #a3d5c9; + margin-bottom: 10px; + font-size: 12px; + + summary { + cursor: pointer; + font-weight: 500; + color: #6ba89a; + user-select: none; + &:hover { + color: #4a8a7c; + } + } } :global(blockquote) { padding-inline-start: 10px; diff --git a/web/src/components/next-markdown-content/index.tsx b/web/src/components/next-markdown-content/index.tsx index 8fc966897d..ebedb0eed6 100644 --- a/web/src/components/next-markdown-content/index.tsx +++ b/web/src/components/next-markdown-content/index.tsx @@ -22,6 +22,7 @@ import { currentReg, parseCitationIndex, preprocessLaTeX, + replaceRetrievingToSection, replaceTextByOldReg, replaceThinkToSection, } from '@/utils/chat'; @@ -170,7 +171,7 @@ function MarkdownContent({ useFetchDocumentThumbnailsByIds(); const contentWithCursor = useMemo(() => { let text = DOMPurify.sanitize(content, { - ADD_TAGS: ['think', 'section'], + ADD_TAGS: ['think', 'section', 'details', 'summary', 'retrieving'], ADD_ATTR: ['class'], }); // let text = content; @@ -178,7 +179,7 @@ function MarkdownContent({ text = t('chat.searching'); } const nextText = replaceTextByOldReg(text); - return pipe(replaceThinkToSection, preprocessLaTeX)(nextText); + return pipe(replaceThinkToSection, replaceRetrievingToSection, preprocessLaTeX)(nextText); }, [content, t]); useEffect(() => { diff --git a/web/src/components/next-message-item/index.module.less b/web/src/components/next-message-item/index.module.less index 5b72726cf6..3589063717 100644 --- a/web/src/components/next-message-item/index.module.less +++ b/web/src/components/next-message-item/index.module.less @@ -37,16 +37,34 @@ .chunkText(); .messageTextBase(); word-break: break-word; - :global(section.think) { + :global(details.think) { color: rgb(166, 166, 166); border-inline-start-color: rgb(78, 78, 86); + + summary { + color: rgb(140, 140, 140); + &:hover { + color: rgb(180, 180, 180); + } + } + } + :global(details.retrieving) { + color: rgb(166, 166, 166); + border-inline-start-color: rgb(60, 100, 90); + + summary { + color: rgb(120, 170, 155); + &:hover { + color: rgb(150, 200, 185); + } + } } // RTL Support &[dir='rtl'] { text-align: right; - :global(section.think) { + :global(details.think) { border-inline-start-color: transparent; border-inline-end-color: rgb(78, 78, 86); border-inline-end-width: 2px; @@ -55,6 +73,15 @@ padding-inline-start: 0; padding-inline-end: 10px; } + :global(details.retrieving) { + border-inline-start-color: transparent; + border-inline-end-color: rgb(60, 100, 90); + border-inline-end-width: 2px; + border-inline-end-style: solid; + border-inline-start: none; + padding-inline-start: 0; + padding-inline-end: 10px; + } } } diff --git a/web/src/pages/next-search/markdown-content/index.tsx b/web/src/pages/next-search/markdown-content/index.tsx index a9eeec0375..a5522b7c7a 100644 --- a/web/src/pages/next-search/markdown-content/index.tsx +++ b/web/src/pages/next-search/markdown-content/index.tsx @@ -20,6 +20,7 @@ import { currentReg, parseCitationIndex, preprocessLaTeX, + replaceRetrievingToSection, replaceTextByOldReg, replaceThinkToSection, } from '@/utils/chat'; @@ -67,7 +68,7 @@ const MarkdownContent = ({ useFetchDocumentThumbnailsByIds(); const contentWithCursor = useMemo(() => { let text = DOMPurify.sanitize(content, { - ADD_TAGS: ['think', 'section'], + ADD_TAGS: ['think', 'section', 'details', 'summary', 'retrieving'], ADD_ATTR: ['class'], }); // let text = content; @@ -75,7 +76,7 @@ const MarkdownContent = ({ text = t('chat.searching'); } const nextText = replaceTextByOldReg(text); - return pipe(replaceThinkToSection, preprocessLaTeX)(nextText); + return pipe(replaceThinkToSection, replaceRetrievingToSection, preprocessLaTeX)(nextText); }, [content, t]); useEffect(() => { diff --git a/web/src/utils/chat.ts b/web/src/utils/chat.ts index 879289d3b1..923a4a9e3e 100644 --- a/web/src/utils/chat.ts +++ b/web/src/utils/chat.ts @@ -81,7 +81,15 @@ export const preprocessLaTeX = (content: string) => { export function replaceThinkToSection(text: string = '') { const pattern = /([\s\S]*?)<\/think>/g; - const result = text.replace(pattern, '
$1
'); + const result = text.replace(pattern, '
Thinking...$1
'); + + return result; +} + +export function replaceRetrievingToSection(text: string = '') { + const pattern = /([\s\S]*?)<\/retrieving>/g; + + const result = text.replace(pattern, '
Retrieving...$1
'); return result; }