/** * ╔═══════════════════════════════════════════════════════════════════════════╗ * ║ FLOWCHART VIEW COMPONENT ║ * ║═══════════════════════════════════════════════════════════════════════════║ * ║ Business process and decision flow visualization ║ * ║ Part of the Visual Cortex Layer ║ * ╚═══════════════════════════════════════════════════════════════════════════╝ */ import { useMemo } from 'react'; import { MermaidDiagram } from './MermaidDiagram'; import { cn } from '@/lib/utils'; export interface FlowNode { id: string; label: string; type?: 'start' | 'end' | 'process' | 'decision' | 'data' | 'subprocess' | 'custom'; style?: string; } export interface FlowEdge { from: string; to: string; label?: string; style?: 'solid' | 'dashed' | 'thick'; } export interface FlowSubgraph { id: string; label: string; nodes: string[]; direction?: 'TB' | 'LR'; } export interface FlowchartViewProps { /** Flowchart title */ title?: string; /** Nodes in the flowchart */ nodes: FlowNode[]; /** Edges connecting nodes */ edges: FlowEdge[]; /** Optional subgraphs for grouping */ subgraphs?: FlowSubgraph[]; /** Flow direction: TB (top-bottom), LR (left-right), BT, RL */ direction?: 'TB' | 'LR' | 'BT' | 'RL'; /** Custom class */ className?: string; /** Callback when rendered */ onRender?: (svg: string) => void; } // Node type to Mermaid shape const NODE_SHAPES: Record = { start: { prefix: '([', suffix: '])' }, // Stadium (rounded) end: { prefix: '([', suffix: '])' }, // Stadium process: { prefix: '[', suffix: ']' }, // Rectangle decision: { prefix: '{', suffix: '}' }, // Diamond data: { prefix: '[/', suffix: '/]' }, // Parallelogram subprocess: { prefix: '[[', suffix: ']]' }, // Subroutine custom: { prefix: '[', suffix: ']' }, // Rectangle }; // Edge style to arrow const EDGE_STYLES: Record = { solid: '-->', dashed: '-.->', thick: '==>', }; export function FlowchartView({ title, nodes, edges, subgraphs = [], direction = 'TB', className, onRender, }: FlowchartViewProps) { const mermaidCode = useMemo(() => { const lines: string[] = [`flowchart ${direction}`]; // Track nodes in subgraphs const nodesInSubgraphs = new Set(); subgraphs.forEach(sg => sg.nodes.forEach(n => nodesInSubgraphs.add(n))); // Generate subgraphs subgraphs.forEach(sg => { const subDir = sg.direction ? ` direction ${sg.direction}` : ''; lines.push(` subgraph ${sg.id}["${sg.label}"]${subDir}`); sg.nodes.forEach(nodeId => { const node = nodes.find(n => n.id === nodeId); if (node) { const shape = NODE_SHAPES[node.type || 'process']; lines.push(` ${node.id}${shape.prefix}"${node.label}"${shape.suffix}`); } }); lines.push(' end'); }); // Generate standalone nodes nodes.forEach(node => { if (!nodesInSubgraphs.has(node.id)) { const shape = NODE_SHAPES[node.type || 'process']; lines.push(` ${node.id}${shape.prefix}"${node.label}"${shape.suffix}`); } }); // Generate edges edges.forEach(edge => { const arrow = EDGE_STYLES[edge.style || 'solid']; if (edge.label) { lines.push(` ${edge.from} ${arrow}|"${edge.label}"| ${edge.to}`); } else { lines.push(` ${edge.from} ${arrow} ${edge.to}`); } }); // Add styling lines.push(''); lines.push(' %% Styling'); // Style start/end nodes const startNodes = nodes.filter(n => n.type === 'start').map(n => n.id); const endNodes = nodes.filter(n => n.type === 'end').map(n => n.id); const decisionNodes = nodes.filter(n => n.type === 'decision').map(n => n.id); if (startNodes.length > 0) { lines.push(` style ${startNodes.join(',')} fill:#22c55e,stroke:#16a34a,color:#fff`); } if (endNodes.length > 0) { lines.push(` style ${endNodes.join(',')} fill:#ef4444,stroke:#dc2626,color:#fff`); } if (decisionNodes.length > 0) { lines.push(` style ${decisionNodes.join(',')} fill:#f59e0b,stroke:#d97706,color:#000`); } return lines.join('\n'); }, [nodes, edges, subgraphs, direction]); return (
); } export default FlowchartView;