Files
deerflow-code/web-code/components/angentchat/flowLayout.ts
T
2026-05-28 18:29:37 +08:00

267 lines
6.9 KiB
TypeScript

import type {
ActorCard,
BehaviorDriverCard,
NarrativeCard,
PurposeCard,
SubStepKey,
} from './types';
export interface LayoutNode {
id: string;
level: number;
x: number;
y: number;
width: number;
height: number;
variant: 'agent' | 'purpose' | 'actor' | 'behavior' | 'driver' | 'narrative';
title: string;
detail: string;
children: string[];
parent?: string;
}
export interface LayoutEdge {
id: string;
fromId: string;
toId: string;
pair: 'agent-purpose' | 'purpose-actor' | 'actor-behavior' | 'behavior-driver' | 'driver-narrative';
}
export interface FlowLayout {
nodes: LayoutNode[];
edges: LayoutEdge[];
width: number;
height: number;
}
export interface BuildLayoutOptions {
nodeWidth?: number;
nodeHeight?: number;
levelGap?: number;
nodeGap?: number;
paddingX?: number;
paddingY?: number;
rootTitle?: string;
rootDetail?: string;
}
export const CURRENT_PAIR_BY_STAGE: Record<SubStepKey, LayoutEdge['pair'] | null> = {
purpose: 'agent-purpose',
actor: 'purpose-actor',
behavior: 'actor-behavior',
narrative: 'driver-narrative',
};
export const STATUS_TITLE: Record<SubStepKey, string> = {
purpose: '正在生成目的中',
actor: '正在生成行为体中',
behavior: '正在生成预期行为和驱动因素中',
narrative: '正在生成叙事中',
};
export const EDGE_COLOR: Record<LayoutEdge['pair'], { dark: string; light: string }> = {
'agent-purpose': { dark: '#a78bfa', light: '#fb923c' },
'purpose-actor': { dark: '#22d3ee', light: '#ef4444' },
'actor-behavior': { dark: '#34d399', light: '#10b981' },
'behavior-driver': { dark: '#5eead4', light: '#0d9488' },
'driver-narrative': { dark: '#fbbf24', light: '#f59e0b' },
};
export const EDGE_LABEL: Record<LayoutEdge['pair'], string> = {
'agent-purpose': '智能体→目的',
'purpose-actor': '目的→行为体',
'actor-behavior': '行为体→预期行为',
'behavior-driver': '预期行为→驱动因素',
'driver-narrative': '驱动因素→叙事',
};
export function buildEdgePath(from: LayoutNode, to: LayoutNode): string {
const x1 = from.x + from.width / 2;
const y1 = from.y + from.height;
const x2 = to.x + to.width / 2;
const y2 = to.y;
const my = (y1 + y2) / 2;
return `M ${x1} ${y1} C ${x1} ${my}, ${x2} ${my}, ${x2} ${y2}`;
}
export function buildLayout(
purposes: PurposeCard[],
actors: ActorCard[],
behaviors: BehaviorDriverCard[],
narratives: NarrativeCard[],
options: BuildLayoutOptions = {},
): FlowLayout {
const NODE_WIDTH = options.nodeWidth ?? 200;
const NODE_HEIGHT = options.nodeHeight ?? 84;
const LEVEL_GAP = options.levelGap ?? 130;
const NODE_GAP = options.nodeGap ?? 28;
const PADDING_X = options.paddingX ?? 48;
const PADDING_Y = options.paddingY ?? 40;
const rootTitle = options.rootTitle?.trim() || '智能体';
const rootDetail = options.rootDetail?.trim() || '协同推演 6 步法生成内容';
const nodes: LayoutNode[] = [];
const nodeMap = new Map<string, LayoutNode>();
const push = (node: LayoutNode) => {
nodes.push(node);
nodeMap.set(node.id, node);
};
const agent: LayoutNode = {
id: 'agent-root',
level: 0,
x: 0,
y: 0,
width: NODE_WIDTH,
height: NODE_HEIGHT,
variant: 'agent',
title: rootTitle,
detail: rootDetail,
children: purposes.map((p) => p.id),
};
push(agent);
purposes.forEach((p) => {
push({
id: p.id,
level: 1,
x: 0,
y: 0,
width: NODE_WIDTH,
height: NODE_HEIGHT,
variant: 'purpose',
title: p.title,
detail: p.reason,
children: actors.filter((a) => a.purposeId === p.id).map((a) => a.id),
parent: 'agent-root',
});
});
actors.forEach((a) => {
push({
id: a.id,
level: 2,
x: 0,
y: 0,
width: NODE_WIDTH,
height: NODE_HEIGHT,
variant: 'actor',
title: a.title,
detail: a.content,
children: behaviors.filter((b) => b.actorId === a.id).map((b) => `${b.id}-behavior`),
parent: a.purposeId,
});
});
behaviors.forEach((b) => {
const behaviorId = `${b.id}-behavior`;
const driverId = `${b.id}-driver`;
push({
id: behaviorId,
level: 3,
x: 0,
y: 0,
width: NODE_WIDTH,
height: NODE_HEIGHT,
variant: 'behavior',
title: '预期行为',
detail: b.expectedBehavior,
children: [driverId],
parent: b.actorId,
});
push({
id: driverId,
level: 4,
x: 0,
y: 0,
width: NODE_WIDTH,
height: NODE_HEIGHT,
variant: 'driver',
title: '驱动因素',
detail: b.driver,
children: narratives.filter((n) => n.driverId === b.id).map((n) => n.id),
parent: behaviorId,
});
});
narratives.forEach((n) => {
push({
id: n.id,
level: 5,
x: 0,
y: 0,
width: NODE_WIDTH,
height: NODE_HEIGHT,
variant: 'narrative',
title: n.title,
detail: n.reason,
children: [],
parent: `${n.driverId}-driver`,
});
});
const leafCounter = { v: 0 };
const place = (node: LayoutNode) => {
if (node.children.length === 0) {
node.x = PADDING_X + leafCounter.v * (NODE_WIDTH + NODE_GAP);
leafCounter.v += 1;
return;
}
node.children.forEach((cid) => {
const child = nodeMap.get(cid);
if (child) place(child);
});
const xs = node.children.map((cid) => nodeMap.get(cid)?.x ?? 0).filter((v) => v >= 0);
if (xs.length > 0) {
node.x = (Math.min(...xs) + Math.max(...xs)) / 2;
} else {
node.x = PADDING_X;
}
};
place(agent);
nodes.forEach((node) => {
node.y = PADDING_Y + node.level * (NODE_HEIGHT + LEVEL_GAP);
});
const edges: LayoutEdge[] = [];
nodes.forEach((node) => {
node.children.forEach((cid) => {
const child = nodeMap.get(cid);
if (!child) return;
let pair: LayoutEdge['pair'];
if (node.variant === 'agent') pair = 'agent-purpose';
else if (node.variant === 'purpose') pair = 'purpose-actor';
else if (node.variant === 'actor') pair = 'actor-behavior';
else if (node.variant === 'behavior') pair = 'behavior-driver';
else pair = 'driver-narrative';
edges.push({ id: `${node.id}->${cid}`, fromId: node.id, toId: cid, pair });
});
});
const allLevels = Math.max(...nodes.map((n) => n.level));
const maxX = Math.max(...nodes.map((n) => n.x + n.width));
const totalWidth = maxX + PADDING_X;
const totalHeight = PADDING_Y * 2 + (allLevels + 1) * NODE_HEIGHT + allLevels * LEVEL_GAP;
return { nodes, edges, width: totalWidth, height: totalHeight };
}
export const VARIANT_ABBREV: Record<LayoutNode['variant'], string> = {
agent: 'AI',
purpose: 'md',
actor: 'xwt',
behavior: 'yqxw',
driver: 'qdys',
narrative: 'xs',
};
export const VARIANT_LABEL: Record<LayoutNode['variant'], string> = {
agent: '智能体',
purpose: '目的',
actor: '行为体',
behavior: '预期行为',
driver: '驱动因素',
narrative: '叙事',
};