diff --git a/frontend-web/src/App.tsx b/frontend-web/src/App.tsx index 660465b4..0c191a09 100644 --- a/frontend-web/src/App.tsx +++ b/frontend-web/src/App.tsx @@ -99,11 +99,15 @@ export function App() { } /> } + element={} /> } + element={} + /> + } /> ` with NO authentication — anyone with - * the link can open it. Two views, driven by the `report` prop / route: + * the link can open it. Three views, driven by the `view` prop / route: * * - landing (`#/roundtable-share/`): a read-only result page mirroring the - * in-app Step 3「最终方案展示」— 总控最终结论 + 各席位最终交付 + a 「查看结果绘制」 - * button that opens the full report in a NEW TAB (when one was generated). - * - report (`#/roundtable-share//report`): the Step 3 report (a + * in-app Step 3「最终方案展示」— 结果绘制/大屏多页入口 + 方案总结报告 + 总控最终 + * 结论 + 各席位最终交付. + * - report (`#/roundtable-share//report`): the Step 3 经典 HTML 看板 (a * self-contained HTML document) rendered full-screen inside a sandboxed - * `iframe` (no `allow-scripts`), so the agent-generated HTML can never execute - * code in the app origin. + * `iframe` (no `allow-scripts`). + * - dashboard (`#/roundtable-share//dashboard`): the Step 3 大屏多页 (模式二) + * — rendered on the fly from the stored structured data via `renderDashboardHtml` + * into the same sandboxed `iframe`. */ export default function PublicRoundtableSharePage({ - report = false, + view = "landing", }: { - report?: boolean; + view?: RoundtableShareView; }) { const { shareCode } = useParams<{ shareCode: string }>(); // 嵌套方可用 ?bg=/?text= 或 postMessage 覆盖背景/文字色。 @@ -108,6 +118,17 @@ export default function PublicRoundtableSharePage({ refetchOnWindowFocus: false, }); + // 大屏多页:用与应用内一致的 React 大屏(DashboardApp)渲染,而非自包含 HTML 模板。 + // 把存储的结构化数据(report-json)解析并 adapt 成 DashboardApp 的入参;与 + // Step3DashboardMode 的「应用渲染」、DashboardStandalonePage 的「新窗口打开」同源。 + const dashboardData = useMemo(() => { + const json = data?.dashboard_json; + if (!json) return undefined; + const parsed = parseReportJson(json); + if (!parsed) return undefined; + return adaptReportData(parsed, data?.generated_at ?? ""); + }, [data?.dashboard_json, data?.generated_at]); + if (isLoading) { return (
@@ -128,8 +149,24 @@ export default function PublicRoundtableSharePage({ ); } - // ── 报告页:整页沙箱 iframe 展示第 3 步结果绘制 ────────────────────────────── - if (report) { + // ── 大屏多页:整屏渲染应用内 React 大屏(DashboardApp),与应用渲染/新窗口打开一致 ── + if (view === "dashboard") { + return ( +
+ {dashboardData ? ( + + ) : ( +
+

暂无大屏多页

+

这次会商还没有生成第三步「大屏多页」的方案报告。

+
+ )} +
+ ); + } + + // ── 结果绘制(经典 HTML 看板):整页沙箱 iframe 展示自包含报告 ───────────────── + if (view === "report") { return (
@@ -161,8 +198,10 @@ export default function PublicRoundtableSharePage({ ); } - // ── 落地页:总控最终结论 + 各席位最终交付 + 查看结果绘制(新标签打开报告页)───── - const reportUrl = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}/report`; + // ── 落地页:结果绘制/大屏多页入口 + 方案总结报告 + 总控最终结论 + 各席位最终交付 ── + const shareBase = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}`; + const reportUrl = `${shareBase}/report`; + const dashboardUrl = `${shareBase}/dashboard`; return (
@@ -182,23 +221,39 @@ export default function PublicRoundtableSharePage({
- {/* 1. 结果绘制 —— 新标签打开报告页 */} + {/* 1. 结果绘制 / 大屏多页 —— 新标签打开整页报告 */}

结果绘制

- {data.has_report ? ( - - - 查看结果绘制 - - + {data.has_report || data.has_dashboard ? ( +
+ {data.has_report && ( + + + 查看结果绘制 + + + )} + {data.has_dashboard && ( + + + 查看大屏多页 + + + )} +
) : (

这次会商还没有生成第三步「结果绘制」的方案报告。 @@ -206,7 +261,22 @@ export default function PublicRoundtableSharePage({ )}

- {/* 2. 总控最终结论 */} + {/* 2. 方案总结报告(Markdown) */} + {data.has_summary_report && ( +
+

+ + 方案总结报告 +

+
+ + {data.summary_report_md} + +
+
+ )} + + {/* 3. 总控最终结论 */}

@@ -223,7 +293,7 @@ export default function PublicRoundtableSharePage({ )}

- {/* 3. 各席位最终交付 */} + {/* 4. 各席位最终交付 */}

diff --git a/offline-backend-20260512/backend/app/gateway/routers/roundtable_draft_shares.py b/offline-backend-20260512/backend/app/gateway/routers/roundtable_draft_shares.py index e2a7b38b..80d01128 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/roundtable_draft_shares.py +++ b/offline-backend-20260512/backend/app/gateway/routers/roundtable_draft_shares.py @@ -115,10 +115,17 @@ class PublicDraftShareSnapshotResponse(BaseModel): seat_deliveries: list[SeatDeliveryItem] = Field(default_factory=list) # The Step 3「结果绘制」report — a self-contained HTML document. Empty when the # shared draft never reached / generated a result report. - html: str = Field(default="", description="Self-contained Step 3 result report HTML") + html: str = Field(default="", description="Self-contained Step 3 result report HTML (经典 HTML 看板)") summary: str | None = Field(default=None, description="The report's closing one-liner") generated_at: str | None = Field(default=None, description="When the report was generated (ISO-8601)") - has_report: bool = Field(default=False, description="Whether a Step 3 report exists") + has_report: bool = Field(default=False, description="Whether a Step 3 HTML report exists") + # Step 3「方案总结报告」—— the Markdown report from roundtable-summary (step3.summaryReport.md). + summary_report_md: str = Field(default="", description="Step 3 Markdown 方案总结报告") + has_summary_report: bool = Field(default=False, description="Whether a Step 3 Markdown summary report exists") + # Step 3「大屏多页」(模式二) —— structured dashboard data (step3.dashboardJson); the + # frontend renders it to self-contained HTML via renderDashboardHtml at view time. + dashboard_json: str | None = Field(default=None, description="Step 3 大屏多页 structured data (report-json)") + has_dashboard: bool = Field(default=False, description="Whether a Step 3 大屏多页 dashboard exists") created_at: str | None = Field(default=None, description="When the share was created (ISO-8601)") @@ -259,29 +266,32 @@ async def import_share( def _result_sections(draft: dict) -> tuple[str, list[dict[str, str]]]: """Derive (总控最终结论, 各席位最终交付) from a draft's Step 2 snapshot. - Mirrors the frontend `Step3Panel`: the consensus is `step2.lastLeaderContent` - and each seat delivery is a Step 2 dialogue bubble tagged - ``confidence == "子智能体交付"``. Falls back to the active run's snapshot when - the top-level Step 2 fields are empty (multi-run drafts). + Mirrors the frontend draft hydration (`RoundtablePlanningPage.normalizeStep2Runs`): + when the draft has ``runs[]`` the rendered content is the **active run** + (``activeRunId``), NOT the top-level ``step2`` fields — those can be a stale + foreground seed (e.g. only the 系统消息/开场 2 条气泡) left over after a later + background job wrote its results into ``runs[]``. Reading the top level there + yields an empty consensus and no deliveries (the reported bug). Only drafts + with no ``runs[]`` (legacy/foreground-only) fall back to the top-level fields. + + The consensus is ``lastLeaderContent`` and each seat delivery is a dialogue + bubble tagged ``confidence == "子智能体交付"``. """ step2 = draft.get("step2") if isinstance(draft.get("step2"), dict) else {} step2 = step2 or {} - consensus = step2.get("lastLeaderContent") or "" - dialogues = step2.get("step2RoundtableDialogues") + + runs = [r for r in step2.get("runs", []) if isinstance(r, dict)] if isinstance(step2.get("runs"), list) else [] + if runs: + active_id = step2.get("activeRunId") + source = next((r for r in runs if r.get("id") == active_id), None) or runs[0] + else: + source = step2 + + consensus = source.get("lastLeaderContent") or "" + dialogues = source.get("step2RoundtableDialogues") if not isinstance(dialogues, list): dialogues = [] - # Multi-run drafts keep the freshest content on the active run; use it when - # the top-level snapshot carries nothing. - if not consensus and not dialogues and isinstance(step2.get("runs"), list): - runs = [r for r in step2["runs"] if isinstance(r, dict)] - active_id = step2.get("activeRunId") - active = next((r for r in runs if r.get("id") == active_id), None) or (runs[0] if runs else None) - if active: - consensus = active.get("lastLeaderContent") or "" - d = active.get("step2RoundtableDialogues") - dialogues = d if isinstance(d, list) else [] - seat_deliveries = [ {"sender": str(d.get("sender") or ""), "content": str(d.get("content") or "")} for d in dialogues @@ -313,8 +323,14 @@ async def get_public_draft_share_snapshot( if draft is None: raise HTTPException(status_code=404, detail="The shared draft no longer exists") - step3 = draft.get("step3") if isinstance(draft.get("step3"), dict) else None - html = (step3 or {}).get("html") or "" + step3 = draft.get("step3") if isinstance(draft.get("step3"), dict) else {} + step3 = step3 or {} + html = step3.get("html") or "" + # 方案总结报告(Markdown,模式:roundtable-summary)—— 嵌在 step3.summaryReport.md。 + summary_report = step3.get("summaryReport") if isinstance(step3.get("summaryReport"), dict) else {} + summary_report_md = (summary_report or {}).get("md") or "" + # 大屏多页(模式二)—— 只存结构化数据 step3.dashboardJson,前端按主题渲染成自包含 HTML。 + dashboard_json = step3.get("dashboardJson") or None consensus_text, seat_deliveries = _result_sections(draft) return PublicDraftShareSnapshotResponse( share_code=share_code, @@ -323,8 +339,12 @@ async def get_public_draft_share_snapshot( consensus_text=consensus_text, seat_deliveries=[SeatDeliveryItem(**s) for s in seat_deliveries], html=html, - summary=(step3 or {}).get("summary"), - generated_at=(step3 or {}).get("generatedAt"), + summary=step3.get("summary"), + generated_at=step3.get("generatedAt"), has_report=bool(html), + summary_report_md=summary_report_md, + has_summary_report=bool(summary_report_md), + dashboard_json=dashboard_json, + has_dashboard=bool(dashboard_json), created_at=share.get("created_at"), )