diff --git a/src/components/Conversation.tsx b/src/components/Conversation.tsx index 2bc5afb..9429a7c 100644 --- a/src/components/Conversation.tsx +++ b/src/components/Conversation.tsx @@ -1,5 +1,7 @@ // 中栏:消息流(Part 渲染器注册表)+ 流式渲染 + 过程折叠 + HITL 卡片 + 输入区(4.4)。 -import { Icon, Avatar, BrandMark } from '@ui' +// 重设计方案 阶段④:折叠区全部迁 @ui Collapsible(
已禁);工具调用块走 +// ToolBlock 契约(运行态扫光、完成边沿收起、展开态记忆);live 行扫光 + 已工作计时。 +import { Icon, Avatar, BrandMark, Collapsible, ToolBlock } from '@ui' import { useEffect, useMemo, useRef, useState } from 'react' import { marked } from 'marked' import { api } from '../api' @@ -21,12 +23,8 @@ function PartView({ part, live }: { part: Part; live?: boolean | null }) { dangerouslySetInnerHTML={{ __html: marked.parse(part.text ?? '') }} /> case 'thinking': // 流式期间展开可实时看到思考;run 终态后收进过程折叠区,默认收起。 - return ( -
- 思考过程 -
{part.text}
-
- ) + //(此单件形态用于过程组之外散落的思考;组内思考由 ProcessParts 渲染。) + return case 'tool_call': return 🛠 {part.toolCall?.name} case 'tool_result': { @@ -94,40 +92,108 @@ function findCard(id: string) { return cardCache[id] } -// 过程折叠:run 终态后把连续 process 部件折叠为摘要行(可展开回看)。 -function ProcessCollapse({ parts }: { parts: Part[] }) { +// 思考块(Collapsible 契约):左线弱色,流式默认展开、终态默认收起;展开态记忆。 +function ThinkingBlock({ part, idKey, defaultOpen }: { part: Part; idKey: string; defaultOpen?: boolean }) { + return ( +
+ ▸思考过程}> +
{part.text}
+
+
+ ) +} + +// 过程段渲染(配对增强):tool_call 吸收紧随其后的同名 tool_result 组成 ToolBlock +//(运行态扫光、完成边沿收起、展开看结果摘要);其余部件原位 PartView。 +function ProcessParts({ parts, live, idKey }: { parts: Part[]; live: boolean; idKey: string }) { + const out: React.ReactNode[] = [] + for (let i = 0; i < parts.length; i++) { + const p = parts[i] + if (p.type === 'tool_call') { + const res = parts[i + 1]?.type === 'tool_result' ? parts[i + 1] : null + if (res) i++ + const st = res?.toolResult?.status + out.push( + + {res &&
{res.toolResult?.summary}
} +
) + continue + } + out.push(
) + } + return <>{out} +} + +// 过程折叠:run 终态后把连续 process 部件折叠为摘要行(可展开回看;Collapsible 契约)。 +function ProcessCollapse({ parts, idKey }: { parts: Part[]; idKey: string }) { const toolCount = parts.filter(p => p.type === 'tool_call').length const hasThinking = parts.some(p => p.type === 'thinking' && p.text) return ( -
- - 过程输出 - {hasThinking ? '含思考' : ''}{toolCount > 0 ? ` · ${toolCount} 次工具调用` : ''}(点击展开) - -
- {parts.map((p, i) =>
)} -
-
+
+ + ▸ + 过程输出 + + {hasThinking ? '含思考' : ''}{toolCount > 0 ? ` · ${toolCount} 次工具调用` : ''} + + }> +
+ +
+
+
) } // 长回复折叠阈值(CF0):终态后超长正文收成摘要行,点击展开全文;live 期间全文可见。 const LONG_REPLY_CHARS = 800 -function LongReplyCollapse({ text }: { text: string }) { +function LongReplyCollapse({ text, idKey }: { text: string; idKey: string }) { // 摘句取压缩空白后的前 100 字;字数按 Markdown 源码长度计。 const excerpt = text.replace(/\s+/g, ' ').trim().slice(0, 100) return ( -
- - 📄 长回复 - {text.length} 字(按 Markdown 源码计) - {excerpt && {excerpt}…} - -
-
-
-
+
+ + ▸ + 📄 长回复 + {text.length} 字(按 Markdown 源码计) + {excerpt && {excerpt}…} + }> +
+
+
+ +
+ ) +} + +// 已工作计时(ZCode「已工作 X 分 X 秒」摘要行;1s 步进,仅 live 期间挂载)。 +function useElapsed(startedAt: number): string { + const [, tick] = useState(0) + useEffect(() => { + const t = window.setInterval(() => tick(n => n + 1), 1000) + return () => window.clearInterval(t) + }, []) + const s = Math.max(0, Math.floor((Date.now() - startedAt) / 1000)) + const m = Math.floor(s / 60) + return m > 0 ? `${m} 分 ${s % 60} 秒` : `${s} 秒` +} + +// 流式状态行(独立组件:计时 hook 只在 live 行挂载期间运转)。 +function LiveBar({ live }: { live: LiveState }) { + const elapsed = useElapsed(live.startedAt) + return ( + + + {live.phase} + 已工作 {elapsed} + ) } @@ -152,16 +218,16 @@ function AssistantMessage({ msg, live }: { msg: Message; live?: LiveState | null
助手 - {live && {live.phase}} + {live && }
{blocks.map((b, i) => b.kind === 'process' ? (showProcess - ?
{b.parts.map((p, j) =>
)}
- : ) + ?
+ : ) : (() => { // CF0:非 live 的超长文本部件折叠为摘要行;live 与短文本不受影响。 if (!live && b.part.type === 'text' && (b.part.text?.length ?? 0) > LONG_REPLY_CHARS) - return + return return
@@ -175,22 +241,25 @@ function AssistantMessage({ msg, live }: { msg: Message; live?: LiveState | null // 上下文组装观测(4.10/4.17):模型实际看到什么、裁了什么——可解释性入口。 function ContextObsView({ obs }: { obs: ContextObs }) { return ( -
- - 🧩 上下文构成 - {obs.items.join(' · ')}(约 {obs.estChars} 字符) - -
-
    - {obs.items.map((it, i) =>
  • {it}
  • )} -
- {obs.truncations && obs.truncations.length > 0 && ( -
- {obs.truncations.map((t, i) =>
⚠ {t}
)} -
- )} -
-
+
+ + ▸ + 🧩 上下文构成 + {obs.items.join(' · ')}(约 {obs.estChars} 字符) + }> +
+
    + {obs.items.map((it, i) =>
  • {it}
  • )} +
+ {obs.truncations && obs.truncations.length > 0 && ( +
+ {obs.truncations.map((t, i) =>
⚠ {t}
)} +
+ )} +
+
+
) } @@ -389,7 +458,9 @@ export function Conversation() { e.target.value = '' }} /> -