diff --git a/.agents/mcp_config.json b/.agents/mcp_config.json deleted file mode 100644 index 4047ca9..0000000 --- a/.agents/mcp_config.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "mcpServers": { - "playwright": { - "command": "npx", - "args": [ - "-y", - "@executeautomation/playwright-mcp-server" - ] - }, - "filesystem": { - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-filesystem", - "C:/Users/HP/eclipse-workspace/AlgorithmRaceVisualizerCopy1" - ] - }, - "puppeteer": { - "command": "npx", - "args": [ - "-y", - "@modelcontextprotocol/server-puppeteer" - ] - }, - "context7": { - "command": "npx", - "args": [ - "-y", - "@upstash/context7-mcp@latest" - ] - } - } -} diff --git a/.vscode/settings.json b/.vscode/settings.json index ccf4e69..e012065 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,4 @@ { "java.compile.nullAnalysis.mode": "automatic", - "java.configuration.updateBuildConfiguration": "interactive", - "mcp.servers.data-agent-kit.enabled": false, - "mcp.servers.notebooks.enabled": false, - "mcp.servers.visualization.enabled": false + "java.configuration.updateBuildConfiguration": "interactive" } \ No newline at end of file diff --git a/ROADMAP_AND_PLAN.md b/ROADMAP_AND_PLAN.md deleted file mode 100644 index 874e024..0000000 --- a/ROADMAP_AND_PLAN.md +++ /dev/null @@ -1,135 +0,0 @@ -# Algorithm Race Visualizer (AlgoRace) -## Strategic Product Analysis & Market-Ready Implementation Roadmap - -> **Audit Date**: August 2026 -> **Status**: Workspace Audit & Production Gap Analysis Complete - ---- - -## 1. Executive Summary & Codebase Audit - -We audited the **Google AI Studio Product Feedback** against the current codebase (`AlgorithmRaceVisualizerCopy1`). The project has a strong foundation with several advanced features already implemented (including multi-lane racing, telemetry cards, and multi-language code line tracing). - -Below is the definitive breakdown of **Existing Capabilities**, **Upgrades Needed**, and **Missing Features**. - ---- - -### Matrix of Feature Status - -| Feature Area | Feedback Item | Current Codebase Status | Status | Action Required | -|---|---|---|---|---| -| **Core Racing Engine** | Side-by-Side Head-to-Head Racing | Implemented for Sorting, Searching, & Pathfinding. | โœ… Done | Maintain & expand algorithm set. | -| **Code Line Debugger** | Multi-Language Code Tracing & Line Highlighting | Implemented in `CodeViewer.tsx` (TS, Java, Py, C++) with active line tracking. | โœ… Done | Add "Why Did This Happen?" step explanation popovers. | -| **Playback & Telemetry** | Timeline Scrubber & Stat Panels | Implemented in `Controls.tsx` (frame slider) and `LaneCard.tsx` / `AlgorithmComparisonCenter.tsx`. | โœ… Done | Add CSV/JSON telemetry exporter. | -| **Custom Inputs** | Custom Array & Grid Canvas Drawing | `arrayParser.ts` exists, but UI modal & drag-drawing on canvas grid are missing. | ๐ŸŸก Partial | Add Custom Input Modal & Interactive Canvas Painter. | -| **Audio Feedback** | Value-to-Pitch Synthesizer | Basic sound triggers exist in `AudioContext.tsx`. | ๐ŸŸก Partial | Upgrade to Web Audio API polyphonic value-to-frequency pitch mapping. | -| **Styling & Tokens** | Design Token Architecture & Accessibility | Hardcoded CSS colors in `styles.css`. | ๐ŸŸก Partial | Refactor to `:root` design tokens & add Colorblind (Deuteranopia) mode. | -| **Mobile Experience** | Mobile Responsive Stacked / Tabbed View | Multi-lane grid breaks on viewports <768px. | โŒ Missing | Build Mobile Tabbed / Stacked View with touch controls. | -| **Performance Engine** | Web Worker & Canvas/WebGL Renderer | React DOM `
` bars cause frame drops for $N > 500$. | โŒ Missing | Implement HTML5 Canvas / Web Worker streaming engine. | -| **Pathfinding Extras** | Maze Generators & Weighted Terrain | Only manual walls exist on grid. | โŒ Missing | Add Maze Generators (Recursive Division, Prim's) & Terrain Weights (Mud/Water). | -| **New Categories** | Dynamic Programming & Data Structure Trees | Only Array Sorting, Searching, and 2D Pathfinding exist. | โŒ Missing | Add Dynamic Programming Matrix (Knapsack, LCS) & BST/AVL Tree Visualizers. | -| **Share & Export** | URL Permalinks & GIF/MP4 Export | No URL encoding or video recording exists. | โŒ Missing | Implement URL Hash State & CanvasRecorder GIF/MP4 Exporter. | -| **Gamification** | LeetCode Prep & Quiz Mode | No quiz or diagnostic assessment mode. | โŒ Missing | Build Interactive Diagnostic Quiz & LeetCode Prep Arena. | - ---- - -## 2. Multi-Phase Future Implementation Plan - -```mermaid -flowchart TD - Phase1["Phase 1: Performance & Mobile Optimization"] --> Phase2["Phase 2: Interactive Education & Custom Input"] - Phase2 --> Phase3["Phase 3: Advanced Algorithm Expansion (DP & Trees)"] - Phase3 --> Phase4["Phase 4: SaaS, Sharing & Gamification Engine"] -``` - ---- - -### Phase 1: High-Performance Engine & Mobile Optimization (Immediate Impact) -*Goal: Ensure 60 FPS performance for large datasets ($N \ge 10,000$) and flawless mobile responsive experience.* - -#### 1.1 Web Worker & HTML5 Canvas Rendering Engine -- **Web Worker Offloading**: Move algorithm simulation frame generation out of the main thread into a dedicated Web Worker (`frontend/src/workers/simulationWorker.ts`). -- **Canvas / WebGL Renderer**: Replace React DOM `
` bar elements with HTML5 Canvas (``) rendering for dataset sizes $N > 300$. Capable of animating 10,000+ elements at 60 FPS smoothly. - -#### 1.2 Mobile Responsive & Touch-Optimized Layout -- **Tabbed / Stacked Mobile Layout**: On viewports `< 768px`, replace side-by-side split cards with an intuitive **Tabbed Race View** or **Vertical Stacked Cards** with sticky bottom playback controls. -- **Touch-Friendly Painting**: Optimize Pathfinding canvas wall painting for touch events (`onTouchStart`, `onTouchMove`). - -#### 1.3 Design System & Accessibility (Colorblind Modes) -- **CSS Custom Property Tokens**: Systematically refactor `styles.css` into semantic `:root` design tokens (`--bg-primary`, `--accent-color`, `--state-comparing`). -- **Accessibility Themes**: Add Deuteranopia/Protanopia colorblind palette toggle and High-Contrast Mode. - ---- - -### Phase 2: Interactive Education, Explanations & Custom Datasets -*Goal: Turn visual racing into an interactive learning and debugging environment.* - -#### 2.1 "Why Did This Happen?" Step Explanations -- **Real-time Step Explanations**: Extend `CodeViewer.tsx` with a live explanatory log popover per frame: - - *Example (Quick Sort)*: `"Pivot element 42 selected at index 8. Comparing element 15 (index 2) with pivot..."* - - *Example (A* Search)*: `"Evaluating neighbor (12, 14) with g=14, h=8, f=22. Adding to open set priority queue."* - -#### 2.2 Custom Datasets & Canvas Painting Suite -- **Custom Array Modal**: Modal allowing users to paste custom CSV/JSON array inputs or generate mathematical presets (Sine wave, Gaussian, Nearly Sorted). -- **Maze Generator Suite**: Implement automated maze algorithms for Pathfinding: - - Recursive Division Maze - - Prim's Randomized MST Maze - - Binary Tree Maze Generator -- **Weighted Terrain Painting**: Allow painting weighted cells (Grass = cost 1, Mud = cost 5, Water = wall). - -#### 2.3 Web Audio Value-to-Pitch Synthesizer -- **Polyphonic Web Audio API**: Map element array values to musical pitches (pentatonic scale 220Hz - 880Hz) to generate classic auditory algorithm soundscapes during races. - ---- - -### Phase 3: Algorithm Category Expansion (Dynamic Programming & Trees) -*Goal: Expand beyond sorting and pathfinding into core Computer Science topics.* - -#### 3.1 Dynamic Programming (DP) Grid Visualizer -- **Interactive DP Matrix Table**: - - 0/1 Knapsack Problem - - Longest Common Subsequence (LCS) - - Edit Distance (Levenshtein Distance) -- **Cell Fill Animation**: Visually trace sub-problem lookup arrows (`dp[i-1][j]` vs `dp[i-1][j-w] + v`). - -#### 3.2 Tree & Graph Data Structure Visualizer -- **Binary Search Tree (BST) & AVL Tree**: - - Interactive Node Insertion, Deletion, and Self-Balancing AVL Rotations (LL, RR, LR, RL). - - Red-Black Tree recoloring and tree restructuring visualization. - ---- - -### Phase 4: SaaS Features, Shareability & Gamification (Market Ready) -*Goal: Drive viral growth, portfolio visibility, ed-tech adoption, and monetization capabilities.* - -#### 4.1 URL Permalinks & Shareable State -- **URL Hash Encoder**: Encode race setup, algorithms, seed, dataset size, and speed into URL hash: - `https://algorace.app/race?category=sorting&algo1=quicksort&algo2=heapsort&size=100&seed=89234` -- **One-Click Share Button**: Instant copy-to-clipboard permalink generation. - -#### 4.2 GIF & MP4 Video Export Engine -- **CanvasRecorder / WebM Converter**: Add an "Export Video" button allowing educators and tech influencers to download visualizer animations as `.mp4` or `.gif` files for LinkedIn/X/YouTube. - -#### 4.3 Embeddable Iframe Widget -- **Iframe Embed Code**: Generate light `` snippets for technical blog posts (Medium, Dev.to, Hashnode). - -#### 4.4 LeetCode Prep Diagnostic Quiz Mode -- **Interactive Quiz Arena**: - - Present automated race simulations and ask diagnostic questions: - - *"Which algorithm used less memory during this race and why?"* - - *"What is the worst-case scenario for the losing algorithm on this dataset?"* - - Instant scoring, explanations, and progress badges. - ---- - -## 3. Recommended Execution Priorities - -``` -Priority 1: Mobile Responsiveness & Web Worker Canvas Engine (Phase 1) -Priority 2: Custom Input Modals, Maze Generators & Step Explanations (Phase 2) -Priority 3: Dynamic Programming & Tree Structure Expansion (Phase 3) -Priority 4: Permalinks, Video Exporter & Quiz Mode (Phase 4) -``` - ---- -*Generated for Algorithm Race Visualizer Copy1* diff --git a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/JPSModel.java b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/JPSModel.java new file mode 100644 index 0000000..86671d5 --- /dev/null +++ b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/JPSModel.java @@ -0,0 +1,241 @@ +package com.algorithmrace.visualizer.algorithms.pathfinding; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.PriorityQueue; + +public class JPSModel extends PathfindingModel { + + private final PriorityQueue openSet = + new PriorityQueue<>(Comparator.comparingDouble(GridCell::fCost)); + + public JPSModel() { + super("Jump Point Search"); + } + + private double heuristic(GridCell a, GridCell b) { + int dx = Math.abs(a.row - b.row); + int dy = Math.abs(a.col - b.col); + return Math.max(dx, dy) + (Math.sqrt(2) - 1) * Math.min(dx, dy); + } + + @Override + public void step() { + if (isDone() || openSet.isEmpty()) { + markDone(); + return; + } + GridCell current = openSet.poll(); + if (current == end) { + reconstructPath(end); + markDone(); + return; + } + if (current.state != CellState.START) { + current.state = CellState.VISITED; + } + addStep(); + + List successors = identifySuccessors(current); + for (GridCell nb : successors) { + double dist = + Math.sqrt(Math.pow(current.row - nb.row, 2) + Math.pow(current.col - nb.col, 2)); + double tentativeG = current.gCost + dist; + if (tentativeG < nb.gCost) { + nb.gCost = tentativeG; + nb.hCost = heuristic(nb, end); + nb.parent = current; + if (nb.state == CellState.EMPTY || nb.state == CellState.VISITED) { + nb.state = CellState.FRONTIER; + } + if (!openSet.contains(nb)) { + openSet.add(nb); + } + } + } + } + + private List identifySuccessors(GridCell current) { + List successors = new ArrayList<>(); + List neighbors = getPrunedNeighbors(current); + for (GridCell neighbor : neighbors) { + int dRow = neighbor.row - current.row; + int dCol = neighbor.col - current.col; + GridCell jumpPoint = jump(current.row, current.col, dRow, dCol); + if (jumpPoint != null) { + successors.add(jumpPoint); + } + } + return successors; + } + + private List getPrunedNeighbors(GridCell current) { + List neighbors = new ArrayList<>(); + if (current.parent == null) { + for (int dr = -1; dr <= 1; dr++) { + for (int dc = -1; dc <= 1; dc++) { + if (dr == 0 && dc == 0) continue; + if (isValid(current.row + dr, current.col + dc)) { + if (dr != 0 && dc != 0) { + if (isValid(current.row + dr, current.col) + || isValid(current.row, current.col + dc)) { + neighbors.add(grid[current.row + dr][current.col + dc]); + } + } else { + neighbors.add(grid[current.row + dr][current.col + dc]); + } + } + } + } + return neighbors; + } + + int dRow = Integer.compare(current.row, current.parent.row); + int dCol = Integer.compare(current.col, current.parent.col); + + if (dRow != 0 && dCol != 0) { + boolean vRow = isValid(current.row + dRow, current.col); + boolean vCol = isValid(current.row, current.col + dCol); + if (vRow) neighbors.add(grid[current.row + dRow][current.col]); + if (vCol) neighbors.add(grid[current.row][current.col + dCol]); + if (vRow || vCol) { + if (isValid(current.row + dRow, current.col + dCol)) { + neighbors.add(grid[current.row + dRow][current.col + dCol]); + } + } + if (!isValid(current.row - dRow, current.col) && vCol) { + if (isValid(current.row - dRow, current.col + dCol)) { + neighbors.add(grid[current.row - dRow][current.col + dCol]); + } + } + if (!isValid(current.row, current.col - dCol) && vRow) { + if (isValid(current.row + dRow, current.col - dCol)) { + neighbors.add(grid[current.row + dRow][current.col - dCol]); + } + } + } else { + if (dRow != 0) { + if (isValid(current.row + dRow, current.col)) { + neighbors.add(grid[current.row + dRow][current.col]); + if (!isValid(current.row, current.col + 1)) { + if (isValid(current.row + dRow, current.col + 1)) { + neighbors.add(grid[current.row + dRow][current.col + 1]); + } + } + if (!isValid(current.row, current.col - 1)) { + if (isValid(current.row + dRow, current.col - 1)) { + neighbors.add(grid[current.row + dRow][current.col - 1]); + } + } + } + } else { + if (isValid(current.row, current.col + dCol)) { + neighbors.add(grid[current.row][current.col + dCol]); + if (!isValid(current.row + 1, current.col)) { + if (isValid(current.row + 1, current.col + dCol)) { + neighbors.add(grid[current.row + 1][current.col + dCol]); + } + } + if (!isValid(current.row - 1, current.col)) { + if (isValid(current.row - 1, current.col + dCol)) { + neighbors.add(grid[current.row - 1][current.col + dCol]); + } + } + } + } + } + return neighbors; + } + + private boolean isValid(int r, int c) { + return r >= 0 && r < rows && c >= 0 && c < cols && grid[r][c].state != CellState.WALL; + } + + private GridCell jump(int r, int c, int dRow, int dCol) { + while (true) { + int nextR = r + dRow; + int nextC = c + dCol; + + if (!isValid(nextR, nextC)) { + return null; + } + + if (dRow != 0 && dCol != 0) { + if (!isValid(r + dRow, c) && !isValid(r, c + dCol)) { + return null; + } + } + + GridCell nextCell = grid[nextR][nextC]; + if (nextCell == end) { + return nextCell; + } + + if (nextCell.state == CellState.EMPTY) { + nextCell.state = CellState.VISITED; + } + + if (dRow != 0 && dCol != 0) { + if ((!isValid(nextR - dRow, nextC) && isValid(nextR - dRow, nextC + dCol)) + || (!isValid(nextR, nextC - dCol) && isValid(nextR + dRow, nextC - dCol))) { + return nextCell; + } + if (jump(nextR, nextC, dRow, 0) != null || jump(nextR, nextC, 0, dCol) != null) { + return nextCell; + } + } else { + if (dRow != 0) { + if ((!isValid(nextR, nextC + 1) && isValid(nextR + dRow, nextC + 1)) + || (!isValid(nextR, nextC - 1) && isValid(nextR + dRow, nextC - 1))) { + return nextCell; + } + } else { + if ((!isValid(nextR + 1, nextC) && isValid(nextR + 1, nextC + dCol)) + || (!isValid(nextR - 1, nextC) && isValid(nextR - 1, nextC + dCol))) { + return nextCell; + } + } + } + r = nextR; + c = nextC; + } + } + + @Override + protected void reconstructPath(GridCell endCell) { + path.clear(); + GridCell current = endCell; + while (current != null && current.parent != null) { + GridCell parent = current.parent; + int r = current.row; + int c = current.col; + int pr = parent.row; + int pc = parent.col; + + int dRow = Integer.compare(r, pr); + int dCol = Integer.compare(c, pc); + + while (r != pr || c != pc) { + path.add(0, grid[r][c]); + r -= dRow; + c -= dCol; + } + current = parent; + } + if (current != null) { + path.add(0, current); + } + pathFound = true; + } + + @Override + public void reset() { + openSet.clear(); + resetStats(); + if (start != null) { + start.gCost = 0; + openSet.add(start); + } + } +} diff --git a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingFactory.java b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingFactory.java index 54aac29..0f71564 100644 --- a/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingFactory.java +++ b/backend/src/main/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingFactory.java @@ -14,6 +14,7 @@ public static PathfindingModel create(String name) { case "Bellman-Ford" -> new BellmanFordModel(); case "Bidirectional BFS" -> new BidirectionalBFSModel(); case "Greedy Best-First" -> new GreedyBFSModel(); + case "Jump Point Search" -> new JPSModel(); default -> throw new IllegalArgumentException("Unrecognized pathfinding algorithm requested."); }; @@ -27,6 +28,7 @@ public static List allNames() { "A* Search", "Bellman-Ford", "Greedy Best-First", - "Bidirectional BFS"); + "Bidirectional BFS", + "Jump Point Search"); } } diff --git a/backend/src/main/java/com/algorithmrace/visualizer/utils/ComplexityCatalog.java b/backend/src/main/java/com/algorithmrace/visualizer/utils/ComplexityCatalog.java index 0699355..dc697e8 100644 --- a/backend/src/main/java/com/algorithmrace/visualizer/utils/ComplexityCatalog.java +++ b/backend/src/main/java/com/algorithmrace/visualizer/utils/ComplexityCatalog.java @@ -223,6 +223,14 @@ public final class ComplexityCatalog { "O(b^(d/2))", "Explores from both start and end, halving the search depth.", "expand forward\nexpand backward\nstop when frontiers intersect"); + add( + "Jump Point Search", + "O(E)", + "O(E)", + "O(b^d)", + "O(V)", + "Optimization of A* on uniform-cost grids that skips symmetric paths by jumping across straight lines.", + "identify successors by jumping\nevaluate jump points and update fScores\nreconstruct path"); add( "Bellman-Ford", "O(V*E)", diff --git a/backend/src/test/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingAlgorithmsTest.java b/backend/src/test/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingAlgorithmsTest.java index 130ad5a..2985006 100644 --- a/backend/src/test/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingAlgorithmsTest.java +++ b/backend/src/test/java/com/algorithmrace/visualizer/algorithms/pathfinding/PathfindingAlgorithmsTest.java @@ -16,7 +16,7 @@ class PathfindingAlgorithmsTest { private final SimulationService simulationService = new SimulationService(); private final List algorithms = - List.of("BFS", "DFS", "Dijkstra", "A* Search", "Bidirectional BFS"); + List.of("BFS", "DFS", "Dijkstra", "A* Search", "Bidirectional BFS", "Jump Point Search"); @Test @DisplayName("Verify pathfinding models find path in unblocked grid") diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f29a390..994f44f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,9 +1,8 @@ import { useEffect, useState } from 'react'; import { Analytics } from '@vercel/analytics/react'; import { SpeedInsights } from '@vercel/speed-insights/react'; -import { Sun, Moon, Menu } from 'lucide-react'; +import { Sun, Moon, Menu, Zap } from 'lucide-react'; import { Sidebar } from './components/Sidebar'; -import { AlgoRaceLogo } from './components/AlgoRaceLogo'; import { LandingPage } from './pages/LandingPage'; import { HistoryPage } from './pages/HistoryPage'; import { PathfindingPage } from './pages/PathfindingPage'; @@ -12,7 +11,6 @@ import { SettingsPage } from './pages/SettingsPage'; import { SortingPage } from './pages/SortingPage'; import { DPPage } from './pages/DPPage'; import { TreesPage } from './pages/TreesPage'; -import { QuizPage } from './pages/QuizPage'; import type { CatalogResponse } from './models/types'; import { api } from './services/api'; import { AudioCtx } from './context/AudioContext'; @@ -20,9 +18,8 @@ import { useAudioSettings } from './hooks/useAudioSettings'; import { useSound } from './hooks/useSound'; import { fallbackCatalog } from './data/fallbackCatalog'; -import { ThemeProvider } from './context/ThemeContext'; -type Page = 'landing' | 'sorting' | 'searching' | 'pathfinding' | 'dp' | 'trees' | 'quiz' | 'history' | 'settings'; +type Page = 'landing' | 'sorting' | 'searching' | 'pathfinding' | 'dp' | 'trees' | 'history' | 'settings'; const getPageFromHash = (): Page => { const searchParams = new URLSearchParams(window.location.search); @@ -34,7 +31,6 @@ const getPageFromHash = (): Page => { if (pageParam === 'pathfinding') return 'pathfinding'; if (pageParam === 'dp') return 'dp'; if (pageParam === 'trees') return 'trees'; - if (pageParam === 'quiz') return 'quiz'; if (pageParam === 'history') return 'history'; if (pageParam === 'settings') return 'settings'; } @@ -45,12 +41,13 @@ const getPageFromHash = (): Page => { if (hash === 'pathfinding' || hash === 'pathfinding-arena') return 'pathfinding'; if (hash === 'dp' || hash === 'dp-arena' || hash === 'dynamic-programming') return 'dp'; if (hash === 'trees' || hash === 'trees-arena' || hash === 'tree-structures') return 'trees'; - if (hash === 'quiz' || hash === 'leetcode-quiz' || hash === 'quiz-arena') return 'quiz'; if (hash === 'history' || hash === 'benchmarks') return 'history'; if (hash === 'settings') return 'settings'; return 'landing'; }; +import { ThemeProvider } from './context/ThemeContext'; + export default function App() { const [active, setActive] = useState(() => getPageFromHash()); const [catalog, setCatalog] = useState(fallbackCatalog); @@ -121,44 +118,6 @@ export default function App() { } }, [darkMode]); - // Global Keyboard Shortcuts (0-5, H, S, T) - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - const activeTag = document.activeElement?.tagName.toLowerCase(); - if (activeTag === 'input' || activeTag === 'textarea' || activeTag === 'select') { - return; - } - if (e.metaKey || e.ctrlKey || e.altKey) return; - - if (e.key === 'Escape' || e.key === '0') { - setActive('landing'); - } else if (e.key === '1') { - setActive('sorting'); - } else if (e.key === '2') { - setActive('searching'); - } else if (e.key === '3') { - setActive('pathfinding'); - } else if (e.key === '4') { - setActive('dp'); - } else if (e.key === '5') { - setActive('trees'); - } else if (e.key === '6' || e.key === 'q' || e.key === 'Q') { - setActive('quiz'); - } else if (e.key === 'h' || e.key === 'H') { - setActive('history'); - } else if (e.key === 's' || e.key === 'S') { - setActive('settings'); - } else if (e.key === 't' || e.key === 'T') { - setDarkMode((prev: boolean) => !prev); - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, []); - - const isEmbedMode = new URLSearchParams(window.location.search).get('embed') === 'true'; - if (error) { return (
@@ -179,55 +138,15 @@ export default function App() { return ( - {isEmbedMode ? ( -
-
- {active === 'sorting' && } - {active === 'searching' && } - {active === 'pathfinding' && } - {active === 'dp' && } - {active === 'trees' && } - {active === 'quiz' && setActive(page as Page)} />} - {active === 'history' && } - {active === 'settings' && } -
- - AlgoRace - โšก Live - -
- ) : active === 'landing' ? ( + {active === 'landing' ? ( ) : (
{/* Mobile Header Bar */}
-
setActive('landing')} style={{ cursor: 'pointer' }}> - +
setActive('landing')}> + + AlgoRace
-

Benchmark Directory & Asymptotic Curves

+

Benchmark Directory

- Explore asymptotic time and space bounds across all 20+ supported competitive algorithm suites with live Big-O curve synchronization. + Explore asymptotic time and space bounds across supported competitive algorithm suites.

- {/* Integrated Interactive Big-O Growth Curves */} -
- -
-
- {(['all', 'sorting', 'searching', 'pathfinding', 'dp', 'trees'] as const).map((cat) => ( + {(['all', 'sorting', 'searching', 'pathfinding'] as const).map((cat) => ( ))}
@@ -382,7 +300,7 @@ export function AlgorithmMatrix({ onNavigate }: Props) { setSearchQuery(e.target.value)} className="matrix-search-input" @@ -406,12 +324,7 @@ export function AlgorithmMatrix({ onNavigate }: Props) { {filteredAlgorithms.map((algo) => ( - setHoveredComplexity(algo.avgTime)} - onMouseLeave={() => setHoveredComplexity(undefined)} - > +
{algo.name} diff --git a/frontend/src/components/BigOGraph.tsx b/frontend/src/components/BigOGraph.tsx deleted file mode 100644 index 677633c..0000000 --- a/frontend/src/components/BigOGraph.tsx +++ /dev/null @@ -1,537 +0,0 @@ -import { useState, useMemo, useRef } from 'react'; -import { Sliders, Sparkles, TrendingUp, Info } from 'lucide-react'; - -interface Props { - highlightedComplexity?: string; -} - -interface ComplexityClass { - id: string; - name: string; - label: string; - color: string; - glowColor: string; - description: string; - examples: string; - calc: (n: number) => number; -} - -const COMPLEXITY_CLASSES: ComplexityClass[] = [ - { - id: 'O(1)', - name: 'Constant', - label: 'O(1)', - color: '#10b981', - glowColor: 'rgba(16, 185, 129, 0.8)', - description: 'Execution time remains flat regardless of dataset size.', - examples: 'Array index lookup, Hash map get, Push/Pop stack', - calc: () => 1, - }, - { - id: 'O(log n)', - name: 'Logarithmic', - label: 'O(log n)', - color: '#06b6d4', - glowColor: 'rgba(6, 182, 212, 0.8)', - description: 'Search space is halved in each step. Highly scalable for billions of items.', - examples: 'Binary Search, AVL Tree lookup, B-Tree indexing', - calc: (n) => Math.max(1, Math.round(Math.log2(Math.max(1, n)) * 10) / 10), - }, - { - id: 'O(n)', - name: 'Linear', - label: 'O(n)', - color: '#3b82f6', - glowColor: 'rgba(59, 130, 246, 0.8)', - description: 'Execution time grows proportionally with the input size.', - examples: 'Linear Search, Counting Sort, Array traversal', - calc: (n) => n, - }, - { - id: 'O(n log n)', - name: 'Linearithmic', - label: 'O(n log n)', - color: '#f59e0b', - glowColor: 'rgba(245, 158, 11, 0.8)', - description: 'Gold standard for general-purpose comparison-based sorting.', - examples: 'MergeSort, QuickSort (Avg), HeapSort', - calc: (n) => Math.round(n * Math.log2(Math.max(1, n))), - }, - { - id: 'O(n^2)', - name: 'Quadratic', - label: 'O(nยฒ)', - color: '#f43f5e', - glowColor: 'rgba(244, 63, 94, 0.8)', - description: 'Operations grow with the square of input size. Impractical for massive datasets.', - examples: 'BubbleSort, InsertionSort (Worst), SelectionSort', - calc: (n) => Math.pow(n, 2), - }, - { - id: 'O(2^n)', - name: 'Exponential', - label: 'O(2โฟ)', - color: '#ec4899', - glowColor: 'rgba(236, 72, 153, 0.8)', - description: 'Operations double with every added element. Rapidly explodes.', - examples: 'Recursive Fibonacci, Power Set generation, Traveling Salesperson (Brute)', - calc: (n) => Math.pow(2, Math.min(n, 30)), - }, -]; - -export function BigOGraph({ highlightedComplexity }: Props) { - const [nValue, setNValue] = useState(16); - const [selectedClassId, setSelectedClassId] = useState(null); - const [scaleMode, setScaleMode] = useState<'linear' | 'log'>('linear'); - const [hoveredN, setHoveredN] = useState(null); - const svgRef = useRef(null); - - // SVG Coordinate Constants (Optimized for sharp rendering across all viewports) - const width = 680; - const height = 290; - const paddingLeft = 56; - const paddingBottom = 38; - const paddingTop = 22; - const paddingRight = 30; - - const graphWidth = width - paddingLeft - paddingRight; - const graphHeight = height - paddingTop - paddingBottom; - - // Max X and Y bounds for graph rendering - const maxX = 64; - const maxYLinear = 2500; - const maxYLog = 6; // 6 decades (10^0 = 1 to 10^6 = 1,000,000) - - const activeN = hoveredN !== null ? hoveredN : nValue; - - // Map value to SVG Y based on current scale mode - const mapYtoSvg = (val: number): number => { - if (scaleMode === 'linear') { - const clampedY = Math.max(0, val); - return paddingTop + graphHeight - (clampedY / maxYLinear) * graphHeight; - } else { - // Logarithmic scaling: log10(1) = 0 to log10(1000000) = 6 - const logVal = Math.log10(Math.max(1, val)); - const normalized = Math.min(1.2, Math.max(0, logVal / maxYLog)); - return paddingTop + graphHeight - normalized * graphHeight; - } - }; - - // Generate SVG Path for a given complexity class with proper boundary exit (no flatlining) - const generateCurvePath = (calc: (n: number) => number) => { - const points: [number, number][] = []; - const samples = 90; - - for (let i = 1; i <= samples; i++) { - const xVal = (i / samples) * maxX; - const yVal = calc(xVal); - - const svgX = paddingLeft + (xVal / maxX) * graphWidth; - const svgY = mapYtoSvg(yVal); - points.push([svgX, svgY]); - } - - return points.reduce((acc, [x, y], idx) => { - return idx === 0 ? `M ${x.toFixed(1)} ${y.toFixed(1)}` : `${acc} L ${x.toFixed(1)} ${y.toFixed(1)}`; - }, ''); - }; - - // Determine which class is active - const activeClass = useMemo(() => { - if (selectedClassId) { - return COMPLEXITY_CLASSES.find((c) => c.id === selectedClassId) || null; - } - if (highlightedComplexity) { - return ( - COMPLEXITY_CLASSES.find( - (c) => - highlightedComplexity.toLowerCase().includes(c.id.toLowerCase()) || - highlightedComplexity.toLowerCase().includes(c.label.toLowerCase()) - ) || null - ); - } - return null; - }, [selectedClassId, highlightedComplexity]); - - // Handle interactive SVG scrubbing - const handleSvgPointerMove = (e: React.PointerEvent) => { - if (!svgRef.current) return; - const rect = svgRef.current.getBoundingClientRect(); - const clientX = e.clientX - rect.left; - const svgX = (clientX / rect.width) * width; - - if (svgX >= paddingLeft && svgX <= width - paddingRight) { - const ratio = (svgX - paddingLeft) / graphWidth; - const calculatedN = Math.max(1, Math.min(maxX, Math.round(ratio * maxX))); - setHoveredN(calculatedN); - } - }; - - const handleSvgPointerLeave = () => { - setHoveredN(null); - }; - - const handleSvgClick = (e: React.PointerEvent) => { - if (!svgRef.current) return; - const rect = svgRef.current.getBoundingClientRect(); - const clientX = e.clientX - rect.left; - const svgX = (clientX / rect.width) * width; - - if (svgX >= paddingLeft && svgX <= width - paddingRight) { - const ratio = (svgX - paddingLeft) / graphWidth; - const calculatedN = Math.max(1, Math.min(maxX, Math.round(ratio * maxX))); - setNValue(calculatedN); - } - }; - - const formatOps = (ops: number): string => { - if (ops >= 1000000) return `${(ops / 1000000).toFixed(1)}M`; - if (ops >= 1000) return `${(ops / 1000).toFixed(1)}k`; - return String(ops); - }; - - const markerX = paddingLeft + (activeN / maxX) * graphWidth; - - return ( -
- {/* Header with Title and Interactive Controls */} -
-
-
- -
-
-

Asymptotic Complexity Growth Curves

-

- Visualizing mathematical operation growth curves (N = 1 to 64) across algorithmic complexity classes. -

-
-
- - {/* Dataset Size Controls & Scale Mode Switcher */} -
- {/* Scale Toggle: Linear vs Logarithmic */} -
- - -
- - {/* Dynamic N Slider */} -
-
- - Dataset Size: - N = {activeN} -
- { - const val = Number(e.target.value); - setNValue(val); - setHoveredN(null); - }} - className="n-range-slider" - aria-label="Adjust dataset size N" - /> -
- {[8, 16, 32, 64].map((preset) => ( - - ))} -
-
-
-
- - {/* SVG Growth Chart with Responsive ViewBox & ClipPath */} -
- - - {/* Strict plot clip boundary to ensure curves exit smoothly without breaking graph borders */} - - - - - - {/* Plot Background Accent */} - - - {/* Coordinate Axes */} - - - - {/* Horizontal Grid Guide Lines */} - {[0.25, 0.5, 0.75].map((fraction) => { - const y = paddingTop + graphHeight * (1 - fraction); - const labelVal = scaleMode === 'linear' - ? `${Math.round(maxYLinear * fraction)}` - : `10^${(maxYLog * fraction).toFixed(0)}`; - - return ( - - - - {labelVal} - - - ); - })} - - {/* Vertical Grid Guide Lines */} - {[0.25, 0.5, 0.75, 1].map((fraction) => { - const x = paddingLeft + graphWidth * fraction; - const labelN = Math.round(maxX * fraction); - return ( - - - - {labelN} - - - ); - })} - - {/* Render Complexity Curves (Clipped smoothly to graph bounds) */} - - {COMPLEXITY_CLASSES.map((cls) => { - const pathData = generateCurvePath(cls.calc); - const isHighlighted = activeClass?.id === cls.id; - const isDimmed = activeClass !== null && !isHighlighted; - - return ( - - {/* Glow Aura for Highlighted Curve */} - {isHighlighted && ( - - )} - - - - ); - })} - - - {/* Interactive Crosshair & Cursor Line */} - - - - {/* Glowing Points on Every Curve at Active N */} - {COMPLEXITY_CLASSES.map((cls) => { - const ops = cls.calc(activeN); - const pointY = mapYtoSvg(ops); - const isHighlighted = activeClass?.id === cls.id; - const isDimmed = activeClass !== null && !isHighlighted; - - // Only render point if within graph bounds - if (pointY < paddingTop - 4 || pointY > paddingTop + graphHeight + 4) return null; - - return ( - - ); - })} - - {/* Bottom N Pill Indicator */} - - - - {/* Axis Labels */} - - {scaleMode === 'linear' ? 'Operations (Ops)' : 'Logโ‚โ‚€ Ops'} - - - Input Size (N) โ†’ - - -
- - {/* Complexity Class Legend & Live Counter Pills */} -
- {COMPLEXITY_CLASSES.map((cls) => { - const ops = cls.calc(activeN); - const formattedOps = formatOps(ops); - const isSelected = activeClass?.id === cls.id; - - return ( - - ); - })} -
- - {/* Active Complexity Detail Card */} - {activeClass && ( -
-
- - - {activeClass.label} ({activeClass.name}) - - - โ‰ˆ {activeClass.calc(activeN).toLocaleString()} operations at N = {activeN} - -
-

{activeClass.description}

-
- - Common in: {activeClass.examples} -
-
- )} -
- ); -} diff --git a/frontend/src/components/CodePlayground.tsx b/frontend/src/components/CodePlayground.tsx deleted file mode 100644 index 8aabc39..0000000 --- a/frontend/src/components/CodePlayground.tsx +++ /dev/null @@ -1,625 +0,0 @@ -import { useState, useEffect } from 'react'; -import { - Code2, - Copy, - Check, - Play, - Pause, - RotateCcw, - SkipForward, - Terminal, - Sparkles, -} from 'lucide-react'; - -type SupportedLanguage = 'typescript' | 'python' | 'java' | 'cpp'; -type SupportedAlgorithm = 'quicksort' | 'binarysearch' | 'astar' | 'knapsack' | 'avl'; - -interface CodeSnippet { - lines: string[]; - activeStepLines: number[]; // Maps step index (0..n) to 1-based line number in `lines` -} - -const CODE_DATABASE: Record> = { - quicksort: { - typescript: { - lines: [ - 'function quickSort(arr: number[], low: number, high: number): void {', - ' if (low < high) {', - ' // Partition array and get pivot index', - ' const pivotIdx = partition(arr, low, high);', - ' // Recursively sort left and right partitions', - ' quickSort(arr, low, pivotIdx - 1);', - ' quickSort(arr, pivotIdx + 1, high);', - ' }', - '}', - '', - 'function partition(arr: number[], low: number, high: number): number {', - ' const pivot = arr[high];', - ' let i = low - 1;', - ' for (let j = low; j < high; j++) {', - ' if (arr[j] < pivot) {', - ' i++;', - ' [arr[i], arr[j]] = [arr[j], arr[i]]; // Swap', - ' }', - ' }', - ' [arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];', - ' return i + 1;', - '}', - ], - activeStepLines: [2, 4, 12, 14, 17, 20, 6, 7], - }, - python: { - lines: [ - 'def quick_sort(arr: list[int], low: int, high: int) -> None:', - ' if low < high:', - ' # Partition array around dynamic pivot', - ' pivot_idx = partition(arr, low, high)', - ' # Recursively conquer sub-arrays', - ' quick_sort(arr, low, pivot_idx - 1)', - ' quick_sort(arr, pivot_idx + 1, high)', - '', - 'def partition(arr: list[int], low: int, high: int) -> int:', - ' pivot = arr[high]', - ' i = low - 1', - ' for j in range(low, high):', - ' if arr[j] < pivot:', - ' i += 1', - ' arr[i], arr[j] = arr[j], arr[i]', - ' arr[i + 1], arr[high] = arr[high], arr[i + 1]', - ' return i + 1', - ], - activeStepLines: [2, 4, 10, 12, 15, 16, 6, 7], - }, - java: { - lines: [ - 'public class QuickSort {', - ' public static void sort(int[] arr, int low, int high) {', - ' if (low < high) {', - ' int pIndex = partition(arr, low, high);', - ' sort(arr, low, pIndex - 1);', - ' sort(arr, pIndex + 1, high);', - ' }', - ' }', - '', - ' private static int partition(int[] arr, int low, int high) {', - ' int pivot = arr[high];', - ' int i = (low - 1);', - ' for (int j = low; j < high; j++) {', - ' if (arr[j] < pivot) {', - ' i++;', - ' swap(arr, i, j);', - ' }', - ' }', - ' swap(arr, i + 1, high);', - ' return i + 1;', - ' }', - '}', - ], - activeStepLines: [3, 4, 11, 13, 16, 19, 5, 6], - }, - cpp: { - lines: [ - '#include ', - '#include ', - '', - 'int partition(std::vector& arr, int low, int high) {', - ' int pivot = arr[high];', - ' int i = low - 1;', - ' for (int j = low; j < high; j++) {', - ' if (arr[j] < pivot) {', - ' i++;', - ' std::swap(arr[i], arr[j]);', - ' }', - ' }', - ' std::swap(arr[i + 1], arr[high]);', - ' return i + 1;', - '}', - '', - 'void quickSort(std::vector& arr, int low, int high) {', - ' if (low < high) {', - ' int p = partition(arr, low, high);', - ' quickSort(arr, low, p - 1);', - ' quickSort(arr, p + 1, high);', - ' }', - '}', - ], - activeStepLines: [18, 19, 5, 7, 10, 13, 20, 21], - }, - }, - - binarysearch: { - typescript: { - lines: [ - 'function binarySearch(arr: number[], target: number): number {', - ' let low = 0;', - ' let high = arr.length - 1;', - '', - ' while (low <= high) {', - ' const mid = Math.floor((low + high) / 2);', - ' if (arr[mid] === target) return mid; // Found!', - ' if (arr[mid] < target) {', - ' low = mid + 1; // Discard left half', - ' } else {', - ' high = mid - 1; // Discard right half', - ' }', - ' }', - ' return -1; // Not found', - '}', - ], - activeStepLines: [2, 3, 5, 6, 8, 9, 7], - }, - python: { - lines: [ - 'def binary_search(arr: list[int], target: int) -> int:', - ' low = 0', - ' high = len(arr) - 1', - '', - ' while low <= high:', - ' mid = (low + high) // 2', - ' if arr[mid] == target:', - ' return mid # Target lock', - ' elif arr[mid] < target:', - ' low = mid + 1', - ' else:', - ' high = mid - 1', - ' return -1', - ], - activeStepLines: [2, 3, 5, 6, 9, 10, 8], - }, - java: { - lines: [ - 'public class BinarySearch {', - ' public static int search(int[] arr, int target) {', - ' int low = 0;', - ' int high = arr.length - 1;', - ' while (low <= high) {', - ' int mid = low + (high - low) / 2;', - ' if (arr[mid] == target) return mid;', - ' if (arr[mid] < target) low = mid + 1;', - ' else high = mid - 1;', - ' }', - ' return -1;', - ' }', - '}', - ], - activeStepLines: [3, 4, 5, 6, 8, 9, 7], - }, - cpp: { - lines: [ - '#include ', - '', - 'int binarySearch(const std::vector& arr, int target) {', - ' int low = 0;', - ' int high = static_cast(arr.size()) - 1;', - ' while (low <= high) {', - ' int mid = low + (high - low) / 2;', - ' if (arr[mid] == target) return mid;', - ' if (arr[mid] < target) low = mid + 1;', - ' else high = mid - 1;', - ' }', - ' return -1;', - '}', - ], - activeStepLines: [4, 5, 6, 7, 9, 10, 8], - }, - }, - - astar: { - typescript: { - lines: [ - 'function aStar(start: Node, target: Node, grid: Grid): Path {', - ' const openSet = new PriorityQueue((a, b) => a.f - b.f);', - ' openSet.push(start);', - '', - ' while (!openSet.isEmpty()) {', - ' const current = openSet.pop()!;', - ' if (current.equals(target)) return reconstructPath(current);', - '', - ' for (const neighbor of grid.getNeighbors(current)) {', - ' const tentativeG = current.g + distance(current, neighbor);', - ' if (tentativeG < neighbor.g) {', - ' neighbor.parent = current;', - ' neighbor.g = tentativeG;', - ' neighbor.f = neighbor.g + heuristic(neighbor, target);', - ' if (!openSet.contains(neighbor)) openSet.push(neighbor);', - ' }', - ' }', - ' }', - ' return []; // Path not found', - '}', - ], - activeStepLines: [2, 3, 5, 6, 9, 10, 14, 7], - }, - python: { - lines: [ - 'import heapq', - '', - 'def a_star_search(start, target, grid):', - ' open_set = []', - ' heapq.heappush(open_set, (0, start))', - ' came_from = {}', - ' g_score = {start: 0}', - '', - ' while open_set:', - ' _, current = heapq.heappop(open_set)', - ' if current == target:', - ' return reconstruct_path(came_from, current)', - '', - ' for neighbor in grid.neighbors(current):', - ' tentative_g = g_score[current] + cost(current, neighbor)', - ' if tentative_g < g_score.get(neighbor, float("inf")):', - ' came_from[neighbor] = current', - ' g_score[neighbor] = tentative_g', - ' f_score = tentative_g + heuristic(neighbor, target)', - ' heapq.heappush(open_set, (f_score, neighbor))', - ' return []', - ], - activeStepLines: [4, 5, 9, 10, 14, 15, 19, 12], - }, - java: { - lines: [ - 'public List aStar(Node start, Node target, Grid grid) {', - ' PriorityQueue openSet = new PriorityQueue<>(Comparator.comparingDouble(n -> n.f));', - ' openSet.add(start);', - ' while (!openSet.isEmpty()) {', - ' Node current = openSet.poll();', - ' if (current.equals(target)) return buildPath(current);', - ' for (Node neighbor : grid.getNeighbors(current)) {', - ' double tentativeG = current.g + distance(current, neighbor);', - ' if (tentativeG < neighbor.g) {', - ' neighbor.parent = current;', - ' neighbor.g = tentativeG;', - ' neighbor.f = neighbor.g + heuristic(neighbor, target);', - ' openSet.add(neighbor);', - ' }', - ' }', - ' }', - ' return Collections.emptyList();', - '}', - ], - activeStepLines: [2, 3, 4, 5, 7, 8, 12, 6], - }, - cpp: { - lines: [ - '#include ', - '#include ', - '', - 'std::vector aStar(Node* start, Node* target, Grid& grid) {', - ' std::priority_queue, CompareF> openSet;', - ' openSet.push(start);', - ' while (!openSet.empty()) {', - ' Node* current = openSet.top(); openSet.pop();', - ' if (current == target) return reconstructPath(current);', - ' for (Node* neighbor : grid.getNeighbors(current)) {', - ' double tentativeG = current->g + dist(current, neighbor);', - ' if (tentativeG < neighbor->g) {', - ' neighbor->parent = current;', - ' neighbor->g = tentativeG;', - ' neighbor->f = neighbor->g + heuristic(neighbor, target);', - ' openSet.push(neighbor);', - ' }', - ' }', - ' }', - ' return {};', - '}', - ], - activeStepLines: [5, 6, 7, 8, 10, 11, 15, 9], - }, - }, - - knapsack: { - typescript: { - lines: [ - 'function knapsack01(weights: number[], values: number[], capacity: number): number {', - ' const n = weights.length;', - ' const dp: number[][] = Array.from({ length: n + 1 }, () => Array(capacity + 1).fill(0));', - '', - ' for (let i = 1; i <= n; i++) {', - ' for (let w = 1; w <= capacity; w++) {', - ' if (weights[i - 1] <= w) {', - ' dp[i][w] = Math.max(', - ' dp[i - 1][w], // Exclude item', - ' values[i - 1] + dp[i - 1][w - weights[i - 1]] // Include item', - ' );', - ' } else {', - ' dp[i][w] = dp[i - 1][w];', - ' }', - ' }', - ' }', - ' return dp[n][capacity];', - '}', - ], - activeStepLines: [3, 5, 6, 7, 8, 13, 17], - }, - python: { - lines: [ - 'def knapsack_01(weights: list[int], values: list[int], capacity: int) -> int:', - ' n = len(weights)', - ' dp = [[0] * (capacity + 1) for _ in range(n + 1)]', - '', - ' for i in range(1, n + 1):', - ' for w in range(1, capacity + 1):', - ' if weights[i - 1] <= w:', - ' dp[i][w] = max(dp[i - 1][w], values[i - 1] + dp[i - 1][w - weights[i - 1]])', - ' else:', - ' dp[i][w] = dp[i - 1][w]', - ' return dp[n][capacity]', - ], - activeStepLines: [3, 5, 6, 7, 8, 10, 11], - }, - java: { - lines: [ - 'public class Knapsack {', - ' public static int solve(int[] weights, int[] values, int capacity) {', - ' int n = weights.length;', - ' int[][] dp = new int[n + 1][capacity + 1];', - ' for (int i = 1; i <= n; i++) {', - ' for (int w = 1; w <= capacity; w++) {', - ' if (weights[i - 1] <= w) {', - ' dp[i][w] = Math.max(dp[i - 1][w], values[i - 1] + dp[i - 1][w - weights[i - 1]]);', - ' } else {', - ' dp[i][w] = dp[i - 1][w];', - ' }', - ' }', - ' }', - ' return dp[n][capacity];', - ' }', - '}', - ], - activeStepLines: [4, 5, 6, 7, 8, 10, 14], - }, - cpp: { - lines: [ - '#include ', - '#include ', - '', - 'int knapsack01(const std::vector& weights, const std::vector& values, int W) {', - ' int n = weights.size();', - ' std::vector> dp(n + 1, std::vector(W + 1, 0));', - ' for (int i = 1; i <= n; ++i) {', - ' for (int w = 1; w <= W; ++w) {', - ' if (weights[i - 1] <= w)', - ' dp[i][w] = std::max(dp[i - 1][w], values[i - 1] + dp[i - 1][w - weights[i - 1]]);', - ' else', - ' dp[i][w] = dp[i - 1][w];', - ' }', - ' }', - ' return dp[n][W];', - '}', - ], - activeStepLines: [6, 7, 8, 9, 10, 12, 15], - }, - }, - - avl: { - typescript: { - lines: [ - 'function rightRotate(y: AVLNode): AVLNode {', - ' const x = y.left!;', - ' const T2 = x.right;', - ' x.right = y;', - ' y.left = T2;', - ' y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;', - ' x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;', - ' return x; // New root of subtree', - '}', - '', - 'function getBalance(node: AVLNode | null): number {', - ' return node ? getHeight(node.left) - getHeight(node.right) : 0;', - '}', - ], - activeStepLines: [2, 3, 4, 5, 6, 7, 8, 12], - }, - python: { - lines: [ - 'def right_rotate(y: AVLNode) -> AVLNode:', - ' x = y.left', - ' t2 = x.right', - ' x.right = y', - ' y.left = t2', - ' y.height = max(get_height(y.left), get_height(y.right)) + 1', - ' x.height = max(get_height(x.left), get_height(x.right)) + 1', - ' return x # New subtree root', - '', - 'def get_balance(node: AVLNode) -> int:', - ' return get_height(node.left) - get_height(node.right) if node else 0', - ], - activeStepLines: [2, 3, 4, 5, 6, 7, 8, 11], - }, - java: { - lines: [ - 'public class AVLTree {', - ' private Node rightRotate(Node y) {', - ' Node x = y.left;', - ' Node T2 = x.right;', - ' x.right = y;', - ' y.left = T2;', - ' y.height = Math.max(height(y.left), height(y.right)) + 1;', - ' x.height = Math.max(height(x.left), height(x.right)) + 1;', - ' return x;', - ' }', - '', - ' private int getBalance(Node n) {', - ' return (n == null) ? 0 : height(n.left) - height(n.right);', - ' }', - '}', - ], - activeStepLines: [3, 4, 5, 6, 7, 8, 9, 13], - }, - cpp: { - lines: [ - 'Node* rightRotate(Node* y) {', - ' Node* x = y->left;', - ' Node* T2 = x->right;', - ' x->right = y;', - ' y->left = T2;', - ' y->height = std::max(height(y->left), height(y->right)) + 1;', - ' x->height = std::max(height(x->left), height(x->right)) + 1;', - ' return x;', - '}', - '', - 'int getBalance(Node* n) {', - ' return n ? height(n->left) - height(n->right) : 0;', - '}', - ], - activeStepLines: [2, 3, 4, 5, 6, 7, 8, 12], - }, - }, -}; - -export function CodePlayground() { - const [algo, setAlgo] = useState('quicksort'); - const [lang, setLang] = useState('typescript'); - const [stepIdx, setStepIdx] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [copied, setCopied] = useState(false); - - const snippet = CODE_DATABASE[algo][lang]; - const maxSteps = snippet.activeStepLines.length; - - // Handle Auto-Play timer - useEffect(() => { - if (!isPlaying) return; - const timer = setInterval(() => { - setStepIdx((prev) => (prev + 1) % maxSteps); - }, 1200); - return () => clearInterval(timer); - }, [isPlaying, maxSteps]); - - // Reset step index when algorithm or language changes - useEffect(() => { - setStepIdx(0); - setIsPlaying(false); - }, [algo, lang]); - - const activeLineNumber = snippet.activeStepLines[stepIdx] ?? 1; - - const handleCopyCode = () => { - const fullCode = snippet.lines.join('\n'); - navigator.clipboard.writeText(fullCode).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }); - }; - - return ( -
- {/* Top Window Header (macOS Terminal Style) */} -
-
- - - -
- - {/* Algorithm Dropdown / Tabs */} -
- {( - [ - { id: 'quicksort', label: 'QuickSort' }, - { id: 'binarysearch', label: 'Binary Search' }, - { id: 'astar', label: 'A* Search' }, - { id: 'knapsack', label: '0/1 Knapsack' }, - { id: 'avl', label: 'AVL Rotation' }, - ] as const - ).map((item) => ( - - ))} -
- - {/* Copy Button */} - -
- - {/* Language Tabs & Playback Stepper Toolbar */} -
- {/* Language Tabs */} -
- {( - [ - { id: 'typescript', label: 'TypeScript', ext: '.ts' }, - { id: 'python', label: 'Python', ext: '.py' }, - { id: 'java', label: 'Java', ext: '.java' }, - { id: 'cpp', label: 'C++', ext: '.cpp' }, - ] as const - ).map((item) => ( - - ))} -
- - {/* Step-by-Step Execution Controls */} -
- - Line {activeLineNumber} โ€ข Step {stepIdx + 1}/{maxSteps} - - - - - - - -
-
- - {/* Code Editor Body with Line-by-Line Tracking */} -
-
-          
-            {snippet.lines.map((lineText, lineIdx) => {
-              const lineNum = lineIdx + 1;
-              const isActive = lineNum === activeLineNumber;
-
-              return (
-                
- {lineNum} - {lineText || ' '} - {isActive && โ† ACTIVE} -
- ); - })} -
-
-
-
- ); -} diff --git a/frontend/src/components/HeroMiniCanvas.tsx b/frontend/src/components/HeroMiniCanvas.tsx index be4c6a9..c4112ca 100644 --- a/frontend/src/components/HeroMiniCanvas.tsx +++ b/frontend/src/components/HeroMiniCanvas.tsx @@ -1,20 +1,7 @@ import { useEffect, useRef, useState, useCallback } from 'react'; -import { - Play, - Pause, - RotateCcw, - Zap, - BarChart3, - GitBranch, - Layers, - Cpu, - Binary, - FastForward, -} from 'lucide-react'; +import { Play, Pause, RotateCcw, Zap } from 'lucide-react'; -export type HeroSimMode = 'sorting' | 'pathfinding' | 'dp' | 'trees' | 'searching'; - -interface SortingStep { +interface Step { array: number[]; comparing: number[]; swapping: number[]; @@ -22,108 +9,61 @@ interface SortingStep { pivot?: number; } -interface PathfindingStep { - grid: number[][]; // 0: empty, 1: wall, 2: visitedA, 3: visitedB, 4: path, 5: start, 6: target - pathNodes: [number, number][]; - currentPos?: [number, number]; - stats: { visited: number; pathLength: number; status: string }; -} - -interface DPStep { - table: (number | null)[][]; - currentRow: number; - currentCol: number; - highlightedCells: [number, number][]; - optimalPath: [number, number][]; - currentVal: number; -} - -interface TreeStep { - nodes: { id: number; val: number; x: number; y: number; level: number; status: 'normal' | 'active' | 'rotated' | 'balanced' }[]; - edges: { from: number; to: number }[]; - statusText: string; -} - -interface SearchStep { - array: number[]; - low: number; - mid: number; - high: number; - target: number; - found: boolean; - stepCount: number; -} - function isMobileViewport() { - return typeof window !== 'undefined' && window.innerWidth <= 768; + return window.innerWidth <= 768; } function prefersReducedMotion() { - return typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + return window.matchMedia('(prefers-reduced-motion: reduce)').matches; } export function HeroMiniCanvas() { const canvasRef = useRef(null); const containerRef = useRef(null); - - const [mode, setMode] = useState('sorting'); const [isPlaying, setIsPlaying] = useState(() => !prefersReducedMotion()); - const [speedMultiplier, setSpeedMultiplier] = useState<1 | 2 | 4>(1); const [isMobileDevice, setIsMobileDevice] = useState(isMobileViewport); const [reducedMotion, setReducedMotion] = useState(prefersReducedMotion); const [isVisible, setIsVisible] = useState(true); - - // Dynamic Telemetry States - const [sortingStats, setSortingStats] = useState({ - lane1: { name: 'QuickSort', comps: 0, swaps: 0, status: 'Racing...' }, - lane2: { name: 'BubbleSort', comps: 0, swaps: 0, status: 'Racing...' }, - }); - const [pathfindingStats, setPathfindingStats] = useState({ visited: 0, pathLength: 0, status: 'Exploring...' }); - const [dpStats, setDPStats] = useState({ cell: '0,0', optimalVal: 0, status: 'Filling Matrix...' }); - const [treeStats, setTreeStats] = useState({ balance: 'In-Balance', rotations: 0, status: 'Inserting nodes...' }); - const [searchStats, setSearchStats] = useState({ low: 0, mid: 0, high: 0, step: 0, status: 'Halving Search Space...' }); - - // Simulation State Storage - const simState = useRef<{ - stepIdx: number; - maxSteps: number; + const [lane1Algo] = useState('Quick Sort'); + const [lane2Algo] = useState('Bubble Sort'); + const [lane1Stats, setLane1Stats] = useState({ comparisons: 0, swaps: 0, status: 'Racing...' }); + const [lane2Stats, setLane2Stats] = useState({ comparisons: 0, swaps: 0, status: 'Racing...' }); + + const stateRef = useRef<{ + lane1Steps: Step[]; + lane2Steps: Step[]; + lane1Idx: number; + lane2Idx: number; + arraySize: number; + initialArray: number[]; timer: number | null; - // Sorting Data - qSteps: SortingStep[]; - bSteps: SortingStep[]; - // Pathfinding Data - pathSteps: PathfindingStep[]; - // DP Data - dpSteps: DPStep[]; - // Tree Data - treeSteps: TreeStep[]; - // Search Data - searchSteps: SearchStep[]; }>({ - stepIdx: 0, - maxSteps: 0, + lane1Steps: [], + lane2Steps: [], + lane1Idx: 0, + lane2Idx: 0, + arraySize: 20, + initialArray: [], timer: null, - qSteps: [], - bSteps: [], - pathSteps: [], - dpSteps: [], - treeSteps: [], - searchSteps: [], }); - // Handle Resize and Accessibility preferences + // Keep the simulator live on mobile while honoring accessibility preferences. useEffect(() => { - const handleResize = () => { - setIsMobileDevice(isMobileViewport()); - const reduceMotion = prefersReducedMotion(); - setReducedMotion(reduceMotion); - if (reduceMotion) setIsPlaying(false); + const checkMobile = () => { + const isMobile = isMobileViewport(); + const shouldReduceMotion = prefersReducedMotion(); + setIsMobileDevice(isMobile); + setReducedMotion(shouldReduceMotion); + if (shouldReduceMotion) { + setIsPlaying(false); + } }; - window.addEventListener('resize', handleResize, { passive: true }); - return () => window.removeEventListener('resize', handleResize); + checkMobile(); + window.addEventListener('resize', checkMobile, { passive: true }); + return () => window.removeEventListener('resize', checkMobile); }, []); - // IntersectionObserver: Pause simulation when scrolled out of view to consume 0% idle CPU + // IntersectionObserver to pause rendering when scrolled offscreen useEffect(() => { if (!containerRef.current) return; const observer = new IntersectionObserver( @@ -136,9 +76,9 @@ export function HeroMiniCanvas() { return () => observer.disconnect(); }, []); - // Tab Visibility API: Pause when backgrounded + // Tab Visibility API to pause rendering when tab is hidden useEffect(() => { - const handleVisibility = () => { + const handleVisibilityChange = () => { if (document.hidden) { setIsVisible(false); } else if (containerRef.current) { @@ -146,37 +86,34 @@ export function HeroMiniCanvas() { setIsVisible(rect.top < window.innerHeight && rect.bottom > 0); } }; - document.addEventListener('visibilitychange', handleVisibility); - return () => document.removeEventListener('visibilitychange', handleVisibility); + document.addEventListener('visibilitychange', handleVisibilityChange); + return () => document.removeEventListener('visibilitychange', handleVisibilityChange); }, []); - // ---------------------------------------------------- - // STEP GENERATORS FOR ALL 5 MODES - // ---------------------------------------------------- - - // 1. Sorting Step Generator - const generateSortingSimulation = () => { - const size = isMobileDevice ? 16 : 22; - const arr = Array.from({ length: size }, () => Math.floor(Math.random() * 80) + 20); - - // QuickSort - const qSteps: SortingStep[] = []; + const generateSteps = (arr: number[]) => { + // QuickSort Step Generator + const qSteps: Step[] = []; const qArr = [...arr]; + let qComp = 0; + let qSwap = 0; + const quickSortHelper = (low: number, high: number) => { if (low < high) { const pivotVal = qArr[high]; let i = low - 1; for (let j = low; j < high; j++) { + qComp++; qSteps.push({ array: [...qArr], comparing: [j, high], swapping: [], - sorted: [], + sorted: getSortedIndices(low, high, qArr), pivot: high, }); if (qArr[j] < pivotVal) { i++; if (i !== j) { + qSwap++; const temp = qArr[i]; qArr[i] = qArr[j]; qArr[j] = temp; @@ -184,12 +121,13 @@ export function HeroMiniCanvas() { array: [...qArr], comparing: [], swapping: [i, j], - sorted: [], + sorted: getSortedIndices(low, high, qArr), pivot: high, }); } } } + qSwap++; const temp = qArr[i + 1]; qArr[i + 1] = qArr[high]; qArr[high] = temp; @@ -198,7 +136,7 @@ export function HeroMiniCanvas() { array: [...qArr], comparing: [], swapping: [i + 1, high], - sorted: [pIndex], + sorted: getSortedIndices(low, high, qArr), pivot: pIndex, }); @@ -206,6 +144,15 @@ export function HeroMiniCanvas() { quickSortHelper(pIndex + 1, high); } }; + + const getSortedIndices = (currentLow: number, currentHigh: number, currentArr: number[]) => { + const sorted: number[] = []; + for (let k = 0; k < currentArr.length; k++) { + if (k < currentLow || k > currentHigh) sorted.push(k); + } + return sorted; + }; + quickSortHelper(0, qArr.length - 1); qSteps.push({ array: [...qArr], @@ -214,20 +161,25 @@ export function HeroMiniCanvas() { sorted: Array.from({ length: qArr.length }, (_, k) => k), }); - // BubbleSort - const bSteps: SortingStep[] = []; + // BubbleSort Step Generator + const bSteps: Step[] = []; const bArr = [...arr]; const n = bArr.length; - const sortedSoFar: number[] = []; + let bComp = 0; + let bSwap = 0; + const sortedIndices: number[] = []; + for (let i = 0; i < n - 1; i++) { for (let j = 0; j < n - i - 1; j++) { + bComp++; bSteps.push({ array: [...bArr], comparing: [j, j + 1], swapping: [], - sorted: [...sortedSoFar], + sorted: [...sortedIndices], }); if (bArr[j] > bArr[j + 1]) { + bSwap++; const temp = bArr[j]; bArr[j] = bArr[j + 1]; bArr[j + 1] = temp; @@ -235,13 +187,13 @@ export function HeroMiniCanvas() { array: [...bArr], comparing: [], swapping: [j, j + 1], - sorted: [...sortedSoFar], + sorted: [...sortedIndices], }); } } - sortedSoFar.push(n - 1 - i); + sortedIndices.push(n - 1 - i); } - sortedSoFar.push(0); + sortedIndices.push(0); bSteps.push({ array: [...bArr], comparing: [], @@ -249,922 +201,260 @@ export function HeroMiniCanvas() { sorted: Array.from({ length: n }, (_, k) => k), }); - simState.current.qSteps = qSteps; - simState.current.bSteps = bSteps; - simState.current.maxSteps = Math.max(qSteps.length, bSteps.length); - simState.current.stepIdx = 0; - }; - - // 2. Pathfinding Step Generator (A* Wavefront on 2D Grid) - const generatePathfindingSimulation = () => { - const rows = 11; - const cols = 23; - const grid: number[][] = Array.from({ length: rows }, () => Array(cols).fill(0)); - - const start: [number, number] = [5, 2]; - const target: [number, number] = [5, 20]; - - // Procedural Walls - for (let r = 2; r < 9; r++) { - if (r !== 5 && r !== 6) grid[r][7] = 1; - if (r !== 3 && r !== 4) grid[r][15] = 1; - } - grid[start[0]][start[1]] = 5; - grid[target[0]][target[1]] = 6; - - const steps: PathfindingStep[] = []; - const openSet: [number, number][] = [[start[0], start[1]]]; - const visited = new Set([`${start[0]},${start[1]}`]); - const parentMap = new Map(); - - const directions = [ - [0, 1], - [1, 0], - [0, -1], - [-1, 0], - ]; - - let foundTarget = false; - let iterations = 0; - - while (openSet.length > 0 && !foundTarget && iterations < 200) { - iterations++; - // A* heuristic sort - openSet.sort((a, b) => { - const distA = Math.abs(a[0] - target[0]) + Math.abs(a[1] - target[1]); - const distB = Math.abs(b[0] - target[0]) + Math.abs(b[1] - target[1]); - return distA - distB; - }); - - const current = openSet.shift()!; - const [cr, cc] = current; - - if (cr === target[0] && cc === target[1]) { - foundTarget = true; - break; - } - - if (grid[cr][cc] !== 5 && grid[cr][cc] !== 6) { - grid[cr][cc] = 2; // Visited - } - - for (const [dr, dc] of directions) { - const nr = cr + dr; - const nc = cc + dc; - const key = `${nr},${nc}`; - if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] !== 1 && !visited.has(key)) { - visited.add(key); - parentMap.set(key, [cr, cc]); - openSet.push([nr, nc]); - if (grid[nr][nc] !== 6) { - grid[nr][nc] = 3; // Open Set Wavefront - } - } - } - - const gridCopy = grid.map((r) => [...r]); - steps.push({ - grid: gridCopy, - pathNodes: [], - currentPos: [cr, cc], - stats: { visited: visited.size, pathLength: 0, status: 'A* Wavefront Expanding...' }, - }); - } - - // Trace shortest path - const path: [number, number][] = []; - let currKey = `${target[0]},${target[1]}`; - while (parentMap.has(currKey)) { - const p = parentMap.get(currKey)!; - path.unshift(p); - currKey = `${p[0]},${p[1]}`; - } - - // Path tracing animation steps - const finalGrid = grid.map((r) => [...r]); - for (let i = 0; i < path.length; i++) { - const [pr, pc] = path[i]; - if (finalGrid[pr][pc] !== 5 && finalGrid[pr][pc] !== 6) { - finalGrid[pr][pc] = 4; // Shortest Path - } - steps.push({ - grid: finalGrid.map((r) => [...r]), - pathNodes: path.slice(0, i + 1), - currentPos: [pr, pc], - stats: { visited: visited.size, pathLength: i + 1, status: 'Tracing Optimal Shortest Path ๐Ÿ†' }, - }); - } - - simState.current.pathSteps = steps; - simState.current.maxSteps = steps.length; - simState.current.stepIdx = 0; - }; - - // 3. Dynamic Programming Step Generator (0/1 Knapsack Grid) - const generateDPMatrixSimulation = () => { - const weights = [2, 3, 4, 5]; - const values = [3, 4, 5, 8]; - const capacity = 6; - const n = weights.length; - - const dp: (number | null)[][] = Array.from({ length: n + 1 }, () => Array(capacity + 1).fill(null)); - for (let w = 0; w <= capacity; w++) dp[0][w] = 0; - for (let i = 0; i <= n; i++) dp[i][0] = 0; - - const steps: DPStep[] = []; - - for (let i = 1; i <= n; i++) { - for (let w = 1; w <= capacity; w++) { - const highlighted: [number, number][] = [[i - 1, w]]; - let optVal = dp[i - 1][w] ?? 0; - - if (weights[i - 1] <= w) { - highlighted.push([i - 1, w - weights[i - 1]]); - optVal = Math.max(optVal, (dp[i - 1][w - weights[i - 1]] ?? 0) + values[i - 1]); - } - dp[i][w] = optVal; - - steps.push({ - table: dp.map((row) => [...row]), - currentRow: i, - currentCol: w, - highlightedCells: highlighted, - optimalPath: [], - currentVal: optVal, - }); - } - } - - // Trace back optimal items - let curW = capacity; - const optCells: [number, number][] = []; - for (let i = n; i > 0; i--) { - if (dp[i][curW] !== dp[i - 1][curW]) { - optCells.push([i, curW]); - curW -= weights[i - 1]; - } - } - - steps.push({ - table: dp.map((row) => [...row]), - currentRow: n, - currentCol: capacity, - highlightedCells: [], - optimalPath: optCells, - currentVal: dp[n][capacity] ?? 0, - }); - - simState.current.dpSteps = steps; - simState.current.maxSteps = steps.length; - simState.current.stepIdx = 0; + return { qSteps, bSteps, qComp, qSwap, bComp, bSwap }; }; - // 4. Tree Balancing / AVL Step Generator - const generateTreeSimulation = () => { - const steps: TreeStep[] = []; - const valuesToInsert = [50, 25, 75, 15, 35, 65, 85, 10]; - - const basePositions = [ - { id: 1, val: 50, x: 290, y: 40, level: 0 }, - { id: 2, val: 25, x: 150, y: 100, level: 1 }, - { id: 3, val: 75, x: 430, y: 100, level: 1 }, - { id: 4, val: 15, x: 80, y: 165, level: 2 }, - { id: 5, val: 35, x: 220, y: 165, level: 2 }, - { id: 6, val: 65, x: 360, y: 165, level: 2 }, - { id: 7, val: 85, x: 500, y: 165, level: 2 }, - { id: 8, val: 10, x: 40, y: 225, level: 3 }, - ]; - - const edges = [ - { from: 1, to: 2 }, - { from: 1, to: 3 }, - { from: 2, to: 4 }, - { from: 2, to: 5 }, - { from: 3, to: 6 }, - { from: 3, to: 7 }, - { from: 4, to: 8 }, - ]; - - for (let i = 1; i <= valuesToInsert.length; i++) { - const activeNodes = basePositions.slice(0, i).map((n, idx) => ({ - ...n, - status: idx === i - 1 ? ('active' as const) : ('normal' as const), - })); - - const activeEdges = edges.filter((e) => e.from <= i && e.to <= i); - - steps.push({ - nodes: activeNodes, - edges: activeEdges, - statusText: `Inserting Node (${valuesToInsert[i - 1]}). Balancing factors: O(log N)`, - }); - } - - // Add balancing rotation frame - steps.push({ - nodes: basePositions.map((n) => ({ - ...n, - status: n.val === 25 || n.val === 15 ? 'rotated' : 'balanced', - })), - edges, - statusText: 'AVL Self-Balancing: Right-Rotation executed. Tree Balanced ๐ŸŒณ', - }); - - simState.current.treeSteps = steps; - simState.current.maxSteps = steps.length; - simState.current.stepIdx = 0; - }; - - // 5. Binary Search Step Generator - const generateSearchingSimulation = () => { - const size = 17; - const array = Array.from({ length: size }, (_, i) => (i + 1) * 5 + Math.floor(Math.random() * 2)); - const target = array[Math.floor(Math.random() * (size - 2)) + 1]; - - const steps: SearchStep[] = []; - let low = 0; - let high = size - 1; - let stepCount = 0; - let found = false; - - while (low <= high) { - stepCount++; - const mid = Math.floor((low + high) / 2); - const isMatch = array[mid] === target; - - steps.push({ - array: [...array], - low, - mid, - high, - target, - found: isMatch, - stepCount, - }); - - if (isMatch) { - found = true; - break; - } - - if (array[mid] < target) { - low = mid + 1; - } else { - high = mid - 1; - } - } - - simState.current.searchSteps = steps; - simState.current.maxSteps = steps.length; - simState.current.stepIdx = 0; - }; - - // ---------------------------------------------------- - // INITIALIZE / RESET ACTIVE SIMULATION - // ---------------------------------------------------- - const resetSimulation = useCallback(() => { - if (mode === 'sorting') { - generateSortingSimulation(); - setSortingStats({ - lane1: { name: 'QuickSort', comps: 0, swaps: 0, status: 'Racing...' }, - lane2: { name: 'BubbleSort', comps: 0, swaps: 0, status: 'Racing...' }, - }); - } else if (mode === 'pathfinding') { - generatePathfindingSimulation(); - setPathfindingStats({ visited: 0, pathLength: 0, status: 'Exploring Grid...' }); - } else if (mode === 'dp') { - generateDPMatrixSimulation(); - setDPStats({ cell: '0,0', optimalVal: 0, status: 'Filling Matrix...' }); - } else if (mode === 'trees') { - generateTreeSimulation(); - setTreeStats({ balance: 'Evaluating', rotations: 0, status: 'Inserting nodes...' }); - } else if (mode === 'searching') { - generateSearchingSimulation(); - setSearchStats({ low: 0, mid: 0, high: 0, step: 0, status: 'Halving Search Space...' }); - } - }, [mode, isMobileDevice]); - - // ---------------------------------------------------- - // CANVAS RENDERING DISPATCHER - // ---------------------------------------------------- const renderCanvas = useCallback(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; - // Retina / High-DPI Scaling - const dpr = window.devicePixelRatio || 1; - const displayWidth = canvas.clientWidth || 580; - const displayHeight = canvas.clientHeight || 260; - - if (canvas.width !== displayWidth * dpr || canvas.height !== displayHeight * dpr) { - canvas.width = displayWidth * dpr; - canvas.height = displayHeight * dpr; - } - - ctx.save(); - ctx.scale(dpr, dpr); - - const width = displayWidth; - const height = displayHeight; + const width = canvas.width; + const height = canvas.height; ctx.clearRect(0, 0, width, height); - // Dark sleek background - ctx.fillStyle = '#080a10'; + // Background fill + ctx.fillStyle = '#090b10'; ctx.fillRect(0, 0, width, height); - // Subtle technical grid - ctx.strokeStyle = 'rgba(255, 255, 255, 0.025)'; + // Subtle Grid background + ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)'; ctx.lineWidth = 1; - for (let x = 0; x < width; x += 22) { + for (let x = 0; x < width; x += 20) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); } - for (let y = 0; y < height; y += 22) { + for (let y = 0; y < height; y += 20) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } - const idx = simState.current.stepIdx; + const laneHeight = (height - 30) / 2; + + // Draw Lane 1 (QuickSort) + drawLane( + ctx, + 0, + 15, + width, + laneHeight, + lane1Algo, + stateRef.current.lane1Steps[stateRef.current.lane1Idx], + '#a855f7' + ); + + // Divider line + ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(15, laneHeight + 15); + ctx.lineTo(width - 15, laneHeight + 15); + ctx.stroke(); + + // Draw Lane 2 (BubbleSort) + drawLane( + ctx, + 0, + laneHeight + 25, + width, + laneHeight, + lane2Algo, + stateRef.current.lane2Steps[stateRef.current.lane2Idx], + '#3b82f6' + ); + }, [lane1Algo, lane2Algo]); + + const resetRace = useCallback(() => { + const size = 22; + const arr = Array.from({ length: size }, () => Math.floor(Math.random() * 85) + 15); + const { qSteps, bSteps } = generateSteps(arr); + + stateRef.current.initialArray = arr; + stateRef.current.lane1Steps = qSteps; + stateRef.current.lane2Steps = bSteps; + stateRef.current.lane1Idx = 0; + stateRef.current.lane2Idx = 0; - if (mode === 'sorting') { - drawSortingMode(ctx, width, height, idx); - } else if (mode === 'pathfinding') { - drawPathfindingMode(ctx, width, height, idx); - } else if (mode === 'dp') { - drawDPMode(ctx, width, height, idx); - } else if (mode === 'trees') { - drawTreeMode(ctx, width, height, idx); - } else if (mode === 'searching') { - drawSearchMode(ctx, width, height, idx); + setLane1Stats({ comparisons: 0, swaps: 0, status: 'Racing...' }); + setLane2Stats({ comparisons: 0, swaps: 0, status: 'Racing...' }); + renderCanvas(); + }, [renderCanvas]); + + useEffect(() => { + resetRace(); + }, [resetRace]); + + // Main animation timer effect - slower on mobile, paused when hidden or reduced motion is requested. + useEffect(() => { + if (!isPlaying || !isVisible || reducedMotion) { + if (stateRef.current.timer) clearInterval(stateRef.current.timer); + return; } - ctx.restore(); - }, [mode]); + stateRef.current.timer = window.setInterval(() => { + let l1Finished = false; + let l2Finished = false; - // Mode 1: Draw Sorting - const drawSortingMode = (ctx: CanvasRenderingContext2D, width: number, height: number, stepIdx: number) => { - const laneHeight = (height - 35) / 2; - const qStep = simState.current.qSteps[Math.min(stepIdx, simState.current.qSteps.length - 1)]; - const bStep = simState.current.bSteps[Math.min(stepIdx, simState.current.bSteps.length - 1)]; + if (stateRef.current.lane1Idx < stateRef.current.lane1Steps.length - 1) { + stateRef.current.lane1Idx++; + } else { + l1Finished = true; + } - // Lane 1: QuickSort - drawArrayLane(ctx, 15, laneHeight, qStep, '#a855f7', 'QuickSort (O(N log N))'); + if (stateRef.current.lane2Idx < stateRef.current.lane2Steps.length - 1) { + stateRef.current.lane2Idx++; + } else { + l2Finished = true; + } - // Divider - ctx.strokeStyle = 'rgba(255, 255, 255, 0.07)'; - ctx.beginPath(); - ctx.moveTo(15, laneHeight + 17); - ctx.lineTo(width - 15, laneHeight + 17); - ctx.stroke(); + renderCanvas(); - // Lane 2: BubbleSort - drawArrayLane(ctx, laneHeight + 25, laneHeight, bStep, '#3b82f6', 'BubbleSort (O(Nยฒ))'); - }; + setLane1Stats({ + comparisons: Math.floor(stateRef.current.lane1Idx * 0.8), + swaps: Math.floor(stateRef.current.lane1Idx * 0.4), + status: l1Finished ? 'Winner ๐Ÿ†' : 'Racing...', + }); + + setLane2Stats({ + comparisons: Math.floor(stateRef.current.lane2Idx * 0.9), + swaps: Math.floor(stateRef.current.lane2Idx * 0.5), + status: l2Finished ? 'Completed' : 'Racing...', + }); + + if (l1Finished && l2Finished) { + setTimeout(() => { + resetRace(); + }, 3000); + } + }, isMobileDevice ? 100 : 70); - const drawArrayLane = ( + return () => { + if (stateRef.current.timer) clearInterval(stateRef.current.timer); + }; + }, [isPlaying, isVisible, isMobileDevice, reducedMotion, renderCanvas, resetRace]); + + const drawLane = ( ctx: CanvasRenderingContext2D, + _xOffset: number, yOffset: number, + width: number, height: number, - step: SortingStep | undefined, - primaryColor: string, - _label: string + _title: string, + step: Step | undefined, + primaryGlow: string ) => { if (!step) return; + const padding = 20; - const availableWidth = (canvasRef.current?.clientWidth || 580) - padding * 2; + const availableWidth = width - padding * 2; const n = step.array.length; const barGap = 4; const barWidth = Math.max(3, (availableWidth - (n - 1) * barGap) / n); + const maxVal = 100; step.array.forEach((val, i) => { - const barHeight = (val / maxVal) * (height - 20); + const barHeight = (val / maxVal) * (height - 25); const x = padding + i * (barWidth + barGap); const y = yOffset + height - barHeight; - let fill = '#334155'; + let fillStyle = '#334155'; let shadowColor = 'transparent'; let shadowBlur = 0; if (step.sorted.includes(i)) { - fill = '#10b981'; - shadowColor = 'rgba(16, 185, 129, 0.6)'; + fillStyle = '#10b981'; + shadowColor = 'rgba(16, 185, 129, 0.5)'; shadowBlur = 8; } else if (step.swapping.includes(i)) { - fill = '#ec4899'; + fillStyle = '#ec4899'; shadowColor = 'rgba(236, 72, 153, 0.8)'; - shadowBlur = 10; + shadowBlur = 12; } else if (step.comparing.includes(i)) { - fill = '#f59e0b'; + fillStyle = '#f59e0b'; shadowColor = 'rgba(245, 158, 11, 0.7)'; - shadowBlur = 8; + shadowBlur = 10; } else if (step.pivot === i) { - fill = '#c084fc'; + fillStyle = '#c084fc'; shadowColor = 'rgba(192, 132, 252, 0.8)'; - shadowBlur = 10; + shadowBlur = 12; } else { - fill = primaryColor; + fillStyle = primaryGlow; } ctx.save(); - ctx.fillStyle = fill; + ctx.fillStyle = fillStyle; if (shadowBlur > 0) { ctx.shadowColor = shadowColor; ctx.shadowBlur = shadowBlur; } - ctx.beginPath(); - ctx.roundRect(x, y, barWidth, barHeight, [3, 3, 0, 0]); - ctx.fill(); - ctx.restore(); - }); - }; - - // Mode 2: Draw Pathfinding Grid - const drawPathfindingMode = (ctx: CanvasRenderingContext2D, width: number, height: number, stepIdx: number) => { - const step = simState.current.pathSteps[Math.min(stepIdx, simState.current.pathSteps.length - 1)]; - if (!step) return; - - const rows = step.grid.length; - const cols = step.grid[0].length; - const cellSize = Math.min((width - 40) / cols, (height - 30) / rows); - const startX = (width - cols * cellSize) / 2; - const startY = (height - rows * cellSize) / 2; - - for (let r = 0; r < rows; r++) { - for (let c = 0; c < cols; c++) { - const val = step.grid[r][c]; - const x = startX + c * cellSize; - const y = startY + r * cellSize; - - let fill = '#111827'; - let shadow = 'transparent'; - - if (val === 1) fill = '#374151'; // Wall - else if (val === 2) fill = 'rgba(6, 182, 212, 0.5)'; // Visited - else if (val === 3) fill = 'rgba(245, 158, 11, 0.7)'; // Wavefront Open - else if (val === 4) { - fill = '#10b981'; // Shortest Path - shadow = 'rgba(16, 185, 129, 0.8)'; - } else if (val === 5) fill = '#3b82f6'; // Start - else if (val === 6) fill = '#ef4444'; // Target - - ctx.save(); - ctx.fillStyle = fill; - if (shadow !== 'transparent') { - ctx.shadowColor = shadow; - ctx.shadowBlur = 10; - } - ctx.beginPath(); - ctx.roundRect(x + 1, y + 1, cellSize - 2, cellSize - 2, 2); - ctx.fill(); - ctx.restore(); - } - } - }; - - // Mode 3: Draw DP Matrix - const drawDPMode = (ctx: CanvasRenderingContext2D, width: number, height: number, stepIdx: number) => { - const step = simState.current.dpSteps[Math.min(stepIdx, simState.current.dpSteps.length - 1)]; - if (!step) return; - - const rows = step.table.length; - const cols = step.table[0].length; - const cellW = Math.min(65, (width - 60) / cols); - const cellH = Math.min(38, (height - 40) / rows); - const startX = (width - cols * cellW) / 2; - const startY = (height - rows * cellH) / 2; - - for (let r = 0; r < rows; r++) { - for (let c = 0; c < cols; c++) { - const val = step.table[r][c]; - const x = startX + c * cellW; - const y = startY + r * cellH; - - const isCurrent = r === step.currentRow && c === step.currentCol; - const isHighlight = step.highlightedCells.some(([hr, hc]) => hr === r && hc === c); - const isOptimal = step.optimalPath.some(([opr, opc]) => opr === r && opc === c); - - let bg = 'rgba(255, 255, 255, 0.03)'; - let border = 'rgba(255, 255, 255, 0.08)'; - - if (isCurrent) { - bg = 'rgba(168, 85, 247, 0.35)'; - border = '#c084fc'; - } else if (isHighlight) { - bg = 'rgba(245, 158, 11, 0.25)'; - border = '#f59e0b'; - } else if (isOptimal) { - bg = 'rgba(16, 185, 129, 0.35)'; - border = '#10b981'; - } - - ctx.save(); - ctx.fillStyle = bg; - ctx.strokeStyle = border; - ctx.lineWidth = isCurrent || isOptimal ? 2 : 1; - ctx.beginPath(); - ctx.roundRect(x + 2, y + 2, cellW - 4, cellH - 4, 4); - ctx.fill(); - ctx.stroke(); - - // Cell Value - if (val !== null) { - ctx.fillStyle = isOptimal ? '#10b981' : isCurrent ? '#c084fc' : '#e2e8f0'; - ctx.font = 'bold 12px "JetBrains Mono", monospace'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(String(val), x + cellW / 2, y + cellH / 2); - } - ctx.restore(); - } - } - }; - - // Mode 4: Draw Trees - const drawTreeMode = (ctx: CanvasRenderingContext2D, width: number, height: number, stepIdx: number) => { - const step = simState.current.treeSteps[Math.min(stepIdx, simState.current.treeSteps.length - 1)]; - if (!step) return; - - // Scale positions to fit current width/height - const scaleX = width / 580; - const scaleY = height / 260; - - // Draw Edges - step.edges.forEach((edge) => { - const fromNode = step.nodes.find((n) => n.id === edge.from); - const toNode = step.nodes.find((n) => n.id === edge.to); - if (fromNode && toNode) { - ctx.save(); - ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(fromNode.x * scaleX, fromNode.y * scaleY); - ctx.lineTo(toNode.x * scaleX, toNode.y * scaleY); - ctx.stroke(); - ctx.restore(); - } - }); - - // Draw Nodes - step.nodes.forEach((node) => { - const nx = node.x * scaleX; - const ny = node.y * scaleY; - const radius = 16; - - let fill = '#1e293b'; - let stroke = '#64748b'; - let shadow = 'transparent'; - - if (node.status === 'active') { - fill = '#7c3aed'; - stroke = '#c084fc'; - shadow = 'rgba(192, 132, 252, 0.8)'; - } else if (node.status === 'rotated') { - fill = '#ec4899'; - stroke = '#f472b6'; - shadow = 'rgba(236, 72, 153, 0.8)'; - } else if (node.status === 'balanced') { - fill = '#059669'; - stroke = '#34d399'; - shadow = 'rgba(52, 211, 153, 0.7)'; - } - - ctx.save(); - ctx.fillStyle = fill; - ctx.strokeStyle = stroke; - ctx.lineWidth = 2; - if (shadow !== 'transparent') { - ctx.shadowColor = shadow; - ctx.shadowBlur = 10; - } - ctx.beginPath(); - ctx.arc(nx, ny, radius, 0, Math.PI * 2); - ctx.fill(); - ctx.stroke(); - - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 11px "JetBrains Mono", monospace'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(String(node.val), nx, ny); - ctx.restore(); - }); - }; - - // Mode 5: Draw Binary Search - const drawSearchMode = (ctx: CanvasRenderingContext2D, width: number, height: number, stepIdx: number) => { - const step = simState.current.searchSteps[Math.min(stepIdx, simState.current.searchSteps.length - 1)]; - if (!step) return; - - const padding = 20; - const availableWidth = width - padding * 2; - const n = step.array.length; - const boxGap = 4; - const boxWidth = (availableWidth - (n - 1) * boxGap) / n; - const boxHeight = 44; - const yCenter = (height - boxHeight) / 2; - - step.array.forEach((val, i) => { - const x = padding + i * (boxWidth + boxGap); - const inRange = i >= step.low && i <= step.high; - const isMid = i === step.mid; - const isMatch = isMid && step.found; - - let fill = inRange ? '#1e293b' : 'rgba(30, 41, 59, 0.3)'; - let border = inRange ? 'rgba(255, 255, 255, 0.15)' : 'rgba(255, 255, 255, 0.03)'; - let textCol = inRange ? '#e2e8f0' : '#475569'; - let shadow = 'transparent'; - - if (isMatch) { - fill = '#10b981'; - border = '#34d399'; - textCol = '#ffffff'; - shadow = 'rgba(16, 185, 129, 0.8)'; - } else if (isMid) { - fill = '#6366f1'; - border = '#818cf8'; - textCol = '#ffffff'; - shadow = 'rgba(99, 102, 241, 0.8)'; - } - ctx.save(); - ctx.fillStyle = fill; - ctx.strokeStyle = border; - ctx.lineWidth = isMid ? 2 : 1; - if (shadow !== 'transparent') { - ctx.shadowColor = shadow; - ctx.shadowBlur = 10; - } + const radius = 3; ctx.beginPath(); - ctx.roundRect(x, yCenter, boxWidth, boxHeight, 4); + ctx.roundRect(x, y, barWidth, barHeight, [radius, radius, 0, 0]); ctx.fill(); - ctx.stroke(); - - ctx.fillStyle = textCol; - ctx.font = 'bold 12px "JetBrains Mono", monospace'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(String(val), x + boxWidth / 2, yCenter + boxHeight / 2); ctx.restore(); }); }; - // ---------------------------------------------------- - // INITIALIZE / RESET SIMULATION ON MODE SWITCH - // ---------------------------------------------------- - useEffect(() => { - resetSimulation(); - // Render initial static preview frame immediately on mount/mode switch - requestAnimationFrame(() => { - renderCanvas(); - }); - }, [resetSimulation, renderCanvas]); - - // ---------------------------------------------------- - // MAIN ANIMATION LOOP - // ---------------------------------------------------- - useEffect(() => { - if (!isPlaying || !isVisible || reducedMotion) { - if (simState.current.timer) clearInterval(simState.current.timer); - return; - } - - const baseDelay = mode === 'sorting' ? 70 : mode === 'pathfinding' ? 60 : 120; - const intervalTime = Math.max(20, baseDelay / speedMultiplier); - - simState.current.timer = window.setInterval(() => { - const curr = simState.current.stepIdx; - const max = simState.current.maxSteps; - - if (curr < max - 1) { - simState.current.stepIdx++; - renderCanvas(); - - // Update mode telemetry - if (mode === 'sorting') { - const qLen = simState.current.qSteps.length; - const bLen = simState.current.bSteps.length; - setSortingStats({ - lane1: { - name: 'QuickSort', - comps: Math.floor(Math.min(curr, qLen) * 0.8), - swaps: Math.floor(Math.min(curr, qLen) * 0.4), - status: curr >= qLen - 1 ? 'Winner ๐Ÿ†' : 'Racing...', - }, - lane2: { - name: 'BubbleSort', - comps: Math.floor(Math.min(curr, bLen) * 0.9), - swaps: Math.floor(Math.min(curr, bLen) * 0.5), - status: curr >= bLen - 1 ? 'Completed' : 'Racing...', - }, - }); - } else if (mode === 'pathfinding') { - const step = simState.current.pathSteps[curr]; - if (step) setPathfindingStats(step.stats); - } else if (mode === 'dp') { - const step = simState.current.dpSteps[curr]; - if (step) { - setDPStats({ - cell: `${step.currentRow},${step.currentCol}`, - optimalVal: step.currentVal, - status: curr >= max - 1 ? 'Optimal Substructure Solved ๐Ÿ†' : 'Memoizing Subproblems...', - }); - } - } else if (mode === 'trees') { - const step = simState.current.treeSteps[curr]; - if (step) { - setTreeStats({ - balance: curr >= max - 1 ? 'Balanced (AVL Factor 0)' : 'Rebalancing Tree', - rotations: curr >= max - 1 ? 1 : 0, - status: step.statusText, - }); - } - } else if (mode === 'searching') { - const step = simState.current.searchSteps[curr]; - if (step) { - setSearchStats({ - low: step.low, - mid: step.mid, - high: step.high, - step: step.stepCount, - status: step.found ? `Target ${step.target} Found in ${step.stepCount} steps! ๐ŸŽฏ` : 'Halving Search Space...', - }); - } - } - } else { - // Loop simulation after short pause - setTimeout(() => { - resetSimulation(); - }, 2200); - } - }, intervalTime); - - return () => { - if (simState.current.timer) clearInterval(simState.current.timer); - }; - }, [isPlaying, isVisible, speedMultiplier, mode, reducedMotion, renderCanvas, resetSimulation]); - return (
- {/* Top Header with Mode Tabs */} -
-
- - - - - - - - - +
+
+
+ LIVE HARDWARE ACCELERATED SIMULATOR
- - {/* Action Controls */}
- - -
- {/* Canvas Element Wrapper */}
- {/* Dynamic Telemetry Footer */}
- {mode === 'sorting' && ( - <> -
- - {sortingStats.lane1.name}: - {sortingStats.lane1.comps} comps - - {sortingStats.lane1.status} - -
- -
- - {sortingStats.lane2.name}: - {sortingStats.lane2.comps} comps - {sortingStats.lane2.status} -
- - )} - - {mode === 'pathfinding' && ( -
- - A* Wavefront: - {pathfindingStats.visited} nodes evaluated - Path: {pathfindingStats.pathLength} steps - {pathfindingStats.status} -
- )} - - {mode === 'dp' && ( -
- - Knapsack Table: - Cell: [{dpStats.cell}] - Max Value: ${dpStats.optimalVal} - {dpStats.status} -
- )} - - {mode === 'trees' && ( -
- - AVL Tree: - {treeStats.balance} - {treeStats.status} -
- )} +
+ + {lane1Algo}: + {lane1Stats.comparisons} comps + + {lane1Stats.status} + +
- {mode === 'searching' && ( -
- - Binary Search: - Step {searchStats.step} (Logโ‚‚ N) - {searchStats.status} -
- )} +
+ + {lane2Algo}: + {lane2Stats.comparisons} comps + {lane2Stats.status} +
); diff --git a/frontend/src/components/ShareBenchmarkModal.tsx b/frontend/src/components/ShareBenchmarkModal.tsx deleted file mode 100644 index 18da5f7..0000000 --- a/frontend/src/components/ShareBenchmarkModal.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Share2, Check, Copy, ExternalLink, X, Sparkles, Layers, Code2, Link } from 'lucide-react'; -import { ShareableBenchmarkConfig, generateShareableUrl } from '../utils/shareableBenchmark'; -import { useAudio } from '../context/AudioContext'; - -interface ShareBenchmarkModalProps { - isOpen: boolean; - onClose: () => void; - config: ShareableBenchmarkConfig; -} - -export const ShareBenchmarkModal: React.FC = ({ - isOpen, - onClose, - config, -}) => { - const [activeTab, setActiveTab] = useState<'link' | 'embed'>('link'); - const [copiedLink, setCopiedLink] = useState(false); - const [copiedEmbed, setCopiedEmbed] = useState(false); - const [embedHeight, setEmbedHeight] = useState(520); - const { play } = useAudio(); - - const shareUrl = React.useMemo(() => { - return generateShareableUrl(config); - }, [config]); - - const embedUrl = React.useMemo(() => { - const url = new URL(shareUrl); - url.searchParams.set('embed', 'true'); - return url.href; - }, [shareUrl]); - - const embedSnippet = ``; - - useEffect(() => { - if (!isOpen) return; - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - onClose(); - } - }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [isOpen, onClose]); - - if (!isOpen) return null; - - const handleCopyLink = () => { - navigator.clipboard.writeText(shareUrl).then(() => { - setCopiedLink(true); - play('click'); - setTimeout(() => setCopiedLink(false), 2500); - }); - }; - - const handleCopyEmbed = () => { - navigator.clipboard.writeText(embedSnippet).then(() => { - setCopiedEmbed(true); - play('click'); - setTimeout(() => setCopiedEmbed(false), 2500); - }); - }; - - const handleOpenNewTab = () => { - window.open(shareUrl, '_blank'); - }; - - const datasetPreview = config.customArray && config.customArray.length > 0 - ? `[${config.customArray.slice(0, 8).join(', ')}${config.customArray.length > 8 ? `, ... +${config.customArray.length - 8} more` : ''}]` - : `${config.datasetType || 'Random'} Dataset (${config.size || 30} elements)`; - - return ( -
-
e.stopPropagation()}> -
-
-
- -
-
-

- Share & Embed Benchmark -

-

- Collaborate with encoded URLs or embed interactive races in blog posts -

-
-
- -
- - {/* Modal Tab Switcher */} -
- - -
- -
- {/* Benchmark Configuration Summary Box */} -
-
- Arena: - - {config.arena} Arena - -
- - {config.algorithms && config.algorithms.length > 0 && ( -
- Algorithms: -
- {config.algorithms.map((algo, idx) => ( - - {algo} - - ))} -
-
- )} - -
- Dataset: - - {datasetPreview} - -
- - {config.target !== undefined && ( -
- Search Target: - - {config.target} - -
- )} -
- - {activeTab === 'link' ? ( - /* Shareable Link Tab */ -
- -
- (e.target as HTMLInputElement).select()} - className="share-url-input" - /> - -
-
- ) : ( - /* Embed Widget Tab */ -
-
- -
- Height: - -
-
-
-