diff --git a/frontend-web/design-qa.md b/frontend-web/design-qa.md new file mode 100644 index 00000000..7260e9cb --- /dev/null +++ b/frontend-web/design-qa.md @@ -0,0 +1,46 @@ +**Comparison target** + +- Source visual truth (dark intelligent-agent list): `C:/Users/董宏凯/AppData/Local/Temp/codex-clipboard-75f6ce1c-cd70-4b8c-a80a-a3d100105c69.png` +- Additional source references: `C:/Users/董宏凯/AppData/Local/Temp/codex-clipboard-00567484-ba8a-4595-b386-52e1467ae9cf.png` (segmented switch), `C:/Users/董宏凯/AppData/Local/Temp/codex-clipboard-02808803-05ba-4f2c-be50-c72b484a9f06.png` (business-chain node card), `C:/Users/董宏凯/AppData/Local/Temp/codex-clipboard-adf38fec-7baf-4d01-9c73-bdbae94b774e.png` (business-chain selector) +- Implementation screenshots: `C:/Users/董宏凯/AppData/Local/Temp/position-roundtable-revised-light.png`, `C:/Users/董宏凯/AppData/Local/Temp/position-roundtable-revised-dark.png` +- Full-view comparison evidence: `C:/Users/董宏凯/AppData/Local/Temp/position-roundtable-revised-comparison.png` +- Viewport: 1280 × 720. Light route: `#/page/strategy/qa/position-roundtable`; dark embedded route: `#/page/strategy/qa/position-roundtable?embed=1&theme=dark`. +- State: local browser has no authenticated API session. The original intent API is invoked and reports `Authentication required`; business-chain and agent endpoints return no usable data. + +**Findings** + +- [P1] Populated agent and business-chain states cannot be compared yet. + Location: right agent list and non-planning-role chain diagram. + Evidence: reference images show populated cards and nodes; the implementation has `0 个` agents and no available business chain in the unauthenticated preview. + Impact: card density, selected-card indicator, chain node flow, and agent-detail transition cannot be truthfully verified against live data. + Fix: verify the route in an authenticated session with at least one public chain and tagged agents. + +**Open Questions** + +- The final business-chain stage arrangement is still pending product confirmation. The compact graph maps whatever stage configuration is returned, while intentionally omitting dependency-label text as requested. + +**Implementation Checklist** + +1. Left task panel is now display-only and retains its independent position-roundtable task adapter. +2. Business-chain selection uses the original roundtable card/radio/relationship-entry pattern. +3. Every position uses the original `useStep1Intent` interface and `Step1Panel` message list/input panel; only the position header changes. +4. The right switch now uses the exact `课题研究 / ai写作` segmented-control treatment (light gray track, white active state; dark blue glass equivalent). +5. The embedded layout widens the left panel to 360px and applies the deep-blue right-panel tokens used by the reference. +6. Other roles render a taller top-to-bottom card-node flow without dependency labels. The canvas supports mouse-drag panning, and each node retains only a name plus a single-line description. + +**Follow-up Polish** + +- [P3] If live content is unusually long, tune the compact flow node width or horizontal scrolling after seeing the production chain data. + +**Comparison history** + +- Iteration 1: task fields were editable and the right TDesign card tabs looked unlike the reference. Replaced the task form with display-only information and replaced the tabs with the existing `课题研究 / ai写作` segmented switch. +- Iteration 2: business-chain selection was a dropdown and the flow was vertical avatar chips. Replaced it with original-roundtable selection cards and compact business-chain configuration-style node cards with link lines but no dependency labels. +- Iteration 3: dark right-panel background differed from the supplied deep-blue reference. Applied the `#032650` / `#042b59` / `#2b8ffe` token family and captured the revised embedded dark state in the full-view comparison. +- Iteration 4: removed the custom disabled input from non-planning positions and reused the original roundtable input panel. Enlarged the left flow viewport, retained its connection lines, and removed node level/seat/status metadata. +- Iteration 5: converted the compact flow from left-to-right to top-to-bottom. Added mouse-drag panning and single-line description truncation; populated-chain visual QA remains blocked by the unauthenticated preview. +- Iteration 6: constrained the flow canvas to its card viewport, narrowed nodes from 190px to 158px, and realigned branch gaps plus connector anchors to prevent clipping and offset lines. + +**Final result** + +blocked diff --git a/frontend-web/src/core/page-layout/sidebar-menu.ts b/frontend-web/src/core/page-layout/sidebar-menu.ts index 247bdb07..c52b2365 100644 --- a/frontend-web/src/core/page-layout/sidebar-menu.ts +++ b/frontend-web/src/core/page-layout/sidebar-menu.ts @@ -65,6 +65,12 @@ export const sidebarMenuItems: MenuItem[] = [ icon: Users, path: "/page/strategy/qa/multi-agent-roundtable", }, + { + id: "position-roundtable", + label: "岗位会商", + icon: Briefcase, + path: "/page/strategy/qa/position-roundtable", + }, { id: "dashboard-agent", label: "大屏绘制", diff --git a/frontend-web/src/pages/StrategyRoutes.tsx b/frontend-web/src/pages/StrategyRoutes.tsx index 1fd42a8e..31ee9214 100644 --- a/frontend-web/src/pages/StrategyRoutes.tsx +++ b/frontend-web/src/pages/StrategyRoutes.tsx @@ -21,6 +21,7 @@ import { ActionAnalyzes } from "@/strategy-components/components/task-components import KnowledgeBaseResourceManagement from "@/strategy-components/components/resource-management/KnowledgeBaseResourceManagement"; import NetworkActorResourceManagement from "@/strategy-components/components/resource-management/NetworkActorResourceManagement"; import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanningPage"; +import PositionRoundtablePage from "@/position-roundtable/pages/PositionRoundtablePage"; import DashboardAgentPage from "@/roundtable-planning/pages/DashboardAgentPage"; import BusinessChainListPage from "@/roundtable-planning/pages/BusinessChainListPage"; import BusinessChainEditorPage from "@/roundtable-planning/pages/BusinessChainEditorPage"; @@ -91,6 +92,14 @@ export default function StrategyRoutes() { } /> + + + + } + /> {/* 大屏绘制智能体独立页面(按 taskId 存取;ChatRuntime 提供 ArtifactsProvider 给沙箱预览) */} ; + return { + ...DEFAULT_POSITION_TASK, + ...data, + }; + } catch { + return DEFAULT_POSITION_TASK; + } +} + +/** + * 第一版先固定岗位,等岗位接口与权限范围确认后只替换常量数据源。 + * 组织筹划岗复用原会商 Step 1,承担意图识别。 + */ +const POSITIONS = [ + { + id: "planning", + name: "组织筹划岗", + description: "识别任务意图、补齐约束并选择业务链条", + }, + { + id: "intelligence", + name: "情报分析岗", + description: "围绕已确认意图开展情报分析", + }, + { + id: "solution", + name: "方案设计岗", + description: "承接上游结果并形成方案产物", + }, + { + id: "evaluation", + name: "综合评估岗", + description: "汇总链条结果并完成综合评估", + }, +] as const; + +type PositionId = (typeof POSITIONS)[number]["id"]; +type WorkspaceTab = "agents" | "results"; + +function uniqueChains(chains: BusinessChain[]): BusinessChain[] { + const seen = new Set(); + return chains.filter((chain) => { + if (seen.has(chain.id)) return false; + seen.add(chain.id); + return true; + }); +} + +function timeLabel(): string { + return new Intl.DateTimeFormat("zh-CN", { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + hour12: false, + }).format(new Date()); +} + +export default function PositionRoundtablePage() { + const { theme } = useTheme(); + const isDark = theme === "dark"; + const [positionId, setPositionId] = useState("planning"); + const task = useMemo(readPositionTaskDraft, []); + const activePosition = POSITIONS.find((item) => item.id === positionId)!; + + const { models } = useModels(); + const selectedModel = models[0]?.name ?? ""; + const triggerToast = useCallback((message: string) => toast.info(message), []); + const intent = useStep1Intent({ + // 全岗位共用原会商的对话接口与输入面板;岗位运行编排后续再按角色接入。 + currentStep: 1, + selectedModel, + triggerToast, + }); + + const { data: chains = [], isLoading: chainsLoading } = useQuery({ + queryKey: ["position-roundtable", "chains"], + queryFn: async () => { + const [mine, published] = await Promise.all([ + listChains("mine"), + listChains("public"), + ]); + return uniqueChains([...mine, ...published]); + }, + staleTime: 30_000, + }); + const [selectedChainId, setSelectedChainId] = useState(""); + useEffect(() => { + if (!selectedChainId && chains[0]) setSelectedChainId(chains[0].id); + }, [chains, selectedChainId]); + const selectedChain = + chains.find((chain) => chain.id === selectedChainId) ?? null; + + const [selectedAgentId, setSelectedAgentId] = useState(null); + const { agent: selectedAgent } = useAgent(selectedAgentId); + const [intentCompletedAt, setIntentCompletedAt] = useState(""); + useEffect(() => { + if (intent.intentReady && !intentCompletedAt) setIntentCompletedAt(timeLabel()); + if (!intent.intentReady && intentCompletedAt) setIntentCompletedAt(""); + }, [intent.intentReady, intentCompletedAt]); + + const handleSkipQuestion = useCallback(() => { + intent.appendLocalMessage({ + id: Date.now(), + sender: "agent", + senderName: "任务理解智能体", + content: "已跳过当前问题。你可以继续补充任务目标或约束。", + time: timeLabel(), + }); + }, [intent]); + + const handleFollowUp = useCallback(() => { + if (!intent.intentThreadId || intent.isIntentSending) return; + void intent.sendIntentMessage("请继续追问当前任务中尚未明确的关键条件。", []); + }, [intent]); + + return ( +
+
+
+ + + +
+

岗位多智能体会商

+

+ 按岗位承接任务、对话与分析产物 +

+
+
+ +
+ + 当前岗位 + + ({ + label: position.name, + value: position.id, + }))} + onChange={(value) => setPositionId(String(value) as PositionId)} + /> +
+
+ +
+ + +
+
+ {intent.intentInitError && ( +
+ + 对话初始化失败:{intent.intentInitError} + + + 重试 + +
+ )} + + +
+

+ {activePosition.name} · {positionId === "planning" ? "意图识别" : "岗位对话"} +

+

+ {activePosition.description} +

+
+
+ } + interactiveMessages={intent.interactiveMessages} + intentThreadId={intent.intentThreadId} + intentReady={intent.intentReady} + isIntentSending={intent.isIntentSending} + pendingIntentClarificationMsgId={ + intent.pendingIntentClarificationMsgId + } + intentClarificationSelections={ + intent.intentClarificationSelections + } + setIntentClarificationSelections={ + intent.setIntentClarificationSelections + } + onSendMessage={(text, files) => + void intent.sendIntentMessage(text, files) + } + onStopIntent={intent.stopIntentStream} + onSkipQuestion={handleSkipQuestion} + onFollowUp={handleFollowUp} + onEnterRoundtable={() => + toast.success("意图理解已确认,等待岗位链条执行逻辑接入") + } + onManualSave={() => undefined} + onTriggerToast={triggerToast} + submitIntentClarifications={intent.submitIntentClarifications} + /> +
+ + + +
+ + ); +} + +/** 仅展示岗位会商任务,不在会商页编辑任务。 */ +function TaskInformationCard({ task }: { task: PositionTaskDraft }) { + return ( +
+
+ + + +

+ 任务信息 +

+
+
+
+

+ 任务名称 +

+

+ {desensitize(task.title)} +

+
+
+

+ 任务描述 +

+

+ {desensitize(task.description)} +

+
+
+ + 任务方向 + + + {desensitize(task.direction)} + +
+ {task.id && ( +

+ #{task.id} +

+ )} +
+
+ ); +} + +function BusinessChainSelector({ + chains, + loading, + value, + onChange, +}: { + chains: BusinessChain[]; + loading: boolean; + value: string; + onChange: (value: string) => void; +}) { + const [expanded, setExpanded] = useState(true); + const [relationOpenId, setRelationOpenId] = useState(null); + + return ( +
+
+ + + +

+ 业务链 +

+ + {chains.length} + + +
+ + {expanded && ( +
+ {loading ? ( +
+ + 正在加载业务链条… +
+ ) : chains.length === 0 ? ( +

暂无可用业务链条

+ ) : ( + chains.map((chain) => { + const selected = chain.id === value; + const relationOpen = relationOpenId === chain.id; + const stageGroups = chainStages(chain); + let avatarOrder = 0; + return ( +
+ + + {selected && ( +
+ + {relationOpen && ( +
+ {stageGroups.flatMap((stage, stageIndex) => { + const content = stage.seats.map((seat) => { + const avatarType = getAvatarTypeFor( + seat.agent_id, + avatarOrder++, + ); + return ( + + {getAvatar(avatarType, "h-5 w-5")} + + ); + }); + return [ + stageIndex > 0 ? ( + + ) : null, + ...content, + ]; + })} +
+ )} +
+ )} +
+ ); + }) + )} +
+ )} +
+ ); +} + +function IntentUnderstandingCard({ + intent, +}: { + intent: ReturnType["intentReady"]; +}) { + return ( +
+

+ + + + 意图理解 +

+ {intent ? ( +
+

{desensitize(intent.objective)}

+
+ {intent.constraints.map((constraint, index) => ( +
+ + {desensitize(constraint)} +
+ ))} +
+
+ ) : ( +
+ 组织筹划岗完成意图识别后,其他岗位会在这里看到统一的任务意图与约束。 +
+ )} +
+ ); +} + +function chainStages(chain: BusinessChain) { + const seatById = new Map(chain.seats.map((seat) => [seat.agent_id, seat])); + if (chain.stages?.length) { + return chain.stages.map((ids, index) => ({ + goal: chain.stageGoals?.[index] ?? null, + seats: ids + .map((id) => seatById.get(id)) + .filter(Boolean) as BusinessChain["seats"], + })); + } + return chain.seats.map((seat) => ({ goal: null, seats: [seat] })); +} + +const FLOW_NODE_COLORS = ["#3b82f6", "#a855f7", "#10b981", "#f59e0b", "#ec4899"]; +const FLOW_NODE_WIDTH = 158; +const FLOW_NODE_GAP = 10; +const FLOW_NODE_CENTER = FLOW_NODE_WIDTH / 2; + +/** 以业务链配置页的节点卡片语义绘制窄栏版,不显示连线依赖说明。 */ +function BusinessChainFlow({ chain }: { chain: BusinessChain | null }) { + const stages = chain ? chainStages(chain) : []; + const [view, setView] = useState({ tx: 0, ty: 0 }); + const [panning, setPanning] = useState(false); + const dragRef = useRef<{ + startX: number; + startY: number; + tx: number; + ty: number; + } | null>(null); + let avatarOrder = 0; + + const onPanStart = (event: React.PointerEvent) => { + if (event.button !== 0) return; + dragRef.current = { + startX: event.clientX, + startY: event.clientY, + tx: view.tx, + ty: view.ty, + }; + setPanning(true); + event.currentTarget.setPointerCapture(event.pointerId); + }; + + const onPanMove = (event: React.PointerEvent) => { + const drag = dragRef.current; + if (!drag) return; + setView({ + tx: drag.tx + event.clientX - drag.startX, + ty: drag.ty + event.clientY - drag.startY, + }); + }; + + const onPanEnd = (event: React.PointerEvent) => { + if (!dragRef.current) return; + dragRef.current = null; + setPanning(false); + try { + event.currentTarget.releasePointerCapture(event.pointerId); + } catch { + // Pointer capture may already be released by the browser. + } + }; + + return ( +
+
+ + + +

+ 业务链条 +

+
+ {!chain ? ( +
+ 请先由组织筹划岗选择业务链条 +
+ ) : ( +
setView({ tx: 0, ty: 0 })} + className={cn( + "h-[456px] overflow-hidden rounded-xl border border-slate-100 bg-slate-50/70 touch-none dark:border-[#2b8ffe]/25 dark:bg-[#032650]/75", + panning ? "cursor-grabbing" : "cursor-grab", + )} + style={{ + backgroundImage: + "radial-gradient(rgba(148,163,184,.55) 1px, transparent 1px)", + backgroundSize: "16px 16px", + }} + title="按住鼠标拖动画布;双击复位" + > +
+ + {stages.map((stage, stageIndex) => { + const stageWidth = Math.max( + FLOW_NODE_WIDTH, + stage.seats.length * (FLOW_NODE_WIDTH + FLOW_NODE_GAP) - FLOW_NODE_GAP, + ); + return ( +
+ +
+ {stage.seats.map((seat, seatIndex) => { + const order = avatarOrder++; + return ( + + ); + })} +
+
+ ); + })} +
+
+ )} +
+ ); +} + +function VerticalFlowConnector({ + seatCount, + width, +}: { + seatCount: number; + width: number; +}) { + if (seatCount <= 1) { + return ; + } + + return ( +
+ + + {Array.from({ length: seatCount }).map((_, index) => ( + + ))} +
+ ); +} + +function CompactChainNode({ + title, + description, + accent, + avatarType, +}: { + title: string; + description: string; + accent: string; + avatarType: string; +}) { + return ( +
+
+ + {getAvatar(avatarType, "h-7 w-7")} + +
+

{title}

+

+ {description} +

+
+
+
+ ); +} + +function RightWorkspace({ + selectedAgent, + selectedAgentId, + onSelectAgent, + onBackToAgents, + intentObjective, + intentCompletedAt, +}: { + selectedAgent: Agent | null; + selectedAgentId: string | null; + onSelectAgent: (agentId: string) => void; + onBackToAgents: () => void; + intentObjective?: string; + intentCompletedAt: string; +}) { + const [tab, setTab] = useState("agents"); + + return ( +
+ {/* 与课题研究 / ai写作一致的分段切换样式。 */} +
+
+ + +
+
+ + {tab === "agents" ? ( +
+
+ {selectedAgentId ? ( +
+
+ } + onClick={onBackToAgents} + > + 返回列表 + + + {selectedAgent ? desensitize(selectedAgent.name) : "加载中…"} + +
+
+ toast.info("请在中间对话区发起岗位会商")} + showRecentChats={false} + /> +
+
+ ) : ( + + )} +
+
+ ) : ( +
+ +
+ )} +
+ ); +} + +/** 复用 ai 写作页的智能体分组数据和同款分组卡片视觉。 */ +function PositionAgentList({ onSelect }: { onSelect: (agentId: string) => void }) { + const { theme } = useTheme(); + const isDark = theme === "dark"; + const { groups, isLoading } = useAgentModuleSidebarData("position-roundtable"); + const [query, setQuery] = useState(""); + const [collapsed, setCollapsed] = useState>(() => new Set()); + + const visibleGroups = useMemo(() => { + const keyword = query.trim().toLowerCase(); + if (!keyword) return groups; + return groups + .map((group) => ({ + ...group, + agents: group.agents.filter((agent) => + `${agent.name} ${agent.description ?? ""}`.toLowerCase().includes(keyword), + ), + })) + .filter((group) => group.agents.length > 0); + }, [groups, query]); + + const toggleGroup = (key: string) => { + setCollapsed((current) => { + const next = new Set(current); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }; + + return ( +
+
+
+
+

智能体类型

+

+ 选择智能体后查看配置 +

+
+ + {groups.reduce((total, group) => total + group.agents.length, 0)} 个 + +
+ } + onChange={setQuery} + /> +
+ +
+ {isLoading && groups.length === 0 ? ( +
+ + 正在加载智能体… +
+ ) : visibleGroups.length === 0 ? ( +
+ 没有找到匹配的智能体 +
+ ) : ( +
+ {visibleGroups.map((group) => { + const isCollapsed = collapsed.has(group.key); + return ( +
+ + {!isCollapsed && ( +
+ {group.agents.map((agent) => { + const { from, to } = agentColor(agent.id, isDark); + return ( + + ); + })} +
+ )} +
+ ); + })} +
+ )} +
+
+ ); +} + +function AnalysisResults({ + intentObjective, + intentCompletedAt, +}: { + intentObjective?: string; + intentCompletedAt: string; +}) { + if (!intentObjective) { + return ( +
+ +

暂无分析结果

+

+ 组织筹划岗完成意图识别后,产物会先进入这里;后续岗位产物沿用同一张卡片追加入列。 +

+
+ ); + } + + return ( +
+
+
+

分析结果列表

+

按生成时间倒序

+
+ + 1 个产物 + +
+
+
+ + + +
+

任务理解智能体

+

任务意图理解

+

+ {desensitize(intentObjective)} +

+
+
+ +
+
+ ); +}