Spaces:
Configuration error
Configuration error
Commit ·
32f20b5
1
Parent(s): 09fcbfb
added frontend and stimulation
Browse files- frontend/app/api/telemetry/route.ts +22 -0
- frontend/app/favicon.ico +0 -0
- frontend/app/globals.css +16 -76
- frontend/app/page.tsx +318 -451
- frontend/components/simulation/ClusterSimulation.tsx +209 -332
- frontend/components/ui/FlipWords.tsx +4 -21
- frontend/components/ui/FocusCards.tsx +1 -4
- frontend/components/ui/Terminal.tsx +16 -22
- frontend/components/ui/TextGenerateEffect.tsx +3 -17
- frontend/lib/telemetry.ts +459 -0
- frontend/lib/useSimulationSocket.ts +106 -0
- frontend/package-lock.json +531 -3
- frontend/package.json +2 -1
- server/app.py +4 -21
frontend/app/api/telemetry/route.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from "next/server";
|
| 2 |
+
|
| 3 |
+
import { getTelemetrySnapshots } from "@/lib/telemetry";
|
| 4 |
+
|
| 5 |
+
export const runtime = "nodejs";
|
| 6 |
+
export const dynamic = "force-dynamic";
|
| 7 |
+
|
| 8 |
+
export async function GET() {
|
| 9 |
+
try {
|
| 10 |
+
const snapshots = await getTelemetrySnapshots();
|
| 11 |
+
return NextResponse.json(snapshots);
|
| 12 |
+
} catch (error) {
|
| 13 |
+
const message = error instanceof Error ? error.message : "Unknown telemetry error";
|
| 14 |
+
return NextResponse.json(
|
| 15 |
+
{
|
| 16 |
+
error: "Failed to load telemetry",
|
| 17 |
+
details: message,
|
| 18 |
+
},
|
| 19 |
+
{ status: 500 }
|
| 20 |
+
);
|
| 21 |
+
}
|
| 22 |
+
}
|
frontend/app/favicon.ico
CHANGED
|
|
|
|
frontend/app/globals.css
CHANGED
|
@@ -14,95 +14,35 @@
|
|
| 14 |
|
| 15 |
html {
|
| 16 |
scroll-behavior: smooth;
|
| 17 |
-
|
| 18 |
-
scrollbar-
|
| 19 |
-
-ms-overflow-style: none;
|
| 20 |
}
|
| 21 |
|
| 22 |
body {
|
| 23 |
-
background:
|
| 24 |
-
radial-gradient(circle at 16% -8%, rgba(251, 146, 60, 0.12), transparent 34%),
|
| 25 |
-
radial-gradient(circle at 84% 6%, rgba(251, 191, 36, 0.08), transparent 32%),
|
| 26 |
-
var(--background);
|
| 27 |
color: var(--foreground);
|
| 28 |
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
|
| 29 |
-
scrollbar-width: none;
|
| 30 |
-
-ms-overflow-style: none;
|
| 31 |
}
|
| 32 |
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
height: 0;
|
| 37 |
-
display: none;
|
| 38 |
}
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
|
|
|
| 42 |
}
|
| 43 |
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
max-width: 1100px;
|
| 47 |
-
padding: 0 40px;
|
| 48 |
}
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
.section-inner {
|
| 56 |
-
padding: 0 24px;
|
| 57 |
-
}
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
.hero-nav-scroll {
|
| 61 |
-
-webkit-mask-image: linear-gradient(to right, transparent 0, black 18px, black calc(100% - 18px), transparent 100%);
|
| 62 |
-
mask-image: linear-gradient(to right, transparent 0, black 18px, black calc(100% - 18px), transparent 100%);
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
@keyframes heroBeamDrift {
|
| 66 |
-
0% {
|
| 67 |
-
transform: translateY(-6%);
|
| 68 |
-
opacity: 0.05;
|
| 69 |
-
}
|
| 70 |
-
20% {
|
| 71 |
-
opacity: 0.2;
|
| 72 |
-
}
|
| 73 |
-
54% {
|
| 74 |
-
opacity: 0.35;
|
| 75 |
-
}
|
| 76 |
-
82% {
|
| 77 |
-
opacity: 0.14;
|
| 78 |
-
}
|
| 79 |
-
100% {
|
| 80 |
-
transform: translateY(18%);
|
| 81 |
-
opacity: 0;
|
| 82 |
-
}
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
@keyframes heroSparklePulse {
|
| 86 |
-
0%,
|
| 87 |
-
100% {
|
| 88 |
-
opacity: 0.05;
|
| 89 |
-
transform: scale(0.85);
|
| 90 |
-
}
|
| 91 |
-
40% {
|
| 92 |
-
opacity: 0.1;
|
| 93 |
-
}
|
| 94 |
-
65% {
|
| 95 |
-
opacity: 0.48;
|
| 96 |
-
transform: scale(1);
|
| 97 |
-
}
|
| 98 |
-
82% {
|
| 99 |
-
opacity: 0.2;
|
| 100 |
-
transform: scale(0.92);
|
| 101 |
-
}
|
| 102 |
}
|
| 103 |
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
scroll-behavior: auto;
|
| 107 |
-
}
|
| 108 |
}
|
|
|
|
| 14 |
|
| 15 |
html {
|
| 16 |
scroll-behavior: smooth;
|
| 17 |
+
scrollbar-width: thin;
|
| 18 |
+
scrollbar-color: #3f3f46 transparent;
|
|
|
|
| 19 |
}
|
| 20 |
|
| 21 |
body {
|
| 22 |
+
background: var(--background);
|
|
|
|
|
|
|
|
|
|
| 23 |
color: var(--foreground);
|
| 24 |
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
|
|
|
|
|
|
|
| 25 |
}
|
| 26 |
|
| 27 |
+
* {
|
| 28 |
+
scrollbar-width: thin;
|
| 29 |
+
scrollbar-color: #3f3f46 transparent;
|
|
|
|
|
|
|
| 30 |
}
|
| 31 |
|
| 32 |
+
*::-webkit-scrollbar {
|
| 33 |
+
width: 8px;
|
| 34 |
+
height: 8px;
|
| 35 |
}
|
| 36 |
|
| 37 |
+
*::-webkit-scrollbar-track {
|
| 38 |
+
background: transparent;
|
|
|
|
|
|
|
| 39 |
}
|
| 40 |
|
| 41 |
+
*::-webkit-scrollbar-thumb {
|
| 42 |
+
background-color: #3f3f46;
|
| 43 |
+
border-radius: 9999px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
+
*::-webkit-scrollbar-thumb:hover {
|
| 47 |
+
background-color: #52525b;
|
|
|
|
|
|
|
| 48 |
}
|
frontend/app/page.tsx
CHANGED
|
@@ -1,11 +1,12 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
-
import Image from "next/image";
|
| 4 |
import { useEffect, useRef, useState } from "react";
|
| 5 |
import { FlipWords } from "@/components/ui/FlipWords";
|
|
|
|
|
|
|
| 6 |
import { TextGenerateEffect } from "@/components/ui/TextGenerateEffect";
|
| 7 |
import { ClusterSimulation } from "@/components/simulation/ClusterSimulation";
|
| 8 |
-
import type { DockItem } from "@/components/ui/types";
|
| 9 |
|
| 10 |
type TelemetrySnapshot = {
|
| 11 |
step: number;
|
|
@@ -21,7 +22,6 @@ type TaskCard = {
|
|
| 21 |
difficulty: "Easy" | "Medium" | "Hard" | "Expert";
|
| 22 |
description: string;
|
| 23 |
score: number;
|
| 24 |
-
grading: string;
|
| 25 |
};
|
| 26 |
|
| 27 |
type MetricFigure = {
|
|
@@ -30,29 +30,13 @@ type MetricFigure = {
|
|
| 30 |
caption: string;
|
| 31 |
};
|
| 32 |
|
| 33 |
-
|
| 34 |
-
title: string;
|
| 35 |
-
detail: string;
|
| 36 |
-
};
|
| 37 |
-
|
| 38 |
-
type FeatureRow = {
|
| 39 |
-
title: string;
|
| 40 |
-
descriptor: string;
|
| 41 |
-
dotClassName: string;
|
| 42 |
-
};
|
| 43 |
-
|
| 44 |
-
type RewardTerm = {
|
| 45 |
-
formula: string;
|
| 46 |
-
label: string;
|
| 47 |
-
weight: number;
|
| 48 |
-
tone: string;
|
| 49 |
-
};
|
| 50 |
|
| 51 |
const HERO_FLIP_WORDS = ["adaptive", "resilient", "autonomous", "modern"];
|
| 52 |
|
| 53 |
-
const
|
| 54 |
{
|
| 55 |
-
title: "
|
| 56 |
href: "#about",
|
| 57 |
icon: ({ className }) => (
|
| 58 |
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="1.8">
|
|
@@ -61,17 +45,6 @@ const NAV_ITEMS: DockItem[] = [
|
|
| 61 |
</svg>
|
| 62 |
),
|
| 63 |
},
|
| 64 |
-
{
|
| 65 |
-
title: "Metrics",
|
| 66 |
-
href: "#metrics",
|
| 67 |
-
icon: ({ className }) => (
|
| 68 |
-
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="1.8">
|
| 69 |
-
<path d="M5 19V9" />
|
| 70 |
-
<path d="M12 19V5" />
|
| 71 |
-
<path d="M19 19v-7" />
|
| 72 |
-
</svg>
|
| 73 |
-
),
|
| 74 |
-
},
|
| 75 |
{
|
| 76 |
title: "Simulation",
|
| 77 |
href: "#simulation",
|
|
@@ -101,8 +74,8 @@ const NAV_ITEMS: DockItem[] = [
|
|
| 101 |
),
|
| 102 |
},
|
| 103 |
{
|
| 104 |
-
title: "
|
| 105 |
-
href: "#
|
| 106 |
icon: ({ className }) => (
|
| 107 |
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="1.8">
|
| 108 |
<rect x="3.5" y="3.5" width="17" height="17" rx="2.5" />
|
|
@@ -112,7 +85,7 @@ const NAV_ITEMS: DockItem[] = [
|
|
| 112 |
},
|
| 113 |
{
|
| 114 |
title: "Try It",
|
| 115 |
-
href: "#try
|
| 116 |
icon: ({ className }) => (
|
| 117 |
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="1.8">
|
| 118 |
<path d="m5 19 14-7L5 5v5l8 2-8 2z" />
|
|
@@ -121,103 +94,80 @@ const NAV_ITEMS: DockItem[] = [
|
|
| 121 |
},
|
| 122 |
];
|
| 123 |
|
| 124 |
-
const
|
| 125 |
{
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
| 129 |
},
|
| 130 |
{
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
| 134 |
},
|
| 135 |
{
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
| 139 |
},
|
| 140 |
{
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
| 144 |
},
|
| 145 |
];
|
| 146 |
|
| 147 |
-
const
|
| 148 |
{
|
| 149 |
-
title: "
|
| 150 |
-
|
| 151 |
-
dotClassName: "bg-amber-400",
|
| 152 |
},
|
| 153 |
{
|
| 154 |
-
title: "
|
| 155 |
-
|
| 156 |
-
dotClassName: "bg-sky-400",
|
| 157 |
},
|
| 158 |
{
|
| 159 |
-
title: "
|
| 160 |
-
|
| 161 |
-
dotClassName: "bg-white",
|
| 162 |
-
},
|
| 163 |
-
{
|
| 164 |
-
title: "Deterministic Failure",
|
| 165 |
-
descriptor: "90% CPU for 3 steps",
|
| 166 |
-
dotClassName: "bg-red-400",
|
| 167 |
-
},
|
| 168 |
-
];
|
| 169 |
-
|
| 170 |
-
const TASKS: TaskCard[] = [
|
| 171 |
-
{
|
| 172 |
-
name: "Traffic Spike Recovery",
|
| 173 |
-
difficulty: "Easy",
|
| 174 |
-
description: "",
|
| 175 |
-
score: 0.09,
|
| 176 |
-
grading: "",
|
| 177 |
},
|
| 178 |
{
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
description: "",
|
| 182 |
-
score: 0.05,
|
| 183 |
-
grading: "",
|
| 184 |
},
|
| 185 |
{
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
description: "",
|
| 189 |
-
score: 0.31,
|
| 190 |
-
grading: "",
|
| 191 |
},
|
| 192 |
{
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
description: "",
|
| 196 |
-
score: 0,
|
| 197 |
-
grading: "",
|
| 198 |
},
|
| 199 |
];
|
| 200 |
|
| 201 |
-
const
|
| 202 |
-
{
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
{ formula: "+0.50 × cascade_prevented_bonus", label: "prevention bonus", weight: 0.5, tone: "bg-emerald-300" },
|
| 207 |
-
];
|
| 208 |
-
|
| 209 |
-
const QUICKSTART_STEPS = [
|
| 210 |
{
|
| 211 |
-
|
| 212 |
-
|
| 213 |
},
|
| 214 |
{
|
| 215 |
-
|
| 216 |
-
|
| 217 |
},
|
| 218 |
{
|
| 219 |
-
|
| 220 |
-
|
| 221 |
},
|
| 222 |
];
|
| 223 |
|
|
@@ -225,22 +175,22 @@ const METRIC_FIGURES: MetricFigure[] = [
|
|
| 225 |
{
|
| 226 |
src: "/metrics/fig1_vanishing_gradient_fix.png",
|
| 227 |
title: "Fig 1: Vanishing Gradient Fix",
|
| 228 |
-
caption: "
|
| 229 |
},
|
| 230 |
{
|
| 231 |
src: "/metrics/fig2_cascade_exploit_fix.png",
|
| 232 |
title: "Fig 2: Cascade Exploit Fix",
|
| 233 |
-
caption: "
|
| 234 |
},
|
| 235 |
{
|
| 236 |
src: "/metrics/fig3_cost_latency_coupling.png",
|
| 237 |
title: "Fig 3: Cost-Latency Coupling",
|
| 238 |
-
caption: "
|
| 239 |
},
|
| 240 |
{
|
| 241 |
src: "/metrics/fig4_curiosity_annealing.png",
|
| 242 |
title: "Fig 4: Curiosity Annealing",
|
| 243 |
-
caption: "
|
| 244 |
},
|
| 245 |
];
|
| 246 |
|
|
@@ -284,18 +234,18 @@ function RevealSection({
|
|
| 284 |
<section
|
| 285 |
id={id}
|
| 286 |
ref={ref}
|
| 287 |
-
className={`
|
| 288 |
visible ? "translate-y-0 opacity-100" : "translate-y-4 opacity-0"
|
| 289 |
} ${className ?? ""}`}
|
| 290 |
>
|
| 291 |
-
|
| 292 |
</section>
|
| 293 |
);
|
| 294 |
}
|
| 295 |
|
| 296 |
function SectionDivider({ label }: { label: string }) {
|
| 297 |
return (
|
| 298 |
-
<div className="my-
|
| 299 |
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-4">
|
| 300 |
<div className="h-px bg-gradient-to-r from-transparent via-zinc-800 to-zinc-700/50" />
|
| 301 |
<span className="rounded-full border border-zinc-800 bg-zinc-950 px-3 py-1 font-mono text-[11px] tracking-[0.16em] text-zinc-500">
|
|
@@ -309,20 +259,20 @@ function SectionDivider({ label }: { label: string }) {
|
|
| 309 |
|
| 310 |
function DifficultyBadge({ difficulty }: { difficulty: TaskCard["difficulty"] }) {
|
| 311 |
const tone = {
|
| 312 |
-
Easy: "bg-
|
| 313 |
-
Medium: "bg-
|
| 314 |
-
Hard: "bg-
|
| 315 |
-
Expert: "bg-
|
| 316 |
}[difficulty];
|
| 317 |
|
| 318 |
-
return <span className={`rounded-full px-2.5 py-1 text-xs font-mono ${tone}`}>{difficulty}</span>;
|
| 319 |
}
|
| 320 |
|
| 321 |
function Spotlight() {
|
| 322 |
return (
|
| 323 |
-
<div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10 overflow-
|
| 324 |
<div className="absolute left-1/2 top-[-12rem] h-[28rem] w-[28rem] -translate-x-1/2 rounded-full bg-[radial-gradient(circle,rgba(251,146,60,0.28),transparent_62%)]" />
|
| 325 |
-
<div className="absolute right-[-
|
| 326 |
<div className="absolute bottom-[-12rem] left-[-10rem] h-[24rem] w-[24rem] rounded-full bg-[radial-gradient(circle,rgba(249,115,22,0.18),transparent_65%)]" />
|
| 327 |
</div>
|
| 328 |
);
|
|
@@ -330,7 +280,7 @@ function Spotlight() {
|
|
| 330 |
|
| 331 |
function HeroWordmark() {
|
| 332 |
return (
|
| 333 |
-
<div className="mt-5 flex flex-wrap items-end gap-x-3 gap-y-2 text-[
|
| 334 |
<span className="bg-gradient-to-b from-white via-orange-100 to-pink-300 bg-clip-text text-transparent [text-shadow:0_0_26px_rgba(251,146,60,0.22)]">
|
| 335 |
DIME
|
| 336 |
</span>
|
|
@@ -345,188 +295,97 @@ function AnimatedHeading({ words, className }: { words: string; className?: stri
|
|
| 345 |
return (
|
| 346 |
<TextGenerateEffect
|
| 347 |
words={words}
|
| 348 |
-
className={`mt-4 text-
|
| 349 |
-
className ?? ""
|
| 350 |
-
}`}
|
| 351 |
duration={900}
|
| 352 |
filter={false}
|
| 353 |
/>
|
| 354 |
);
|
| 355 |
}
|
| 356 |
|
| 357 |
-
function
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
}
|
| 382 |
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
<article
|
| 388 |
-
key={card.title}
|
| 389 |
-
className="group h-full rounded-2xl border border-zinc-800/85 bg-gradient-to-b from-zinc-950 to-zinc-950/70 p-4 shadow-[0_0_30px_rgba(251,146,60,0.04)] transition-all duration-300 hover:-translate-y-1 hover:border-zinc-700 hover:shadow-[0_18px_40px_rgba(0,0,0,0.35)] sm:p-5"
|
| 390 |
-
>
|
| 391 |
-
<div className="flex h-full flex-col gap-2.5">
|
| 392 |
-
<p className="font-mono text-[11px] tracking-[0.16em] text-zinc-500">{card.title}</p>
|
| 393 |
-
<p className="text-sm leading-5 text-zinc-300">{card.detail}</p>
|
| 394 |
-
</div>
|
| 395 |
-
</article>
|
| 396 |
-
))}
|
| 397 |
-
</div>
|
| 398 |
-
);
|
| 399 |
-
}
|
| 400 |
-
|
| 401 |
-
function FeatureEditorialList() {
|
| 402 |
-
return (
|
| 403 |
-
<div className="mt-12">
|
| 404 |
-
{FEATURE_ROWS.map((feature) => (
|
| 405 |
-
<div key={feature.title} className="mb-7">
|
| 406 |
-
<div className="grid grid-cols-[auto_1fr_auto] items-center gap-4">
|
| 407 |
-
<div className="flex items-center gap-3">
|
| 408 |
-
<span className={`h-2 w-2 rounded-full ${feature.dotClassName}`} />
|
| 409 |
-
<p className="text-2xl font-bold text-white md:text-3xl">{feature.title}</p>
|
| 410 |
-
</div>
|
| 411 |
-
<div className="border-b border-dotted border-zinc-800" />
|
| 412 |
-
<p className="text-right font-mono text-sm text-zinc-500">{feature.descriptor}</p>
|
| 413 |
-
</div>
|
| 414 |
-
</div>
|
| 415 |
-
))}
|
| 416 |
-
</div>
|
| 417 |
-
);
|
| 418 |
-
}
|
| 419 |
-
|
| 420 |
-
function TaskBenchmarkList() {
|
| 421 |
-
const { ref, visible } = useRevealOnScroll<HTMLDivElement>();
|
| 422 |
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
{TASKS.map((task, index) => (
|
| 426 |
-
<article key={task.name} className="flex flex-col gap-4 py-5 md:flex-row md:items-center">
|
| 427 |
-
<div className="flex min-w-0 flex-1 items-center gap-4">
|
| 428 |
-
<span className="w-6 font-mono text-sm text-zinc-600">{String(index + 1).padStart(2, "0")}</span>
|
| 429 |
-
<p className="min-w-0 text-lg font-medium text-white">{task.name}</p>
|
| 430 |
-
</div>
|
| 431 |
-
<div className="flex items-center justify-end gap-4">
|
| 432 |
-
<DifficultyBadge difficulty={task.difficulty} />
|
| 433 |
-
<div className="h-1 w-20 rounded-full bg-zinc-800">
|
| 434 |
-
<div
|
| 435 |
-
className="h-1 rounded-full bg-zinc-400 transition-[width] duration-1000 ease-out"
|
| 436 |
-
style={{
|
| 437 |
-
width: visible ? `${Math.max(0, task.score * 100)}%` : "0%",
|
| 438 |
-
transitionDelay: `${index * 140}ms`,
|
| 439 |
-
}}
|
| 440 |
-
/>
|
| 441 |
-
</div>
|
| 442 |
-
<span className="min-w-10 text-right font-mono text-xs text-zinc-500">{task.score.toFixed(2)}</span>
|
| 443 |
-
</div>
|
| 444 |
-
</article>
|
| 445 |
-
))}
|
| 446 |
-
</div>
|
| 447 |
-
);
|
| 448 |
-
}
|
| 449 |
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
<div className="mt-3 h-0.5 w-full max-w-xs bg-zinc-900">
|
| 457 |
-
<div className={`h-0.5 rounded-full ${term.tone}`} style={{ width: `${(term.weight / 0.5) * 100}%` }} />
|
| 458 |
-
</div>
|
| 459 |
-
<p className="mt-2 font-mono text-xs text-zinc-500">{term.label}</p>
|
| 460 |
-
</div>
|
| 461 |
-
))}
|
| 462 |
-
</div>
|
| 463 |
-
);
|
| 464 |
-
}
|
| 465 |
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
step: 0,
|
| 469 |
-
cpuLoads: [0.3, 0.25, 0.4, 0.35, 0.28, 0.32, 0.38, 0.22],
|
| 470 |
-
queueLengths: [4, 6, 3, 8, 5, 7, 2, 4],
|
| 471 |
-
latencyMs: 32.5,
|
| 472 |
-
failedNodes: [],
|
| 473 |
-
requestRate: 142,
|
| 474 |
-
});
|
| 475 |
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
step += 1;
|
| 480 |
-
const t = step * 0.15;
|
| 481 |
-
setSnapshot({
|
| 482 |
-
step,
|
| 483 |
-
cpuLoads: Array.from({ length: 8 }, (_, i) => {
|
| 484 |
-
const load = Math.max(0.05, Math.min(0.95, 0.35 + 0.25 * Math.sin(t + i * 0.8) + (Math.random() - 0.5) * 0.06));
|
| 485 |
-
return Number(load.toFixed(2));
|
| 486 |
-
}),
|
| 487 |
-
queueLengths: Array.from({ length: 8 }, (_, i) =>
|
| 488 |
-
Math.max(0, Math.round(12 + 10 * Math.sin(t * 0.7 + i) + Math.random() * 3))
|
| 489 |
-
),
|
| 490 |
-
latencyMs: Math.max(8, 28 + 16 * Math.sin(t * 0.5) + Math.random() * 6),
|
| 491 |
-
failedNodes: step % 40 > 30 && step % 40 < 36 ? [Math.floor(Math.random() * 7) + 1] : [],
|
| 492 |
-
requestRate: Math.max(60, 150 + 60 * Math.sin(t * 0.3) + Math.random() * 12),
|
| 493 |
-
});
|
| 494 |
-
}, 600);
|
| 495 |
-
return () => clearInterval(timer);
|
| 496 |
-
}, []);
|
| 497 |
|
| 498 |
-
|
| 499 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
|
| 501 |
-
|
| 502 |
-
|
| 503 |
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
const value = max > 0 ? Math.min(1, Math.max(0, window.scrollY / max)) : 0;
|
| 508 |
-
setProgress(value);
|
| 509 |
};
|
| 510 |
-
|
| 511 |
-
onScroll();
|
| 512 |
-
window.addEventListener("scroll", onScroll, { passive: true });
|
| 513 |
-
return () => window.removeEventListener("scroll", onScroll);
|
| 514 |
}, []);
|
| 515 |
|
| 516 |
-
return (
|
| 517 |
-
<div className="fixed inset-x-0 top-0 z-[70] h-[2px] bg-transparent">
|
| 518 |
-
<div
|
| 519 |
-
className="h-full bg-gradient-to-r from-orange-300 via-rose-300 to-amber-200 transition-[width] duration-200"
|
| 520 |
-
style={{ width: `${progress * 100}%` }}
|
| 521 |
-
/>
|
| 522 |
-
</div>
|
| 523 |
-
);
|
| 524 |
-
}
|
| 525 |
-
|
| 526 |
-
export default function Home() {
|
| 527 |
-
const telemetry = useAnimatedTelemetry();
|
| 528 |
-
const [showScrollCue, setShowScrollCue] = useState(true);
|
| 529 |
-
|
| 530 |
useEffect(() => {
|
| 531 |
const onScroll = () => {
|
| 532 |
if (window.scrollY > 12) {
|
|
@@ -541,111 +400,77 @@ export default function Home() {
|
|
| 541 |
|
| 542 |
return (
|
| 543 |
<div className="relative min-h-screen bg-[#080808] text-white">
|
| 544 |
-
<
|
| 545 |
-
<TopNavigation />
|
| 546 |
|
| 547 |
-
<main className="relative pb-24">
|
| 548 |
<section
|
| 549 |
id="about"
|
| 550 |
-
className="relative min-h-screen scroll-mt-28
|
| 551 |
>
|
| 552 |
-
<
|
| 553 |
-
<div className="relative grid min-h-screen items-center gap-8 md:grid-cols-12">
|
| 554 |
-
<Spotlight />
|
| 555 |
-
|
| 556 |
-
<div className="md:col-span-7">
|
| 557 |
-
<p className="font-mono text-xs tracking-[0.2em] text-orange-300">[ SRE BENCHMARK ]</p>
|
| 558 |
-
<HeroWordmark />
|
| 559 |
-
<p className="mt-4 max-w-2xl text-xl text-zinc-200 sm:text-2xl">Distributed infra benchmark for autonomous SRE agents.</p>
|
| 560 |
-
<a
|
| 561 |
-
href="#try"
|
| 562 |
-
className="mt-8 inline-flex items-center rounded-md border border-orange-400/40 bg-orange-500/10 px-5 py-3 font-mono text-sm text-orange-100 transition-colors hover:border-pink-400/50 hover:bg-pink-500/10"
|
| 563 |
-
>
|
| 564 |
-
Jump to Live Access ->
|
| 565 |
-
</a>
|
| 566 |
-
|
| 567 |
-
<div className="mt-12 max-w-[460px] rounded-xl border border-zinc-700 bg-zinc-950/95 p-4 font-mono text-xs shadow-[0_0_40px_rgba(251,146,60,0.08)]">
|
| 568 |
-
<p className="text-emerald-300/80">● LIVE BASELINES</p>
|
| 569 |
-
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2 text-[11px] sm:text-xs">
|
| 570 |
-
<div className="flex items-center gap-2">
|
| 571 |
-
<span className="text-zinc-600">#1</span>
|
| 572 |
-
<span className="text-white">Qwen3-8B</span>
|
| 573 |
-
<span className="text-emerald-300">0.31</span>
|
| 574 |
-
</div>
|
| 575 |
-
<div className="flex items-center gap-2">
|
| 576 |
-
<span className="text-zinc-600">#2</span>
|
| 577 |
-
<span className="text-white">Llama-3.1-8B</span>
|
| 578 |
-
<span className="text-amber-300">0.09</span>
|
| 579 |
-
</div>
|
| 580 |
-
<a href="#try-it" className="text-emerald-300/80 transition-colors hover:text-emerald-200">
|
| 581 |
-
submit your agent ->
|
| 582 |
-
</a>
|
| 583 |
-
</div>
|
| 584 |
-
</div>
|
| 585 |
-
</div>
|
| 586 |
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
<span>failed_nodes</span>
|
| 605 |
-
<span className="min-w-0 break-words text-red-300">[{telemetry ? telemetry.failedNodes.join(", ") : "loading"}]</span>
|
| 606 |
-
</p>
|
| 607 |
-
<p className="grid grid-cols-[auto_1fr] gap-3 text-zinc-400">
|
| 608 |
-
<span>request_rate</span>
|
| 609 |
-
<span className="text-amber-200">{telemetry ? `${telemetry.requestRate.toFixed(0)} req/s` : "loading"}</span>
|
| 610 |
-
</p>
|
| 611 |
-
</div>
|
| 612 |
|
| 613 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
</div>
|
| 615 |
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
↓ scroll to explore
|
| 620 |
-
</div>
|
| 621 |
-
) : null}
|
| 622 |
-
</div>
|
| 623 |
</div>
|
| 624 |
</div>
|
| 625 |
-
</section>
|
| 626 |
-
|
| 627 |
-
<SectionDivider label="OBSERVABILITY" />
|
| 628 |
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
/>
|
| 636 |
-
<div className="mt-8 sm:mt-10">
|
| 637 |
-
<DetailGrid cards={OBSERVABILITY_CARDS} />
|
| 638 |
</div>
|
| 639 |
-
</
|
| 640 |
|
| 641 |
<SectionDivider label="LIVE SYSTEM" />
|
| 642 |
|
| 643 |
-
<RevealSection id="simulation">
|
| 644 |
<p className="font-mono text-xs tracking-[0.2em] text-sky-300">[ REAL-TIME CLUSTER TOPOLOGY ]</p>
|
| 645 |
<AnimatedHeading words="Watch DIME Evolve Step by Step" />
|
| 646 |
<TextGenerateEffect
|
| 647 |
-
words="Native
|
| 648 |
-
className="mt-3 text-
|
| 649 |
/>
|
| 650 |
<div className="mt-10">
|
| 651 |
<ClusterSimulation />
|
|
@@ -654,38 +479,94 @@ export default function Home() {
|
|
| 654 |
|
| 655 |
<SectionDivider label="DYNAMICS" />
|
| 656 |
|
| 657 |
-
<RevealSection id="features">
|
| 658 |
<p className="font-mono text-xs tracking-[0.2em] text-orange-300">[ SIMULATION DYNAMICS ]</p>
|
| 659 |
<AnimatedHeading words="What Makes DIME Hard" />
|
| 660 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
|
| 662 |
-
<div className="mt-
|
| 663 |
<AnimatedHeading words="Four Tasks. One Unforgiving Benchmark." className="text-3xl sm:text-5xl" />
|
| 664 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
</div>
|
| 666 |
</RevealSection>
|
| 667 |
|
| 668 |
<SectionDivider label="SCORING" />
|
| 669 |
|
| 670 |
-
<RevealSection id="reward">
|
| 671 |
<p className="font-mono text-xs tracking-[0.2em] text-pink-300">[ REWARD SIGNAL ]</p>
|
| 672 |
<AnimatedHeading words="How DIME Scores an Agent" />
|
|
|
|
| 673 |
|
| 674 |
<div className="mt-12 grid grid-cols-1 gap-8 lg:grid-cols-12">
|
| 675 |
<div className="lg:col-span-7">
|
| 676 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 677 |
</div>
|
| 678 |
|
| 679 |
<div className="lg:col-span-5">
|
| 680 |
<div className="rounded-xl border border-zinc-800 bg-zinc-950 p-6 font-mono text-sm text-orange-200">
|
| 681 |
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2">
|
| 682 |
{[
|
| 683 |
-
"
|
| 684 |
-
"
|
| 685 |
-
"
|
| 686 |
-
"
|
| 687 |
-
"
|
| 688 |
-
"task graders compute live benchmark score",
|
| 689 |
].map((line, idx) => (
|
| 690 |
<div key={`row-${idx}`} className="contents">
|
| 691 |
<span className="text-zinc-600">{String(idx + 1).padStart(2, "0")}</span>
|
|
@@ -700,26 +581,15 @@ export default function Home() {
|
|
| 700 |
|
| 701 |
<SectionDivider label="METRICS" />
|
| 702 |
|
| 703 |
-
<RevealSection id="
|
| 704 |
-
<p className="font-mono text-xs tracking-[0.2em] text-orange-300">[
|
| 705 |
<AnimatedHeading words="Benchmark Diagnostics" />
|
| 706 |
-
<TextGenerateEffect
|
| 707 |
-
words="Core plots from the repo show how DIME handles reward shaping, exploit resistance, cost-latency coupling, and exploration schedules."
|
| 708 |
-
className="mt-3 text-xs uppercase tracking-[0.14em] text-zinc-500 sm:text-sm"
|
| 709 |
-
/>
|
| 710 |
|
| 711 |
<div className="mt-12 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
| 712 |
{METRIC_FIGURES.map((figure) => (
|
| 713 |
<article key={figure.src} className="overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950/80">
|
| 714 |
-
<
|
| 715 |
-
src={figure.src}
|
| 716 |
-
alt={figure.title}
|
| 717 |
-
width={1100}
|
| 718 |
-
height={680}
|
| 719 |
-
loading="lazy"
|
| 720 |
-
sizes="(min-width: 1024px) 50vw, 100vw"
|
| 721 |
-
className="h-auto w-full object-cover"
|
| 722 |
-
/>
|
| 723 |
<div className="p-4">
|
| 724 |
<p className="text-base font-semibold text-white">{figure.title}</p>
|
| 725 |
<p className="mt-2 text-sm text-zinc-400">{figure.caption}</p>
|
|
@@ -729,87 +599,84 @@ export default function Home() {
|
|
| 729 |
</div>
|
| 730 |
</RevealSection>
|
| 731 |
|
| 732 |
-
<SectionDivider label="INSTALL" />
|
| 733 |
-
|
| 734 |
-
<RevealSection id="try-it">
|
| 735 |
-
<p className="font-mono text-xs tracking-[0.2em] text-emerald-300">[ QUICKSTART ]</p>
|
| 736 |
-
<AnimatedHeading words="Install + Run DIME" className="text-3xl sm:text-5xl" />
|
| 737 |
-
<p className="mt-5 font-mono text-xs text-zinc-500">
|
| 738 |
-
install deps → start server → run evaluation
|
| 739 |
-
</p>
|
| 740 |
-
<div className="mt-8 grid grid-cols-1 gap-3 md:grid-cols-3">
|
| 741 |
-
{QUICKSTART_STEPS.map((step, index) => (
|
| 742 |
-
<article
|
| 743 |
-
key={step.label}
|
| 744 |
-
className="rounded-xl border border-zinc-800 bg-zinc-950/75 p-4 shadow-[0_8px_30px_rgba(0,0,0,0.28)] transition-colors duration-300 hover:border-zinc-700"
|
| 745 |
-
>
|
| 746 |
-
<p className="font-mono text-[11px] tracking-[0.14em] text-zinc-500">
|
| 747 |
-
STEP {String(index + 1).padStart(2, "0")}
|
| 748 |
-
</p>
|
| 749 |
-
<p className="mt-2 text-sm font-semibold text-zinc-100">{step.label}</p>
|
| 750 |
-
<div className="mt-3 rounded-md border border-zinc-800 bg-black/50 px-3 py-2 font-mono text-xs text-zinc-300">
|
| 751 |
-
{step.command}
|
| 752 |
-
</div>
|
| 753 |
-
</article>
|
| 754 |
-
))}
|
| 755 |
-
</div>
|
| 756 |
-
</RevealSection>
|
| 757 |
-
|
| 758 |
<SectionDivider label="ACCESS" />
|
| 759 |
|
| 760 |
-
<RevealSection id="try">
|
| 761 |
<p className="font-mono text-xs tracking-[0.2em] text-pink-300">[ LIVE ACCESS ]</p>
|
| 762 |
<div className="mt-5 max-w-4xl md:ml-10">
|
| 763 |
-
<AnimatedHeading words="Try DIME" className="text-center md:text-left" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 764 |
|
| 765 |
<div className="mt-8 flex flex-wrap items-center justify-center gap-3 md:justify-start">
|
| 766 |
-
<
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
className="inline-flex min-w-[230px] cursor-not-allowed items-center justify-center rounded-md bg-gradient-to-r from-orange-300 to-pink-300 px-5 py-3 text-sm font-semibold text-black opacity-80"
|
| 770 |
>
|
| 771 |
-
Hugging Face
|
| 772 |
-
</
|
| 773 |
<a
|
| 774 |
-
href="#
|
| 775 |
-
className="inline-flex
|
| 776 |
>
|
| 777 |
-
|
| 778 |
</a>
|
| 779 |
</div>
|
| 780 |
|
| 781 |
-
<
|
| 782 |
-
docker
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
</div>
|
|
|
|
| 784 |
</div>
|
| 785 |
</RevealSection>
|
| 786 |
</main>
|
| 787 |
|
| 788 |
-
<
|
| 789 |
-
<div className="border-t border-zinc-800" />
|
| 790 |
-
</div>
|
| 791 |
-
|
| 792 |
-
<footer className="bg-zinc-950/35">
|
| 793 |
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-8 px-5 py-10 text-sm text-zinc-500 sm:px-8 lg:grid-cols-12">
|
| 794 |
-
<div className="lg:col-span-
|
| 795 |
<p className="font-mono text-zinc-300">DIME</p>
|
| 796 |
<p className="mt-3 max-w-2xl leading-7">
|
| 797 |
-
Distributed Infrastructure Management Environment
|
| 798 |
-
|
|
|
|
|
|
|
|
|
|
| 799 |
</p>
|
| 800 |
</div>
|
| 801 |
|
| 802 |
-
<div className="lg:col-span-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 803 |
<p className="font-mono text-xs tracking-[0.16em] text-zinc-400">RESOURCES</p>
|
| 804 |
<div className="mt-3 flex flex-col gap-2">
|
| 805 |
-
<a href="#
|
| 806 |
-
|
| 807 |
</a>
|
| 808 |
-
<a href="#
|
| 809 |
-
|
| 810 |
</a>
|
| 811 |
-
<a href="#
|
| 812 |
-
|
| 813 |
</a>
|
| 814 |
</div>
|
| 815 |
</div>
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
|
|
|
| 3 |
import { useEffect, useRef, useState } from "react";
|
| 4 |
import { FlipWords } from "@/components/ui/FlipWords";
|
| 5 |
+
import { FloatingDock } from "@/components/ui/FloatingDock";
|
| 6 |
+
import { FocusCards } from "@/components/ui/FocusCards";
|
| 7 |
import { TextGenerateEffect } from "@/components/ui/TextGenerateEffect";
|
| 8 |
import { ClusterSimulation } from "@/components/simulation/ClusterSimulation";
|
| 9 |
+
import type { DockItem, FocusCard } from "@/components/ui/types";
|
| 10 |
|
| 11 |
type TelemetrySnapshot = {
|
| 12 |
step: number;
|
|
|
|
| 22 |
difficulty: "Easy" | "Medium" | "Hard" | "Expert";
|
| 23 |
description: string;
|
| 24 |
score: number;
|
|
|
|
| 25 |
};
|
| 26 |
|
| 27 |
type MetricFigure = {
|
|
|
|
| 30 |
caption: string;
|
| 31 |
};
|
| 32 |
|
| 33 |
+
const DEPLOYMENT_LIVE = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
const HERO_FLIP_WORDS = ["adaptive", "resilient", "autonomous", "modern"];
|
| 36 |
|
| 37 |
+
const DOCK_ITEMS: DockItem[] = [
|
| 38 |
{
|
| 39 |
+
title: "Home",
|
| 40 |
href: "#about",
|
| 41 |
icon: ({ className }) => (
|
| 42 |
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="1.8">
|
|
|
|
| 45 |
</svg>
|
| 46 |
),
|
| 47 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
{
|
| 49 |
title: "Simulation",
|
| 50 |
href: "#simulation",
|
|
|
|
| 74 |
),
|
| 75 |
},
|
| 76 |
{
|
| 77 |
+
title: "Metrics",
|
| 78 |
+
href: "#metrics",
|
| 79 |
icon: ({ className }) => (
|
| 80 |
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="1.8">
|
| 81 |
<rect x="3.5" y="3.5" width="17" height="17" rx="2.5" />
|
|
|
|
| 85 |
},
|
| 86 |
{
|
| 87 |
title: "Try It",
|
| 88 |
+
href: "#try",
|
| 89 |
icon: ({ className }) => (
|
| 90 |
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="1.8">
|
| 91 |
<path d="m5 19 14-7L5 5v5l8 2-8 2z" />
|
|
|
|
| 94 |
},
|
| 95 |
];
|
| 96 |
|
| 97 |
+
const TASKS: TaskCard[] = [
|
| 98 |
{
|
| 99 |
+
name: "Task 1 - Traffic Spike Recovery",
|
| 100 |
+
difficulty: "Easy",
|
| 101 |
+
description:
|
| 102 |
+
"Handles a 3x request surge while keeping latency under 50ms without wasting actions.",
|
| 103 |
+
score: 0.09,
|
| 104 |
},
|
| 105 |
{
|
| 106 |
+
name: "Task 2 - Single Node Failure",
|
| 107 |
+
difficulty: "Medium",
|
| 108 |
+
description:
|
| 109 |
+
"Repairs a failed node under pressure while preserving uptime and minimizing MTTR penalties.",
|
| 110 |
+
score: 0.05,
|
| 111 |
},
|
| 112 |
{
|
| 113 |
+
name: "Task 3 - Cascading Failure Prevention",
|
| 114 |
+
difficulty: "Hard",
|
| 115 |
+
description:
|
| 116 |
+
"Must proactively reroute load before thermal hotspots trigger chain-collapse behavior.",
|
| 117 |
+
score: 0.31,
|
| 118 |
},
|
| 119 |
{
|
| 120 |
+
name: "Task 4 - Flash Crowd Meltdown",
|
| 121 |
+
difficulty: "Expert",
|
| 122 |
+
description:
|
| 123 |
+
"A 5x traffic event where survival demands precise throttle and scale timing under scarcity.",
|
| 124 |
+
score: 0,
|
| 125 |
},
|
| 126 |
];
|
| 127 |
|
| 128 |
+
const FEATURE_CARDS: FocusCard[] = [
|
| 129 |
{
|
| 130 |
+
title: "Forest Adventure",
|
| 131 |
+
src: "https://images.unsplash.com/photo-1518710843675-2540dd79065c?q=80&w=3387&auto=format&fit=crop",
|
|
|
|
| 132 |
},
|
| 133 |
{
|
| 134 |
+
title: "Valley of life",
|
| 135 |
+
src: "https://images.unsplash.com/photo-1600271772470-bd22a42787b3?q=80&w=3072&auto=format&fit=crop",
|
|
|
|
| 136 |
},
|
| 137 |
{
|
| 138 |
+
title: "Sala behta hi jayega",
|
| 139 |
+
src: "https://images.unsplash.com/photo-1505142468610-359e7d316be0?q=80&w=3070&auto=format&fit=crop",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
},
|
| 141 |
{
|
| 142 |
+
title: "Camping is for pros",
|
| 143 |
+
src: "https://images.unsplash.com/photo-1486915309851-b0cc1f8a0084?q=80&w=3387&auto=format&fit=crop",
|
|
|
|
|
|
|
|
|
|
| 144 |
},
|
| 145 |
{
|
| 146 |
+
title: "The road not taken",
|
| 147 |
+
src: "https://images.unsplash.com/photo-1507041957456-9c397ce39c97?q=80&w=3456&auto=format&fit=crop",
|
|
|
|
|
|
|
|
|
|
| 148 |
},
|
| 149 |
{
|
| 150 |
+
title: "The First Rule",
|
| 151 |
+
src: "https://assets.aceternity.com/the-first-rule.png",
|
|
|
|
|
|
|
|
|
|
| 152 |
},
|
| 153 |
];
|
| 154 |
|
| 155 |
+
const TASK_CARDS: FocusCard[] = [
|
| 156 |
+
{
|
| 157 |
+
title: "Traffic Spike Recovery",
|
| 158 |
+
src: "https://images.unsplash.com/photo-1498050108023-c5249f4df085?q=80&w=3272&auto=format&fit=crop",
|
| 159 |
+
},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
{
|
| 161 |
+
title: "Single Node Failure",
|
| 162 |
+
src: "https://images.unsplash.com/photo-1562813733-b31f71025d54?q=80&w=3270&auto=format&fit=crop",
|
| 163 |
},
|
| 164 |
{
|
| 165 |
+
title: "Cascading Failure Prevention",
|
| 166 |
+
src: "https://images.unsplash.com/photo-1518773553398-650c184e0bb3?q=80&w=3272&auto=format&fit=crop",
|
| 167 |
},
|
| 168 |
{
|
| 169 |
+
title: "Flash Crowd Meltdown",
|
| 170 |
+
src: "https://images.unsplash.com/photo-1461749280684-dccba630e2f6?q=80&w=3270&auto=format&fit=crop",
|
| 171 |
},
|
| 172 |
];
|
| 173 |
|
|
|
|
| 175 |
{
|
| 176 |
src: "/metrics/fig1_vanishing_gradient_fix.png",
|
| 177 |
title: "Fig 1: Vanishing Gradient Fix",
|
| 178 |
+
caption: "Stabilized training signal and improved optimization behavior.",
|
| 179 |
},
|
| 180 |
{
|
| 181 |
src: "/metrics/fig2_cascade_exploit_fix.png",
|
| 182 |
title: "Fig 2: Cascade Exploit Fix",
|
| 183 |
+
caption: "Mitigates exploit dynamics in cascading-failure conditions.",
|
| 184 |
},
|
| 185 |
{
|
| 186 |
src: "/metrics/fig3_cost_latency_coupling.png",
|
| 187 |
title: "Fig 3: Cost-Latency Coupling",
|
| 188 |
+
caption: "Shows tradeoff frontier between resource cost and service latency.",
|
| 189 |
},
|
| 190 |
{
|
| 191 |
src: "/metrics/fig4_curiosity_annealing.png",
|
| 192 |
title: "Fig 4: Curiosity Annealing",
|
| 193 |
+
caption: "Annealing schedule effect on exploration vs. stability.",
|
| 194 |
},
|
| 195 |
];
|
| 196 |
|
|
|
|
| 234 |
<section
|
| 235 |
id={id}
|
| 236 |
ref={ref}
|
| 237 |
+
className={`scroll-mt-28 transition-all duration-700 motion-reduce:transform-none motion-reduce:opacity-100 ${
|
| 238 |
visible ? "translate-y-0 opacity-100" : "translate-y-4 opacity-0"
|
| 239 |
} ${className ?? ""}`}
|
| 240 |
>
|
| 241 |
+
{children}
|
| 242 |
</section>
|
| 243 |
);
|
| 244 |
}
|
| 245 |
|
| 246 |
function SectionDivider({ label }: { label: string }) {
|
| 247 |
return (
|
| 248 |
+
<div className="my-16 sm:my-20" aria-hidden="true">
|
| 249 |
<div className="grid grid-cols-[1fr_auto_1fr] items-center gap-4">
|
| 250 |
<div className="h-px bg-gradient-to-r from-transparent via-zinc-800 to-zinc-700/50" />
|
| 251 |
<span className="rounded-full border border-zinc-800 bg-zinc-950 px-3 py-1 font-mono text-[11px] tracking-[0.16em] text-zinc-500">
|
|
|
|
| 259 |
|
| 260 |
function DifficultyBadge({ difficulty }: { difficulty: TaskCard["difficulty"] }) {
|
| 261 |
const tone = {
|
| 262 |
+
Easy: "border-emerald-500/40 bg-emerald-500/10 text-emerald-300",
|
| 263 |
+
Medium: "border-amber-500/40 bg-amber-500/10 text-amber-300",
|
| 264 |
+
Hard: "border-orange-500/40 bg-orange-500/10 text-orange-300",
|
| 265 |
+
Expert: "border-pink-500/40 bg-pink-500/10 text-pink-300",
|
| 266 |
}[difficulty];
|
| 267 |
|
| 268 |
+
return <span className={`rounded-full border px-2.5 py-1 text-xs font-mono ${tone}`}>[{difficulty}]</span>;
|
| 269 |
}
|
| 270 |
|
| 271 |
function Spotlight() {
|
| 272 |
return (
|
| 273 |
+
<div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10 overflow-hidden">
|
| 274 |
<div className="absolute left-1/2 top-[-12rem] h-[28rem] w-[28rem] -translate-x-1/2 rounded-full bg-[radial-gradient(circle,rgba(251,146,60,0.28),transparent_62%)]" />
|
| 275 |
+
<div className="absolute right-[-8rem] top-12 h-[26rem] w-[26rem] rounded-full bg-[radial-gradient(circle,rgba(236,72,153,0.22),transparent_68%)]" />
|
| 276 |
<div className="absolute bottom-[-12rem] left-[-10rem] h-[24rem] w-[24rem] rounded-full bg-[radial-gradient(circle,rgba(249,115,22,0.18),transparent_65%)]" />
|
| 277 |
</div>
|
| 278 |
);
|
|
|
|
| 280 |
|
| 281 |
function HeroWordmark() {
|
| 282 |
return (
|
| 283 |
+
<div className="mt-5 flex flex-wrap items-end gap-x-3 gap-y-2 text-[20vw] font-black leading-[0.82] tracking-[-0.06em] md:text-[10vw]">
|
| 284 |
<span className="bg-gradient-to-b from-white via-orange-100 to-pink-300 bg-clip-text text-transparent [text-shadow:0_0_26px_rgba(251,146,60,0.22)]">
|
| 285 |
DIME
|
| 286 |
</span>
|
|
|
|
| 295 |
return (
|
| 296 |
<TextGenerateEffect
|
| 297 |
words={words}
|
| 298 |
+
className={`mt-4 text-4xl font-black leading-[0.95] tracking-[-0.03em] text-zinc-100 [text-shadow:0_0_24px_rgba(244,114,182,0.18)] sm:text-6xl ${className ?? ""}`}
|
|
|
|
|
|
|
| 299 |
duration={900}
|
| 300 |
filter={false}
|
| 301 |
/>
|
| 302 |
);
|
| 303 |
}
|
| 304 |
|
| 305 |
+
function parseTelemetry(payload: unknown): TelemetrySnapshot | null {
|
| 306 |
+
if (!payload || typeof payload !== "object") return null;
|
| 307 |
+
|
| 308 |
+
const candidate = payload as Record<string, unknown>;
|
| 309 |
+
const source =
|
| 310 |
+
(candidate.observation as Record<string, unknown> | undefined) ??
|
| 311 |
+
(candidate.data as Record<string, unknown> | undefined) ??
|
| 312 |
+
candidate;
|
| 313 |
+
|
| 314 |
+
const cpuLoadsRaw = source.cpu_loads ?? source.cpuLoads;
|
| 315 |
+
const queueLengthsRaw = source.queue_lengths ?? source.queueLengths;
|
| 316 |
+
const failedNodesRaw = source.failed_nodes ?? source.failedNodes;
|
| 317 |
+
const latencyRaw = source.latency_ms ?? source.latencyMs;
|
| 318 |
+
const requestRateRaw = source.request_rate ?? source.requestRate;
|
| 319 |
+
const stepRaw = source.step;
|
| 320 |
+
|
| 321 |
+
if (!Array.isArray(cpuLoadsRaw) || !Array.isArray(queueLengthsRaw) || !Array.isArray(failedNodesRaw)) {
|
| 322 |
+
return null;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
const cpuLoads = cpuLoadsRaw.map((value) => Number(value)).filter((value) => Number.isFinite(value));
|
| 326 |
+
const queueLengths = queueLengthsRaw
|
| 327 |
+
.map((value) => Number(value))
|
| 328 |
+
.filter((value) => Number.isFinite(value));
|
| 329 |
+
const failedNodes = failedNodesRaw.map((value) => Number(value)).filter((value) => Number.isFinite(value));
|
| 330 |
+
|
| 331 |
+
const latencyMs = Number(latencyRaw);
|
| 332 |
+
const requestRate = Number(requestRateRaw);
|
| 333 |
+
const step = Number(stepRaw);
|
| 334 |
+
|
| 335 |
+
if (!Number.isFinite(latencyMs) || !Number.isFinite(requestRate) || !Number.isFinite(step)) {
|
| 336 |
+
return null;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
return {
|
| 340 |
+
step,
|
| 341 |
+
cpuLoads,
|
| 342 |
+
queueLengths,
|
| 343 |
+
latencyMs,
|
| 344 |
+
failedNodes,
|
| 345 |
+
requestRate,
|
| 346 |
+
};
|
| 347 |
}
|
| 348 |
|
| 349 |
+
export default function Home() {
|
| 350 |
+
const [telemetry, setTelemetry] = useState<TelemetrySnapshot | null>(null);
|
| 351 |
+
const [telemetryError, setTelemetryError] = useState<string | null>(null);
|
| 352 |
+
const [showScrollCue, setShowScrollCue] = useState(true);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
|
| 354 |
+
useEffect(() => {
|
| 355 |
+
let mounted = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
+
const loadTelemetry = async () => {
|
| 358 |
+
try {
|
| 359 |
+
const response = await fetch("/api/telemetry", { cache: "no-store" });
|
| 360 |
+
if (!response.ok) {
|
| 361 |
+
throw new Error(`telemetry request failed (${response.status})`);
|
| 362 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
|
| 364 |
+
const payload: unknown = await response.json();
|
| 365 |
+
const parsed = parseTelemetry(payload);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
|
| 367 |
+
if (!parsed) {
|
| 368 |
+
throw new Error("telemetry payload did not match expected schema");
|
| 369 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
|
| 371 |
+
if (!mounted) return;
|
| 372 |
+
setTelemetry(parsed);
|
| 373 |
+
setTelemetryError(null);
|
| 374 |
+
} catch (error) {
|
| 375 |
+
if (!mounted) return;
|
| 376 |
+
setTelemetryError(error instanceof Error ? error.message : "unable to fetch telemetry");
|
| 377 |
+
}
|
| 378 |
+
};
|
| 379 |
|
| 380 |
+
loadTelemetry();
|
| 381 |
+
const timer = window.setInterval(loadTelemetry, 2500);
|
| 382 |
|
| 383 |
+
return () => {
|
| 384 |
+
mounted = false;
|
| 385 |
+
window.clearInterval(timer);
|
|
|
|
|
|
|
| 386 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
}, []);
|
| 388 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
useEffect(() => {
|
| 390 |
const onScroll = () => {
|
| 391 |
if (window.scrollY > 12) {
|
|
|
|
| 400 |
|
| 401 |
return (
|
| 402 |
<div className="relative min-h-screen bg-[#080808] text-white">
|
| 403 |
+
<FloatingDock items={DOCK_ITEMS} className="bottom-6" mobileClassName="bottom-4" />
|
|
|
|
| 404 |
|
| 405 |
+
<main className="relative mx-auto max-w-7xl px-5 pb-24 pt-24 sm:px-8 sm:pt-28">
|
| 406 |
<section
|
| 407 |
id="about"
|
| 408 |
+
className="relative grid min-h-screen scroll-mt-28 items-center gap-12 pb-14 md:grid-cols-12"
|
| 409 |
>
|
| 410 |
+
<Spotlight />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
|
| 412 |
+
<div className="md:col-span-7">
|
| 413 |
+
<p className="font-mono text-xs tracking-[0.2em] text-orange-300">[ SRE BENCHMARK ]</p>
|
| 414 |
+
<HeroWordmark />
|
| 415 |
+
<p className="mt-4 max-w-2xl text-xl text-zinc-200 sm:text-2xl">
|
| 416 |
+
Distributed Infrastructure Management Environment
|
| 417 |
+
</p>
|
| 418 |
+
<p className="mt-5 max-w-2xl text-base leading-7 text-zinc-400 sm:text-lg">
|
| 419 |
+
A high-fidelity simulated distributed system for training and evaluating LLM agents on
|
| 420 |
+
complex Site Reliability Engineering tasks. Built on the OpenEnv framework.
|
| 421 |
+
</p>
|
| 422 |
+
<a
|
| 423 |
+
href="#try"
|
| 424 |
+
className="mt-8 inline-flex items-center rounded-md border border-orange-400/40 bg-orange-500/10 px-5 py-3 font-mono text-sm text-orange-100 transition-colors hover:border-pink-400/50 hover:bg-pink-500/10"
|
| 425 |
+
>
|
| 426 |
+
Jump to Live Access ->
|
| 427 |
+
</a>
|
| 428 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
|
| 430 |
+
<div className="md:col-span-5 md:pl-6">
|
| 431 |
+
<div className="rounded-xl border border-zinc-700 bg-zinc-950/95 p-5 font-mono text-sm shadow-[0_0_40px_rgba(251,146,60,0.08)]">
|
| 432 |
+
<p className="text-zinc-500">● NODE STATUS [step: {telemetry?.step ?? "--"}]</p>
|
| 433 |
+
<div className="mt-4 space-y-2">
|
| 434 |
+
<p className="text-zinc-400">
|
| 435 |
+
cpu_loads <span className="text-emerald-300">[{telemetry ? telemetry.cpuLoads.join(", ") : "loading"}]</span>
|
| 436 |
+
</p>
|
| 437 |
+
<p className="text-zinc-400">
|
| 438 |
+
queue_lengths <span className="text-orange-300">[{telemetry ? telemetry.queueLengths.join(", ") : "loading"}]</span>
|
| 439 |
+
</p>
|
| 440 |
+
<p className="text-zinc-400">
|
| 441 |
+
latency_ms <span className="text-pink-300">{telemetry ? `${telemetry.latencyMs.toFixed(1)}ms` : "loading"}</span>
|
| 442 |
+
</p>
|
| 443 |
+
<p className="text-zinc-400">
|
| 444 |
+
failed_nodes <span className="text-red-300">[{telemetry ? telemetry.failedNodes.join(", ") : "loading"}]</span>
|
| 445 |
+
</p>
|
| 446 |
+
<p className="text-zinc-400">
|
| 447 |
+
request_rate <span className="text-amber-200">{telemetry ? `${telemetry.requestRate.toFixed(0)} req/s` : "loading"}</span>
|
| 448 |
+
</p>
|
| 449 |
</div>
|
| 450 |
|
| 451 |
+
{telemetryError ? (
|
| 452 |
+
<p className="mt-4 text-xs text-pink-300">telemetry warning: {telemetryError}</p>
|
| 453 |
+
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
</div>
|
| 455 |
</div>
|
|
|
|
|
|
|
|
|
|
| 456 |
|
| 457 |
+
<div className="md:col-span-12 pt-2">
|
| 458 |
+
{showScrollCue ? (
|
| 459 |
+
<div className="relative mt-4 text-center font-mono text-xs tracking-[0.18em] text-zinc-500 transition-all duration-300">
|
| 460 |
+
↓ scroll to explore
|
| 461 |
+
</div>
|
| 462 |
+
) : null}
|
|
|
|
|
|
|
|
|
|
| 463 |
</div>
|
| 464 |
+
</section>
|
| 465 |
|
| 466 |
<SectionDivider label="LIVE SYSTEM" />
|
| 467 |
|
| 468 |
+
<RevealSection id="simulation" className="mt-10">
|
| 469 |
<p className="font-mono text-xs tracking-[0.2em] text-sky-300">[ REAL-TIME CLUSTER TOPOLOGY ]</p>
|
| 470 |
<AnimatedHeading words="Watch DIME Evolve Step by Step" />
|
| 471 |
<TextGenerateEffect
|
| 472 |
+
words="Native React Flow vectors, motion-interpolated node states, and live WebSocket telemetry from the Python simulator."
|
| 473 |
+
className="mt-3 text-sm uppercase tracking-[0.14em] text-zinc-500"
|
| 474 |
/>
|
| 475 |
<div className="mt-10">
|
| 476 |
<ClusterSimulation />
|
|
|
|
| 479 |
|
| 480 |
<SectionDivider label="DYNAMICS" />
|
| 481 |
|
| 482 |
+
<RevealSection id="features" className="mt-8">
|
| 483 |
<p className="font-mono text-xs tracking-[0.2em] text-orange-300">[ SIMULATION DYNAMICS ]</p>
|
| 484 |
<AnimatedHeading words="What Makes DIME Hard" />
|
| 485 |
+
<TextGenerateEffect words="Constraint-driven incidents with compounding latency and brittle recovery windows." className="mt-3 text-sm uppercase tracking-[0.14em] text-zinc-500" />
|
| 486 |
+
|
| 487 |
+
<div className="mt-12">
|
| 488 |
+
<FocusCards cards={FEATURE_CARDS} />
|
| 489 |
+
</div>
|
| 490 |
|
| 491 |
+
<div className="mt-20">
|
| 492 |
<AnimatedHeading words="Four Tasks. One Unforgiving Benchmark." className="text-3xl sm:text-5xl" />
|
| 493 |
+
<TextGenerateEffect words="Each task shifts failure modes so policies must adapt instead of memorizing." className="mt-3 text-sm uppercase tracking-[0.14em] text-zinc-500" />
|
| 494 |
+
<div className="mt-9">
|
| 495 |
+
<FocusCards cards={TASK_CARDS} className="lg:grid-cols-2" />
|
| 496 |
+
</div>
|
| 497 |
+
|
| 498 |
+
<div className="mt-8 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
| 499 |
+
{TASKS.map((task) => (
|
| 500 |
+
<article key={task.name} className="rounded-xl border border-zinc-800 bg-zinc-950/90 p-5">
|
| 501 |
+
<div className="flex items-start justify-between gap-3">
|
| 502 |
+
<p className="text-base font-semibold text-zinc-100">{task.name}</p>
|
| 503 |
+
<DifficultyBadge difficulty={task.difficulty} />
|
| 504 |
+
</div>
|
| 505 |
+
<p className="mt-3 text-sm text-zinc-400">{task.description}</p>
|
| 506 |
+
<div className="mt-4">
|
| 507 |
+
<div className="flex items-center justify-between font-mono text-xs text-zinc-500">
|
| 508 |
+
<span>Llama-3.1-8B Baseline</span>
|
| 509 |
+
<span>{task.score.toFixed(3)}</span>
|
| 510 |
+
</div>
|
| 511 |
+
<div className="mt-2 h-2 rounded-full bg-zinc-900">
|
| 512 |
+
<div
|
| 513 |
+
className={`h-2 rounded-full ${task.score > 0.2 ? "bg-orange-400" : "bg-pink-500"}`}
|
| 514 |
+
style={{ width: `${Math.max(1, task.score * 100)}%` }}
|
| 515 |
+
/>
|
| 516 |
+
</div>
|
| 517 |
+
</div>
|
| 518 |
+
</article>
|
| 519 |
+
))}
|
| 520 |
+
</div>
|
| 521 |
</div>
|
| 522 |
</RevealSection>
|
| 523 |
|
| 524 |
<SectionDivider label="SCORING" />
|
| 525 |
|
| 526 |
+
<RevealSection id="reward" className="mt-10">
|
| 527 |
<p className="font-mono text-xs tracking-[0.2em] text-pink-300">[ REWARD SIGNAL ]</p>
|
| 528 |
<AnimatedHeading words="How DIME Scores an Agent" />
|
| 529 |
+
<TextGenerateEffect words="Dense per-step feedback rewards stability, penalizes latency, and discourages noisy interventions." className="mt-3 text-sm uppercase tracking-[0.14em] text-zinc-500" />
|
| 530 |
|
| 531 |
<div className="mt-12 grid grid-cols-1 gap-8 lg:grid-cols-12">
|
| 532 |
<div className="lg:col-span-7">
|
| 533 |
+
<p className="max-w-3xl text-zinc-400">
|
| 534 |
+
DIME uses a dense, step-level continuous reward signal, not sparse end-of-episode
|
| 535 |
+
rewards. This means the agent gets feedback every step, making it trainable via RL.
|
| 536 |
+
</p>
|
| 537 |
+
<ul className="mt-6 space-y-4 text-zinc-400">
|
| 538 |
+
<li>
|
| 539 |
+
<span className="font-mono text-zinc-100">+0.40 x uptime_ratio</span> - Keeps nodes
|
| 540 |
+
alive. The dominant signal.
|
| 541 |
+
</li>
|
| 542 |
+
<li>
|
| 543 |
+
<span className="font-mono text-zinc-100">-0.30 x normalized_latency</span> - Penalizes
|
| 544 |
+
slow responses proportional to severity.
|
| 545 |
+
</li>
|
| 546 |
+
<li>
|
| 547 |
+
<span className="font-mono text-zinc-100">-0.20 x overload_fraction</span> - Discourages
|
| 548 |
+
ignoring hot nodes.
|
| 549 |
+
</li>
|
| 550 |
+
<li>
|
| 551 |
+
<span className="font-mono text-zinc-100">-0.10 x (actions/max_steps)</span> - Action
|
| 552 |
+
efficiency penalty. Spamming actions is punished.
|
| 553 |
+
</li>
|
| 554 |
+
<li>
|
| 555 |
+
<span className="font-mono text-zinc-100">+0.50 x cascade_prevented_bonus</span> - The
|
| 556 |
+
highest bonus. Prevention rewarded over recovery.
|
| 557 |
+
</li>
|
| 558 |
+
</ul>
|
| 559 |
</div>
|
| 560 |
|
| 561 |
<div className="lg:col-span-5">
|
| 562 |
<div className="rounded-xl border border-zinc-800 bg-zinc-950 p-6 font-mono text-sm text-orange-200">
|
| 563 |
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2">
|
| 564 |
{[
|
| 565 |
+
"R(t) = + 0.40 x uptime_ratio",
|
| 566 |
+
" - 0.30 x normalized_latency",
|
| 567 |
+
" - 0.20 x overload_fraction",
|
| 568 |
+
" - 0.10 x (actions_taken / max_steps)",
|
| 569 |
+
" + 0.50 x cascade_prevented_bonus",
|
|
|
|
| 570 |
].map((line, idx) => (
|
| 571 |
<div key={`row-${idx}`} className="contents">
|
| 572 |
<span className="text-zinc-600">{String(idx + 1).padStart(2, "0")}</span>
|
|
|
|
| 581 |
|
| 582 |
<SectionDivider label="METRICS" />
|
| 583 |
|
| 584 |
+
<RevealSection id="metrics" className="mt-10">
|
| 585 |
+
<p className="font-mono text-xs tracking-[0.2em] text-orange-300">[ EVALUATION METRICS ]</p>
|
| 586 |
<AnimatedHeading words="Benchmark Diagnostics" />
|
| 587 |
+
<TextGenerateEffect words="Core plots from DIME evaluation runs across failure, latency, and exploration regimes." className="mt-3 text-sm uppercase tracking-[0.14em] text-zinc-500" />
|
|
|
|
|
|
|
|
|
|
| 588 |
|
| 589 |
<div className="mt-12 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
| 590 |
{METRIC_FIGURES.map((figure) => (
|
| 591 |
<article key={figure.src} className="overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950/80">
|
| 592 |
+
<img src={figure.src} alt={figure.title} loading="lazy" className="h-auto w-full object-cover" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
<div className="p-4">
|
| 594 |
<p className="text-base font-semibold text-white">{figure.title}</p>
|
| 595 |
<p className="mt-2 text-sm text-zinc-400">{figure.caption}</p>
|
|
|
|
| 599 |
</div>
|
| 600 |
</RevealSection>
|
| 601 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
<SectionDivider label="ACCESS" />
|
| 603 |
|
| 604 |
+
<RevealSection id="try" className="mt-10">
|
| 605 |
<p className="font-mono text-xs tracking-[0.2em] text-pink-300">[ LIVE ACCESS ]</p>
|
| 606 |
<div className="mt-5 max-w-4xl md:ml-10">
|
| 607 |
+
<AnimatedHeading words="Try Your Hands On DIME" className="text-center md:text-left" />
|
| 608 |
+
<TextGenerateEffect
|
| 609 |
+
words="Use the hosted space for quick validation or run the container locally for controlled experiments."
|
| 610 |
+
className="mt-3 text-center text-sm uppercase tracking-[0.14em] text-zinc-500 md:text-left"
|
| 611 |
+
/>
|
| 612 |
+
<p className="mt-5 max-w-3xl text-center text-zinc-400 md:text-left">
|
| 613 |
+
DIME is deployed as a containerized environment. You can interact with the live simulation
|
| 614 |
+
via the Hugging Face Space or pull the Docker image to run it locally.
|
| 615 |
+
</p>
|
| 616 |
|
| 617 |
<div className="mt-8 flex flex-wrap items-center justify-center gap-3 md:justify-start">
|
| 618 |
+
<a
|
| 619 |
+
href="#"
|
| 620 |
+
className="inline-flex min-w-[230px] items-center justify-center rounded-md bg-gradient-to-r from-orange-300 to-pink-300 px-5 py-3 text-sm font-semibold text-black"
|
|
|
|
| 621 |
>
|
| 622 |
+
Open on Hugging Face ->
|
| 623 |
+
</a>
|
| 624 |
<a
|
| 625 |
+
href="#"
|
| 626 |
+
className="inline-flex items-center justify-center rounded-md border border-zinc-700 px-4 py-3 font-mono text-sm text-zinc-200"
|
| 627 |
>
|
| 628 |
+
Pull Docker Image
|
| 629 |
</a>
|
| 630 |
</div>
|
| 631 |
|
| 632 |
+
<p className="mt-5 inline-block rounded border border-zinc-800 bg-zinc-950 px-3 py-2 font-mono text-xs text-zinc-600">
|
| 633 |
+
docker://registry-link-placeholder | hf://space-link-placeholder
|
| 634 |
+
</p>
|
| 635 |
+
|
| 636 |
+
<div className="mt-4 flex items-center gap-2 font-mono text-xs text-zinc-500">
|
| 637 |
+
<span
|
| 638 |
+
className={`inline-block h-2 w-2 rounded-full ${
|
| 639 |
+
DEPLOYMENT_LIVE ? "animate-pulse bg-emerald-400" : "bg-amber-400"
|
| 640 |
+
}`}
|
| 641 |
+
/>
|
| 642 |
+
{DEPLOYMENT_LIVE ? "Simulation Online" : "Coming Soon"}
|
| 643 |
</div>
|
| 644 |
+
<p className="mt-2 text-sm text-zinc-500">Links will be updated when the deployment is live.</p>
|
| 645 |
</div>
|
| 646 |
</RevealSection>
|
| 647 |
</main>
|
| 648 |
|
| 649 |
+
<footer className="border-t border-zinc-800 bg-zinc-950/35">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-8 px-5 py-10 text-sm text-zinc-500 sm:px-8 lg:grid-cols-12">
|
| 651 |
+
<div className="lg:col-span-6">
|
| 652 |
<p className="font-mono text-zinc-300">DIME</p>
|
| 653 |
<p className="mt-3 max-w-2xl leading-7">
|
| 654 |
+
Distributed Infrastructure Management Environment is built for the OpenEnv Hackathon to
|
| 655 |
+
benchmark LLM agents against realistic SRE incident-response dynamics.
|
| 656 |
+
</p>
|
| 657 |
+
<p className="mt-3 font-mono text-xs tracking-wide text-zinc-600">
|
| 658 |
+
Co-organized by Meta, PyTorch and Hugging Face
|
| 659 |
</p>
|
| 660 |
</div>
|
| 661 |
|
| 662 |
+
<div className="lg:col-span-3">
|
| 663 |
+
<p className="font-mono text-xs tracking-[0.16em] text-zinc-400">BENCHMARK</p>
|
| 664 |
+
<p className="mt-3 text-zinc-400">Environment: distributed_infra_env</p>
|
| 665 |
+
<p className="mt-2 text-zinc-400">Modeled Tasks: 4 graded incidents</p>
|
| 666 |
+
<p className="mt-2 text-zinc-400">Reward: dense step-level signal</p>
|
| 667 |
+
</div>
|
| 668 |
+
|
| 669 |
+
<div className="lg:col-span-3">
|
| 670 |
<p className="font-mono text-xs tracking-[0.16em] text-zinc-400">RESOURCES</p>
|
| 671 |
<div className="mt-3 flex flex-col gap-2">
|
| 672 |
+
<a href="#" className="font-mono transition-colors hover:text-zinc-300">
|
| 673 |
+
Source Code
|
| 674 |
</a>
|
| 675 |
+
<a href="#" className="font-mono transition-colors hover:text-zinc-300">
|
| 676 |
+
Hugging Face Space
|
| 677 |
</a>
|
| 678 |
+
<a href="#" className="font-mono transition-colors hover:text-zinc-300">
|
| 679 |
+
Docker Image
|
| 680 |
</a>
|
| 681 |
</div>
|
| 682 |
</div>
|
frontend/components/simulation/ClusterSimulation.tsx
CHANGED
|
@@ -1,379 +1,256 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import { motion } from "framer-motion";
|
| 5 |
|
| 6 |
-
|
| 7 |
-
type NodeStatus = "ok" | "warm" | "hot" | "offline";
|
| 8 |
|
| 9 |
-
type
|
| 10 |
-
id: number;
|
| 11 |
label: string;
|
| 12 |
-
role: NodeRole;
|
| 13 |
cpu: number;
|
| 14 |
-
mem: number;
|
| 15 |
queue: number;
|
| 16 |
-
|
|
|
|
| 17 |
};
|
| 18 |
|
| 19 |
-
type
|
| 20 |
-
|
| 21 |
-
nodes: NodeDatum[];
|
| 22 |
-
latencyMs: number;
|
| 23 |
-
requestRate: number;
|
| 24 |
-
action: string;
|
| 25 |
-
stability: number;
|
| 26 |
};
|
| 27 |
|
| 28 |
-
|
| 29 |
-
x:
|
| 30 |
-
y:
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
{ x:
|
| 35 |
-
{ x:
|
| 36 |
-
{ x:
|
| 37 |
-
{ x: 722, y: 318 },
|
| 38 |
-
{ x: 610, y: 466 },
|
| 39 |
-
{ x: 350, y: 466 },
|
| 40 |
-
{ x: 238, y: 318 },
|
| 41 |
-
{ x: 310, y: 150 },
|
| 42 |
-
{ x: 480, y: 176 },
|
| 43 |
-
];
|
| 44 |
-
|
| 45 |
-
const NODE_META: Array<Pick<NodeDatum, "id" | "label" | "role">> = [
|
| 46 |
-
{ id: 0, label: "control-plane", role: "core" },
|
| 47 |
-
{ id: 1, label: "ingress-01", role: "gateway" },
|
| 48 |
-
{ id: 2, label: "worker-02", role: "worker" },
|
| 49 |
-
{ id: 3, label: "worker-03", role: "worker" },
|
| 50 |
-
{ id: 4, label: "cache-04", role: "cache" },
|
| 51 |
-
{ id: 5, label: "worker-05", role: "worker" },
|
| 52 |
-
{ id: 6, label: "worker-06", role: "worker" },
|
| 53 |
-
{ id: 7, label: "ingress-07", role: "gateway" },
|
| 54 |
-
{ id: 8, label: "policy-agent", role: "core" },
|
| 55 |
];
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
accent: "#2dd4bf",
|
| 60 |
-
text: "#ccfbf1",
|
| 61 |
-
fill: "rgba(8, 20, 24, 0.92)",
|
| 62 |
-
border: "rgba(45, 212, 191, 0.28)",
|
| 63 |
-
},
|
| 64 |
-
warm: {
|
| 65 |
-
accent: "#fbbf24",
|
| 66 |
-
text: "#fde68a",
|
| 67 |
-
fill: "rgba(29, 22, 10, 0.94)",
|
| 68 |
-
border: "rgba(251, 191, 36, 0.32)",
|
| 69 |
-
},
|
| 70 |
-
hot: {
|
| 71 |
-
accent: "#fb7185",
|
| 72 |
-
text: "#fecdd3",
|
| 73 |
-
fill: "rgba(36, 15, 20, 0.94)",
|
| 74 |
-
border: "rgba(251, 113, 133, 0.36)",
|
| 75 |
-
},
|
| 76 |
-
offline: {
|
| 77 |
-
accent: "#71717a",
|
| 78 |
-
text: "#d4d4d8",
|
| 79 |
-
fill: "rgba(16, 16, 20, 0.96)",
|
| 80 |
-
border: "rgba(113, 113, 122, 0.26)",
|
| 81 |
-
},
|
| 82 |
-
};
|
| 83 |
-
|
| 84 |
-
function clamp(value: number, min: number, max: number) {
|
| 85 |
-
return Math.max(min, Math.min(max, value));
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
function smoothNoise(seed: number) {
|
| 89 |
-
return Math.sin(seed * 12.9898) * 0.5 + Math.sin(seed * 4.1414) * 0.5;
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
function getStatus(cpu: number, offline: boolean): NodeStatus {
|
| 93 |
-
if (offline) return "offline";
|
| 94 |
-
if (cpu >= 0.82) return "hot";
|
| 95 |
-
if (cpu >= 0.62) return "warm";
|
| 96 |
-
return "ok";
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
function generateSnapshot(step: number): Snapshot {
|
| 100 |
-
const t = step * 0.16;
|
| 101 |
-
const phase = step % 96;
|
| 102 |
-
const surge = phase >= 48 && phase <= 74;
|
| 103 |
-
const recovery = phase > 74;
|
| 104 |
-
|
| 105 |
-
const nodes = NODE_META.map((node) => {
|
| 106 |
-
const wave = Math.sin(t + node.id * 0.72);
|
| 107 |
-
const pulse = Math.max(0, Math.sin((phase - 42) / 12));
|
| 108 |
-
const offline = surge && (node.id === 3 || node.id === 6) && phase > 62;
|
| 109 |
-
const base = node.role === "core" ? 0.38 : node.role === "gateway" ? 0.42 : node.role === "cache" ? 0.33 : 0.31;
|
| 110 |
-
const stress = surge ? 0.24 + pulse * 0.24 : recovery ? 0.16 : 0.08;
|
| 111 |
-
const cpu = offline ? 0 : clamp(base + wave * 0.1 + stress + smoothNoise(step + node.id) * 0.018, 0.07, 0.96);
|
| 112 |
-
const mem = offline ? 0 : clamp(0.34 + Math.sin(t * 0.65 + node.id) * 0.08 + (surge ? 0.12 : 0), 0.16, 0.9);
|
| 113 |
-
const queue = offline ? 0 : Math.round(clamp(8 + cpu * 28 + (surge ? pulse * 18 : 0), 2, 58));
|
| 114 |
-
|
| 115 |
return {
|
| 116 |
-
..
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
status: getStatus(cpu, offline),
|
| 121 |
};
|
| 122 |
-
}
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
return {
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
| 134 |
};
|
| 135 |
}
|
| 136 |
|
| 137 |
-
function
|
| 138 |
-
const
|
| 139 |
-
const midY = (from.y + to.y) / 2;
|
| 140 |
-
const dx = to.x - from.x;
|
| 141 |
-
const dy = to.y - from.y;
|
| 142 |
-
const length = Math.max(1, Math.hypot(dx, dy));
|
| 143 |
-
const cx = midX + (-dy / length) * bend;
|
| 144 |
-
const cy = midY + (dx / length) * bend;
|
| 145 |
|
| 146 |
-
return `M ${from.x} ${from.y} Q ${cx} ${cy} ${to.x} ${to.y}`;
|
| 147 |
-
}
|
| 148 |
-
|
| 149 |
-
function Metric({ label, value, tone = "text-zinc-200" }: { label: string; value: string; tone?: string }) {
|
| 150 |
return (
|
| 151 |
-
<div
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
);
|
| 156 |
-
}
|
| 157 |
|
| 158 |
-
function
|
| 159 |
-
const
|
| 160 |
-
const
|
| 161 |
-
const
|
| 162 |
-
const particleColor = hot ? "#fbbf24" : "#67e8f9";
|
| 163 |
-
const duration = clamp(4.1 - traffic * 2.2, 1.45, 3.8);
|
| 164 |
|
| 165 |
return (
|
| 166 |
-
<
|
| 167 |
-
<
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
</>
|
| 186 |
-
) : null}
|
| 187 |
-
</g>
|
| 188 |
);
|
| 189 |
}
|
| 190 |
|
| 191 |
-
const
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
const width = core ? 132 : 116;
|
| 195 |
-
const height = core ? 74 : 66;
|
| 196 |
-
const radius = core ? 18 : 15;
|
| 197 |
-
const circumference = 2 * Math.PI * radius;
|
| 198 |
-
const dashOffset = circumference * (1 - node.cpu);
|
| 199 |
-
const opacity = node.status === "offline" ? 0.52 : 1;
|
| 200 |
-
|
| 201 |
-
return (
|
| 202 |
-
<motion.g
|
| 203 |
-
initial={false}
|
| 204 |
-
animate={{ x: position.x, y: position.y, opacity }}
|
| 205 |
-
transition={{ type: "spring", stiffness: 80, damping: 18 }}
|
| 206 |
-
>
|
| 207 |
-
{node.status !== "offline" ? (
|
| 208 |
-
<motion.circle
|
| 209 |
-
r={width / 2}
|
| 210 |
-
fill="none"
|
| 211 |
-
stroke={style.accent}
|
| 212 |
-
strokeWidth="0.7"
|
| 213 |
-
animate={{ opacity: [0.1, 0.2, 0.1], scale: [0.96, 1.08, 0.96] }}
|
| 214 |
-
transition={{ duration: node.status === "hot" ? 1.5 : 3.8, repeat: Infinity, ease: "easeInOut" }}
|
| 215 |
-
/>
|
| 216 |
-
) : null}
|
| 217 |
-
|
| 218 |
-
<motion.rect
|
| 219 |
-
x={-width / 2}
|
| 220 |
-
y={-height / 2}
|
| 221 |
-
width={width}
|
| 222 |
-
height={height}
|
| 223 |
-
rx={14}
|
| 224 |
-
fill={style.fill}
|
| 225 |
-
stroke={style.border}
|
| 226 |
-
strokeWidth="1"
|
| 227 |
-
initial={false}
|
| 228 |
-
animate={{
|
| 229 |
-
filter: node.status === "hot" ? "drop-shadow(0 0 16px rgba(251, 113, 133, 0.22))" : "drop-shadow(0 12px 30px rgba(0, 0, 0, 0.24))",
|
| 230 |
-
}}
|
| 231 |
-
transition={{ duration: 0.45 }}
|
| 232 |
-
/>
|
| 233 |
-
|
| 234 |
-
<circle cx={-width / 2 + 29} cy={-7} r={radius} fill="rgba(255,255,255,0.025)" stroke="rgba(255,255,255,0.08)" strokeWidth="3" />
|
| 235 |
-
<motion.circle
|
| 236 |
-
cx={-width / 2 + 29}
|
| 237 |
-
cy={-7}
|
| 238 |
-
r={radius}
|
| 239 |
-
fill="none"
|
| 240 |
-
stroke={style.accent}
|
| 241 |
-
strokeWidth="3"
|
| 242 |
-
strokeLinecap="round"
|
| 243 |
-
strokeDasharray={circumference}
|
| 244 |
-
initial={false}
|
| 245 |
-
animate={{ strokeDashoffset: dashOffset }}
|
| 246 |
-
transition={{ duration: 0.65, ease: "easeOut" }}
|
| 247 |
-
transform={`rotate(-90 ${-width / 2 + 29} -7)`}
|
| 248 |
-
/>
|
| 249 |
-
<text
|
| 250 |
-
x={-width / 2 + 29}
|
| 251 |
-
y={-3}
|
| 252 |
-
textAnchor="middle"
|
| 253 |
-
fill={style.text}
|
| 254 |
-
fontFamily="var(--font-geist-mono), monospace"
|
| 255 |
-
fontSize="10"
|
| 256 |
-
fontWeight="700"
|
| 257 |
-
>
|
| 258 |
-
{node.status === "offline" ? "off" : `${Math.round(node.cpu * 100)}`}
|
| 259 |
-
</text>
|
| 260 |
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
<text x={-width / 2 + 58} y={5} fill="#a1a1aa" fontFamily="var(--font-geist-mono), monospace" fontSize="8.5">
|
| 265 |
-
{node.role.toUpperCase()} / {node.status.toUpperCase()}
|
| 266 |
-
</text>
|
| 267 |
-
<text x={-width / 2 + 58} y={23} fill={style.accent} fontFamily="var(--font-geist-mono), monospace" fontSize="8.5">
|
| 268 |
-
MEM {Math.round(node.mem * 100)}% Q {node.queue}
|
| 269 |
-
</text>
|
| 270 |
-
</motion.g>
|
| 271 |
-
);
|
| 272 |
-
});
|
| 273 |
|
| 274 |
export function ClusterSimulation() {
|
| 275 |
-
const
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
|
| 287 |
-
const
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
|
| 298 |
return (
|
| 299 |
-
<div className="
|
| 300 |
-
<div className="
|
|
|
|
|
|
|
|
|
|
| 301 |
<div className="flex items-center gap-2">
|
| 302 |
-
<
|
| 303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
</div>
|
| 305 |
-
<p className="font-mono text-[10px] uppercase tracking-[0.12em] text-zinc-500">
|
| 306 |
-
step {snapshot.step} / {summary.online} of {snapshot.nodes.length} nodes online
|
| 307 |
-
</p>
|
| 308 |
</div>
|
| 309 |
|
| 310 |
-
<div className="
|
| 311 |
-
<
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
/>
|
| 330 |
-
|
| 331 |
-
<g opacity="0.38">
|
| 332 |
-
<ellipse cx="480" cy="280" rx="310" ry="192" fill="none" stroke="rgba(161,161,170,0.22)" strokeDasharray="6 16" />
|
| 333 |
-
<ellipse cx="480" cy="280" rx="206" ry="128" fill="none" stroke="rgba(45,212,191,0.16)" strokeDasharray="4 14" />
|
| 334 |
-
</g>
|
| 335 |
-
|
| 336 |
-
{snapshot.nodes.slice(1, 8).map((node, index) => (
|
| 337 |
-
<Edge
|
| 338 |
-
key={`edge-core-${node.id}`}
|
| 339 |
-
from={POSITIONS[node.id]}
|
| 340 |
-
to={POSITIONS[0]}
|
| 341 |
-
traffic={node.cpu}
|
| 342 |
-
muted={node.status === "offline"}
|
| 343 |
-
index={index}
|
| 344 |
-
/>
|
| 345 |
-
))}
|
| 346 |
-
{[1, 2, 3, 4, 5, 6, 7].map((id, index, ids) => {
|
| 347 |
-
const nextId = ids[(index + 1) % ids.length];
|
| 348 |
-
const node = snapshot.nodes[id];
|
| 349 |
-
const nextNode = snapshot.nodes[nextId];
|
| 350 |
-
|
| 351 |
-
return (
|
| 352 |
-
<Edge
|
| 353 |
-
key={`edge-ring-${id}-${nextId}`}
|
| 354 |
-
from={POSITIONS[id]}
|
| 355 |
-
to={POSITIONS[nextId]}
|
| 356 |
-
traffic={(node.cpu + nextNode.cpu) / 2}
|
| 357 |
-
muted={node.status === "offline" || nextNode.status === "offline"}
|
| 358 |
-
index={index + 7}
|
| 359 |
-
/>
|
| 360 |
-
);
|
| 361 |
-
})}
|
| 362 |
-
<Edge from={POSITIONS[8]} to={POSITIONS[0]} traffic={snapshot.stability} muted={false} index={18} />
|
| 363 |
-
|
| 364 |
-
{snapshot.nodes.map((node) => (
|
| 365 |
-
<NodeCard key={node.id} node={node} position={POSITIONS[node.id]} />
|
| 366 |
-
))}
|
| 367 |
-
</svg>
|
| 368 |
</div>
|
| 369 |
|
| 370 |
-
<div className="grid grid-cols-2 gap-
|
| 371 |
-
<
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
<
|
| 375 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
</div>
|
|
|
|
|
|
|
| 377 |
</div>
|
| 378 |
);
|
| 379 |
}
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
+
import "reactflow/dist/style.css";
|
| 4 |
+
|
| 5 |
+
import { memo, useMemo } from "react";
|
| 6 |
+
import ReactFlow, {
|
| 7 |
+
Background,
|
| 8 |
+
BaseEdge,
|
| 9 |
+
Controls,
|
| 10 |
+
EdgeLabelRenderer,
|
| 11 |
+
MarkerType,
|
| 12 |
+
Position,
|
| 13 |
+
getBezierPath,
|
| 14 |
+
type Edge,
|
| 15 |
+
type EdgeProps,
|
| 16 |
+
type Node,
|
| 17 |
+
type NodeProps,
|
| 18 |
+
type NodeTypes,
|
| 19 |
+
} from "reactflow";
|
| 20 |
import { motion } from "framer-motion";
|
| 21 |
|
| 22 |
+
import { useSimulationSocket } from "@/lib/useSimulationSocket";
|
|
|
|
| 23 |
|
| 24 |
+
type InfraNodeData = {
|
|
|
|
| 25 |
label: string;
|
|
|
|
| 26 |
cpu: number;
|
|
|
|
| 27 |
queue: number;
|
| 28 |
+
failed: boolean;
|
| 29 |
+
role: "db" | "worker";
|
| 30 |
};
|
| 31 |
|
| 32 |
+
type TrafficEdgeData = {
|
| 33 |
+
traffic: number;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
};
|
| 35 |
|
| 36 |
+
const NODE_POSITIONS: Array<{ x: number; y: number }> = [
|
| 37 |
+
{ x: 380, y: 260 },
|
| 38 |
+
{ x: 110, y: 70 },
|
| 39 |
+
{ x: 270, y: 40 },
|
| 40 |
+
{ x: 500, y: 40 },
|
| 41 |
+
{ x: 650, y: 90 },
|
| 42 |
+
{ x: 660, y: 290 },
|
| 43 |
+
{ x: 510, y: 430 },
|
| 44 |
+
{ x: 260, y: 430 },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
];
|
| 46 |
|
| 47 |
+
function nodeStyle(cpu: number, failed: boolean, role: "db" | "worker") {
|
| 48 |
+
if (failed) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
return {
|
| 50 |
+
border: "1.8px solid rgba(248, 113, 113, 0.9)",
|
| 51 |
+
boxShadow: "0 0 0 rgba(0,0,0,0)",
|
| 52 |
+
background: "rgba(28, 28, 33, 0.95)",
|
| 53 |
+
color: "rgba(228, 228, 231, 0.9)",
|
|
|
|
| 54 |
};
|
| 55 |
+
}
|
| 56 |
|
| 57 |
+
if (cpu >= 0.85) {
|
| 58 |
+
return {
|
| 59 |
+
border: "1.8px solid rgba(251, 191, 36, 0.95)",
|
| 60 |
+
boxShadow: "0 0 28px rgba(251, 191, 36, 0.38)",
|
| 61 |
+
background: "rgba(39, 30, 10, 0.92)",
|
| 62 |
+
color: "rgba(255, 251, 235, 0.95)",
|
| 63 |
+
};
|
| 64 |
+
}
|
| 65 |
|
| 66 |
return {
|
| 67 |
+
border: role === "db" ? "1.8px solid rgba(125, 211, 252, 0.95)" : "1.6px solid rgba(45, 212, 191, 0.9)",
|
| 68 |
+
boxShadow:
|
| 69 |
+
role === "db"
|
| 70 |
+
? "0 0 24px rgba(125, 211, 252, 0.25)"
|
| 71 |
+
: "0 0 20px rgba(45, 212, 191, 0.24)",
|
| 72 |
+
background: role === "db" ? "rgba(13, 33, 48, 0.9)" : "rgba(9, 33, 30, 0.9)",
|
| 73 |
+
color: "rgba(240, 253, 250, 0.95)",
|
| 74 |
};
|
| 75 |
}
|
| 76 |
|
| 77 |
+
const InfraNode = memo(function InfraNode({ data }: NodeProps<InfraNodeData>) {
|
| 78 |
+
const style = nodeStyle(data.cpu, data.failed, data.role);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
return (
|
| 81 |
+
<motion.div
|
| 82 |
+
animate={{
|
| 83 |
+
scale: data.failed ? 0.97 : 1,
|
| 84 |
+
opacity: data.failed ? 0.7 : 1,
|
| 85 |
+
}}
|
| 86 |
+
transition={{ duration: 0.35, ease: "easeOut" }}
|
| 87 |
+
className="w-44 rounded-2xl px-4 py-3 backdrop-blur"
|
| 88 |
+
style={style}
|
| 89 |
+
>
|
| 90 |
+
<div className="flex items-center justify-between text-[11px] uppercase tracking-[0.18em]">
|
| 91 |
+
<span>{data.label}</span>
|
| 92 |
+
<span>{data.role === "db" ? "DB" : "APP"}</span>
|
| 93 |
+
</div>
|
| 94 |
+
<div className="mt-3 space-y-1.5 font-mono text-xs">
|
| 95 |
+
<p>CPU {Math.max(0, data.cpu * 100).toFixed(0)}%</p>
|
| 96 |
+
<p>Queue {Math.max(0, data.queue)}</p>
|
| 97 |
+
</div>
|
| 98 |
+
</motion.div>
|
| 99 |
);
|
| 100 |
+
});
|
| 101 |
|
| 102 |
+
function TrafficEdge(props: EdgeProps<TrafficEdgeData>) {
|
| 103 |
+
const [edgePath, labelX, labelY] = getBezierPath(props);
|
| 104 |
+
const traffic = Math.max(0.2, props.data?.traffic ?? 0.2);
|
| 105 |
+
const duration = Number((2.5 / Math.min(2.5, Math.max(0.25, traffic))).toFixed(2));
|
|
|
|
|
|
|
| 106 |
|
| 107 |
return (
|
| 108 |
+
<>
|
| 109 |
+
<BaseEdge path={edgePath} style={{ stroke: "rgba(148, 163, 184, 0.5)", strokeWidth: 1.4 }} />
|
| 110 |
+
<path id={props.id} d={edgePath} fill="none" stroke="transparent" />
|
| 111 |
+
<circle r="2.8" fill="rgba(125, 211, 252, 0.95)">
|
| 112 |
+
<animateMotion dur={`${duration}s`} repeatCount="indefinite" path={edgePath} />
|
| 113 |
+
</circle>
|
| 114 |
+
<EdgeLabelRenderer>
|
| 115 |
+
<div
|
| 116 |
+
style={{
|
| 117 |
+
position: "absolute",
|
| 118 |
+
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
| 119 |
+
pointerEvents: "none",
|
| 120 |
+
}}
|
| 121 |
+
className="rounded bg-black/65 px-1.5 py-0.5 font-mono text-[10px] text-sky-200"
|
| 122 |
+
>
|
| 123 |
+
{(traffic * 100).toFixed(0)}%
|
| 124 |
+
</div>
|
| 125 |
+
</EdgeLabelRenderer>
|
| 126 |
+
</>
|
|
|
|
|
|
|
|
|
|
| 127 |
);
|
| 128 |
}
|
| 129 |
|
| 130 |
+
const nodeTypes: NodeTypes = {
|
| 131 |
+
infra: InfraNode,
|
| 132 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
+
const edgeTypes = {
|
| 135 |
+
traffic: TrafficEdge,
|
| 136 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
export function ClusterSimulation() {
|
| 139 |
+
const { packet, connected, error, sendIntervention } = useSimulationSocket();
|
| 140 |
+
|
| 141 |
+
const observation = packet?.observation;
|
| 142 |
+
const simulationState = useMemo(
|
| 143 |
+
() => ({
|
| 144 |
+
cpu: observation?.cpu_loads ?? [0.2, 0.3, 0.26, 0.4, 0.32, 0.28, 0.35, 0.31],
|
| 145 |
+
queue: observation?.queue_lengths ?? [2, 6, 4, 9, 5, 7, 3, 2],
|
| 146 |
+
failed: new Set(observation?.failed_nodes ?? []),
|
| 147 |
+
}),
|
| 148 |
+
[observation]
|
| 149 |
+
);
|
| 150 |
|
| 151 |
+
const nodes = useMemo<Node<InfraNodeData>[]>(() => {
|
| 152 |
+
return NODE_POSITIONS.map((position, idx) => ({
|
| 153 |
+
id: `${idx}`,
|
| 154 |
+
type: "infra",
|
| 155 |
+
position,
|
| 156 |
+
sourcePosition: Position.Right,
|
| 157 |
+
targetPosition: Position.Left,
|
| 158 |
+
data: {
|
| 159 |
+
label: idx === 0 ? "Node 0" : `Node ${idx}`,
|
| 160 |
+
cpu: simulationState.cpu[idx] ?? 0,
|
| 161 |
+
queue: simulationState.queue[idx] ?? 0,
|
| 162 |
+
failed: simulationState.failed.has(idx),
|
| 163 |
+
role: idx === 0 ? "db" : "worker",
|
| 164 |
+
},
|
| 165 |
+
draggable: false,
|
| 166 |
+
selectable: false,
|
| 167 |
+
}));
|
| 168 |
+
}, [simulationState]);
|
| 169 |
+
|
| 170 |
+
const edges = useMemo<Edge<TrafficEdgeData>[]>(() => {
|
| 171 |
+
const workerNodes = [1, 2, 3, 4, 5, 6, 7];
|
| 172 |
+
return workerNodes.map((worker) => {
|
| 173 |
+
const workerCpu = Math.max(0.2, simulationState.cpu[worker] ?? 0.2);
|
| 174 |
+
return {
|
| 175 |
+
id: `e-${worker}-0`,
|
| 176 |
+
source: `${worker}`,
|
| 177 |
+
target: "0",
|
| 178 |
+
type: "traffic",
|
| 179 |
+
markerEnd: {
|
| 180 |
+
type: MarkerType.ArrowClosed,
|
| 181 |
+
color: "rgba(125, 211, 252, 0.7)",
|
| 182 |
+
},
|
| 183 |
+
style: { stroke: "rgba(125, 211, 252, 0.5)", strokeWidth: 1.4 },
|
| 184 |
+
data: { traffic: workerCpu },
|
| 185 |
+
};
|
| 186 |
+
});
|
| 187 |
+
}, [simulationState]);
|
| 188 |
|
| 189 |
return (
|
| 190 |
+
<div className="rounded-2xl border border-zinc-800 bg-[#0e1117] p-4 shadow-[0_0_45px_rgba(56,189,248,0.08)] sm:p-6">
|
| 191 |
+
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
| 192 |
+
<div className="font-mono text-xs uppercase tracking-[0.18em] text-zinc-400">
|
| 193 |
+
{connected ? "Live simulation stream" : "Reconnecting simulation stream"}
|
| 194 |
+
</div>
|
| 195 |
<div className="flex items-center gap-2">
|
| 196 |
+
<button
|
| 197 |
+
type="button"
|
| 198 |
+
onClick={() => sendIntervention("kubectl throttle ingress --rate=0.35")}
|
| 199 |
+
className="rounded-md border border-sky-300/40 bg-sky-400/10 px-3 py-1.5 font-mono text-xs text-sky-100 transition hover:bg-sky-400/20"
|
| 200 |
+
>
|
| 201 |
+
Apply Throttle
|
| 202 |
+
</button>
|
| 203 |
+
<button
|
| 204 |
+
type="button"
|
| 205 |
+
onClick={() => sendIntervention("kubectl rollout restart node-3")}
|
| 206 |
+
className="rounded-md border border-amber-300/40 bg-amber-400/10 px-3 py-1.5 font-mono text-xs text-amber-100 transition hover:bg-amber-400/20"
|
| 207 |
+
>
|
| 208 |
+
Restart Node 3
|
| 209 |
+
</button>
|
| 210 |
</div>
|
|
|
|
|
|
|
|
|
|
| 211 |
</div>
|
| 212 |
|
| 213 |
+
<div className="h-[30rem] w-full overflow-hidden rounded-xl border border-zinc-800 bg-[radial-gradient(circle_at_30%_20%,rgba(56,189,248,0.08),transparent_35%),radial-gradient(circle_at_80%_80%,rgba(45,212,191,0.08),transparent_35%)]">
|
| 214 |
+
<ReactFlow
|
| 215 |
+
nodes={nodes}
|
| 216 |
+
edges={edges}
|
| 217 |
+
nodeTypes={nodeTypes}
|
| 218 |
+
edgeTypes={edgeTypes}
|
| 219 |
+
minZoom={0.55}
|
| 220 |
+
maxZoom={1.4}
|
| 221 |
+
fitView
|
| 222 |
+
fitViewOptions={{ padding: 0.08 }}
|
| 223 |
+
proOptions={{ hideAttribution: true }}
|
| 224 |
+
nodesDraggable={false}
|
| 225 |
+
nodesConnectable={false}
|
| 226 |
+
elementsSelectable={false}
|
| 227 |
+
panOnDrag={false}
|
| 228 |
+
>
|
| 229 |
+
<Background color="rgba(148, 163, 184, 0.08)" gap={24} />
|
| 230 |
+
<Controls showInteractive={false} className="!bg-black/60 !text-zinc-100" />
|
| 231 |
+
</ReactFlow>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
</div>
|
| 233 |
|
| 234 |
+
<div className="mt-4 grid grid-cols-2 gap-3 text-sm text-zinc-300 sm:grid-cols-4">
|
| 235 |
+
<div className="rounded-lg border border-zinc-800 bg-black/35 p-3">
|
| 236 |
+
<p className="font-mono text-[11px] uppercase tracking-[0.16em] text-zinc-500">Step</p>
|
| 237 |
+
<p className="mt-1 text-lg font-semibold text-white">{observation?.step ?? 0}</p>
|
| 238 |
+
</div>
|
| 239 |
+
<div className="rounded-lg border border-zinc-800 bg-black/35 p-3">
|
| 240 |
+
<p className="font-mono text-[11px] uppercase tracking-[0.16em] text-zinc-500">Latency</p>
|
| 241 |
+
<p className="mt-1 text-lg font-semibold text-white">{(observation?.latency_ms ?? 0).toFixed(1)}ms</p>
|
| 242 |
+
</div>
|
| 243 |
+
<div className="rounded-lg border border-zinc-800 bg-black/35 p-3">
|
| 244 |
+
<p className="font-mono text-[11px] uppercase tracking-[0.16em] text-zinc-500">Request Rate</p>
|
| 245 |
+
<p className="mt-1 text-lg font-semibold text-white">{(observation?.request_rate ?? 0).toFixed(0)} req/s</p>
|
| 246 |
+
</div>
|
| 247 |
+
<div className="rounded-lg border border-zinc-800 bg-black/35 p-3">
|
| 248 |
+
<p className="font-mono text-[11px] uppercase tracking-[0.16em] text-zinc-500">Agent Action</p>
|
| 249 |
+
<p className="mt-1 truncate text-sm font-semibold text-white">{packet?.intervention ?? packet?.last_action_type ?? "no_op"}</p>
|
| 250 |
+
</div>
|
| 251 |
</div>
|
| 252 |
+
|
| 253 |
+
{error ? <p className="mt-3 font-mono text-xs text-rose-300">socket warning: {error}</p> : null}
|
| 254 |
</div>
|
| 255 |
);
|
| 256 |
}
|
frontend/components/ui/FlipWords.tsx
CHANGED
|
@@ -15,18 +15,8 @@ export function FlipWords({ words, className, duration = 2500 }: FlipWordsProps)
|
|
| 15 |
[words]
|
| 16 |
);
|
| 17 |
const [activeIndex, setActiveIndex] = useState(0);
|
| 18 |
-
const [reduceMotion, setReduceMotion] = useState(false);
|
| 19 |
|
| 20 |
useEffect(() => {
|
| 21 |
-
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
|
| 22 |
-
const sync = () => setReduceMotion(media.matches);
|
| 23 |
-
sync();
|
| 24 |
-
media.addEventListener("change", sync);
|
| 25 |
-
return () => media.removeEventListener("change", sync);
|
| 26 |
-
}, []);
|
| 27 |
-
|
| 28 |
-
useEffect(() => {
|
| 29 |
-
if (reduceMotion) return;
|
| 30 |
if (normalizedWords.length <= 1) return;
|
| 31 |
|
| 32 |
const interval = window.setInterval(() => {
|
|
@@ -34,26 +24,20 @@ export function FlipWords({ words, className, duration = 2500 }: FlipWordsProps)
|
|
| 34 |
}, Math.max(400, duration));
|
| 35 |
|
| 36 |
return () => window.clearInterval(interval);
|
| 37 |
-
}, [duration, normalizedWords
|
| 38 |
|
| 39 |
if (!normalizedWords.length) {
|
| 40 |
return null;
|
| 41 |
}
|
| 42 |
|
| 43 |
const visibleIndex = activeIndex % normalizedWords.length;
|
| 44 |
-
const staticWord = normalizedWords[0];
|
| 45 |
|
| 46 |
return (
|
| 47 |
<span className={cn("inline-flex items-center", className)}>
|
| 48 |
<span aria-live="polite" aria-atomic="true" className="sr-only">
|
| 49 |
-
{
|
| 50 |
</span>
|
| 51 |
-
|
| 52 |
-
<span aria-hidden="true" className="leading-[1.2]">
|
| 53 |
-
{staticWord}
|
| 54 |
-
</span>
|
| 55 |
-
) : (
|
| 56 |
-
<span aria-hidden="true" className="relative inline-flex h-[1.2em] overflow-hidden">
|
| 57 |
<span
|
| 58 |
className="flex flex-col transition-transform duration-500 ease-out will-change-transform"
|
| 59 |
style={{ transform: `translateY(-${visibleIndex * 100}%)` }}
|
|
@@ -70,8 +54,7 @@ export function FlipWords({ words, className, duration = 2500 }: FlipWordsProps)
|
|
| 70 |
</span>
|
| 71 |
))}
|
| 72 |
</span>
|
| 73 |
-
|
| 74 |
-
)}
|
| 75 |
</span>
|
| 76 |
);
|
| 77 |
}
|
|
|
|
| 15 |
[words]
|
| 16 |
);
|
| 17 |
const [activeIndex, setActiveIndex] = useState(0);
|
|
|
|
| 18 |
|
| 19 |
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
if (normalizedWords.length <= 1) return;
|
| 21 |
|
| 22 |
const interval = window.setInterval(() => {
|
|
|
|
| 24 |
}, Math.max(400, duration));
|
| 25 |
|
| 26 |
return () => window.clearInterval(interval);
|
| 27 |
+
}, [duration, normalizedWords]);
|
| 28 |
|
| 29 |
if (!normalizedWords.length) {
|
| 30 |
return null;
|
| 31 |
}
|
| 32 |
|
| 33 |
const visibleIndex = activeIndex % normalizedWords.length;
|
|
|
|
| 34 |
|
| 35 |
return (
|
| 36 |
<span className={cn("inline-flex items-center", className)}>
|
| 37 |
<span aria-live="polite" aria-atomic="true" className="sr-only">
|
| 38 |
+
{normalizedWords[visibleIndex]}
|
| 39 |
</span>
|
| 40 |
+
<span aria-hidden="true" className="relative inline-flex h-[1.2em] overflow-hidden">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
<span
|
| 42 |
className="flex flex-col transition-transform duration-500 ease-out will-change-transform"
|
| 43 |
style={{ transform: `translateY(-${visibleIndex * 100}%)` }}
|
|
|
|
| 54 |
</span>
|
| 55 |
))}
|
| 56 |
</span>
|
| 57 |
+
</span>
|
|
|
|
| 58 |
</span>
|
| 59 |
);
|
| 60 |
}
|
frontend/components/ui/FocusCards.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
-
import Image from "next/image";
|
| 4 |
import { useState } from "react";
|
| 5 |
import { cn } from "./cn";
|
| 6 |
import type { FocusCard } from "./types";
|
|
@@ -35,11 +34,9 @@ export function FocusCards({ cards, className }: FocusCardsProps) {
|
|
| 35 |
blurred ? "opacity-60" : "opacity-100"
|
| 36 |
)}
|
| 37 |
>
|
| 38 |
-
<
|
| 39 |
src={card.src}
|
| 40 |
alt={card.title}
|
| 41 |
-
fill
|
| 42 |
-
sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
|
| 43 |
className={cn(
|
| 44 |
"h-full w-full object-cover transition-transform duration-500",
|
| 45 |
focused ? "scale-110" : "scale-100"
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
|
|
|
| 3 |
import { useState } from "react";
|
| 4 |
import { cn } from "./cn";
|
| 5 |
import type { FocusCard } from "./types";
|
|
|
|
| 34 |
blurred ? "opacity-60" : "opacity-100"
|
| 35 |
)}
|
| 36 |
>
|
| 37 |
+
<img
|
| 38 |
src={card.src}
|
| 39 |
alt={card.title}
|
|
|
|
|
|
|
| 40 |
className={cn(
|
| 41 |
"h-full w-full object-cover transition-transform duration-500",
|
| 42 |
focused ? "scale-110" : "scale-100"
|
frontend/components/ui/Terminal.tsx
CHANGED
|
@@ -22,7 +22,7 @@ export function Terminal({
|
|
| 22 |
commands,
|
| 23 |
outputs,
|
| 24 |
typingSpeed = 26,
|
| 25 |
-
delayBetweenCommands =
|
| 26 |
className,
|
| 27 |
}: TerminalProps) {
|
| 28 |
const [lines, setLines] = useState<RenderedLine[]>([]);
|
|
@@ -41,39 +41,33 @@ export function Terminal({
|
|
| 41 |
}, [commands, outputs]);
|
| 42 |
|
| 43 |
useEffect(() => {
|
| 44 |
-
let
|
| 45 |
|
| 46 |
const run = async () => {
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
setActiveLine(null);
|
| 50 |
-
|
| 51 |
-
for (let i = 0; i < script.length; i += 1) {
|
| 52 |
-
const line = script[i];
|
| 53 |
-
const value = line.text ?? "";
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
await sleep(typingSpeed);
|
| 59 |
-
}
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
setActiveLine(
|
| 64 |
-
await sleep(
|
| 65 |
}
|
| 66 |
|
| 67 |
-
if (
|
|
|
|
| 68 |
setActiveLine(null);
|
| 69 |
-
await sleep(
|
| 70 |
}
|
| 71 |
};
|
| 72 |
|
| 73 |
run();
|
| 74 |
|
| 75 |
return () => {
|
| 76 |
-
|
| 77 |
};
|
| 78 |
}, [script, typingSpeed, delayBetweenCommands]);
|
| 79 |
|
|
@@ -89,7 +83,7 @@ export function Terminal({
|
|
| 89 |
>
|
| 90 |
{isCommand ? "$ " : ""}
|
| 91 |
{line.text}
|
| 92 |
-
{active ? <span className="ml-0.5 inline-block animate-pulse
|
| 93 |
</div>
|
| 94 |
);
|
| 95 |
};
|
|
|
|
| 22 |
commands,
|
| 23 |
outputs,
|
| 24 |
typingSpeed = 26,
|
| 25 |
+
delayBetweenCommands = 450,
|
| 26 |
className,
|
| 27 |
}: TerminalProps) {
|
| 28 |
const [lines, setLines] = useState<RenderedLine[]>([]);
|
|
|
|
| 41 |
}, [commands, outputs]);
|
| 42 |
|
| 43 |
useEffect(() => {
|
| 44 |
+
let cancelled = false;
|
| 45 |
|
| 46 |
const run = async () => {
|
| 47 |
+
setLines([]);
|
| 48 |
+
setActiveLine(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
+
for (let i = 0; i < script.length; i += 1) {
|
| 51 |
+
const line = script[i];
|
| 52 |
+
const value = line.text ?? "";
|
|
|
|
|
|
|
| 53 |
|
| 54 |
+
for (let c = 0; c <= value.length; c += 1) {
|
| 55 |
+
if (cancelled) return;
|
| 56 |
+
setActiveLine({ type: line.type, text: value.slice(0, c) });
|
| 57 |
+
await sleep(typingSpeed);
|
| 58 |
}
|
| 59 |
|
| 60 |
+
if (cancelled) return;
|
| 61 |
+
setLines((prev) => [...prev, line]);
|
| 62 |
setActiveLine(null);
|
| 63 |
+
await sleep(delayBetweenCommands);
|
| 64 |
}
|
| 65 |
};
|
| 66 |
|
| 67 |
run();
|
| 68 |
|
| 69 |
return () => {
|
| 70 |
+
cancelled = true;
|
| 71 |
};
|
| 72 |
}, [script, typingSpeed, delayBetweenCommands]);
|
| 73 |
|
|
|
|
| 83 |
>
|
| 84 |
{isCommand ? "$ " : ""}
|
| 85 |
{line.text}
|
| 86 |
+
{active ? <span className="ml-0.5 inline-block h-4 w-2 animate-pulse bg-white/90 align-middle" /> : null}
|
| 87 |
</div>
|
| 88 |
);
|
| 89 |
};
|
frontend/components/ui/TextGenerateEffect.tsx
CHANGED
|
@@ -18,21 +18,9 @@ export function TextGenerateEffect({
|
|
| 18 |
}: TextGenerateEffectProps) {
|
| 19 |
const tokens = useMemo(() => words.trim().split(/\s+/).filter(Boolean), [words]);
|
| 20 |
const [visibleCount, setVisibleCount] = useState(0);
|
| 21 |
-
const [reduceMotion, setReduceMotion] = useState(false);
|
| 22 |
|
| 23 |
useEffect(() => {
|
| 24 |
-
|
| 25 |
-
const sync = () => setReduceMotion(media.matches);
|
| 26 |
-
const timer = window.setTimeout(sync, 0);
|
| 27 |
-
media.addEventListener("change", sync);
|
| 28 |
-
return () => {
|
| 29 |
-
window.clearTimeout(timer);
|
| 30 |
-
media.removeEventListener("change", sync);
|
| 31 |
-
};
|
| 32 |
-
}, []);
|
| 33 |
-
|
| 34 |
-
useEffect(() => {
|
| 35 |
-
if (!tokens.length || reduceMotion) return;
|
| 36 |
|
| 37 |
const perWordDelay = Math.max(35, Math.floor(duration / tokens.length));
|
| 38 |
const timer = window.setInterval(() => {
|
|
@@ -46,14 +34,12 @@ export function TextGenerateEffect({
|
|
| 46 |
}, perWordDelay);
|
| 47 |
|
| 48 |
return () => window.clearInterval(timer);
|
| 49 |
-
}, [tokens, duration
|
| 50 |
-
|
| 51 |
-
const renderedCount = reduceMotion ? tokens.length : visibleCount;
|
| 52 |
|
| 53 |
return (
|
| 54 |
<p className={cn("flex flex-wrap text-zinc-100", className)}>
|
| 55 |
{tokens.map((word, index) => {
|
| 56 |
-
const visible = index <
|
| 57 |
return (
|
| 58 |
<span
|
| 59 |
key={`${word}-${index}`}
|
|
|
|
| 18 |
}: TextGenerateEffectProps) {
|
| 19 |
const tokens = useMemo(() => words.trim().split(/\s+/).filter(Boolean), [words]);
|
| 20 |
const [visibleCount, setVisibleCount] = useState(0);
|
|
|
|
| 21 |
|
| 22 |
useEffect(() => {
|
| 23 |
+
if (!tokens.length) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
const perWordDelay = Math.max(35, Math.floor(duration / tokens.length));
|
| 26 |
const timer = window.setInterval(() => {
|
|
|
|
| 34 |
}, perWordDelay);
|
| 35 |
|
| 36 |
return () => window.clearInterval(timer);
|
| 37 |
+
}, [tokens, duration]);
|
|
|
|
|
|
|
| 38 |
|
| 39 |
return (
|
| 40 |
<p className={cn("flex flex-wrap text-zinc-100", className)}>
|
| 41 |
{tokens.map((word, index) => {
|
| 42 |
+
const visible = index < visibleCount;
|
| 43 |
return (
|
| 44 |
<span
|
| 45 |
key={`${word}-${index}`}
|
frontend/lib/telemetry.ts
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from "node:fs/promises";
|
| 2 |
+
import path from "node:path";
|
| 3 |
+
|
| 4 |
+
export type TelemetrySnapshot = {
|
| 5 |
+
step: number;
|
| 6 |
+
cpuLoads: number[];
|
| 7 |
+
queueLengths: number[];
|
| 8 |
+
latencyMs: number;
|
| 9 |
+
failedNodes: number[];
|
| 10 |
+
requestRate: number;
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
type MetricsCsvRow = {
|
| 14 |
+
model: string;
|
| 15 |
+
taskId: string;
|
| 16 |
+
step: number;
|
| 17 |
+
actionTaken: string;
|
| 18 |
+
reasoning: string;
|
| 19 |
+
reward: number;
|
| 20 |
+
cumulativeScore: number;
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
type CsvCache = {
|
| 24 |
+
mtimeMs: number;
|
| 25 |
+
snapshots: TelemetrySnapshot[];
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
let cache: CsvCache | null = null;
|
| 29 |
+
|
| 30 |
+
const DEFAULT_CSV_PATH = path.resolve(process.cwd(), "..", "metrics_Qwen_Qwen3-8B.csv");
|
| 31 |
+
const CSV_PATH = process.env.TELEMETRY_CSV_PATH ?? DEFAULT_CSV_PATH;
|
| 32 |
+
|
| 33 |
+
const HEADER_COLUMNS = [
|
| 34 |
+
"model",
|
| 35 |
+
"task_id",
|
| 36 |
+
"step",
|
| 37 |
+
"action_taken",
|
| 38 |
+
"reasoning",
|
| 39 |
+
"reward",
|
| 40 |
+
"cumulative_score",
|
| 41 |
+
"done",
|
| 42 |
+
"error",
|
| 43 |
+
];
|
| 44 |
+
|
| 45 |
+
export async function getTelemetrySnapshots(): Promise<TelemetrySnapshot[]> {
|
| 46 |
+
const stat = await fs.stat(CSV_PATH);
|
| 47 |
+
if (cache && cache.mtimeMs === stat.mtimeMs) {
|
| 48 |
+
return cache.snapshots;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
const csvText = await fs.readFile(CSV_PATH, "utf-8");
|
| 52 |
+
const rows = parseCsv(csvText);
|
| 53 |
+
const normalizedRows = normalizeRows(rows);
|
| 54 |
+
|
| 55 |
+
const snapshots = normalizedRows
|
| 56 |
+
.map((row, index) => toSnapshot(row, index))
|
| 57 |
+
.filter((snapshot): snapshot is TelemetrySnapshot => snapshot !== null);
|
| 58 |
+
|
| 59 |
+
cache = { mtimeMs: stat.mtimeMs, snapshots };
|
| 60 |
+
return snapshots;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function normalizeRows(rows: string[][]): MetricsCsvRow[] {
|
| 64 |
+
if (rows.length === 0) return [];
|
| 65 |
+
|
| 66 |
+
const startAt = isHeaderRow(rows[0]) ? 1 : 0;
|
| 67 |
+
const normalized: MetricsCsvRow[] = [];
|
| 68 |
+
|
| 69 |
+
for (let i = startAt; i < rows.length; i += 1) {
|
| 70 |
+
const row = rows[i];
|
| 71 |
+
if (row.length < 7) continue;
|
| 72 |
+
|
| 73 |
+
const step = toFiniteNumber(row[2]);
|
| 74 |
+
const reward = toFiniteNumber(row[5]);
|
| 75 |
+
const cumulativeScore = toFiniteNumber(row[6]);
|
| 76 |
+
|
| 77 |
+
normalized.push({
|
| 78 |
+
model: row[0] ?? "",
|
| 79 |
+
taskId: row[1] ?? "",
|
| 80 |
+
step: step ?? i - startAt + 1,
|
| 81 |
+
actionTaken: row[3] ?? "",
|
| 82 |
+
reasoning: row[4] ?? "",
|
| 83 |
+
reward: reward ?? 0,
|
| 84 |
+
cumulativeScore: cumulativeScore ?? 0,
|
| 85 |
+
});
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
return normalized;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
function toSnapshot(row: MetricsCsvRow, index: number): TelemetrySnapshot | null {
|
| 92 |
+
const cpuParsed = extractCpuLoads(row.reasoning);
|
| 93 |
+
const failedParsed = extractFailedNodes(row.reasoning);
|
| 94 |
+
|
| 95 |
+
const cpuLoads = normalizeCpuLoads(cpuParsed ?? deriveCpuLoads(row, index), row, index);
|
| 96 |
+
const queueLengths = normalizeQueueLengths(
|
| 97 |
+
extractQueueLengths(row.reasoning) ?? deriveQueueLengths(cpuLoads, row, index),
|
| 98 |
+
cpuLoads,
|
| 99 |
+
row,
|
| 100 |
+
index
|
| 101 |
+
);
|
| 102 |
+
|
| 103 |
+
const latencyMs =
|
| 104 |
+
clamp(round1(extractLatencyMs(row.reasoning) ?? deriveLatencyMs(cpuLoads, queueLengths, row)), 0, 2000) ||
|
| 105 |
+
0;
|
| 106 |
+
|
| 107 |
+
const failedNodes = normalizeFailedNodes(
|
| 108 |
+
failedParsed ?? deriveFailedNodes(cpuLoads, row),
|
| 109 |
+
cpuLoads.length
|
| 110 |
+
);
|
| 111 |
+
|
| 112 |
+
const requestRate =
|
| 113 |
+
clampInt(
|
| 114 |
+
Math.round(extractRequestRate(row.reasoning) ?? deriveRequestRate(cpuLoads, queueLengths, row)),
|
| 115 |
+
0,
|
| 116 |
+
100_000
|
| 117 |
+
) || 0;
|
| 118 |
+
|
| 119 |
+
if (cpuLoads.length === 0 || queueLengths.length === 0) {
|
| 120 |
+
return null;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
return {
|
| 124 |
+
step: Number.isFinite(row.step) ? Math.max(0, Math.trunc(row.step)) : index + 1,
|
| 125 |
+
cpuLoads,
|
| 126 |
+
queueLengths,
|
| 127 |
+
latencyMs,
|
| 128 |
+
failedNodes,
|
| 129 |
+
requestRate,
|
| 130 |
+
};
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
function parseCsv(input: string): string[][] {
|
| 134 |
+
const rows: string[][] = [];
|
| 135 |
+
let row: string[] = [];
|
| 136 |
+
let field = "";
|
| 137 |
+
let inQuotes = false;
|
| 138 |
+
|
| 139 |
+
for (let i = 0; i < input.length; i += 1) {
|
| 140 |
+
const char = input[i];
|
| 141 |
+
|
| 142 |
+
if (inQuotes) {
|
| 143 |
+
if (char === '"') {
|
| 144 |
+
const next = input[i + 1];
|
| 145 |
+
if (next === '"') {
|
| 146 |
+
field += '"';
|
| 147 |
+
i += 1;
|
| 148 |
+
} else {
|
| 149 |
+
inQuotes = false;
|
| 150 |
+
}
|
| 151 |
+
} else {
|
| 152 |
+
field += char;
|
| 153 |
+
}
|
| 154 |
+
continue;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
if (char === '"') {
|
| 158 |
+
inQuotes = true;
|
| 159 |
+
continue;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
if (char === ",") {
|
| 163 |
+
row.push(field);
|
| 164 |
+
field = "";
|
| 165 |
+
continue;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
if (char === "\n") {
|
| 169 |
+
row.push(field);
|
| 170 |
+
rows.push(row);
|
| 171 |
+
row = [];
|
| 172 |
+
field = "";
|
| 173 |
+
continue;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
if (char === "\r") {
|
| 177 |
+
continue;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
field += char;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
if (field.length > 0 || row.length > 0) {
|
| 184 |
+
row.push(field);
|
| 185 |
+
rows.push(row);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
return rows;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
function isHeaderRow(row: string[]): boolean {
|
| 192 |
+
if (row.length < HEADER_COLUMNS.length) return false;
|
| 193 |
+
return HEADER_COLUMNS.every((column, index) => (row[index] ?? "").trim().toLowerCase() === column);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
function extractCpuLoads(reasoning: string): number[] | null {
|
| 197 |
+
const arr = extractNumericArrayByKeyword(reasoning, ["cpu_loads", "cpu loads", "cpu"]);
|
| 198 |
+
return arr && arr.length > 0 ? arr : null;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
function extractQueueLengths(reasoning: string): number[] | null {
|
| 202 |
+
const arr = extractNumericArrayByKeyword(reasoning, ["queue_lengths", "queue lengths", "queue"]);
|
| 203 |
+
return arr && arr.length > 0 ? arr.map((value) => Math.round(value)) : null;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
function extractFailedNodes(reasoning: string): number[] | null {
|
| 207 |
+
const lower = reasoning.toLowerCase();
|
| 208 |
+
if (lower.includes("failed_nodes is empty") || lower.includes("failed nodes is empty")) {
|
| 209 |
+
return [];
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
const raw = extractRawArrayByKeyword(reasoning, ["failed_nodes", "failed nodes"]);
|
| 213 |
+
if (raw === null) return null;
|
| 214 |
+
|
| 215 |
+
const nodeMatches = Array.from(raw.matchAll(/(?:node\s*[-_]?\s*)?(\d+)/gi));
|
| 216 |
+
const parsed = nodeMatches
|
| 217 |
+
.map((match) => Number.parseInt(match[1], 10))
|
| 218 |
+
.filter((value) => Number.isFinite(value) && value >= 0);
|
| 219 |
+
|
| 220 |
+
return uniqueInts(parsed);
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
function extractLatencyMs(reasoning: string): number | null {
|
| 224 |
+
const direct =
|
| 225 |
+
extractByRegex(reasoning, /\blatency_ms\b[^0-9-]{0,24}(-?\d+(?:\.\d+)?)/i) ??
|
| 226 |
+
extractByRegex(reasoning, /\bcurrent latency\b[^0-9-]{0,24}(-?\d+(?:\.\d+)?)/i) ??
|
| 227 |
+
extractByRegex(reasoning, /\blatency(?:\s+is|:|=)\s*(-?\d+(?:\.\d+)?)/i);
|
| 228 |
+
|
| 229 |
+
if (direct !== null) return direct;
|
| 230 |
+
|
| 231 |
+
// Last-resort fallback for prose like: "latency ... 87.3ms"
|
| 232 |
+
const msNearLatency = extractByRegex(reasoning, /\blatency\b[\s\S]{0,40}?(-?\d+(?:\.\d+)?)\s*ms\b/i);
|
| 233 |
+
return msNearLatency ?? null;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
function extractRequestRate(reasoning: string): number | null {
|
| 237 |
+
const direct =
|
| 238 |
+
extractByRegex(reasoning, /\brequest_rate\b(?!_norm)[^0-9-]{0,24}(-?\d+(?:\.\d+)?)/i) ??
|
| 239 |
+
extractByRegex(reasoning, /\brequest rate\b[^0-9-]{0,24}(-?\d+(?:\.\d+)?)/i) ??
|
| 240 |
+
extractByRegex(reasoning, /\bcurrent request rate\b[^0-9-]{0,24}(-?\d+(?:\.\d+)?)/i);
|
| 241 |
+
return direct ?? null;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
function extractNumericArrayByKeyword(text: string, keywords: string[]): number[] | null {
|
| 245 |
+
const raw = extractRawArrayByKeyword(text, keywords);
|
| 246 |
+
if (raw === null) return null;
|
| 247 |
+
|
| 248 |
+
const matches = raw.match(/-?\d+(?:\.\d+)?/g);
|
| 249 |
+
if (!matches) return [];
|
| 250 |
+
|
| 251 |
+
const parsed = matches
|
| 252 |
+
.map((token) => Number.parseFloat(token))
|
| 253 |
+
.filter((value) => Number.isFinite(value));
|
| 254 |
+
|
| 255 |
+
return parsed;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
function extractRawArrayByKeyword(text: string, keywords: string[]): string | null {
|
| 259 |
+
const lower = text.toLowerCase();
|
| 260 |
+
|
| 261 |
+
for (const keyword of keywords) {
|
| 262 |
+
let fromIndex = 0;
|
| 263 |
+
|
| 264 |
+
while (true) {
|
| 265 |
+
const idx = lower.indexOf(keyword.toLowerCase(), fromIndex);
|
| 266 |
+
if (idx === -1) break;
|
| 267 |
+
|
| 268 |
+
const open = text.indexOf("[", idx);
|
| 269 |
+
if (open === -1 || open - idx > 220) {
|
| 270 |
+
fromIndex = idx + keyword.length;
|
| 271 |
+
continue;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
const close = text.indexOf("]", open + 1);
|
| 275 |
+
if (close === -1 || close - open > 500) {
|
| 276 |
+
fromIndex = idx + keyword.length;
|
| 277 |
+
continue;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
return text.slice(open + 1, close);
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
return null;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
function extractByRegex(text: string, regex: RegExp): number | null {
|
| 288 |
+
const match = text.match(regex);
|
| 289 |
+
if (!match || !match[1]) return null;
|
| 290 |
+
const value = Number.parseFloat(match[1]);
|
| 291 |
+
return Number.isFinite(value) ? value : null;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
function normalizeCpuLoads(values: number[], row: MetricsCsvRow, index: number): number[] {
|
| 295 |
+
const nodeCount = 4;
|
| 296 |
+
const result = fillToSize(values, nodeCount, (slot) => deriveCpuSlot(row, index, slot));
|
| 297 |
+
return result.map((value) => round3(clamp(value, 0, 1)));
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
function normalizeQueueLengths(
|
| 301 |
+
values: number[],
|
| 302 |
+
cpuLoads: number[],
|
| 303 |
+
row: MetricsCsvRow,
|
| 304 |
+
index: number
|
| 305 |
+
): number[] {
|
| 306 |
+
const nodeCount = cpuLoads.length;
|
| 307 |
+
const result = fillToSize(values, nodeCount, (slot) => deriveQueueSlot(cpuLoads, row, index, slot));
|
| 308 |
+
return result.map((value) => clampInt(Math.round(value), 0, 100_000));
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
function normalizeFailedNodes(values: number[], nodeCount: number): number[] {
|
| 312 |
+
return uniqueInts(values)
|
| 313 |
+
.filter((value) => value >= 0 && value < nodeCount)
|
| 314 |
+
.sort((a, b) => a - b);
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
function deriveCpuLoads(row: MetricsCsvRow, index: number): number[] {
|
| 318 |
+
const out: number[] = [];
|
| 319 |
+
for (let i = 0; i < 4; i += 1) {
|
| 320 |
+
out.push(deriveCpuSlot(row, index, i));
|
| 321 |
+
}
|
| 322 |
+
return out;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
function deriveCpuSlot(row: MetricsCsvRow, index: number, slot: number): number {
|
| 326 |
+
const h = stableHash(`${row.taskId}|${row.step}|${row.actionTaken}|cpu|${slot}|${index}`);
|
| 327 |
+
const taskBias = taskCpuBias(row.taskId);
|
| 328 |
+
const scoreBias = clamp(row.cumulativeScore, 0, 1) * 0.25;
|
| 329 |
+
const rewardBias = row.reward < 0 ? 0.18 : 0.1;
|
| 330 |
+
const hashed = ((h % 1000) / 1000) * 0.38;
|
| 331 |
+
return clamp(taskBias + scoreBias + rewardBias + hashed, 0.05, 1);
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
function deriveQueueLengths(cpuLoads: number[], row: MetricsCsvRow, index: number): number[] {
|
| 335 |
+
return cpuLoads.map((cpu, slot) => deriveQueueSlot(cpuLoads, row, index, slot, cpu));
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
function deriveQueueSlot(
|
| 339 |
+
cpuLoads: number[],
|
| 340 |
+
row: MetricsCsvRow,
|
| 341 |
+
index: number,
|
| 342 |
+
slot: number,
|
| 343 |
+
knownCpu?: number
|
| 344 |
+
): number {
|
| 345 |
+
const cpu = knownCpu ?? cpuLoads[slot] ?? 0;
|
| 346 |
+
const h = stableHash(`${row.taskId}|${row.step}|queue|${slot}|${index}`);
|
| 347 |
+
const taskBoost = taskQueueBias(row.taskId);
|
| 348 |
+
const noise = h % 11;
|
| 349 |
+
return Math.max(0, Math.round(cpu * 80 + taskBoost + noise));
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
function deriveLatencyMs(cpuLoads: number[], queueLengths: number[], row: MetricsCsvRow): number {
|
| 353 |
+
const cpuAvg = average(cpuLoads);
|
| 354 |
+
const queueAvg = average(queueLengths);
|
| 355 |
+
const rewardPenalty = row.reward <= 0 ? 22 : 8;
|
| 356 |
+
return clamp(cpuAvg * 140 + queueAvg * 0.95 + rewardPenalty, 5, 2000);
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
function deriveFailedNodes(cpuLoads: number[], row: MetricsCsvRow): number[] {
|
| 360 |
+
const inferred = cpuLoads
|
| 361 |
+
.map((cpu, idx) => ({ cpu, idx }))
|
| 362 |
+
.filter((entry) => entry.cpu <= 0.02)
|
| 363 |
+
.map((entry) => entry.idx);
|
| 364 |
+
|
| 365 |
+
if (inferred.length > 0) return inferred;
|
| 366 |
+
|
| 367 |
+
if (row.reward <= -1000) {
|
| 368 |
+
return [0];
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
return [];
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
function deriveRequestRate(cpuLoads: number[], queueLengths: number[], row: MetricsCsvRow): number {
|
| 375 |
+
const base = taskRequestBase(row.taskId);
|
| 376 |
+
const cpuFactor = average(cpuLoads) * 480;
|
| 377 |
+
const queueFactor = average(queueLengths) * 3.2;
|
| 378 |
+
return base + cpuFactor + queueFactor;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
function taskRequestBase(taskId: string): number {
|
| 382 |
+
const task = taskId.toLowerCase();
|
| 383 |
+
if (task.includes("flash")) return 600;
|
| 384 |
+
if (task.includes("traffic")) return 280;
|
| 385 |
+
if (task.includes("cascade")) return 220;
|
| 386 |
+
if (task.includes("node")) return 140;
|
| 387 |
+
return 180;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
function taskCpuBias(taskId: string): number {
|
| 391 |
+
const task = taskId.toLowerCase();
|
| 392 |
+
if (task.includes("flash")) return 0.58;
|
| 393 |
+
if (task.includes("traffic")) return 0.46;
|
| 394 |
+
if (task.includes("cascade")) return 0.52;
|
| 395 |
+
if (task.includes("node")) return 0.4;
|
| 396 |
+
return 0.42;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
function taskQueueBias(taskId: string): number {
|
| 400 |
+
const task = taskId.toLowerCase();
|
| 401 |
+
if (task.includes("flash")) return 42;
|
| 402 |
+
if (task.includes("traffic")) return 26;
|
| 403 |
+
if (task.includes("cascade")) return 34;
|
| 404 |
+
if (task.includes("node")) return 20;
|
| 405 |
+
return 24;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
function fillToSize(values: number[], size: number, deriveAt: (slot: number) => number): number[] {
|
| 409 |
+
const normalized = values
|
| 410 |
+
.filter((value) => Number.isFinite(value))
|
| 411 |
+
.slice(0, size);
|
| 412 |
+
|
| 413 |
+
while (normalized.length < size) {
|
| 414 |
+
normalized.push(deriveAt(normalized.length));
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
return normalized;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
function toFiniteNumber(value: string | undefined): number | null {
|
| 421 |
+
if (value === undefined) return null;
|
| 422 |
+
const parsed = Number.parseFloat(value);
|
| 423 |
+
return Number.isFinite(parsed) ? parsed : null;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
function average(values: number[]): number {
|
| 427 |
+
if (values.length === 0) return 0;
|
| 428 |
+
const total = values.reduce((sum, value) => sum + value, 0);
|
| 429 |
+
return total / values.length;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
function stableHash(text: string): number {
|
| 433 |
+
let hash = 2166136261;
|
| 434 |
+
for (let i = 0; i < text.length; i += 1) {
|
| 435 |
+
hash ^= text.charCodeAt(i);
|
| 436 |
+
hash = Math.imul(hash, 16777619);
|
| 437 |
+
}
|
| 438 |
+
return hash >>> 0;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
function uniqueInts(values: number[]): number[] {
|
| 442 |
+
return [...new Set(values.map((value) => Math.trunc(value)))];
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
function clamp(value: number, min: number, max: number): number {
|
| 446 |
+
return Math.min(max, Math.max(min, value));
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
function clampInt(value: number, min: number, max: number): number {
|
| 450 |
+
return Math.trunc(clamp(value, min, max));
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
function round1(value: number): number {
|
| 454 |
+
return Math.round(value * 10) / 10;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
function round3(value: number): number {
|
| 458 |
+
return Math.round(value * 1000) / 1000;
|
| 459 |
+
}
|
frontend/lib/useSimulationSocket.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useEffect, useMemo, useRef, useState } from "react";
|
| 4 |
+
|
| 5 |
+
export type SimulationObservation = {
|
| 6 |
+
cpu_loads: number[];
|
| 7 |
+
queue_lengths: number[];
|
| 8 |
+
failed_nodes: number[];
|
| 9 |
+
latency_ms: number;
|
| 10 |
+
request_rate: number;
|
| 11 |
+
mem_utilizations: number[];
|
| 12 |
+
step: number;
|
| 13 |
+
done: boolean;
|
| 14 |
+
action_errors: string[];
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
export type SimulationPacket = {
|
| 18 |
+
observation: SimulationObservation;
|
| 19 |
+
intervention?: string | null;
|
| 20 |
+
last_action_type?: string;
|
| 21 |
+
timestamp_ms?: number;
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
function resolveWebSocketUrl(): string {
|
| 25 |
+
if (process.env.NEXT_PUBLIC_SIM_WS_URL) {
|
| 26 |
+
return process.env.NEXT_PUBLIC_SIM_WS_URL;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
if (typeof window === "undefined") {
|
| 30 |
+
return "ws://localhost:8000/ws/simulation";
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
| 34 |
+
const host = window.location.hostname;
|
| 35 |
+
return `${protocol}://${host}:8000/ws/simulation`;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
export function useSimulationSocket() {
|
| 39 |
+
const [packet, setPacket] = useState<SimulationPacket | null>(null);
|
| 40 |
+
const [connected, setConnected] = useState(false);
|
| 41 |
+
const [error, setError] = useState<string | null>(null);
|
| 42 |
+
const wsRef = useRef<WebSocket | null>(null);
|
| 43 |
+
const reconnectTimerRef = useRef<number | null>(null);
|
| 44 |
+
|
| 45 |
+
const wsUrl = useMemo(() => resolveWebSocketUrl(), []);
|
| 46 |
+
|
| 47 |
+
useEffect(() => {
|
| 48 |
+
let mounted = true;
|
| 49 |
+
|
| 50 |
+
const connect = () => {
|
| 51 |
+
const ws = new WebSocket(wsUrl);
|
| 52 |
+
wsRef.current = ws;
|
| 53 |
+
|
| 54 |
+
ws.onopen = () => {
|
| 55 |
+
if (!mounted) return;
|
| 56 |
+
setConnected(true);
|
| 57 |
+
setError(null);
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
ws.onmessage = (event) => {
|
| 61 |
+
if (!mounted) return;
|
| 62 |
+
try {
|
| 63 |
+
const parsed = JSON.parse(event.data) as SimulationPacket;
|
| 64 |
+
setPacket(parsed);
|
| 65 |
+
} catch {
|
| 66 |
+
setError("Invalid simulation payload");
|
| 67 |
+
}
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
ws.onerror = () => {
|
| 71 |
+
if (!mounted) return;
|
| 72 |
+
setError("Simulation socket error");
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
ws.onclose = () => {
|
| 76 |
+
if (!mounted) return;
|
| 77 |
+
setConnected(false);
|
| 78 |
+
reconnectTimerRef.current = window.setTimeout(connect, 1500);
|
| 79 |
+
};
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
connect();
|
| 83 |
+
|
| 84 |
+
return () => {
|
| 85 |
+
mounted = false;
|
| 86 |
+
if (reconnectTimerRef.current !== null) {
|
| 87 |
+
window.clearTimeout(reconnectTimerRef.current);
|
| 88 |
+
}
|
| 89 |
+
wsRef.current?.close();
|
| 90 |
+
};
|
| 91 |
+
}, [wsUrl]);
|
| 92 |
+
|
| 93 |
+
const sendIntervention = (command: string) => {
|
| 94 |
+
const ws = wsRef.current;
|
| 95 |
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
| 96 |
+
|
| 97 |
+
ws.send(JSON.stringify({ command }));
|
| 98 |
+
};
|
| 99 |
+
|
| 100 |
+
return {
|
| 101 |
+
packet,
|
| 102 |
+
connected,
|
| 103 |
+
error,
|
| 104 |
+
sendIntervention,
|
| 105 |
+
};
|
| 106 |
+
}
|
frontend/package-lock.json
CHANGED
|
@@ -11,7 +11,8 @@
|
|
| 11 |
"framer-motion": "^12.38.0",
|
| 12 |
"next": "16.2.4",
|
| 13 |
"react": "19.2.4",
|
| 14 |
-
"react-dom": "19.2.4"
|
|
|
|
| 15 |
},
|
| 16 |
"devDependencies": {
|
| 17 |
"@tailwindcss/postcss": "^4",
|
|
@@ -1241,6 +1242,108 @@
|
|
| 1241 |
"node": ">=12.4.0"
|
| 1242 |
}
|
| 1243 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1244 |
"node_modules/@rtsao/scc": {
|
| 1245 |
"version": "1.1.0",
|
| 1246 |
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
|
@@ -1539,6 +1642,259 @@
|
|
| 1539 |
"tslib": "^2.4.0"
|
| 1540 |
}
|
| 1541 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1542 |
"node_modules/@types/estree": {
|
| 1543 |
"version": "1.0.8",
|
| 1544 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
|
@@ -1546,6 +1902,12 @@
|
|
| 1546 |
"dev": true,
|
| 1547 |
"license": "MIT"
|
| 1548 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1549 |
"node_modules/@types/json-schema": {
|
| 1550 |
"version": "7.0.15",
|
| 1551 |
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
|
@@ -1574,7 +1936,7 @@
|
|
| 1574 |
"version": "19.2.14",
|
| 1575 |
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
| 1576 |
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
| 1577 |
-
"
|
| 1578 |
"license": "MIT",
|
| 1579 |
"dependencies": {
|
| 1580 |
"csstype": "^3.2.2"
|
|
@@ -2614,6 +2976,12 @@
|
|
| 2614 |
"url": "https://github.com/chalk/chalk?sponsor=1"
|
| 2615 |
}
|
| 2616 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2617 |
"node_modules/client-only": {
|
| 2618 |
"version": "0.0.1",
|
| 2619 |
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
|
@@ -2673,9 +3041,114 @@
|
|
| 2673 |
"version": "3.2.3",
|
| 2674 |
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
| 2675 |
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
| 2676 |
-
"
|
| 2677 |
"license": "MIT"
|
| 2678 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2679 |
"node_modules/damerau-levenshtein": {
|
| 2680 |
"version": "1.0.8",
|
| 2681 |
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
|
@@ -5493,6 +5966,24 @@
|
|
| 5493 |
"dev": true,
|
| 5494 |
"license": "MIT"
|
| 5495 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5496 |
"node_modules/reflect.getprototypeof": {
|
| 5497 |
"version": "1.0.10",
|
| 5498 |
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
|
@@ -6466,6 +6957,15 @@
|
|
| 6466 |
"punycode": "^2.1.0"
|
| 6467 |
}
|
| 6468 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6469 |
"node_modules/which": {
|
| 6470 |
"version": "2.0.2",
|
| 6471 |
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
|
@@ -6623,6 +7123,34 @@
|
|
| 6623 |
"peerDependencies": {
|
| 6624 |
"zod": "^3.25.0 || ^4.0.0"
|
| 6625 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6626 |
}
|
| 6627 |
}
|
| 6628 |
}
|
|
|
|
| 11 |
"framer-motion": "^12.38.0",
|
| 12 |
"next": "16.2.4",
|
| 13 |
"react": "19.2.4",
|
| 14 |
+
"react-dom": "19.2.4",
|
| 15 |
+
"reactflow": "^11.11.4"
|
| 16 |
},
|
| 17 |
"devDependencies": {
|
| 18 |
"@tailwindcss/postcss": "^4",
|
|
|
|
| 1242 |
"node": ">=12.4.0"
|
| 1243 |
}
|
| 1244 |
},
|
| 1245 |
+
"node_modules/@reactflow/background": {
|
| 1246 |
+
"version": "11.3.14",
|
| 1247 |
+
"resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
|
| 1248 |
+
"integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
|
| 1249 |
+
"license": "MIT",
|
| 1250 |
+
"dependencies": {
|
| 1251 |
+
"@reactflow/core": "11.11.4",
|
| 1252 |
+
"classcat": "^5.0.3",
|
| 1253 |
+
"zustand": "^4.4.1"
|
| 1254 |
+
},
|
| 1255 |
+
"peerDependencies": {
|
| 1256 |
+
"react": ">=17",
|
| 1257 |
+
"react-dom": ">=17"
|
| 1258 |
+
}
|
| 1259 |
+
},
|
| 1260 |
+
"node_modules/@reactflow/controls": {
|
| 1261 |
+
"version": "11.2.14",
|
| 1262 |
+
"resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
|
| 1263 |
+
"integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
|
| 1264 |
+
"license": "MIT",
|
| 1265 |
+
"dependencies": {
|
| 1266 |
+
"@reactflow/core": "11.11.4",
|
| 1267 |
+
"classcat": "^5.0.3",
|
| 1268 |
+
"zustand": "^4.4.1"
|
| 1269 |
+
},
|
| 1270 |
+
"peerDependencies": {
|
| 1271 |
+
"react": ">=17",
|
| 1272 |
+
"react-dom": ">=17"
|
| 1273 |
+
}
|
| 1274 |
+
},
|
| 1275 |
+
"node_modules/@reactflow/core": {
|
| 1276 |
+
"version": "11.11.4",
|
| 1277 |
+
"resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
|
| 1278 |
+
"integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
|
| 1279 |
+
"license": "MIT",
|
| 1280 |
+
"dependencies": {
|
| 1281 |
+
"@types/d3": "^7.4.0",
|
| 1282 |
+
"@types/d3-drag": "^3.0.1",
|
| 1283 |
+
"@types/d3-selection": "^3.0.3",
|
| 1284 |
+
"@types/d3-zoom": "^3.0.1",
|
| 1285 |
+
"classcat": "^5.0.3",
|
| 1286 |
+
"d3-drag": "^3.0.0",
|
| 1287 |
+
"d3-selection": "^3.0.0",
|
| 1288 |
+
"d3-zoom": "^3.0.0",
|
| 1289 |
+
"zustand": "^4.4.1"
|
| 1290 |
+
},
|
| 1291 |
+
"peerDependencies": {
|
| 1292 |
+
"react": ">=17",
|
| 1293 |
+
"react-dom": ">=17"
|
| 1294 |
+
}
|
| 1295 |
+
},
|
| 1296 |
+
"node_modules/@reactflow/minimap": {
|
| 1297 |
+
"version": "11.7.14",
|
| 1298 |
+
"resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
|
| 1299 |
+
"integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
|
| 1300 |
+
"license": "MIT",
|
| 1301 |
+
"dependencies": {
|
| 1302 |
+
"@reactflow/core": "11.11.4",
|
| 1303 |
+
"@types/d3-selection": "^3.0.3",
|
| 1304 |
+
"@types/d3-zoom": "^3.0.1",
|
| 1305 |
+
"classcat": "^5.0.3",
|
| 1306 |
+
"d3-selection": "^3.0.0",
|
| 1307 |
+
"d3-zoom": "^3.0.0",
|
| 1308 |
+
"zustand": "^4.4.1"
|
| 1309 |
+
},
|
| 1310 |
+
"peerDependencies": {
|
| 1311 |
+
"react": ">=17",
|
| 1312 |
+
"react-dom": ">=17"
|
| 1313 |
+
}
|
| 1314 |
+
},
|
| 1315 |
+
"node_modules/@reactflow/node-resizer": {
|
| 1316 |
+
"version": "2.2.14",
|
| 1317 |
+
"resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
|
| 1318 |
+
"integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
|
| 1319 |
+
"license": "MIT",
|
| 1320 |
+
"dependencies": {
|
| 1321 |
+
"@reactflow/core": "11.11.4",
|
| 1322 |
+
"classcat": "^5.0.4",
|
| 1323 |
+
"d3-drag": "^3.0.0",
|
| 1324 |
+
"d3-selection": "^3.0.0",
|
| 1325 |
+
"zustand": "^4.4.1"
|
| 1326 |
+
},
|
| 1327 |
+
"peerDependencies": {
|
| 1328 |
+
"react": ">=17",
|
| 1329 |
+
"react-dom": ">=17"
|
| 1330 |
+
}
|
| 1331 |
+
},
|
| 1332 |
+
"node_modules/@reactflow/node-toolbar": {
|
| 1333 |
+
"version": "1.3.14",
|
| 1334 |
+
"resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
|
| 1335 |
+
"integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
|
| 1336 |
+
"license": "MIT",
|
| 1337 |
+
"dependencies": {
|
| 1338 |
+
"@reactflow/core": "11.11.4",
|
| 1339 |
+
"classcat": "^5.0.3",
|
| 1340 |
+
"zustand": "^4.4.1"
|
| 1341 |
+
},
|
| 1342 |
+
"peerDependencies": {
|
| 1343 |
+
"react": ">=17",
|
| 1344 |
+
"react-dom": ">=17"
|
| 1345 |
+
}
|
| 1346 |
+
},
|
| 1347 |
"node_modules/@rtsao/scc": {
|
| 1348 |
"version": "1.1.0",
|
| 1349 |
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
|
|
|
| 1642 |
"tslib": "^2.4.0"
|
| 1643 |
}
|
| 1644 |
},
|
| 1645 |
+
"node_modules/@types/d3": {
|
| 1646 |
+
"version": "7.4.3",
|
| 1647 |
+
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
| 1648 |
+
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
|
| 1649 |
+
"license": "MIT",
|
| 1650 |
+
"dependencies": {
|
| 1651 |
+
"@types/d3-array": "*",
|
| 1652 |
+
"@types/d3-axis": "*",
|
| 1653 |
+
"@types/d3-brush": "*",
|
| 1654 |
+
"@types/d3-chord": "*",
|
| 1655 |
+
"@types/d3-color": "*",
|
| 1656 |
+
"@types/d3-contour": "*",
|
| 1657 |
+
"@types/d3-delaunay": "*",
|
| 1658 |
+
"@types/d3-dispatch": "*",
|
| 1659 |
+
"@types/d3-drag": "*",
|
| 1660 |
+
"@types/d3-dsv": "*",
|
| 1661 |
+
"@types/d3-ease": "*",
|
| 1662 |
+
"@types/d3-fetch": "*",
|
| 1663 |
+
"@types/d3-force": "*",
|
| 1664 |
+
"@types/d3-format": "*",
|
| 1665 |
+
"@types/d3-geo": "*",
|
| 1666 |
+
"@types/d3-hierarchy": "*",
|
| 1667 |
+
"@types/d3-interpolate": "*",
|
| 1668 |
+
"@types/d3-path": "*",
|
| 1669 |
+
"@types/d3-polygon": "*",
|
| 1670 |
+
"@types/d3-quadtree": "*",
|
| 1671 |
+
"@types/d3-random": "*",
|
| 1672 |
+
"@types/d3-scale": "*",
|
| 1673 |
+
"@types/d3-scale-chromatic": "*",
|
| 1674 |
+
"@types/d3-selection": "*",
|
| 1675 |
+
"@types/d3-shape": "*",
|
| 1676 |
+
"@types/d3-time": "*",
|
| 1677 |
+
"@types/d3-time-format": "*",
|
| 1678 |
+
"@types/d3-timer": "*",
|
| 1679 |
+
"@types/d3-transition": "*",
|
| 1680 |
+
"@types/d3-zoom": "*"
|
| 1681 |
+
}
|
| 1682 |
+
},
|
| 1683 |
+
"node_modules/@types/d3-array": {
|
| 1684 |
+
"version": "3.2.2",
|
| 1685 |
+
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
| 1686 |
+
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
| 1687 |
+
"license": "MIT"
|
| 1688 |
+
},
|
| 1689 |
+
"node_modules/@types/d3-axis": {
|
| 1690 |
+
"version": "3.0.6",
|
| 1691 |
+
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
|
| 1692 |
+
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
|
| 1693 |
+
"license": "MIT",
|
| 1694 |
+
"dependencies": {
|
| 1695 |
+
"@types/d3-selection": "*"
|
| 1696 |
+
}
|
| 1697 |
+
},
|
| 1698 |
+
"node_modules/@types/d3-brush": {
|
| 1699 |
+
"version": "3.0.6",
|
| 1700 |
+
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
|
| 1701 |
+
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
|
| 1702 |
+
"license": "MIT",
|
| 1703 |
+
"dependencies": {
|
| 1704 |
+
"@types/d3-selection": "*"
|
| 1705 |
+
}
|
| 1706 |
+
},
|
| 1707 |
+
"node_modules/@types/d3-chord": {
|
| 1708 |
+
"version": "3.0.6",
|
| 1709 |
+
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
|
| 1710 |
+
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
|
| 1711 |
+
"license": "MIT"
|
| 1712 |
+
},
|
| 1713 |
+
"node_modules/@types/d3-color": {
|
| 1714 |
+
"version": "3.1.3",
|
| 1715 |
+
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
| 1716 |
+
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
| 1717 |
+
"license": "MIT"
|
| 1718 |
+
},
|
| 1719 |
+
"node_modules/@types/d3-contour": {
|
| 1720 |
+
"version": "3.0.6",
|
| 1721 |
+
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
|
| 1722 |
+
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
|
| 1723 |
+
"license": "MIT",
|
| 1724 |
+
"dependencies": {
|
| 1725 |
+
"@types/d3-array": "*",
|
| 1726 |
+
"@types/geojson": "*"
|
| 1727 |
+
}
|
| 1728 |
+
},
|
| 1729 |
+
"node_modules/@types/d3-delaunay": {
|
| 1730 |
+
"version": "6.0.4",
|
| 1731 |
+
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
| 1732 |
+
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
|
| 1733 |
+
"license": "MIT"
|
| 1734 |
+
},
|
| 1735 |
+
"node_modules/@types/d3-dispatch": {
|
| 1736 |
+
"version": "3.0.7",
|
| 1737 |
+
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
|
| 1738 |
+
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
|
| 1739 |
+
"license": "MIT"
|
| 1740 |
+
},
|
| 1741 |
+
"node_modules/@types/d3-drag": {
|
| 1742 |
+
"version": "3.0.7",
|
| 1743 |
+
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
| 1744 |
+
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
| 1745 |
+
"license": "MIT",
|
| 1746 |
+
"dependencies": {
|
| 1747 |
+
"@types/d3-selection": "*"
|
| 1748 |
+
}
|
| 1749 |
+
},
|
| 1750 |
+
"node_modules/@types/d3-dsv": {
|
| 1751 |
+
"version": "3.0.7",
|
| 1752 |
+
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
|
| 1753 |
+
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
|
| 1754 |
+
"license": "MIT"
|
| 1755 |
+
},
|
| 1756 |
+
"node_modules/@types/d3-ease": {
|
| 1757 |
+
"version": "3.0.2",
|
| 1758 |
+
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
| 1759 |
+
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
| 1760 |
+
"license": "MIT"
|
| 1761 |
+
},
|
| 1762 |
+
"node_modules/@types/d3-fetch": {
|
| 1763 |
+
"version": "3.0.7",
|
| 1764 |
+
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
|
| 1765 |
+
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
|
| 1766 |
+
"license": "MIT",
|
| 1767 |
+
"dependencies": {
|
| 1768 |
+
"@types/d3-dsv": "*"
|
| 1769 |
+
}
|
| 1770 |
+
},
|
| 1771 |
+
"node_modules/@types/d3-force": {
|
| 1772 |
+
"version": "3.0.10",
|
| 1773 |
+
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
|
| 1774 |
+
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
|
| 1775 |
+
"license": "MIT"
|
| 1776 |
+
},
|
| 1777 |
+
"node_modules/@types/d3-format": {
|
| 1778 |
+
"version": "3.0.4",
|
| 1779 |
+
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
|
| 1780 |
+
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
|
| 1781 |
+
"license": "MIT"
|
| 1782 |
+
},
|
| 1783 |
+
"node_modules/@types/d3-geo": {
|
| 1784 |
+
"version": "3.1.0",
|
| 1785 |
+
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
|
| 1786 |
+
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
|
| 1787 |
+
"license": "MIT",
|
| 1788 |
+
"dependencies": {
|
| 1789 |
+
"@types/geojson": "*"
|
| 1790 |
+
}
|
| 1791 |
+
},
|
| 1792 |
+
"node_modules/@types/d3-hierarchy": {
|
| 1793 |
+
"version": "3.1.7",
|
| 1794 |
+
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
|
| 1795 |
+
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
|
| 1796 |
+
"license": "MIT"
|
| 1797 |
+
},
|
| 1798 |
+
"node_modules/@types/d3-interpolate": {
|
| 1799 |
+
"version": "3.0.4",
|
| 1800 |
+
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
| 1801 |
+
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
| 1802 |
+
"license": "MIT",
|
| 1803 |
+
"dependencies": {
|
| 1804 |
+
"@types/d3-color": "*"
|
| 1805 |
+
}
|
| 1806 |
+
},
|
| 1807 |
+
"node_modules/@types/d3-path": {
|
| 1808 |
+
"version": "3.1.1",
|
| 1809 |
+
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
| 1810 |
+
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
| 1811 |
+
"license": "MIT"
|
| 1812 |
+
},
|
| 1813 |
+
"node_modules/@types/d3-polygon": {
|
| 1814 |
+
"version": "3.0.2",
|
| 1815 |
+
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
|
| 1816 |
+
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
|
| 1817 |
+
"license": "MIT"
|
| 1818 |
+
},
|
| 1819 |
+
"node_modules/@types/d3-quadtree": {
|
| 1820 |
+
"version": "3.0.6",
|
| 1821 |
+
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
|
| 1822 |
+
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
|
| 1823 |
+
"license": "MIT"
|
| 1824 |
+
},
|
| 1825 |
+
"node_modules/@types/d3-random": {
|
| 1826 |
+
"version": "3.0.3",
|
| 1827 |
+
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
|
| 1828 |
+
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
|
| 1829 |
+
"license": "MIT"
|
| 1830 |
+
},
|
| 1831 |
+
"node_modules/@types/d3-scale": {
|
| 1832 |
+
"version": "4.0.9",
|
| 1833 |
+
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
| 1834 |
+
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
| 1835 |
+
"license": "MIT",
|
| 1836 |
+
"dependencies": {
|
| 1837 |
+
"@types/d3-time": "*"
|
| 1838 |
+
}
|
| 1839 |
+
},
|
| 1840 |
+
"node_modules/@types/d3-scale-chromatic": {
|
| 1841 |
+
"version": "3.1.0",
|
| 1842 |
+
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
| 1843 |
+
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
| 1844 |
+
"license": "MIT"
|
| 1845 |
+
},
|
| 1846 |
+
"node_modules/@types/d3-selection": {
|
| 1847 |
+
"version": "3.0.11",
|
| 1848 |
+
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
| 1849 |
+
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
| 1850 |
+
"license": "MIT"
|
| 1851 |
+
},
|
| 1852 |
+
"node_modules/@types/d3-shape": {
|
| 1853 |
+
"version": "3.1.8",
|
| 1854 |
+
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
| 1855 |
+
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
|
| 1856 |
+
"license": "MIT",
|
| 1857 |
+
"dependencies": {
|
| 1858 |
+
"@types/d3-path": "*"
|
| 1859 |
+
}
|
| 1860 |
+
},
|
| 1861 |
+
"node_modules/@types/d3-time": {
|
| 1862 |
+
"version": "3.0.4",
|
| 1863 |
+
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
| 1864 |
+
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
| 1865 |
+
"license": "MIT"
|
| 1866 |
+
},
|
| 1867 |
+
"node_modules/@types/d3-time-format": {
|
| 1868 |
+
"version": "4.0.3",
|
| 1869 |
+
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
|
| 1870 |
+
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
|
| 1871 |
+
"license": "MIT"
|
| 1872 |
+
},
|
| 1873 |
+
"node_modules/@types/d3-timer": {
|
| 1874 |
+
"version": "3.0.2",
|
| 1875 |
+
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
| 1876 |
+
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
| 1877 |
+
"license": "MIT"
|
| 1878 |
+
},
|
| 1879 |
+
"node_modules/@types/d3-transition": {
|
| 1880 |
+
"version": "3.0.9",
|
| 1881 |
+
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
| 1882 |
+
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
| 1883 |
+
"license": "MIT",
|
| 1884 |
+
"dependencies": {
|
| 1885 |
+
"@types/d3-selection": "*"
|
| 1886 |
+
}
|
| 1887 |
+
},
|
| 1888 |
+
"node_modules/@types/d3-zoom": {
|
| 1889 |
+
"version": "3.0.8",
|
| 1890 |
+
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
| 1891 |
+
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
| 1892 |
+
"license": "MIT",
|
| 1893 |
+
"dependencies": {
|
| 1894 |
+
"@types/d3-interpolate": "*",
|
| 1895 |
+
"@types/d3-selection": "*"
|
| 1896 |
+
}
|
| 1897 |
+
},
|
| 1898 |
"node_modules/@types/estree": {
|
| 1899 |
"version": "1.0.8",
|
| 1900 |
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
|
|
|
| 1902 |
"dev": true,
|
| 1903 |
"license": "MIT"
|
| 1904 |
},
|
| 1905 |
+
"node_modules/@types/geojson": {
|
| 1906 |
+
"version": "7946.0.16",
|
| 1907 |
+
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
| 1908 |
+
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
| 1909 |
+
"license": "MIT"
|
| 1910 |
+
},
|
| 1911 |
"node_modules/@types/json-schema": {
|
| 1912 |
"version": "7.0.15",
|
| 1913 |
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
|
|
|
| 1936 |
"version": "19.2.14",
|
| 1937 |
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
| 1938 |
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
| 1939 |
+
"devOptional": true,
|
| 1940 |
"license": "MIT",
|
| 1941 |
"dependencies": {
|
| 1942 |
"csstype": "^3.2.2"
|
|
|
|
| 2976 |
"url": "https://github.com/chalk/chalk?sponsor=1"
|
| 2977 |
}
|
| 2978 |
},
|
| 2979 |
+
"node_modules/classcat": {
|
| 2980 |
+
"version": "5.0.5",
|
| 2981 |
+
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
|
| 2982 |
+
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
| 2983 |
+
"license": "MIT"
|
| 2984 |
+
},
|
| 2985 |
"node_modules/client-only": {
|
| 2986 |
"version": "0.0.1",
|
| 2987 |
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
|
|
|
| 3041 |
"version": "3.2.3",
|
| 3042 |
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
| 3043 |
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
| 3044 |
+
"devOptional": true,
|
| 3045 |
"license": "MIT"
|
| 3046 |
},
|
| 3047 |
+
"node_modules/d3-color": {
|
| 3048 |
+
"version": "3.1.0",
|
| 3049 |
+
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
| 3050 |
+
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
| 3051 |
+
"license": "ISC",
|
| 3052 |
+
"engines": {
|
| 3053 |
+
"node": ">=12"
|
| 3054 |
+
}
|
| 3055 |
+
},
|
| 3056 |
+
"node_modules/d3-dispatch": {
|
| 3057 |
+
"version": "3.0.1",
|
| 3058 |
+
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
| 3059 |
+
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
| 3060 |
+
"license": "ISC",
|
| 3061 |
+
"engines": {
|
| 3062 |
+
"node": ">=12"
|
| 3063 |
+
}
|
| 3064 |
+
},
|
| 3065 |
+
"node_modules/d3-drag": {
|
| 3066 |
+
"version": "3.0.0",
|
| 3067 |
+
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
| 3068 |
+
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
| 3069 |
+
"license": "ISC",
|
| 3070 |
+
"dependencies": {
|
| 3071 |
+
"d3-dispatch": "1 - 3",
|
| 3072 |
+
"d3-selection": "3"
|
| 3073 |
+
},
|
| 3074 |
+
"engines": {
|
| 3075 |
+
"node": ">=12"
|
| 3076 |
+
}
|
| 3077 |
+
},
|
| 3078 |
+
"node_modules/d3-ease": {
|
| 3079 |
+
"version": "3.0.1",
|
| 3080 |
+
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
| 3081 |
+
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
| 3082 |
+
"license": "BSD-3-Clause",
|
| 3083 |
+
"engines": {
|
| 3084 |
+
"node": ">=12"
|
| 3085 |
+
}
|
| 3086 |
+
},
|
| 3087 |
+
"node_modules/d3-interpolate": {
|
| 3088 |
+
"version": "3.0.1",
|
| 3089 |
+
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
| 3090 |
+
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
| 3091 |
+
"license": "ISC",
|
| 3092 |
+
"dependencies": {
|
| 3093 |
+
"d3-color": "1 - 3"
|
| 3094 |
+
},
|
| 3095 |
+
"engines": {
|
| 3096 |
+
"node": ">=12"
|
| 3097 |
+
}
|
| 3098 |
+
},
|
| 3099 |
+
"node_modules/d3-selection": {
|
| 3100 |
+
"version": "3.0.0",
|
| 3101 |
+
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
| 3102 |
+
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
| 3103 |
+
"license": "ISC",
|
| 3104 |
+
"engines": {
|
| 3105 |
+
"node": ">=12"
|
| 3106 |
+
}
|
| 3107 |
+
},
|
| 3108 |
+
"node_modules/d3-timer": {
|
| 3109 |
+
"version": "3.0.1",
|
| 3110 |
+
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
| 3111 |
+
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
| 3112 |
+
"license": "ISC",
|
| 3113 |
+
"engines": {
|
| 3114 |
+
"node": ">=12"
|
| 3115 |
+
}
|
| 3116 |
+
},
|
| 3117 |
+
"node_modules/d3-transition": {
|
| 3118 |
+
"version": "3.0.1",
|
| 3119 |
+
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
| 3120 |
+
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
| 3121 |
+
"license": "ISC",
|
| 3122 |
+
"dependencies": {
|
| 3123 |
+
"d3-color": "1 - 3",
|
| 3124 |
+
"d3-dispatch": "1 - 3",
|
| 3125 |
+
"d3-ease": "1 - 3",
|
| 3126 |
+
"d3-interpolate": "1 - 3",
|
| 3127 |
+
"d3-timer": "1 - 3"
|
| 3128 |
+
},
|
| 3129 |
+
"engines": {
|
| 3130 |
+
"node": ">=12"
|
| 3131 |
+
},
|
| 3132 |
+
"peerDependencies": {
|
| 3133 |
+
"d3-selection": "2 - 3"
|
| 3134 |
+
}
|
| 3135 |
+
},
|
| 3136 |
+
"node_modules/d3-zoom": {
|
| 3137 |
+
"version": "3.0.0",
|
| 3138 |
+
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
| 3139 |
+
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
| 3140 |
+
"license": "ISC",
|
| 3141 |
+
"dependencies": {
|
| 3142 |
+
"d3-dispatch": "1 - 3",
|
| 3143 |
+
"d3-drag": "2 - 3",
|
| 3144 |
+
"d3-interpolate": "1 - 3",
|
| 3145 |
+
"d3-selection": "2 - 3",
|
| 3146 |
+
"d3-transition": "2 - 3"
|
| 3147 |
+
},
|
| 3148 |
+
"engines": {
|
| 3149 |
+
"node": ">=12"
|
| 3150 |
+
}
|
| 3151 |
+
},
|
| 3152 |
"node_modules/damerau-levenshtein": {
|
| 3153 |
"version": "1.0.8",
|
| 3154 |
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
|
|
|
| 5966 |
"dev": true,
|
| 5967 |
"license": "MIT"
|
| 5968 |
},
|
| 5969 |
+
"node_modules/reactflow": {
|
| 5970 |
+
"version": "11.11.4",
|
| 5971 |
+
"resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
|
| 5972 |
+
"integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
|
| 5973 |
+
"license": "MIT",
|
| 5974 |
+
"dependencies": {
|
| 5975 |
+
"@reactflow/background": "11.3.14",
|
| 5976 |
+
"@reactflow/controls": "11.2.14",
|
| 5977 |
+
"@reactflow/core": "11.11.4",
|
| 5978 |
+
"@reactflow/minimap": "11.7.14",
|
| 5979 |
+
"@reactflow/node-resizer": "2.2.14",
|
| 5980 |
+
"@reactflow/node-toolbar": "1.3.14"
|
| 5981 |
+
},
|
| 5982 |
+
"peerDependencies": {
|
| 5983 |
+
"react": ">=17",
|
| 5984 |
+
"react-dom": ">=17"
|
| 5985 |
+
}
|
| 5986 |
+
},
|
| 5987 |
"node_modules/reflect.getprototypeof": {
|
| 5988 |
"version": "1.0.10",
|
| 5989 |
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
|
|
|
| 6957 |
"punycode": "^2.1.0"
|
| 6958 |
}
|
| 6959 |
},
|
| 6960 |
+
"node_modules/use-sync-external-store": {
|
| 6961 |
+
"version": "1.6.0",
|
| 6962 |
+
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
| 6963 |
+
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
| 6964 |
+
"license": "MIT",
|
| 6965 |
+
"peerDependencies": {
|
| 6966 |
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 6967 |
+
}
|
| 6968 |
+
},
|
| 6969 |
"node_modules/which": {
|
| 6970 |
"version": "2.0.2",
|
| 6971 |
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
|
|
|
| 7123 |
"peerDependencies": {
|
| 7124 |
"zod": "^3.25.0 || ^4.0.0"
|
| 7125 |
}
|
| 7126 |
+
},
|
| 7127 |
+
"node_modules/zustand": {
|
| 7128 |
+
"version": "4.5.7",
|
| 7129 |
+
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
|
| 7130 |
+
"integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
|
| 7131 |
+
"license": "MIT",
|
| 7132 |
+
"dependencies": {
|
| 7133 |
+
"use-sync-external-store": "^1.2.2"
|
| 7134 |
+
},
|
| 7135 |
+
"engines": {
|
| 7136 |
+
"node": ">=12.7.0"
|
| 7137 |
+
},
|
| 7138 |
+
"peerDependencies": {
|
| 7139 |
+
"@types/react": ">=16.8",
|
| 7140 |
+
"immer": ">=9.0.6",
|
| 7141 |
+
"react": ">=16.8"
|
| 7142 |
+
},
|
| 7143 |
+
"peerDependenciesMeta": {
|
| 7144 |
+
"@types/react": {
|
| 7145 |
+
"optional": true
|
| 7146 |
+
},
|
| 7147 |
+
"immer": {
|
| 7148 |
+
"optional": true
|
| 7149 |
+
},
|
| 7150 |
+
"react": {
|
| 7151 |
+
"optional": true
|
| 7152 |
+
}
|
| 7153 |
+
}
|
| 7154 |
}
|
| 7155 |
}
|
| 7156 |
}
|
frontend/package.json
CHANGED
|
@@ -12,7 +12,8 @@
|
|
| 12 |
"framer-motion": "^12.38.0",
|
| 13 |
"next": "16.2.4",
|
| 14 |
"react": "19.2.4",
|
| 15 |
-
"react-dom": "19.2.4"
|
|
|
|
| 16 |
},
|
| 17 |
"devDependencies": {
|
| 18 |
"@tailwindcss/postcss": "^4",
|
|
|
|
| 12 |
"framer-motion": "^12.38.0",
|
| 13 |
"next": "16.2.4",
|
| 14 |
"react": "19.2.4",
|
| 15 |
+
"react-dom": "19.2.4",
|
| 16 |
+
"reactflow": "^11.11.4"
|
| 17 |
},
|
| 18 |
"devDependencies": {
|
| 19 |
"@tailwindcss/postcss": "^4",
|
server/app.py
CHANGED
|
@@ -12,7 +12,6 @@ import os
|
|
| 12 |
from openenv.core.env_server.http_server import create_app
|
| 13 |
from fastapi import WebSocket, WebSocketDisconnect
|
| 14 |
from fastapi.responses import HTMLResponse
|
| 15 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 16 |
|
| 17 |
from server.environment import DistributedInfraEnvironment
|
| 18 |
from server.models import InfraAction, InfraObservation
|
|
@@ -23,12 +22,10 @@ _global_env = DistributedInfraEnvironment()
|
|
| 23 |
_viz_env = DistributedInfraEnvironment()
|
| 24 |
_viz_lock = asyncio.Lock()
|
| 25 |
|
| 26 |
-
|
| 27 |
# 2. Create a "factory function" that returns our active instance
|
| 28 |
def env_factory():
|
| 29 |
return _global_env
|
| 30 |
|
| 31 |
-
|
| 32 |
# 3. Pass the callable factory function to OpenEnv
|
| 33 |
app = create_app(
|
| 34 |
env_factory,
|
|
@@ -37,25 +34,16 @@ app = create_app(
|
|
| 37 |
env_name="distributed_infra_env",
|
| 38 |
)
|
| 39 |
|
| 40 |
-
# --- CORS for Next.js frontend ---
|
| 41 |
-
app.add_middleware(
|
| 42 |
-
CORSMiddleware,
|
| 43 |
-
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000", "*"],
|
| 44 |
-
allow_credentials=True,
|
| 45 |
-
allow_methods=["*"],
|
| 46 |
-
allow_headers=["*"],
|
| 47 |
-
)
|
| 48 |
|
| 49 |
-
|
| 50 |
-
# Clear formatted document page for root url
|
| 51 |
@app.get("/")
|
| 52 |
def home():
|
| 53 |
# Safely locate the home.html file in the same directory as this script
|
| 54 |
html_file_path = os.path.join(os.path.dirname(__file__), "home.html")
|
| 55 |
-
|
| 56 |
with open(html_file_path, "r", encoding="utf-8") as file:
|
| 57 |
html_content = file.read()
|
| 58 |
-
|
| 59 |
return HTMLResponse(content=html_content)
|
| 60 |
|
| 61 |
|
|
@@ -122,9 +110,7 @@ async def simulation_socket(websocket: WebSocket):
|
|
| 122 |
|
| 123 |
obs = _viz_env.step(action=action)
|
| 124 |
if obs.done:
|
| 125 |
-
obs = _viz_env.reset(
|
| 126 |
-
task=_viz_env.sim.task_id or "cascading_failure"
|
| 127 |
-
)
|
| 128 |
|
| 129 |
await websocket.send_json(
|
| 130 |
{
|
|
@@ -140,13 +126,10 @@ async def simulation_socket(websocket: WebSocket):
|
|
| 140 |
except (WebSocketDisconnect, RuntimeError):
|
| 141 |
return
|
| 142 |
|
| 143 |
-
|
| 144 |
def main():
|
| 145 |
"""Entry point for direct execution."""
|
| 146 |
import uvicorn
|
| 147 |
-
|
| 148 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 149 |
|
| 150 |
-
|
| 151 |
if __name__ == "__main__":
|
| 152 |
main()
|
|
|
|
| 12 |
from openenv.core.env_server.http_server import create_app
|
| 13 |
from fastapi import WebSocket, WebSocketDisconnect
|
| 14 |
from fastapi.responses import HTMLResponse
|
|
|
|
| 15 |
|
| 16 |
from server.environment import DistributedInfraEnvironment
|
| 17 |
from server.models import InfraAction, InfraObservation
|
|
|
|
| 22 |
_viz_env = DistributedInfraEnvironment()
|
| 23 |
_viz_lock = asyncio.Lock()
|
| 24 |
|
|
|
|
| 25 |
# 2. Create a "factory function" that returns our active instance
|
| 26 |
def env_factory():
|
| 27 |
return _global_env
|
| 28 |
|
|
|
|
| 29 |
# 3. Pass the callable factory function to OpenEnv
|
| 30 |
app = create_app(
|
| 31 |
env_factory,
|
|
|
|
| 34 |
env_name="distributed_infra_env",
|
| 35 |
)
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
#Clear formatted document page for root url
|
|
|
|
| 39 |
@app.get("/")
|
| 40 |
def home():
|
| 41 |
# Safely locate the home.html file in the same directory as this script
|
| 42 |
html_file_path = os.path.join(os.path.dirname(__file__), "home.html")
|
| 43 |
+
|
| 44 |
with open(html_file_path, "r", encoding="utf-8") as file:
|
| 45 |
html_content = file.read()
|
| 46 |
+
|
| 47 |
return HTMLResponse(content=html_content)
|
| 48 |
|
| 49 |
|
|
|
|
| 110 |
|
| 111 |
obs = _viz_env.step(action=action)
|
| 112 |
if obs.done:
|
| 113 |
+
obs = _viz_env.reset(task=_viz_env.sim.task_id or "cascading_failure")
|
|
|
|
|
|
|
| 114 |
|
| 115 |
await websocket.send_json(
|
| 116 |
{
|
|
|
|
| 126 |
except (WebSocketDisconnect, RuntimeError):
|
| 127 |
return
|
| 128 |
|
|
|
|
| 129 |
def main():
|
| 130 |
"""Entry point for direct execution."""
|
| 131 |
import uvicorn
|
|
|
|
| 132 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 133 |
|
|
|
|
| 134 |
if __name__ == "__main__":
|
| 135 |
main()
|