'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 OutcomeNode from './nodes/OutcomeNode' import type { FlowNode, FlowEdge, FunnelNodeData, OutcomeNodeData } from '@/lib/types' const nodeTypes = { funnel: FunnelNode, outcome: OutcomeNode, } // Colors for allocations const SPENDING_COLORS = ['#3b82f6', '#8b5cf6', '#ec4899', '#06b6d4', '#10b981', '#6366f1'] const OVERFLOW_COLORS = ['#f59e0b', '#ef4444', '#f97316', '#eab308', '#dc2626', '#ea580c'] const initialNodes: FlowNode[] = [ // Main Treasury Funnel { id: 'treasury', type: 'funnel', position: { x: 400, y: 0 }, data: { label: 'Treasury', currentValue: 85000, minThreshold: 20000, maxThreshold: 70000, maxCapacity: 100000, inflowRate: 1000, // Overflow goes SIDEWAYS to other funnels overflowAllocations: [ { targetId: 'public-goods', percentage: 40, color: OVERFLOW_COLORS[0] }, { targetId: 'research', percentage: 35, color: OVERFLOW_COLORS[1] }, { targetId: 'emergency', percentage: 25, color: OVERFLOW_COLORS[2] }, ], // Spending goes DOWN to outcomes spendingAllocations: [ { targetId: 'treasury-ops', percentage: 60, color: SPENDING_COLORS[0] }, { targetId: 'treasury-audit', percentage: 40, color: SPENDING_COLORS[1] }, ], } as FunnelNodeData, }, // Sub-funnels (receive overflow from Treasury) { id: 'public-goods', type: 'funnel', position: { x: 50, y: 300 }, data: { label: 'Public Goods', currentValue: 45000, minThreshold: 15000, maxThreshold: 50000, maxCapacity: 70000, inflowRate: 400, overflowAllocations: [], spendingAllocations: [ { targetId: 'pg-infra', percentage: 50, color: SPENDING_COLORS[0] }, { targetId: 'pg-education', percentage: 30, color: SPENDING_COLORS[1] }, { targetId: 'pg-tooling', percentage: 20, color: SPENDING_COLORS[2] }, ], } as FunnelNodeData, }, { id: 'research', type: 'funnel', position: { x: 400, y: 300 }, data: { label: 'Research', currentValue: 28000, minThreshold: 20000, maxThreshold: 45000, maxCapacity: 60000, inflowRate: 350, overflowAllocations: [], spendingAllocations: [ { targetId: 'research-grants', percentage: 70, color: SPENDING_COLORS[0] }, { targetId: 'research-papers', percentage: 30, color: SPENDING_COLORS[1] }, ], } as FunnelNodeData, }, { id: 'emergency', type: 'funnel', position: { x: 750, y: 300 }, data: { label: 'Emergency Fund', currentValue: 12000, minThreshold: 25000, maxThreshold: 60000, maxCapacity: 80000, inflowRate: 250, overflowAllocations: [], spendingAllocations: [ { targetId: 'emergency-response', percentage: 100, color: SPENDING_COLORS[0] }, ], } as FunnelNodeData, }, // Outcome nodes (receive spending from funnels) { id: 'treasury-ops', type: 'outcome', position: { x: 350, y: 600 }, data: { label: 'Treasury Operations', description: 'Day-to-day treasury management and reporting', fundingReceived: 15000, fundingTarget: 25000, status: 'in-progress', } as OutcomeNodeData, }, { id: 'treasury-audit', type: 'outcome', position: { x: 550, y: 600 }, data: { label: 'Annual Audit', description: 'Third-party financial audit and compliance', fundingReceived: 8000, fundingTarget: 15000, status: 'in-progress', } as OutcomeNodeData, }, { id: 'pg-infra', type: 'outcome', position: { x: -50, y: 600 }, data: { label: 'Infrastructure', description: 'Core infrastructure development and maintenance', fundingReceived: 22000, fundingTarget: 30000, status: 'in-progress', } as OutcomeNodeData, }, { id: 'pg-education', type: 'outcome', position: { x: 100, y: 700 }, data: { label: 'Education Programs', description: 'Developer education and onboarding materials', fundingReceived: 12000, fundingTarget: 20000, status: 'in-progress', } as OutcomeNodeData, }, { id: 'pg-tooling', type: 'outcome', position: { x: 250, y: 600 }, data: { label: 'Dev Tooling', description: 'Open-source developer tools and SDKs', fundingReceived: 5000, fundingTarget: 15000, status: 'not-started', } as OutcomeNodeData, }, { id: 'research-grants', type: 'outcome', position: { x: 400, y: 600 }, data: { label: 'Research Grants', description: 'Academic research grants for protocol improvements', fundingReceived: 18000, fundingTarget: 25000, status: 'in-progress', } as OutcomeNodeData, }, { id: 'research-papers', type: 'outcome', position: { x: 500, y: 700 }, data: { label: 'Published Papers', description: 'Peer-reviewed research publications', fundingReceived: 8000, fundingTarget: 10000, status: 'in-progress', } as OutcomeNodeData, }, { id: 'emergency-response', type: 'outcome', position: { x: 750, y: 600 }, data: { label: 'Emergency Response', description: 'Rapid response fund for critical issues', fundingReceived: 5000, fundingTarget: 50000, status: 'not-started', } as OutcomeNodeData, }, ] // Generate edges from node allocations function generateEdges(nodes: FlowNode[]): FlowEdge[] { const edges: FlowEdge[] = [] nodes.forEach((node) => { if (node.type !== 'funnel') return const data = node.data as FunnelNodeData // OVERFLOW edges - go SIDEWAYS to other funnels data.overflowAllocations?.forEach((alloc, idx) => { const strokeWidth = 2 + (alloc.percentage / 100) * 8 const isLeftSide = idx % 2 === 0 edges.push({ id: `overflow-${node.id}-${alloc.targetId}`, source: node.id, target: alloc.targetId, sourceHandle: isLeftSide ? 'overflow-left' : 'overflow-right', targetHandle: isLeftSide ? 'inflow-right' : 'inflow-left', animated: true, style: { stroke: alloc.color, strokeWidth, opacity: 0.8, }, markerEnd: { type: MarkerType.ArrowClosed, color: alloc.color, width: 12 + alloc.percentage / 10, height: 12 + alloc.percentage / 10, }, data: { allocation: alloc.percentage, color: alloc.color, edgeType: 'overflow' as const, }, type: 'smoothstep', }) }) // SPENDING edges - go DOWN to outcomes data.spendingAllocations?.forEach((alloc) => { const strokeWidth = 2 + (alloc.percentage / 100) * 8 edges.push({ id: `spending-${node.id}-${alloc.targetId}`, source: node.id, target: alloc.targetId, animated: true, style: { stroke: alloc.color, strokeWidth, opacity: 0.9, }, markerEnd: { type: MarkerType.ArrowClosed, color: alloc.color, width: 12 + alloc.percentage / 10, height: 12 + alloc.percentage / 10, }, data: { allocation: alloc.percentage, color: alloc.color, edgeType: 'spending' as const, }, }) }) }) 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 - update funnel values and outcome funding useEffect(() => { if (!isSimulating) return const interval = setInterval(() => { setNodes((nds) => nds.map((node) => { if (node.type === 'funnel') { const data = node.data as FunnelNodeData const change = (Math.random() - 0.45) * 300 return { ...node, data: { ...data, currentValue: Math.max(0, Math.min(data.maxCapacity * 1.1, data.currentValue + change)), }, } } else if (node.type === 'outcome') { const data = node.data as OutcomeNodeData const change = Math.random() * 100 const newReceived = Math.min(data.fundingTarget * 1.1, data.fundingReceived + change) return { ...node, data: { ...data, fundingReceived: newReceived, status: newReceived >= data.fundingTarget ? 'completed' : data.status === 'not-started' && newReceived > 0 ? 'in-progress' : data.status, }, } } return node }) ) }, 500) return () => clearInterval(interval) }, [isSimulating, setNodes]) // Regenerate edges when nodes change useEffect(() => { setEdges(generateEdges(nodes)) }, [nodes, setEdges]) return (
{/* Title Panel */}

Threshold-Based Flow Funding

Overflow → Funnels (sideways) • Spending → Outcomes (down)

Double-click funnels to edit • Drag thresholds to adjust

{/* Simulation Toggle */} {/* Legend */}
Flow Types
Overflow → Other Funnels
Spending → Outcomes
Funnel Status
Overflow (above MAX)
Healthy (MIN to MAX)
Critical (below MIN)
) }