'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 FunnelNode from './nodes/FunnelNode' import type { FlowNode, FlowEdge, FunnelNodeData } from '@/lib/types' const nodeTypes = { funnel: FunnelNode, } // Color palette for allocations const COLORS = ['#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981', '#06b6d4'] const initialNodes: FlowNode[] = [ { id: 'treasury', type: 'funnel', position: { x: 400, y: 0 }, data: { label: 'Treasury', currentValue: 85000, minThreshold: 20000, maxThreshold: 70000, maxCapacity: 100000, inflowRate: 1000, outflowAllocations: [ { targetId: 'public-goods', percentage: 40, color: COLORS[0] }, { targetId: 'research', percentage: 35, color: COLORS[1] }, { targetId: 'emergency', percentage: 25, color: COLORS[2] }, ], }, }, { id: 'public-goods', type: 'funnel', position: { x: 100, y: 350 }, data: { label: 'Public Goods', currentValue: 45000, minThreshold: 15000, maxThreshold: 50000, maxCapacity: 70000, inflowRate: 400, outflowAllocations: [ { targetId: 'project-alpha', percentage: 60, color: COLORS[0] }, { targetId: 'project-beta', percentage: 40, color: COLORS[1] }, ], }, }, { id: 'research', type: 'funnel', position: { x: 400, y: 350 }, data: { label: 'Research', currentValue: 28000, minThreshold: 20000, maxThreshold: 45000, maxCapacity: 60000, inflowRate: 350, outflowAllocations: [ { targetId: 'project-gamma', percentage: 70, color: COLORS[0] }, { targetId: 'project-beta', percentage: 30, color: COLORS[1] }, ], }, }, { id: 'emergency', type: 'funnel', position: { x: 700, y: 350 }, data: { label: 'Emergency', currentValue: 12000, minThreshold: 25000, maxThreshold: 60000, maxCapacity: 80000, inflowRate: 250, outflowAllocations: [ { targetId: 'reserve', percentage: 100, color: COLORS[0] }, ], }, }, { id: 'project-alpha', type: 'funnel', position: { x: 0, y: 700 }, data: { label: 'Project Alpha', currentValue: 18000, minThreshold: 10000, maxThreshold: 30000, maxCapacity: 40000, inflowRate: 240, outflowAllocations: [], }, }, { id: 'project-beta', type: 'funnel', position: { x: 300, y: 700 }, data: { label: 'Project Beta', currentValue: 22000, minThreshold: 15000, maxThreshold: 35000, maxCapacity: 45000, inflowRate: 265, outflowAllocations: [], }, }, { id: 'project-gamma', type: 'funnel', position: { x: 600, y: 700 }, data: { label: 'Project Gamma', currentValue: 8000, minThreshold: 12000, maxThreshold: 28000, maxCapacity: 35000, inflowRate: 245, outflowAllocations: [], }, }, { id: 'reserve', type: 'funnel', position: { x: 900, y: 700 }, data: { label: 'Reserve', currentValue: 5000, minThreshold: 20000, maxThreshold: 50000, maxCapacity: 60000, inflowRate: 250, outflowAllocations: [], }, }, ] // Generate edges from node allocations with proportional thickness function generateEdges(nodes: FlowNode[]): FlowEdge[] { const edges: FlowEdge[] = [] const maxAllocation = 100 // Max percentage for scaling nodes.forEach((node) => { const data = node.data as FunnelNodeData data.outflowAllocations.forEach((alloc) => { // Calculate stroke width: min 2px, max 12px based on percentage const strokeWidth = 2 + (alloc.percentage / maxAllocation) * 10 edges.push({ id: `e-${node.id}-${alloc.targetId}`, source: node.id, target: alloc.targetId, animated: true, style: { stroke: alloc.color, strokeWidth, opacity: 0.8, }, markerEnd: { type: MarkerType.ArrowClosed, color: alloc.color, width: 15 + alloc.percentage / 10, height: 15 + alloc.percentage / 10, }, data: { allocation: alloc.percentage, color: alloc.color, }, }) }) }) return edges } export default function FlowCanvas() { const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes) const [edges, setEdges, onEdgesChange] = useEdgesState(generateEdges(initialNodes)) const [isSimulating, setIsSimulating] = useState(true) const onConnect = useCallback( (params: Connection) => setEdges((eds) => addEdge( { ...params, animated: true, style: { stroke: '#64748b', strokeWidth: 4 }, markerEnd: { type: MarkerType.ArrowClosed, color: '#64748b' }, }, eds ) ), [setEdges] ) // Simulation effect useEffect(() => { if (!isSimulating) return const interval = setInterval(() => { setNodes((nds) => nds.map((node) => { const data = node.data as FunnelNodeData // Random walk for demo const change = (Math.random() - 0.45) * 300 return { ...node, data: { ...data, currentValue: Math.max(0, Math.min(data.maxCapacity * 1.1, data.currentValue + change)), }, } }) ) }, 500) return () => clearInterval(interval) }, [isSimulating, setNodes]) // Regenerate edges when nodes change (to update proportions if needed) useEffect(() => { setEdges(generateEdges(nodes)) }, [nodes, setEdges]) return (
{/* Title Panel */}

Threshold-Based Flow Funding

Drag min/max handles • Line thickness = allocation %

{/* Simulation Toggle */} {/* Legend */}
Funnel Zones
Overflow (above MAX)
Healthy (MIN to MAX)
Critical (below MIN)
Flow Lines
Thin = small allocation
Thick = large allocation
) }