From 111cdb249700195555127e809309bf81732721ff Mon Sep 17 00:00:00 2001 From: Sanan507 <227714367+Sanan507@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:24:24 +0000 Subject: [PATCH] feat: add export telemetry functionality (CSV/JSON) - Added `exportTelemetry.ts` for handling CSV and JSON Blob creation and download. - Added CSV injection sanitization. - Updated `AlgorithmComparisonCenter.tsx` to display Export CSV and Export JSON buttons. - Passed `raceResponse` data from `SortingPage`, `SearchingPage`, and `PathfindingPage` components to `AlgorithmComparisonCenter`. Closes #87 --- .../components/AlgorithmComparisonCenter.tsx | 31 ++++++- frontend/src/pages/PathfindingPage.tsx | 1 + frontend/src/pages/SearchingPage.tsx | 1 + frontend/src/pages/SortingPage.tsx | 1 + frontend/src/utils/exportTelemetry.ts | 93 +++++++++++++++++++ 5 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 frontend/src/utils/exportTelemetry.ts diff --git a/frontend/src/components/AlgorithmComparisonCenter.tsx b/frontend/src/components/AlgorithmComparisonCenter.tsx index 32f68ae..7233473 100644 --- a/frontend/src/components/AlgorithmComparisonCenter.tsx +++ b/frontend/src/components/AlgorithmComparisonCenter.tsx @@ -1,11 +1,14 @@ import { useState } from 'react'; import type { CatalogResponse, SimulationFrame } from '../models/types'; +import type { RaceResponse } from '../models/types'; import { getSortingMeta, getSearchingMeta, getPathfindingMeta, } from '../data/algorithmMetadata'; import { PseudocodeViewer } from './PseudocodeViewer'; +import { Download } from 'lucide-react'; +import { exportRaceToCSV, exportRaceToJSON } from '../utils/exportTelemetry'; interface AlgorithmComparisonCenterProps { algorithms: string[]; @@ -14,6 +17,7 @@ interface AlgorithmComparisonCenterProps { activeFrames?: Record | null; prevFrames?: Record | null; maxFrames?: number; + raceResponse?: RaceResponse | null; } export function AlgorithmComparisonCenter({ @@ -23,6 +27,7 @@ export function AlgorithmComparisonCenter({ activeFrames, prevFrames, maxFrames, + raceResponse, }: AlgorithmComparisonCenterProps) { const [active, setActive] = useState(0); const alg = algorithms[active] ?? algorithms[0] ?? ''; @@ -68,9 +73,29 @@ export function AlgorithmComparisonCenter({ return (
{/* Header */} -
-
Algorithm Comparison Center
-
Multi-Language Code & Algorithm Deep-Dive
+
+
+
Algorithm Comparison Center
+
Multi-Language Code & Algorithm Deep-Dive
+
+ {raceResponse && ( +
+ + +
+ )}
diff --git a/frontend/src/pages/PathfindingPage.tsx b/frontend/src/pages/PathfindingPage.tsx index 96cc2e5..11c1130 100644 --- a/frontend/src/pages/PathfindingPage.tsx +++ b/frontend/src/pages/PathfindingPage.tsx @@ -583,6 +583,7 @@ export function PathfindingPage({ catalog }: { catalog: CatalogResponse }) { activeFrames={activeFramesMap} prevFrames={prevFramesMap} maxFrames={playback.maxFrames} + raceResponse={response} />
diff --git a/frontend/src/pages/SearchingPage.tsx b/frontend/src/pages/SearchingPage.tsx index e31b34f..4d10fd8 100644 --- a/frontend/src/pages/SearchingPage.tsx +++ b/frontend/src/pages/SearchingPage.tsx @@ -644,6 +644,7 @@ export function SearchingPage({ catalog }: { catalog: CatalogResponse }) { activeFrames={activeFramesMap} prevFrames={prevFramesMap} maxFrames={playback.maxFrames} + raceResponse={activeResponse} />
diff --git a/frontend/src/pages/SortingPage.tsx b/frontend/src/pages/SortingPage.tsx index 9cf3764..4157f8b 100644 --- a/frontend/src/pages/SortingPage.tsx +++ b/frontend/src/pages/SortingPage.tsx @@ -609,6 +609,7 @@ export function SortingPage({ catalog }: { catalog: CatalogResponse }) { activeFrames={activeFramesMap} prevFrames={prevFramesMap} maxFrames={playback.maxFrames} + raceResponse={activeResponse} /> diff --git a/frontend/src/utils/exportTelemetry.ts b/frontend/src/utils/exportTelemetry.ts new file mode 100644 index 0000000..e0f49f7 --- /dev/null +++ b/frontend/src/utils/exportTelemetry.ts @@ -0,0 +1,93 @@ +import type { RaceResponse } from '../models/types'; + +function triggerDownload(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +// Security: RFC 4180 CSV field sanitizer +function sanitizeCsvField(field: string | number | undefined | null | boolean): string { + if (field === undefined || field === null) return '""'; + let str = String(field); + if (/^[=+\-@\t\r]/.test(str)) { + str = `'${str}`; + } + const escaped = str.replace(/"/g, '""'); + return `"${escaped}"`; +} + +export function exportRaceToCSV(raceData: RaceResponse) { + const headers = ['Algorithm', 'Operations', 'Execution Time (ms)', 'Swaps / Path Steps', 'Winner', 'Dataset Config']; + const rows: string[][] = []; + + const datasetConfig = raceData.type === 'sorting' + ? (raceData.dataset ? `Array[${raceData.dataset.length}]` : 'N/A') + : raceData.type === 'searching' + ? (raceData.dataset ? `Array[${raceData.dataset.length}] Target: ${raceData.target}` : 'N/A') + : (raceData.walls ? `Grid[${raceData.walls.length}x${raceData.walls[0].length}]` : 'N/A'); + + raceData.lanes.forEach(lane => { + const isWinner = raceData.winner === lane.name ? 'Yes' : 'No'; + let operations = 0; + let secValue = 0; + + if (raceData.type === 'sorting') { + operations = lane.stats.comparisons; + secValue = lane.stats.swaps; + } else if (raceData.type === 'searching') { + operations = lane.stats.comparisons; + } else { + operations = lane.stats.comparisons; + secValue = lane.stats.steps; + } + + const row = [ + lane.name, + operations.toString(), + lane.stats.timeMs.toString(), + secValue.toString(), + isWinner, + datasetConfig + ]; + rows.push(row.map(sanitizeCsvField)); + }); + + const csvContent = [ + headers.map(sanitizeCsvField).join(','), + ...rows.map(row => row.join(',')) + ].join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + triggerDownload(blob, `race_telemetry_${Date.now()}.csv`); +} + +export function exportRaceToJSON(raceData: RaceResponse) { + const telemetry = { + type: raceData.type, + winner: raceData.winner, + datasetConfig: raceData.type === 'sorting' + ? { size: raceData.dataset?.length } + : raceData.type === 'searching' + ? { size: raceData.dataset?.length, target: raceData.target } + : { gridRows: raceData.walls?.length, gridCols: raceData.walls?.[0]?.length }, + results: raceData.lanes.map(lane => ({ + algorithm: lane.name, + operations: raceData.type !== 'pathfinding' ? lane.stats.comparisons : lane.stats.steps, + comparisons: lane.stats.comparisons, + swaps: lane.stats.swaps, + steps: lane.stats.steps, + timeMs: lane.stats.timeMs, + isWinner: raceData.winner === lane.name + })) + }; + + const jsonContent = JSON.stringify(telemetry, null, 2); + const blob = new Blob([jsonContent], { type: 'application/json' }); + triggerDownload(blob, `race_telemetry_${Date.now()}.json`); +}