From fbf9031e25a6b62c14e79b025b2d409c027b1e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Sun, 30 Aug 2026 21:24:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=BC=9A=E5=95=86=E7=AC=AC=E4=B8=89?= =?UTF-8?q?=E6=AD=A5):=20=E6=81=A2=E5=A4=8D=E6=99=AE=E9=80=9A=E4=BC=9A?= =?UTF-8?q?=E5=95=86=E5=AE=8C=E6=95=B4=E6=88=90=E6=9E=9C=E5=B9=B6=E9=9A=94?= =?UTF-8?q?=E7=A6=BB8BF=E6=8A=A5=E5=91=8A=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为普通多智能体会商恢复报告完成后的结构化流程图、最终方案展示、经典结果绘制和大屏多页,并保持三个生成流独立并行。 - 8BF 入口显式采用 report-only 模式:只生成和编辑 Markdown 总结报告,不再启动或露出流程图、结果绘制及大屏功能。 - 结构化流程图增加抽取、校验、暂停、失败重试和歧义选择交互;报告完成后状态文案准确切换,自动滚动定位到抽取与校验进度。 - 为总结报告、经典 HTML 绘制和大屏虚拟 write_file 补齐完成结果及历史恢复处理,避免并行分支或旧草稿让完成产物误显示为生成中。 - 最终方案展示中的已落盘文件直接按完成态预览,并恢复 MD、HTML 等文件类型徽标,不再被提交按钮覆盖。 --- .../pages/EightBfRoundtablePage.tsx | 1 + .../components/HighFidelityReport.tsx | 76 ++++-- .../components/Step3Panel.tsx | 4 + .../components/Step3SummaryMode.tsx | 253 ++++++++++++++++-- .../hooks/useStep3Dashboard.ts | 40 ++- .../hooks/useStep3Report.ts | 41 +++ .../hooks/useStep3Summary.ts | 72 ++++- .../pages/RoundtablePlanningPage.tsx | 58 ++-- 8 files changed, 469 insertions(+), 76 deletions(-) diff --git a/frontend-web/src/eightbf-roundtable/pages/EightBfRoundtablePage.tsx b/frontend-web/src/eightbf-roundtable/pages/EightBfRoundtablePage.tsx index 4bd4f5ed..ef5dbf95 100644 --- a/frontend-web/src/eightbf-roundtable/pages/EightBfRoundtablePage.tsx +++ b/frontend-web/src/eightbf-roundtable/pages/EightBfRoundtablePage.tsx @@ -273,6 +273,7 @@ export default function EightBfRoundtablePage({ compactChrome = false }: { compa hideHeaderSaveDraft hideHeaderSettings newTaskLabel="新建会话" + step3Mode="report-only" hideDefaultStep1RightSidebar step2HeaderTitle={`${configuredChain?.title || "8BF 业务链"} · 多智能体会商`} step3HeaderTitle={`${configuredChain?.title || "8BF 业务链"} · 方案总结`} diff --git a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx index 34d98be8..124a5637 100644 --- a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx +++ b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx @@ -61,6 +61,8 @@ interface SeatDelivery { interface HighFidelityReportProps { useStep1Chrome?: boolean; headerTitle?: string; + /** `report-only` is used by 8BF; normal roundtables retain every Step 3 view. */ + step3Mode?: "full" | "report-only"; threadIds: ThreadIdMap | null; coordinatorName: string | null; /** The most recent leader-agent text — rendered as the final consensus. */ @@ -121,9 +123,26 @@ function formatGeneratedAt(d: Date): string { return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } +/** 最终成果区打开的是已落盘文件,不走实时流;独立给工具栏一个稳定的文件类型徽标。 */ +function artifactTypeLabel(fileName: string): string { + const normalized = fileName.trim().toLowerCase(); + if (/\.(md|markdown)$/.test(normalized)) return "MD"; + if (/\.html?$/.test(normalized)) return "HTML"; + if (/\.(mmd|mermaid)$/.test(normalized)) return "MMD"; + if (/\.json$/.test(normalized)) return "JSON"; + if (/\.csv$/.test(normalized)) return "CSV"; + if (/\.ya?ml$/.test(normalized)) return "YAML"; + if (/\.tsx?$/.test(normalized)) return "TS"; + if (/\.js$/.test(normalized)) return "JS"; + if (/\.py$/.test(normalized)) return "PY"; + if (/\.txt$/.test(normalized)) return "TXT"; + return "FILE"; +} + export const HighFidelityReport: React.FC = ({ useStep1Chrome = false, headerTitle, + step3Mode = "full", threadIds, coordinatorName, consensusText, @@ -156,7 +175,9 @@ export const HighFidelityReport: React.FC = ({ return `圆桌总控结论-${short}`; }, [intentReady?.objective]); - // 第 3 步只保留「总结报告」工作台;旧的最终方案展示 / HTML 绘制入口不再开放。 + const hasFullStep3Outputs = step3Mode === "full"; + // 第 3 步主页为总结报告。普通会商可继续打开最终方案、经典可视化和大屏多页; + // 8BF 明确停在报告工作台,不启动或露出这些后续交付。 const [summaryMode, setSummaryMode] = useState(true); const [drawMode, setDrawMode] = useState(false); // 文件沙箱(2026-06-13 改内联):点文件卡片在右侧滑入沙箱面板、左侧席位栏收起 @@ -398,6 +419,9 @@ export const HighFidelityReport: React.FC = ({ const fileSandboxVirtualUrl = fileSandboxTarget ? `write-file:${fileSandboxStubPath}?message_id=hfr-file-stub&tool_call_id=tc-hfr-0` : ""; + const fileSandboxTypeLabel = fileSandboxTarget + ? artifactTypeLabel(fileSandboxTarget.file.name) + : "FILE"; const fileSandboxThreadStub = useMemo(() => { const messages = fileSandboxTarget && fileSandboxContent != null @@ -414,6 +438,14 @@ export const HighFidelityReport: React.FC = ({ }, ], }, + // 最终产物仅借用 write-file 虚拟 URL 来复用预览器;内容已经通过下载接口完整读回, + // 不是流式写入。因此要同时提供 tool result,避免 ArtifactFileDetail 误显示「正在生成」。 + { + id: "hfr-file-tool-result", + type: "tool", + content: "", + tool_call_id: "tc-hfr-0", + }, ] : []; return { @@ -434,7 +466,7 @@ export const HighFidelityReport: React.FC = ({ [], ); - if (summaryMode) { + if (summaryMode || !hasFullStep3Outputs) { return ( = ({ intentReady={intentReady} selectedModel={selectedModel} availableModels={availableModels} + modelsLoading={modelsLoading} setSelectedModel={setSelectedModel} coordinatorPrompt={coordinatorPrompt} savedSummary={savedSummary} summary={summaryFlow} + enableFlow={hasFullStep3Outputs} + onViewStatic={hasFullStep3Outputs ? () => setSummaryMode(false) : undefined} taskArtifactSubmission={taskArtifactSubmission} /> ); } - if (drawMode) { + if (drawMode && hasFullStep3Outputs) { return ( = ({ allowVirtualMarkdownEdit headerLeadingActionBare headerLeadingAction={( - +
+ + {fileSandboxTypeLabel} + + +
)} /> diff --git a/frontend-web/src/roundtable-planning/components/Step3Panel.tsx b/frontend-web/src/roundtable-planning/components/Step3Panel.tsx index 36b271c9..12612f87 100644 --- a/frontend-web/src/roundtable-planning/components/Step3Panel.tsx +++ b/frontend-web/src/roundtable-planning/components/Step3Panel.tsx @@ -26,6 +26,8 @@ export interface Step3PanelProps { /** 8BF 第三步对话区与第一、二步共用深蓝顶栏;默认不改变原会商。 */ useStep1Chrome?: boolean; headerTitle?: string; + /** 普通会商提供完整交付;8BF 仅保留 Markdown 总结报告。 */ + step3Mode?: "full" | "report-only"; threadIds: ThreadIdMap | null; coordinatorName: string | null; consensusText: string; @@ -65,6 +67,7 @@ export function Step3Panel(props: Step3PanelProps) { const { useStep1Chrome = false, headerTitle, + step3Mode = "full", threadIds, coordinatorName, consensusText, @@ -115,6 +118,7 @@ export function Step3Panel(props: Step3PanelProps) { ; + modelsLoading: boolean; setSelectedModel: (model: string) => void; /** 业务链写给总控的编排提示 —— 注入总结智能体作为最高优先级口径。 */ coordinatorPrompt?: string | null; @@ -81,6 +90,10 @@ interface Step3SummaryModeProps { savedSummary?: DraftStep3SummarySnapshot | null; /** 页面层常驻的总结生成流(2026-06-13 提升:切走不断流,进入本视图续看)。 */ summary: UseStep3SummaryReturn; + /** 普通会商保留流程图交付;8BF 为 false。 */ + enableFlow?: boolean; + /** 普通会商进入完整的最终方案展示;8BF 不提供该入口。 */ + onViewStatic?: () => void; taskArtifactSubmission?: TaskArtifactSubmissionController; } @@ -98,10 +111,13 @@ export function Step3SummaryMode({ intentReady, selectedModel, availableModels, + modelsLoading, setSelectedModel, coordinatorPrompt, savedSummary, summary, + enableFlow = false, + onViewStatic, taskArtifactSubmission, }: Step3SummaryModeProps) { void coordinatorPrompt; // 选项已随 hook 提升到页面层,这里仅保留 prop 兼容 @@ -135,6 +151,36 @@ export function Step3SummaryMode({ const { open: artifactsOpen } = useArtifacts(); + // 普通会商的结构化流程图:优先使用后端抽取+校验的结果,兼容旧草稿里 flow-json。 + const flowGraph = useMemo(() => { + if (!enableFlow) return null; + if (summary.flowPhase === "done" && summary.flowGraph) { + return { graph: summary.flowGraph, streaming: false }; + } + if (summary.flowPhase === "extracting" || summary.flowPhase === "validating") { + return null; + } + for (let index = dialogues.length - 1; index >= 0; index -= 1) { + const dialogue = dialogues[index]; + if (dialogue.avatarType === "human") continue; + const graph = extractFlowGraph(dialogue.content ?? ""); + if (graph && (dialogue.content ?? "").includes(FLOW_FENCE_OPEN)) { + return { graph, streaming: dialogue.streaming === true }; + } + } + return null; + }, [dialogues, enableFlow, summary.flowGraph, summary.flowPhase]); + + const [flowView, setFlowView] = useState(false); + const flowAutoOpenedRef = useRef(false); + useEffect(() => { + if (!enableFlow || !flowGraph || flowGraph.graph.nodes.length === 0) return; + if (flowAutoOpenedRef.current) return; + flowAutoOpenedRef.current = true; + // 自动生成的图优先打开;若用户已在报告沙箱内,保留当前阅读位置并露出切换入口。 + if (summary.autoFlow || !artifactsOpen) setFlowView(true); + }, [artifactsOpen, enableFlow, flowGraph, summary.autoFlow]); + // 沙箱里已写出的 Markdown 总结报告。 const mdArtifact = useMemo(() => { for (const [threadId, msgs] of Object.entries(seatStubMessages)) { @@ -156,16 +202,22 @@ export function Step3SummaryMode({ return null; }, [seatStubMessages]); - const handleAsk = (message: string, files?: File[]) => ask(message, files); + const handleAsk = (message: string, files?: File[]) => { + if (enableFlow && detectFlowIntent(message)) { + flowAutoOpenedRef.current = true; + setFlowView(true); + } + ask(message, files); + }; // 报告生成过程(思考、文字说明、文件写入步骤)交由 Step2Message 原样渲染, // 保持与第二步一致的流式过程可见性。 const displayDialogues = dialogues; const [leftCollapsed, setLeftCollapsed] = useState(false); - // 右侧沙箱露出时自动收起左栏,关闭时复原——边沿触发,手动切换不回弹。 + // 右侧沙箱或流程图露出时自动收起左栏,关闭时复原——边沿触发,手动切换不回弹。 // 初值固定为 false:这样即使「进入/续看时沙箱已是打开态」(rightPanelVisible 一上来就为 true), // 首个 effect 也能识别 false→true 的边沿并收起左栏(修此前已打开时左栏不收起的问题)。 - const rightPanelVisible = artifactsOpen; + const rightPanelVisible = artifactsOpen || (!!flowGraph && flowView); const prevRightPanelVisibleRef = useRef(false); useEffect(() => { if (rightPanelVisible !== prevRightPanelVisibleRef.current) { @@ -195,16 +247,23 @@ export function Step3SummaryMode({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const isFlowRunning = + enableFlow && + (summary.flowPhase === "extracting" || summary.flowPhase === "validating"); const isStreaming = phase === "creating" || phase === "streaming" || + isFlowRunning || dialogues.some((d) => d.streaming === true); const activeSeatMessages = activeArtifactThreadId ? seatStubMessages[activeArtifactThreadId] : undefined; - const statusLabel = - phase === "creating" + const statusLabel = isFlowRunning + ? summary.flowPhase === "extracting" + ? "结构化流程图抽取中…" + : "流程图校验 / 修复中…" + : phase === "creating" ? "正在初始化总结线程…" : phase === "streaming" ? "方案总结报告智能体撰写中…" @@ -225,7 +284,7 @@ export function Step3SummaryMode({

方案总结 · 报告

- {(phase === "creating" || phase === "streaming") && ( + {isStreaming && ( )} {phase === "paused" && } @@ -237,12 +296,22 @@ export function Step3SummaryMode({ + {onViewStatic && ( + + )} )} @@ -347,6 +416,24 @@ export function Step3SummaryMode({ isStreaming={isStreaming} seatMessages={activeSeatMessages} taskArtifactSubmission={taskArtifactSubmission} + sidePanel={ + enableFlow && flowGraph && flowView ? ( + setFlowView(false)} + onSwitchToReport={ + mdArtifact + ? () => { + setFlowView(false); + openArtifactPreview(mdArtifact.threadId, mdArtifact.path); + } + : undefined + } + onChange={summary.setFlowGraph} + /> + ) : undefined + } > + {enableFlow && flowGraph && !flowView && ( + + )} + @@ -481,6 +593,11 @@ function AgentProgressPanel({ error, busy, mdAvailable, + enableFlow, + flowPhase, + onRegenerateFlow, + pendingFlowClarification, + onResolveFlowClarification, onRetry, onAsk, onPause, @@ -488,6 +605,7 @@ function AgentProgressPanel({ onArtifactPreview, selectedModel, availableModels, + modelsLoading, setSelectedModel, taskArtifactSubmission, }: { @@ -499,6 +617,12 @@ function AgentProgressPanel({ busy: boolean; /** 沙箱里是否已有 Markdown 总结报告。 */ mdAvailable: boolean; + /** 普通会商才会生成、展示和编辑流程图。 */ + enableFlow: boolean; + flowPhase: UseStep3SummaryReturn["flowPhase"]; + onRegenerateFlow: () => void; + pendingFlowClarification: string | null; + onResolveFlowClarification: UseStep3SummaryReturn["resolveFlowClarification"]; onRetry: () => void; onAsk: (message: string, files?: File[]) => void; onPause: () => void; @@ -506,21 +630,46 @@ function AgentProgressPanel({ onArtifactPreview: UseStep3SummaryReturn["openArtifactPreview"]; selectedModel: string; availableModels: ReadonlyArray; + modelsLoading: boolean; setSelectedModel: (model: string) => void; taskArtifactSubmission?: TaskArtifactSubmissionController; }) { + // 进度面板独立根据 props 判断流程图阶段,不能引用外层 Step3SummaryMode 的局部变量。 + const isFlowRunning = + enableFlow && (flowPhase === "extracting" || flowPhase === "validating"); const scrollRef = useRef(null); - const { atBottom, scrollToBottom } = useAutoScroll(scrollRef, [dialogues]); + const contentRef = useRef(null); + const { atBottom, scrollToBottom, forceScrollToBottom } = useAutoScroll( + scrollRef, + [dialogues, flowPhase], + { contentRef }, + ); // 新一轮气泡出现(报告生成或用户续问)→ 强制滚到底, // 让用户看到新阶段。仅在气泡**条数**增加时触发, // 流式增量内容仍由 useAutoScroll 的「在底部才跟随」逻辑处理,不抢用户向上翻阅。 const prevDialogueCountRef = useRef(dialogues.length); useEffect(() => { if (dialogues.length > prevDialogueCountRef.current) { - scrollToBottom(); + forceScrollToBottom(); } prevDialogueCountRef.current = dialogues.length; - }, [dialogues.length, scrollToBottom]); + }, [dialogues.length, forceScrollToBottom]); + // 结构化抽取可能复用前一张「调度中」气泡,导致消息条数不变;只依赖上面的 + // dialogues.length 时就不会触发定位。阶段切换后等 DOM 提交一帧再强制下滑, + // 保证用户能立刻看到抽取/校验进度;后续增量仍由 useAutoScroll 按用户是否上翻决定。 + useEffect(() => { + if (!enableFlow || (flowPhase !== "extracting" && flowPhase !== "validating")) return; + let followUpFrameId: number | null = null; + const frameId = window.requestAnimationFrame(() => { + forceScrollToBottom(); + // 校验卡及其中的流式内容可能在第一帧后才完成布局,再确认一次最终高度。 + followUpFrameId = window.requestAnimationFrame(forceScrollToBottom); + }); + return () => { + window.cancelAnimationFrame(frameId); + if (followUpFrameId !== null) window.cancelAnimationFrame(followUpFrameId); + }; + }, [enableFlow, flowPhase, forceScrollToBottom]); // 「回答速度指示器」—— 与主页面共用全局开关 streamSpeedMeterEnabled。 const [localSettings] = useLocalSettings(); const speedMeterEnabled = localSettings.display.streamSpeedMeterEnabled; @@ -598,10 +747,12 @@ function AgentProgressPanel({ id="step3-summary-model-select" value={selectedModel} onChange={(e) => setSelectedModel(e.target.value)} - disabled={busy || availableModels.length === 0} + disabled={busy || modelsLoading || availableModels.length === 0} title={ - availableModels.length === 0 - ? "未在 config.yaml 中配置任何模型" + modelsLoading + ? "正在加载模型列表…" + : availableModels.length === 0 + ? "未在 config.yaml 中配置任何模型" : busy ? "撰写中不可切换;完成后或重新生成时生效" : "切换撰写使用的推理模型(下次生成生效)" @@ -609,7 +760,7 @@ function AgentProgressPanel({ className="shrink-0 max-w-[180px] bg-slate-50/50 border border-slate-200 rounded-xl px-2.5 py-1.5 text-[11px] font-bold text-slate-700 cursor-pointer focus:outline-none focus:border-primary focus:ring-1 focus:ring-[var(--brand-200)]/50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-secondary dark:text-foreground dark:border-border" > {availableModels.length === 0 && ( - + )} {availableModels.map((m) => (