diff --git a/app/services/grok/services/chat.py b/app/services/grok/services/chat.py
index 0ee6428..dae6ecc 100644
--- a/app/services/grok/services/chat.py
+++ b/app/services/grok/services/chat.py
@@ -419,6 +419,9 @@ class StreamProcessor(proc_base.BaseProcessor):
self.rollout_id: str = ""
self.fingerprint: str = ""
self.think_opened: bool = False
+ self.seen_think_once: bool = False
+ self.image_think_active: bool = False
+ self.answer_buffer: list[str] = []
self.role_sent: bool = False
self.filter_tags = get_config("app.filter_tags")
self.tool_usage_enabled = (
@@ -566,12 +569,11 @@ class StreamProcessor(proc_base.BaseProcessor):
if img := resp.get("streamingImageGenerationResponse"):
if not self.show_think:
continue
- if is_thinking and not self.think_opened:
+ self.image_think_active = True
+ if not self.think_opened:
yield self._sse("\n")
self.think_opened = True
- if (not is_thinking) and self.think_opened:
- yield self._sse("\n\n")
- self.think_opened = False
+ self.seen_think_once = True
idx = img.get("imageIndex", 0) + 1
progress = img.get("progress", 0)
yield self._sse(
@@ -580,6 +582,7 @@ class StreamProcessor(proc_base.BaseProcessor):
continue
if mr := resp.get("modelResponse"):
+ self.image_think_active = False
for url in proc_base._collect_images(mr):
parts = url.split("/")
img_id = parts[-2] if len(parts) >= 2 else "image"
@@ -622,20 +625,37 @@ class StreamProcessor(proc_base.BaseProcessor):
filtered = self._filter_token(token)
if not filtered:
continue
- if is_thinking:
+ # 避免上游字面 标签与前端思考分段逻辑冲突,导致多段思考块
+ filtered = re.sub(r"?think>", "", filtered, flags=re.IGNORECASE)
+ if not filtered:
+ continue
+ in_think = (
+ is_thinking
+ or self.image_think_active
+ )
+ if in_think:
if not self.show_think:
continue
if not self.think_opened:
yield self._sse("\n")
self.think_opened = True
- else:
- if self.think_opened:
- yield self._sse("\n\n")
- self.think_opened = False
+ self.seen_think_once = True
+ yield self._sse(filtered)
+ continue
+
+ # 一旦进入思考模式,先缓存正文,等思考结束后再统一输出
+ if self.show_think and self.seen_think_once:
+ self.answer_buffer.append(filtered)
+ continue
+
yield self._sse(filtered)
if self.think_opened:
yield self._sse("\n")
+ self.think_opened = False
+ if self.answer_buffer:
+ yield self._sse("".join(self.answer_buffer))
+ self.answer_buffer = []
yield self._sse(finish="stop")
yield "data: [DONE]\n\n"
except asyncio.CancelledError:
diff --git a/app/static/public/css/chat.css b/app/static/public/css/chat.css
index a699a53..275f38b 100644
--- a/app/static/public/css/chat.css
+++ b/app/static/public/css/chat.css
@@ -484,7 +484,10 @@ body {
}
.think-agent {
+ border: 1px solid var(--chat-border);
+ background: var(--chat-surface);
border-radius: 12px;
+ padding: 8px 10px 10px;
width: 100%;
}
@@ -499,6 +502,9 @@ body {
gap: 8px;
padding: 4px 8px;
border-radius: 999px;
+ background: transparent;
+ width: auto;
+ max-width: 100%;
}
.think-agent summary::before {
@@ -518,7 +524,7 @@ body {
border-right: 2px solid var(--accents-4);
border-bottom: 2px solid var(--accents-4);
transform: rotate(45deg);
- margin-left: 4px;
+ margin-left: 2px;
}
.think-agent[open] summary::after {
@@ -531,15 +537,15 @@ body {
@keyframes thinkGlow {
0% {
- box-shadow: 0 0 0 rgba(255, 255, 255, 0);
+ box-shadow: 0 0 0 rgba(17, 17, 17, 0);
background: transparent;
}
50% {
- box-shadow: 0 0 12px rgba(255, 255, 255, 0.9);
- background: rgba(255, 255, 255, 0.8);
+ box-shadow: 0 0 0 2px rgba(14, 165, 233, 0.14);
+ background: rgba(14, 165, 233, 0.08);
}
100% {
- box-shadow: 0 0 0 rgba(255, 255, 255, 0);
+ box-shadow: 0 0 0 rgba(17, 17, 17, 0);
background: transparent;
}
}
@@ -551,7 +557,8 @@ body {
.think-agent-items {
margin-top: 10px;
margin-left: 0;
- padding-left: 0;
+ padding-left: 2px;
+ padding-right: 2px;
border-left: none;
display: flex;
flex-direction: column;
@@ -623,7 +630,10 @@ body {
.think-item-row {
border-left: none;
- padding-left: 0;
+ padding: 8px 10px;
+ border: 1px solid var(--chat-border);
+ border-radius: 10px;
+ background: rgba(17, 17, 17, 0.02);
display: flex;
flex-direction: column;
gap: 6px;
@@ -668,6 +678,47 @@ body {
line-height: 1.6;
}
+html[data-theme='dark'] .think-agent {
+ background: #101924;
+ border-color: #273142;
+}
+
+html[data-theme='dark'] .think-agent summary {
+ background: transparent;
+}
+
+html[data-theme='dark'] .think-item-row {
+ background: #0f1824;
+ border-color: #273142;
+}
+
+html[data-theme='dark'] .think-item-type {
+ color: #9fb2c9;
+}
+
+html[data-theme='dark'] .think-item-body {
+ color: #d5e0ee;
+}
+
+@keyframes thinkGlowDark {
+ 0% {
+ box-shadow: 0 0 0 rgba(96, 165, 250, 0);
+ background: transparent;
+ }
+ 50% {
+ box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2);
+ background: rgba(96, 165, 250, 0.08);
+ }
+ 100% {
+ box-shadow: 0 0 0 rgba(96, 165, 250, 0);
+ background: transparent;
+ }
+}
+
+html[data-theme='dark'] .think-block[data-thinking="true"] .think-agent summary {
+ animation: thinkGlowDark 1.6s ease-in-out infinite;
+}
+
.composer-shell {
position: sticky;
bottom: 8px;
diff --git a/app/static/public/js/chat.js b/app/static/public/js/chat.js
index 3fe7596..6344b4f 100644
--- a/app/static/public/js/chat.js
+++ b/app/static/public/js/chat.js
@@ -799,49 +799,37 @@
const ids = Array.from(new Set(list.map((b) => String(b.id || '').trim()).filter(Boolean)));
const nonGeneralIds = ids.filter((id) => !/^general$/i.test(id));
- // 优先按 rollout id 分组,能更稳定地映射多 agent。
- if (nonGeneralIds.length >= 2) {
- const groups = [];
- const map = new Map();
- for (const block of list) {
- const key = String(block.id || 'General');
- let group = map.get(key);
- if (!group) {
- group = { key, blocks: [] };
- map.set(key, group);
- groups.push(group);
- }
- group.blocks.push(block);
- }
- return groups.map((group, idx) => ({
- title: idx === 0 ? 'Grok Leader' : `Agent ${idx}`,
- blocks: group.blocks
- }));
+ if (nonGeneralIds.length <= 1) {
+ return [];
}
-
- // 兜底:按 AgentThink 作为阶段收束点切分。
- const sections = [];
- let current = [];
+ const groups = [];
+ const map = new Map();
for (const block of list) {
- current.push(block);
- if (/^agentthink$/i.test(String(block.type || '').trim())) {
- sections.push(current);
- current = [];
+ const key = String(block.id || 'General');
+ let group = map.get(key);
+ if (!group) {
+ group = { key, blocks: [] };
+ map.set(key, group);
+ groups.push(group);
}
+ group.blocks.push(block);
}
- if (current.length) {
- sections.push(current);
- }
-
- if (sections.length <= 1) {
- return [{ title: 'Grok Leader', blocks: list }];
- }
- return sections.map((part, idx) => ({
+ return groups.map((group, idx) => ({
title: idx === 0 ? 'Grok Leader' : `Agent ${idx}`,
- blocks: part
+ blocks: group.blocks
}));
}
+ function renderFlatBlocks(blocks) {
+ return (Array.isArray(blocks) ? blocks : []).map((item) => {
+ const body = renderBasicMarkdown((item.lines || []).join('\n').trim());
+ const typeText = escapeHtml(item.type);
+ const typeKey = String(item.type || '').trim().toLowerCase().replace(/\s+/g, '');
+ const typeAttr = escapeHtml(typeKey);
+ return `
${typeText}
${body || '(空)'}
`;
+ }).join('');
+ }
+
function renderThinkContent(text, openAll) {
const sections = parseAgentSections(text);
if (!sections.length) {
@@ -878,9 +866,9 @@
const blocks = parseRolloutBlocks(section.lines.join('\n'), section.title || 'General');
if (!section.title && blocks.length) {
const synthetic = splitBlocksIntoSyntheticAgents(blocks);
- if (synthetic.length > 1) {
+ if (synthetic.length) {
return synthetic.map((agent, agentIdx) => {
- const inner = renderGroups(agent.blocks, openAll);
+ const inner = renderFlatBlocks(agent.blocks);
const title = escapeHtml(agent.title);
const openAttr = openAll ? ' open' : (idx === 0 && agentIdx === 0 ? ' open' : '');
return `${title}
${inner}
`;
@@ -1126,6 +1114,27 @@
if (!entry) return;
entry.raw = content || '';
if (!entry.contentNode) return;
+ const captureOpenState = (root, selector) => {
+ if (!root || !root.querySelectorAll) return null;
+ const nodes = Array.from(root.querySelectorAll(selector));
+ if (!nodes.length) return null;
+ return nodes.map((node) => node.hasAttribute('open'));
+ };
+ const restoreOpenState = (root, selector, states) => {
+ if (!root || !root.querySelectorAll || !Array.isArray(states) || !states.length) return;
+ const nodes = Array.from(root.querySelectorAll(selector));
+ const max = Math.min(nodes.length, states.length);
+ for (let i = 0; i < max; i += 1) {
+ if (states[i]) {
+ nodes[i].setAttribute('open', '');
+ } else {
+ nodes[i].removeAttribute('open');
+ }
+ }
+ };
+ const savedThinkBlockState = captureOpenState(entry.contentNode, '.think-block');
+ const savedThinkAgentState = captureOpenState(entry.contentNode, '.think-agent');
+ const savedRolloutState = captureOpenState(entry.contentNode, '.think-rollout-group');
if (!entry.hasThink && entry.raw.includes('')) {
entry.hasThink = true;
}
@@ -1139,7 +1148,13 @@
entry.contentNode.textContent = entry.raw;
}
}
+ restoreOpenState(entry.contentNode, '.think-block', savedThinkBlockState);
+ restoreOpenState(entry.contentNode, '.think-agent', savedThinkAgentState);
+ restoreOpenState(entry.contentNode, '.think-rollout-group', savedRolloutState);
if (entry.hasThink) {
+ if (finalize && (entry.thinkElapsed === null || typeof entry.thinkElapsed === 'undefined')) {
+ entry.thinkElapsed = 0;
+ }
updateThinkSummary(entry, entry.thinkElapsed);
}
if (entry.role === 'assistant' || entry.role === 'user') {