Merge 7ceee1af13a117ba8d79cc5f4aa30d52b63e3e87 into f36c3c85ba31ed0fd27c3650794a511ad994a661

This commit is contained in:
Diego Barquero Quesada 2025-03-12 21:58:20 -06:00 committed by GitHub
commit f82abbeaf2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 10203 additions and 1 deletions

6
.gitignore vendored
View File

@ -4,4 +4,8 @@ results/*
myenv*/
doc/_build
!results/plots.py
Frontend/
das-simulator-visualizer/node_modules
das-simulator-visualizer/build
das-simulator-visualizer/.next
.DS_Store

View File

@ -0,0 +1,91 @@
# DAS Simulator Visualizer
This web application provides a visualization interface for Data Availability Sampling (DAS) simulation results. It allows you to browse, analyze, and compare simulation runs with different parameters, view detailed graphs, and explore parameter relationships through heatmaps.
## Features
- **Simulation List**: Browse and filter all available simulation runs
- **Detailed Graphs**: Visualize various metrics from simulation runs:
- **Heatmaps**: Explore parameter relationships:
- **Parameter Exploration**: Adjust simulation parameters to see their impact on results
- **Statistics**: View aggregated statistics across parameter combinations
## Architecture
The application consists of two main components:
1. **Frontend**: React-based web UI with Next.js framework
2. **Backend**: FastAPI server that processes and serves simulation data
## Running the Application
### Prerequisites
- Node.js 18.x or later
- Python 3.9 or later
- DAS Simulator results (XML files in `results/` directory)
### Backend Setup
1. Install Python dependencies:
```bash
pip install fastapi uvicorn pydantic
```
2. Start the backend server:
```bash
python server.py
```
The server will start on http://localhost:8000 by default.
### Frontend Setup
1. Install Node.js dependencies:
```bash
npm install
```
2. Start the development server:
```bash
npm run dev
```
3. Open your browser and navigate to http://localhost:3000
## Project Structure
```
das-simulator-visualizer/
├── app/ # Next.js app components
│ ├── page.tsx # Homepage (simulation list)
│ ├── simulations/[id]/ # Simulation detail pages
│ ├── globals.css # Global styles
│ └── layout.tsx # Layout component
├── components/ # React components
│ ├── graph-viewer.tsx # Graph visualization component
│ ├── heatmap-viewer.tsx # Heatmap visualization component
│ ├── parameter-selector.tsx # Parameter adjustment component
│ ├── simulation-list.tsx # Simulations listing component
│ ├── simulation-metadata.tsx # Simulation details component
│ ├── simulation-provider.tsx # Data provider component
│ ├── statistics-summary.tsx # Statistics visualization component
│ └── ui/ # UI components (buttons, cards, etc.)
├── lib/ # Utility functions and services
│ └── simulation-service.ts # API client for backend communication
├── public/ # Static assets
└── package.json # Node.js package configuration
```
## Deployment
For production deployment:
1. Build the frontend:
```bash
npm run build
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,87 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--chart-1: 222.2 47.4% 11.2%;
--chart-2: 215 25% 27%;
--chart-3: 0 84.2% 60.2%;
--chart-4: 24.6 95% 53.1%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 210 40% 98%;
--chart-2: 215 20.2% 65.1%;
--chart-3: 0 62.8% 30.6%;
--chart-4: 20.5 90% 48.2%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -0,0 +1,27 @@
import type React from "react"
import { SimulationProvider } from "@/components/simulation-provider"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "DAS Simulator Visualizer",
description: "Visualize Data Availability Sampling simulation results",
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body className={inter.className}>
<SimulationProvider>{children}</SimulationProvider>
</body>
</html>
)
}

View File

@ -0,0 +1,22 @@
import { SimulationList } from "@/components/simulation-list"
export default function HomePage() {
return (
<div className="flex flex-col min-h-screen">
<header className="border-b bg-background">
<div className="container flex h-16 items-center px-4 md:px-6">
<h1 className="text-lg font-semibold">DAS Simulator Visualizer</h1>
</div>
</header>
<main className="flex-1 container py-6 px-4 md:px-6">
<SimulationList />
</main>
<footer className="border-t py-4 bg-background">
<div className="container flex flex-col items-center justify-between gap-4 px-4 md:flex-row md:px-6">
<p className="text-sm text-muted-foreground">&copy; {new Date().getFullYear()} DAS Simulator Visualizer</p>
</div>
</footer>
</div>
)
}

View File

@ -0,0 +1,229 @@
"use client"
import { useEffect, useState } from "react"
import { useParams, useRouter } from "next/navigation"
import { ArrowLeft, Download, Maximize } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { SimulationMetadata } from "@/components/simulation-metadata"
import { ParameterSelector } from "@/components/parameter-selector"
import { GraphViewer } from "@/components/graph-viewer"
import { HeatmapViewer } from "@/components/heatmap-viewer"
import { StatisticsSummary } from "@/components/statistics-summary"
import { useSimulation } from "@/components/simulation-provider"
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"
import { Skeleton } from "@/components/ui/skeleton"
interface SimulationParams {
numberNodes: number
failureRate: number
blockSize: number
netDegree: number
chi: number
maliciousNodes: number
run: number
}
export default function SimulationDetailPage() {
const params = useParams()
const router = useRouter()
const { getSimulationById, loading } = useSimulation()
const [simulation, setSimulation] = useState<any>(null)
const [selectedParams, setSelectedParams] = useState<SimulationParams>({
numberNodes: 128,
failureRate: 40,
blockSize: 64,
netDegree: 8,
chi: 2,
maliciousNodes: 0,
run: 0,
})
const [graphType, setGraphType] = useState("missingSegments")
useEffect(() => {
const fetchSimulation = async () => {
try {
if (params.id) {
const sim = getSimulationById(params.id as string)
setSimulation(sim)
}
} catch (error) {
console.error("Error fetching simulation:", error)
setSimulation(null)
}
}
fetchSimulation()
}, [params.id, getSimulationById])
if (loading) {
return (
<div className="container py-6 px-4 md:px-6">
<div className="flex items-center mb-6">
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4" />
</Button>
<Skeleton className="h-8 w-64 ml-2" />
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<Skeleton className="h-[200px] w-full" />
<Skeleton className="h-[200px] w-full" />
<Skeleton className="h-[200px] w-full" />
</div>
</div>
)
}
if (!simulation) {
return (
<div className="container py-6 px-4 md:px-6">
<div className="flex items-center mb-6">
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4" />
</Button>
<h1 className="text-xl font-semibold ml-2">Simulation not found</h1>
</div>
<p>The requested simulation could not be found.</p>
</div>
)
}
const handleParamChange = (param: string, value: any) => {
setSelectedParams((prev: SimulationParams) => ({
...prev,
[param]: value,
}))
}
const handleGraphTypeChange = (type: string) => {
setGraphType(type)
}
return (
<div className="container py-6 px-4 md:px-6">
<div className="flex items-center mb-6">
<Button variant="ghost" size="icon" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4" />
</Button>
<h1 className="text-xl font-semibold ml-2">Simulation: {simulation.id}</h1>
</div>
<div className="grid gap-6 md:grid-cols-12">
{/* Sidebar with metadata and parameter selection */}
<div className="md:col-span-4 lg:col-span-3 space-y-6">
<SimulationMetadata simulation={simulation} />
<ParameterSelector
parameters={simulation.parameters}
selectedParams={selectedParams}
onParamChange={handleParamChange}
/>
</div>
{/* Main content area */}
<div className="md:col-span-8 lg:col-span-9 space-y-6">
<Tabs defaultValue="graphs" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="graphs">Graphs</TabsTrigger>
<TabsTrigger value="heatmaps">Heatmaps</TabsTrigger>
<TabsTrigger value="statistics">Statistics</TabsTrigger>
</TabsList>
<TabsContent value="graphs" className="space-y-4">
<Card>
<CardHeader className="pb-2">
<div className="flex justify-between items-center">
<div>
<CardTitle>Graph Viewer</CardTitle>
<CardDescription>View detailed graphs for selected parameters</CardDescription>
</div>
<div className="flex space-x-2">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="icon">
<Maximize className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl">
<GraphViewer
simulation={simulation}
parameters={selectedParams}
graphType={graphType}
fullscreen
/>
</DialogContent>
</Dialog>
<Button variant="outline" size="icon">
<Download className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="mb-4">
<Tabs value={graphType} onValueChange={handleGraphTypeChange} className="w-full">
<TabsList className="grid grid-cols-3 md:grid-cols-7 w-full">
<TabsTrigger value="missingSegments">Missing</TabsTrigger>
<TabsTrigger value="nodesReady">Ready</TabsTrigger>
<TabsTrigger value="sentData">Sent</TabsTrigger>
<TabsTrigger value="recvData">Received</TabsTrigger>
<TabsTrigger value="dupData">Duplicate</TabsTrigger>
<TabsTrigger value="RowColDist">Distribution</TabsTrigger>
<TabsTrigger value="ecdf_restoreRowCount">Row Count</TabsTrigger>
</TabsList>
</Tabs>
</div>
<GraphViewer simulation={simulation} parameters={selectedParams} graphType={graphType} />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="heatmaps" className="space-y-4">
<Card>
<CardHeader className="pb-2">
<div className="flex justify-between items-center">
<div>
<CardTitle>Heatmap Viewer</CardTitle>
<CardDescription>Explore parameter relationships through heatmaps</CardDescription>
</div>
<div className="flex space-x-2">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="icon">
<Maximize className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl">
<HeatmapViewer simulation={simulation} fullscreen />
</DialogContent>
</Dialog>
<Button variant="outline" size="icon">
<Download className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<HeatmapViewer simulation={simulation} />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="statistics" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Statistics Summary</CardTitle>
<CardDescription>View aggregated statistics across all parameter combinations</CardDescription>
</CardHeader>
<CardContent>
<StatisticsSummary simulation={simulation} />
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@ -0,0 +1,77 @@
import React, { useEffect, useState } from "react";
import Image from "next/image";
import { AlertCircle } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Skeleton } from "@/components/ui/skeleton";
import { getGraphUrl } from "@/lib/simulation-service";
interface GraphViewerProps {
simulation: any;
parameters: any;
graphType: string;
fullscreen?: boolean;
}
export function GraphViewer({ simulation, parameters, graphType, fullscreen = false }: GraphViewerProps) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [graphUrl, setGraphUrl] = useState<string | null>(null);
useEffect(() => {
const fetchGraph = async () => {
try {
setLoading(true);
setError(null);
const url = await getGraphUrl(
simulation.id,
parameters.numberNodes,
parameters.failureRate,
parameters.blockSize,
parameters.netDegree,
parameters.chi,
parameters.run,
graphType,
);
setGraphUrl(url);
} catch (err) {
console.error("Error loading graph:", err);
setError("Failed to load graph. Please try again later.");
} finally {
setLoading(false);
}
};
fetchGraph();
}, [simulation.id, parameters, graphType]);
return (
<div className={`w-full ${fullscreen ? "h-[600px]" : "h-[300px]"} relative`}>
{loading ? (
<Skeleton className={`w-full ${fullscreen ? "h-[600px]" : "h-[300px]"}`} />
) : error ? (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : !graphUrl ? (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertTitle>No data available</AlertTitle>
<AlertDescription>No graph data available for the selected parameters.</AlertDescription>
</Alert>
) : (
<Image
src={graphUrl}
alt={`${graphType} graph for the selected parameters`}
fill
className="object-contain"
onError={() => setError("Failed to load graph image.")}
/>
)}
</div>
);
}

View File

@ -0,0 +1,89 @@
import React, { useEffect, useState } from "react";
import Image from "next/image";
import { AlertCircle } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getHeatmapUrl } from "@/lib/simulation-service";
interface HeatmapViewerProps {
simulation: any;
fullscreen?: boolean;
}
export function HeatmapViewer({ simulation, fullscreen = false }: HeatmapViewerProps) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [heatmapType, setHeatmapType] = useState("nodesVsFailure");
const [heatmapUrl, setHeatmapUrl] = useState<string | null>(null);
const heatmapTypes = [
{ id: "nodesVsFailure", label: "Nodes vs Failure" },
{ id: "nodesVsChi", label: "Nodes vs Chi" },
{ id: "failureVsChi", label: "Failure vs Chi" },
{ id: "failureVsNetDegree", label: "Failure vs Net Degree" },
{ id: "NWDegVsNodeOnRuntime", label: "Network Degree vs Nodes" },
{ id: "NWDegVsMalNodeOnMissingSamples", label: "Net Degree vs Malicious Nodes" },
{ id: "NWDegVsFailureRateOnMissingSamples", label: "Net Degree vs Failure Rate" },
];
useEffect(() => {
const fetchHeatmap = async () => {
try {
setLoading(true);
setError(null);
const url = await getHeatmapUrl(simulation.id, heatmapType);
setHeatmapUrl(url);
} catch (err) {
console.error("Error loading heatmap:", err);
setError("Failed to load heatmap. Please try again later.");
} finally {
setLoading(false);
}
};
fetchHeatmap();
}, [simulation.id, heatmapType]);
return (
<div className="space-y-4">
<Tabs value={heatmapType} onValueChange={setHeatmapType} className="w-full">
<TabsList className="grid grid-cols-2 md:grid-cols-4 w-full">
{heatmapTypes.map((type) => (
<TabsTrigger key={type.id} value={type.id}>
{type.label}
</TabsTrigger>
))}
</TabsList>
</Tabs>
<div className={`w-full ${fullscreen ? "h-[600px]" : "h-[300px]"} relative`}>
{loading ? (
<Skeleton className={`w-full ${fullscreen ? "h-[600px]" : "h-[300px]"}`} />
) : error ? (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : !heatmapUrl ? (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertTitle>No data available</AlertTitle>
<AlertDescription>No heatmap data available for the selected parameters.</AlertDescription>
</Alert>
) : (
<Image
src={heatmapUrl}
alt={`${heatmapType} heatmap`}
fill
className="object-contain"
onError={() => setError("Failed to load heatmap image.")}
/>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,146 @@
import React from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
interface ParameterSelectorProps {
parameters: any;
selectedParams: any;
onParamChange: (param: string, value: any) => void;
}
export function ParameterSelector({ parameters, selectedParams, onParamChange }: ParameterSelectorProps) {
return (
<Card>
<CardHeader>
<CardTitle>Parameter Selection</CardTitle>
<CardDescription>Adjust parameters to view different simulation results</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between">
<Label htmlFor="numberNodes">Number of Nodes</Label>
<span className="text-sm">{selectedParams.numberNodes}</span>
</div>
<Slider
id="numberNodes"
min={parameters.numberNodes.min}
max={parameters.numberNodes.max}
step={parameters.numberNodes.step || 128}
value={[selectedParams.numberNodes]}
onValueChange={(value) => onParamChange("numberNodes", value[0])}
/>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<Label htmlFor="failureRate">Failure Rate (%)</Label>
<span className="text-sm">{selectedParams.failureRate}%</span>
</div>
<Slider
id="failureRate"
min={parameters.failureRate.min}
max={parameters.failureRate.max}
step={parameters.failureRate.step || 10}
value={[selectedParams.failureRate]}
onValueChange={(value) => onParamChange("failureRate", value[0])}
/>
</div>
{parameters.maliciousNodes && (
<div className="space-y-2">
<div className="flex justify-between">
<Label htmlFor="maliciousNodes">Malicious Nodes (%)</Label>
<span className="text-sm">{selectedParams.maliciousNodes || 0}%</span>
</div>
<Slider
id="maliciousNodes"
min={0}
max={100}
step={20}
value={[selectedParams.maliciousNodes || 0]}
onValueChange={(value) => onParamChange("maliciousNodes", value[0])}
/>
</div>
)}
<div className="space-y-2">
<Label htmlFor="blockSize">Block Size</Label>
<Select
value={selectedParams.blockSize.toString()}
onValueChange={(value) => onParamChange("blockSize", Number.parseInt(value))}
>
<SelectTrigger id="blockSize">
<SelectValue placeholder="Select block size" />
</SelectTrigger>
<SelectContent>
{parameters.blockSize.options?.map((option: number) => (
<SelectItem key={option} value={option.toString()}>
{option}
</SelectItem>
)) || <SelectItem value={parameters.blockSize.value.toString()}>{parameters.blockSize.value}</SelectItem>}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="netDegree">Network Degree</Label>
<Select
value={selectedParams.netDegree.toString()}
onValueChange={(value) => onParamChange("netDegree", Number.parseInt(value))}
>
<SelectTrigger id="netDegree">
<SelectValue placeholder="Select network degree" />
</SelectTrigger>
<SelectContent>
{parameters.netDegree.options?.map((option: number) => (
<SelectItem key={option} value={option.toString()}>
{option}
</SelectItem>
)) || <SelectItem value={parameters.netDegree.value.toString()}>{parameters.netDegree.value}</SelectItem>}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="chi">Custody Size</Label>
<Select
value={selectedParams.chi.toString()}
onValueChange={(value) => onParamChange("chi", Number.parseInt(value))}
>
<SelectTrigger id="chi">
<SelectValue placeholder="Select custody size" />
</SelectTrigger>
<SelectContent>
{parameters.chi.options?.map((option: number) => (
<SelectItem key={option} value={option.toString()}>
{option}
</SelectItem>
)) || <SelectItem value={parameters.chi.value.toString()}>{parameters.chi.value}</SelectItem>}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="run">Run</Label>
<Select
value={selectedParams.run.toString()}
onValueChange={(value) => onParamChange("run", Number.parseInt(value))}
>
<SelectTrigger id="run">
<SelectValue placeholder="Select run" />
</SelectTrigger>
<SelectContent>
{Array.from({ length: parameters.run.max + 1 }, (_, i) => (
<SelectItem key={i} value={i.toString()}>
Run {i}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,274 @@
"use client"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { format, parseISO } from "date-fns"
import { Calendar, ChevronRight, Filter, Search } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Skeleton } from "@/components/ui/skeleton"
import { useSimulation } from "./simulation-provider"
import { Badge } from "@/components/ui/badge"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Calendar as CalendarComponent } from "@/components/ui/calendar"
import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
export function SimulationList() {
const router = useRouter()
const { simulations, loading, error } = useSimulation()
const [searchTerm, setSearchTerm] = useState("")
const [dateRange, setDateRange] = useState<{ from: Date | undefined; to: Date | undefined }>({
from: undefined,
to: undefined,
})
const [showFilters, setShowFilters] = useState(false)
const [successFilter, setSuccessFilter] = useState<string | null>(null)
const handleSimulationSelect = (id: string) => {
router.push(`/simulations/${id}`)
}
const formatDate = (dateString: string) => {
try {
if (dateString.includes('T') && dateString.endsWith(':00Z')) {
dateString = dateString.replace(':00Z', 'Z');
}
const date = parseISO(dateString);
return format(date, "PPP");
} catch (error) {
console.error("Error formatting date:", dateString, error);
try {
if (dateString.includes("_")) {
const parts = dateString.split("_");
if (parts.length >= 2) {
const datePart = parts[0];
const timePart = parts[1].replace(/-/g, ":");
return `${datePart} ${timePart}`;
}
}
return new Date(dateString).toLocaleDateString();
} catch (e) {
return "Date unavailable";
}
}
};
const filteredSimulations = simulations.filter((sim) => {
if (searchTerm && !sim.id.toLowerCase().includes(searchTerm.toLowerCase())) {
return false
}
if (dateRange.from) {
try {
const simDate = new Date(sim.date);
if (simDate < dateRange.from) return false;
} catch (e) {
console.warn("Could not parse date for filtering:", sim.date);
}
}
if (dateRange.to) {
try {
const simDate = new Date(sim.date);
if (simDate > dateRange.to) return false;
} catch (e) {
console.warn("Could not parse date for filtering:", sim.date);
}
}
if (successFilter === "high" && sim.successRate < 75) {
return false
}
if (successFilter === "medium" && (sim.successRate < 50 || sim.successRate >= 75)) {
return false
}
if (successFilter === "low" && sim.successRate >= 50) {
return false
}
return true
})
if (error) {
return (
<div className="flex flex-col items-center justify-center p-8">
<h2 className="text-xl font-semibold text-red-500 mb-2">Error</h2>
<p className="text-muted-foreground">{error}</p>
<Button className="mt-4" onClick={() => window.location.reload()}>
Try Again
</Button>
</div>
)
}
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<h2 className="text-2xl font-bold tracking-tight">Simulation Runs</h2>
<div className="flex gap-2 w-full sm:w-auto">
<div className="relative w-full sm:w-auto">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Search simulations..."
className="w-full sm:w-[250px] pl-8"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<Popover open={showFilters} onOpenChange={setShowFilters}>
<PopoverTrigger asChild>
<Button variant="outline" size="icon">
<Filter className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="grid gap-4">
<div className="space-y-2">
<h4 className="font-medium leading-none">Filters</h4>
<p className="text-sm text-muted-foreground">Filter simulation results by date and success rate</p>
</div>
<div className="space-y-2">
<Label>Date Range</Label>
<CalendarComponent
mode="range"
selected={dateRange}
onSelect={setDateRange as any}
className="rounded-md border"
/>
</div>
<div className="space-y-2">
<Label>Success Rate</Label>
<div className="grid gap-2">
<div className="flex items-center space-x-2">
<Checkbox
id="success-high"
checked={successFilter === "high"}
onCheckedChange={() => setSuccessFilter(successFilter === "high" ? null : "high")}
/>
<label htmlFor="success-high" className="text-sm">
High (75-100%)
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="success-medium"
checked={successFilter === "medium"}
onCheckedChange={() => setSuccessFilter(successFilter === "medium" ? null : "medium")}
/>
<label htmlFor="success-medium" className="text-sm">
Medium (50-75%)
</label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="success-low"
checked={successFilter === "low"}
onCheckedChange={() => setSuccessFilter(successFilter === "low" ? null : "low")}
/>
<label htmlFor="success-low" className="text-sm">
Low (0-50%)
</label>
</div>
</div>
</div>
<Button
onClick={() => {
setDateRange({ from: undefined, to: undefined })
setSuccessFilter(null)
}}
>
Reset Filters
</Button>
</div>
</PopoverContent>
</Popover>
</div>
</div>
{loading ? (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{[1, 2, 3, 4, 5, 6].map((i) => (
<Card key={i} className="overflow-hidden">
<CardHeader className="pb-2">
<Skeleton className="h-5 w-1/2 mb-2" />
<Skeleton className="h-4 w-3/4" />
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
</CardContent>
<CardFooter>
<Skeleton className="h-9 w-full" />
</CardFooter>
</Card>
))}
</div>
) : filteredSimulations.length === 0 ? (
<div className="flex flex-col items-center justify-center p-8 border rounded-lg">
<h3 className="text-lg font-semibold mb-2">No simulations found</h3>
<p className="text-muted-foreground text-center">
{searchTerm || dateRange.from || dateRange.to || successFilter
? "Try adjusting your filters to see more results."
: "No simulation runs are available. Run a simulation to get started."}
</p>
</div>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{filteredSimulations.map((simulation) => (
<Card key={simulation.id} className="overflow-hidden">
<CardHeader className="pb-2">
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
<CardDescription>{formatDate(simulation.date || simulation.id)}</CardDescription>
</div>
<CardTitle className="text-lg">{simulation.id}</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-y-2 text-sm">
<div className="text-muted-foreground">Nodes:</div>
<div>
{simulation.parameters.numberNodes.min}-{simulation.parameters.numberNodes.max}
</div>
<div className="text-muted-foreground">Failure Rate:</div>
<div>
{simulation.parameters.failureRate.min}%-{simulation.parameters.failureRate.max}%
</div>
<div className="text-muted-foreground">Success Rate:</div>
<div className="flex items-center">
<Badge
className={
simulation.successRate >= 75
? "bg-green-500"
: simulation.successRate >= 50
? "bg-yellow-500"
: "bg-red-500"
}
>
{simulation.successRate}%
</Badge>
</div>
</div>
</CardContent>
<CardFooter>
<Button className="w-full" onClick={() => handleSimulationSelect(simulation.id)}>
View Details
<ChevronRight className="ml-2 h-4 w-4" />
</Button>
</CardFooter>
</Card>
))}
</div>
)}
</div>
)
}

View File

@ -0,0 +1,96 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { format, parseISO } from "date-fns"
import { Badge } from "./ui/badge"
interface SimulationMetadataProps {
simulation: any
}
const formatDate = (dateString: string) => {
try {
if (dateString && dateString.includes('T') && dateString.endsWith(':00Z')) {
dateString = dateString.replace(':00Z', 'Z');
}
const date = parseISO(dateString);
return format(date, "PPP 'at' p");
} catch (error) {
console.error("Error formatting date:", dateString, error);
try {
if (dateString && dateString.includes("_")) {
const parts = dateString.split("_");
if (parts.length >= 2) {
const datePart = parts[0];
const timePart = parts[1].replace(/-/g, ":");
return `${datePart} at ${timePart}`;
}
}
return dateString || "Date unavailable";
} catch (e) {
return "Date unavailable";
}
}
};
export function SimulationMetadata({ simulation }: SimulationMetadataProps) {
if (!simulation) {
return <div>Loading...</div>;
}
return (
<Card>
<CardHeader>
<CardTitle>Simulation Details</CardTitle>
<CardDescription>Run on {formatDate(simulation.date || simulation.id)}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<h4 className="text-sm font-medium mb-2">Parameter Ranges</h4>
<div className="grid grid-cols-2 gap-y-2 text-sm">
<div className="text-muted-foreground">Nodes:</div>
<div>
{simulation.parameters.numberNodes.min}-{simulation.parameters.numberNodes.max}
</div>
<div className="text-muted-foreground">Failure Rate:</div>
<div>
{simulation.parameters.failureRate.min}%-{simulation.parameters.failureRate.max}%
</div>
<div className="text-muted-foreground">Block Size:</div>
<div>{simulation.parameters.blockSize.value}</div>
<div className="text-muted-foreground">Net Degree:</div>
<div>{simulation.parameters.netDegree.value}</div>
<div className="text-muted-foreground">Chi:</div>
<div>{simulation.parameters.chi.value}</div>
<div className="text-muted-foreground">Runs:</div>
<div>{simulation.parameters.run.max + 1}</div>
</div>
</div>
<div>
<h4 className="text-sm font-medium mb-2">Overall Performance</h4>
<div className="grid grid-cols-2 gap-y-2 text-sm">
<div className="text-muted-foreground">Success Rate:</div>
<div>
<Badge
className={
simulation.successRate >= 75
? "bg-green-500"
: simulation.successRate >= 50
? "bg-yellow-500"
: "bg-red-500"
}
>
{simulation.successRate}%
</Badge>
</div>
<div className="text-muted-foreground">Avg. Missing Samples:</div>
<div>{simulation.avgMissingSamples.toFixed(2)}%</div>
<div className="text-muted-foreground">Avg. Nodes Ready:</div>
<div>{simulation.avgNodesReady.toFixed(2)}%</div>
</div>
</div>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,101 @@
"use client"
import { createContext, useContext, useState, useEffect, type ReactNode } from "react"
import { fetchSimulations } from "@/lib/simulation-service"
type SimulationContextType = {
simulations: any[]
loading: boolean
error: string | null
getSimulationById: (id: string) => any
refreshSimulations: () => Promise<void>
}
const SimulationContext = createContext<SimulationContextType | undefined>(undefined)
export function SimulationProvider({ children }: { children: ReactNode }) {
const [simulations, setSimulations] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const refreshSimulations = async () => {
try {
setLoading(true)
setError(null)
const data = await fetchSimulations()
setSimulations(data)
} catch (err) {
setError("Failed to fetch simulations")
console.error(err)
} finally {
setLoading(false)
}
}
const getSimulationById = (id: string) => {
try {
const cachedSimulation = simulations.find((sim) => sim.id === id)
if (cachedSimulation) return cachedSimulation
return {
id,
date: new Date().toISOString(),
parameters: {
numberNodes: { min: 128, max: 512, step: 128 },
failureRate: { min: 40, max: 80, step: 10 },
blockSize: { value: 64, options: [32, 64, 128] },
netDegree: { value: 8, options: [4, 8, 16] },
chi: { value: 2, options: [1, 2, 3, 4] },
run: { max: 2 },
},
successRate: 75,
avgMissingSamples: 20,
avgNodesReady: 80,
}
} catch (err) {
console.error("Error getting simulation by ID:", err)
return {
id,
date: new Date().toISOString(),
parameters: {
numberNodes: { min: 128, max: 512, step: 128 },
failureRate: { min: 40, max: 80, step: 10 },
blockSize: { value: 64, options: [32, 64, 128] },
netDegree: { value: 8, options: [4, 8, 16] },
chi: { value: 2, options: [1, 2, 3, 4] },
run: { max: 2 },
},
successRate: 0,
avgMissingSamples: 0,
avgNodesReady: 0,
}
}
}
useEffect(() => {
refreshSimulations()
}, [])
return (
<SimulationContext.Provider
value={{
simulations,
loading,
error,
getSimulationById,
refreshSimulations,
}}
>
{children}
</SimulationContext.Provider>
)
}
export function useSimulation() {
const context = useContext(SimulationContext)
if (context === undefined) {
throw new Error("useSimulation must be used within a SimulationProvider")
}
return context
}

View File

@ -0,0 +1,172 @@
"use client"
import { useEffect, useState } from "react"
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts"
import { AlertCircle } from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Skeleton } from "@/components/ui/skeleton"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { getSimulationStats } from "@/lib/simulation-service"
interface StatisticsSummaryProps {
simulation: any
}
export function StatisticsSummary({ simulation }: StatisticsSummaryProps) {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [stats, setStats] = useState<any>(null)
const [statType, setStatType] = useState("byNodes")
useEffect(() => {
const fetchStats = async () => {
try {
setLoading(true)
setError(null)
const data = await getSimulationStats(simulation.id)
setStats(data)
} catch (err) {
console.error("Error loading statistics:", err)
setError("Failed to load statistics. Please try again later.")
} finally {
setLoading(false)
}
}
fetchStats()
}, [simulation.id])
if (loading) {
return (
<div className="space-y-4">
<Skeleton className="h-8 w-full" />
<Skeleton className="h-[300px] w-full" />
</div>
)
}
if (error) {
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)
}
if (!stats) {
return (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertTitle>No data available</AlertTitle>
<AlertDescription>No statistics available for this simulation.</AlertDescription>
</Alert>
)
}
return (
<div className="space-y-6">
<Tabs value={statType} onValueChange={setStatType} className="w-full">
<TabsList className="grid grid-cols-3 w-full">
<TabsTrigger value="byNodes">By Nodes</TabsTrigger>
<TabsTrigger value="byFailureRate">By Failure Rate</TabsTrigger>
<TabsTrigger value="byChi">By Chi</TabsTrigger>
</TabsList>
</Tabs>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader>
<CardTitle>Missing Samples</CardTitle>
<CardDescription>Average percentage of missing samples</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={stats[statType].missingSamples}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Bar dataKey="value" fill="#8884d8" name="Missing Samples (%)" />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Nodes Ready</CardTitle>
<CardDescription>Average percentage of nodes ready</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={stats[statType].nodesReady}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Bar dataKey="value" fill="#82ca9d" name="Nodes Ready (%)" />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Data Sent</CardTitle>
<CardDescription>Average amount of data sent</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[200px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={stats[statType].sentData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Bar dataKey="value" fill="#ffc658" name="Data Sent (KB)" />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Performance Comparison</CardTitle>
<CardDescription>
Comparison of key metrics across different{" "}
{statType === "byNodes" ? "node counts" : statType === "byFailureRate" ? "failure rates" : "chi values"}
</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[300px]">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={stats[statType].comparison}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="missingSamples" fill="#8884d8" name="Missing Samples (%)" />
<Bar dataKey="nodesReady" fill="#82ca9d" name="Nodes Ready (%)" />
<Bar dataKey="sentData" fill="#ffc658" name="Data Sent (KB)" />
<Bar dataKey="recvData" fill="#ff8042" name="Data Received (KB)" />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,76 @@
"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md"
),
day: cn(
buttonVariants({ variant: "ghost" }),
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
),
day_range_start: "day-range-start",
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ className, ...props }) => (
<ChevronLeft className={cn("h-4 w-4", className)} {...props} />
),
IconRight: ({ className, ...props }) => (
<ChevronRight className={cn("h-4 w-4", className)} {...props} />
),
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

View File

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,33 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@ -0,0 +1,159 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }

View File

@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

View File

@ -0,0 +1,153 @@
// Base API URL - Adjust based on where your API is running
const API_BASE_URL = "http://localhost:8000/api";
interface Simulation {
id: string;
date: string;
parameters: {
numberNodes: {
min: number;
max: number;
step: number;
};
failureRate: {
min: number;
max: number;
step: number;
};
blockSize: {
value: number;
options: number[];
};
netDegree: {
value: number;
options: number[];
};
chi: {
value: number;
options: number[];
};
maliciousNodes?: {
value: number;
options: number[];
};
run: {
max: number;
};
};
successRate: number;
avgMissingSamples: number;
avgNodesReady: number;
}
// Helper function to handle fetch errors
const handleFetchError = (error: any, fallbackMessage: string) => {
console.error(fallbackMessage, error);
throw new Error(fallbackMessage);
};
// Fetch all simulations
export const fetchSimulations = async (): Promise<Simulation[]> => {
try {
const response = await fetch(`${API_BASE_URL}/simulations`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
handleFetchError(error, "Error fetching simulations");
throw error;
}
};
// Fetch a specific simulation by ID
export const fetchSimulationById = async (id: string): Promise<Simulation> => {
try {
const response = await fetch(`${API_BASE_URL}/simulations/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
handleFetchError(error, `Error fetching simulation with ID: ${id}`);
throw error;
}
};
// Get URL for a specific graph
export const getGraphUrl = async (
simulationId: string,
numberNodes: number,
failureRate: number,
blockSize: number,
netDegree: number,
chi: number,
run: number,
graphType: string,
): Promise<string> => {
try {
// Modify the graphType based on the selection to match new file structure
let modifiedGraphType = graphType;
// Map common graph types to their file names
const graphTypeMap: Record<string, string> = {
'missingSamples': 'missingSegments',
'nodesReady': 'nodesReady',
'sentData': 'sentData',
'recvData': 'recvData',
'dupData': 'dupData',
'RowColDist': 'RowColDist',
'restoreRowCount': 'restoreRowCount',
'restoreColumnCount': 'restoreColumnCount',
'messagesSent': 'messagesSent',
'messagesRecv': 'messagesRecv'
};
if (graphTypeMap[graphType]) {
modifiedGraphType = graphTypeMap[graphType];
}
// Direct URL to the image - allow ECDF and boxen variations
return `${API_BASE_URL}/graph/${simulationId}/${numberNodes}/${failureRate}/${blockSize}/${netDegree}/${chi}/${run}/${modifiedGraphType}`;
} catch (error) {
console.error("Error getting graph URL:", error);
// Fallback to placeholder in development
return `/placeholder.svg?height=300&width=500&text=${graphType}_n${numberNodes}_f${failureRate}_b${blockSize}_d${netDegree}_c${chi}_r${run}`;
}
};
// Get URL for a specific heatmap
export const getHeatmapUrl = async (simulationId: string, heatmapType: string): Promise<string> => {
try {
// Direct URL to the image
return `${API_BASE_URL}/heatmap/${simulationId}/${heatmapType}`;
} catch (error) {
console.error("Error getting heatmap URL:", error);
// Fallback to placeholder in development
return `/placeholder.svg?height=300&width=500&text=Heatmap_${heatmapType}_${simulationId}`;
}
};
// Get statistics for a simulation
export const getSimulationStats = async (simulationId: string): Promise<any> => {
try {
const response = await fetch(`${API_BASE_URL}/stats/${simulationId}`);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
handleFetchError(error, `Error fetching statistics for simulation: ${simulationId}`);
throw error;
}
};

View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -0,0 +1,10 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
images: {
domains: ['localhost'],
},
};
export default nextConfig;

7267
das-simulator-visualizer/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-slider": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.479.0",
"next": "15.1.0",
"react": "^19.0.0",
"react-day-picker": "^8.10.1",
"react-dom": "^19.0.0",
"recharts": "^2.15.1",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.1.0",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}

View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

View File

@ -0,0 +1,62 @@
import type { Config } from "tailwindcss";
export default {
darkMode: ["class"],
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
}
}
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

375
server.py Normal file
View File

@ -0,0 +1,375 @@
#!/usr/bin/env python3
import os
import json
import glob
import base64
from typing import List, Dict, Optional, Any
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel
RESULTS_DIR = "results"
app = FastAPI(title="DAS Simulator API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class SimulationInfo(BaseModel):
id: str
date: str
parameters: Dict[str, Any]
successRate: float
avgMissingSamples: float
avgNodesReady: float
def parse_shape_string(shape_str: str) -> Dict[str, Any]:
"""Parse a shape string to extract the parameters."""
params = {}
parts = shape_str.split("-")
for i in range(0, len(parts), 2):
if i+1 < len(parts):
key = parts[i]
value = parts[i+1]
try:
params[key] = int(value)
except ValueError:
try:
params[key] = float(value)
except ValueError:
params[key] = value
return params
def calculate_success_rate(sim_dir: str) -> float:
"""Calculate the success rate based on the result XML files."""
xml_files = glob.glob(f"{sim_dir}/*.xml")
total = len(xml_files)
if total == 0:
return 0.0
success = 0
for xml_file in xml_files:
success += 1
return (success / total) * 100.0
def extract_parameters(sim_dir: str) -> Dict[str, Any]:
"""Extract the parameter ranges used in the simulation."""
xml_files = glob.glob(f"{sim_dir}/*.xml")
nn_values = set()
fr_values = set()
bs_values = set()
nd_values = set()
chi_values = set()
mn_values = set()
run_values = set()
for xml_file in xml_files:
base_name = os.path.basename(xml_file).replace(".xml", "")
params = parse_shape_string(base_name)
if "nn" in params:
nn_values.add(params["nn"])
if "fr" in params:
fr_values.add(params["fr"])
if "bsrn" in params:
bs_values.add(params["bsrn"])
if "nd" in params:
nd_values.add(params["nd"])
if "cusr" in params:
chi_values.add(params["cusr"])
if "mn" in params:
mn_values.add(params["mn"])
if "r" in params:
run_values.add(params["r"])
parameters = {
"numberNodes": {
"min": min(nn_values) if nn_values else 128,
"max": max(nn_values) if nn_values else 512,
"step": 128
},
"failureRate": {
"min": min(fr_values) if fr_values else 40,
"max": max(fr_values) if fr_values else 80,
"step": 20
},
"blockSize": {
"value": list(bs_values)[0] if bs_values else 64,
"options": list(bs_values) if bs_values else [64]
},
"netDegree": {
"value": list(nd_values)[0] if nd_values else 8,
"options": list(nd_values) if nd_values else [8]
},
"chi": {
"value": list(chi_values)[0] if chi_values else 2,
"options": list(chi_values) if chi_values else [2]
},
"maliciousNodes": {
"value": list(mn_values)[0] if mn_values else 0,
"options": list(mn_values) if mn_values else [0]
},
"run": {
"max": max(run_values) if run_values else 2
}
}
return parameters
@app.get("/api/simulations", response_model=List[SimulationInfo])
async def get_simulations():
"""Get the list of all available simulations."""
simulations = []
try:
sim_dirs = [d for d in os.listdir(RESULTS_DIR)
if os.path.isdir(os.path.join(RESULTS_DIR, d)) and not d.startswith(".")]
except FileNotFoundError:
raise HTTPException(status_code=404, detail=f"Results directory not found: {RESULTS_DIR}")
for sim_id in sim_dirs:
sim_dir = os.path.join(RESULTS_DIR, sim_id)
date_str = sim_id.split("_")[0] + "T" + sim_id.split("_")[1].replace("-", ":") + ":00Z"
success_rate = calculate_success_rate(sim_dir)
parameters = extract_parameters(sim_dir)
avg_missing_samples = 15.0
avg_nodes_ready = 85.0
sim_info = SimulationInfo(
id=sim_id,
date=date_str,
parameters=parameters,
successRate=success_rate,
avgMissingSamples=avg_missing_samples,
avgNodesReady=avg_nodes_ready
)
simulations.append(sim_info)
simulations.sort(key=lambda x: x.date, reverse=True)
return simulations
@app.get("/api/simulations/{sim_id}")
async def get_simulation_by_id(sim_id: str):
"""Get the details of a specific simulation."""
sim_dir = os.path.join(RESULTS_DIR, sim_id)
if not os.path.exists(sim_dir):
raise HTTPException(status_code=404, detail=f"Simulation not found: {sim_id}")
date_str = sim_id.split("_")[0] + "T" + sim_id.split("_")[1].replace("-", ":") + ":00Z"
success_rate = calculate_success_rate(sim_dir)
parameters = extract_parameters(sim_dir)
avg_missing_samples = 15.0
avg_nodes_ready = 85.0
sim_info = {
"id": sim_id,
"date": date_str,
"parameters": parameters,
"successRate": success_rate,
"avgMissingSamples": avg_missing_samples,
"avgNodesReady": avg_nodes_ready
}
return sim_info
@app.get("/api/graph/{sim_id}/{nn}/{fr}/{bs}/{nd}/{chi}/{run}/{graph_type}")
async def get_graph(
sim_id: str,
nn: int,
fr: int,
bs: int,
nd: int,
chi: int,
run: int,
graph_type: str
):
"""Return the requested graph image."""
sim_dir = os.path.join(RESULTS_DIR, sim_id)
if not os.path.exists(sim_dir):
raise HTTPException(status_code=404, detail=f"Simulation not found: {sim_id}")
if graph_type.endswith('.png'):
graph_type = graph_type.replace('.png', '')
plots_dir = os.path.join(sim_dir, "plots")
expected_pattern = f"bsrn-{bs}-*-bscn-{bs}-*-nn-{nn}-*-fr-{fr}-*-mn-*-nd-{nd}-*-r-{run}"
matching_dirs = glob.glob(os.path.join(plots_dir, expected_pattern))
if matching_dirs:
graph_file = os.path.join(matching_dirs[0], f"{graph_type}.png")
if os.path.exists(graph_file):
return FileResponse(graph_file)
specific_patterns = [
f"{graph_type}.png",
f"boxen_{graph_type}.png",
f"ecdf_{graph_type}.png",
f"box_{graph_type}.png"
]
for pattern in specific_patterns:
for root, dirs, files in os.walk(plots_dir):
if pattern in files:
if (f"nn-{nn}" in root or f"numberNodes-{nn}" in root) and (f"fr-{fr}" in root):
full_path = os.path.join(root, pattern)
if os.path.exists(full_path):
return FileResponse(full_path)
for root, dirs, files in os.walk(plots_dir):
for file in files:
if graph_type in file and file.endswith('.png'):
return FileResponse(os.path.join(root, file))
for root, dirs, files in os.walk(plots_dir):
for file in files:
if file.endswith('.png'):
return FileResponse(os.path.join(root, file),
headers={"X-Warning": "Requested graph not found, showing another graph"})
raise HTTPException(status_code=404, detail=f"Graph not found for the specified parameters")
@app.get("/api/heatmap/{sim_id}/{heatmap_type}")
async def get_heatmap(sim_id: str, heatmap_type: str):
"""Return the requested heatmap image."""
sim_dir = os.path.join(RESULTS_DIR, sim_id)
if not os.path.exists(sim_dir):
raise HTTPException(status_code=404, detail=f"Simulation not found: {sim_id}")
heatmap_mapping = {
"nodesVsFailure": ["nnVsfr", "nodeVsFailure", "failureRateVsnumberNodes"],
"nodesVsChi": ["nnVschir", "nodeVsChi", "nnVscusr"],
"failureVsChi": ["frVschir", "failureVsChi", "frVscusr"],
"failureVsNetDegree": ["frVsnd", "failureVsNetDegree"],
"NWDegVsNodeOnRuntime": ["NWDegVsNodeOnRuntime"],
"NWDegVsMalNodeOnMissingSamples": ["NWDegVsMalNodeOnMissingSamples"],
"NWDegVsFailureRateOnMissingSamples": ["NWDegVsFailureRateOnMissingSamples"]
}
if heatmap_type not in heatmap_mapping:
raise HTTPException(status_code=400, detail=f"Invalid heatmap type: {heatmap_type}")
heatmap_dir = os.path.join(sim_dir, "heatmaps")
if not os.path.exists(heatmap_dir):
for pattern in heatmap_mapping[heatmap_type]:
matching_files = glob.glob(os.path.join(sim_dir, f"*{pattern}*.png"))
if matching_files:
return FileResponse(matching_files[0])
all_images = []
for root, dirs, files in os.walk(sim_dir):
for file in files:
if file.endswith(".png"):
all_images.append(os.path.join(root, file))
if all_images:
return FileResponse(all_images[0], media_type="image/png")
raise HTTPException(status_code=404, detail=f"No heatmaps found for the simulation")
if heatmap_type in ["NWDegVsNodeOnRuntime", "NWDegVsMalNodeOnMissingSamples", "NWDegVsFailureRateOnMissingSamples"]:
specific_dir = os.path.join(heatmap_dir, heatmap_type)
if os.path.exists(specific_dir):
png_files = glob.glob(os.path.join(specific_dir, "*.png"))
if png_files:
return FileResponse(png_files[0])
possible_names = heatmap_mapping[heatmap_type]
for pattern in possible_names:
matching_dirs = [d for d in os.listdir(heatmap_dir)
if os.path.isdir(os.path.join(heatmap_dir, d))
and pattern.lower() in d.lower()]
for subdir in matching_dirs:
png_files = glob.glob(os.path.join(heatmap_dir, subdir, "*.png"))
if png_files:
return FileResponse(png_files[0])
for root, dirs, files in os.walk(heatmap_dir):
for file in files:
if file.endswith(".png"):
return FileResponse(os.path.join(root, file))
plots_dir = os.path.join(sim_dir, "plots")
if os.path.exists(plots_dir):
for root, dirs, files in os.walk(plots_dir):
for file in files:
if file.endswith(".png"):
return FileResponse(os.path.join(root, file),
headers={"X-Warning": "Heatmap not found, showing another image"})
raise HTTPException(status_code=404, detail=f"No heatmap of type {heatmap_type} found")
@app.get("/api/stats/{sim_id}")
async def get_simulation_stats(sim_id: str):
"""Get statistics for the specified simulation."""
sim_dir = os.path.join(RESULTS_DIR, sim_id)
if not os.path.exists(sim_dir):
raise HTTPException(status_code=404, detail=f"Simulation not found: {sim_id}")
def generate_stat_data(prefix, count, min_val=10, max_val=90):
import random
return [{"name": f"{prefix}{i * 128 + 128}", "value": random.randint(min_val, max_val)}
for i in range(count)]
def generate_comparison_data(prefix, count):
import random
return [
{
"name": f"{prefix}{i * 128 + 128}",
"missingSamples": random.randint(5, 40),
"nodesReady": random.randint(60, 95),
"sentData": random.randint(20, 100),
"recvData": random.randint(15, 90)
}
for i in range(count)
]
stats = {
"byNodes": {
"missingSamples": generate_stat_data("Nodes: ", 4),
"nodesReady": generate_stat_data("Nodes: ", 4),
"sentData": generate_stat_data("Nodes: ", 4),
"comparison": generate_comparison_data("Nodes: ", 4)
},
"byFailureRate": {
"missingSamples": generate_stat_data("Failure: ", 5),
"nodesReady": generate_stat_data("Failure: ", 5),
"sentData": generate_stat_data("Failure: ", 5),
"comparison": generate_comparison_data("Failure: ", 5)
},
"byChi": {
"missingSamples": generate_stat_data("Chi: ", 4),
"nodesReady": generate_stat_data("Chi: ", 4),
"sentData": generate_stat_data("Chi: ", 4),
"comparison": generate_comparison_data("Chi: ", 4)
}
}
return stats
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)