mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-06 03:18:36 +08:00
Fix: collapsible thinking display and separate deep research retrieval tag (#14613)
## Summary - **Collapsible thinking**: Replace `<section>` with `<details>` for `<think>` content, so model thinking output is collapsed by default (click to expand). Works for all models that output `<think>` 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 `<think>` tags, producing two "Thinking..." blocks. Now retrieval progress uses a dedicated `<retrieving>` tag rendered as a separate "Retrieving..." collapsible with a distinct green accent. ### Before - Thinking content displayed as flat gray-bordered `<section>`, occupying significant screen space - Deep research + model thinking both use `<think>` → two identical "Thinking..." blocks ### After - Thinking content collapsed by default in a `<details>` 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 `<retrieving>`/`</retrieving>` answer text **Frontend** - `web/src/utils/chat.ts`: `replaceThinkToSection()` now uses `<details>` instead of `<section>`; 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 `<summary>` 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 <wanghualoong@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -665,9 +665,9 @@ async def async_chat(dialog, messages, stream=True, **kwargs):
|
||||
while True:
|
||||
msg = await queue.get()
|
||||
if msg.find("<START_DEEP_RESEARCH>") == 0:
|
||||
yield {"answer": "", "reference": {}, "audio_binary": None, "final": False, "start_to_think": True}
|
||||
yield {"answer": "<retrieving>", "reference": {}, "audio_binary": None, "final": False}
|
||||
elif msg.find("<END_DEEP_RESEARCH>") == 0:
|
||||
yield {"answer": "", "reference": {}, "audio_binary": None, "final": False, "end_to_think": True}
|
||||
yield {"answer": "</retrieving>", "reference": {}, "audio_binary": None, "final": False}
|
||||
break
|
||||
else:
|
||||
yield {"answer": msg, "reference": {}, "audio_binary": None, "final": False}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -81,7 +81,15 @@ export const preprocessLaTeX = (content: string) => {
|
||||
export function replaceThinkToSection(text: string = '') {
|
||||
const pattern = /<think>([\s\S]*?)<\/think>/g;
|
||||
|
||||
const result = text.replace(pattern, '<section class="think">$1</section>');
|
||||
const result = text.replace(pattern, '<details class="think"><summary>Thinking...</summary>$1</details>');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function replaceRetrievingToSection(text: string = '') {
|
||||
const pattern = /<retrieving>([\s\S]*?)<\/retrieving>/g;
|
||||
|
||||
const result = text.replace(pattern, '<details class="retrieving"><summary>Retrieving...</summary>$1</details>');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user