feat: refine position roundtable workflow
This commit is contained in:
1 parent
ae184f434d
commit
6ade3b69b9
3 files changed
+366
-113
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -390,6 +390,7 @@ export default function PositionRoundtablePage({
|
||||
const [rightTab, setRightTab] = useState<WorkspaceTab>("agents");
|
||||
const artifactsCtx = useArtifactsOptional();
|
||||
const [sandboxArtifact, setSandboxArtifact] = useState<PositionArtifactPreview | null>(null);
|
||||
const [flowDetailItem, setFlowDetailItem] = useState<ResultItem | null>(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 && (
|
||||
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-amber-200 bg-amber-50 px-4 py-2 text-xs text-amber-800 dark:border-amber-900/60 dark:bg-amber-950/30 dark:text-amber-200">
|
||||
<span className="line-clamp-2">对话初始化失败:{intent.intentInitError}</span>
|
||||
<TButton size="small" variant="outline" onClick={intent.runStep1Init}>
|
||||
重试
|
||||
</TButton>
|
||||
</div>
|
||||
)}
|
||||
<Step1Panel
|
||||
headerLeftSlot={
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<MessageSquareText className="size-4 text-primary" />
|
||||
<div className="min-w-0">
|
||||
<h2 className="truncate text-sm font-extrabold text-slate-900 dark:text-foreground">
|
||||
{activePosition.name} · 意图识别
|
||||
</h2>
|
||||
<p className="truncate text-[11px] text-muted-foreground">
|
||||
{activePosition.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
headerRightSlot={
|
||||
chainStarted ? (
|
||||
<span className="rounded-full border border-emerald-500/20 bg-emerald-500/10 px-2 py-0.5 text-[10px] font-semibold text-emerald-700 dark:text-emerald-300">
|
||||
业务链已启动
|
||||
</span>
|
||||
) : 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) && (
|
||||
<div className="flex shrink-0 items-center gap-2 border-t border-slate-100 px-4 py-2 text-xs text-muted-foreground dark:border-[#2b8ffe]/20 dark:text-[#8aa0c4]">
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
{startingChain ? "正在保存意图并生成岗位节点…" : "正在恢复岗位协同会话…"}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -975,68 +1098,9 @@ export default function PositionRoundtablePage({
|
||||
title="打开右侧配置"
|
||||
/>
|
||||
)}
|
||||
<div className="flex min-h-0 flex-1 flex-col [&_#save-draft-btn]:hidden">
|
||||
{isIntelligenceDeliveryOverview ? (
|
||||
<PositionDeliveryOverview
|
||||
intent={frozenIntent ?? intent.intentReady}
|
||||
intentCompletedAt={intentCompletedAt || positionSession?.updatedAt}
|
||||
nodes={positionSession?.nodes ?? []}
|
||||
chain={activeChain}
|
||||
session={positionSession}
|
||||
/>
|
||||
) : activePosition.isIntentPosition ? (
|
||||
positionSession?.status !== "intent_pending" && frozenIntent ? (
|
||||
<IntentConfirmedState intent={frozenIntent} />
|
||||
) : (
|
||||
<>
|
||||
{intent.intentInitError && (
|
||||
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-amber-200 bg-amber-50 px-4 py-2 text-xs text-amber-800 dark:border-amber-900/60 dark:bg-amber-950/30 dark:text-amber-200">
|
||||
<span className="line-clamp-2">对话初始化失败:{intent.intentInitError}</span>
|
||||
<TButton size="small" variant="outline" onClick={intent.runStep1Init}>
|
||||
重试
|
||||
</TButton>
|
||||
</div>
|
||||
)}
|
||||
<Step1Panel
|
||||
headerLeftSlot={
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<MessageSquareText className="size-4 text-primary" />
|
||||
<div className="min-w-0">
|
||||
<h2 className="truncate text-sm font-extrabold text-slate-900 dark:text-foreground">
|
||||
{activePosition.name} · 意图识别
|
||||
</h2>
|
||||
<p className="truncate text-[11px] text-muted-foreground">{activePosition.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
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) && (
|
||||
<div className="flex shrink-0 items-center gap-2 border-t border-slate-100 px-4 py-2 text-xs text-muted-foreground dark:border-[#2b8ffe]/20 dark:text-[#8aa0c4]">
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
{startingChain ? "正在保存意图并生成岗位节点…" : "正在恢复岗位协同会话…"}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
<div className="flex min-h-0 flex-1 flex-col [&_#save-draft-btn]:hidden [&_#skip-question-btn]:hidden [&_#suggest-followup-btn]:hidden">
|
||||
{activePosition.isIntentPosition ? (
|
||||
renderIntentChatPanel(isIntelligenceDeliveryOverview)
|
||||
) : positionSession && activeNode ? (
|
||||
<PositionNodeChat
|
||||
key={activeNode.nodeKey}
|
||||
@@ -1067,13 +1131,10 @@ export default function PositionRoundtablePage({
|
||||
nodes={positionSession?.nodes ?? []}
|
||||
session={positionSession}
|
||||
activeNodeKey={activeNodeKey}
|
||||
onSelectNode={(node) => {
|
||||
setActiveNodeKey(node.nodeKey);
|
||||
setSelectedAgentId(node.agentId);
|
||||
}}
|
||||
onSelectBuiltin={() => {
|
||||
toast.info("内置智能体结果可在交付产物卡片中查看");
|
||||
}}
|
||||
onSelectIntent={openIntentFlowDetail}
|
||||
onSelectNode={openNodeFlowDetail}
|
||||
onSelectSeat={openSeatFlowDetail}
|
||||
onSelectBuiltin={openBuiltinFlowDetail}
|
||||
className="h-full w-full"
|
||||
/>
|
||||
</aside>
|
||||
@@ -1177,6 +1238,16 @@ export default function PositionRoundtablePage({
|
||||
/>
|
||||
</aside>
|
||||
)}
|
||||
<DeliverableDetailDialog
|
||||
item={flowDetailItem}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setFlowDetailItem(null);
|
||||
}}
|
||||
onReject={() => undefined}
|
||||
onPreviewArtifact={handlePreviewFlowArtifact}
|
||||
showReject={false}
|
||||
showBuiltinAsk={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -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<HTMLDivElement>) => {
|
||||
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}
|
||||
/>
|
||||
<div className="flex items-start justify-center" style={{ gap: nodeGap }}>
|
||||
{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({
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<VerticalFlowConnector seatCount={1} width={nodeWidth} nodeWidth={nodeWidth} nodeGap={nodeGap} />
|
||||
<VerticalFlowConnector
|
||||
seatCount={1}
|
||||
width={nodeWidth}
|
||||
nodeWidth={nodeWidth}
|
||||
nodeGap={nodeGap}
|
||||
connectorColor={connectorColor}
|
||||
/>
|
||||
<CompactChainNode
|
||||
title="方案总结智能体"
|
||||
description="内置总结节点"
|
||||
@@ -2087,7 +2197,13 @@ function ChainFlowCanvas({
|
||||
titleAttr="对所有有效交付产物进行方案总结"
|
||||
onClick={() => onSelectBuiltin("summary")}
|
||||
/>
|
||||
<VerticalFlowConnector seatCount={1} width={nodeWidth} nodeWidth={nodeWidth} nodeGap={nodeGap} />
|
||||
<VerticalFlowConnector
|
||||
seatCount={1}
|
||||
width={nodeWidth}
|
||||
nodeWidth={nodeWidth}
|
||||
nodeGap={nodeGap}
|
||||
connectorColor={connectorColor}
|
||||
/>
|
||||
<CompactChainNode
|
||||
title="行动规划智能体"
|
||||
description="内置规划节点"
|
||||
@@ -2107,37 +2223,58 @@ function VerticalFlowConnector({
|
||||
width,
|
||||
nodeWidth = FLOW_NODE_WIDTH,
|
||||
nodeGap = FLOW_NODE_GAP,
|
||||
connectorColor,
|
||||
}: {
|
||||
seatCount: number;
|
||||
width: number;
|
||||
nodeWidth?: number;
|
||||
nodeGap?: number;
|
||||
connectorColor: string;
|
||||
}) {
|
||||
const nodeCenter = nodeWidth / 2;
|
||||
const lineStyle = {
|
||||
backgroundColor: connectorColor,
|
||||
boxShadow: "0 0 8px rgba(43, 143, 254, 0.32)",
|
||||
};
|
||||
const arrowStyle = {
|
||||
borderBottomColor: connectorColor,
|
||||
borderRightColor: connectorColor,
|
||||
filter: "drop-shadow(0 0 3px rgba(43, 143, 254, 0.36))",
|
||||
};
|
||||
if (seatCount <= 1) {
|
||||
return (
|
||||
<span className="relative h-7 w-px bg-slate-300 dark:bg-[#2b8ffe]/55">
|
||||
<span className="absolute -bottom-0.5 left-1/2 size-1.5 -translate-x-1/2 rotate-45 border-b border-r border-slate-300 dark:border-[#2b8ffe]/55" />
|
||||
<span className="relative mb-1 h-7 w-0.5 rounded-full" style={lineStyle}>
|
||||
<span
|
||||
className="absolute bottom-1 left-1/2 size-2 -translate-x-1/2 rotate-45 border-b-2 border-r-2 border-transparent"
|
||||
style={arrowStyle}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-8" style={{ width }}>
|
||||
<span className="absolute left-1/2 top-0 h-3 w-px -translate-x-1/2 bg-slate-300 dark:bg-[#2b8ffe]/55" />
|
||||
<div className="relative mb-1 h-8" style={{ width }}>
|
||||
<span
|
||||
className="absolute top-3 h-px bg-slate-300 dark:bg-[#2b8ffe]/55"
|
||||
style={{ left: nodeCenter, right: nodeCenter }}
|
||||
className="absolute left-1/2 top-0 h-3.5 w-0.5 -translate-x-1/2 rounded-full"
|
||||
style={lineStyle}
|
||||
/>
|
||||
<span
|
||||
className="absolute top-3.5 h-0.5 rounded-full"
|
||||
style={{ left: nodeCenter, right: nodeCenter, ...lineStyle }}
|
||||
/>
|
||||
{Array.from({ length: seatCount }).map((_, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="absolute top-3 bottom-0 w-px -translate-x-1/2 bg-slate-300 dark:bg-[#2b8ffe]/55"
|
||||
className="absolute top-3.5 bottom-0 w-0.5 -translate-x-1/2 rounded-full"
|
||||
style={{
|
||||
left: `${((index * (nodeWidth + nodeGap) + nodeCenter) / width) * 100}%`,
|
||||
...lineStyle,
|
||||
}}
|
||||
>
|
||||
<span className="absolute -bottom-0.5 left-1/2 size-1.5 -translate-x-1/2 rotate-45 border-b border-r border-slate-300 dark:border-[#2b8ffe]/55" />
|
||||
<span
|
||||
className="absolute bottom-1 left-1/2 size-2 -translate-x-1/2 rotate-45 border-b-2 border-r-2 border-transparent"
|
||||
style={arrowStyle}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -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<string, unknown> | null | undefined): string | undefined {
|
||||
const path = recordString(artifact, "path");
|
||||
return path || undefined;
|
||||
}
|
||||
|
||||
function artifactMimeType(artifact: Record<string, unknown> | 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<boolean>;
|
||||
askSubmitting: boolean;
|
||||
onAskBuiltin?: (kind: "summary" | "action_plan", message: string) => Promise<boolean>;
|
||||
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({
|
||||
<TButton
|
||||
size="small"
|
||||
theme="primary"
|
||||
loading={askSubmitting}
|
||||
disabled={!followUp.trim() || askSubmitting}
|
||||
loading={Boolean(askSubmitting)}
|
||||
disabled={!followUp.trim() || Boolean(askSubmitting)}
|
||||
onClick={() => {
|
||||
void onAskBuiltin(builtinKind, followUp).then((saved) => {
|
||||
void onAskBuiltin?.(builtinKind, followUp).then((saved) => {
|
||||
if (saved) setFollowUp("");
|
||||
});
|
||||
}}
|
||||
@@ -3283,7 +3552,7 @@ function DeliverableDetailDialog({
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter className="shrink-0 border-t px-5 py-3 dark:border-[#2b8ffe]/25">
|
||||
{item?.node && (
|
||||
{showReject && item?.node && (
|
||||
<TButton
|
||||
theme="danger"
|
||||
variant="outline"
|
||||
|
||||
Reference in new issue
Block a user