feat: creating frontend app and server file

This commit is contained in:
DiegoB1911 2025-03-12 19:35:09 -06:00
parent f36c3c85ba
commit 04010a04d0
No known key found for this signature in database
GPG Key ID: 4F405C71255D3527
44 changed files with 10318 additions and 1 deletions

6
.gitignore vendored
View File

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

BIN
frontend/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

87
frontend/app/globals.css Normal file
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;
}
}

27
frontend/app/layout.tsx Normal file
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>
)
}

22
frontend/app/page.tsx Normal file
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,220 @@
"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"
export default function SimulationDetailPage() {
const params = useParams()
const router = useRouter()
const { getSimulationById, loading } = useSimulation()
const [simulation, setSimulation] = useState<any>(null)
const [selectedParams, setSelectedParams] = useState<any>({
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)
// Set a default simulation or handle the error appropriately
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) => ({
...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>
)
}

21
frontend/components.json Normal file
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,79 @@
// graph-viewer.tsx
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);
// Get the graph URL based on the parameters and graph type
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,91 @@
// heatmap-viewer.tsx
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);
// Get the heatmap URL based on the heatmap type
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,148 @@
// parameter-selector.tsx
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,286 @@
"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}`)
}
// Función auxiliar para formatear fechas de manera segura
const formatDate = (dateString: string) => {
try {
// Corregir formato incorrecto con :00Z al final
if (dateString.includes('T') && dateString.endsWith(':00Z')) {
dateString = dateString.replace(':00Z', 'Z');
}
// Intenta parsear la fecha desde ISO
const date = parseISO(dateString);
return format(date, "PPP");
} catch (error) {
console.error("Error formatting date:", dateString, error);
// Si falla, intenta otro enfoque: crear una fecha a partir del ID de simulación
try {
if (dateString.includes("_")) {
// Si la fecha es parte del ID (como en "2025-02-11_00-11-23_825")
const parts = dateString.split("_");
if (parts.length >= 2) {
const datePart = parts[0];
const timePart = parts[1].replace(/-/g, ":");
return `${datePart} ${timePart}`;
}
}
// Si todo lo anterior falla, intentar crear una fecha simple
return new Date(dateString).toLocaleDateString();
} catch (e) {
// Si todo falla, devuelve un marcador de posición
return "Date unavailable";
}
}
};
const filteredSimulations = simulations.filter((sim) => {
// Search filter
if (searchTerm && !sim.id.toLowerCase().includes(searchTerm.toLowerCase())) {
return false
}
// Date filter
if (dateRange.from) {
try {
const simDate = new Date(sim.date);
if (simDate < dateRange.from) return false;
} catch (e) {
// Si la fecha no se puede parsear, mantenemos el elemento
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) {
// Si la fecha no se puede parsear, mantenemos el elemento
console.warn("Could not parse date for filtering:", sim.date);
}
}
// Success rate filter
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,104 @@
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
}
// Función auxiliar para formatear fechas de manera segura
const formatDate = (dateString: string) => {
try {
// Corregir formato incorrecto con :00Z al final
if (dateString && dateString.includes('T') && dateString.endsWith(':00Z')) {
dateString = dateString.replace(':00Z', 'Z');
}
// Intenta parsear la fecha desde ISO
const date = parseISO(dateString);
return format(date, "PPP 'at' p");
} catch (error) {
console.error("Error formatting date:", dateString, error);
// Si falla, intenta otro enfoque: crear una fecha a partir del ID de simulación
try {
if (dateString && dateString.includes("_")) {
// Si la fecha es parte del ID (como en "2025-02-11_00-11-23_825")
const parts = dateString.split("_");
if (parts.length >= 2) {
const datePart = parts[0];
const timePart = parts[1].replace(/-/g, ":");
return `${datePart} at ${timePart}`;
}
}
// Si todo lo anterior falla, usar el ID
return dateString || "Date unavailable";
} catch (e) {
// Si todo falla, devuelve un marcador de posición
return "Date unavailable";
}
}
};
export function SimulationMetadata({ simulation }: SimulationMetadataProps) {
// Manejar el caso donde simulation podría ser null o undefined
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,105 @@
"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 {
// First check if the simulation is in our cached state
const cachedSimulation = simulations.find((sim) => sim.id === id)
if (cachedSimulation) return cachedSimulation
// If not in cache, we'll return a promise that fetches it
// This is wrapped in a try/catch to handle any errors
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 a default simulation object instead of throwing
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,173 @@
"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)
// Get the statistics for the simulation
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,292 @@
// Base API URL - Ajustar según donde se esté ejecutando tu API
const API_BASE_URL = "http://localhost:8000/api";
// Interfaces para tipado
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;
}
// Función auxiliar para manejar errores de fetch
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");
// En modo desarrollo, podríamos devolver datos de prueba como fallback
if (process.env.NODE_ENV === "development") {
console.warn("Using mock data as fallback in development mode");
return generateMockSimulations();
}
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}`);
// En modo desarrollo, podríamos devolver datos de prueba como fallback
if (process.env.NODE_ENV === "development") {
console.warn("Using mock data as fallback in development mode");
return getMockSimulation(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];
}
// URL directa a la imagen - 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 {
// URL directa a la imagen
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}`);
// En modo desarrollo, podríamos devolver datos de prueba como fallback
if (process.env.NODE_ENV === "development") {
console.warn("Using mock data as fallback in development mode");
return getMockStats();
}
throw error;
}
};
// ----------------------
// Funciones auxiliares para datos de prueba (fallback)
// ----------------------
const randomInRange = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
// Generate mock simulation data
const generateMockSimulations = (): Simulation[] => {
const simulations = [];
// Generate 10 mock simulations
for (let i = 0; i < 10; i++) {
const date = new Date();
date.setDate(date.getDate() - i * 3); // Space them out by 3 days
const id = `${date.toISOString().split("T")[0].replace(/-/g, "")}_${randomInRange(10, 99)}_DAS`;
simulations.push({
id,
date: 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],
},
maliciousNodes: {
value: 0,
options: [0, 20, 40, 60],
},
run: {
max: 2,
},
},
successRate: randomInRange(30, 95),
avgMissingSamples: randomInRange(5, 40),
avgNodesReady: randomInRange(60, 95),
});
}
return simulations;
};
// Get mock simulation by ID
const getMockSimulation = (id: string): Simulation => {
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] },
maliciousNodes: { value: 0, options: [0, 20, 40, 60] },
run: { max: 2 },
},
successRate: 75,
avgMissingSamples: 20,
avgNodesReady: 80,
};
};
// Get mock statistics
const getMockStats = () => {
const generateStatData = (prefix: string, count: number) => {
return Array.from({ length: count }, (_, i) => ({
name: `${prefix}${i === 0 ? 128 : i === 1 ? 256 : i === 2 ? 384 : 512}`,
value: randomInRange(10, 90),
}));
};
const generateComparisonData = (prefix: string, count: number) => {
return Array.from({ length: count }, (_, i) => ({
name: `${prefix}${i === 0 ? 128 : i === 1 ? 256 : i === 2 ? 384 : 512}`,
missingSamples: randomInRange(5, 40),
nodesReady: randomInRange(60, 95),
sentData: randomInRange(20, 100),
recvData: randomInRange(15, 90),
}));
};
return {
byNodes: {
missingSamples: generateStatData("Nodes: ", 4),
nodesReady: generateStatData("Nodes: ", 4),
sentData: generateStatData("Nodes: ", 4),
comparison: generateComparisonData("Nodes: ", 4),
},
byFailureRate: {
missingSamples: generateStatData("Failure: ", 5),
nodesReady: generateStatData("Failure: ", 5),
sentData: generateStatData("Failure: ", 5),
comparison: generateComparisonData("Failure: ", 5),
},
byChi: {
missingSamples: generateStatData("Chi: ", 4),
nodesReady: generateStatData("Chi: ", 4),
sentData: generateStatData("Chi: ", 4),
comparison: generateComparisonData("Chi: ", 4),
},
};
};

6
frontend/lib/utils.ts Normal file
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))
}

5
frontend/next-env.d.ts vendored Normal file
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.

10
frontend/next.config.ts Normal file
View File

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

7267
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
frontend/package.json Normal file
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;

1
frontend/public/file.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
frontend/public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

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;

27
frontend/tsconfig.json Normal file
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"]
}

415
server.py Normal file
View File

@ -0,0 +1,415 @@
#!/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
# Ruta base donde se encuentran los resultados de las simulaciones
RESULTS_DIR = "results"
app = FastAPI(title="DAS Simulator API")
# Configurar CORS para permitir peticiones desde el frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # En producción, restringe esto a la URL de tu frontend
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]:
"""Parsea un string de shape para extraer los parámetros."""
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]
# Intentar convertir a entero si es posible
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:
"""Calcula el porcentaje de éxito basado en los XML de resultados."""
xml_files = glob.glob(f"{sim_dir}/*.xml")
total = len(xml_files)
if total == 0:
return 0.0
# Contar cuántos blocks se marcaron como disponibles
success = 0
for xml_file in xml_files:
# Aquí podrías parsear el XML para buscar blockAvailable=1
# Por simplicidad, asumimos un éxito del 70%
success += 1
return (success / total) * 100.0
def extract_parameters(sim_dir: str) -> Dict[str, Any]:
"""Extrae los rangos de parámetros usados en la simulación."""
xml_files = glob.glob(f"{sim_dir}/*.xml")
# Extraer valores únicos para cada parámetro
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"])
# Crear el objeto de parámetros con min, max, step
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():
"""Obtiene la lista de todas las simulaciones disponibles."""
simulations = []
# Listar todos los directorios de simulación
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"No se encontró el directorio de resultados: {RESULTS_DIR}")
for sim_id in sim_dirs:
sim_dir = os.path.join(RESULTS_DIR, sim_id)
# Extraer la fecha del formato de ID (YYYY-MM-DD_HH-MM-SS_XXX)
date_str = sim_id.split("_")[0] + "T" + sim_id.split("_")[1].replace("-", ":") + ":00Z"
# Calcular métricas y extraer parámetros
success_rate = calculate_success_rate(sim_dir)
parameters = extract_parameters(sim_dir)
# Calcular métricas adicionales
avg_missing_samples = 15.0 # Valor simulado, deberías calcularlo realmente
avg_nodes_ready = 85.0 # Valor simulado, deberías calcularlo realmente
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)
# Ordenar por fecha, más reciente primero
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):
"""Obtiene los detalles de una simulación específica."""
sim_dir = os.path.join(RESULTS_DIR, sim_id)
if not os.path.exists(sim_dir):
raise HTTPException(status_code=404, detail=f"Simulación no encontrada: {sim_id}")
# Extraer la fecha del formato de ID
date_str = sim_id.split("_")[0] + "T" + sim_id.split("_")[1].replace("-", ":") + ":00Z"
# Calcular métricas y extraer parámetros
success_rate = calculate_success_rate(sim_dir)
parameters = extract_parameters(sim_dir)
# Calcular métricas adicionales
avg_missing_samples = 15.0 # Valor simulado
avg_nodes_ready = 85.0 # Valor simulado
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
):
"""Devuelve la imagen del gráfico solicitado."""
sim_dir = os.path.join(RESULTS_DIR, sim_id)
if not os.path.exists(sim_dir):
raise HTTPException(status_code=404, detail=f"Simulación no encontrada: {sim_id}")
# Limpiar el tipo de gráfico (quitar la extensión si la tiene)
if graph_type.endswith('.png'):
graph_type = graph_type.replace('.png', '')
# Intentar encontrar el directorio que corresponde a estos parámetros
plots_dir = os.path.join(sim_dir, "plots")
# Búsqueda directa primero - con los nuevos nombres de parámetro
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:
# Si encontramos un directorio que coincide, buscar el archivo de gráfico
graph_file = os.path.join(matching_dirs[0], f"{graph_type}.png")
if os.path.exists(graph_file):
return FileResponse(graph_file)
# Buscar gráficos específicos con diferentes patrones de nombre
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:
# Verificar si los parámetros clave están en la ruta
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)
# Si aún no encontramos, buscar en todos los subdirectorios recursivamente
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))
# Si realmente no encontramos nada, devolver cualquier imagen como respaldo
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"Gráfico no encontrado para los parámetros especificados")
@app.get("/api/heatmap/{sim_id}/{heatmap_type}")
async def get_heatmap(sim_id: str, heatmap_type: str):
"""Devuelve la imagen del heatmap solicitado."""
sim_dir = os.path.join(RESULTS_DIR, sim_id)
if not os.path.exists(sim_dir):
raise HTTPException(status_code=404, detail=f"Simulación no encontrada: {sim_id}")
# Mapear el tipo de heatmap a los nombres de archivo y carpetas
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"Tipo de heatmap no válido: {heatmap_type}")
# Buscar archivos de heatmap en el directorio correspondiente
heatmap_dir = os.path.join(sim_dir, "heatmaps")
if not os.path.exists(heatmap_dir):
# Si no hay directorio de heatmaps, buscar en la raíz de la simulación
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])
# Si no hay heatmaps, buscar cualquier imagen
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 se encontraron heatmaps para la simulación")
# Buscar primero en subdirectorios específicos para los nuevos tipos de heatmap
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])
# Buscar con todas las variantes de nombres posibles
possible_names = heatmap_mapping[heatmap_type]
# Primero buscar en subdirectorios específicos
for pattern in possible_names:
# Buscar directorios que contengan el patrón
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:
# Buscar archivos PNG en este directorio
png_files = glob.glob(os.path.join(heatmap_dir, subdir, "*.png"))
if png_files:
return FileResponse(png_files[0])
# Si no encontramos nada, buscar cualquier PNG en heatmaps
for root, dirs, files in os.walk(heatmap_dir):
for file in files:
if file.endswith(".png"):
return FileResponse(os.path.join(root, file))
# Si no encontramos ningún heatmap, verificar si hay alguna imagen en plots
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 no encontrado, mostrando otra imagen"})
raise HTTPException(status_code=404, detail=f"No se encontró heatmap del tipo {heatmap_type}")
@app.get("/api/stats/{sim_id}")
async def get_simulation_stats(sim_id: str):
"""Obtiene estadísticas para la simulación especificada."""
sim_dir = os.path.join(RESULTS_DIR, sim_id)
if not os.path.exists(sim_dir):
raise HTTPException(status_code=404, detail=f"Simulación no encontrada: {sim_id}")
# Aquí deberías procesar los archivos XML y generar estadísticas reales
# Por ahora, retornaremos datos de muestra similares a los datos de prueba del frontend
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)
]
# Estos datos deberían generarse procesando los resultados reales
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)