shivi3743892y8 commited on
Commit
32f20b5
·
1 Parent(s): 09fcbfb

added frontend and stimulation

Browse files
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
- scroll-padding-top: 7rem;
18
- scrollbar-width: none;
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
- html::-webkit-scrollbar,
34
- body::-webkit-scrollbar {
35
- width: 0;
36
- height: 0;
37
- display: none;
38
  }
39
 
40
- .landing-section {
41
- padding: clamp(3rem, 6vw, 4rem) 0;
 
42
  }
43
 
44
- .section-inner {
45
- margin: 0 auto;
46
- max-width: 1100px;
47
- padding: 0 40px;
48
  }
49
 
50
- @media (max-width: 768px) {
51
- .landing-section {
52
- padding: clamp(2.5rem, 7vw, 3rem) 0;
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
- @media (prefers-reduced-motion: reduce) {
105
- html {
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
- type DetailCard = {
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 NAV_ITEMS: DockItem[] = [
54
  {
55
- title: "About",
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: "Diagnostics",
105
- href: "#diagnostics",
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-it",
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 OBSERVABILITY_CARDS: DetailCard[] = [
125
  {
126
- title: "cpu_loads",
127
- detail:
128
- "Per-node CPU utilization in [0.0, 1.0], with -1.0 used when telemetry times out.",
 
 
129
  },
130
  {
131
- title: "queue_lengths",
132
- detail:
133
- "Pending request counts per node, which expose pressure before outright failure.",
 
 
134
  },
135
  {
136
- title: "latency_ms + p99_latency",
137
- detail:
138
- "Rolling end-to-end and tail latency so policies must control both average flow and spikes.",
 
 
139
  },
140
  {
141
- title: "task_hint + task_score",
142
- detail:
143
- "A natural-language objective hint plus a live benchmark score on every step.",
 
 
144
  },
145
  ];
146
 
147
- const FEATURE_ROWS: FeatureRow[] = [
148
  {
149
- title: "Stochastic Traffic",
150
- descriptor: "Burst-prone Gaussian arrivals",
151
- dotClassName: "bg-amber-400",
152
  },
153
  {
154
- title: "Non-Linear Latency",
155
- descriptor: "Penalty grows with CPU²",
156
- dotClassName: "bg-sky-400",
157
  },
158
  {
159
- title: "Cascading Load Redistribution",
160
- descriptor: "Mesh neighbor collapse",
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
- name: "Single Node Failure",
180
- difficulty: "Medium",
181
- description: "",
182
- score: 0.05,
183
- grading: "",
184
  },
185
  {
186
- name: "Cascading Failure Prevention",
187
- difficulty: "Hard",
188
- description: "",
189
- score: 0.31,
190
- grading: "",
191
  },
192
  {
193
- name: "Flash Crowd Meltdown",
194
- difficulty: "Expert",
195
- description: "",
196
- score: 0,
197
- grading: "",
198
  },
199
  ];
200
 
201
- const REWARD_TERMS: RewardTerm[] = [
202
- { formula: "+0.40 × uptime_ratio", label: "dominant signal", weight: 0.4, tone: "bg-emerald-400" },
203
- { formula: "−0.30 × normalized_latency", label: "latency penalty", weight: 0.3, tone: "bg-red-400" },
204
- { formula: "−0.20 × overload_fraction", label: "hot-node penalty", weight: 0.2, tone: "bg-zinc-400" },
205
- { formula: "−0.10 × actions / max_steps", label: "anti-spam tax", weight: 0.1, tone: "bg-zinc-500" },
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
- label: "Install Dependencies",
212
- command: "pip install -r requirements.txt",
213
  },
214
  {
215
- label: "Start API Server",
216
- command: "uvicorn server.app:app --host 0.0.0.0 --port 8000",
217
  },
218
  {
219
- label: "Run Inference",
220
- command: "python inference.py",
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: "Shows the reward-shaping change used to keep latency penalties informative instead of flattening into a dead training signal.",
229
  },
230
  {
231
  src: "/metrics/fig2_cascade_exploit_fix.png",
232
  title: "Fig 2: Cascade Exploit Fix",
233
- caption: "Illustrates the protection against oscillation-based reward farming in cascading-failure scenarios.",
234
  },
235
  {
236
  src: "/metrics/fig3_cost_latency_coupling.png",
237
  title: "Fig 3: Cost-Latency Coupling",
238
- caption: "Maps how capacity cost and service latency move together when the policy tries to stabilize the cluster efficiently.",
239
  },
240
  {
241
  src: "/metrics/fig4_curiosity_annealing.png",
242
  title: "Fig 4: Curiosity Annealing",
243
- caption: "Visualizes how intrinsic exploration pressure decays as the agent shifts from discovery toward stable control.",
244
  },
245
  ];
246
 
@@ -284,18 +234,18 @@ function RevealSection({
284
  <section
285
  id={id}
286
  ref={ref}
287
- className={`landing-section scroll-mt-28 transition-all duration-700 motion-reduce:transform-none motion-reduce:opacity-100 ${
288
  visible ? "translate-y-0 opacity-100" : "translate-y-4 opacity-0"
289
  } ${className ?? ""}`}
290
  >
291
- <div className="section-inner">{children}</div>
292
  </section>
293
  );
294
  }
295
 
296
  function SectionDivider({ label }: { label: string }) {
297
  return (
298
- <div className="my-8 sm:my-10" aria-hidden="true">
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-[#22c55e]/10 text-[#22c55e]",
313
- Medium: "bg-[#3b82f6]/10 text-[#3b82f6]",
314
- Hard: "bg-[#f97316]/10 text-[#f97316]",
315
- Expert: "bg-[#ef4444]/10 text-[#ef4444]",
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-x-clip">
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-[-11rem] top-12 h-[26rem] w-[26rem] rounded-full bg-[radial-gradient(circle,rgba(236,72,153,0.16),transparent_70%)]" />
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-[clamp(4.4rem,18vw,12rem)] font-black leading-[0.82] tracking-[-0.05em]">
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-3xl font-black leading-[0.98] tracking-[-0.02em] text-zinc-100 [text-shadow:0_0_24px_rgba(244,114,182,0.18)] sm:text-5xl lg:text-6xl ${
349
- className ?? ""
350
- }`}
351
  duration={900}
352
  filter={false}
353
  />
354
  );
355
  }
356
 
357
- function TopNavigation() {
358
- return (
359
- <header className="fixed inset-x-0 top-4 z-50 px-3 sm:px-5">
360
- <div className="mx-auto flex w-full max-w-6xl items-center justify-between gap-3 rounded-[1.6rem] border border-white/12 bg-black/55 px-3 py-3 shadow-[0_18px_80px_rgba(0,0,0,0.45)] backdrop-blur-2xl">
361
- <a href="#about" className="shrink-0 rounded-full border border-orange-400/20 bg-orange-500/10 px-3 py-2 font-mono text-xs tracking-[0.22em] text-orange-200">
362
- DIME
363
- </a>
364
- <nav className="hero-nav-scroll flex min-w-0 flex-1 items-center justify-end gap-1 overflow-x-auto">
365
- {NAV_ITEMS.map((item) => (
366
- <a
367
- key={`top-${item.href}-${item.title}`}
368
- href={item.href}
369
- className="group whitespace-nowrap rounded-full px-3 py-2 text-xs font-medium text-zinc-300 transition-all duration-300 hover:bg-white/8 hover:text-white"
370
- >
371
- <span className="relative">
372
- {item.title}
373
- <span className="absolute inset-x-0 -bottom-1 h-px origin-left scale-x-0 bg-gradient-to-r from-orange-300 via-pink-300 to-transparent transition-transform duration-300 group-hover:scale-x-100" />
374
- </span>
375
- </a>
376
- ))}
377
- </nav>
378
- </div>
379
- </header>
380
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  }
382
 
383
- function DetailGrid({ cards, columns = "lg:grid-cols-2" }: { cards: DetailCard[]; columns?: string }) {
384
- return (
385
- <div className={`grid grid-cols-1 gap-4 sm:grid-cols-2 ${columns}`}>
386
- {cards.map((card) => (
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
- return (
424
- <div ref={ref} className="mt-10 divide-y divide-zinc-900 border-y border-zinc-900">
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
- function RewardWeightMeter() {
451
- return (
452
- <div className="space-y-8">
453
- {REWARD_TERMS.map((term) => (
454
- <div key={term.formula}>
455
- <p className="font-mono text-sm text-zinc-200">{term.formula}</p>
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
- function useAnimatedTelemetry(): TelemetrySnapshot {
467
- const [snapshot, setSnapshot] = useState<TelemetrySnapshot>({
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
- useEffect(() => {
477
- let step = 0;
478
- const timer = setInterval(() => {
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
- return snapshot;
499
- }
 
 
 
 
 
 
500
 
501
- function ScrollProgress() {
502
- const [progress, setProgress] = useState(0);
503
 
504
- useEffect(() => {
505
- const onScroll = () => {
506
- const max = document.documentElement.scrollHeight - window.innerHeight;
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
- <ScrollProgress />
545
- <TopNavigation />
546
 
547
- <main className="relative pb-24">
548
  <section
549
  id="about"
550
- className="relative min-h-screen scroll-mt-28 overflow-x-clip pt-28 sm:pt-32"
551
  >
552
- <div className="section-inner">
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 -&gt;
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 -&gt;
582
- </a>
583
- </div>
584
- </div>
585
- </div>
586
 
587
- <div className="md:col-span-5 md:pl-6">
588
- <div className="rounded-xl border border-zinc-800 bg-zinc-950/95 p-5 font-mono text-sm shadow-[0_0_40px_rgba(251,146,60,0.08)]">
589
- <p className="text-zinc-500">● NODE STATUS [step: {telemetry?.step ?? "--"}]</p>
590
- <div className="mt-4 space-y-2">
591
- <p className="grid grid-cols-[auto_1fr] gap-3 text-zinc-400">
592
- <span>cpu_loads</span>
593
- <span className="min-w-0 break-words text-emerald-300">[{telemetry ? telemetry.cpuLoads.join(", ") : "loading"}]</span>
594
- </p>
595
- <p className="grid grid-cols-[auto_1fr] gap-3 text-zinc-400">
596
- <span>queue_lengths</span>
597
- <span className="min-w-0 break-words text-orange-300">[{telemetry ? telemetry.queueLengths.join(", ") : "loading"}]</span>
598
- </p>
599
- <p className="grid grid-cols-[auto_1fr] gap-3 text-zinc-400">
600
- <span>latency_ms</span>
601
- <span className="text-pink-300">{telemetry ? `${telemetry.latencyMs.toFixed(1)}ms` : "loading"}</span>
602
- </p>
603
- <p className="grid grid-cols-[auto_1fr] gap-3 text-zinc-400">
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
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  </div>
615
 
616
- <div className="md:col-span-12 pt-2">
617
- {showScrollCue ? (
618
- <div className="relative mt-4 text-center font-mono text-xs tracking-[0.18em] text-zinc-500 transition-all duration-300">
619
- ↓ scroll to explore
620
- </div>
621
- ) : null}
622
- </div>
623
  </div>
624
  </div>
625
- </section>
626
-
627
- <SectionDivider label="OBSERVABILITY" />
628
 
629
- <RevealSection id="metrics">
630
- <p className="font-mono text-xs tracking-[0.2em] text-emerald-300">[ AGENT INPUT METRICS ]</p>
631
- <AnimatedHeading words="The Signals an Agent Sees" />
632
- <TextGenerateEffect
633
- words="Observation fields come directly from the environment state, task curriculum, and partial-observability sandbox."
634
- className="mt-3 text-xs uppercase tracking-[0.14em] text-zinc-500 sm:text-sm"
635
- />
636
- <div className="mt-8 sm:mt-10">
637
- <DetailGrid cards={OBSERVABILITY_CARDS} />
638
  </div>
639
- </RevealSection>
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 vector topology, motion-interpolated node states, and live-style telemetry from the Python simulator."
648
- className="mt-3 text-xs uppercase tracking-[0.14em] text-zinc-500 sm:text-sm"
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
- <FeatureEditorialList />
 
 
 
 
661
 
662
- <div className="mt-14 sm:mt-16">
663
  <AnimatedHeading words="Four Tasks. One Unforgiving Benchmark." className="text-3xl sm:text-5xl" />
664
- <TaskBenchmarkList />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- <RewardWeightMeter />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- "step reward in [-5.0, +5.0]",
684
- "db failure => -5.0 terminal penalty",
685
- ">=80% failed nodes => -4.0 penalty",
686
- "rubric logs: format, stability, latency",
687
- "rubric logs: cascade, efficiency, throughput",
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="diagnostics">
704
- <p className="font-mono text-xs tracking-[0.2em] text-orange-300">[ BENCHMARK DIAGNOSTICS ]</p>
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
- <Image
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
- <button
767
- type="button"
768
- disabled
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 Space
772
- </button>
773
  <a
774
- href="#try-it"
775
- className="inline-flex min-w-[180px] items-center justify-center rounded-md border border-zinc-700 px-5 py-3 font-mono text-sm text-zinc-200 transition-colors hover:border-zinc-500 hover:text-white"
776
  >
777
- Run via Docker
778
  </a>
779
  </div>
780
 
781
- <div className="mt-6 inline-flex whitespace-pre-wrap rounded-xl border border-zinc-800 bg-zinc-950 px-4 py-3 font-mono text-xs text-zinc-400">
782
- docker build -t distributed-infra-env .{"\n"}docker run -p 8000:8000 distributed-infra-env
 
 
 
 
 
 
 
 
 
783
  </div>
 
784
  </div>
785
  </RevealSection>
786
  </main>
787
 
788
- <div className="mx-auto max-w-7xl px-5 sm:px-8">
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-7">
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 was built for the Meta Hackathon in
798
- Bangalore 2026 under Meta and OpenEnv, with Hugging Face as the deployment surface.
 
 
 
799
  </p>
800
  </div>
801
 
802
- <div className="lg:col-span-5">
 
 
 
 
 
 
 
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="#about" className="font-mono transition-colors hover:text-zinc-300">
806
- About DIME
807
  </a>
808
- <a href="#reward" className="font-mono transition-colors hover:text-zinc-300">
809
- Scoring Model
810
  </a>
811
- <a href="#try" className="font-mono transition-colors hover:text-zinc-300">
812
- Hugging Face Access
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 -&gt;
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 -&gt;
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 { memo, useEffect, useMemo, useState } from "react";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import { motion } from "framer-motion";
5
 
6
- type NodeRole = "core" | "gateway" | "worker" | "cache";
7
- type NodeStatus = "ok" | "warm" | "hot" | "offline";
8
 
9
- type NodeDatum = {
10
- id: number;
11
  label: string;
12
- role: NodeRole;
13
  cpu: number;
14
- mem: number;
15
  queue: number;
16
- status: NodeStatus;
 
17
  };
18
 
19
- type Snapshot = {
20
- step: number;
21
- nodes: NodeDatum[];
22
- latencyMs: number;
23
- requestRate: number;
24
- action: string;
25
- stability: number;
26
  };
27
 
28
- type Point = {
29
- x: number;
30
- y: number;
31
- };
32
-
33
- const POSITIONS: Point[] = [
34
- { x: 480, y: 280 },
35
- { x: 480, y: 92 },
36
- { x: 650, y: 150 },
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
- const STATUS_STYLE: Record<NodeStatus, { accent: string; text: string; fill: string; border: string }> = {
58
- ok: {
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
- ...node,
117
- cpu,
118
- mem,
119
- queue,
120
- status: getStatus(cpu, offline),
121
  };
122
- });
123
 
124
- const activeNodes = nodes.filter((node) => node.status !== "offline").length;
125
- const avgCpu = nodes.reduce((sum, node) => sum + node.cpu, 0) / Math.max(activeNodes, 1);
 
 
 
 
 
 
126
 
127
  return {
128
- step,
129
- nodes,
130
- latencyMs: clamp(32 + avgCpu * 96 + (surge ? Math.max(0, Math.sin((phase - 48) / 10)) * 64 : 0), 28, 240),
131
- requestRate: clamp(148 + Math.sin(t * 0.4) * 32 + (surge ? 170 : recovery ? 82 : 0), 80, 430),
132
- action: surge ? "traffic_shift + scale_out" : recovery ? "drain_and_recover" : "steady_state",
133
- stability: clamp((activeNodes / nodes.length) * (1 - Math.max(0, avgCpu - 0.58) * 0.75), 0.42, 0.99),
 
134
  };
135
  }
136
 
137
- function edgePath(from: Point, to: Point, bend = 0) {
138
- const midX = (from.x + to.x) / 2;
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 className="min-w-0 rounded-lg border border-zinc-800/80 bg-black/25 px-3 py-2">
152
- <p className="font-mono text-[10px] uppercase tracking-[0.12em] text-zinc-500">{label}</p>
153
- <p className={`mt-1 truncate font-mono text-sm ${tone}`}>{value}</p>
154
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  );
156
- }
157
 
158
- function Edge({ from, to, traffic, muted, index }: { from: Point; to: Point; traffic: number; muted: boolean; index: number }) {
159
- const path = edgePath(from, to, index % 2 === 0 ? 18 : -18);
160
- const hot = traffic > 0.78;
161
- const color = muted ? "rgba(113, 113, 122, 0.28)" : hot ? "rgba(251, 191, 36, 0.58)" : "rgba(45, 212, 191, 0.42)";
162
- const particleColor = hot ? "#fbbf24" : "#67e8f9";
163
- const duration = clamp(4.1 - traffic * 2.2, 1.45, 3.8);
164
 
165
  return (
166
- <g>
167
- <motion.path
168
- d={path}
169
- fill="none"
170
- stroke={color}
171
- strokeWidth={muted ? 0.8 : 1 + traffic * 1.1}
172
- strokeLinecap="round"
173
- initial={false}
174
- animate={{ opacity: muted ? 0.18 : 0.34 + traffic * 0.34 }}
175
- transition={{ duration: 0.5 }}
176
- />
177
- {!muted ? (
178
- <>
179
- <circle r={2.4} fill={particleColor} opacity="0.76">
180
- <animateMotion dur={`${duration}s`} repeatCount="indefinite" path={path} />
181
- </circle>
182
- <circle r={1.45} fill={particleColor} opacity="0.42">
183
- <animateMotion dur={`${duration * 1.25}s`} begin={`${duration * 0.45}s`} repeatCount="indefinite" path={path} />
184
- </circle>
185
- </>
186
- ) : null}
187
- </g>
188
  );
189
  }
190
 
191
- const NodeCard = memo(function NodeCard({ node, position }: { node: NodeDatum; position: Point }) {
192
- const style = STATUS_STYLE[node.status];
193
- const core = node.role === "core";
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
- <text x={-width / 2 + 58} y={-14} fill="#f4f4f5" fontFamily="var(--font-geist-mono), monospace" fontSize="10" fontWeight="700">
262
- {node.label}
263
- </text>
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 [snapshot, setSnapshot] = useState(() => generateSnapshot(0));
276
-
277
- useEffect(() => {
278
- let step = 0;
279
- const timer = window.setInterval(() => {
280
- step += 1;
281
- setSnapshot(generateSnapshot(step));
282
- }, 700);
283
-
284
- return () => window.clearInterval(timer);
285
- }, []);
286
 
287
- const summary = useMemo(() => {
288
- const online = snapshot.nodes.filter((node) => node.status !== "offline").length;
289
- const avgCpu = snapshot.nodes.reduce((sum, node) => sum + node.cpu, 0) / Math.max(online, 1);
290
-
291
- return {
292
- online,
293
- avgCpu,
294
- hotNodes: snapshot.nodes.filter((node) => node.status === "hot").length,
295
- };
296
- }, [snapshot.nodes]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
 
298
  return (
299
- <div className="overflow-hidden rounded-2xl border border-zinc-800/80 bg-[#080b0f] shadow-[0_24px_80px_rgba(0,0,0,0.38)]">
300
- <div className="flex flex-col gap-3 border-b border-zinc-800/70 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5">
 
 
 
301
  <div className="flex items-center gap-2">
302
- <span className="h-1.5 w-1.5 rounded-full bg-emerald-300 shadow-[0_0_12px_rgba(110,231,183,0.7)]" />
303
- <p className="font-mono text-[10px] uppercase tracking-[0.16em] text-emerald-200/80">Simulation</p>
 
 
 
 
 
 
 
 
 
 
 
 
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="relative min-h-[25rem] overflow-hidden bg-[radial-gradient(circle_at_50%_40%,rgba(45,212,191,0.12),transparent_38%),linear-gradient(180deg,rgba(24,24,27,0.28),rgba(0,0,0,0.08))]">
311
- <div className="pointer-events-none absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.035)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.035)_1px,transparent_1px)] bg-[size:40px_40px] opacity-35" />
312
- <svg viewBox="0 0 960 560" className="relative h-[25rem] w-full sm:h-[31rem]" preserveAspectRatio="xMidYMid meet" aria-label="Animated distributed cluster simulation">
313
- <defs>
314
- <radialGradient id="cluster-core-glow" cx="50%" cy="50%" r="50%">
315
- <stop offset="0%" stopColor="rgba(45, 212, 191, 0.22)" />
316
- <stop offset="65%" stopColor="rgba(45, 212, 191, 0.04)" />
317
- <stop offset="100%" stopColor="rgba(45, 212, 191, 0)" />
318
- </radialGradient>
319
- </defs>
320
-
321
- <motion.circle
322
- cx="480"
323
- cy="280"
324
- r="220"
325
- fill="url(#cluster-core-glow)"
326
- animate={{ opacity: [0.45, 0.65, 0.45], scale: [0.98, 1.02, 0.98] }}
327
- transition={{ duration: 6, repeat: Infinity, ease: "easeInOut" }}
328
- style={{ transformOrigin: "480px 280px" }}
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-2 border-t border-zinc-800/70 p-3 sm:grid-cols-5 sm:p-4">
371
- <Metric label="latency" value={`${snapshot.latencyMs.toFixed(0)}ms`} tone={snapshot.latencyMs > 150 ? "text-amber-200" : "text-sky-200"} />
372
- <Metric label="request rate" value={`${snapshot.requestRate.toFixed(0)} rps`} tone="text-violet-200" />
373
- <Metric label="avg cpu" value={`${Math.round(summary.avgCpu * 100)}%`} tone={summary.avgCpu > 0.72 ? "text-rose-200" : "text-emerald-200"} />
374
- <Metric label="stability" value={`${Math.round(snapshot.stability * 100)}%`} tone="text-emerald-200" />
375
- <Metric label="action" value={snapshot.action} tone={summary.hotNodes > 0 ? "text-amber-200" : "text-zinc-300"} />
 
 
 
 
 
 
 
 
 
 
 
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, reduceMotion]);
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
- {reduceMotion ? staticWord : normalizedWords[visibleIndex]}
50
  </span>
51
- {reduceMotion ? (
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
- </span>
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
- <Image
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 = 400,
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 running = true;
45
 
46
  const run = async () => {
47
- while (running) {
48
- setLines([]);
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
- for (let c = 0; c <= value.length; c += 1) {
56
- if (!running) return;
57
- setActiveLine({ type: line.type, text: value.slice(0, c) });
58
- await sleep(typingSpeed);
59
- }
60
 
61
- if (!running) return;
62
- setLines((prev) => [...prev, line]);
63
- setActiveLine(null);
64
- await sleep(delayBetweenCommands);
65
  }
66
 
67
- if (!running) return;
 
68
  setActiveLine(null);
69
- await sleep(2500);
70
  }
71
  };
72
 
73
  run();
74
 
75
  return () => {
76
- running = false;
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 align-middle text-zinc-100">█</span> : null}
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
- const media = window.matchMedia("(prefers-reduced-motion: reduce)");
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, reduceMotion]);
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 < renderedCount;
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
- "dev": true,
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
- "dev": true,
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()