|
|
@@ -30,7 +30,9 @@ export type SubTraceEntry = { id: string; mission?: string };
|
|
|
const EMPTY_GOALS: Goal[] = [];
|
|
|
|
|
|
export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeClick, onSubTraceClick }) => {
|
|
|
- console.log("%c [ goals---2 ]-33", "font-size:13px; background:pink; color:#bf2c9f;", goals);
|
|
|
+ goals = goals.filter((g) => !g.parent_id);
|
|
|
+
|
|
|
+ console.log("%c [ FlowChart-goals ]-33", "font-size:13px; background:pink; color:#bf2c9f;", goals);
|
|
|
// 确保 goals 中包含 END 节点
|
|
|
const displayGoals = useMemo(() => {
|
|
|
if (!goals) return [];
|
|
|
@@ -348,6 +350,83 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
|
|
|
return links;
|
|
|
}, [displayGoals, handleSubTraceClick, layoutData, normalizeSubTraceEntries]);
|
|
|
|
|
|
+ const subGoalsOverlay = useMemo(() => {
|
|
|
+ if (!layoutData)
|
|
|
+ return [] as Array<{
|
|
|
+ parentId: string;
|
|
|
+ nodes: Array<{ x: number; y: number; fullText: string; shortText: string }>;
|
|
|
+ paths: Array<{ d: string }>;
|
|
|
+ }>;
|
|
|
+
|
|
|
+ const nodeMap = new Map(layoutData.nodes.map((node) => [node.data.id, node]));
|
|
|
+
|
|
|
+ const truncateMiddle = (text: string, limit: number) => {
|
|
|
+ if (text.length <= limit) return text;
|
|
|
+ const half = Math.floor((limit - 3) / 2);
|
|
|
+ return `${text.slice(0, half)}...${text.slice(-half)}`;
|
|
|
+ };
|
|
|
+
|
|
|
+ const flattenSubGoals = (items: Goal[] | undefined): Goal[] => {
|
|
|
+ if (!items || items.length === 0) return [];
|
|
|
+ const result: Goal[] = [];
|
|
|
+ items.forEach((item) => {
|
|
|
+ result.push(item);
|
|
|
+ const children = flattenSubGoals(item.sub_goals);
|
|
|
+ if (children.length > 0) result.push(...children);
|
|
|
+ });
|
|
|
+ return result;
|
|
|
+ };
|
|
|
+
|
|
|
+ const buildSegment = (sx: number, sy: number, tx: number, ty: number) => {
|
|
|
+ const controlX = (sx + tx) / 2 - 40;
|
|
|
+ const controlY = Math.max(sy, ty) - 30;
|
|
|
+ return `M${sx},${sy} Q${controlX},${controlY} ${tx},${ty}`;
|
|
|
+ };
|
|
|
+
|
|
|
+ const overlays: Array<{
|
|
|
+ parentId: string;
|
|
|
+ nodes: Array<{ x: number; y: number; fullText: string; shortText: string }>;
|
|
|
+ paths: Array<{ d: string }>;
|
|
|
+ }> = [];
|
|
|
+
|
|
|
+ displayGoals.forEach((goal) => {
|
|
|
+ const anchorNode = nodeMap.get(goal.id);
|
|
|
+ if (!anchorNode) return;
|
|
|
+
|
|
|
+ const subGoals = flattenSubGoals(goal.sub_goals);
|
|
|
+ if (subGoals.length === 0) return;
|
|
|
+
|
|
|
+ const anchorX = anchorNode.x;
|
|
|
+ const anchorY = anchorNode.y;
|
|
|
+ const count = subGoals.length;
|
|
|
+ const sides = count + 2;
|
|
|
+ const radius = Math.max(90, 44 + count * 16 + Math.max(0, count - 4) * 6);
|
|
|
+ const centerX = anchorX;
|
|
|
+ const centerY = anchorY + radius;
|
|
|
+ const angleStep = (Math.PI * 2) / sides;
|
|
|
+
|
|
|
+ const nodes = subGoals.map((subGoal, index) => {
|
|
|
+ const angle = -Math.PI / 2 - angleStep * (index + 1);
|
|
|
+ const fullText = subGoal.description || subGoal.id;
|
|
|
+ return {
|
|
|
+ x: centerX + Math.cos(angle) * radius,
|
|
|
+ y: centerY + Math.sin(angle) * radius,
|
|
|
+ fullText,
|
|
|
+ shortText: truncateMiddle(fullText, 10),
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ const nodeHalfH = 5;
|
|
|
+ const paths = nodes.map((node) => ({
|
|
|
+ d: buildSegment(anchorX, anchorY + nodeHalfH, node.x, node.y),
|
|
|
+ }));
|
|
|
+
|
|
|
+ overlays.push({ parentId: goal.id, nodes, paths });
|
|
|
+ });
|
|
|
+
|
|
|
+ return overlays;
|
|
|
+ }, [displayGoals, layoutData]);
|
|
|
+
|
|
|
// 当前选中节点的消息链
|
|
|
const selectedMessages = useMemo(() => {
|
|
|
if (selectedSubTrace) return subTraceMessages || [];
|
|
|
@@ -472,8 +551,9 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
|
|
|
<g transform={`translate(${layoutData.margin.left},${layoutData.margin.top})`}>
|
|
|
{(() => {
|
|
|
const visibleNodes = layoutData.nodes.filter((n) => n.data.id !== VIRTUAL_ROOT_ID);
|
|
|
- const xs = visibleNodes.map((n) => n.x);
|
|
|
- const ys = visibleNodes.map((n) => n.y);
|
|
|
+ const overlayPoints = subGoalsOverlay.flatMap((overlay) => overlay.nodes);
|
|
|
+ const xs = [...visibleNodes.map((n) => n.x), ...overlayPoints.map((n) => n.x)];
|
|
|
+ const ys = [...visibleNodes.map((n) => n.y), ...overlayPoints.map((n) => n.y)];
|
|
|
const nodeHalfW = 70;
|
|
|
const nodeHalfH = 26;
|
|
|
const minX = Math.min(...xs) - nodeHalfW;
|
|
|
@@ -584,6 +664,45 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
|
|
|
))}
|
|
|
</g>
|
|
|
)}
|
|
|
+ {subGoalsOverlay.length > 0 && (
|
|
|
+ <g>
|
|
|
+ {subGoalsOverlay.flatMap((overlay) =>
|
|
|
+ overlay.paths.map((p, idx) => (
|
|
|
+ <path
|
|
|
+ key={`subgoals-path-${overlay.parentId}-${idx}`}
|
|
|
+ d={p.d}
|
|
|
+ fill="none"
|
|
|
+ stroke="#7aa0d6"
|
|
|
+ strokeWidth={1.5}
|
|
|
+ strokeDasharray="6,6"
|
|
|
+ markerEnd="url(#arrow-default)"
|
|
|
+ opacity={0.7}
|
|
|
+ strokeLinecap="round"
|
|
|
+ />
|
|
|
+ )),
|
|
|
+ )}
|
|
|
+ {subGoalsOverlay.flatMap((overlay) =>
|
|
|
+ overlay.nodes.map((node, idx) => (
|
|
|
+ <g
|
|
|
+ key={`subgoals-node-${overlay.parentId}-${idx}`}
|
|
|
+ transform={`translate(${node.x},${node.y})`}
|
|
|
+ >
|
|
|
+ <Tooltip content={node.fullText}>
|
|
|
+ <text
|
|
|
+ x={0}
|
|
|
+ y={16}
|
|
|
+ fontSize={12}
|
|
|
+ fill="#333"
|
|
|
+ textAnchor="middle"
|
|
|
+ >
|
|
|
+ {node.shortText}
|
|
|
+ </text>
|
|
|
+ </Tooltip>
|
|
|
+ </g>
|
|
|
+ )),
|
|
|
+ )}
|
|
|
+ </g>
|
|
|
+ )}
|
|
|
{messageOverlay && (
|
|
|
<g>
|
|
|
{messageOverlay.paths.map((p, idx) => (
|