diff --git a/frontend-web/src/position-roundtable/lib/mock-task-source.ts b/frontend-web/src/position-roundtable/lib/mock-task-source.ts index de6fb023..5e53c612 100644 --- a/frontend-web/src/position-roundtable/lib/mock-task-source.ts +++ b/frontend-web/src/position-roundtable/lib/mock-task-source.ts @@ -17,26 +17,10 @@ export interface PositionTask { export const MOCK_POSITION_TASKS: readonly PositionTask[] = [ { id: "mock-position-task-001", - title: "重点区域态势研判与协同处置方案", + title: "暑期活动", description: - "围绕重点区域近期态势,梳理风险信号、影响范围与处置约束,形成可执行的协同方案及评估建议。", - direction: "态势研判", - source: "mock", - }, - { - id: "mock-position-task-002", - title: "重大活动保障资源统筹任务", - description: - "针对重大活动保障场景,识别关键保障目标、现有资源和跨部门协同难点,输出分阶段资源统筹方案。", - direction: "组织筹划", - source: "mock", - }, - { - id: "mock-position-task-003", - title: "专项行动效果复盘与优化建议", - description: - "汇总专项行动过程信息与阶段产物,研判实际效果、剩余风险和优化路径,形成后续行动建议。", - direction: "综合评估", + "围绕暑期活动的游泳夏令营做宣传", + direction: "暑期活动", source: "mock", }, ] as const; diff --git a/frontend-web/src/position-roundtable/lib/positions.ts b/frontend-web/src/position-roundtable/lib/positions.ts index 34c0582f..68f5feca 100644 --- a/frontend-web/src/position-roundtable/lib/positions.ts +++ b/frontend-web/src/position-roundtable/lib/positions.ts @@ -7,14 +7,14 @@ export const POSITION_OPTIONS = [ { id: "intelligence", - name: "情报分析岗", + name: "测试分析岗", description: "识别任务意图、补齐约束并确认业务链条", isIntentPosition: true, }, { id: "planning", - name: "组织筹划岗", - description: "承接上游信息并进行组织筹划", + name: "组织测试岗", + description: "承接上游信息并进行组织测试", isIntentPosition: false, }, { @@ -25,8 +25,8 @@ export const POSITION_OPTIONS = [ }, { id: "evaluation", - name: "综合评估岗", - description: "评估链条产物并提出综合建议", + name: "综合测试岗", + description: "测试链条产物并提出综合建议", isIntentPosition: false, }, ] as const; diff --git a/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx b/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx index 4e5488e4..2f494a38 100644 --- a/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx +++ b/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx @@ -390,6 +390,7 @@ export default function PositionRoundtablePage({ const [rightTab, setRightTab] = useState("agents"); const artifactsCtx = useArtifactsOptional(); const [sandboxArtifact, setSandboxArtifact] = useState(null); + const [flowDetailItem, setFlowDetailItem] = useState(null); const activePosition = POSITIONS.find((item) => item.id === positionId)!; const frozenIntent = useMemo( () => parseIntentSummary(positionSession?.intentSnapshot), @@ -828,6 +829,59 @@ export default function PositionRoundtablePage({ }, [artifactsCtx], ); + const handlePreviewFlowArtifact = useCallback( + (item: ResultItem) => { + const threadId = item.artifactThreadId || item.node?.threadId; + if (!threadId || !item.artifactPath) { + toast.info("该节点暂无可预览文件"); + return; + } + handlePreviewArtifact({ + threadId, + path: item.artifactPath, + name: item.productName, + mimeType: item.artifactMimeType ?? null, + }); + }, + [handlePreviewArtifact], + ); + const openIntentFlowDetail = useCallback(() => { + setFlowDetailItem( + buildIntentResultItem( + frozenIntent ?? intent.intentReady, + intentCompletedAt || positionSession?.updatedAt, + ), + ); + }, [frozenIntent, intent.intentReady, intentCompletedAt, positionSession?.updatedAt]); + const openNodeFlowDetail = useCallback( + (node: PositionRoundtableNode) => { + setActiveNodeKey(node.nodeKey); + setSelectedAgentId(node.agentId); + setFlowDetailItem(buildNodeResultItem(node, activeChain)); + }, + [activeChain], + ); + const openSeatFlowDetail = useCallback( + (seat: BusinessChain["seats"][number], stageIndex: number, seatIndex: number) => { + setFlowDetailItem(buildSeatPendingResultItem(seat, stageIndex, seatIndex)); + }, + [], + ); + const openBuiltinFlowDetail = useCallback( + (kind: "summary" | "action_plan") => { + setFlowDetailItem( + buildBuiltinResultItem( + kind, + positionSession, + allExecutableNodesDone(positionSession?.nodes ?? []), + ), + ); + }, + [positionSession], + ); + const handleStartedIntentSend = useCallback(() => { + toast.info("业务链已启动,任务意图已冻结;如需重新识别请新建会话"); + }, []); const sandboxOpen = Boolean( sandboxArtifact && artifactsCtx?.open && artifactsCtx.selectedArtifact, ); @@ -835,6 +889,75 @@ export default function PositionRoundtablePage({ artifactsCtx?.setOpen(false); setSandboxArtifact(null); }, [artifactsCtx]); + const renderIntentChatPanel = (chainStarted: boolean) => ( + <> + {intent.intentInitError && ( +
+ 对话初始化失败:{intent.intentInitError} + + 重试 + +
+ )} + + +
+

+ {activePosition.name} · 意图识别 +

+

+ {activePosition.description} +

+
+ + } + headerRightSlot={ + chainStarted ? ( + + 业务链已启动 + + ) : undefined + } + interactiveMessages={intent.interactiveMessages} + intentThreadId={intent.intentThreadId} + intentReady={chainStarted ? frozenIntent ?? intent.intentReady : intent.intentReady} + isIntentSending={intent.isIntentSending} + pendingIntentClarificationMsgId={intent.pendingIntentClarificationMsgId} + intentClarificationSelections={intent.intentClarificationSelections} + setIntentClarificationSelections={intent.setIntentClarificationSelections} + onSendMessage={(text, files) => { + if (chainStarted) { + handleStartedIntentSend(); + return; + } + setRightPanelOpen(false); + void intent.sendIntentMessage(text, files); + }} + onStopIntent={intent.stopIntentStream} + onSkipQuestion={chainStarted ? handleStartedIntentSend : handleSkipQuestion} + onFollowUp={chainStarted ? handleStartedIntentSend : handleFollowUp} + onEnterRoundtable={chainStarted ? handleStartedIntentSend : () => void handleActivateChain()} + enterButtonLabel={ + chainStarted + ? "业务链已启动" + : startingChain + ? "正在启动业务链…" + : "确认意图并启动业务链 >" + } + onManualSave={() => undefined} + onTriggerToast={triggerToast} + submitIntentClarifications={intent.submitIntentClarifications} + /> + {(startingChain || sessionLoading) && ( +
+ + {startingChain ? "正在保存意图并生成岗位节点…" : "正在恢复岗位协同会话…"} +
+ )} + + ); return (
)} -
- {isIntelligenceDeliveryOverview ? ( - - ) : activePosition.isIntentPosition ? ( - positionSession?.status !== "intent_pending" && frozenIntent ? ( - - ) : ( - <> - {intent.intentInitError && ( -
- 对话初始化失败:{intent.intentInitError} - - 重试 - -
- )} - - -
-

- {activePosition.name} · 意图识别 -

-

{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) => { - setRightPanelOpen(false); - void intent.sendIntentMessage(text, files); - }} - onStopIntent={intent.stopIntentStream} - onSkipQuestion={handleSkipQuestion} - onFollowUp={handleFollowUp} - onEnterRoundtable={() => void handleActivateChain()} - enterButtonLabel={startingChain ? "正在启动业务链…" : "确认意图并启动业务链 >"} - onManualSave={() => undefined} - onTriggerToast={triggerToast} - submitIntentClarifications={intent.submitIntentClarifications} - /> - {(startingChain || sessionLoading) && ( -
- - {startingChain ? "正在保存意图并生成岗位节点…" : "正在恢复岗位协同会话…"} -
- )} - - ) +
+ {activePosition.isIntentPosition ? ( + renderIntentChatPanel(isIntelligenceDeliveryOverview) ) : positionSession && activeNode ? ( { - setActiveNodeKey(node.nodeKey); - setSelectedAgentId(node.agentId); - }} - onSelectBuiltin={() => { - toast.info("内置智能体结果可在交付产物卡片中查看"); - }} + onSelectIntent={openIntentFlowDetail} + onSelectNode={openNodeFlowDetail} + onSelectSeat={openSeatFlowDetail} + onSelectBuiltin={openBuiltinFlowDetail} className="h-full w-full" /> @@ -1177,6 +1238,16 @@ export default function PositionRoundtablePage({ /> )} + { + if (!open) setFlowDetailItem(null); + }} + onReject={() => undefined} + onPreviewArtifact={handlePreviewFlowArtifact} + showReject={false} + showBuiltinAsk={false} + />
); @@ -1843,7 +1914,9 @@ function BusinessChainFlow({ nodes, session, activeNodeKey, + onSelectIntent, onSelectNode, + onSelectSeat, onSelectBuiltin, className, }: { @@ -1851,7 +1924,9 @@ function BusinessChainFlow({ nodes: PositionRoundtableNode[]; session: PositionRoundtableSession | null; activeNodeKey: string | null; + onSelectIntent?: () => void; onSelectNode: (node: PositionRoundtableNode) => void; + onSelectSeat?: (seat: BusinessChain["seats"][number], stageIndex: number, seatIndex: number) => void; onSelectBuiltin: (kind: "summary" | "action_plan") => void; className?: string; }) { @@ -1896,7 +1971,9 @@ function BusinessChainFlow({ nodes={nodes} session={session} activeNodeKey={activeNodeKey} + onSelectIntent={onSelectIntent} onSelectNode={onSelectNode} + onSelectSeat={onSelectSeat} onSelectBuiltin={onSelectBuiltin} className="min-h-0 flex-1" /> @@ -1914,10 +1991,22 @@ function BusinessChainFlow({ nodes={nodes} session={session} activeNodeKey={activeNodeKey} + onSelectIntent={ + onSelectIntent + ? () => { + onSelectIntent(); + setFlowDialogOpen(false); + } + : undefined + } onSelectNode={(node) => { onSelectNode(node); setFlowDialogOpen(false); }} + onSelectSeat={(seat, stageIndex, seatIndex) => { + onSelectSeat?.(seat, stageIndex, seatIndex); + setFlowDialogOpen(false); + }} onSelectBuiltin={(kind) => { onSelectBuiltin(kind); setFlowDialogOpen(false); @@ -1939,7 +2028,9 @@ function ChainFlowCanvas({ nodes, session, activeNodeKey, + onSelectIntent, onSelectNode, + onSelectSeat, onSelectBuiltin, className, expanded = false, @@ -1948,12 +2039,15 @@ function ChainFlowCanvas({ nodes: PositionRoundtableNode[]; session: PositionRoundtableSession | null; activeNodeKey: string | null; + onSelectIntent?: () => void; onSelectNode: (node: PositionRoundtableNode) => void; + onSelectSeat?: (seat: BusinessChain["seats"][number], stageIndex: number, seatIndex: number) => void; onSelectBuiltin: (kind: "summary" | "action_plan") => void; className?: string; expanded?: boolean; }) { const stages = chainStages(chain); + const { theme } = useTheme(); const [view, setView] = useState({ tx: 0, ty: 0 }); const [panning, setPanning] = useState(false); const dragRef = useRef<{ @@ -1970,6 +2064,8 @@ function ChainFlowCanvas({ session?.actionPlanSnapshot, snapshotStatus(session?.summarySnapshot) === "done", ); + const connectorColor = + theme === "dark" ? "rgba(143, 196, 255, 0.95)" : "rgba(148, 163, 184, 0.88)"; const onPanStart = (event: React.PointerEvent) => { if (event.button !== 0) return; @@ -2031,6 +2127,7 @@ function ChainFlowCanvas({ accent="#3b82f6" nodeStatus={nodes.length > 0 ? "done" : "ready"} width={nodeWidth} + onClick={onSelectIntent} /> {stages.map((stage, stageIndex) => { const stageWidth = Math.max( @@ -2047,6 +2144,7 @@ function ChainFlowCanvas({ width={stageWidth} nodeWidth={nodeWidth} nodeGap={nodeGap} + connectorColor={connectorColor} />
{stage.seats.map((seat, seatIndex) => { @@ -2068,7 +2166,13 @@ function ChainFlowCanvas({ } selected={node?.nodeKey === activeNodeKey} nodeStatus={node?.status} - onClick={node ? () => onSelectNode(node) : undefined} + onClick={ + node + ? () => onSelectNode(node) + : onSelectSeat + ? () => onSelectSeat(seat, stageIndex, seatIndex) + : undefined + } width={nodeWidth} /> ); @@ -2077,7 +2181,13 @@ function ChainFlowCanvas({
); })} - + onSelectBuiltin("summary")} /> - + - + + ); } return ( -
- +
+ {Array.from({ length: seatCount }).map((_, index) => ( - + ))}
@@ -2179,7 +2316,7 @@ function CompactChainNode({ if (onClick) event.stopPropagation(); }} className={cn( - "relative overflow-hidden rounded-lg border border-slate-200 bg-white p-3 pr-9 shadow-sm dark:border-[#2b8ffe]/40 dark:bg-[#0a2d58]", + "relative overflow-hidden rounded-lg border border-slate-200 bg-white p-3 pr-10 shadow-sm dark:border-[#2b8ffe]/40 dark:bg-[#0a2d58]", onClick && "cursor-pointer transition hover:-translate-y-0.5 hover:shadow-md", selected && "z-10 -translate-y-px bg-slate-50 dark:bg-[#0d3768]", nodeStatus === "locked" && "opacity-65", @@ -2532,6 +2669,132 @@ interface ResultItem { artifactThreadId?: string | null; } +function artifactPath(artifact: Record | null | undefined): string | undefined { + const path = recordString(artifact, "path"); + return path || undefined; +} + +function artifactMimeType(artifact: Record | null | undefined): string | null { + return recordString(artifact, "mime_type") || recordString(artifact, "mimeType") || null; +} + +function buildIntentResultItem( + intent: IntentSummary | null, + updatedAt: string | undefined, +): ResultItem { + return { + id: "flow:intent", + kind: "intent", + node: null, + agentName: "情报分析智能体", + productName: "任务意图理解", + summary: intent?.objective || "任务意图已确认,当前没有更多意图摘要内容。", + positionId: DEFAULT_POSITION_ID, + updatedAt, + status: intent ? "done" : "ready", + }; +} + +function buildSeatPendingResultItem( + seat: BusinessChain["seats"][number], + stageIndex: number, + seatIndex: number, +): ResultItem { + const agentName = seat.name?.trim() || seat.agent_id; + return { + id: `flow:seat:${stageIndex}:${seatIndex}:${seat.agent_id}`, + kind: "delivery", + node: null, + agentName, + productName: `待交付:${agentName} Markdown报告`, + summary: "该席位已按业务链配置排入流程,尚未生成可查看的问答结果或交付产物。", + positionId: seat.position_id ?? null, + updatedAt: undefined, + status: "ready", + }; +} + +function buildNodeResultItem( + node: PositionRoundtableNode, + chain: BusinessChain | null, +): ResultItem { + const seat = chain?.seats.find((item) => item.agent_id === node.agentId); + const agentName = seat?.name?.trim() || node.agentId; + const artifact = node.artifactManifest.find(artifactIsMarkdown) ?? node.artifactManifest[0] ?? null; + const productName = + artifactName(artifact) || + (node.status === "done" ? `${agentName}报告` : `待交付:${agentName} Markdown报告`); + const summary = + node.latestAnswer?.trim() || + (node.status === "locked" + ? node.invalidatedBy + ? "上游交付已被驳回或重做,等待前序节点重新完成。" + : "等待上游阶段完成后解锁。" + : node.status === "running" + ? "智能体正在问答并生成 Markdown 报告。" + : node.status === "stale" + ? "上游产物已更新,该席位需要基于最新结果重做。" + : node.status === "rejected" + ? "产物已被驳回,需要重新问答并交付。" + : "该席位尚未交付 Markdown 报告。"); + return { + id: `flow:node:${node.nodeKey}:${node.revision}`, + kind: "delivery", + node, + agentName, + productName, + summary, + positionId: node.positionId, + updatedAt: node.updatedAt, + status: node.status, + artifactPath: artifactPath(artifact), + artifactMimeType: artifactMimeType(artifact), + artifactThreadId: node.threadId, + }; +} + +function buildBuiltinResultItem( + kind: "summary" | "action_plan", + session: PositionRoundtableSession | null, + allDelivered: boolean, +): ResultItem { + const isSummary = kind === "summary"; + const snapshot = isSummary ? session?.summarySnapshot : session?.actionPlanSnapshot; + const artifact = snapshotArtifact(snapshot, "primary_artifact"); + const ready = isSummary + ? allDelivered + : snapshotStatus(session?.summarySnapshot) === "done"; + const status = builtinNodeStatus(snapshot, ready); + const fallbackSummary = + status === "locked" + ? isSummary + ? "所有岗位 Markdown 报告交付后,可启动方案总结。" + : "方案总结完成后,可继续拆解行动规划与子任务。" + : isSummary + ? "等待基于各岗位交付产物生成最终方案总结。" + : "等待结合方案总结与各岗位产物生成行动规划。"; + return { + id: isSummary ? "flow:builtin:summary" : "flow:builtin:action-plan", + kind, + node: null, + agentName: isSummary ? "方案总结智能体" : "行动规划智能体", + productName: + artifactName(artifact) || + snapshotText(snapshot, "title") || + (isSummary ? "待生成:岗位会商方案总结报告" : "待生成:岗位会商行动规划"), + summary: snapshotText(snapshot, "content") || fallbackSummary, + positionId: isSummary ? "summary" : "action_plan", + updatedAt: snapshotText(snapshot, "generated_at") || session?.updatedAt, + status, + artifactPath: artifactPath(artifact), + artifactMimeType: artifactMimeType(artifact), + artifactThreadId: + snapshotText(snapshot, "thread_id") || + (isSummary ? session?.summaryThreadId : session?.actionPlanThreadId) || + null, + }; +} + function AnalysisResults({ intentObjective, intentCompletedAt, @@ -3143,20 +3406,26 @@ function DeliverableDetailDialog({ onPreviewArtifact, onAskBuiltin, askSubmitting, + showReject = true, + showBuiltinAsk = true, }: { item: ResultItem | null; onOpenChange: (open: boolean) => void; onReject: (node: PositionRoundtableNode) => void; onPreviewArtifact: (item: ResultItem) => void; - onAskBuiltin: (kind: "summary" | "action_plan", message: string) => Promise; - askSubmitting: boolean; + onAskBuiltin?: (kind: "summary" | "action_plan", message: string) => Promise; + askSubmitting?: boolean; + showReject?: boolean; + showBuiltinAsk?: boolean; }) { const [followUp, setFollowUp] = useState(""); const status = item?.status ? nodeStatusMeta(item.status) : null; const StatusIcon = status?.Icon; - const canReject = item?.node ? canRejectNodeDelivery(item.node) : false; + const canReject = showReject && item?.node ? canRejectNodeDelivery(item.node) : false; const builtinKind = - item?.kind === "summary" || item?.kind === "action_plan" ? item.kind : null; + showBuiltinAsk && onAskBuiltin && (item?.kind === "summary" || item?.kind === "action_plan") + ? item.kind + : null; const rejectionTitle = item?.node ? nodeRejectionTitle(item.node) : ""; const rejectionReason = item?.node ? nodeRejectionReason(item.node) : ""; const displayedTime = item?.updatedAt @@ -3267,10 +3536,10 @@ function DeliverableDetailDialog({ { - void onAskBuiltin(builtinKind, followUp).then((saved) => { + void onAskBuiltin?.(builtinKind, followUp).then((saved) => { if (saved) setFollowUp(""); }); }} @@ -3283,7 +3552,7 @@ function DeliverableDetailDialog({
)} - {item?.node && ( + {showReject && item?.node && (