diff --git a/frontend-web/src/components/ui/dialog.tsx b/frontend-web/src/components/ui/dialog.tsx index 4fa8d7fb..a95a00ae 100644 --- a/frontend-web/src/components/ui/dialog.tsx +++ b/frontend-web/src/components/ui/dialog.tsx @@ -4,28 +4,38 @@ import * as React from "react"; import * as DialogPrimitive from "@radix-ui/react-dialog"; import { XIcon } from "lucide-react"; +import { isWujieEnv } from "@/core/embed/wujie"; import { cn } from "@/lib/utils"; function Dialog({ + modal, ...props }: React.ComponentProps) { - return ; + // 无界嵌入时 Radix modal 会给 body 注入异常 margin-right,导致右侧空白。 + // 默认降为 non-modal;显式传 modal={true} 仍可强制 modal。 + return ( + + ); } -const DialogTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ ...props }, ref) => { - return ( - - ); -}); - -DialogTrigger.displayName = DialogPrimitive.Trigger.displayName; +const DialogTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ ...props }, ref) => { + return ( + + ); +}); + +DialogTrigger.displayName = DialogPrimitive.Trigger.displayName; function DialogPortal({ ...props @@ -33,68 +43,68 @@ function DialogPortal({ return ; } -const DialogClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ ...props }, ref) => { - return ; -}); - -DialogClose.displayName = DialogPrimitive.Close.displayName; +const DialogClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ ...props }, ref) => { + return ; +}); -const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { - return ( - - ); -}); - -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; +DialogClose.displayName = DialogPrimitive.Close.displayName; -const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - showCloseButton?: boolean; - } ->(({ className, children, showCloseButton = true, ...props }, ref) => { - return ( - - - - {children} - {showCloseButton && ( - - - Close - - )} - - - ); -}); - -DialogContent.displayName = DialogPrimitive.Content.displayName; +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + showCloseButton?: boolean; + } +>(({ className, children, showCloseButton = true, ...props }, ref) => { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ); +}); + +DialogContent.displayName = DialogPrimitive.Content.displayName; function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return ( diff --git a/frontend-web/src/pages/EmbedRoundtablePage.tsx b/frontend-web/src/pages/EmbedRoundtablePage.tsx index 1e35f85f..2d626389 100644 --- a/frontend-web/src/pages/EmbedRoundtablePage.tsx +++ b/frontend-web/src/pages/EmbedRoundtablePage.tsx @@ -6,7 +6,7 @@ import { useSearchParams } from "next/navigation"; import { useTheme } from "@/contexts/ThemeContext"; import { loginByUsername } from "@/core/auth/api"; import { setStoredAuth } from "@/core/auth"; -import { readWujieProps } from "@/core/embed/wujie"; +import { readWujieProps, isWujieEnv } from "@/core/embed/wujie"; import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanningPage"; import { ChatRuntime } from "./WorkspaceRoutes"; import "@/roundtable-planning/styles/roundtable-planning.css"; @@ -66,6 +66,12 @@ export default function EmbedRoundtablePage() { setTheme(resolveEmbedRoundtableTheme(themeParam)); }, [themeParam, setTheme]); + useEffect(() => { + if (!isWujieEnv()) return; + document.documentElement.classList.add("wujie-embed"); + return () => document.documentElement.classList.remove("wujie-embed"); + }, []); + useEffect(() => { let cancelled = false; async function runLogin() { diff --git a/frontend-web/src/roundtable-planning/components/BusinessChainListDialog.tsx b/frontend-web/src/roundtable-planning/components/BusinessChainListDialog.tsx index e34e0ea9..107d33d7 100644 --- a/frontend-web/src/roundtable-planning/components/BusinessChainListDialog.tsx +++ b/frontend-web/src/roundtable-planning/components/BusinessChainListDialog.tsx @@ -27,7 +27,6 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { isWujieEnv } from "@/core/embed/wujie"; import { cn } from "@/lib/utils"; import { listChains, setChainPublic, type ChainScope } from "../api/chains"; @@ -188,15 +187,8 @@ export function BusinessChainListDialog({ onOpenChange, onConfigure, }: BusinessChainListDialogProps) { - const inWujie = isWujieEnv(); return ( - + diff --git a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx index e394dc90..b01d7c15 100644 --- a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx +++ b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx @@ -157,6 +157,8 @@ export default function RoundtablePlanningPage() { const isDark = theme === "dark"; const navigate = useNavigate(); const location = useLocation(); + /** 无界(wujie)嵌入抽屉:隐藏业务链条入口,设置按钮改作手动回传交付。 */ + const inWujieEmbed = isWujieEnv(); // ─── 会商自动化串联(模块 C):任务页带进来的预填 ───────────────── // 只在首次渲染读一次 navigation state;随后清掉 history state,避免刷新/后退重放。 @@ -167,6 +169,12 @@ export default function RoundtablePlanningPage() { ); // 由 prefill.businessCode 解析出的「自动选用业务链」结果;有值时「进入研讨」跳过选链弹窗直接进 Step 2。 const [autoChainResult, setAutoChainResult] = useState(null); + /** 无界直传 chainId 时,链条解析完成前禁止弹选链窗(避免 modal 布局抖动)。 */ + const [autoChainLoading, setAutoChainLoading] = useState( + () => + inWujieEmbed && + !!(prefillRef.current?.chainId || prefillRef.current?.businessCode), + ); const autoStartedRef = useRef(false); const prefillClearedRef = useRef(false); // 共识达成后只向父页面(无界)回传一次交付结果。 @@ -774,6 +782,7 @@ export default function RoundtablePlanningPage() { useEffect(() => { const prefill = prefillRef.current; if (!prefill?.chainId && !prefill?.businessCode) return; + setAutoChainLoading(true); let cancelled = false; void (async () => { try { @@ -794,6 +803,8 @@ export default function RoundtablePlanningPage() { if (!cancelled && result) setAutoChainResult(result); } catch { /* 解析失败 → 保持 null,回退弹窗手选 */ + } finally { + if (!cancelled) setAutoChainLoading(false); } })(); return () => { @@ -801,13 +812,9 @@ export default function RoundtablePlanningPage() { }; }, []); - // C4(无界回传):第二步达成共识后,把总控结论 + 各智能体交付一次性广播给父页面。 - // 用 ref 防重(共识态会多次触发渲染);非无界环境静默跳过。 - useEffect(() => { - if (!step2.hasConsensus || consensusEmittedRef.current) return; - if (!isWujieEnv()) return; - consensusEmittedRef.current = true; - emitWujieEvent("roundtable:consensus", { + /** 组装无界回传载荷:总控共识 + 各智能体最终交付(剔除总控席位)。 */ + const buildWujieConsensusPayload = useCallback( + () => ({ taskTitle: prefillRef.current?.taskTitle ?? null, chainId: prefillRef.current?.chainId ?? null, chainTitle: chainMeta?.title ?? null, @@ -817,14 +824,43 @@ export default function RoundtablePlanningPage() { step2.step2RoundtableDialogues, step2.coordinatorName, ), - }); - }, [ - step2.hasConsensus, - step2.coordinatorName, - step2.lastLeaderContent, - step2.step2RoundtableDialogues, - chainMeta, - ]); + }), + [ + chainMeta, + step2.coordinatorName, + step2.lastLeaderContent, + step2.step2RoundtableDialogues, + ], + ); + + const emitConsensusToParent = useCallback(() => { + if (!inWujieEmbed) return false; + return emitWujieEvent("roundtable:consensus", buildWujieConsensusPayload()); + }, [buildWujieConsensusPayload, inWujieEmbed]); + + const handleSettingsClick = useCallback(() => { + if (inWujieEmbed) { + const deliverables = buildWujieConsensusPayload().deliverables; + if (deliverables.length === 0) { + triggerToast("暂无可回传的智能体交付结果"); + return; + } + if (emitConsensusToParent()) { + triggerToast(`已回传 ${deliverables.length} 个智能体交付结果`); + } + return; + } + triggerToast("⚙️ 系统管理面板拉取中(暂不开放外部审计)"); + }, [buildWujieConsensusPayload, emitConsensusToParent, inWujieEmbed, triggerToast]); + + // C4(无界回传):第二步达成共识后,把总控结论 + 各智能体交付一次性广播给父页面。 + // 用 ref 防重(共识态会多次触发渲染);非无界环境静默跳过。 + useEffect(() => { + if (!step2.hasConsensus || consensusEmittedRef.current) return; + if (!inWujieEmbed) return; + consensusEmittedRef.current = true; + emitConsensusToParent(); + }, [step2.hasConsensus, inWujieEmbed, emitConsensusToParent]); // 把页面重置回「第一步全新任务」——新建任务 + 后台挂起后都复用。 // 注意:只清前端状态,不删后端已建的草稿 / 已挂起的后台作业(它们继续在后台跑, @@ -1016,15 +1052,17 @@ export default function RoundtablePlanningPage() { />
- + {!inWujieEmbed && ( + + )}