Manager
View Site
Name
Type
React (.jsx)
HTML (.html)
Icon
Description
Code Editor
Revision History (0)
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { Play, Square, Octagon, Flag, Bot, Eraser, BrickWall, Undo2, RefreshCw, Trash2, Eye, EyeOff, Code, X, Settings2, Zap, Gauge, BarChart3, History, TrendingUp, Dices } from 'lucide-react'; // --- WORKER SCRIPT (Algorithms with Timing) --- const WORKER_CODE = ` class PriorityQueue { constructor() { this.elements = []; } enqueue(item, priority) { this.elements.push({ item, priority }); this.elements.sort((a, b) => a.priority - b.priority); } dequeue() { return this.elements.shift().item; } isEmpty() { return this.elements.length === 0; } } const heuristic = (a, b) => Math.sqrt(Math.pow(a.row - b.row, 2) + Math.pow(a.col - b.col, 2)); // Euclidean for Theta* const getNeighbors = (node, grid, rows, cols) => { const directions = [{r:-1,c:0}, {r:1,c:0}, {r:0,c:-1}, {r:0,c:1}]; const neighbors = []; directions.forEach(({r, c}) => { const nr = node.row + r; const nc = node.col + c; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) { neighbors.push(grid[nr][nc]); } }); return neighbors; }; const stepTowards = (from, toRow, toCol, grid, rows, cols) => { let dr = toRow - from.row; let dc = toCol - from.col; if (Math.abs(dr) > Math.abs(dc)) { dr = Math.sign(dr); dc = 0; } else { dc = Math.sign(dc); dr = 0; } const nr = from.row + dr; const nc = from.col + dc; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) return grid[nr][nc]; return null; }; // Bresenham's Line Algorithm for Line of Sight const hasLineOfSight = (nodeA, nodeB, grid) => { let x0 = nodeA.col, y0 = nodeA.row; let x1 = nodeB.col, y1 = nodeB.row; let dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0); let sx = (x0 < x1) ? 1 : -1, sy = (y0 < y1) ? 1 : -1; let err = dx - dy; while (true) { if (grid[y0][x0].isWall) return false; if (x0 === x1 && y0 === y1) break; let e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } return true; }; self.onmessage = function(e) { const { grid, startPos, goalPos, algo, rows, cols, customScript } = e.data; const startTime = performance.now(); const calcGrid = grid.map(row => row.map(n => ({ ...n, id: n.row + '-' + n.col, distance: Infinity, parent: null, visited: false }))); const startNode = calcGrid[startPos.row][startPos.col]; const goalNode = calcGrid[goalPos.row][goalPos.col]; startNode.distance = 0; const visitedOrder = []; let success = false; let path = []; try { if (algo === 'Custom Script') { const userFunc = new Function('start', 'goal', 'grid', 'utils', customScript + '\\n return findPath(start, goal, grid, utils);'); const utils = { getNeighbors: (n, g) => getNeighbors(n, g, rows, cols) }; const result = userFunc(startNode, goalNode, calcGrid, utils); const endTime = performance.now(); self.postMessage({ ...result, success: result.path && result.path.length > 0, duration: endTime - startTime }); return; } // --- Theta* Algorithm (Line of Sight) --- if (algo === 'Theta*') { const frontier = new PriorityQueue(); frontier.enqueue(startNode, 0); const visitedSet = new Set(); startNode.parent = startNode; // Point to self initially while (!frontier.isEmpty()) { const current = frontier.dequeue(); if (visitedSet.has(current.id)) continue; visitedSet.add(current.id); visitedOrder.push({row: current.row, col: current.col}); if (current.id === goalNode.id) { success = true; let curr = current; while (curr.id !== startNode.id) { path.unshift({row: curr.row, col: curr.col}); curr = curr.parent; } path.unshift({row: startNode.row, col: startNode.col}); break; } const neighbors = getNeighbors(current, calcGrid, rows, cols); for (let neighbor of neighbors) { if (!visitedSet.has(neighbor.id) && !neighbor.isWall) { let newParent = current; let newG = current.distance + 1; // Grid distance default // Key Theta* Logic: Check line of sight to grandparent if (current.parent && current.parent.id !== current.id) { if (hasLineOfSight(current.parent, neighbor, calcGrid)) { newParent = current.parent; const distToGrandparent = Math.sqrt(Math.pow(current.parent.row - neighbor.row, 2) + Math.pow(current.parent.col - neighbor.col, 2)); newG = current.parent.distance + distToGrandparent; } } if (newG < neighbor.distance) { neighbor.distance = newG; neighbor.parent = newParent; frontier.enqueue(neighbor, newG + heuristic(neighbor, goalNode)); } } } } } // --- RRT Family --- else if (algo === 'RRT' || algo === 'RRT-Connect') { const treeA = [startNode]; startNode.visited = true; const treeB = [goalNode]; if (algo === 'RRT-Connect') goalNode.visited = true; const maxIter = rows * cols * 6; // More iterations for complex maps for(let i=0; i<maxIter; i++) { const bias = algo === 'RRT-Connect' ? 0.05 : 0.1; let rr, rc; if (Math.random() < bias) { rr = goalNode.row; rc = goalNode.col; } else { rr = Math.floor(Math.random() * rows); rc = Math.floor(Math.random() * cols); } let nearest = null; let minDist = Infinity; for(let node of treeA) { let d = Math.abs(node.row - rr) + Math.abs(node.col - rc); if(d < minDist) { minDist = d; nearest = node; } } const nextNode = stepTowards(nearest, rr, rc, calcGrid, rows, cols); if (nextNode && !nextNode.isWall && !nextNode.visited) { nextNode.parent = nearest; nextNode.visited = true; treeA.push(nextNode); visitedOrder.push({row: nextNode.row, col: nextNode.col}); if (algo === 'RRT' && nextNode.id === goalNode.id) { success = true; let curr = nextNode; while(curr) { path.unshift({row:curr.row, col:curr.col}); curr = curr.parent; } break; } if (algo === 'RRT-Connect') { const neighbors = getNeighbors(nextNode, calcGrid, rows, cols); let bridge = null; for(let n of neighbors) { const match = treeB.find(bNode => bNode.id === n.id); if (match) { bridge = match; break; } } if (bridge) { success = true; let curr = nextNode; while(curr) { path.unshift({row:curr.row, col:curr.col}); curr = curr.parent; } curr = bridge; while(curr) { path.push({row:curr.row, col:curr.col}); curr = curr.parent; } break; } } } if (algo === 'RRT-Connect') { let nearestB = null; let minDistB = Infinity; const target = treeA[treeA.length-1]; for(let node of treeB) { let d = Math.abs(node.row - target.row) + Math.abs(node.col - target.col); if(d < minDistB) { minDistB = d; nearestB = node; } } const nextB = stepTowards(nearestB, target.row, target.col, calcGrid, rows, cols); if (nextB && !nextB.isWall) { if (nextB.id === target.id) { success = true; let curr = target; while(curr) { path.unshift({row:curr.row, col:curr.col}); curr = curr.parent; } curr = nearestB; while(curr) { path.push({row:curr.row, col:curr.col}); curr = curr.parent; } break; } if (!nextB.visited) { nextB.parent = nearestB; nextB.visited = true; treeB.push(nextB); visitedOrder.push({row: nextB.row, col: nextB.col}); } } } } } else if (algo === 'Bi-Direct BFS') { let startQ = [startNode]; let goalQ = [goalNode]; let startVisited = new Map(); let goalVisited = new Map(); startVisited.set(startNode.id, null); goalVisited.set(goalNode.id, null); while(startQ.length > 0 && goalQ.length > 0) { let currS = startQ.shift(); visitedOrder.push({row: currS.row, col: currS.col}); if(goalVisited.has(currS.id)) { path = reconstructBiDir(currS, startVisited, goalVisited); success = true; break; } for(let n of getNeighbors(currS, calcGrid, rows, cols)) { if(!n.isWall && !startVisited.has(n.id)) { startVisited.set(n.id, currS); startQ.push(n); } } let currG = goalQ.shift(); visitedOrder.push({row: currG.row, col: currG.col}); if(startVisited.has(currG.id)) { path = reconstructBiDir(currG, startVisited, goalVisited); success = true; break; } for(let n of getNeighbors(currG, calcGrid, rows, cols)) { if(!n.isWall && !goalVisited.has(n.id)) { goalVisited.set(n.id, currG); goalQ.push(n); } } } } else { let frontier; const isBFS = algo === 'BFS'; const isDFS = algo === 'DFS'; if (isBFS || isDFS) { frontier = [startNode]; } else { frontier = new PriorityQueue(); frontier.enqueue(startNode, 0); } const visitedSet = new Set(); while ((isBFS || isDFS) ? frontier.length > 0 : !frontier.isEmpty()) { const current = isDFS ? frontier.pop() : (isBFS ? frontier.shift() : frontier.dequeue()); if (visitedSet.has(current.id)) continue; visitedSet.add(current.id); visitedOrder.push({ row: current.row, col: current.col }); if (current.id === goalNode.id) { let temp = current; while (temp) { path.unshift({ row: temp.row, col: temp.col }); temp = temp.parent; } success = true; break; } const neighbors = getNeighbors(current, calcGrid, rows, cols); if (isDFS) neighbors.sort(() => Math.random() - 0.5); for (let neighbor of neighbors) { if (neighbor.isWall || visitedSet.has(neighbor.id)) continue; const newDist = current.distance + 1; if (isBFS || isDFS) { if (!neighbor.parent) { neighbor.parent = current; frontier.push(neighbor); } } else { if (newDist < neighbor.distance) { neighbor.distance = newDist; neighbor.parent = current; let priority = 0; const h = heuristic(neighbor, goalNode); if (algo === 'A*') priority = newDist + h; else if (algo === 'Dijkstra') priority = newDist; else if (algo === 'Greedy') priority = h; frontier.enqueue(neighbor, priority); } } } } } } catch (err) { } const endTime = performance.now(); self.postMessage({ path, visitedOrder, success, duration: endTime - startTime }); }; function reconstructBiDir(meetingNode, startMap, goalMap) { let path = []; let curr = meetingNode; while(curr) { path.unshift({row:curr.row, col:curr.col}); curr = startMap.get(curr.id); } curr = goalMap.get(meetingNode.id); while(curr) { path.push({row:curr.row, col:curr.col}); curr = goalMap.get(curr.id); } return path; } `; const DEFAULT_CODE = `function findPath(start, goal, grid, utils) { // Simple BFS Example let frontier = [start]; start.parent = null; let visited = []; while(frontier.length > 0) { let current = frontier.shift(); visited.push(current); if(current.id === goal.id) break; for(let n of utils.getNeighbors(current, grid)) { if(!n.isWall && !n.parent && n !== start) { n.parent = current; frontier.push(n); } } } // Reconstruct path... return { path: [], visited }; }`; const GRID_SIZES = { SMALL: { r: 15, c: 10, label: 'Small (15x10)' }, MEDIUM: { r: 25, c: 15, label: 'Medium (25x15)' }, LARGE: { r: 40, c: 25, label: 'Large (40x25)' }, HUGE: { r: 60, c: 40, label: 'Huge (60x40)' } }; const ALGOS = ['A*', 'Theta*', 'RRT', 'RRT-Connect', 'Dijkstra', 'Bi-Direct BFS', 'Greedy', 'DFS', 'Custom Script']; const VISIBILITY = { GOD: 'Omniscient', FOG: 'Explorer' }; export default function RoboPath() { // --- Config --- const [sizeCfg, setSizeCfg] = useState(GRID_SIZES.SMALL); const [algo, setAlgo] = useState('Theta*'); const [visibility, setVisibility] = useState(VISIBILITY.GOD); const [speed, setSpeed] = useState(50); // --- State --- const [grid, setGrid] = useState([]); const [robotPos, setRobotPos] = useState(null); const [isRunning, setIsRunning] = useState(false); const [status, setStatus] = useState('Ready'); const [mode, setMode] = useState('WALL'); // --- Visuals --- const [visitedNodes, setVisitedNodes] = useState(new Set()); const [traveledPath, setTraveledPath] = useState([]); const [plannedPath, setPlannedPath] = useState([]); const [fogMask, setFogMask] = useState(new Set()); // --- Analytics --- const [logs, setLogs] = useState([]); const [showReport, setShowReport] = useState(false); // --- Tools --- const [showCode, setShowCode] = useState(false); const [customCode, setCustomCode] = useState(DEFAULT_CODE); // --- Refs --- const workerRef = useRef(null); const isDrawing = useRef(false); const timeoutsRef = useRef([]); const gridRef = useRef([]); const runningRef = useRef(false); // --- Init & Storage --- useEffect(() => { const blob = new Blob([WORKER_CODE], { type: 'application/javascript' }); workerRef.current = new Worker(URL.createObjectURL(blob)); const savedLogs = localStorage.getItem('roboPathLogs'); if (savedLogs) setLogs(JSON.parse(savedLogs)); return () => workerRef.current?.terminate(); }, []); const saveRunLog = (entry) => { const newLogs = [entry, ...logs].slice(0, 100); setLogs(newLogs); localStorage.setItem('roboPathLogs', JSON.stringify(newLogs)); }; const generateMapHash = () => { const walls = gridRef.current.flat().filter(n => n.isWall).map(n => `${n.row}.${n.col}`).join('|'); const s = gridRef.current.flat().find(n => n.isStart); const g = gridRef.current.flat().find(n => n.isGoal); return `${sizeCfg.label}_S${s?.row}.${s?.col}_G${g?.row}.${g?.col}_W${walls.length}_${walls.substring(0, 50)}`; }; const initGrid = useCallback((keepWalls = false) => { stopSimulation(); const rows = sizeCfg.r; const cols = sizeCfg.c; let newGrid = []; if (keepWalls && gridRef.current.length === rows) { newGrid = gridRef.current.map(row => row.map(cell => ({ ...cell, distance: Infinity, parent: null }))); } else { for (let r = 0; r < rows; r++) { const row = []; for (let c = 0; c < cols; c++) { row.push({ row: r, col: c, isWall: false, isStart: r === 1 && c === 1, isGoal: r === rows - 2 && c === cols - 2 }); } newGrid.push(row); } } setGrid(newGrid); gridRef.current = newGrid; const start = newGrid.flat().find(n => n.isStart) || {row:1, col:1}; setRobotPos({ row: start.row, col: start.col, dir: 0 }); setVisitedNodes(new Set()); setTraveledPath([]); setPlannedPath([]); setFogMask(new Set()); setStatus('Ready'); }, [sizeCfg]); useEffect(() => { initGrid(); }, [initGrid]); const randomizeMap = () => { if (isRunning) return; stopSimulation(); const rows = sizeCfg.r; const cols = sizeCfg.c; let newGrid = gridRef.current.map(row => row.map(cell => ({ ...cell, isWall: false, distance: Infinity, parent: null }))); const start = newGrid.flat().find(n => n.isStart); const goal = newGrid.flat().find(n => n.isGoal); newGrid = newGrid.map(row => row.map(cell => { if (cell === start || cell === goal) return cell; if (Math.random() < 0.3) return { ...cell, isWall: true }; return cell; })); setGrid(newGrid); gridRef.current = newGrid; setVisitedNodes(new Set()); setTraveledPath([]); setPlannedPath([]); setStatus('Randomized'); }; const stopSimulation = () => { setIsRunning(false); runningRef.current = false; timeoutsRef.current.forEach(t => clearTimeout(t)); timeoutsRef.current = []; if (workerRef.current) { workerRef.current.terminate(); const blob = new Blob([WORKER_CODE], { type: 'application/javascript' }); workerRef.current = new Worker(URL.createObjectURL(blob)); } setStatus('Stopped'); }; const startSimulation = () => { if (isRunning) return stopSimulation(); setIsRunning(true); runningRef.current = true; setVisitedNodes(new Set()); setTraveledPath([]); setPlannedPath([]); if (visibility === VISIBILITY.GOD) runGodMode(); else runExplorerMode(); }; // --- Visualization Helpers --- const getDelays = (totalSteps) => { if (speed === 100) return { stepDelay: 0, batchSize: totalSteps, moveDelay: 0 }; const invSpeed = 100 - speed; const stepDelay = Math.max(0, invSpeed * 0.5); const moveDelay = Math.max(0, invSpeed * 5); let batchSize = 1; if (speed > 50) batchSize = 5; if (speed > 80) batchSize = 20; if (speed > 90) batchSize = 50; if (speed > 95) batchSize = 100; return { stepDelay, batchSize, moveDelay }; }; const runGodMode = () => { setStatus(`Computing ${algo}...`); const start = gridRef.current.flat().find(n => n.isStart); const goal = gridRef.current.flat().find(n => n.isGoal); const mapHash = generateMapHash(); workerRef.current.onmessage = (e) => { const { path, visitedOrder, success, duration } = e.data; saveRunLog({ id: Date.now(), date: new Date().toLocaleString(), algo, mapHash, mapSize: sizeCfg.label, success, duration: duration.toFixed(2), nodesVisited: visitedOrder.length, pathLength: path.length, mode: 'God Mode' }); if (!success) { setStatus('No Path!'); setIsRunning(false); return; } const { stepDelay, batchSize, moveDelay } = getDelays(visitedOrder.length); if (speed === 100) { setVisitedNodes(new Set(visitedOrder.map(n => `${n.row}-${n.col}`))); setPlannedPath(path.map(n => `${n.row}-${n.col}`)); setTraveledPath(path.map(n => `${n.row}-${n.col}`)); const endNode = path[path.length - 1]; updateRobotPos(endNode); setIsRunning(false); setStatus('Teleported'); return; } let animTime = 0; for (let i = 0; i < visitedOrder.length; i += batchSize) { animTime += stepDelay; timeoutsRef.current.push(setTimeout(() => { if (!runningRef.current) return; setVisitedNodes(prev => { const next = new Set(prev); for (let j = 0; j < batchSize && i+j < visitedOrder.length; j++) { next.add(`${visitedOrder[i+j].row}-${visitedOrder[i+j].col}`); } return next; }); }, animTime)); } const moveStart = animTime + 100; timeoutsRef.current.push(setTimeout(() => { setPlannedPath(path.map(n => `${n.row}-${n.col}`)); setStatus('Moving...'); if (moveDelay === 0) { setTraveledPath(path.map(n => `${n.row}-${n.col}`)); updateRobotPos(path[path.length - 1]); setIsRunning(false); setStatus('Arrived'); } else { path.forEach((node, i) => { timeoutsRef.current.push(setTimeout(() => { if (!runningRef.current) return; setTraveledPath(prev => [...prev, `${node.row}-${node.col}`]); updateRobotPos(node); if (i === path.length - 1) { setIsRunning(false); setStatus('Arrived'); } }, i * moveDelay)); }); } }, moveStart)); }; workerRef.current.postMessage({ grid: gridRef.current, startPos: start, goalPos: goal, algo, rows: sizeCfg.r, cols: sizeCfg.c, customScript: customCode }); }; const runExplorerMode = async () => { setStatus('Sensors Online...'); let currentPos = gridRef.current.flat().find(n => n.isStart); const goal = gridRef.current.flat().find(n => n.isGoal); const mapHash = generateMapHash(); const startTime = Date.now(); let knownWalls = new Set(); let revealed = new Set(); let pathHist = [`${currentPos.row}-${currentPos.col}`]; setTraveledPath(pathHist); const getPathFromWorker = (partialGrid, start, end) => { return new Promise((resolve) => { workerRef.current.onmessage = (e) => resolve(e.data); workerRef.current.postMessage({ grid: partialGrid, startPos: start, goalPos: end, algo, rows: sizeCfg.r, cols: sizeCfg.c, customScript: customCode }); }); }; const gameLoop = async () => { if (!runningRef.current) return; const directions = [{r:-1,c:0}, {r:1,c:0}, {r:0,c:-1}, {r:0,c:1}, {r:-1,c:-1}, {r:-1,c:1}, {r:1,c:-1}, {r:1,c:1}]; let wallFound = false; directions.forEach(d => { const nr = currentPos.row + d.r; const nc = currentPos.col + d.c; if (nr >= 0 && nr < sizeCfg.r && nc >= 0 && nc < sizeCfg.c) { const id = `${nr}-${nc}`; revealed.add(id); if (gridRef.current[nr][nc].isWall) { if (!knownWalls.has(id)) wallFound = true; knownWalls.add(id); } } }); setFogMask(new Set(revealed)); const mentalGrid = gridRef.current.map(row => row.map(c => ({ ...c, isWall: knownWalls.has(`${c.row}-${c.col}`) }))); const result = await getPathFromWorker(mentalGrid, currentPos, goal); if (!runningRef.current) return; setPlannedPath(result.path.map(n => `${n.row}-${n.col}`)); if (!result.success || result.path.length <= 1) { setStatus(result.success ? 'Arrived!' : 'Blocked!'); setIsRunning(false); saveRunLog({ id: Date.now(), date: new Date().toLocaleString(), algo, mapHash, mapSize: sizeCfg.label, success: result.success, duration: (Date.now() - startTime).toFixed(0), nodesVisited: revealed.size, pathLength: pathHist.length, mode: 'Explorer Mode' }); return; } const nextStep = result.path[1]; updateRobotPos(nextStep); pathHist.push(`${nextStep.row}-${nextStep.col}`); setTraveledPath([...pathHist]); currentPos = nextStep; const delay = speed >= 99 ? 0 : Math.max(20, (100 - speed) * 5); if (delay === 0) { gameLoop(); } else { setTimeout(() => requestAnimationFrame(gameLoop), delay); } }; gameLoop(); }; const updateRobotPos = (node) => { setRobotPos(prev => { const dRow = node.row - prev.row; const dCol = node.col - prev.col; let angle = prev.dir; if (dRow > 0) angle = 180; else if (dRow < 0) angle = 0; else if (dCol > 0) angle = 90; else if (dCol < 0) angle = 270; return { row: node.row, col: node.col, dir: angle }; }); }; // --- Interaction --- const handleGridClick = (r, c) => { if (isRunning) return; const newGrid = gridRef.current.map(row => row.map(cell => ({...cell}))); const cell = newGrid[r][c]; if (mode === 'WALL') { if(!cell.isStart && !cell.isGoal) cell.isWall = true; } else if (mode === 'ERASE') { cell.isWall = false; } else if (mode === 'START') { newGrid.flat().forEach(n => n.isStart = false); cell.isStart = true; cell.isWall = false; cell.isGoal = false; setRobotPos({row:r, col:c, dir:0}); } else if (mode === 'GOAL') { newGrid.flat().forEach(n => n.isGoal = false); cell.isGoal = true; cell.isWall = false; cell.isStart = false; } setGrid(newGrid); gridRef.current = newGrid; }; const getCellClass = (r, c) => { const cell = grid[r][c]; const id = `${r}-${c}`; const isLarge = sizeCfg.r > 30; let base = isLarge ? "w-full h-full border-slate-800/30 border-[0.5px] " : "w-full h-full border-slate-800 border-[0.5px] "; base += "flex items-center justify-center select-none "; if (visibility === VISIBILITY.FOG && isRunning && !fogMask.has(id) && !cell.isStart && !cell.isGoal) return base + "bg-slate-950"; if (cell.isWall) return base + "bg-slate-600"; if (traveledPath.includes(id)) return base + "bg-blue-500/60 border-none"; if (plannedPath.includes(id)) return base + "bg-blue-400/20"; if (visitedNodes.has(id)) return base + (isLarge ? "bg-cyan-500/30" : "bg-cyan-900/40"); if (visibility === VISIBILITY.FOG && fogMask.has(id)) return base + "bg-slate-800"; return base + "bg-slate-900"; }; // Changed h-screen to h-[100dvh] to fix mobile browser viewport issues return ( <div className="flex flex-col h-[100dvh] bg-slate-950 text-slate-100 font-sans overflow-hidden touch-none"> {/* Header */} <header className="flex-none p-3 bg-slate-900 border-b border-slate-800 flex justify-between items-center shadow-md z-20"> <div className="flex flex-col"> <h1 className="font-bold text-sm text-slate-200 flex items-center gap-2"> <Bot size={18} className={isRunning ? "text-amber-400 animate-pulse" : "text-blue-400"} /> <span className="hidden sm:inline">RoboPath</span> <span className="sm:hidden">RP</span> <span className="text-[9px] bg-emerald-500/20 text-emerald-300 px-1.5 py-0.5 rounded border border-emerald-500/30 font-bold italic">HYPER</span> </h1> <div className="text-[10px] text-slate-400 font-mono mt-0.5 hidden sm:block">{status}</div> </div> <div className="flex items-center gap-2"> <div className="flex bg-slate-800 rounded-lg p-0.5 border border-slate-700 mr-1 sm:mr-2"> <select value={sizeCfg.label} onChange={(e) => setSizeCfg(Object.values(GRID_SIZES).find(s => s.label === e.target.value))} disabled={isRunning} className="bg-transparent text-[10px] font-bold text-slate-300 px-1 py-1.5 sm:px-2 outline-none cursor-pointer hover:bg-slate-700 rounded max-w-[80px] sm:max-w-none"> {Object.values(GRID_SIZES).map(s => <option key={s.label} value={s.label}>{s.label.split(' ')[0]}</option>)} </select> </div> <button onClick={randomizeMap} disabled={isRunning} className="p-1.5 sm:p-2 hover:bg-slate-700 rounded-lg text-indigo-400 disabled:opacity-30 relative"> <Dices size={16}/> </button> <button onClick={() => setShowReport(true)} disabled={isRunning} className="p-1.5 sm:p-2 hover:bg-slate-700 rounded-lg text-blue-400 disabled:opacity-30 relative"> <BarChart3 size={16}/> {logs.length > 0 && <span className="absolute top-1 right-1 w-2 h-2 bg-blue-500 rounded-full"></span>} </button> <button onClick={() => initGrid(true)} disabled={isRunning} className="p-1.5 sm:p-2 hover:bg-slate-700 rounded-lg text-yellow-400 disabled:opacity-30"><RefreshCw size={16}/></button> <button onClick={() => initGrid(false)} disabled={isRunning} className="p-1.5 sm:p-2 hover:bg-slate-700 rounded-lg text-rose-400 disabled:opacity-30"><Trash2 size={16}/></button> </div> </header> {/* Sub-Header / Settings - Added flex-wrap for mobile */} <div className="flex-none bg-slate-900/50 border-b border-slate-800 px-2 py-2 flex flex-wrap gap-2 items-center justify-center sm:justify-start overflow-y-auto max-h-[80px]"> <div className="flex items-center bg-slate-800 rounded-md px-2 py-1 gap-1 border border-slate-700 shrink-0"> <Settings2 size={14} className="text-indigo-400" /> <select value={algo} onChange={(e) => setAlgo(e.target.value)} disabled={isRunning} className="bg-transparent text-xs font-medium text-slate-200 outline-none cursor-pointer appearance-none max-w-[100px] sm:max-w-none"> {ALGOS.map(a => <option key={a} value={a}>{a}</option>)} </select> </div> <div className="flex items-center bg-slate-800 rounded-md px-2 py-1 gap-2 border border-slate-700 shrink-0 grow sm:grow-0 sm:min-w-[150px]"> {speed === 100 ? <Zap size={14} className="text-yellow-400 animate-bounce"/> : <Gauge size={14} className="text-slate-400"/>} <input type="range" min="1" max="100" value={speed} onChange={(e) => setSpeed(parseInt(e.target.value))} className="w-full h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-blue-500" /> </div> <div className="flex items-center bg-slate-800 rounded-md px-2 py-1 gap-2 border border-slate-700 shrink-0 ml-auto sm:ml-0"> {visibility === VISIBILITY.GOD ? <Eye size={14} className="text-emerald-400"/> : <EyeOff size={14} className="text-amber-400"/>} <select value={visibility} onChange={(e) => setVisibility(e.target.value)} disabled={isRunning} className="bg-transparent text-xs font-medium text-slate-200 outline-none cursor-pointer max-w-[80px] sm:max-w-none"> {Object.values(VISIBILITY).map(v => <option key={v} value={v}>{v.split(' ')[0]}</option>)} </select> </div> </div> {algo === 'Custom Script' && !showReport && ( <button onClick={() => setShowCode(true)} className="absolute top-[110px] left-1/2 -translate-x-1/2 z-30 flex items-center bg-indigo-600 hover:bg-indigo-500 text-white rounded-b-md px-4 py-1 text-xs font-bold gap-1 shadow-lg"> <Code size={12}/> Edit Script </button> )} {/* Main Grid - adjusted padding for mobile */} <main className="flex-1 relative flex items-center justify-center p-2 bg-slate-950 overflow-hidden"> <div className="relative grid gap-0 border border-slate-800 rounded overflow-hidden shadow-2xl bg-slate-900 cursor-crosshair" onPointerUp={() => { isDrawing.current = false; }} onPointerLeave={() => { isDrawing.current = false; }} onPointerMove={(e) => { if (!isDrawing.current || isRunning) return; const el = document.elementFromPoint(e.clientX, e.clientY); if (el?.dataset?.row) handleGridClick(parseInt(el.dataset.row), parseInt(el.dataset.col)); }} style={{ gridTemplateColumns: `repeat(${sizeCfg.c}, minmax(0, 1fr))`, width: '100%', height: '100%', maxHeight: '65vh', aspectRatio: `${sizeCfg.c}/${sizeCfg.r}` }} > {grid.map((row, r) => row.map((cell, c) => ( <div key={`${r}-${c}`} data-row={r} data-col={c} onPointerDown={(e) => { e.target.releasePointerCapture(e.pointerId); if(!isRunning) { isDrawing.current = true; handleGridClick(r, c); } }} className={getCellClass(r, c)} > {(cell.isStart || cell.isGoal) && ( <div className="pointer-events-none relative z-10"> {cell.isStart && !robotPos && <div className="w-2 h-2 bg-emerald-500 rounded-full shadow-[0_0_8px_#10b981]" />} {cell.isGoal && <Flag size={sizeCfg.r > 40 ? 10 : 16} className="text-rose-500" fill="currentColor" />} </div> )} </div> )))} {robotPos && ( <div className="absolute flex items-center justify-center transition-all duration-75 ease-linear z-30 pointer-events-none" style={{ width: `${100 / sizeCfg.c}%`, height: `${100 / sizeCfg.r}%`, top: `${(robotPos.row / sizeCfg.r) * 100}%`, left: `${(robotPos.col / sizeCfg.c) * 100}%`, transform: `rotate(${robotPos.dir}deg)` }}> <Bot size={sizeCfg.r > 40 ? 12 : 24} className="text-blue-400 relative z-10 drop-shadow-lg" /> </div> )} </div> </main> {/* Toolbar - Added pb-safe and adjusted height */} <div className="flex-none bg-slate-900 border-t border-slate-800 p-2 pb-8 sm:pb-6 safe-area-pb"> <div className="flex justify-between max-w-md mx-auto gap-2 items-end px-1"> <div className="flex-1 flex gap-1 bg-slate-800/50 p-1 rounded-xl backdrop-blur-sm border border-slate-800"> <ToolBtn active={mode === 'WALL'} onClick={() => setMode('WALL')} icon={BrickWall} /> <ToolBtn active={mode === 'ERASE'} onClick={() => setMode('ERASE')} icon={Eraser} /> <div className="w-px bg-slate-700 mx-0.5 my-1"></div> <ToolBtn active={mode === 'START'} onClick={() => setMode('START')} icon={Bot} color="text-emerald-400" /> <ToolBtn active={mode === 'GOAL'} onClick={() => setMode('GOAL')} icon={Flag} color="text-rose-400" /> </div> <button onClick={startSimulation} className={`h-12 px-4 sm:px-5 rounded-xl font-bold shadow-lg flex items-center justify-center gap-2 transition-all ${isRunning ? 'bg-rose-600 hover:bg-rose-500 text-white animate-pulse' : 'bg-blue-600 hover:bg-blue-500 text-white active:scale-95'}`}> {isRunning ? <Octagon size={20} fill="currentColor" /> : <Play size={20} fill="currentColor" />} <span className="text-xs hidden sm:block">{isRunning ? 'STOP' : 'GO'}</span> </button> </div> </div> {/* Code Modal */} {showCode && ( <div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"> <div className="bg-slate-900 w-full max-w-2xl h-[80vh] rounded-xl border border-slate-700 flex flex-col shadow-2xl"> <div className="flex justify-between items-center p-4 border-b border-slate-800 bg-slate-900 rounded-t-xl"> <h3 className="text-white font-bold flex items-center gap-2"><Code size={18}/> Custom Algorithm</h3> <button onClick={() => setShowCode(false)} className="text-slate-400 hover:text-white"><X size={20}/></button> </div> <textarea className="w-full h-full bg-[#0d1117] text-slate-300 p-4 font-mono text-sm resize-none focus:outline-none" value={customCode} onChange={(e) => setCustomCode(e.target.value)} spellCheck="false" /> </div> </div> )} {/* Report Modal */} {showReport && ( <ReportModal logs={logs} onClose={() => setShowReport(false)} onClear={() => { setLogs([]); localStorage.removeItem('roboPathLogs'); }} /> )} <style>{` /* Safe area padding support for iPhone X and newer */ .safe-area-pb { padding-bottom: env(safe-area-inset-bottom, 20px); } `}</style> </div> ); } // --- Report Component --- const ReportModal = ({ logs, onClose, onClear }) => { const [tab, setTab] = useState('HISTORY'); // Group logs by mapHash const groupedLogs = useMemo(() => { const groups = {}; logs.forEach(log => { if (!groups[log.mapHash]) groups[log.mapHash] = []; groups[log.mapHash].push(log); }); return Object.values(groups).filter(g => g.length > 1); // Only show groups with multiple runs for comparison }, [logs]); return ( <div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4 animate-in fade-in duration-200"> <div className="bg-slate-900 w-full max-w-3xl h-[85vh] rounded-xl border border-slate-700 flex flex-col shadow-2xl overflow-hidden"> <div className="flex justify-between items-center p-4 border-b border-slate-800 bg-slate-950"> <div className="flex items-center gap-3"> <BarChart3 size={20} className="text-blue-400"/> <h3 className="text-white font-bold text-lg">Simulation Report</h3> </div> <button onClick={onClose} className="p-1 hover:bg-slate-800 rounded text-slate-400 hover:text-white"><X size={20}/></button> </div> {/* Tabs */} <div className="flex border-b border-slate-800 bg-slate-900"> <button onClick={() => setTab('HISTORY')} className={`flex-1 py-3 text-xs font-bold flex items-center justify-center gap-2 ${tab === 'HISTORY' ? 'text-blue-400 border-b-2 border-blue-400 bg-slate-800/50' : 'text-slate-500 hover:text-slate-300'}`}> <History size={14}/> RUN HISTORY </button> <button onClick={() => setTab('COMPARE')} className={`flex-1 py-3 text-xs font-bold flex items-center justify-center gap-2 ${tab === 'COMPARE' ? 'text-emerald-400 border-b-2 border-emerald-400 bg-slate-800/50' : 'text-slate-500 hover:text-slate-300'}`}> <TrendingUp size={14}/> MAP COMPARISON <span className="bg-slate-700 px-1.5 rounded-full text-[10px]">{groupedLogs.length}</span> </button> </div> {/* Content */} <div className="flex-1 overflow-y-auto p-4 bg-slate-950/50"> {logs.length === 0 ? ( <div className="h-full flex flex-col items-center justify-center text-slate-500 gap-2"> <History size={48} className="opacity-20"/> <p>No simulations run yet.</p> </div> ) : ( <> {tab === 'HISTORY' && ( <div className="space-y-2"> {logs.map((log) => ( <div key={log.id} className="bg-slate-900 border border-slate-800 rounded-lg p-3 flex justify-between items-center text-sm hover:border-slate-700 transition-colors"> <div> <div className="font-bold text-slate-200 flex items-center gap-2"> {log.algo} <span className={`text-[10px] px-1.5 rounded ${log.success ? 'bg-emerald-500/20 text-emerald-400' : 'bg-rose-500/20 text-rose-400'}`}>{log.success ? 'SUCCESS' : 'FAILED'}</span> </div> <div className="text-xs text-slate-500 flex gap-2 mt-1"> <span>{log.date}</span> <span>•</span> <span>{log.mapSize}</span> <span>•</span> <span>{log.mode}</span> </div> </div> <div className="text-right flex gap-4 text-xs"> <div className="flex flex-col items-end"> <span className="text-slate-400 uppercase text-[10px]">Time</span> <span className="font-mono text-indigo-400">{log.duration}ms</span> </div> <div className="flex flex-col items-end"> <span className="text-slate-400 uppercase text-[10px]">Steps</span> <span className="font-mono text-amber-400">{log.nodesVisited}</span> </div> <div className="flex flex-col items-end"> <span className="text-slate-400 uppercase text-[10px]">Path</span> <span className="font-mono text-blue-400">{log.pathLength}</span> </div> </div> </div> ))} </div> )} {tab === 'COMPARE' && ( <div className="space-y-6"> {groupedLogs.length === 0 ? ( <div className="text-center text-slate-500 mt-10"> <p>Run different algorithms on the <b>same map</b> to see comparisons here.</p> </div> ) : groupedLogs.map((group, idx) => ( <div key={idx} className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden"> <div className="bg-slate-800/50 p-3 border-b border-slate-800 flex justify-between"> <span className="text-xs font-bold text-slate-300 uppercase tracking-wider">Map Configuration #{idx + 1}</span> <span className="text-[10px] text-slate-500">{group[0].mapSize}</span> </div> <div className="overflow-x-auto"> <table className="w-full text-xs text-left"> <thead className="text-slate-500 bg-slate-950/50"> <tr> <th className="p-3">Algorithm</th> <th className="p-3 text-right">Time (ms)</th> <th className="p-3 text-right">Search Steps</th> <th className="p-3 text-right">Path Length</th> </tr> </thead> <tbody className="divide-y divide-slate-800"> {group.map(run => ( <tr key={run.id} className="hover:bg-slate-800/30"> <td className="p-3 font-medium text-slate-200">{run.algo}</td> <td className="p-3 text-right font-mono text-indigo-400">{run.duration}</td> <td className="p-3 text-right font-mono text-amber-400">{run.nodesVisited}</td> <td className="p-3 text-right font-mono text-blue-400">{run.pathLength}</td> </tr> ))} </tbody> </table> </div> </div> ))} </div> )} </> )} </div> {/* Footer */} <div className="p-3 border-t border-slate-800 bg-slate-900 flex justify-between items-center"> <span className="text-[10px] text-slate-500">Stored in LocalStorage</span> <button onClick={onClear} className="flex items-center gap-2 text-xs text-rose-400 hover:text-rose-300 px-3 py-2 rounded hover:bg-rose-900/20 transition-colors"> <Trash2 size={14}/> Clear History </button> </div> </div> </div> ); }; const ToolBtn = ({ active, onClick, icon: Icon, color = 'text-slate-300' }) => ( <button onClick={onClick} className={`flex-1 flex items-center justify-center h-10 rounded-lg transition-all ${active ? 'bg-slate-700 shadow-inner scale-95 ring-1 ring-white/10' : 'hover:bg-slate-800'}`}> <Icon size={18} className={`${active ? color : 'text-slate-500'}`} /> </button> );
No revisions found. Save the file to create a backup.
Delete
Update App