'use client' import { useCallback, useState, useEffect } from 'react' import { ReactFlow, Controls, Background, BackgroundVariant, useNodesState, useEdgesState, addEdge, Connection, MarkerType, Panel, } from '@xyflow/react' import '@xyflow/react/dist/style.css' import SourceNode from './nodes/SourceNode' import ThresholdNode from './nodes/ThresholdNode' import RecipientNode from './nodes/RecipientNode' import type { FlowNode, FlowEdge, SourceNodeData, ThresholdNodeData, RecipientNodeData } from '@/lib/types' const nodeTypes = { source: SourceNode, threshold: ThresholdNode, recipient: RecipientNode, } // Vertical layout - sources at top, recipients at bottom const initialNodes: FlowNode[] = [ // Top row - Source { id: 'source-1', type: 'source', position: { x: 350, y: 0 }, data: { label: 'Treasury', balance: 100000, flowRate: 1000, }, }, // Middle row - Threshold funnels { id: 'threshold-1', type: 'threshold', position: { x: 100, y: 200 }, data: { label: 'Public Goods', minThreshold: 15000, maxThreshold: 60000, currentValue: 72000, // Overflowing }, }, { id: 'threshold-2', type: 'threshold', position: { x: 400, y: 200 }, data: { label: 'Research', minThreshold: 20000, maxThreshold: 50000, currentValue: 35000, // Healthy }, }, { id: 'threshold-3', type: 'threshold', position: { x: 700, y: 200 }, data: { label: 'Emergency', minThreshold: 30000, maxThreshold: 80000, currentValue: 18000, // Critical }, }, // Bottom row - Recipients { id: 'recipient-1', type: 'recipient', position: { x: 50, y: 620 }, data: { label: 'Project Alpha', received: 24500, target: 30000, }, }, { id: 'recipient-2', type: 'recipient', position: { x: 300, y: 620 }, data: { label: 'Project Beta', received: 18000, target: 25000, }, }, { id: 'recipient-3', type: 'recipient', position: { x: 550, y: 620 }, data: { label: 'Research Lab', received: 12000, target: 40000, }, }, { id: 'recipient-4', type: 'recipient', position: { x: 800, y: 620 }, data: { label: 'Reserve Fund', received: 5000, target: 50000, }, }, ] const initialEdges: FlowEdge[] = [ // Source to thresholds (top to middle) { id: 'e-source-t1', source: 'source-1', target: 'threshold-1', animated: true, style: { stroke: '#3b82f6', strokeWidth: 3 }, markerEnd: { type: MarkerType.ArrowClosed, color: '#3b82f6' }, }, { id: 'e-source-t2', source: 'source-1', target: 'threshold-2', animated: true, style: { stroke: '#3b82f6', strokeWidth: 3 }, markerEnd: { type: MarkerType.ArrowClosed, color: '#3b82f6' }, }, { id: 'e-source-t3', source: 'source-1', target: 'threshold-3', animated: true, style: { stroke: '#3b82f6', strokeWidth: 3 }, markerEnd: { type: MarkerType.ArrowClosed, color: '#3b82f6' }, }, // Threshold to recipients (middle to bottom) { id: 'e-t1-r1', source: 'threshold-1', target: 'recipient-1', animated: true, style: { stroke: '#ec4899', strokeWidth: 2 }, markerEnd: { type: MarkerType.ArrowClosed, color: '#ec4899' }, }, { id: 'e-t1-r2', source: 'threshold-1', target: 'recipient-2', animated: true, style: { stroke: '#ec4899', strokeWidth: 2 }, markerEnd: { type: MarkerType.ArrowClosed, color: '#ec4899' }, }, { id: 'e-t2-r3', source: 'threshold-2', target: 'recipient-3', animated: true, style: { stroke: '#ec4899', strokeWidth: 2 }, markerEnd: { type: MarkerType.ArrowClosed, color: '#ec4899' }, }, { id: 'e-t3-r4', source: 'threshold-3', target: 'recipient-4', animated: true, style: { stroke: '#ec4899', strokeWidth: 2 }, markerEnd: { type: MarkerType.ArrowClosed, color: '#ec4899' }, }, // Overflow connections (side handles) - from overflowing funnel to neighbors { id: 'e-overflow-1', source: 'threshold-1', sourceHandle: 'overflow-right', target: 'threshold-2', animated: true, style: { stroke: '#f59e0b', strokeWidth: 2, strokeDasharray: '5 5' }, markerEnd: { type: MarkerType.ArrowClosed, color: '#f59e0b' }, }, ] export default function FlowCanvas() { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes) const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges) const [isSimulating, setIsSimulating] = useState(true) const onConnect = useCallback( (params: Connection) => setEdges((eds) => addEdge( { ...params, animated: true, style: { stroke: '#64748b', strokeWidth: 2 }, markerEnd: { type: MarkerType.ArrowClosed, color: '#64748b' }, }, eds ) ), [setEdges] ) // Simulation effect useEffect(() => { if (!isSimulating) return const interval = setInterval(() => { setNodes((nds) => nds.map((node) => { if (node.type === 'source') { const data = node.data as SourceNodeData return { ...node, data: { ...data, balance: Math.max(0, data.balance - data.flowRate / 3600), }, } } if (node.type === 'threshold') { const data = node.data as ThresholdNodeData // Random walk for demo const change = (Math.random() - 0.4) * 200 return { ...node, data: { ...data, currentValue: Math.max(0, Math.min(100000, data.currentValue + change)), }, } } if (node.type === 'recipient') { const data = node.data as RecipientNodeData if (data.received < data.target) { return { ...node, data: { ...data, received: Math.min(data.target, data.received + Math.random() * 20), }, } } } return node }) ) }, 500) return () => clearInterval(interval) }, [isSimulating, setNodes]) return (
Funds flow top→bottom through funnel thresholds