import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { Play, Pause, RefreshCw, Settings, Activity, Zap, Layers, Calculator } from 'lucide-react'; import GridCell from './components/GridCell'; // --- Constants (fix #8: removed unused CELL_SIZE, Info, ChevronRight) --- const GRID_SIZE = 15; const CELL_COUNT = GRID_SIZE * GRID_SIZE; const DELAY_BASE = 500; const START_IDX = 0; const END_IDX = CELL_COUNT - 1; // --- Heuristic (Manhattan Distance) --- const heuristic = (ax, ay, bx, by) => Math.abs(ax - bx) + Math.abs(ay - by); // Neighbor offsets const DIRS = [{ dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 1 }, { dx: 0, dy: -1 }]; // --- Main Application --- export default function App() { const [grid, setGrid] = useState([]); const [beamWidth, setBeamWidth] = useState(3); const [isPlaying, setIsPlaying] = useState(false); const [searchState, setSearchState] = useState(null); const [currentStep, setCurrentStep] = useState(0); const [history, setHistory] = useState([]); const [speed, setSpeed] = useState(1); const [stats, setStats] = useState({ visited: 0, activeBeam: 0, currentBestCost: 0 }); const timerRef = useRef(null); const speedRef = useRef(speed); useEffect(() => { speedRef.current = speed; }, [speed]); // Fix #9: stable callbacks const resetSearch = useCallback(() => { clearInterval(timerRef.current); setIsPlaying(false); setSearchState(null); setCurrentStep(0); setHistory([]); setStats({ visited: 0, activeBeam: 0, currentBestCost: 0 }); }, []); const generateGrid = useCallback(() => { const newGrid = new Array(CELL_COUNT); for (let y = 0; y < GRID_SIZE; y++) { for (let x = 0; x < GRID_SIZE; x++) { const noise = Math.random(); let cost = 1; if (noise > 0.7) cost = 3; if (noise > 0.9) cost = 8; newGrid[y * GRID_SIZE + x] = { id: `${x}-${y}`, x, y, cost }; } } setGrid(newGrid); resetSearch(); }, [resetSearch]); useEffect(() => { generateGrid(); }, [generateGrid]); // --- Beam Search (fixes #1, #6, #7) --- const runBeamSearch = useCallback(() => { if (grid.length === 0) return; const startNode = grid[START_IDX]; // fix #6: direct index const endNode = grid[END_IDX]; const endX = endNode.x, endY = endNode.y; let beam = [{ node: startNode, path: [startNode.id], pathSet: new Set([startNode.id]), // fix #1: O(1) cycle check gScore: 0, fScore: heuristic(startNode.x, startNode.y, endX, endY), }]; const steps = []; const visitedGlobal = new Set([startNode.id]); let active = true; let finalPath = null; // Record initial state steps.push({ beamIds: [startNode.id], beamScores: new Map([[startNode.id, { g: 0, f: beam[0].fScore }]]), visited: new Set(visitedGlobal), prunedSet: new Set(), bestPath: null, bestPathSet: null, complete: false, }); while (active && beam.length > 0) { const candidates = []; for (const item of beam) { if (item.node.id === endNode.id) { finalPath = item.path; active = false; break; } for (const { dx, dy } of DIRS) { const nx = item.node.x + dx; const ny = item.node.y + dy; if (nx < 0 || nx >= GRID_SIZE || ny < 0 || ny >= GRID_SIZE) continue; const neighbor = grid[ny * GRID_SIZE + nx]; if (item.pathSet.has(neighbor.id)) continue; // fix #1: O(1) lookup const newG = item.gScore + neighbor.cost; const newF = newG + heuristic(nx, ny, endX, endY); const newPathSet = new Set(item.pathSet); newPathSet.add(neighbor.id); candidates.push({ node: neighbor, path: [...item.path, neighbor.id], pathSet: newPathSet, gScore: newG, fScore: newF, }); } } if (!active) break; if (candidates.length === 0) { active = false; break; } candidates.sort((a, b) => a.fScore - b.fScore); const nextBeam = candidates.slice(0, beamWidth); const pruned = candidates.slice(beamWidth); const beamIds = []; const beamScores = new Map(); for (const b of nextBeam) { visitedGlobal.add(b.node.id); beamIds.push(b.node.id); beamScores.set(b.node.id, { g: b.gScore, f: b.fScore }); } const prunedSet = new Set(); for (const p of pruned) prunedSet.add(p.node.id); const bestPath = nextBeam[0]?.path || []; steps.push({ beamIds, beamScores, visited: new Set(visitedGlobal), prunedSet, bestPath, bestPathSet: new Set(bestPath), // fix #3: pre-computed Set complete: false, }); beam = nextBeam; } if (finalPath) { steps.push({ beamIds: [], beamScores: new Map(), visited: visitedGlobal, prunedSet: new Set(), bestPath: finalPath, bestPathSet: new Set(finalPath), complete: true, }); } setHistory(steps); setSearchState('running'); setIsPlaying(true); setCurrentStep(0); }, [grid, beamWidth]); // --- Animation Loop (fix #5: no nested state updates) --- useEffect(() => { if (!isPlaying || searchState !== 'running') { clearInterval(timerRef.current); return; } timerRef.current = setInterval(() => { setCurrentStep(prev => { const next = prev + 1; if (next >= history.length) { clearInterval(timerRef.current); setIsPlaying(false); // fix #5: schedule state update outside updater setTimeout(() => { setSearchState(history[history.length - 1]?.complete ? 'finished' : 'failed'); }, 0); return prev; } return next; }); }, DELAY_BASE / speedRef.current); return () => clearInterval(timerRef.current); }, [isPlaying, searchState, history]); // Update stats from current step useEffect(() => { const stepData = history[currentStep]; if (stepData) { setStats({ visited: stepData.visited.size, activeBeam: stepData.beamIds.length, currentBestCost: stepData.beamScores.values().next().value?.g || 0, }); } }, [currentStep, history]); // --- Pre-compute per-step lookup maps for O(1) cell classification (fix #2, #3) --- const stepData = history[currentStep] || null; const beamScoreMap = stepData?.beamScores || null; const bestPathSet = stepData?.bestPathSet || null; // Compute cell props once per step, memoized const cellProps = useMemo(() => { if (!grid.length) return []; return grid.map((node) => { const isStart = node.x === 0 && node.y === 0; const isEnd = node.x === GRID_SIZE - 1 && node.y === GRID_SIZE - 1; const isObstacle = node.cost > 5; const isHighCost = node.cost > 1 && node.cost <= 5; // Determine visual class let cellClass = 'bg-slate-900 border-slate-800'; if (isStart) { cellClass = 'bg-emerald-500 border-emerald-600 shadow-emerald-500/50'; } else if (isEnd) { cellClass = 'bg-rose-500 border-rose-600 shadow-rose-500/50'; } else if (node.cost > 1) { cellClass = 'bg-slate-800 border-slate-700'; } let tooltipContent = null; if (stepData) { const beamScore = beamScoreMap?.get(node.id); const isOnBestPath = bestPathSet?.has(node.id); if (isOnBestPath && !isStart && !isEnd) { cellClass = 'bg-amber-500/20 border-amber-500 shadow-[0_0_10px_rgba(245,158,11,0.3)] animate-pulse'; } if (beamScore) { cellClass = 'bg-blue-500 border-blue-400 shadow-[0_0_15px_rgba(59,130,246,0.6)] z-10 scale-110 transition-transform cursor-help'; const h = beamScore.f - beamScore.g; tooltipContent = ( <>
Node {node.x},{node.y}
Total (f): {beamScore.f.toFixed(1)}
Cost (g): {beamScore.g.toFixed(1)}
Dist (h): {h.toFixed(1)}
); } else if (stepData.prunedSet?.has(node.id) && !isStart && !isEnd) { cellClass = 'bg-red-900/30 border-red-900/50'; } else if (stepData.visited.has(node.id) && !isStart && !isEnd && !isOnBestPath) { cellClass = 'bg-blue-900/20 border-blue-800/30'; } } return { cellClass, isObstacle, isHighCost, isStart, isEnd, tooltipContent }; }); }, [grid, stepData, beamScoreMap, bestPathSet]); // Slider handlers (fix #13) const onBeamWidth = useCallback((e) => { setBeamWidth(parseInt(e.target.value)); resetSearch(); }, [resetSearch]); const onSpeed = useCallback((e) => { setSpeed(parseInt(e.target.value)); }, []); const onScrub = useCallback((e) => { setIsPlaying(false); setCurrentStep(Number(e.target.value)); }, []); const togglePlay = useCallback(() => { setIsPlaying(p => !p); }, []); return (
{/* Header */}

LGI | Lattice Grid Interface

Beam Search Visualizer v1.2

Depth {currentStep}
Active Nodes {stats.activeBeam || 0}
{/* fix #10: removed dead */}
{/* Left Control Panel */}
{beamWidth}

Determines how many paths are kept in memory at each step. Higher width = better accuracy but slower.

{speed}x

{!history.length || searchState === null ? ( ) : ( <> )}
{/* Legend */}

Node Legend

Start Node
Target Node
Active Beam
Current Best Path
Hover blue nodes to see score
{/* Right Grid Visualization */}
{/* Grid Background */}
{/* Grid */}
{cellProps.map((props, i) => ( ))}
{/* Result Overlays */} {searchState === 'finished' && (
Target Reached! Optimal path found within beam constraints.
)} {searchState === 'failed' && (
Search Exhausted. Beam width too narrow to find target.
)}
{/* Timeline Scrubber */} {history.length > 0 && (
START
1 ? (currentStep / (history.length - 1)) * 100 : 0}%` }} />
1 ? (currentStep / (history.length - 1)) * 100 : 0}% - 8px)` }} />
GOAL
)}
); }