"use client" import { useEffect, useRef, useState } from "react" import Link from "next/link" import type { FlowNetwork, FlowAllocation, FlowParticle } from "@/lib/tbff-flow/types" import { renderFlowNetwork } from "@/lib/tbff-flow/rendering" import { flowSampleNetworks, flowNetworkOptions, getFlowSampleNetwork, } from "@/lib/tbff-flow/sample-networks" import { formatFlow, getFlowStatusColorClass, normalizeFlowAllocations, calculateFlowNetworkTotals, updateFlowNodeProperties, } from "@/lib/tbff-flow/utils" import { propagateFlow, updateFlowParticles } from "@/lib/tbff-flow/algorithms" type Tool = 'select' | 'create-allocation' export default function TBFFFlowPage() { const canvasRef = useRef(null) const animationFrameRef = useRef(null) const [network, setNetwork] = useState(flowSampleNetworks.linear) const [particles, setParticles] = useState([]) const [selectedNodeId, setSelectedNodeId] = useState(null) const [selectedAllocationId, setSelectedAllocationId] = useState(null) const [selectedNetworkKey, setSelectedNetworkKey] = useState('linear') const [tool, setTool] = useState('select') const [allocationSourceId, setAllocationSourceId] = useState(null) const [isAnimating, setIsAnimating] = useState(true) // Animation loop useEffect(() => { if (!isAnimating) return const animate = () => { // Update particles setParticles(prev => updateFlowParticles(prev)) animationFrameRef.current = requestAnimationFrame(animate) } animationFrameRef.current = requestAnimationFrame(animate) return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current) } } }, [isAnimating]) // Render canvas useEffect(() => { const canvas = canvasRef.current if (!canvas) return const ctx = canvas.getContext("2d") if (!ctx) return // Set canvas size canvas.width = canvas.offsetWidth canvas.height = canvas.offsetHeight // Render the network renderFlowNetwork( ctx, network, canvas.width, canvas.height, particles, selectedNodeId, selectedAllocationId ) }, [network, particles, selectedNodeId, selectedAllocationId]) // Propagate flow when network changes const handlePropagateFlow = () => { const result = propagateFlow(network) setNetwork(result.network) setParticles(result.particles) } // Set external flow for selected node const handleSetNodeFlow = (nodeId: string, flow: number) => { const updatedNodes = network.nodes.map(node => node.id === nodeId ? { ...node, externalFlow: Math.max(0, flow) } : node ) const updatedNetwork = { ...network, nodes: updatedNodes, } setNetwork(updatedNetwork) // Auto-propagate setTimeout(() => { const result = propagateFlow(updatedNetwork) setNetwork(result.network) setParticles(result.particles) }, 100) } // Handle canvas click const handleCanvasClick = (e: React.MouseEvent) => { const canvas = canvasRef.current if (!canvas) return const rect = canvas.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top // Find clicked node const clickedNode = network.nodes.find( node => x >= node.x && x <= node.x + node.width && y >= node.y && y <= node.y + node.height ) if (tool === 'select') { if (clickedNode) { setSelectedNodeId(clickedNode.id) setSelectedAllocationId(null) } else { // Check if clicked on allocation const clickedAllocation = findAllocationAtPoint(x, y) if (clickedAllocation) { setSelectedAllocationId(clickedAllocation.id) setSelectedNodeId(null) } else { // Deselect all setSelectedNodeId(null) setSelectedAllocationId(null) } } } else if (tool === 'create-allocation') { if (clickedNode) { if (!allocationSourceId) { // First click: set source setAllocationSourceId(clickedNode.id) } else { // Second click: create allocation if (clickedNode.id !== allocationSourceId) { createAllocation(allocationSourceId, clickedNode.id) } setAllocationSourceId(null) } } } } // Find allocation at point const findAllocationAtPoint = (x: number, y: number): FlowAllocation | null => { const tolerance = 15 for (const allocation of network.allocations) { const sourceNode = network.nodes.find(n => n.id === allocation.sourceNodeId) const targetNode = network.nodes.find(n => n.id === allocation.targetNodeId) if (!sourceNode || !targetNode) continue const sourceCenter = { x: sourceNode.x + sourceNode.width / 2, y: sourceNode.y + sourceNode.height / 2, } const targetCenter = { x: targetNode.x + targetNode.width / 2, y: targetNode.y + targetNode.height / 2, } const distance = pointToLineDistance( x, y, sourceCenter.x, sourceCenter.y, targetCenter.x, targetCenter.y ) if (distance < tolerance) { return allocation } } return null } // Point to line distance const pointToLineDistance = ( px: number, py: number, x1: number, y1: number, x2: number, y2: number ): number => { const dot = (px - x1) * (x2 - x1) + (py - y1) * (y2 - y1) const lenSq = (x2 - x1) ** 2 + (y2 - y1) ** 2 const param = lenSq !== 0 ? dot / lenSq : -1 let xx, yy if (param < 0) { [xx, yy] = [x1, y1] } else if (param > 1) { [xx, yy] = [x2, y2] } else { xx = x1 + param * (x2 - x1) yy = y1 + param * (y2 - y1) } return Math.sqrt((px - xx) ** 2 + (py - yy) ** 2) } // Create allocation const createAllocation = (sourceId: string, targetId: string) => { const newAllocation: FlowAllocation = { id: `alloc_${Date.now()}`, sourceNodeId: sourceId, targetNodeId: targetId, percentage: 0.5, } const updatedAllocations = [...network.allocations, newAllocation] const sourceAllocations = updatedAllocations.filter(a => a.sourceNodeId === sourceId) const normalized = normalizeFlowAllocations(sourceAllocations) const finalAllocations = updatedAllocations.map(a => { const normalizedVersion = normalized.find(n => n.id === a.id) return normalizedVersion || a }) const updatedNetwork = { ...network, allocations: finalAllocations, } setNetwork(updatedNetwork) setSelectedAllocationId(newAllocation.id) setTool('select') // Re-propagate setTimeout(() => handlePropagateFlow(), 100) } // Update allocation percentage const updateAllocationPercentage = (allocationId: string, newPercentage: number) => { const allocation = network.allocations.find(a => a.id === allocationId) if (!allocation) return const updatedAllocations = network.allocations.map(a => a.id === allocationId ? { ...a, percentage: Math.max(0, Math.min(1, newPercentage)) } : a ) const sourceAllocations = updatedAllocations.filter( a => a.sourceNodeId === allocation.sourceNodeId ) const normalized = normalizeFlowAllocations(sourceAllocations) const finalAllocations = updatedAllocations.map(a => { const normalizedVersion = normalized.find(n => n.id === a.id) return normalizedVersion || a }) const updatedNetwork = { ...network, allocations: finalAllocations, } setNetwork(updatedNetwork) // Re-propagate setTimeout(() => { const result = propagateFlow(updatedNetwork) setNetwork(result.network) setParticles(result.particles) }, 100) } // Delete allocation const deleteAllocation = (allocationId: string) => { const allocation = network.allocations.find(a => a.id === allocationId) if (!allocation) return const updatedAllocations = network.allocations.filter(a => a.id !== allocationId) const sourceAllocations = updatedAllocations.filter( a => a.sourceNodeId === allocation.sourceNodeId ) const normalized = normalizeFlowAllocations(sourceAllocations) const finalAllocations = updatedAllocations.map(a => { const normalizedVersion = normalized.find(n => n.id === a.id) return normalizedVersion || a }) const updatedNetwork = { ...network, allocations: finalAllocations, } setNetwork(updatedNetwork) setSelectedAllocationId(null) // Re-propagate setTimeout(() => handlePropagateFlow(), 100) } // Load network const handleLoadNetwork = (key: string) => { setSelectedNetworkKey(key) const newNetwork = getFlowSampleNetwork(key as keyof typeof flowSampleNetworks) setNetwork(newNetwork) setSelectedNodeId(null) setSelectedAllocationId(null) setAllocationSourceId(null) setTool('select') // Propagate initial flows setTimeout(() => handlePropagateFlow(), 100) } // Get selected details const selectedNode = selectedNodeId ? network.nodes.find(n => n.id === selectedNodeId) : null const selectedAllocation = selectedAllocationId ? network.allocations.find(a => a.id === selectedAllocationId) : null const outgoingAllocations = selectedNode ? network.allocations.filter(a => a.sourceNodeId === selectedNode.id) : [] const selectedAllocationSiblings = selectedAllocation ? network.allocations.filter(a => a.sourceNodeId === selectedAllocation.sourceNodeId) : [] // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { setTool('select') setAllocationSourceId(null) setSelectedNodeId(null) setSelectedAllocationId(null) } else if (e.key === 'Delete' && selectedAllocationId) { deleteAllocation(selectedAllocationId) } else if (e.key === ' ') { e.preventDefault() setIsAnimating(prev => !prev) } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [selectedAllocationId]) return (
{/* Header */}

Flow-Based Flow Funding

Resource circulation visualization

Stock Model → ← Home
{/* Main Content */}
{/* Canvas */}
{/* Tool indicator */} {allocationSourceId && (
Click target node to create allocation
)} {/* Animation status */}
{isAnimating ? '▶️ Animating' : '⏸️ Paused'} (Space to toggle)
{/* Sidebar */}
{/* Tools */}

Tools

{/* Network Selector */}

Select Network

{/* Network Info */}

{network.name}

Nodes: {network.nodes.filter(n => !n.isOverflowSink).length}
Allocations: {network.allocations.length}
Total Inflow: {formatFlow(network.totalInflow)}
Total Absorbed: {formatFlow(network.totalAbsorbed)}
Total Outflow: {formatFlow(network.totalOutflow)}
{/* Set Flow Input */} {selectedNode && !selectedNode.isOverflowSink && (

💧 Set Flow Input

handleSetNodeFlow(selectedNode.id, parseFloat(e.target.value) || 0)} className="w-full px-3 py-2 bg-slate-700 rounded text-sm" min="0" step="10" />
Current inflow: {formatFlow(selectedNode.inflow)}
Absorbed: {formatFlow(selectedNode.absorbed)}
Outflow: {formatFlow(selectedNode.outflow)}
)} {/* Selected Allocation Editor */} {selectedAllocation && (

Edit Allocation

From: {network.nodes.find(n => n.id === selectedAllocation.sourceNodeId)?.name}
To: {network.nodes.find(n => n.id === selectedAllocation.targetNodeId)?.name}
{selectedAllocationSiblings.length === 1 ? (
Single allocation must be 100%.
) : ( updateAllocationPercentage( selectedAllocation.id, parseFloat(e.target.value) / 100 ) } className="w-full" /> )}
)} {/* Selected Node Details */} {selectedNode && (

Node Details

Name: {selectedNode.name}
Status: {selectedNode.status.toUpperCase()}
Inflow: {formatFlow(selectedNode.inflow)}
Absorbed: {formatFlow(selectedNode.absorbed)}
Outflow: {formatFlow(selectedNode.outflow)}
Min Absorption: {formatFlow(selectedNode.minAbsorption)}
Max Absorption: {formatFlow(selectedNode.maxAbsorption)}
{/* Outgoing Allocations */} {outgoingAllocations.length > 0 && (
Outgoing Allocations:
{outgoingAllocations.map((alloc) => { const target = network.nodes.find(n => n.id === alloc.targetNodeId) return (
setSelectedAllocationId(alloc.id)} > → {target?.name} {Math.round(alloc.percentage * 100)}%
) })}
)}
)} {/* Legend */}

Legend

Starved - Below minimum absorption
Minimum - At minimum absorption
Healthy - Between min and max
Saturated - At maximum capacity
Particle - Flow animation
{/* Instructions */}

Flow-Based Model

  • Click node to select and set flow
  • Use Create Arrow to draw allocations
  • Watch flows propagate in real-time
  • Press Space to pause/play animation
  • Overflow sink appears automatically if needed
) }