| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493 |
- import { MarkerType, type Edge, type Node } from "@xyflow/react";
- import type {
- BranchStory,
- CanvasItem,
- DirectToolGroup,
- ExecutionViewV8,
- OpenTarget,
- RetrievalAgentRun,
- RetrievalStage,
- RoundStoryV8,
- StoryNode,
- } from "@/lib/types";
- const STORY_W = 320;
- const RETRIEVAL_W = 340;
- const SUMMARY_W = 360;
- const ROUND_GAP = 104;
- const ROUND_HEADER = 88;
- const REGION_TOP = ROUND_HEADER + 16;
- const REGION_HEADER = 54;
- const REGION_PAD = 24;
- const REGION_GAP = 32;
- const CARD_GAP = 32;
- const BRANCH_HEADER = 66;
- const RETRIEVAL_HEADER = 72;
- const RETRIEVAL_ITEM_GAP = 18;
- const FLOW_CENTER_Y = 350;
- const FINAL_RESULT_H = 368;
- const PLANNING_W = STORY_W * 2 + CARD_GAP + REGION_PAD * 2;
- const IMPLEMENT_COMPACT_W = SUMMARY_W + REGION_PAD * 2;
- const SINGLE_REGION_W = STORY_W + REGION_PAD * 2;
- export type AgentRegionTone = "main" | "implementer" | "evaluator" | "outcome";
- export type CanvasDetailLevel = "full" | "compact" | "overview";
- export interface AgentRegionProjection { label: string; description: string; tone: AgentRegionTone }
- export interface CanvasNodeData extends Record<string, unknown> {
- buildId?: string;
- item?: CanvasItem;
- round?: RoundStoryV8;
- region?: AgentRegionProjection;
- expanded?: boolean;
- onOpen: (item: CanvasItem, target: OpenTarget) => void;
- onPrompt?: (promptRef: string) => void;
- onToggleRound: (roundIndex: number) => void;
- onToggleBranch: (roundIndex: number, branchId: number) => void;
- onToggleRetrievalAgent: (agentId: string) => void;
- }
- export interface LayoutOptions {
- buildId?: string;
- expandedRounds: Set<number>;
- expandedBranches: Set<string>;
- expandedRetrievalAgents: Set<string>;
- detailLevel?: CanvasDetailLevel;
- onOpen: CanvasNodeData["onOpen"];
- onPrompt?: CanvasNodeData["onPrompt"];
- onToggleRound: CanvasNodeData["onToggleRound"];
- onToggleBranch: CanvasNodeData["onToggleBranch"];
- onToggleRetrievalAgent: CanvasNodeData["onToggleRetrievalAgent"];
- }
- export interface LayoutResult { nodes: Array<Node<CanvasNodeData>>; edges: Edge[] }
- interface RoundMetrics {
- width: number;
- height: number;
- regionHeight: number;
- laneTops: number[];
- laneHeights: number[];
- contentCenterY: number;
- planningX: number;
- implementationX: number;
- implementationWidth: number;
- reviewX: number;
- convergenceX: number;
- evaluatorX: number;
- outcomeX: number;
- batchTops: number[];
- batchHeights: Array<{ review: number; decision: number; group: number }>;
- }
- export function buildHorizontalLayout(view: ExecutionViewV8, options: LayoutOptions): LayoutResult {
- const nodes: Array<Node<CanvasNodeData>> = [];
- const edges: Edge[] = [];
- const planning = view.planning ? storyItem(view.planning) : undefined;
- const objective = storyItem(view.objective);
- if (planning) nodes.push(canvasNode(planning, "storyCard", { x: 60, y: 210 }, options));
- const objectiveX = planning ? 60 + STORY_W + ROUND_GAP : 60;
- nodes.push(canvasNode(objective, "storyCard", { x: objectiveX, y: 210 }, options));
- if (planning) edges.push(flowEdge(planning.id, objective.id));
- const roundX = objectiveX + STORY_W + ROUND_GAP;
- let previousRowBottom = 0;
- let previous = objective.id;
- let finalX = roundX;
- let finalY = 170;
- for (const [roundPosition, round] of [...view.rounds].sort((a, b) => a.roundIndex - b.roundIndex).entries()) {
- if (!options.expandedRounds.has(round.roundIndex)) {
- const item = roundSummaryItem(round);
- const rowHeight = collapsedRoundHeight(options.detailLevel ?? "full");
- const rowY = roundPosition === 0 ? 148 : previousRowBottom + ROUND_GAP;
- nodes.push(canvasNode(item, "roundSummary", { x: roundX, y: rowY }, options));
- edges.push(flowEdge(previous, item.id, { status: round.state }));
- previous = item.id;
- previousRowBottom = rowY + rowHeight;
- finalX = roundX + SUMMARY_W + ROUND_GAP;
- finalY = rowY + (rowHeight - FINAL_RESULT_H) / 2;
- continue;
- }
- const groupId = `frame:round:${round.roundIndex}`;
- const metrics = expandedRoundMetrics(round, options);
- const rowY = roundPosition === 0
- ? Math.min(42, FLOW_CENTER_Y - metrics.contentCenterY)
- : previousRowBottom + ROUND_GAP;
- nodes.push({
- id: groupId,
- type: "roundFrame",
- position: { x: roundX, y: rowY },
- data: baseData(options, { round, expanded: true }),
- style: { width: metrics.width, height: metrics.height },
- draggable: false,
- selectable: true,
- focusable: false,
- zIndex: -3,
- });
- addAgentRegions(nodes, round, groupId, options, metrics);
- const result = addExpandedRound(nodes, edges, round, groupId, options, metrics);
- edges.push(flowEdge(previous, result.entry));
- previous = result.exit;
- previousRowBottom = rowY + metrics.height;
- finalX = roundX + metrics.width + ROUND_GAP;
- finalY = rowY + metrics.contentCenterY - FINAL_RESULT_H / 2;
- }
- const finalItem = finalResultItem(view);
- nodes.push(canvasNode(finalItem, "finalResult", { x: finalX, y: finalY }, options));
- edges.push(flowEdge(previous, finalItem.id, { status: view.finalResult.status }));
- return { nodes, edges };
- }
- function expandedRoundMetrics(round: RoundStoryV8, options: LayoutOptions): RoundMetrics {
- const detailLevel = options.detailLevel ?? "full";
- const density = densityFor(detailLevel);
- const expandedWidths = round.branches
- .filter((branch) => options.expandedBranches.has(branchKey(round.roundIndex, branch.branchId)))
- .map((branch) => branchFrameWidth(branch));
- const implementationWidth = expandedWidths.length
- ? Math.max(...expandedWidths) + REGION_PAD * 2
- : IMPLEMENT_COMPACT_W;
- const planningX = 32;
- const implementationX = planningX + PLANNING_W + REGION_GAP;
- const reviewX = implementationX + implementationWidth + REGION_GAP;
- const convergenceX = reviewX + SINGLE_REGION_W + REGION_GAP;
- const evaluatorX = convergenceX + SINGLE_REGION_W + REGION_GAP;
- const outcomeX = evaluatorX + SINGLE_REGION_W + REGION_GAP;
- const width = outcomeX + SINGLE_REGION_W + 32;
- const branches: Array<BranchStory | undefined> = round.branches.length ? round.branches : [undefined];
- const laneHeights = branches.map((branch) => {
- if (!branch) return 240;
- return options.expandedBranches.has(branchKey(round.roundIndex, branch.branchId))
- ? branchFrameHeight(branch, detailLevel, options.expandedRetrievalAgents)
- : estimateBranchHeight(branch, detailLevel);
- });
- const laneTops: number[] = [];
- const laneStart = REGION_TOP + REGION_HEADER + density.contentInset;
- laneHeights.reduce((top, height) => { laneTops.push(top); return top + height + density.laneGap; }, laneStart);
- const laneBottom = laneTops.at(-1)! + laneHeights.at(-1)!;
- const batchHeights = round.convergenceBatches.map((batch) => {
- const review = batch.review
- ? estimateMultipathReviewHeight(batch.review, detailLevel)
- : batch.missingReview ? estimateStoryHeight(batch.missingReview, detailLevel) : 244;
- const decision = batch.decision
- ? estimateMultipathDecisionHeight(batch.decision, detailLevel)
- : batch.missingDecision ? estimateStoryHeight(batch.missingDecision, detailLevel) : 244;
- return { review, decision, group: Math.max(review, decision) };
- });
- const batchStackHeight = batchHeights.reduce((sum, height) => sum + height.group, 0) + Math.max(0, batchHeights.length - 1) * density.laneGap;
- const contentHeight = Math.max(
- laneBottom - laneStart,
- batchStackHeight,
- estimateStoryHeight(round.goal, detailLevel),
- estimateStoryHeight(round.plan.node, detailLevel),
- estimateStoryHeight(round.overallEvaluation, detailLevel),
- estimateStoryHeight(round.result, detailLevel),
- );
- const regionHeight = REGION_HEADER + density.contentInset + contentHeight + density.contentInset;
- const contentCenterY = laneStart + contentHeight / 2;
- const batchStart = laneStart + Math.max(0, (contentHeight - batchStackHeight) / 2);
- const batchTops: number[] = [];
- batchHeights.reduce((top, height) => { batchTops.push(top); return top + height.group + density.laneGap; }, batchStart);
- return { width, height: REGION_TOP + regionHeight + 34, regionHeight, laneTops, laneHeights, contentCenterY, planningX, implementationX, implementationWidth, reviewX, convergenceX, evaluatorX, outcomeX, batchTops, batchHeights };
- }
- function addAgentRegions(nodes: Array<Node<CanvasNodeData>>, round: RoundStoryV8, parentId: string, options: LayoutOptions, metrics: RoundMetrics) {
- const regions = [
- { id: "planning", x: metrics.planningX, width: PLANNING_W, projection: { label: "主 Agent · 规划", description: "确定本轮目标,并拆成多路方案", tone: "main" as const } },
- { id: "implementation", x: metrics.implementationX, width: metrics.implementationWidth, projection: { label: "实现 Agent · 多路方案", description: "每个方案独立取数、取舍并形成产出", tone: "implementer" as const } },
- { id: "multipath-review", x: metrics.reviewX, width: SINGLE_REGION_W, projection: { label: "多方案评审 Agent", description: "比较候选脚本并给出评审建议", tone: "evaluator" as const } },
- { id: "convergence", x: metrics.convergenceX, width: SINGLE_REGION_W, projection: { label: "主 Agent · 收敛", description: "结合评审处理各个候选方案", tone: "main" as const } },
- { id: "evaluation", x: metrics.evaluatorX, width: SINGLE_REGION_W, projection: { label: "整体评审 Agent", description: "检查合并后的主脚本是否达标", tone: "evaluator" as const } },
- { id: "outcome", x: metrics.outcomeX, width: SINGLE_REGION_W, projection: { label: "本轮结果", description: "汇总主脚本和领域信息产出", tone: "outcome" as const } },
- ];
- for (const region of regions) nodes.push({
- id: `region:round:${round.roundIndex}:${region.id}`,
- type: "agentRegion",
- position: { x: region.x, y: REGION_TOP },
- parentId,
- extent: "parent",
- data: baseData(options, { region: region.projection }),
- style: { width: region.width, height: metrics.regionHeight },
- draggable: false,
- selectable: false,
- focusable: false,
- zIndex: -2,
- });
- }
- function addExpandedRound(nodes: Array<Node<CanvasNodeData>>, edges: Edge[], round: RoundStoryV8, parentId: string, options: LayoutOptions, metrics: RoundMetrics) {
- const detailLevel = options.detailLevel ?? "full";
- const addStory = (story: StoryNode, x: number, y: number, branchId?: number, storyParentId = parentId) => {
- const item = storyItem(story, round.roundIndex, branchId);
- nodes.push(canvasNode(item, "storyCard", { x, y }, options, storyParentId));
- return item.id;
- };
- const goalId = addStory(round.goal, metrics.planningX + REGION_PAD, centeredRoundY(metrics, estimateStoryHeight(round.goal, detailLevel)));
- const planId = addStory(round.plan.node, metrics.planningX + REGION_PAD + STORY_W + CARD_GAP, centeredRoundY(metrics, estimateStoryHeight(round.plan.node, detailLevel)));
- edges.push(flowEdge(goalId, planId));
- const branchEnds = new Map<number, string>();
- round.branches.forEach((branch, index) => {
- const laneTop = metrics.laneTops[index];
- const laneHeight = metrics.laneHeights[index];
- const expanded = options.expandedBranches.has(branchKey(round.roundIndex, branch.branchId));
- if (!expanded) {
- const item = branchSummaryItem(branch, round.roundIndex);
- nodes.push(canvasNode(item, "candidateCard", { x: metrics.implementationX + REGION_PAD, y: laneTop + Math.max(0, (laneHeight - estimateBranchHeight(branch, detailLevel)) / 2) }, options, parentId));
- edges.push(flowEdge(planId, item.id, { kind: "delegation" }));
- branchEnds.set(branch.branchId, item.id);
- return;
- }
- const frameId = `frame:round:${round.roundIndex}:branch:${branch.branchId}`;
- const frameHeight = branchFrameHeight(branch, detailLevel, options.expandedRetrievalAgents);
- const frameWidth = branchFrameWidth(branch);
- nodes.push({
- id: frameId,
- type: "branchFrame",
- position: { x: metrics.implementationX + REGION_PAD, y: laneTop + Math.max(0, (laneHeight - frameHeight) / 2) },
- parentId,
- extent: "parent",
- data: baseData(options, { item: branchSummaryItem(branch, round.roundIndex), expanded: true }),
- style: { width: frameWidth, height: frameHeight },
- draggable: false,
- selectable: true,
- focusable: false,
- zIndex: 1,
- });
- branchEnds.set(branch.branchId, addExpandedBranch(nodes, edges, round, branch, frameId, options, planId));
- });
- const batchIds: string[] = [];
- const covered = new Set<number>();
- round.convergenceBatches.forEach((batch, index) => {
- const batchTop = metrics.batchTops[index] ?? centeredRoundY(metrics, 244);
- const heights = metrics.batchHeights[index] ?? { review: 244, decision: 244, group: 244 };
- const reviewY = batchTop + Math.max(0, (heights.group - heights.review) / 2);
- const decisionY = batchTop + Math.max(0, (heights.group - heights.decision) / 2);
- let reviewId: string;
- if (batch.review) {
- const item = multipathReviewItem(batch.review, round.roundIndex);
- nodes.push(canvasNode(item, "multipathReview", { x: metrics.reviewX + REGION_PAD, y: reviewY }, options, parentId));
- reviewId = item.id;
- } else if (batch.missingReview) {
- reviewId = addStory(batch.missingReview, metrics.reviewX + REGION_PAD, reviewY);
- } else return;
- let decisionId: string;
- if (batch.decision) {
- const item = multipathDecisionItem(batch.decision, round.roundIndex);
- nodes.push(canvasNode(item, "multipathDecision", { x: metrics.convergenceX + REGION_PAD, y: decisionY }, options, parentId));
- decisionId = item.id;
- } else if (batch.missingDecision) {
- decisionId = addStory(batch.missingDecision, metrics.convergenceX + REGION_PAD, decisionY);
- } else return;
- batchIds.push(decisionId);
- batch.branchIds.forEach((branchId) => {
- const source = branchEnds.get(branchId);
- if (source) { edges.push(flowEdge(source, reviewId, { kind: "delegation" })); covered.add(branchId); }
- });
- edges.push(flowEdge(reviewId, decisionId));
- });
- const evaluationId = addStory(round.overallEvaluation, metrics.evaluatorX + REGION_PAD, centeredRoundY(metrics, estimateStoryHeight(round.overallEvaluation, detailLevel)));
- const resultId = addStory(round.result, metrics.outcomeX + REGION_PAD, centeredRoundY(metrics, estimateStoryHeight(round.result, detailLevel)));
- if (batchIds.length) batchIds.forEach((id) => edges.push(flowEdge(id, evaluationId, { kind: "delegation" })));
- else edges.push(flowEdge(planId, evaluationId));
- branchEnds.forEach((id, branchId) => { if (!covered.has(branchId) && batchIds.length) edges.push(flowEdge(id, evaluationId, { kind: "unassigned" })); });
- edges.push(flowEdge(evaluationId, resultId, { status: round.state }));
- return { entry: goalId, exit: resultId };
- }
- function addExpandedBranch(nodes: Array<Node<CanvasNodeData>>, edges: Edge[], round: RoundStoryV8, branch: BranchStory, parentId: string, options: LayoutOptions, planId: string) {
- const detailLevel = options.detailLevel ?? "full";
- const density = densityFor(detailLevel);
- const contentTop = BRANCH_HEADER + density.contentInset;
- const frameHeight = branchFrameHeight(branch, detailLevel, options.expandedRetrievalAgents);
- const contentHeight = frameHeight - contentTop - density.contentInset;
- const taskItem = storyItem(branch.task, round.roundIndex, branch.branchId);
- const taskY = contentTop + Math.max(0, (contentHeight - estimateStoryHeight(branch.task, detailLevel)) / 2);
- nodes.push(canvasNode(taskItem, "storyCard", { x: REGION_PAD, y: taskY }, options, parentId));
- edges.push(flowEdge(planId, taskItem.id, { kind: "delegation" }));
- const retrievalX = REGION_PAD + STORY_W + CARD_GAP;
- const retrievalHeight = retrievalFrameHeight(branch.retrievalStage, options.expandedRetrievalAgents, detailLevel);
- const retrievalY = contentTop + Math.max(0, (contentHeight - retrievalHeight) / 2);
- const stageItem = retrievalStageItem(branch.retrievalStage, round.roundIndex, branch.branchId);
- nodes.push({
- id: `frame:${stageItem.id}`,
- type: "retrievalStageFrame",
- position: { x: retrievalX, y: retrievalY },
- parentId,
- extent: "parent",
- data: baseData(options, { item: stageItem }),
- style: { width: retrievalFrameWidth(branch.retrievalStage), height: retrievalHeight },
- draggable: false,
- selectable: true,
- focusable: false,
- zIndex: 2,
- });
- const retrievalEnds = addRetrievalOperations(nodes, edges, branch.retrievalStage, round.roundIndex, branch.branchId, `frame:${stageItem.id}`, options, taskItem.id);
- let cursorX = retrievalX + retrievalFrameWidth(branch.retrievalStage) + CARD_GAP;
- let previous = retrievalEnds.length ? retrievalEnds : [taskItem.id];
- if (branch.dataDecisions.length) {
- const heights = branch.dataDecisions.map((item) => estimateStoryHeight(item.node, detailLevel));
- const stackHeight = heights.reduce((sum, height) => sum + height, 0) + Math.max(0, heights.length - 1) * density.laneGap;
- let top = contentTop + Math.max(0, (contentHeight - stackHeight) / 2);
- const decisionIds: string[] = [];
- branch.dataDecisions.forEach((decision, index) => {
- const item = storyItem(decision.node, round.roundIndex, branch.branchId);
- nodes.push(canvasNode(item, "storyCard", { x: cursorX, y: top }, options, parentId));
- decisionIds.push(item.id);
- top += heights[index] + density.laneGap;
- });
- previous.forEach((source) => decisionIds.forEach((target) => edges.push(flowEdge(source, target))));
- previous = decisionIds;
- cursorX += STORY_W + CARD_GAP;
- }
- const tail = [branch.output.node];
- tail.forEach((story) => {
- const item = storyItem(story, round.roundIndex, branch.branchId);
- const y = contentTop + Math.max(0, (contentHeight - estimateStoryHeight(story, detailLevel)) / 2);
- nodes.push(canvasNode(item, "storyCard", { x: cursorX, y }, options, parentId));
- previous.forEach((source) => edges.push(flowEdge(source, item.id)));
- previous = [item.id];
- cursorX += STORY_W + CARD_GAP;
- });
- return previous[0];
- }
- function addRetrievalOperations(nodes: Array<Node<CanvasNodeData>>, edges: Edge[], stage: RetrievalStage, roundIndex: number, branchId: number, parentId: string, options: LayoutOptions, sourceId: string) {
- const detailLevel = options.detailLevel ?? "full";
- const operations = retrievalOperations(stage);
- if (!operations.length) return [];
- const waveIndexes = stage.waves.length ? stage.waves.map((wave) => wave.index) : [0];
- const byWave = new Map<number, Array<DirectToolGroup | RetrievalAgentRun>>();
- operations.forEach((operation) => {
- // No timestamp means no defensible ordering. Keep every operation in one
- // neutral column instead of inventing a left-to-right sequence.
- const wave = operation.waveIndex ?? 0;
- byWave.set(wave, [...(byWave.get(wave) || []), operation]);
- });
- let previous = [sourceId];
- let top = RETRIEVAL_HEADER + RETRIEVAL_ITEM_GAP;
- for (const waveIndex of [...new Set([...waveIndexes, ...byWave.keys()])].sort((a, b) => a - b)) {
- const waveItems = byWave.get(waveIndex) || [];
- if (!waveItems.length) continue;
- const heights = waveItems.map((item) => retrievalOperationHeight(item, options.expandedRetrievalAgents, detailLevel));
- const ids: string[] = [];
- waveItems.forEach((operation, index) => {
- const item = operation.kind === "direct-tools"
- ? retrievalDirectItem(operation, roundIndex, branchId)
- : retrievalAgentItem(operation, roundIndex, branchId);
- nodes.push(canvasNode(item, operation.kind === "direct-tools" ? "directToolGroup" : "retrievalAgent", { x: 20, y: top }, options, parentId));
- ids.push(item.id);
- top += heights[index] + RETRIEVAL_ITEM_GAP;
- });
- previous.forEach((source) => ids.forEach((target) => edges.push(flowEdge(source, target, { kind: "delegation" }))));
- previous = ids;
- }
- return previous;
- }
- function retrievalOperations(stage: RetrievalStage): Array<DirectToolGroup | RetrievalAgentRun> {
- return [...stage.directToolGroups, ...stage.agentRuns].sort((a, b) => {
- const wave = (a.waveIndex ?? 999) - (b.waveIndex ?? 999);
- return wave || String(a.startedAt || "").localeCompare(String(b.startedAt || "")) || a.id.localeCompare(b.id);
- });
- }
- function branchFrameWidth(branch: BranchStory) {
- const decisionColumn = branch.dataDecisions.length ? STORY_W + CARD_GAP : 0;
- const tailColumns = 1;
- return REGION_PAD * 2 + STORY_W + CARD_GAP + retrievalFrameWidth(branch.retrievalStage) + CARD_GAP + decisionColumn + tailColumns * STORY_W + (tailColumns - 1) * CARD_GAP;
- }
- function retrievalFrameWidth(_stage: RetrievalStage) {
- return 40 + RETRIEVAL_W;
- }
- function retrievalFrameHeight(stage: RetrievalStage, expandedAgents: Set<string>, detailLevel: CanvasDetailLevel) {
- const operations = retrievalOperations(stage);
- if (!operations.length) return detailLevel === "overview" ? 156 : 210;
- const contentHeight = operations.reduce((height, item) => height + retrievalOperationHeight(item, expandedAgents, detailLevel), 0)
- + Math.max(0, operations.length - 1) * RETRIEVAL_ITEM_GAP;
- return RETRIEVAL_HEADER + RETRIEVAL_ITEM_GAP + contentHeight + 24;
- }
- function retrievalOperationHeight(item: DirectToolGroup | RetrievalAgentRun, expandedAgents: Set<string>, detailLevel: CanvasDetailLevel) {
- if (detailLevel === "overview") return item.kind === "direct-tools" ? 132 : 148;
- if (item.kind === "direct-tools") return detailLevel === "compact" ? 196 : 248;
- const attempts = expandedAgents.has(item.id) ? item.attempts.length : 0;
- if (detailLevel === "compact") return 220 + (attempts ? 44 + attempts * 40 : 0);
- return 292 + (attempts ? 52 + attempts * 48 : 0);
- }
- function branchFrameHeight(branch: BranchStory, detailLevel: CanvasDetailLevel, expandedAgents: Set<string>) {
- const density = densityFor(detailLevel);
- const decisions = branch.dataDecisions.map((item) => estimateStoryHeight(item.node, detailLevel));
- const decisionStack = decisions.reduce((sum, height) => sum + height, 0) + Math.max(0, decisions.length - 1) * density.laneGap;
- const storyHeights = [branch.task, branch.output.node].map((story) => estimateStoryHeight(story, detailLevel));
- return BRANCH_HEADER + density.contentInset + Math.max(density.branchFloor, retrievalFrameHeight(branch.retrievalStage, expandedAgents, detailLevel), decisionStack, ...storyHeights) + density.contentInset;
- }
- export function branchFlow(branch: BranchStory): StoryNode[] {
- return [branch.task, ...branch.dataDecisions.map((item) => item.node), branch.output.node];
- }
- export function storyItem(story: StoryNode, roundIndex?: number, branchId?: number): CanvasItem { return { ...story, itemType: "story", roundIndex, branchId }; }
- export function retrievalStageItem(stage: RetrievalStage, roundIndex: number, branchId: number): CanvasItem { return { itemType: "retrieval-stage", id: stage.id, title: "取数阶段", roundIndex, branchId, stage, detailRef: stage.detailRef }; }
- export function retrievalDirectItem(group: DirectToolGroup, roundIndex: number, branchId: number): CanvasItem { return { itemType: "retrieval-direct", id: group.id, title: "工具取数", roundIndex, branchId, group, detailRef: group.detailRef }; }
- export function retrievalAgentItem(run: RetrievalAgentRun, roundIndex: number, branchId: number): CanvasItem { return { itemType: "retrieval-agent", id: run.id, title: run.businessLabel, roundIndex, branchId, run, detailRef: run.detailRef }; }
- export function queryAttemptItem(attempt: RetrievalAgentRun["attempts"][number], roundIndex: number, branchId: number): CanvasItem { return { itemType: "query-attempt", id: attempt.id, title: attempt.queryLabel, roundIndex, branchId, attempt, detailRef: attempt.detailRef }; }
- export function retrievalActivityItem(activity: import("@/lib/types").RetrievalActivity, roundIndex: number, branchId: number): CanvasItem { return { itemType: "retrieval-activity", id: activity.id, title: activity.label, roundIndex, branchId, activity, detailRef: activity.detailRef }; }
- export function branchSummaryItem(branch: BranchStory, roundIndex: number): CanvasItem { return { itemType: "branch-summary", id: `round:${roundIndex}:branch:${branch.branchId}:summary`, title: branch.title, roundIndex, branchId: branch.branchId, branch, detailRef: branch.detailRef }; }
- export function multipathReviewItem(review: import("@/lib/types").MultipathReview, roundIndex: number): CanvasItem { return { itemType: "multipath-review", id: review.id, title: "多方案评审", roundIndex, review, detailRef: review.detailRef }; }
- export function multipathDecisionItem(decision: import("@/lib/types").MultipathDecisionBatch, roundIndex: number): CanvasItem { return { itemType: "multipath-decision", id: decision.id, title: "主 Agent 多路决策", roundIndex, decision, detailRef: decision.node.detailRef || decision.id }; }
- export function roundSummaryItem(round: RoundStoryV8): CanvasItem { return { itemType: "round-summary", id: `round:${round.roundIndex}:summary`, title: round.summary.title, roundIndex: round.roundIndex, round, detailRef: round.detailRef }; }
- export function finalResultItem(view: ExecutionViewV8): CanvasItem { return { itemType: "final-result", id: view.finalResult.id, title: view.finalResult.title, result: view.finalResult, detailRef: "run:final-result" }; }
- function canvasNode(item: CanvasItem, type: string, position: { x: number; y: number }, options: LayoutOptions, parentId?: string): Node<CanvasNodeData> {
- const resolvedType = hasDecision(item) ? "agentDecision" : type;
- return { id: item.id, type: resolvedType, position, parentId, extent: parentId ? "parent" : undefined, data: baseData(options, { item, expanded: item.itemType === "branch-summary" ? options.expandedBranches.has(branchKey(item.roundIndex, item.branchId)) : item.itemType === "retrieval-agent" ? options.expandedRetrievalAgents.has(item.id) : undefined }), draggable: true, selectable: true, focusable: false, zIndex: 4 };
- }
- function baseData(options: LayoutOptions, extra: Partial<CanvasNodeData>): CanvasNodeData { return { buildId: options.buildId, onOpen: options.onOpen, onPrompt: options.onPrompt, onToggleRound: options.onToggleRound, onToggleBranch: options.onToggleBranch, onToggleRetrievalAgent: options.onToggleRetrievalAgent, ...extra }; }
- function hasDecision(item: CanvasItem) { return item.itemType === "story" ? Boolean(item.decision) : item.itemType === "multipath-review" ? Boolean(item.review.decisionProjection) : item.itemType === "multipath-decision" ? Boolean(item.decision.decisionProjection) : false; }
- function flowEdge(source: string, target: string, options: { status?: string; kind?: string } = {}): Edge {
- const color = options.kind === "unassigned" ? "#f4c152" : options.status === "merged" ? "#63c883" : options.status === "parked" ? "#f4c152" : options.status === "discarded" ? "#ff806e" : options.kind === "delegation" ? "#7bb8ff" : "#7e848f";
- return { id: `edge:${source}:${target}`, source, target, type: "smoothstep", animated: options.status === "running", style: { stroke: color, strokeWidth: 1.7, strokeDasharray: options.kind === "delegation" || options.kind === "unassigned" ? "6 5" : undefined }, markerEnd: { type: MarkerType.ArrowClosed, color, width: 14, height: 14 } };
- }
- function estimateStoryHeight(story: StoryNode, detailLevel: CanvasDetailLevel) {
- if (detailLevel === "overview") return 120;
- const visibleValues = [story.card.primary?.value, ...story.card.secondary.map((item) => item.value)].filter(Boolean).slice(0, detailLevel === "compact" ? 1 : undefined) as string[];
- const lines = visibleValues.reduce((sum, value, index) => sum + Math.min(index === 0 ? 4 : 2, Math.max(1, Math.ceil(value.length / 25))), 0);
- return Math.max(detailLevel === "compact" ? 196 : 244, (detailLevel === "compact" ? 142 : 164) + lines * 23 + (story.sourceNotice && detailLevel === "full" ? 28 : 0));
- }
- function estimateBranchHeight(branch: BranchStory, detailLevel: CanvasDetailLevel) {
- if (detailLevel === "overview") return 132;
- const values = [branch.title, branch.summary.task, branch.summary.output].filter(Boolean) as string[];
- const lines = values.reduce((sum, value) => sum + Math.min(2, Math.max(1, Math.ceil(value.length / 25))), 0);
- const sourceAllowance = branch.outcome.sourceNotice || branch.task.sourceNotice || branch.output.node.sourceNotice ? 28 : 0;
- return Math.max(detailLevel === "compact" ? 232 : 320, (detailLevel === "full" ? 188 : 148) + lines * 23 + sourceAllowance);
- }
- function estimateMultipathReviewHeight(review: import("@/lib/types").MultipathReview, detailLevel: CanvasDetailLevel) {
- return estimateDecisionCardHeight(review.decisionProjection?.card, detailLevel);
- }
- function estimateMultipathDecisionHeight(decision: import("@/lib/types").MultipathDecisionBatch, detailLevel: CanvasDetailLevel) {
- return estimateDecisionCardHeight(decision.decisionProjection?.card, detailLevel);
- }
- function estimateDecisionCardHeight(card: import("@/lib/types").CardProjection | undefined, detailLevel: CanvasDetailLevel) {
- if (detailLevel === "overview") return 132;
- const values = card ? [card.primary?.value, ...card.secondary.map((item) => item.value)].filter(Boolean) as string[] : [];
- const visible = values.slice(0, detailLevel === "compact" ? 1 : 4);
- const lines = visible.reduce((sum, value, index) => sum + Math.min(index === 0 ? 4 : 2, Math.max(1, Math.ceil(value.length / 25))), 0);
- return Math.max(detailLevel === "compact" ? 216 : 296, (detailLevel === "compact" ? 150 : 190) + lines * 23);
- }
- function densityFor(detailLevel: CanvasDetailLevel) {
- if (detailLevel === "overview") return { laneGap: 12, contentInset: 12, branchFloor: 148 };
- if (detailLevel === "compact") return { laneGap: 18, contentInset: 16, branchFloor: 208 };
- return { laneGap: 28, contentInset: 24, branchFloor: 244 };
- }
- function collapsedRoundHeight(detailLevel: CanvasDetailLevel) {
- if (detailLevel === "overview") return 164;
- if (detailLevel === "compact") return 280;
- return 400;
- }
- function branchKey(roundIndex: number, branchId: number) { return `${roundIndex}:${branchId}`; }
- function centeredRoundY(metrics: RoundMetrics, height: number) { return metrics.contentCenterY - height / 2; }
|