From 432c12fbac8bb6eb920c6a18b090e3fda65cd485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Sat, 13 Jun 2026 15:32:49 +0800 Subject: [PATCH] =?UTF-8?q?@=20feat(=E5=9C=86=E6=A1=8C):=20Step3=20?= =?UTF-8?q?=E5=A4=A7=E5=B1=8F=E5=A4=9A=E9=A1=B5=E6=A8=A1=E6=9D=BF=E9=87=8D?= =?UTF-8?q?=E6=9E=84=20+=20=E7=8B=AC=E7=AB=8B=20dashboard=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 roundtable-planning/dashboard 模块化大屏(DashboardApp + 各分析卡片 组件 + data-context + 类型/样式) - Step3DashboardMode 接入新 dashboard,report-template 大屏 HTML 模板重写 - useStep3Dashboard 配套调整 Co-Authored-By: Claude Opus 4.8 (1M context) @ --- .../components/Step3DashboardMode.tsx | 559 +++++++++++------- .../dashboard/DashboardApp.tsx | 163 +++++ .../dashboard/components/AnalyticsCenter.tsx | 286 +++++++++ .../dashboard/components/BrandScores.tsx | 249 ++++++++ .../dashboard/components/ConsensusPanel.tsx | 60 ++ .../dashboard/components/DetailView.tsx | 213 +++++++ .../dashboard/components/DraftingCard.tsx | 46 ++ .../dashboard/components/MetricsHeader.tsx | 59 ++ .../dashboard/components/RiskDonut.tsx | 115 ++++ .../dashboard/components/Roadmap.tsx | 97 +++ .../components/SeatDeliveryOverview.tsx | 141 +++++ .../dashboard/dashboard.css | 37 ++ .../dashboard/data-context.tsx | 439 ++++++++++++++ .../roundtable-planning/dashboard/types.ts | 104 ++++ .../hooks/useStep3Dashboard.ts | 22 +- .../lib/report-template.ts | 373 ++++++++---- 16 files changed, 2642 insertions(+), 321 deletions(-) create mode 100644 frontend-web/src/roundtable-planning/dashboard/DashboardApp.tsx create mode 100644 frontend-web/src/roundtable-planning/dashboard/components/AnalyticsCenter.tsx create mode 100644 frontend-web/src/roundtable-planning/dashboard/components/BrandScores.tsx create mode 100644 frontend-web/src/roundtable-planning/dashboard/components/ConsensusPanel.tsx create mode 100644 frontend-web/src/roundtable-planning/dashboard/components/DetailView.tsx create mode 100644 frontend-web/src/roundtable-planning/dashboard/components/DraftingCard.tsx create mode 100644 frontend-web/src/roundtable-planning/dashboard/components/MetricsHeader.tsx create mode 100644 frontend-web/src/roundtable-planning/dashboard/components/RiskDonut.tsx create mode 100644 frontend-web/src/roundtable-planning/dashboard/components/Roadmap.tsx create mode 100644 frontend-web/src/roundtable-planning/dashboard/components/SeatDeliveryOverview.tsx create mode 100644 frontend-web/src/roundtable-planning/dashboard/dashboard.css create mode 100644 frontend-web/src/roundtable-planning/dashboard/data-context.tsx create mode 100644 frontend-web/src/roundtable-planning/dashboard/types.ts diff --git a/frontend-web/src/roundtable-planning/components/Step3DashboardMode.tsx b/frontend-web/src/roundtable-planning/components/Step3DashboardMode.tsx index c1af8c62..7d0a5ec6 100644 --- a/frontend-web/src/roundtable-planning/components/Step3DashboardMode.tsx +++ b/frontend-web/src/roundtable-planning/components/Step3DashboardMode.tsx @@ -1,20 +1,29 @@ /** - * Step3「结果绘制 · 大屏多页模式(模式二)」—— 与模式一同款交互(2026-06-13 重构)。 + * Step3「结果绘制 · 大屏多页」—— 两视图: * - * 布局/交互对齐模式一(Step3ReportMode):左侧流式对话气泡 + 右侧沙箱(复用 - * `Step2SandboxLayout`,大屏 HTML 以 write_file 虚拟 stub 进沙箱 → 代码/预览切换、 - * 下载、新窗口打开全部可用)+ 底部修改要求输入框(同线程续轮修订全量 report-json)。 + * ① 交互区 (view="chat"):左侧大屏绘制智能体的聊天记录 + 输入框(模型问答改页面), + * 右侧复用 `Step2SandboxLayout` 沙箱「代码预览」实时展示数据抽取过程(report-json + * 边出边以代码态显示)。生成完成自动切到 ②。 + * ② 大屏页面 (view="page"):应用内直接渲染参考大屏 React 组件(`DashboardApp`,真实 + * 数据经 `adaptReportData` 注入)。顶部按钮:重新绘制(回交互区并重画)、新窗口打开 + * (把自包含 HTML 大屏在新标签页打开)、单页看板、返回报告。 * - * 与模式一的差别只在产物管线:模型输出 1-2KB ```report-json,前端模板渲染 - * (lib/report-template)—— 快一个数量级、零截断;链路(initReportThread + - * streamMultiAgent)与模式一完全同源。 + * 数据抽取/聊天/沙箱链路全部复用既有 `useStep3Dashboard`(流式问答、stub 沙箱、续看), + * 只是把「完成后的产物」从 iframe HTML 预览换成应用内 React 大屏;HTML 沙箱那条路保留, + * 供「新窗口打开」与并行的经典模式使用。 */ -import { useEffect, useMemo, useRef, useState, type FormEvent } from "react"; +import { + useEffect, + useMemo, + useRef, + useState, + type FormEvent, +} from "react"; import { ArrowLeft, CheckCircle, - Eye, - LayoutDashboard, + ExternalLink, + LayoutPanelTop, Loader2, Pause, RefreshCw, @@ -22,15 +31,18 @@ import { Sparkles, } from "lucide-react"; -import { cn } from "@/lib/utils"; +import { useLocalSettings } from "@/core/settings"; import type { DraftStep3Snapshot } from "@/roundtable-planning/api/drafts"; -import { getAvatar } from "@/roundtable-planning/components/Avatars"; -import { MarkdownContent } from "@/roundtable-planning/components/MessageBubble"; -import { Step2SandboxLayout } from "@/roundtable-planning/components/Step2SandboxLayout"; +import { DashboardApp } from "@/roundtable-planning/dashboard/DashboardApp"; +import { adaptReportData } from "@/roundtable-planning/dashboard/data-context"; import type { ModelOption } from "@/roundtable-planning/components/Step3ReportMode"; +import { ScrollToBottomButton } from "@/roundtable-planning/components/ScrollToBottomButton"; +import { Step2Message } from "@/roundtable-planning/components/Step2Panel"; +import { Step2SandboxLayout } from "@/roundtable-planning/components/Step2SandboxLayout"; import { useAutoScroll } from "@/roundtable-planning/hooks/useAutoScroll"; +import { useDialogueSpeedMetrics } from "@/roundtable-planning/hooks/useDialogueSpeedMetrics"; import type { UseStep3DashboardReturn } from "@/roundtable-planning/hooks/useStep3Dashboard"; -import { isDashboardHtml, stripReportFence } from "@/roundtable-planning/lib/report-template"; +import { parseReportJson } from "@/roundtable-planning/lib/report-template"; interface Step3DashboardModeProps { selectedModel: string; @@ -38,7 +50,6 @@ interface Step3DashboardModeProps { modelsLoading: boolean; setSelectedModel: (model: string) => void; savedReport?: DraftStep3Snapshot | null; - /** 页面层常驻的大屏生成流(2026-06-13 提升:切走不断流,进入续看)。 */ dashboard: UseStep3DashboardReturn; /** 切回经典「单页看板」绘制模式。 */ onSwitchClassic: () => void; @@ -46,6 +57,14 @@ interface Step3DashboardModeProps { onBack: () => void; } +function formatNow(): string { + const d = new Date(); + const pad = (n: number) => n.toString().padStart(2, "0"); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; +} + +type DashView = "chat" | "page"; + export function Step3DashboardMode({ selectedModel, availableModels, @@ -56,154 +75,115 @@ export function Step3DashboardMode({ onSwitchClassic, onBack, }: Step3DashboardModeProps) { - // 生成流来自页面层常驻 hook(切走不断流,进入本视图续看进行中过程/结果)。 const { phase, - error, - dashThreadId, dialogues, seatStubMessages, activeArtifactThreadId, - busy, html, - start, + busy, redraw, ask, abort, - hydrateSaved, + setPreviewEnabled, openDashboardPreview, } = dashboard; - // 预览开关:本视图在前台时本流才允许自动打开沙箱(并行流防抢);进入时若已有 - // 产物/进行中会立即接管沙箱续看。 + // 真实数据 → 大屏数据:优先本轮实时生成,其次草稿快照;都没有则 undefined(用 DEMO)。 + const dashboardData = useMemo(() => { + const report = dashboard.data ?? parseReportJson(savedReport?.dashboardJson ?? null); + return report ? adaptReportData(report, formatNow()) : undefined; + }, [dashboard.data, savedReport?.dashboardJson]); + + const hasResult = !!dashboardData || !!html; + + // 视图:有产物默认看「大屏页面」,否则进「交互区」看抽取过程。 + const [view, setView] = useState(() => (hasResult ? "page" : "chat")); + + // 完成即自动切到大屏页面(含问答修订后的再次完成)。 + const prevPhaseRef = useRef(phase); useEffect(() => { - dashboard.setPreviewEnabled(true); - return () => dashboard.setPreviewEnabled(false); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + if (phase === "done" && prevPhaseRef.current !== "done") setView("page"); + prevPhaseRef.current = phase; + }, [phase]); - // 续看语义:只兜底"从未启动过"(idle);**卸载不再 abort**(切走不断流)。 - // 优先 dashboardJson(结构化数据,按当前主题渲染);旧版 marker html 兜底。 + // 仅在交互区允许本流自动接管沙箱(并行流防抢);离开/卸载时让出。 useEffect(() => { - if (phase !== "idle") return; - if (savedReport?.dashboardJson) { - dashboard.hydrateFromJson(savedReport.dashboardJson); - } else if (savedReport?.html && isDashboardHtml(savedReport.html)) { - hydrateSaved(savedReport.html); - } else { - start(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + setPreviewEnabled(view === "chat"); + return () => setPreviewEnabled(false); + }, [view, setPreviewEnabled]); - const [revision, setRevision] = useState(""); - const scrollRef = useRef(null); - useAutoScroll(scrollRef, [dialogues]); - - const handleSubmit = (e: FormEvent) => { - e.preventDefault(); - const text = revision.trim(); - if (!text) return; - setRevision(""); - ask(text); + // 新窗口打开:把自包含 HTML 大屏(renderDashboardHtml 产物)在新标签页打开。 + const handleOpenNewWindow = () => { + if (!html || typeof window === "undefined") return; + const blob = new Blob([html], { type: "text/html;charset=utf-8" }); + const url = URL.createObjectURL(blob); + window.open(url, "_blank", "noopener,noreferrer"); + window.setTimeout(() => URL.revokeObjectURL(url), 60_000); }; - // 气泡显示:report-json 生 JSON 替换成一句人话(对齐 flow-json 的处理)。 - const displayDialogues = useMemo( - () => - dialogues.map((d) => { - const content = d.content ?? ""; - if (!content.includes("```report-json")) return d; - return { ...d, content: stripReportFence(content, d.streaming === true) }; - }), - [dialogues], - ); + const handleRedraw = () => { + setView("chat"); + redraw(); + }; - const isStreaming = busy || dialogues.some((d) => d.streaming === true); - const activeSeatMessages = activeArtifactThreadId - ? seatStubMessages[activeArtifactThreadId] - : undefined; - - const statusLabel = - phase === "creating" - ? "正在初始化绘制线程…" - : phase === "streaming" - ? "大屏数据提炼中…" - : phase === "done" - ? "绘制完成" - : phase === "error" - ? "绘制失败" - : "待绘制"; + const isStreaming = + phase === "creating" || phase === "streaming" || dialogues.some((d) => d.streaming === true); return (
- {/* 顶栏(对齐模式一样式) */} -
+ {/* 顶栏 */} +
- +

结果绘制 · 大屏多页

- {(phase === "creating" || phase === "streaming") && ( - - )} + {(phase === "creating" || phase === "streaming") && } {phase === "done" && } - {statusLabel} + {view === "chat" ? "交互绘制中" : "大屏页面"}
+
- - {busy && ( - + {view === "page" ? ( + <> + + + + ) : ( + hasResult && ( + + ) )} - {html && ( - - )} -
- {/* 主体:对话 + 沙箱(复用第 2 步分栏,与模式一一致) */} -
-
- -
- {/* 对话流 */} -
- {displayDialogues.length === 0 && ( -
- - 正在初始化大屏绘制… -
- )} - {displayDialogues.map((d) => ( -
-
{getAvatar(d.avatarType ?? "blue", "h-7 w-7")}
-
-
- {d.sender} - - {d.confidence} - - {d.time} -
-
- {d.content || "…"} - {d.streaming && ( - - )} -
-
-
- ))} - {error && phase === "error" && ( -
- {error} -
- )} -
+ {/* 主体:交互区(聊天 + 抽取沙箱) / 大屏页面(React) */} + {view === "page" ? ( +
+ +
+ ) : ( +
+
+ + + +
+
+ )} +
+ ); +} - {/* 修改要求输入框(与模式一同款交互:随时可发,busy 时先打断再续) */} -
-
-