Spaces:
Paused
Paused
| /** | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * β 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<string, { prefix: string; suffix: string }> = { | |
| 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<string, string> = { | |
| 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<string>(); | |
| 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 ( | |
| <div className={cn('flowchart-view', className)}> | |
| <MermaidDiagram | |
| code={mermaidCode} | |
| title={title} | |
| theme="dark" | |
| zoomable={true} | |
| onRender={onRender} | |
| /> | |
| </div> | |
| ); | |
| } | |
| export default FlowchartView; | |