"use client"; import { useEffect, useState } from "react"; import { motion } from "framer-motion"; import { getFlowForVersion } from "@/data/execution-flows"; import type { FlowNode, FlowEdge } from "@/types/agent-data"; const NODE_WIDTH = 140; const NODE_HEIGHT = 44; const DIAMOND_WIDTH = 92; const DIAMOND_HEIGHT = 64; const LAYER_COLORS: Record = { start: "#3B82F6", process: "#10B981", decision: "#F59E0B", subprocess: "#8B5CF6", end: "#EF4444", }; function getNodeLines(node: FlowNode): string[] { const maxChars = node.type === "decision" ? 12 : 18; return node.label.split("\n").flatMap((line) => { if (line.length <= maxChars) return [line]; const parts = line.split(/(\s+\/\s+|\s+|_)/).filter(Boolean); const chunks: string[] = []; let current = ""; for (const part of parts) { const next = `${current}${part}`; if (current && next.trim().length > maxChars) { chunks.push(current.trim()); current = part.trimStart(); } else { current = next; } } if (current.trim()) chunks.push(current.trim()); return chunks.length ? chunks : [line]; }); } function estimateTextWidth(line: string, fontSize: number): number { return line.length * fontSize * 0.62; } function getNodeMetrics(node: FlowNode) { const lines = getNodeLines(node); const longest = Math.max(...lines.map((line) => estimateTextWidth(line, 11)), 0); if (node.type === "decision") { return { lines, width: Math.max(DIAMOND_WIDTH, longest + 54), height: Math.max(DIAMOND_HEIGHT, lines.length * 15 + 42), }; } if (node.type === "start" || node.type === "end") { return { lines, width: Math.max(NODE_WIDTH, longest + 34), height: Math.max(NODE_HEIGHT, lines.length * 15 + 24), }; } return { lines, width: Math.max(NODE_WIDTH, longest + 30), height: Math.max(NODE_HEIGHT, lines.length * 15 + 24), }; } function getNodeBounds(node: FlowNode) { const metrics = getNodeMetrics(node); const halfW = metrics.width / 2; const halfH = metrics.height / 2; return { cx: node.x, cy: node.y, left: node.x - halfW, right: node.x + halfW, top: node.y - halfH, bottom: node.y + halfH, }; } const LOOP_RAIL_X = -48; const RIGHT_LOOP_RAIL_X = 576; const FLOW_CENTER_X = 300; const LOOP_PAD = 28; const LOOP_BACK_DX_LIMIT = 360; const LOOP_BACK_DY_LIMIT = 70; type LoopSide = "left" | "right"; function getLoopSide(start: ReturnType, end: ReturnType): LoopSide { return (start.cx + end.cx) / 2 > FLOW_CENTER_X ? "right" : "left"; } function getLoopRailX( start: ReturnType, end: ReturnType, side = getLoopSide(start, end), ) { if (side === "right") { return Math.max(RIGHT_LOOP_RAIL_X, start.right + LOOP_PAD, end.right + LOOP_PAD); } return Math.min(LOOP_RAIL_X, start.left - LOOP_PAD, end.left - LOOP_PAD); } function isLoopBack(start: ReturnType, end: ReturnType) { const dx = end.cx - start.cx; const dy = end.cy - start.cy; return dy < -LOOP_BACK_DY_LIMIT && Math.abs(dx) <= LOOP_BACK_DX_LIMIT; } function shouldUseStepRoute(start: ReturnType, end: ReturnType) { const dx = end.cx - start.cx; const dy = end.cy - start.cy; return dy > 28 && Math.abs(dx) > 44 && end.top > start.bottom; } function getStepBusY(start: ReturnType, end: ReturnType) { const room = end.top - start.bottom; return Math.min(end.top - 16, start.bottom + Math.max(18, room * 0.35)); } function getEdgePath(from: FlowNode, to: FlowNode): string { const start = getNodeBounds(from); const end = getNodeBounds(to); const dx = end.cx - start.cx; const dy = end.cy - start.cy; if (isLoopBack(start, end)) { const side = getLoopSide(start, end); const railX = getLoopRailX(start, end, side); const startX = side === "right" ? start.right : start.left; const endX = side === "right" ? end.right : end.left; const midY = (start.cy + end.cy) / 2; return `M ${startX} ${start.cy} C ${railX} ${start.cy}, ${railX} ${midY}, ${railX} ${midY} C ${railX} ${end.cy}, ${endX} ${end.cy}, ${endX} ${end.cy}`; } if (Math.abs(dx) < 10) { if (dy >= 0) { return `M ${start.cx} ${start.bottom} L ${end.cx} ${end.top}`; } return `M ${start.cx} ${start.top} L ${end.cx} ${end.bottom}`; } if (Math.abs(dy) < 10) { const startX = dx > 0 ? start.right : start.left; const endX = dx > 0 ? end.left : end.right; const midX = (startX + endX) / 2; return `M ${startX} ${start.cy} C ${midX} ${start.cy}, ${midX} ${end.cy}, ${endX} ${end.cy}`; } if (shouldUseStepRoute(start, end)) { const busY = getStepBusY(start, end); return `M ${start.cx} ${start.bottom} L ${start.cx} ${busY} L ${end.cx} ${busY} L ${end.cx} ${end.top}`; } if (Math.abs(dx) > 70) { const startX = dx > 0 ? start.right : start.left; const endX = dx > 0 ? end.left : end.right; const control = Math.max(56, Math.abs(dx) * 0.45); return `M ${startX} ${start.cy} C ${startX + (dx > 0 ? control : -control)} ${start.cy}, ${endX - (dx > 0 ? control : -control)} ${end.cy}, ${endX} ${end.cy}`; } const startY = dy > 0 ? start.bottom : start.top; const endY = dy > 0 ? end.top : end.bottom; const controlDistance = Math.max(44, Math.abs(endY - startY) * 0.42); const controlY1 = startY + (endY > startY ? controlDistance : -controlDistance); const controlY2 = endY - (endY > startY ? controlDistance : -controlDistance); return `M ${start.cx} ${startY} C ${start.cx} ${controlY1}, ${end.cx} ${controlY2}, ${end.cx} ${endY}`; } function getEdgeLabelPosition(from: FlowNode, to: FlowNode): { x: number; y: number } { const start = getNodeBounds(from); const end = getNodeBounds(to); const dx = end.cx - start.cx; const dy = end.cy - start.cy; if (isLoopBack(start, end)) { const side = getLoopSide(start, end); return { x: getLoopRailX(start, end, side) + (side === "right" ? -24 : 24), y: (start.cy + end.cy) / 2 - 6, }; } if (Math.abs(dy) < 10) { return { x: (start.cx + end.cx) / 2, y: start.cy - 12 }; } if (shouldUseStepRoute(start, end)) { return { x: (start.cx + end.cx) / 2, y: getStepBusY(start, end) - 8 }; } return { x: (start.cx + end.cx) / 2 + (dx > 0 ? 18 : -18), y: (start.bottom + end.top) / 2 - 8, }; } function NodeShape({ node }: { node: FlowNode }) { const color = LAYER_COLORS[node.type]; const { lines, width, height } = getNodeMetrics(node); if (node.type === "decision") { const halfW = width / 2; const halfH = height / 2; return ( {lines.map((line, i) => ( 2 ? 9 : 10} fontFamily="monospace" fill="currentColor" > {line} ))} ); } if (node.type === "start" || node.type === "end") { return ( {lines.map((line, i) => ( {line} ))} ); } const isSubprocess = node.type === "subprocess"; return ( {lines.map((line, i) => ( {line} ))} ); } function EdgePath({ edge, nodes, index, }: { edge: FlowEdge; nodes: FlowNode[]; index: number; }) { const from = nodes.find((n) => n.id === edge.from); const to = nodes.find((n) => n.id === edge.to); if (!from || !to) return null; const d = getEdgePath(from, to); const label = getEdgeLabelPosition(from, to); return ( {edge.label && ( {edge.label} )} ); } interface ExecutionFlowProps { version: string; } export function ExecutionFlow({ version }: ExecutionFlowProps) { const [flow, setFlow] = useState>(null); useEffect(() => { setFlow(getFlowForVersion(version)); }, [version]); if (!flow) { return (
Execution flow is not available for this lesson yet.
); } const bounds = flow.nodes.map(getNodeBounds); const minX = Math.min(-40, ...bounds.map((b) => b.left)) - 24; const maxX = Math.max(700, ...bounds.map((b) => b.right)) + 24; const maxY = Math.max(...bounds.map((b) => b.bottom)) + 50; return (
{flow.edges.map((edge, i) => ( ))} {flow.nodes.map((node, i) => ( ))}
); }