Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 73 additions & 2 deletions frontend/src/pages/HistoryPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import type { CatalogResponse } from '../models/types';
import {
Trophy,
Expand All @@ -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;
Expand Down Expand Up @@ -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<RaceHistoryEntry[]>([]);
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 = [
Expand Down Expand Up @@ -692,6 +710,59 @@ export function HistoryPage({ catalog }: { catalog: CatalogResponse }) {
</div>
</section>

{/* Persistent Race Session History */}
<section className="panel" style={{ padding: '28px' }}>
<div className="section-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Persistent Race Session History</span>
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<select
className="select-dropdown"
value={historyFilter}
onChange={(e) => setHistoryFilter(e.target.value as 'all' | ArenaType)}
style={{ padding: '4px 12px', fontSize: '0.9rem' }}
>
<option value="all">All Arenas</option>
<option value="sorting">Sorting</option>
<option value="searching">Searching</option>
<option value="pathfinding">Pathfinding</option>
</select>
<button
className="btn btn-secondary"
style={{ display: 'flex', gap: '6px', alignItems: 'center', padding: '6px 12px', color: '#ff4444', borderColor: '#ff4444' }}
onClick={handleClearHistory}
>
<Trash2 size={16} /> Clear History
</button>
</div>
</div>

{filteredHistory.length === 0 ? (
<div style={{ padding: '40px', textAlign: 'center', color: 'var(--muted)' }}>
No race history available. Run some algorithms in the arenas to generate history!
</div>
) : (
<div className="matrix-list">
<div className="matrix-row matrix-header" style={{ gridTemplateColumns: '1fr 1fr 1fr 1.5fr' }}>
<strong>Date</strong>
<span>Arena Type</span>
<span>Dataset Size</span>
<span>Winner</span>
</div>
{filteredHistory.map((entry) => (
<div className="matrix-row" key={entry.id} style={{ gridTemplateColumns: '1fr 1fr 1fr 1.5fr' }}>
<strong>{new Date(entry.date).toLocaleString()}</strong>
<span style={{ textTransform: 'capitalize' }}>{entry.arenaType}</span>
<span>{entry.arenaType === 'pathfinding' ? 'N/A' : entry.datasetSize}</span>
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Trophy size={14} color="gold" />
{entry.winner}
</span>
</div>
))}
</div>
)}
</section>

{/* Real World Applications */}
<section className="panel" style={{ padding: '28px' }}>
<div className="section-title">Real World Production Applications</div>
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/pages/PathfindingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/pages/SearchingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 {
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/pages/SortingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 {
Expand Down
48 changes: 48 additions & 0 deletions frontend/src/utils/historyStorage.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading