'use client' /** * Flow Funding Demo - Interactive Mode * * Enhanced with: * - Animated flow particles * - Distribution timeline * - Auto-play with speed controls * - INTERACTIVE: Click accounts to add targeted funding */ import { useState, useEffect, useRef } from 'react' import { cloneAccounts, runDistribution, } from '@/lib/flow-funding/engine' import { runTargetedDistribution } from '@/lib/flow-funding/targeted' import type { Account, DistributionResult } from '@/lib/flow-funding/types' import { getAccountState } from '@/lib/flow-funding/types' import { ALL_SCENARIOS, getScenario, type Scenario, } from '@/lib/flow-funding/scenarios' // Flow particle for animation interface FlowParticle { id: string fromX: number fromY: number toX: number toY: number progress: number // 0 to 1 amount: number color: string } type FundingMode = 'global' | 'interactive' export default function FlowFundingPage() { const [selectedScenarioId, setSelectedScenarioId] = useState( 'mutual-aid-circle' ) const [funding, setFunding] = useState(1500) const [targetedAmount, setTargetedAmount] = useState(500) const [result, setResult] = useState(null) const [currentIteration, setCurrentIteration] = useState(0) const [animationSpeed, setAnimationSpeed] = useState(1) const [particles, setParticles] = useState([]) const [autoPlay, setAutoPlay] = useState(false) const [fundingMode, setFundingMode] = useState('interactive') // Interactive mode state const [selectedAccountId, setSelectedAccountId] = useState(null) const [currentAccounts, setCurrentAccounts] = useState([]) const scenario = getScenario(selectedScenarioId) const animationFrameRef = useRef() // Initialize current accounts when scenario changes useEffect(() => { if (scenario) { setCurrentAccounts(cloneAccounts(scenario.accounts)) setSelectedAccountId(null) } }, [selectedScenarioId]) // Auto-play through iterations useEffect(() => { if (!autoPlay || !result) return const interval = setInterval(() => { setCurrentIteration(prev => { if (prev >= result.iterations.length - 1) { setAutoPlay(false) return prev } return prev + 1 }) }, 1000 / animationSpeed) return () => clearInterval(interval) }, [autoPlay, result, animationSpeed]) // Animate particles when iteration changes useEffect(() => { if (!result || !scenario) return const iteration = result.iterations[currentIteration] if (!iteration || iteration.flows.size === 0) { setParticles([]) return } // Create particles for each flow const newParticles: FlowParticle[] = [] let particleId = 0 iteration.flows.forEach((amount, flowKey) => { const [sourceId, targetId] = flowKey.split('->') const sourcePos = scenario.layout.get(sourceId) const targetPos = scenario.layout.get(targetId) if (!sourcePos || !targetPos || amount <= 0) return const numParticles = Math.min(3, Math.max(1, Math.floor(amount / 100))) for (let i = 0; i < numParticles; i++) { newParticles.push({ id: `${flowKey}-${particleId++}`, fromX: sourcePos.x, fromY: sourcePos.y, toX: targetPos.x, toY: targetPos.y, progress: i * (1 / numParticles), amount: amount / numParticles, color: '#60a5fa', }) } }) setParticles(newParticles) // Animate particles let startTime: number | null = null const duration = 1500 / animationSpeed const animate = (timestamp: number) => { if (!startTime) startTime = timestamp const elapsed = timestamp - startTime const progress = Math.min(elapsed / duration, 1) setParticles(prev => prev.map(p => ({ ...p, progress: Math.min(p.progress + progress, 1), })) ) if (progress < 1) { animationFrameRef.current = requestAnimationFrame(animate) } else { setParticles([]) } } animationFrameRef.current = requestAnimationFrame(animate) return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current) } } }, [currentIteration, result, scenario, animationSpeed]) const handleScenarioChange = (scenarioId: string) => { setSelectedScenarioId(scenarioId) const newScenario = getScenario(scenarioId) if (newScenario) { setFunding(newScenario.suggestedFunding) setCurrentAccounts(cloneAccounts(newScenario.accounts)) } setResult(null) setCurrentIteration(0) setAutoPlay(false) setParticles([]) setSelectedAccountId(null) } const handleDistribute = () => { if (!scenario) return const accounts = cloneAccounts(scenario.accounts) const distributionResult = runDistribution(accounts, funding, { verbose: true, maxIterations: 100, epsilon: 0.01, }) setCurrentAccounts(accounts) setResult(distributionResult) setCurrentIteration(0) setAutoPlay(true) console.log('Distribution complete:', distributionResult) } const handleAddTargetedFunding = () => { if (!selectedAccountId) return // Add funding to selected account const accounts = cloneAccounts(currentAccounts) const targetAccount = accounts.find(a => a.id === selectedAccountId) if (!targetAccount) return targetAccount.balance += targetedAmount console.log(`\nšŸ’° Adding $${targetedAmount} to ${targetAccount.name}`) console.log(`New balance: $${targetAccount.balance.toFixed(2)}`) // Run targeted distribution const distributionResult = runTargetedDistribution(accounts, { verbose: true, maxIterations: 100, epsilon: 0.01, }) setCurrentAccounts(accounts) setResult(distributionResult) setCurrentIteration(0) setAutoPlay(true) } const handleAccountClick = (accountId: string) => { if (fundingMode === 'interactive') { setSelectedAccountId(accountId) } } const handleReset = () => { if (scenario) { setCurrentAccounts(cloneAccounts(scenario.accounts)) } setResult(null) setCurrentIteration(0) setAutoPlay(false) setParticles([]) setSelectedAccountId(null) } const handleTimelineClick = (iteration: number) => { setCurrentIteration(iteration) setAutoPlay(false) } const handleNextStep = () => { if (!result || currentIteration >= result.iterations.length - 1) return setCurrentIteration(prev => prev + 1) } const handlePrevStep = () => { if (currentIteration <= 0) return setCurrentIteration(prev => prev - 1) } // Get current state for visualization const getCurrentBalances = (): Map => { if (!result) { return new Map(currentAccounts.map(a => [a.id, a.balance])) } return result.iterations[currentIteration]?.balances || result.finalBalances } const getCurrentOverflows = (): Map => { if (!result || currentIteration < 0) { return new Map() } return result.iterations[currentIteration]?.overflows || new Map() } return (
{/* Header */}

Flow Funding Demo

{fundingMode === 'interactive' ? 'šŸ’” Click any account to add funding and watch it propagate through the network' : 'Watch resources flow through the network. Thresholds create circulation.' }

{/* Left Panel: Controls */}
{/* Mode Selector */}

Funding Mode

{fundingMode === 'interactive' ? 'Click accounts to add funding directly' : 'Distribute funding equally to all accounts' }

{/* Scenario Selector */}

Select Scenario

{scenario && (

{scenario.description}

)}
{/* Funding Controls */} {fundingMode === 'global' ? (

Global Funding

setFunding(Number(e.target.value))} className="w-full bg-slate-900 border border-slate-600 rounded-lg px-4 py-3 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" min="0" step="100" />
) : (

Interactive Funding

{selectedAccountId ? ( <>
Selected Account
{currentAccounts.find(a => a.id === selectedAccountId)?.name}
Current: ${currentAccounts.find(a => a.id === selectedAccountId)?.balance.toFixed(0)}
setTargetedAmount(Number(e.target.value))} className="w-full bg-slate-900 border border-slate-600 rounded-lg px-4 py-3 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" min="0" step="100" />
) : (
šŸ‘†
Click an account on the network
to add funding and watch it propagate
)}
)} {/* Playback Controls */} {result && (

Playback Controls

Iteration {currentIteration + 1} of {result.iterations.length}
{[1, 2, 5].map(speed => ( ))}
{result.converged ? (
āœ“ Converged in {result.iterationCount} iterations
) : (
⚠ Did not converge
)}
)} {/* Legend */}

Account States

Below Minimum
Sustainable
At Maximum
Overflowing
{fundingMode === 'interactive' && (
Selected (click to target)
)}
{/* Right Panel: Visualization */}

Network Visualization

{scenario && ( )}
{/* Timeline */} {result && (

Distribution Timeline

)} {/* Results Table */} {result && (

Distribution Results

)}
) } /** * Network Visualization Component with Animated Particles and Click Handlers */ function NetworkVisualization({ scenario, balances, overflows, flows, particles, selectedAccountId, onAccountClick, interactiveMode, }: { scenario: Scenario balances: Map overflows: Map flows: Map particles: FlowParticle[] selectedAccountId: string | null onAccountClick: (accountId: string) => void interactiveMode: boolean }) { const { accounts, layout } = scenario const padding = 80 const maxX = Math.max(...Array.from(layout.values()).map(p => p.x)) + padding const maxY = Math.max(...Array.from(layout.values()).map(p => p.y)) + padding return ( {/* Draw arrows */} {accounts.map(account => { const sourcePos = layout.get(account.id) if (!sourcePos) return null return Array.from(account.allocations.entries()).map( ([targetId, percentage]) => { const targetPos = layout.get(targetId) if (!targetPos) return null const flowKey = `${account.id}->${targetId}` const flowAmount = flows.get(flowKey) || 0 const isActive = flowAmount > 0 return ( {percentage}% {isActive && ( ${flowAmount.toFixed(0)} )} ) } ) })} {/* Draw animated particles */} {particles.map(particle => { const x = particle.fromX + (particle.toX - particle.fromX) * particle.progress const y = particle.fromY + (particle.toY - particle.fromY) * particle.progress const size = Math.max(4, Math.min(8, particle.amount / 50)) return ( ) })} {/* Draw accounts */} {accounts.map(account => { const pos = layout.get(account.id) if (!pos) return null const balance = balances.get(account.id) || account.balance const overflow = overflows.get(account.id) || 0 const state = getAccountState( balance, account.minThreshold, account.maxThreshold, overflow > 0 ) const isSelected = selectedAccountId === account.id const stateColors = { 'below-minimum': '#ef4444', sustainable: '#eab308', 'at-maximum': '#22c55e', overflowing: '#3b82f6', } const color = isSelected ? '#a855f7' : stateColors[state] return ( onAccountClick(account.id)} style={{ cursor: interactiveMode ? 'pointer' : 'default' }} > {/* Selection ring */} {isSelected && ( )} {/* Account circle */} {/* Account name */} {account.name} {/* Balance */} ${balance.toFixed(0)} {/* Thresholds */} ({account.minThreshold}–{account.maxThreshold}) ) })} ) } /** * Distribution Timeline Component */ function DistributionTimeline({ result, currentIteration, onIterationClick, }: { result: DistributionResult currentIteration: number onIterationClick: (iteration: number) => void }) { const maxOverflow = Math.max( ...result.iterations.map(iter => iter.totalOverflow), 1 ) return (
Click any iteration to jump to that state Total overflow decreases over time →
{result.iterations.map((iter, index) => { const isCurrent = index === currentIteration const dotSize = Math.max( 8, Math.min(20, (iter.totalOverflow / maxOverflow) * 20) ) return ( ) })}
Start:{' '} ${result.iterations[0]?.totalOverflow.toFixed(2)}
Current:{' '} ${result.iterations[currentIteration]?.totalOverflow.toFixed(2) || '0.00'}
End:{' '} ${result.iterations[result.iterations.length - 1]?.totalOverflow.toFixed(2)}
) } /** * Results Table Component */ function ResultsTable({ scenario, result, currentIteration, }: { scenario: Scenario result: DistributionResult currentIteration: number }) { const iteration = result.iterations[currentIteration] const balances = iteration?.balances || result.finalBalances return (
{scenario.accounts.map(account => { const initial = result.initialBalances.get(account.id) || 0 const current = balances.get(account.id) || 0 const change = current - initial const overflow = iteration?.overflows.get(account.id) || 0 const state = getAccountState( current, account.minThreshold, account.maxThreshold, overflow > 0 ) const stateLabels = { 'below-minimum': 'Below Min', sustainable: 'Sustainable', 'at-maximum': 'At Max', overflowing: 'Overflowing', } const stateColors = { 'below-minimum': 'text-red-400', sustainable: 'text-yellow-400', 'at-maximum': 'text-green-400', overflowing: 'text-blue-400', } return ( ) })}
Account Initial Current Change Min/Max State
{account.name} ${initial.toFixed(2)} ${current.toFixed(2)} = 0 ? 'text-green-400' : 'text-red-400' }`} > {change >= 0 ? '+' : ''}${change.toFixed(2)} ${account.minThreshold} / ${account.maxThreshold} {stateLabels[state]}
) }