From 9375db4d91f432cc3d7b3f2d9e81d3710302eb8f Mon Sep 17 00:00:00 2001 From: Sanan507 <227714367+Sanan507@users.noreply.github.com> Date: Fri, 7 Aug 2026 00:53:54 +0000 Subject: [PATCH] feat(history): Persistent Race Session History & Performance Analytics Center Closes #88. Implemented persistent history using `localStorage` for race execution sessions. Captured run history on completion in `SortingPage.tsx`, `SearchingPage.tsx`, and `PathfindingPage.tsx`. Refactored `HistoryPage.tsx` to read, display, filter, and clear stored history logs. Added `frontend/src/utils/historyStorage.ts` for standardized storage operations. Evaluated against unit tests and manual visual verification with Playwright. --- frontend/src/pages/HistoryPage.tsx | 75 +++++++++++++++++++++++++- frontend/src/pages/PathfindingPage.tsx | 16 ++++++ frontend/src/pages/SearchingPage.tsx | 15 ++++++ frontend/src/pages/SortingPage.tsx | 16 ++++++ frontend/src/utils/historyStorage.ts | 48 +++++++++++++++++ 5 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 frontend/src/utils/historyStorage.ts diff --git a/frontend/src/pages/HistoryPage.tsx b/frontend/src/pages/HistoryPage.tsx index bcc6b60..d1125d3 100644 --- a/frontend/src/pages/HistoryPage.tsx +++ b/frontend/src/pages/HistoryPage.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import type { CatalogResponse } from '../models/types'; import { Trophy, @@ -14,8 +14,10 @@ import { Database, ArrowRight, Sparkles, - Info + Info, + Trash2 } from 'lucide-react'; +import { getHistory, clearHistory, type RaceHistoryEntry, type ArenaType } from '../utils/historyStorage'; interface AlgoMeta { name: string; @@ -384,6 +386,22 @@ export function HistoryPage({ catalog }: { catalog: CatalogResponse }) { const [selectedAlgo, setSelectedAlgo] = useState('Quick Sort'); const [activeTab, setActiveTab] = useState<'sorting' | 'searching' | 'pathfinding'>('sorting'); + const [historyEntries, setHistoryEntries] = useState([]); + const [historyFilter, setHistoryFilter] = useState<'all' | ArenaType>('all'); + + useEffect(() => { + setHistoryEntries(getHistory().reverse()); + }, []); + + const handleClearHistory = () => { + clearHistory(); + setHistoryEntries([]); + }; + + const filteredHistory = historyEntries.filter(entry => + historyFilter === 'all' || entry.arenaType === historyFilter + ); + const selectedData = algoDatabase[getNormalizedName(selectedAlgo)] || algoDatabase['Quick Sort']; const speedRankings = [ @@ -692,6 +710,59 @@ export function HistoryPage({ catalog }: { catalog: CatalogResponse }) { + {/* Persistent Race Session History */} +
+
+ Persistent Race Session History +
+ + +
+
+ + {filteredHistory.length === 0 ? ( +
+ No race history available. Run some algorithms in the arenas to generate history! +
+ ) : ( +
+
+ Date + Arena Type + Dataset Size + Winner +
+ {filteredHistory.map((entry) => ( +
+ {new Date(entry.date).toLocaleString()} + {entry.arenaType} + {entry.arenaType === 'pathfinding' ? 'N/A' : entry.datasetSize} + + + {entry.winner} + +
+ ))} +
+ )} +
+ {/* Real World Applications */}
Real World Production Applications
diff --git a/frontend/src/pages/PathfindingPage.tsx b/frontend/src/pages/PathfindingPage.tsx index 96cc2e5..b0f858b 100644 --- a/frontend/src/pages/PathfindingPage.tsx +++ b/frontend/src/pages/PathfindingPage.tsx @@ -15,6 +15,7 @@ import { StepExplanationCard } from '../components/StepExplanationCard'; import { Share2, RefreshCw, Sparkles, Palette } from 'lucide-react'; import { getUrlParams } from '../utils/urlParams'; import { generateClientMaze } from '../utils/clientMazeGenerator'; +import { appendHistory } from '../utils/historyStorage'; const defaultMazeTypes = [ 'Recursive Backtracker', @@ -355,6 +356,21 @@ export function PathfindingPage({ catalog }: { catalog: CatalogResponse }) { useEffect(() => { if (isCompleted && response && !winnerAnnouncedRef.current) { winnerAnnouncedRef.current = true; + + appendHistory({ + id: Date.now().toString(), + date: new Date().toISOString(), + arenaType: 'pathfinding', + winner: response.winner || 'Tie', + datasetSize: 0, + lanes: response.lanes.map(l => ({ + name: l.name, + comparisons: l.stats.comparisons, + steps: l.stats.steps, + timeMs: l.stats.timeMs + })) + }); + if (response.winner) { setTimeout(() => play('winner'), 120); } else { diff --git a/frontend/src/pages/SearchingPage.tsx b/frontend/src/pages/SearchingPage.tsx index e31b34f..4c60c5e 100644 --- a/frontend/src/pages/SearchingPage.tsx +++ b/frontend/src/pages/SearchingPage.tsx @@ -15,6 +15,7 @@ import { parseCustomArrayInput } from '../utils/arrayParser'; import { StepExplanationCard } from '../components/StepExplanationCard'; import { CustomDatasetModal } from '../components/CustomDatasetModal'; import { CsvUploader } from '../components/CsvUploader'; +import { appendHistory } from '../utils/historyStorage'; import { Share2 } from 'lucide-react'; import { getUrlParams } from '../utils/urlParams'; @@ -450,6 +451,20 @@ export function SearchingPage({ catalog }: { catalog: CatalogResponse }) { useEffect(() => { if (isCompleted && activeResponse && !winnerAnnouncedRef.current) { winnerAnnouncedRef.current = true; + + appendHistory({ + id: Date.now().toString(), + date: new Date().toISOString(), + arenaType: 'searching', + winner: activeResponse.winner || 'Tie', + datasetSize: size, + lanes: activeResponse.lanes.map(l => ({ + name: l.name, + comparisons: l.stats.comparisons, + timeMs: l.stats.timeMs + })) + }); + if (activeResponse.winner) { setTimeout(() => play('winner'), 120); } else { diff --git a/frontend/src/pages/SortingPage.tsx b/frontend/src/pages/SortingPage.tsx index 9cf3764..f05660d 100644 --- a/frontend/src/pages/SortingPage.tsx +++ b/frontend/src/pages/SortingPage.tsx @@ -15,6 +15,7 @@ import { parseCustomArrayInput } from '../utils/arrayParser'; import { StepExplanationCard } from '../components/StepExplanationCard'; import { CustomDatasetModal } from '../components/CustomDatasetModal'; import { CsvUploader } from '../components/CsvUploader'; +import { appendHistory } from '../utils/historyStorage'; import { Share2 } from 'lucide-react'; import { getUrlParams } from '../utils/urlParams'; @@ -429,6 +430,21 @@ export function SortingPage({ catalog }: { catalog: CatalogResponse }) { useEffect(() => { if (isCompleted && activeResponse && !winnerAnnouncedRef.current) { winnerAnnouncedRef.current = true; + + appendHistory({ + id: Date.now().toString(), + date: new Date().toISOString(), + arenaType: 'sorting', + winner: activeResponse.winner || 'Tie', + datasetSize: size, + lanes: activeResponse.lanes.map(l => ({ + name: l.name, + comparisons: l.stats.comparisons, + swaps: l.stats.swaps, + timeMs: l.stats.timeMs + })) + }); + if (activeResponse.winner) { setTimeout(() => play('winner'), 120); } else { diff --git a/frontend/src/utils/historyStorage.ts b/frontend/src/utils/historyStorage.ts new file mode 100644 index 0000000..a8b6340 --- /dev/null +++ b/frontend/src/utils/historyStorage.ts @@ -0,0 +1,48 @@ +export type ArenaType = 'sorting' | 'searching' | 'pathfinding'; + +export type LaneMetric = { + name: string; + comparisons: number; + swaps?: number; + steps?: number; + timeMs: number; +}; + +export type RaceHistoryEntry = { + id: string; + date: string; + arenaType: ArenaType; + winner: string; + datasetSize: number; + lanes: LaneMetric[]; +}; + +const HISTORY_KEY = 'algorace_history'; + +export function getHistory(): RaceHistoryEntry[] { + try { + const data = localStorage.getItem(HISTORY_KEY); + return data ? JSON.parse(data) : []; + } catch (err) { + console.error('Failed to parse history from localStorage', err); + return []; + } +} + +export function appendHistory(entry: RaceHistoryEntry): void { + try { + const history = getHistory(); + history.push(entry); + localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); + } catch (err) { + console.error('Failed to save history to localStorage', err); + } +} + +export function clearHistory(): void { + try { + localStorage.removeItem(HISTORY_KEY); + } catch (err) { + console.error('Failed to clear history from localStorage', err); + } +}