feat: refine position roundtable workflow

This commit is contained in:
fengxiangdebaozi committed 2026-07-22 16:53:10 +08:00
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"