mirror of
https://github.com/logos-storage/das-research.git
synced 2026-01-04 22:23:08 +00:00
feat: creating frontend app and server file
This commit is contained in:
parent
f36c3c85ba
commit
04010a04d0
6
.gitignore
vendored
6
.gitignore
vendored
@ -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
BIN
frontend/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
87
frontend/app/globals.css
Normal file
87
frontend/app/globals.css
Normal 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
27
frontend/app/layout.tsx
Normal 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
22
frontend/app/page.tsx
Normal 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">© {new Date().getFullYear()} DAS Simulator Visualizer</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
220
frontend/app/simulations/[id]/page.tsx
Normal file
220
frontend/app/simulations/[id]/page.tsx
Normal 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
21
frontend/components.json
Normal 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"
|
||||
}
|
||||
79
frontend/components/graph-viewer.tsx
Normal file
79
frontend/components/graph-viewer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
91
frontend/components/heatmap-viewer.tsx
Normal file
91
frontend/components/heatmap-viewer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
148
frontend/components/parameter-selector.tsx
Normal file
148
frontend/components/parameter-selector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
286
frontend/components/simulation-list.tsx
Normal file
286
frontend/components/simulation-list.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
104
frontend/components/simulation-metadata.tsx
Normal file
104
frontend/components/simulation-metadata.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
105
frontend/components/simulation-provider.tsx
Normal file
105
frontend/components/simulation-provider.tsx
Normal 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
|
||||
}
|
||||
|
||||
173
frontend/components/statistics-summary.tsx
Normal file
173
frontend/components/statistics-summary.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
59
frontend/components/ui/alert.tsx
Normal file
59
frontend/components/ui/alert.tsx
Normal 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 }
|
||||
36
frontend/components/ui/badge.tsx
Normal file
36
frontend/components/ui/badge.tsx
Normal 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 }
|
||||
57
frontend/components/ui/button.tsx
Normal file
57
frontend/components/ui/button.tsx
Normal 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 }
|
||||
76
frontend/components/ui/calendar.tsx
Normal file
76
frontend/components/ui/calendar.tsx
Normal 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 }
|
||||
76
frontend/components/ui/card.tsx
Normal file
76
frontend/components/ui/card.tsx
Normal 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 }
|
||||
30
frontend/components/ui/checkbox.tsx
Normal file
30
frontend/components/ui/checkbox.tsx
Normal 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 }
|
||||
122
frontend/components/ui/dialog.tsx
Normal file
122
frontend/components/ui/dialog.tsx
Normal 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,
|
||||
}
|
||||
22
frontend/components/ui/input.tsx
Normal file
22
frontend/components/ui/input.tsx
Normal 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 }
|
||||
26
frontend/components/ui/label.tsx
Normal file
26
frontend/components/ui/label.tsx
Normal 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 }
|
||||
33
frontend/components/ui/popover.tsx
Normal file
33
frontend/components/ui/popover.tsx
Normal 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 }
|
||||
159
frontend/components/ui/select.tsx
Normal file
159
frontend/components/ui/select.tsx
Normal 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,
|
||||
}
|
||||
15
frontend/components/ui/skeleton.tsx
Normal file
15
frontend/components/ui/skeleton.tsx
Normal 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 }
|
||||
28
frontend/components/ui/slider.tsx
Normal file
28
frontend/components/ui/slider.tsx
Normal 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 }
|
||||
55
frontend/components/ui/tabs.tsx
Normal file
55
frontend/components/ui/tabs.tsx
Normal 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 }
|
||||
16
frontend/eslint.config.mjs
Normal file
16
frontend/eslint.config.mjs
Normal 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;
|
||||
292
frontend/lib/simulation-service.ts
Normal file
292
frontend/lib/simulation-service.ts
Normal 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
6
frontend/lib/utils.ts
Normal 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
5
frontend/next-env.d.ts
vendored
Normal 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
10
frontend/next.config.ts
Normal 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
7267
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
frontend/package.json
Normal file
43
frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
8
frontend/postcss.config.mjs
Normal file
8
frontend/postcss.config.mjs
Normal 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
1
frontend/public/file.svg
Normal 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 |
1
frontend/public/globe.svg
Normal file
1
frontend/public/globe.svg
Normal 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
1
frontend/public/next.svg
Normal 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 |
1
frontend/public/vercel.svg
Normal file
1
frontend/public/vercel.svg
Normal 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 |
1
frontend/public/window.svg
Normal file
1
frontend/public/window.svg
Normal 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 |
62
frontend/tailwind.config.ts
Normal file
62
frontend/tailwind.config.ts
Normal 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
27
frontend/tsconfig.json
Normal 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
415
server.py
Normal 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)
|
||||
Loading…
x
Reference in New Issue
Block a user