mishig HF Staff commited on
Commit
c7f2f22
·
verified ·
1 Parent(s): 1c12887

Sync from GitHub via hub-sync

Browse files
src/app/[org]/[dataset]/[episode]/episode-viewer.tsx CHANGED
@@ -93,10 +93,12 @@ export default function EpisodeViewer({
93
 
94
  if (error) {
95
  return (
96
- <div className="flex h-screen items-center justify-center bg-slate-950 text-red-400">
97
- <div className="max-w-xl p-8 rounded bg-slate-900 border border-red-500 shadow-lg">
98
- <h2 className="text-2xl font-bold mb-4">Something went wrong</h2>
99
- <p className="text-lg font-mono whitespace-pre-wrap mb-4">{error}</p>
 
 
100
  </div>
101
  </div>
102
  );
@@ -104,7 +106,7 @@ export default function EpisodeViewer({
104
 
105
  if (!data) {
106
  return (
107
- <div className="relative h-screen bg-slate-950">
108
  <Loading />
109
  </div>
110
  );
@@ -480,105 +482,54 @@ function EpisodeViewerInner({
480
  }
481
  };
482
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  return (
484
- <div className="flex flex-col h-screen max-h-screen bg-slate-950 text-gray-200">
485
  {/* Top tab bar */}
486
- <div className="flex items-center border-b border-slate-700 bg-slate-900 shrink-0">
487
- <button
488
- className={`px-6 py-2.5 text-sm font-medium transition-colors relative ${
489
- activeTab === "episodes"
490
- ? "text-orange-400"
491
- : "text-slate-400 hover:text-slate-200"
492
- }`}
493
- onClick={() => handleTabChange("episodes")}
494
- >
495
- Episodes
496
- {activeTab === "episodes" && (
497
- <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-orange-500" />
498
- )}
499
- </button>
500
  {hasURDFSupport(datasetInfo.robot_type) &&
501
  datasetInfo.codebase_version >= "v3.0" && (
502
- <button
503
- className={`px-6 py-2.5 text-sm font-medium transition-colors relative ${
504
- activeTab === "urdf"
505
- ? "text-orange-400"
506
- : "text-slate-400 hover:text-slate-200"
507
- }`}
508
- onClick={() => handleTabChange("urdf")}
509
- >
510
- 3D Replay
511
- {activeTab === "urdf" && (
512
- <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-orange-500" />
513
- )}
514
- </button>
515
- )}
516
- <button
517
- className={`px-6 py-2.5 text-sm font-medium transition-colors relative ${
518
- activeTab === "statistics"
519
- ? "text-orange-400"
520
- : "text-slate-400 hover:text-slate-200"
521
- }`}
522
- onClick={() => handleTabChange("statistics")}
523
- >
524
- Statistics
525
- {activeTab === "statistics" && (
526
- <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-orange-500" />
527
  )}
528
- </button>
529
- <button
530
- className={`px-6 py-2.5 text-sm font-medium transition-colors relative ${
531
- activeTab === "filtering"
532
- ? "text-orange-400"
533
- : "text-slate-400 hover:text-slate-200"
534
- }`}
535
- onClick={() => handleTabChange("filtering")}
536
- >
537
- Filtering
538
- {activeTab === "filtering" && (
539
- <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-orange-500" />
540
- )}
541
- </button>
542
- <button
543
- className={`px-6 py-2.5 text-sm font-medium transition-colors relative ${
544
- activeTab === "frames"
545
- ? "text-orange-400"
546
- : "text-slate-400 hover:text-slate-200"
547
- }`}
548
- onClick={() => handleTabChange("frames")}
549
- >
550
- Frames
551
- {activeTab === "frames" && (
552
- <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-orange-500" />
553
- )}
554
- </button>
555
- <button
556
- className={`px-6 py-2.5 text-sm font-medium transition-colors relative ${
557
- activeTab === "insights"
558
- ? "text-orange-400"
559
- : "text-slate-400 hover:text-slate-200"
560
- }`}
561
- onClick={() => handleTabChange("insights")}
562
- >
563
- Action Insights
564
- {activeTab === "insights" && (
565
- <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-orange-500" />
566
- )}
567
- </button>
568
- <button
569
- className={`px-6 py-2.5 text-sm font-medium transition-colors relative ${
570
- activeTab === "doctor"
571
- ? "text-orange-400"
572
- : "text-slate-400 hover:text-slate-200"
573
- }`}
574
- onClick={() => handleTabChange("doctor")}
575
  title="Dataset quality diagnostics (powered by lerobot-doctor)"
576
- >
577
- Doctor
578
- {activeTab === "doctor" && (
579
- <span className="absolute bottom-0 left-0 right-0 h-0.5 bg-orange-500" />
580
- )}
581
- </button>
582
  </div>
583
 
584
  {/* Body: sidebar + content */}
@@ -614,32 +565,32 @@ function EpisodeViewerInner({
614
 
615
  {activeTab === "episodes" && (
616
  <>
617
- <div className="flex items-center justify-start my-4">
618
  <a
619
  href="https://github.com/huggingface/lerobot"
620
  target="_blank"
621
- className="block"
622
  >
623
  {/* eslint-disable-next-line @next/next/no-img-element */}
624
  <img
625
  src="https://github.com/huggingface/lerobot/raw/main/media/readme/lerobot-logo-thumbnail.png"
626
  alt="LeRobot Logo"
627
- className="w-32"
628
  />
629
  </a>
630
 
631
- <div>
632
  <a
633
  href={`https://huggingface.co/datasets/${datasetInfo.repoId}`}
634
  target="_blank"
 
635
  >
636
- <p className="text-lg font-semibold">
637
  {datasetInfo.repoId}
638
  </p>
639
  </a>
640
-
641
- <p className="font-mono text-lg font-semibold">
642
- episode {episodeId}
643
  </p>
644
  </div>
645
  </div>
@@ -654,19 +605,15 @@ function EpisodeViewerInner({
654
 
655
  {/* Language Instruction */}
656
  {task && (
657
- <div className="mb-6 p-4 bg-slate-800 rounded-lg border border-slate-600">
658
- <p className="text-slate-300">
659
- <span className="font-semibold text-slate-100">
660
- Language Instruction:
661
- </span>
662
  </p>
663
- <div className="mt-2 text-slate-300">
664
  {task
665
  .split("\n")
666
  .map((instruction: string, index: number) => (
667
- <p key={index} className="mb-1">
668
- {instruction}
669
- </p>
670
  ))}
671
  </div>
672
  </div>
 
93
 
94
  if (error) {
95
  return (
96
+ <div className="flex h-screen items-center justify-center bg-[var(--bg)] text-red-300">
97
+ <div className="panel-raised max-w-xl p-6 border-red-500/40">
98
+ <h2 className="text-xl font-medium mb-3">Something went wrong</h2>
99
+ <p className="text-sm font-mono whitespace-pre-wrap text-red-200/90">
100
+ {error}
101
+ </p>
102
  </div>
103
  </div>
104
  );
 
106
 
107
  if (!data) {
108
  return (
109
+ <div className="relative h-screen bg-[var(--bg)]">
110
  <Loading />
111
  </div>
112
  );
 
482
  }
483
  };
484
 
485
+ const TabButton = ({
486
+ tab,
487
+ label,
488
+ title,
489
+ }: {
490
+ tab: ActiveTab;
491
+ label: string;
492
+ title?: string;
493
+ }) => {
494
+ const active = activeTab === tab;
495
+ return (
496
+ <button
497
+ onClick={() => handleTabChange(tab)}
498
+ title={title}
499
+ className={`relative px-5 py-3 text-xs font-medium tracking-wide uppercase transition-colors ${
500
+ active ? "text-cyan-300" : "text-slate-400 hover:text-slate-100"
501
+ }`}
502
+ >
503
+ {label}
504
+ <span
505
+ className={`pointer-events-none absolute bottom-0 left-3 right-3 h-px transition-all ${
506
+ active
507
+ ? "bg-cyan-400 shadow-[0_0_8px_rgba(56,189,248,0.55)]"
508
+ : "bg-transparent"
509
+ }`}
510
+ />
511
+ </button>
512
+ );
513
+ };
514
+
515
  return (
516
+ <div className="flex flex-col h-screen max-h-screen bg-[var(--bg)] text-[var(--text-primary)]">
517
  {/* Top tab bar */}
518
+ <div className="flex items-center border-b border-white/5 bg-[var(--surface-0)] shrink-0">
519
+ <TabButton tab="episodes" label="Episodes" />
 
 
 
 
 
 
 
 
 
 
 
 
520
  {hasURDFSupport(datasetInfo.robot_type) &&
521
  datasetInfo.codebase_version >= "v3.0" && (
522
+ <TabButton tab="urdf" label="3D Replay" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
  )}
524
+ <TabButton tab="statistics" label="Statistics" />
525
+ <TabButton tab="filtering" label="Filtering" />
526
+ <TabButton tab="frames" label="Frames" />
527
+ <TabButton tab="insights" label="Action Insights" />
528
+ <TabButton
529
+ tab="doctor"
530
+ label="Doctor"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531
  title="Dataset quality diagnostics (powered by lerobot-doctor)"
532
+ />
 
 
 
 
 
533
  </div>
534
 
535
  {/* Body: sidebar + content */}
 
565
 
566
  {activeTab === "episodes" && (
567
  <>
568
+ <div className="flex items-center gap-4 mb-2">
569
  <a
570
  href="https://github.com/huggingface/lerobot"
571
  target="_blank"
572
+ className="block shrink-0 opacity-90 hover:opacity-100 transition-opacity"
573
  >
574
  {/* eslint-disable-next-line @next/next/no-img-element */}
575
  <img
576
  src="https://github.com/huggingface/lerobot/raw/main/media/readme/lerobot-logo-thumbnail.png"
577
  alt="LeRobot Logo"
578
+ className="w-24"
579
  />
580
  </a>
581
 
582
+ <div className="min-w-0">
583
  <a
584
  href={`https://huggingface.co/datasets/${datasetInfo.repoId}`}
585
  target="_blank"
586
+ className="text-slate-200 hover:text-cyan-300 transition-colors"
587
  >
588
+ <p className="text-base font-medium truncate">
589
  {datasetInfo.repoId}
590
  </p>
591
  </a>
592
+ <p className="text-[10px] uppercase tracking-wide text-slate-500 mt-0.5 tabular">
593
+ Episode · {episodeId}
 
594
  </p>
595
  </div>
596
  </div>
 
605
 
606
  {/* Language Instruction */}
607
  {task && (
608
+ <div className="mb-6 panel p-4">
609
+ <p className="text-[10px] uppercase tracking-wide text-slate-500">
610
+ Language Instruction
 
 
611
  </p>
612
+ <div className="mt-1.5 space-y-0.5 text-sm text-slate-200">
613
  {task
614
  .split("\n")
615
  .map((instruction: string, index: number) => (
616
+ <p key={index}>{instruction}</p>
 
 
617
  ))}
618
  </div>
619
  </div>
src/app/explore/explore-grid.tsx CHANGED
@@ -26,14 +26,16 @@ export default function ExploreGrid({
26
  const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);
27
 
28
  return (
29
- <main className="p-8">
30
- <h1 className="text-2xl font-bold mb-6">Explore LeRobot Datasets</h1>
31
- <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
 
 
32
  {datasets.map((ds, idx) => (
33
  <Link
34
  key={ds.id}
35
  href={`/${ds.id}`}
36
- className="relative border rounded-lg p-4 bg-white shadow hover:shadow-lg transition overflow-hidden h-48 flex items-end group"
37
  onMouseEnter={() => {
38
  const vid = videoRefs.current[idx];
39
  if (vid) vid.play();
@@ -64,36 +66,36 @@ export default function ExploreGrid({
64
  }
65
  }}
66
  />
67
- <div className="absolute top-0 left-0 w-full h-full bg-black/40 z-10 pointer-events-none" />
68
- <div className="relative z-20 font-mono text-blue-100 break-all text-sm bg-black/60 backdrop-blur px-2 py-1 rounded shadow">
69
  {ds.id}
70
  </div>
71
  </Link>
72
  ))}
73
  </div>
74
- <div className="flex justify-center mt-8 gap-4">
75
  {currentPage > 1 && (
76
  <button
77
- className="px-6 py-2 bg-gray-600 text-white rounded shadow hover:bg-gray-700 transition"
78
  onClick={() => {
79
  const params = new URLSearchParams(window.location.search);
80
  params.set("p", (currentPage - 1).toString());
81
  window.location.search = params.toString();
82
  }}
83
  >
84
- Previous
85
  </button>
86
  )}
87
  {currentPage < totalPages && (
88
  <button
89
- className="px-6 py-2 bg-blue-600 text-white rounded shadow hover:bg-blue-700 transition"
90
  onClick={() => {
91
  const params = new URLSearchParams(window.location.search);
92
  params.set("p", (currentPage + 1).toString());
93
  window.location.search = params.toString();
94
  }}
95
  >
96
- Next
97
  </button>
98
  )}
99
  </div>
 
26
  const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);
27
 
28
  return (
29
+ <main className="px-8 py-10 max-w-7xl mx-auto">
30
+ <h1 className="text-xl font-medium tracking-tight mb-6 text-slate-100">
31
+ Explore LeRobot datasets
32
+ </h1>
33
+ <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
34
  {datasets.map((ds, idx) => (
35
  <Link
36
  key={ds.id}
37
  href={`/${ds.id}`}
38
+ className="relative rounded-md overflow-hidden h-48 flex items-end group panel hover:border-cyan-400/40 transition-colors"
39
  onMouseEnter={() => {
40
  const vid = videoRefs.current[idx];
41
  if (vid) vid.play();
 
66
  }
67
  }}
68
  />
69
+ <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent z-10 pointer-events-none" />
70
+ <div className="relative z-20 w-full px-3 py-2 text-xs text-slate-200 truncate">
71
  {ds.id}
72
  </div>
73
  </Link>
74
  ))}
75
  </div>
76
+ <div className="flex justify-center mt-8 gap-3">
77
  {currentPage > 1 && (
78
  <button
79
+ className="px-4 py-2 rounded-md panel text-sm text-slate-300 hover:text-slate-100 hover:bg-white/5 transition-colors"
80
  onClick={() => {
81
  const params = new URLSearchParams(window.location.search);
82
  params.set("p", (currentPage - 1).toString());
83
  window.location.search = params.toString();
84
  }}
85
  >
86
+ Previous
87
  </button>
88
  )}
89
  {currentPage < totalPages && (
90
  <button
91
+ className="px-4 py-2 rounded-md bg-cyan-400/10 border border-cyan-400/30 text-cyan-300 text-sm hover:bg-cyan-400/15 transition-colors"
92
  onClick={() => {
93
  const params = new URLSearchParams(window.location.search);
94
  params.set("p", (currentPage + 1).toString());
95
  window.location.search = params.toString();
96
  }}
97
  >
98
+ Next
99
  </button>
100
  )}
101
  </div>
src/app/globals.css CHANGED
@@ -1,35 +1,88 @@
1
  @import "tailwindcss";
2
 
 
 
 
3
  :root {
4
- --background: #ffffff;
5
- --foreground: #171717;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  }
7
 
8
  @theme inline {
9
- --color-background: var(--background);
10
- --color-foreground: var(--foreground);
 
 
 
11
  --font-sans: var(--font-geist-sans);
12
  --font-mono: var(--font-geist-mono);
13
  }
14
 
15
- @media (prefers-color-scheme: dark) {
16
- :root {
17
- --background: #0a0a0a;
18
- --foreground: #ededed;
19
- }
20
- }
21
-
22
  html {
23
- /* Scale all rem-based sizes (text, padding, buttons) up ~12% */
24
  font-size: 18px;
25
  }
26
 
27
  body {
28
- background: var(--background);
29
- color: var(--foreground);
30
- font-family: Arial, Helvetica, sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
 
 
33
  .video-background {
34
  @apply fixed top-0 right-0 bottom-0 left-0 -z-10 overflow-hidden w-screen h-screen;
35
  }
@@ -41,13 +94,13 @@ body {
41
  height: auto;
42
  transform: translate(-50%, -50%);
43
  object-fit: cover;
44
- filter: brightness(0.9);
45
  }
46
 
47
  @keyframes fadeInUp {
48
  from {
49
  opacity: 0;
50
- transform: translateY(20px);
51
  }
52
  to {
53
  opacity: 1;
@@ -56,5 +109,5 @@ body {
56
  }
57
 
58
  .animate-fade-in-up {
59
- animation: fadeInUp 0.6s ease-out forwards;
60
  }
 
1
  @import "tailwindcss";
2
 
3
+ /* ── Design tokens ────────────────────────────────────────────────
4
+ Consolidated palette + surface scale. Use via Tailwind arbitrary
5
+ values (bg-[var(--surface-1)]) or via the semantic helpers below. */
6
  :root {
7
+ /* surface / depth scale */
8
+ --bg: #0a0e17;
9
+ --surface-0: #0d1220;
10
+ --surface-1: #11172a;
11
+ --surface-2: #151c33;
12
+ --border-subtle: rgba(255, 255, 255, 0.05);
13
+ --border-strong: rgba(255, 255, 255, 0.1);
14
+
15
+ /* text scale */
16
+ --text-primary: #e7ebf3;
17
+ --text-muted: #9aa3b5;
18
+ --text-faint: #5b6274;
19
+
20
+ /* single accent (cyan). Orange is reserved for destructive. */
21
+ --accent: #38bdf8;
22
+ --accent-soft: rgba(56, 189, 248, 0.18);
23
+ --accent-ring: rgba(56, 189, 248, 0.55);
24
+
25
+ --radius: 6px;
26
  }
27
 
28
  @theme inline {
29
+ --color-bg: var(--bg);
30
+ --color-surface-0: var(--surface-0);
31
+ --color-surface-1: var(--surface-1);
32
+ --color-surface-2: var(--surface-2);
33
+ --color-accent: var(--accent);
34
  --font-sans: var(--font-geist-sans);
35
  --font-mono: var(--font-geist-mono);
36
  }
37
 
 
 
 
 
 
 
 
38
  html {
 
39
  font-size: 18px;
40
  }
41
 
42
  body {
43
+ background: var(--bg);
44
+ color: var(--text-primary);
45
+ font-feature-settings:
46
+ "cv11" 1,
47
+ "ss01" 1;
48
+ -webkit-font-smoothing: antialiased;
49
+ text-rendering: optimizeLegibility;
50
+ }
51
+
52
+ /* Numeric readouts always use tabular figures so rows don't jitter. */
53
+ .tabular,
54
+ [data-tabular] {
55
+ font-variant-numeric: tabular-nums;
56
+ }
57
+
58
+ /* ── Semantic helpers ────────────────────────────────────────────── */
59
+ .panel {
60
+ @apply rounded-md border border-white/5 bg-white/[0.02];
61
+ }
62
+ .panel-raised {
63
+ @apply rounded-md border border-white/10 bg-white/[0.03];
64
+ }
65
+
66
+ /* Scrollbar: thin, subtle, matches the surface palette. */
67
+ *::-webkit-scrollbar {
68
+ width: 10px;
69
+ height: 10px;
70
+ }
71
+ *::-webkit-scrollbar-track {
72
+ background: transparent;
73
+ }
74
+ *::-webkit-scrollbar-thumb {
75
+ background: rgba(255, 255, 255, 0.06);
76
+ border-radius: 10px;
77
+ border: 2px solid transparent;
78
+ background-clip: padding-box;
79
+ }
80
+ *::-webkit-scrollbar-thumb:hover {
81
+ background-color: rgba(255, 255, 255, 0.12);
82
+ background-clip: padding-box;
83
  }
84
 
85
+ /* ── Background video (used on the landing page) ─────────────────── */
86
  .video-background {
87
  @apply fixed top-0 right-0 bottom-0 left-0 -z-10 overflow-hidden w-screen h-screen;
88
  }
 
94
  height: auto;
95
  transform: translate(-50%, -50%);
96
  object-fit: cover;
97
+ filter: brightness(0.55) saturate(0.85);
98
  }
99
 
100
  @keyframes fadeInUp {
101
  from {
102
  opacity: 0;
103
+ transform: translateY(16px);
104
  }
105
  to {
106
  opacity: 1;
 
109
  }
110
 
111
  .animate-fade-in-up {
112
+ animation: fadeInUp 0.55s ease-out forwards;
113
  }
src/app/page.tsx CHANGED
@@ -171,7 +171,7 @@ function HomeInner() {
171
  {/* Title */}
172
  <h1 className="text-4xl md:text-5xl font-bold mb-2 drop-shadow-lg tracking-tight">
173
  LeRobot{" "}
174
- <span className="bg-gradient-to-r from-sky-400 to-indigo-400 bg-clip-text text-transparent">
175
  Dataset
176
  </span>{" "}
177
  Visualizer
@@ -208,13 +208,13 @@ function HomeInner() {
208
  onKeyDown={handleKeyDown}
209
  onFocus={() => query.trim() && setShowSuggestions(true)}
210
  placeholder="Enter dataset id (e.g. lerobot/pusht)"
211
- className="pl-10 pr-4 py-2.5 rounded-md text-base text-white bg-white/10 backdrop-blur-sm border border-white/30 focus:outline-none focus:border-sky-400 focus:bg-white/15 w-[380px] shadow-md placeholder:text-white/40 transition-colors"
212
  autoComplete="off"
213
  />
214
 
215
  {/* Suggestions dropdown */}
216
  {showSuggestions && (
217
- <ul className="absolute left-0 right-0 top-full mt-1 rounded-md bg-slate-900/95 backdrop-blur-sm border border-white/10 shadow-xl overflow-hidden z-50 max-h-64 overflow-y-auto">
218
  {isLoading ? (
219
  <li className="flex items-center gap-2.5 px-4 py-3 text-sm text-white/50">
220
  <svg
@@ -246,8 +246,8 @@ function HomeInner() {
246
  type="button"
247
  className={`w-full text-left px-4 py-2.5 text-sm transition-colors ${
248
  i === activeIndex
249
- ? "bg-sky-600 text-white"
250
- : "text-slate-200 hover:bg-slate-700"
251
  }`}
252
  onMouseDown={(e) => {
253
  e.preventDefault();
@@ -272,7 +272,7 @@ function HomeInner() {
272
 
273
  <button
274
  type="submit"
275
- className="px-5 py-2.5 rounded-md bg-sky-500 text-white font-semibold text-base hover:bg-sky-400 active:scale-95 transition-all shadow-md flex items-center gap-2"
276
  >
277
  Go
278
  <kbd className="text-xs font-mono bg-white/20 rounded px-1 py-0.5 leading-tight">
@@ -291,7 +291,7 @@ function HomeInner() {
291
  <button
292
  key={ds}
293
  type="button"
294
- className="px-3 py-1.5 rounded-full border border-white/20 text-sm text-sky-200/80 hover:border-sky-400 hover:text-white hover:bg-sky-500/15 active:scale-95 transition-all backdrop-blur-sm"
295
  onClick={() => navigate(ds)}
296
  >
297
  {ds}
@@ -303,7 +303,7 @@ function HomeInner() {
303
  {/* Explore CTA */}
304
  <Link
305
  href="/explore"
306
- className="inline-flex items-center gap-2 px-6 py-3 mt-8 rounded-md bg-sky-500/90 backdrop-blur-sm text-white font-semibold text-lg shadow-lg hover:bg-sky-400 active:scale-95 transition-all"
307
  >
308
  Explore Open Datasets
309
  <svg
 
171
  {/* Title */}
172
  <h1 className="text-4xl md:text-5xl font-bold mb-2 drop-shadow-lg tracking-tight">
173
  LeRobot{" "}
174
+ <span className="bg-gradient-to-r from-cyan-400 to-sky-300 bg-clip-text text-transparent">
175
  Dataset
176
  </span>{" "}
177
  Visualizer
 
208
  onKeyDown={handleKeyDown}
209
  onFocus={() => query.trim() && setShowSuggestions(true)}
210
  placeholder="Enter dataset id (e.g. lerobot/pusht)"
211
+ className="pl-10 pr-4 py-2.5 rounded-md text-base text-white bg-white/10 backdrop-blur-sm border border-white/30 focus:outline-none focus:border-cyan-400 focus:bg-white/15 w-[380px] shadow-md placeholder:text-white/40 transition-colors"
212
  autoComplete="off"
213
  />
214
 
215
  {/* Suggestions dropdown */}
216
  {showSuggestions && (
217
+ <ul className="absolute left-0 right-0 top-full mt-1 rounded-md bg-[var(--surface-1)]/95 backdrop-blur-sm border border-white/10 shadow-xl overflow-hidden z-50 max-h-64 overflow-y-auto">
218
  {isLoading ? (
219
  <li className="flex items-center gap-2.5 px-4 py-3 text-sm text-white/50">
220
  <svg
 
246
  type="button"
247
  className={`w-full text-left px-4 py-2.5 text-sm transition-colors ${
248
  i === activeIndex
249
+ ? "bg-cyan-500 text-white"
250
+ : "text-slate-200 hover:bg-white/10"
251
  }`}
252
  onMouseDown={(e) => {
253
  e.preventDefault();
 
272
 
273
  <button
274
  type="submit"
275
+ className="px-5 py-2.5 rounded-md bg-cyan-500 text-white font-semibold text-base hover:bg-cyan-400 active:scale-95 transition-all shadow-md flex items-center gap-2"
276
  >
277
  Go
278
  <kbd className="text-xs font-mono bg-white/20 rounded px-1 py-0.5 leading-tight">
 
291
  <button
292
  key={ds}
293
  type="button"
294
+ className="px-3 py-1.5 rounded-full border border-white/20 text-sm text-cyan-200/80 hover:border-cyan-400 hover:text-white hover:bg-cyan-500/15 active:scale-95 transition-all backdrop-blur-sm"
295
  onClick={() => navigate(ds)}
296
  >
297
  {ds}
 
303
  {/* Explore CTA */}
304
  <Link
305
  href="/explore"
306
+ className="inline-flex items-center gap-2 px-6 py-3 mt-8 rounded-md bg-cyan-500/90 backdrop-blur-sm text-white font-semibold text-lg shadow-lg hover:bg-cyan-400 active:scale-95 transition-all"
307
  >
308
  Explore Open Datasets
309
  <svg
src/components/action-insights-panel.tsx CHANGED
@@ -70,7 +70,7 @@ function FullscreenWrapper({ children }: { children: React.ReactNode }) {
70
  <div className="relative">
71
  <button
72
  onClick={() => setFs((v) => !v)}
73
- className="absolute top-3 right-3 z-10 p-1.5 rounded bg-slate-700/60 hover:bg-slate-600 text-slate-400 hover:text-slate-200 transition-colors backdrop-blur-sm"
74
  title={fs ? "Exit fullscreen" : "Fullscreen"}
75
  >
76
  <svg
@@ -102,10 +102,10 @@ function FullscreenWrapper({ children }: { children: React.ReactNode }) {
102
  </svg>
103
  </button>
104
  {fs ? (
105
- <div className="fixed inset-0 z-50 bg-slate-950/95 overflow-auto p-6">
106
  <button
107
  onClick={() => setFs(false)}
108
- className="fixed top-4 right-4 z-50 p-2 rounded bg-slate-700/80 hover:bg-slate-600 text-slate-300 hover:text-white transition-colors"
109
  title="Exit fullscreen (Esc)"
110
  >
111
  <svg
@@ -145,7 +145,7 @@ function FlagBtn({ id }: { id: number }) {
145
  <button
146
  onClick={() => toggle(id)}
147
  title={flagged ? "Unflag episode" : "Flag for review"}
148
- className={`p-0.5 rounded transition-colors ${flagged ? "text-orange-400" : "text-slate-600 hover:text-slate-400"}`}
149
  >
150
  <svg
151
  xmlns="http://www.w3.org/2000/svg"
@@ -170,7 +170,7 @@ function FlagAllBtn({ ids, label }: { ids: number[]; label?: string }) {
170
  return (
171
  <button
172
  onClick={() => addMany(ids)}
173
- className="text-xs text-slate-500 hover:text-orange-400 transition-colors flex items-center gap-1"
174
  >
175
  <svg
176
  xmlns="http://www.w3.org/2000/svg"
@@ -329,7 +329,7 @@ function AutocorrelationSection({
329
  return <p className="text-slate-500 italic">No action columns found.</p>;
330
 
331
  return (
332
- <div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700 space-y-4">
333
  <div>
334
  <div className="flex items-center gap-2">
335
  <h3 className="text-sm font-semibold text-slate-200">
@@ -343,7 +343,7 @@ function AutocorrelationSection({
343
  Shows how correlated each action dimension is with itself over
344
  increasing time lags. Where autocorrelation drops below 0.5
345
  suggests a{" "}
346
- <span className="text-orange-400 font-medium">
347
  natural action chunk boundary
348
  </span>{" "}
349
  — actions beyond this lag are essentially independent, so
@@ -368,12 +368,12 @@ function AutocorrelationSection({
368
  </div>
369
 
370
  {suggestedChunk && (
371
- <div className="flex items-center gap-3 bg-orange-500/10 border border-orange-500/30 rounded-md px-4 py-2.5">
372
- <span className="text-orange-400 font-bold text-lg tabular-nums">
373
  {suggestedChunk}
374
  </span>
375
  <div>
376
- <p className="text-sm text-orange-300 font-medium">
377
  Suggested chunk length: {suggestedChunk} steps (
378
  {(suggestedChunk / fps).toFixed(2)}s)
379
  </p>
@@ -639,7 +639,7 @@ function ActionVelocitySection({
639
  );
640
 
641
  return (
642
- <div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700 space-y-4">
643
  <div>
644
  <div className="flex items-center gap-2">
645
  <h3 className="text-sm font-semibold text-slate-200">
@@ -700,7 +700,7 @@ function ActionVelocitySection({
700
  return (
701
  <div
702
  key={s.name}
703
- className={`rounded-md px-2.5 py-2 space-y-1 ${dimmed ? "bg-slate-900/30 opacity-50" : "bg-slate-900/50"}`}
704
  >
705
  <p
706
  className={`text-xs font-medium truncate ${dimmed ? "text-slate-500" : "text-slate-200"}`}
@@ -743,7 +743,7 @@ function ActionVelocitySection({
743
  );
744
  })}
745
  </svg>
746
- <div className="h-1 w-full bg-slate-700 rounded-full overflow-hidden">
747
  <div
748
  className="h-full rounded-full"
749
  style={{
@@ -764,7 +764,7 @@ function ActionVelocitySection({
764
  </div>
765
 
766
  {insight && (
767
- <div className="bg-slate-900/60 rounded-md px-4 py-3 border border-slate-700/60 space-y-1.5">
768
  <p className="text-sm font-medium text-slate-200">
769
  Overall:{" "}
770
  <span className={insight.verdict.color}>
@@ -792,7 +792,7 @@ function JerkyEpisodesList({ episodes }: { episodes: JerkyEpisode[] }) {
792
  const display = showAll ? episodes : episodes.slice(0, 15);
793
 
794
  return (
795
- <div className="bg-slate-900/60 rounded-md px-4 py-3 border border-slate-700/60 space-y-2">
796
  <div className="flex items-center justify-between">
797
  <p className="text-sm font-medium text-slate-200">
798
  Most Jerky Episodes{" "}
@@ -815,7 +815,7 @@ function JerkyEpisodesList({ episodes }: { episodes: JerkyEpisode[] }) {
815
  <div className="max-h-48 overflow-y-auto">
816
  <table className="w-full text-xs">
817
  <thead>
818
- <tr className="text-slate-500 border-b border-slate-700">
819
  <th className="w-5 py-1" />
820
  <th className="text-left py-1 pr-3">Episode</th>
821
  <th className="text-right py-1">Mean |Δa|</th>
@@ -825,7 +825,7 @@ function JerkyEpisodesList({ episodes }: { episodes: JerkyEpisode[] }) {
825
  {display.map((e) => (
826
  <tr
827
  key={e.episodeIndex}
828
- className="border-b border-slate-800/40 text-slate-300"
829
  >
830
  <td className="py-1">
831
  <FlagBtn id={e.episodeIndex} />
@@ -856,7 +856,7 @@ function VarianceHeatmap({
856
 
857
  if (loading) {
858
  return (
859
- <div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700">
860
  <h3 className="text-sm font-semibold text-slate-200 mb-2">
861
  Cross-Episode Action Variance
862
  </h3>
@@ -884,7 +884,7 @@ function VarianceHeatmap({
884
 
885
  if (!data) {
886
  return (
887
- <div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700">
888
  <h3 className="text-sm font-semibold text-slate-200 mb-2">
889
  Cross-Episode Action Variance
890
  </h3>
@@ -924,7 +924,7 @@ function VarianceHeatmap({
924
  }
925
 
926
  return (
927
- <div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700 space-y-4">
928
  <div>
929
  <div className="flex items-center gap-2">
930
  <h3 className="text-sm font-semibold text-slate-200">
@@ -937,10 +937,7 @@ function VarianceHeatmap({
937
  <p className="text-xs text-slate-400">
938
  Shows how much each action dimension varies across episodes at
939
  each point in time (normalized 0–100%).
940
- <span className="text-orange-400">
941
- {" "}
942
- High-variance regions
943
- </span>{" "}
944
  indicate multi-modal or inconsistent demonstrations — generative
945
  policies (diffusion, flow-matching) and action chunking help here
946
  by modeling multiple modes.
@@ -1135,7 +1132,7 @@ function SpeedVarianceSection({
1135
  const barW = Math.max(8, Math.floor((isFs ? 900 : 500) / bins.length));
1136
 
1137
  return (
1138
- <div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700 space-y-4">
1139
  <div>
1140
  <div className="flex items-center gap-2">
1141
  <h3 className="text-sm font-semibold text-slate-200">
@@ -1148,11 +1145,11 @@ function SpeedVarianceSection({
1148
  <p className="text-xs text-slate-400">
1149
  Distribution of average execution speed (mean ‖Δa<sub>t</sub>‖ per
1150
  frame) across all episodes. Different human demonstrators often
1151
- execute at{" "}
1152
- <span className="text-orange-400">different speeds</span>,
1153
- creating artificial multimodality in the action distribution that
1154
- confuses the policy. A coefficient of variation (CV) above 0.3
1155
- strongly suggests normalizing trajectory speed before training.
1156
  <br />
1157
  <span className="text-slate-500">
1158
  Based on &quot;Is Diversity All You Need&quot; (AGI-Bot, 2025)
@@ -1234,7 +1231,7 @@ function SpeedVarianceSection({
1234
  </div>
1235
  </div>
1236
 
1237
- <div className="bg-slate-900/60 rounded-md px-4 py-3 border border-slate-700/60 space-y-1.5">
1238
  <p className="text-sm font-medium text-slate-200">
1239
  Verdict: <span className={verdict.color}>{verdict.label}</span>
1240
  </p>
@@ -1402,7 +1399,7 @@ function StateActionAlignmentSection({
1402
  : "current episode";
1403
 
1404
  return (
1405
- <div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700 space-y-4">
1406
  <div>
1407
  <div className="flex items-center gap-2">
1408
  <h3 className="text-sm font-semibold text-slate-200">
@@ -1415,11 +1412,11 @@ function StateActionAlignmentSection({
1415
  <p className="text-xs text-slate-400">
1416
  Per-dimension cross-correlation between Δaction<sub>d</sub>(t) and
1417
  Δstate<sub>d</sub>(t+lag), aggregated as
1418
- <span className="text-orange-400"> max</span>,{" "}
1419
  <span className="text-slate-200">mean</span>, and
1420
  <span className="text-blue-400"> min</span> across all matched
1421
  action–state pairs. The{" "}
1422
- <span className="text-orange-400">peak lag</span> reveals the
1423
  effective control delay — the time between when an action is
1424
  commanded and when the corresponding state changes.
1425
  <br />
@@ -1461,12 +1458,12 @@ function StateActionAlignmentSection({
1461
  </div>
1462
 
1463
  {meanPeakLag !== 0 && (
1464
- <div className="flex items-center gap-3 bg-orange-500/10 border border-orange-500/30 rounded-md px-4 py-2.5">
1465
- <span className="text-orange-400 font-bold text-lg tabular-nums">
1466
  {meanPeakLag}
1467
  </span>
1468
  <div>
1469
- <p className="text-sm text-orange-300 font-medium">
1470
  Mean control delay: {meanPeakLag} step
1471
  {Math.abs(meanPeakLag) !== 1 ? "s" : ""} (
1472
  {(meanPeakLag / fps).toFixed(3)}s)
@@ -1555,7 +1552,7 @@ function StateActionAlignmentSection({
1555
 
1556
  <div className="flex flex-wrap gap-x-4 gap-y-1 px-1">
1557
  <div className="flex items-center gap-1.5">
1558
- <span className="w-3 h-[3px] rounded-full shrink-0 bg-orange-500" />
1559
  <span className="text-xs text-slate-400">
1560
  max (peak: lag {maxPeakLag}, r={maxPeakCorr.toFixed(3)})
1561
  </span>
@@ -1622,7 +1619,7 @@ function ActionInsightsPanel({
1622
  onClick={() =>
1623
  setMode((m) => (m === "episode" ? "dataset" : "episode"))
1624
  }
1625
- className={`relative inline-flex items-center w-9 h-5 rounded-full transition-colors shrink-0 ${mode === "dataset" ? "bg-orange-500" : "bg-slate-600"}`}
1626
  aria-label="Toggle episode/dataset scope"
1627
  >
1628
  <span
 
70
  <div className="relative">
71
  <button
72
  onClick={() => setFs((v) => !v)}
73
+ className="absolute top-3 right-3 z-10 p-1.5 rounded bg-white/5/60 hover:bg-white/5 text-slate-400 hover:text-slate-200 transition-colors backdrop-blur-sm"
74
  title={fs ? "Exit fullscreen" : "Fullscreen"}
75
  >
76
  <svg
 
102
  </svg>
103
  </button>
104
  {fs ? (
105
+ <div className="fixed inset-0 z-50 bg-[var(--bg)]/95 overflow-auto p-6">
106
  <button
107
  onClick={() => setFs(false)}
108
+ className="fixed top-4 right-4 z-50 p-2 rounded bg-white/5/80 hover:bg-white/5 text-slate-300 hover:text-white transition-colors"
109
  title="Exit fullscreen (Esc)"
110
  >
111
  <svg
 
145
  <button
146
  onClick={() => toggle(id)}
147
  title={flagged ? "Unflag episode" : "Flag for review"}
148
+ className={`p-0.5 rounded transition-colors ${flagged ? "text-cyan-300" : "text-slate-600 hover:text-slate-400"}`}
149
  >
150
  <svg
151
  xmlns="http://www.w3.org/2000/svg"
 
170
  return (
171
  <button
172
  onClick={() => addMany(ids)}
173
+ className="text-xs text-slate-500 hover:text-cyan-300 transition-colors flex items-center gap-1"
174
  >
175
  <svg
176
  xmlns="http://www.w3.org/2000/svg"
 
329
  return <p className="text-slate-500 italic">No action columns found.</p>;
330
 
331
  return (
332
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-5 border border-white/10 space-y-4">
333
  <div>
334
  <div className="flex items-center gap-2">
335
  <h3 className="text-sm font-semibold text-slate-200">
 
343
  Shows how correlated each action dimension is with itself over
344
  increasing time lags. Where autocorrelation drops below 0.5
345
  suggests a{" "}
346
+ <span className="text-cyan-300 font-medium">
347
  natural action chunk boundary
348
  </span>{" "}
349
  — actions beyond this lag are essentially independent, so
 
368
  </div>
369
 
370
  {suggestedChunk && (
371
+ <div className="flex items-center gap-3 bg-cyan-400/10 border border-cyan-400/30 rounded-md px-4 py-2.5">
372
+ <span className="text-cyan-300 font-bold text-lg tabular-nums">
373
  {suggestedChunk}
374
  </span>
375
  <div>
376
+ <p className="text-sm text-cyan-200 font-medium">
377
  Suggested chunk length: {suggestedChunk} steps (
378
  {(suggestedChunk / fps).toFixed(2)}s)
379
  </p>
 
639
  );
640
 
641
  return (
642
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-5 border border-white/10 space-y-4">
643
  <div>
644
  <div className="flex items-center gap-2">
645
  <h3 className="text-sm font-semibold text-slate-200">
 
700
  return (
701
  <div
702
  key={s.name}
703
+ className={`rounded-md px-2.5 py-2 space-y-1 ${dimmed ? "bg-[var(--surface-0)]/30 opacity-50" : "bg-[var(--surface-0)]/50"}`}
704
  >
705
  <p
706
  className={`text-xs font-medium truncate ${dimmed ? "text-slate-500" : "text-slate-200"}`}
 
743
  );
744
  })}
745
  </svg>
746
+ <div className="h-1 w-full bg-white/5 rounded-full overflow-hidden">
747
  <div
748
  className="h-full rounded-full"
749
  style={{
 
764
  </div>
765
 
766
  {insight && (
767
+ <div className="bg-[var(--surface-0)]/60 rounded-md px-4 py-3 border border-white/10/60 space-y-1.5">
768
  <p className="text-sm font-medium text-slate-200">
769
  Overall:{" "}
770
  <span className={insight.verdict.color}>
 
792
  const display = showAll ? episodes : episodes.slice(0, 15);
793
 
794
  return (
795
+ <div className="bg-[var(--surface-0)]/60 rounded-md px-4 py-3 border border-white/10/60 space-y-2">
796
  <div className="flex items-center justify-between">
797
  <p className="text-sm font-medium text-slate-200">
798
  Most Jerky Episodes{" "}
 
815
  <div className="max-h-48 overflow-y-auto">
816
  <table className="w-full text-xs">
817
  <thead>
818
+ <tr className="text-slate-500 border-b border-white/10">
819
  <th className="w-5 py-1" />
820
  <th className="text-left py-1 pr-3">Episode</th>
821
  <th className="text-right py-1">Mean |Δa|</th>
 
825
  {display.map((e) => (
826
  <tr
827
  key={e.episodeIndex}
828
+ className="border-b border-white/5/40 text-slate-300"
829
  >
830
  <td className="py-1">
831
  <FlagBtn id={e.episodeIndex} />
 
856
 
857
  if (loading) {
858
  return (
859
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-5 border border-white/10">
860
  <h3 className="text-sm font-semibold text-slate-200 mb-2">
861
  Cross-Episode Action Variance
862
  </h3>
 
884
 
885
  if (!data) {
886
  return (
887
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-5 border border-white/10">
888
  <h3 className="text-sm font-semibold text-slate-200 mb-2">
889
  Cross-Episode Action Variance
890
  </h3>
 
924
  }
925
 
926
  return (
927
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-5 border border-white/10 space-y-4">
928
  <div>
929
  <div className="flex items-center gap-2">
930
  <h3 className="text-sm font-semibold text-slate-200">
 
937
  <p className="text-xs text-slate-400">
938
  Shows how much each action dimension varies across episodes at
939
  each point in time (normalized 0–100%).
940
+ <span className="text-cyan-300"> High-variance regions</span>{" "}
 
 
 
941
  indicate multi-modal or inconsistent demonstrations — generative
942
  policies (diffusion, flow-matching) and action chunking help here
943
  by modeling multiple modes.
 
1132
  const barW = Math.max(8, Math.floor((isFs ? 900 : 500) / bins.length));
1133
 
1134
  return (
1135
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-5 border border-white/10 space-y-4">
1136
  <div>
1137
  <div className="flex items-center gap-2">
1138
  <h3 className="text-sm font-semibold text-slate-200">
 
1145
  <p className="text-xs text-slate-400">
1146
  Distribution of average execution speed (mean ‖Δa<sub>t</sub>‖ per
1147
  frame) across all episodes. Different human demonstrators often
1148
+ execute at <span className="text-cyan-300">different speeds</span>
1149
+ , creating artificial multimodality in the action distribution
1150
+ that confuses the policy. A coefficient of variation (CV) above
1151
+ 0.3 strongly suggests normalizing trajectory speed before
1152
+ training.
1153
  <br />
1154
  <span className="text-slate-500">
1155
  Based on &quot;Is Diversity All You Need&quot; (AGI-Bot, 2025)
 
1231
  </div>
1232
  </div>
1233
 
1234
+ <div className="bg-[var(--surface-0)]/60 rounded-md px-4 py-3 border border-white/10/60 space-y-1.5">
1235
  <p className="text-sm font-medium text-slate-200">
1236
  Verdict: <span className={verdict.color}>{verdict.label}</span>
1237
  </p>
 
1399
  : "current episode";
1400
 
1401
  return (
1402
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-5 border border-white/10 space-y-4">
1403
  <div>
1404
  <div className="flex items-center gap-2">
1405
  <h3 className="text-sm font-semibold text-slate-200">
 
1412
  <p className="text-xs text-slate-400">
1413
  Per-dimension cross-correlation between Δaction<sub>d</sub>(t) and
1414
  Δstate<sub>d</sub>(t+lag), aggregated as
1415
+ <span className="text-cyan-300"> max</span>,{" "}
1416
  <span className="text-slate-200">mean</span>, and
1417
  <span className="text-blue-400"> min</span> across all matched
1418
  action–state pairs. The{" "}
1419
+ <span className="text-cyan-300">peak lag</span> reveals the
1420
  effective control delay — the time between when an action is
1421
  commanded and when the corresponding state changes.
1422
  <br />
 
1458
  </div>
1459
 
1460
  {meanPeakLag !== 0 && (
1461
+ <div className="flex items-center gap-3 bg-cyan-400/10 border border-cyan-400/30 rounded-md px-4 py-2.5">
1462
+ <span className="text-cyan-300 font-bold text-lg tabular-nums">
1463
  {meanPeakLag}
1464
  </span>
1465
  <div>
1466
+ <p className="text-sm text-cyan-200 font-medium">
1467
  Mean control delay: {meanPeakLag} step
1468
  {Math.abs(meanPeakLag) !== 1 ? "s" : ""} (
1469
  {(meanPeakLag / fps).toFixed(3)}s)
 
1552
 
1553
  <div className="flex flex-wrap gap-x-4 gap-y-1 px-1">
1554
  <div className="flex items-center gap-1.5">
1555
+ <span className="w-3 h-[3px] rounded-full shrink-0 bg-cyan-500" />
1556
  <span className="text-xs text-slate-400">
1557
  max (peak: lag {maxPeakLag}, r={maxPeakCorr.toFixed(3)})
1558
  </span>
 
1619
  onClick={() =>
1620
  setMode((m) => (m === "episode" ? "dataset" : "episode"))
1621
  }
1622
+ className={`relative inline-flex items-center w-9 h-5 rounded-full transition-colors shrink-0 ${mode === "dataset" ? "bg-cyan-500" : "bg-white/10"}`}
1623
  aria-label="Toggle episode/dataset scope"
1624
  >
1625
  <span
src/components/data-recharts.tsx CHANGED
@@ -83,8 +83,8 @@ export const DataRecharts = React.memo(
83
  onClick={() => setExpanded((v) => !v)}
84
  className={`text-xs px-2.5 py-1 rounded transition-colors flex items-center gap-1.5 ${
85
  expanded
86
- ? "bg-orange-500/20 text-orange-400 border border-orange-500/40"
87
- : "bg-slate-800/60 text-slate-400 hover:text-slate-200 border border-slate-700/50"
88
  }`}
89
  >
90
  <svg
@@ -325,7 +325,7 @@ const SingleDataGraph = React.memo(
325
  {label}
326
  </span>
327
  <span
328
- className={`text-xs font-mono tabular-nums ml-1 ${visibleKeys.includes(key) ? "text-orange-300/80" : "text-slate-600"}`}
329
  >
330
  {typeof currentData[key] === "number"
331
  ? currentData[key].toFixed(2)
@@ -358,7 +358,7 @@ const SingleDataGraph = React.memo(
358
  {key}
359
  </span>
360
  <span
361
- className={`text-xs font-mono tabular-nums ml-1 ${visibleKeys.includes(key) ? "text-orange-300/80" : "text-slate-600"}`}
362
  >
363
  {typeof currentData[key] === "number"
364
  ? currentData[key].toFixed(2)
@@ -385,7 +385,7 @@ const SingleDataGraph = React.memo(
385
  }, [groups, singles]);
386
 
387
  return (
388
- <div className="w-full bg-slate-800/40 rounded-lg border border-slate-700/50 p-3">
389
  {chartTitle && (
390
  <p
391
  className="text-xs font-medium text-slate-300 mb-1 px-1 truncate"
 
83
  onClick={() => setExpanded((v) => !v)}
84
  className={`text-xs px-2.5 py-1 rounded transition-colors flex items-center gap-1.5 ${
85
  expanded
86
+ ? "bg-cyan-400/15 text-cyan-300 border border-cyan-400/40"
87
+ : "bg-[var(--surface-1)]/60 text-slate-400 hover:text-slate-200 border border-white/10/50"
88
  }`}
89
  >
90
  <svg
 
325
  {label}
326
  </span>
327
  <span
328
+ className={`text-xs font-mono tabular-nums ml-1 ${visibleKeys.includes(key) ? "text-cyan-200/80" : "text-slate-600"}`}
329
  >
330
  {typeof currentData[key] === "number"
331
  ? currentData[key].toFixed(2)
 
358
  {key}
359
  </span>
360
  <span
361
+ className={`text-xs font-mono tabular-nums ml-1 ${visibleKeys.includes(key) ? "text-cyan-200/80" : "text-slate-600"}`}
362
  >
363
  {typeof currentData[key] === "number"
364
  ? currentData[key].toFixed(2)
 
385
  }, [groups, singles]);
386
 
387
  return (
388
+ <div className="w-full bg-[var(--surface-1)]/40 rounded-lg border border-white/10/50 p-3">
389
  {chartTitle && (
390
  <p
391
  className="text-xs font-medium text-slate-300 mb-1 px-1 truncate"
src/components/filtering-panel.tsx CHANGED
@@ -22,7 +22,7 @@ function FlagBtn({ id }: { id: number }) {
22
  <button
23
  onClick={() => toggle(id)}
24
  title={flagged ? "Unflag episode" : "Flag for review"}
25
- className={`p-0.5 rounded transition-colors ${flagged ? "text-orange-400" : "text-slate-600 hover:text-slate-400"}`}
26
  >
27
  <svg
28
  xmlns="http://www.w3.org/2000/svg"
@@ -47,7 +47,7 @@ function FlagAllBtn({ ids, label }: { ids: number[]; label?: string }) {
47
  return (
48
  <button
49
  onClick={() => addMany(ids)}
50
- className="text-xs text-slate-500 hover:text-orange-400 transition-colors flex items-center gap-1"
51
  >
52
  <svg
53
  xmlns="http://www.w3.org/2000/svg"
@@ -75,7 +75,7 @@ function LowMovementSection({ episodes }: { episodes: LowMovementEpisode[] }) {
75
  const maxMovement = Math.max(...episodes.map((e) => e.totalMovement), 1e-10);
76
 
77
  return (
78
- <div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700 space-y-3">
79
  <div className="flex items-center justify-between">
80
  <h3 className="text-sm font-semibold text-slate-200">
81
  Lowest-Movement Episodes
@@ -94,14 +94,14 @@ function LowMovementSection({ episodes }: { episodes: LowMovementEpisode[] }) {
94
  {episodes.map((ep) => (
95
  <div
96
  key={ep.episodeIndex}
97
- className="bg-slate-900/50 rounded-md px-3 py-2 flex items-center gap-3"
98
  >
99
  <FlagBtn id={ep.episodeIndex} />
100
  <span className="text-xs text-slate-300 font-medium shrink-0">
101
  ep {ep.episodeIndex}
102
  </span>
103
  <div className="flex-1 min-w-0">
104
- <div className="h-1.5 bg-slate-700 rounded-full overflow-hidden">
105
  <div
106
  className="h-full rounded-full"
107
  style={{
@@ -157,7 +157,7 @@ function EpisodeLengthFilter({ episodes }: { episodes: EpisodeLengthInfo[] }) {
157
  0.01;
158
 
159
  return (
160
- <div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700 space-y-4">
161
  <h3 className="text-sm font-semibold text-slate-200">
162
  Episode Length Filter
163
  </h3>
@@ -168,9 +168,9 @@ function EpisodeLengthFilter({ episodes }: { episodes: EpisodeLengthInfo[] }) {
168
  <span className="tabular-nums">{rangeMax.toFixed(1)}s</span>
169
  </div>
170
  <div className="relative h-5">
171
- <div className="absolute top-1/2 -translate-y-1/2 left-0 right-0 h-1 rounded bg-slate-700" />
172
  <div
173
- className="absolute top-1/2 -translate-y-1/2 h-1 rounded bg-orange-500"
174
  style={{
175
  left: `${((rangeMin - globalMin) / (globalMax - globalMin || 1)) * 100}%`,
176
  right: `${100 - ((rangeMax - globalMin) / (globalMax - globalMin || 1)) * 100}%`,
@@ -185,7 +185,7 @@ function EpisodeLengthFilter({ episodes }: { episodes: EpisodeLengthInfo[] }) {
185
  onChange={(e) =>
186
  setRangeMin(Math.min(Number(e.target.value), rangeMax))
187
  }
188
- className="absolute inset-0 w-full appearance-none bg-transparent pointer-events-none [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-orange-500 [&::-webkit-slider-thumb]:cursor-pointer [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:w-3.5 [&::-moz-range-thumb]:h-3.5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-orange-500 [&::-moz-range-thumb]:cursor-pointer"
189
  />
190
  <input
191
  type="range"
@@ -196,7 +196,7 @@ function EpisodeLengthFilter({ episodes }: { episodes: EpisodeLengthInfo[] }) {
196
  onChange={(e) =>
197
  setRangeMax(Math.max(Number(e.target.value), rangeMin))
198
  }
199
- className="absolute inset-0 w-full appearance-none bg-transparent pointer-events-none [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-orange-500 [&::-webkit-slider-thumb]:cursor-pointer [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:w-3.5 [&::-moz-range-thumb]:h-3.5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-orange-500 [&::-moz-range-thumb]:cursor-pointer"
200
  />
201
  </div>
202
  </div>
@@ -210,7 +210,7 @@ function EpisodeLengthFilter({ episodes }: { episodes: EpisodeLengthInfo[] }) {
210
  {outsideIds.length > 0 && (
211
  <button
212
  onClick={() => addMany(outsideIds)}
213
- className="text-xs bg-orange-500/20 text-orange-400 border border-orange-500/40 rounded px-2 py-1 hover:bg-orange-500/30 transition-colors"
214
  >
215
  Flag {outsideIds.length} outside range
216
  </button>
@@ -254,9 +254,9 @@ function FlaggedIdsCopyBar({
254
  if (count === 0) return null;
255
 
256
  return (
257
- <div className="bg-slate-800/60 rounded-lg p-4 border border-orange-500/30 space-y-3">
258
  <div className="flex items-center justify-between">
259
- <h3 className="text-sm font-semibold text-orange-400">
260
  Flagged Episodes
261
  <span className="text-xs text-slate-500 ml-2 font-normal">
262
  ({count})
@@ -311,7 +311,7 @@ function FlaggedIdsCopyBar({
311
  {onViewEpisodes && (
312
  <button
313
  onClick={onViewEpisodes}
314
- className="w-full text-xs py-1.5 rounded bg-slate-700/80 hover:bg-slate-600 text-slate-300 hover:text-white transition-colors flex items-center justify-center gap-1.5"
315
  >
316
  <svg
317
  xmlns="http://www.w3.org/2000/svg"
@@ -330,20 +330,20 @@ function FlaggedIdsCopyBar({
330
  View flagged episodes
331
  </button>
332
  )}
333
- <div className="bg-slate-900/60 rounded-md px-3 py-2 border border-slate-700/60 space-y-2.5">
334
  <p className="text-xs text-slate-400">
335
  <a
336
  href="https://github.com/huggingface/lerobot"
337
  target="_blank"
338
  rel="noopener noreferrer"
339
- className="text-orange-400 underline"
340
  >
341
  LeRobot CLI
342
  </a>{" "}
343
  — delete flagged episodes:
344
  </p>
345
- <pre className="text-xs text-slate-300 bg-slate-950/50 rounded px-2 py-1.5 overflow-x-auto select-all">{`# Delete episodes (modifies original dataset)\nlerobot-edit-dataset \\\n --repo_id ${repoId} \\\n --operation.type delete_episodes \\\n --operation.episode_indices "[${ids.join(", ")}]"`}</pre>
346
- <pre className="text-xs text-slate-300 bg-slate-950/50 rounded px-2 py-1.5 overflow-x-auto select-all">{`# Delete episodes and save to a new dataset (preserves original)\nlerobot-edit-dataset \\\n --repo_id ${repoId} \\\n --new_repo_id ${repoId}_filtered \\\n --operation.type delete_episodes \\\n --operation.episode_indices "[${ids.join(", ")}]"`}</pre>
347
  </div>
348
  </div>
349
  );
@@ -377,7 +377,7 @@ function FilteringPanel({
377
  )}
378
 
379
  {crossEpisodeLoading && (
380
- <div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700">
381
  <div className="flex items-center gap-2 text-slate-400 text-sm py-4 justify-center">
382
  <svg
383
  className="animate-spin h-4 w-4"
 
22
  <button
23
  onClick={() => toggle(id)}
24
  title={flagged ? "Unflag episode" : "Flag for review"}
25
+ className={`p-0.5 rounded transition-colors ${flagged ? "text-cyan-300" : "text-slate-600 hover:text-slate-400"}`}
26
  >
27
  <svg
28
  xmlns="http://www.w3.org/2000/svg"
 
47
  return (
48
  <button
49
  onClick={() => addMany(ids)}
50
+ className="text-xs text-slate-500 hover:text-cyan-300 transition-colors flex items-center gap-1"
51
  >
52
  <svg
53
  xmlns="http://www.w3.org/2000/svg"
 
75
  const maxMovement = Math.max(...episodes.map((e) => e.totalMovement), 1e-10);
76
 
77
  return (
78
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-5 border border-white/10 space-y-3">
79
  <div className="flex items-center justify-between">
80
  <h3 className="text-sm font-semibold text-slate-200">
81
  Lowest-Movement Episodes
 
94
  {episodes.map((ep) => (
95
  <div
96
  key={ep.episodeIndex}
97
+ className="bg-[var(--surface-0)]/50 rounded-md px-3 py-2 flex items-center gap-3"
98
  >
99
  <FlagBtn id={ep.episodeIndex} />
100
  <span className="text-xs text-slate-300 font-medium shrink-0">
101
  ep {ep.episodeIndex}
102
  </span>
103
  <div className="flex-1 min-w-0">
104
+ <div className="h-1.5 bg-white/5 rounded-full overflow-hidden">
105
  <div
106
  className="h-full rounded-full"
107
  style={{
 
157
  0.01;
158
 
159
  return (
160
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-5 border border-white/10 space-y-4">
161
  <h3 className="text-sm font-semibold text-slate-200">
162
  Episode Length Filter
163
  </h3>
 
168
  <span className="tabular-nums">{rangeMax.toFixed(1)}s</span>
169
  </div>
170
  <div className="relative h-5">
171
+ <div className="absolute top-1/2 -translate-y-1/2 left-0 right-0 h-1 rounded bg-white/5" />
172
  <div
173
+ className="absolute top-1/2 -translate-y-1/2 h-1 rounded bg-cyan-500"
174
  style={{
175
  left: `${((rangeMin - globalMin) / (globalMax - globalMin || 1)) * 100}%`,
176
  right: `${100 - ((rangeMax - globalMin) / (globalMax - globalMin || 1)) * 100}%`,
 
185
  onChange={(e) =>
186
  setRangeMin(Math.min(Number(e.target.value), rangeMax))
187
  }
188
+ className="absolute inset-0 w-full appearance-none bg-transparent pointer-events-none [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-cyan-400 [&::-webkit-slider-thumb]:cursor-pointer [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:w-3.5 [&::-moz-range-thumb]:h-3.5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-cyan-400 [&::-moz-range-thumb]:cursor-pointer"
189
  />
190
  <input
191
  type="range"
 
196
  onChange={(e) =>
197
  setRangeMax(Math.max(Number(e.target.value), rangeMin))
198
  }
199
+ className="absolute inset-0 w-full appearance-none bg-transparent pointer-events-none [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-cyan-400 [&::-webkit-slider-thumb]:cursor-pointer [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:appearance-none [&::-moz-range-thumb]:w-3.5 [&::-moz-range-thumb]:h-3.5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-cyan-400 [&::-moz-range-thumb]:cursor-pointer"
200
  />
201
  </div>
202
  </div>
 
210
  {outsideIds.length > 0 && (
211
  <button
212
  onClick={() => addMany(outsideIds)}
213
+ className="text-xs bg-cyan-400/15 text-cyan-300 border border-cyan-400/40 rounded px-2 py-1 hover:bg-cyan-400/20 transition-colors"
214
  >
215
  Flag {outsideIds.length} outside range
216
  </button>
 
254
  if (count === 0) return null;
255
 
256
  return (
257
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-4 border border-cyan-400/30 space-y-3">
258
  <div className="flex items-center justify-between">
259
+ <h3 className="text-sm font-semibold text-cyan-300">
260
  Flagged Episodes
261
  <span className="text-xs text-slate-500 ml-2 font-normal">
262
  ({count})
 
311
  {onViewEpisodes && (
312
  <button
313
  onClick={onViewEpisodes}
314
+ className="w-full text-xs py-1.5 rounded bg-white/5/80 hover:bg-white/5 text-slate-300 hover:text-white transition-colors flex items-center justify-center gap-1.5"
315
  >
316
  <svg
317
  xmlns="http://www.w3.org/2000/svg"
 
330
  View flagged episodes
331
  </button>
332
  )}
333
+ <div className="bg-[var(--surface-0)]/60 rounded-md px-3 py-2 border border-white/10/60 space-y-2.5">
334
  <p className="text-xs text-slate-400">
335
  <a
336
  href="https://github.com/huggingface/lerobot"
337
  target="_blank"
338
  rel="noopener noreferrer"
339
+ className="text-cyan-300 underline"
340
  >
341
  LeRobot CLI
342
  </a>{" "}
343
  — delete flagged episodes:
344
  </p>
345
+ <pre className="text-xs text-slate-300 bg-[var(--bg)]/50 rounded px-2 py-1.5 overflow-x-auto select-all">{`# Delete episodes (modifies original dataset)\nlerobot-edit-dataset \\\n --repo_id ${repoId} \\\n --operation.type delete_episodes \\\n --operation.episode_indices "[${ids.join(", ")}]"`}</pre>
346
+ <pre className="text-xs text-slate-300 bg-[var(--bg)]/50 rounded px-2 py-1.5 overflow-x-auto select-all">{`# Delete episodes and save to a new dataset (preserves original)\nlerobot-edit-dataset \\\n --repo_id ${repoId} \\\n --new_repo_id ${repoId}_filtered \\\n --operation.type delete_episodes \\\n --operation.episode_indices "[${ids.join(", ")}]"`}</pre>
347
  </div>
348
  </div>
349
  );
 
377
  )}
378
 
379
  {crossEpisodeLoading && (
380
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-5 border border-white/10">
381
  <div className="flex items-center gap-2 text-slate-400 text-sm py-4 justify-center">
382
  <svg
383
  className="animate-spin h-4 w-4"
src/components/loading-component.tsx CHANGED
@@ -3,35 +3,37 @@
3
  export default function Loading() {
4
  return (
5
  <div
6
- className="absolute inset-0 flex flex-col items-center justify-center bg-slate-950/70 z-10 text-slate-100 animate-fade-in"
7
  tabIndex={-1}
8
  aria-modal="true"
9
  role="dialog"
10
  >
11
  <svg
12
- className="animate-spin mb-8"
13
- width="64"
14
- height="64"
15
  viewBox="0 0 24 24"
16
  fill="none"
17
  xmlns="http://www.w3.org/2000/svg"
18
  >
19
  <circle
20
- className="opacity-25"
21
  cx="12"
22
  cy="12"
23
  r="10"
24
  stroke="currentColor"
25
- strokeWidth="4"
26
  />
27
  <path
28
- className="opacity-75"
29
  fill="currentColor"
30
  d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
31
  />
32
  </svg>
33
- <h1 className="text-2xl font-bold mb-2">Loading...</h1>
34
- <p className="text-slate-400">preparing data & videos</p>
 
 
35
  </div>
36
  );
37
  }
 
3
  export default function Loading() {
4
  return (
5
  <div
6
+ className="absolute inset-0 flex flex-col items-center justify-center bg-[var(--bg)]/80 backdrop-blur-sm z-10 text-slate-200"
7
  tabIndex={-1}
8
  aria-modal="true"
9
  role="dialog"
10
  >
11
  <svg
12
+ className="animate-spin mb-5 text-cyan-300"
13
+ width="42"
14
+ height="42"
15
  viewBox="0 0 24 24"
16
  fill="none"
17
  xmlns="http://www.w3.org/2000/svg"
18
  >
19
  <circle
20
+ className="opacity-15"
21
  cx="12"
22
  cy="12"
23
  r="10"
24
  stroke="currentColor"
25
+ strokeWidth="3"
26
  />
27
  <path
28
+ className="opacity-80"
29
  fill="currentColor"
30
  d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
31
  />
32
  </svg>
33
+ <h1 className="text-sm font-medium tracking-wide uppercase text-slate-300">
34
+ Loading
35
+ </h1>
36
+ <p className="text-xs text-slate-500 mt-1">preparing data &amp; videos</p>
37
  </div>
38
  );
39
  }
src/components/overview-panel.tsx CHANGED
@@ -62,7 +62,7 @@ function FrameThumbnail({
62
 
63
  return (
64
  <div ref={containerRef} className="flex flex-col items-center">
65
- <div className="w-full aspect-video bg-slate-800 rounded overflow-hidden relative group">
66
  {inView ? (
67
  <video
68
  ref={videoRef}
@@ -72,14 +72,14 @@ function FrameThumbnail({
72
  className="w-full h-full object-cover"
73
  />
74
  ) : (
75
- <div className="w-full h-full animate-pulse bg-slate-700" />
76
  )}
77
  <button
78
  onClick={() => toggle(info.episodeIndex)}
79
  className={`absolute top-1 right-1 p-1 rounded transition-opacity ${
80
  isFlagged
81
- ? "opacity-100 text-orange-400"
82
- : "opacity-0 group-hover:opacity-100 text-slate-400 hover:text-orange-400"
83
  }`}
84
  title={isFlagged ? "Unflag episode" : "Flag episode"}
85
  >
@@ -100,7 +100,7 @@ function FrameThumbnail({
100
  </button>
101
  </div>
102
  <p
103
- className={`text-xs mt-1 tabular-nums ${isFlagged ? "text-orange-400" : "text-slate-400"}`}
104
  >
105
  ep {info.episodeIndex}
106
  {isFlagged ? " ⚑" : ""}
@@ -181,7 +181,7 @@ export default function OverviewPanel({
181
  {flaggedOnly && onFlaggedOnlyChange && (
182
  <button
183
  onClick={() => onFlaggedOnlyChange(false)}
184
- className="text-xs text-orange-400 hover:text-orange-300 underline"
185
  >
186
  Show all episodes
187
  </button>
@@ -209,7 +209,7 @@ export default function OverviewPanel({
209
  <select
210
  value={selectedCamera}
211
  onChange={handleCameraChange}
212
- className="bg-slate-800 text-slate-200 text-sm rounded px-3 py-1.5 border border-slate-600 focus:outline-none focus:border-orange-500"
213
  >
214
  {data.cameras.map((cam) => (
215
  <option key={cam} value={cam}>
@@ -228,8 +228,8 @@ export default function OverviewPanel({
228
  }}
229
  className={`text-xs px-2.5 py-1 rounded transition-colors flex items-center gap-1.5 ${
230
  flaggedOnly
231
- ? "bg-orange-500/20 text-orange-400 border border-orange-500/40"
232
- : "text-slate-400 hover:text-slate-200 border border-slate-700"
233
  }`}
234
  >
235
  <svg
@@ -259,7 +259,7 @@ export default function OverviewPanel({
259
  </span>
260
  <button
261
  onClick={() => setShowLast((v) => !v)}
262
- className={`relative inline-flex items-center w-9 h-5 rounded-full transition-colors shrink-0 ${showLast ? "bg-orange-500" : "bg-slate-600"}`}
263
  aria-label="Toggle first/last frame"
264
  >
265
  <span
@@ -280,7 +280,7 @@ export default function OverviewPanel({
280
  <button
281
  disabled={page === 0}
282
  onClick={() => setPage((p) => p - 1)}
283
- className="px-2 py-1 rounded bg-slate-800 hover:bg-slate-700 disabled:opacity-30 disabled:cursor-not-allowed"
284
  >
285
  ← Prev
286
  </button>
@@ -290,7 +290,7 @@ export default function OverviewPanel({
290
  <button
291
  disabled={page === totalPages - 1}
292
  onClick={() => setPage((p) => p + 1)}
293
- className="px-2 py-1 rounded bg-slate-800 hover:bg-slate-700 disabled:opacity-30 disabled:cursor-not-allowed"
294
  >
295
  Next →
296
  </button>
 
62
 
63
  return (
64
  <div ref={containerRef} className="flex flex-col items-center">
65
+ <div className="w-full aspect-video bg-[var(--surface-1)] rounded overflow-hidden relative group">
66
  {inView ? (
67
  <video
68
  ref={videoRef}
 
72
  className="w-full h-full object-cover"
73
  />
74
  ) : (
75
+ <div className="w-full h-full animate-pulse bg-white/5" />
76
  )}
77
  <button
78
  onClick={() => toggle(info.episodeIndex)}
79
  className={`absolute top-1 right-1 p-1 rounded transition-opacity ${
80
  isFlagged
81
+ ? "opacity-100 text-cyan-300"
82
+ : "opacity-0 group-hover:opacity-100 text-slate-400 hover:text-cyan-300"
83
  }`}
84
  title={isFlagged ? "Unflag episode" : "Flag episode"}
85
  >
 
100
  </button>
101
  </div>
102
  <p
103
+ className={`text-xs mt-1 tabular-nums ${isFlagged ? "text-cyan-300" : "text-slate-400"}`}
104
  >
105
  ep {info.episodeIndex}
106
  {isFlagged ? " ⚑" : ""}
 
181
  {flaggedOnly && onFlaggedOnlyChange && (
182
  <button
183
  onClick={() => onFlaggedOnlyChange(false)}
184
+ className="text-xs text-cyan-300 hover:text-cyan-200 underline"
185
  >
186
  Show all episodes
187
  </button>
 
209
  <select
210
  value={selectedCamera}
211
  onChange={handleCameraChange}
212
+ className="bg-[var(--surface-1)] text-slate-200 text-sm rounded px-3 py-1.5 border border-white/10 focus:outline-none focus:border-cyan-400"
213
  >
214
  {data.cameras.map((cam) => (
215
  <option key={cam} value={cam}>
 
228
  }}
229
  className={`text-xs px-2.5 py-1 rounded transition-colors flex items-center gap-1.5 ${
230
  flaggedOnly
231
+ ? "bg-cyan-400/15 text-cyan-300 border border-cyan-400/40"
232
+ : "text-slate-400 hover:text-slate-200 border border-white/10"
233
  }`}
234
  >
235
  <svg
 
259
  </span>
260
  <button
261
  onClick={() => setShowLast((v) => !v)}
262
+ className={`relative inline-flex items-center w-9 h-5 rounded-full transition-colors shrink-0 ${showLast ? "bg-cyan-500" : "bg-white/10"}`}
263
  aria-label="Toggle first/last frame"
264
  >
265
  <span
 
280
  <button
281
  disabled={page === 0}
282
  onClick={() => setPage((p) => p - 1)}
283
+ className="px-2 py-1 rounded bg-[var(--surface-1)] hover:bg-white/5 disabled:opacity-30 disabled:cursor-not-allowed"
284
  >
285
  ← Prev
286
  </button>
 
290
  <button
291
  disabled={page === totalPages - 1}
292
  onClick={() => setPage((p) => p + 1)}
293
+ className="px-2 py-1 rounded bg-[var(--surface-1)] hover:bg-white/5 disabled:opacity-30 disabled:cursor-not-allowed"
294
  >
295
  Next →
296
  </button>
src/components/playback-bar.tsx CHANGED
@@ -48,43 +48,36 @@ const PlaybackBar: React.FC = () => {
48
  };
49
 
50
  return (
51
- <div className="flex items-center gap-4 w-full max-w-4xl mx-auto sticky bottom-0 bg-slate-900/95 px-4 py-3 rounded-3xl mt-auto">
52
  <button
53
  title="Jump backward 5 seconds"
54
  onClick={() => setCurrentTime(Math.max(0, currentTime - 5))}
55
- className="text-2xl hidden md:block"
56
  >
57
- <FaBackward size={24} />
58
  </button>
59
  <button
60
- className={`text-3xl transition-transform ${isPlaying ? "scale-90 opacity-60" : "scale-110"}`}
61
- title="Play. Toggle with Space"
62
- onClick={() => setIsPlaying(true)}
63
- style={{ display: isPlaying ? "none" : "inline-block" }}
 
64
  >
65
- <FaPlay size={24} />
66
- </button>
67
- <button
68
- className={`text-3xl transition-transform ${!isPlaying ? "scale-90 opacity-60" : "scale-110"}`}
69
- title="Pause. Toggle with Space"
70
- onClick={() => setIsPlaying(false)}
71
- style={{ display: !isPlaying ? "none" : "inline-block" }}
72
- >
73
- <FaPause size={24} />
74
  </button>
75
  <button
76
  title="Jump forward 5 seconds"
77
  onClick={() => setCurrentTime(Math.min(duration, currentTime + 5))}
78
- className="text-2xl hidden md:block"
79
  >
80
- <FaForward size={24} />
81
  </button>
82
  <button
83
  title="Rewind from start"
84
  onClick={() => setCurrentTime(0)}
85
- className="text-2xl hidden md:block"
86
  >
87
- <FaUndoAlt size={24} />
88
  </button>
89
  <input
90
  type="range"
@@ -97,27 +90,26 @@ const PlaybackBar: React.FC = () => {
97
  onMouseUp={handleSliderMouseUp}
98
  onTouchStart={handleSliderMouseDown}
99
  onTouchEnd={handleSliderMouseUp}
100
- className="flex-1 mx-2 accent-orange-500 focus:outline-none focus:ring-0"
101
  aria-label="Seek video"
102
  />
103
- <span className="w-16 text-right tabular-nums text-xs text-slate-200 shrink-0">
104
  {Math.floor(sliderValue)} / {Math.floor(duration)}
105
  </span>
106
 
107
- <div className="text-xs text-slate-300 select-none ml-8 flex-col gap-y-0.5 hidden md:flex">
108
- <p>
109
- <span className="inline-flex items-center gap-1 font-mono align-middle">
110
- <span className="px-2 py-0.5 rounded border border-slate-400 bg-slate-800 text-slate-200 text-xs shadow-inner">
111
- Space
112
- </span>
113
- </span>{" "}
114
- to pause/unpause
115
  </p>
116
- <p>
117
- <span className="inline-flex items-center gap-1 font-mono align-middle">
118
- <FaArrowUp size={14} />/<FaArrowDown size={14} />
119
- </span>{" "}
120
- to previous/next episode
 
121
  </p>
122
  </div>
123
  </div>
 
48
  };
49
 
50
  return (
51
+ <div className="sticky bottom-0 mt-auto w-full max-w-4xl mx-auto flex items-center gap-3 panel-raised bg-[var(--surface-0)]/90 backdrop-blur px-3 py-2">
52
  <button
53
  title="Jump backward 5 seconds"
54
  onClick={() => setCurrentTime(Math.max(0, currentTime - 5))}
55
+ className="hidden md:flex h-8 w-8 items-center justify-center rounded-md text-slate-400 hover:text-slate-100 hover:bg-white/5 transition-colors"
56
  >
57
+ <FaBackward size={14} />
58
  </button>
59
  <button
60
+ className="flex h-9 w-9 items-center justify-center rounded-md bg-cyan-400/10 border border-cyan-400/30 text-cyan-300 hover:bg-cyan-400/15 transition-colors"
61
+ title={
62
+ isPlaying ? "Pause. Toggle with Space" : "Play. Toggle with Space"
63
+ }
64
+ onClick={() => setIsPlaying(!isPlaying)}
65
  >
66
+ {isPlaying ? <FaPause size={14} /> : <FaPlay size={14} />}
 
 
 
 
 
 
 
 
67
  </button>
68
  <button
69
  title="Jump forward 5 seconds"
70
  onClick={() => setCurrentTime(Math.min(duration, currentTime + 5))}
71
+ className="hidden md:flex h-8 w-8 items-center justify-center rounded-md text-slate-400 hover:text-slate-100 hover:bg-white/5 transition-colors"
72
  >
73
+ <FaForward size={14} />
74
  </button>
75
  <button
76
  title="Rewind from start"
77
  onClick={() => setCurrentTime(0)}
78
+ className="hidden md:flex h-8 w-8 items-center justify-center rounded-md text-slate-400 hover:text-slate-100 hover:bg-white/5 transition-colors"
79
  >
80
+ <FaUndoAlt size={14} />
81
  </button>
82
  <input
83
  type="range"
 
90
  onMouseUp={handleSliderMouseUp}
91
  onTouchStart={handleSliderMouseDown}
92
  onTouchEnd={handleSliderMouseUp}
93
+ className="flex-1 mx-1 h-1 accent-cyan-400 cursor-pointer focus:outline-none focus:ring-0"
94
  aria-label="Seek video"
95
  />
96
+ <span className="w-16 text-right tabular text-[11px] text-slate-400 shrink-0">
97
  {Math.floor(sliderValue)} / {Math.floor(duration)}
98
  </span>
99
 
100
+ <div className="hidden lg:flex flex-col gap-y-0.5 ml-4 text-[10px] text-slate-500 select-none">
101
+ <p className="inline-flex items-center gap-1.5">
102
+ <kbd className="px-1.5 py-0.5 rounded border border-white/10 bg-white/5 text-slate-300 text-[10px]">
103
+ Space
104
+ </kbd>
105
+ <span>pause/unpause</span>
 
 
106
  </p>
107
+ <p className="inline-flex items-center gap-1.5">
108
+ <span className="inline-flex items-center gap-0.5 text-slate-300">
109
+ <FaArrowUp size={10} />
110
+ <FaArrowDown size={10} />
111
+ </span>
112
+ <span>prev/next episode</span>
113
  </p>
114
  </div>
115
  </div>
src/components/side-nav.tsx CHANGED
@@ -42,98 +42,131 @@ const Sidebar: React.FC<SidebarProps> = ({
42
  return (
43
  <div className="flex z-10 shrink-0">
44
  <nav
45
- className={`shrink-0 overflow-y-auto bg-slate-900 p-5 break-words w-60 ${
46
  mobileVisible ? "block" : "hidden"
47
  } md:block`}
48
  aria-label="Sidebar navigation"
49
  >
50
- <ul className="text-sm text-slate-300 space-y-0.5">
51
- <li>Frames: {datasetInfo.total_frames.toLocaleString()}</li>
52
- <li>Episodes: {datasetInfo.total_episodes.toLocaleString()}</li>
53
- <li>FPS: {datasetInfo.fps}</li>
54
- </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- <div className="mt-4 flex items-center justify-between">
57
- <p className="text-sm font-semibold text-slate-200">Episodes:</p>
 
 
58
  {count > 0 && (
59
  <button
60
  onClick={() => onShowFlaggedOnlyChange(!showFlaggedOnly)}
61
- className={`text-xs px-1.5 py-0.5 rounded transition-colors ${
62
  showFlaggedOnly
63
- ? "bg-orange-500/20 text-orange-400 border border-orange-500/40"
64
- : "text-slate-500 hover:text-slate-300 border border-slate-700"
65
  }`}
66
  >
67
- Flagged ({count})
68
  </button>
69
  )}
70
  </div>
71
 
72
- <div className="ml-2 mt-1">
73
- <ul>
74
- {displayEpisodes.map((episode) => (
75
- <li
76
- key={episode}
77
- className="mt-0.5 font-mono text-sm flex items-center gap-1"
78
- >
 
 
 
79
  {onEpisodeSelect ? (
80
- <button
81
- onClick={() => onEpisodeSelect(episode)}
82
- className={`underline text-left cursor-pointer ${episode === episodeId ? "-ml-1 font-bold text-orange-400" : ""}`}
83
- >
84
- Episode {episode}
85
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  ) : (
87
- <Link
88
- href={`./episode_${episode}`}
89
- className={`underline ${episode === episodeId ? "-ml-1 font-bold" : ""}`}
90
- >
91
- Episode {episode}
92
- </Link>
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  )}
94
- <button
95
- onClick={() => toggle(episode)}
96
- className={`text-xs leading-none px-0.5 rounded transition-colors ${
97
- flagged.has(episode)
98
- ? "text-orange-400 hover:text-orange-300"
99
- : "text-slate-600 hover:text-slate-400"
100
- }`}
101
- title={flagged.has(episode) ? "Unflag" : "Flag"}
102
- >
103
-
104
- </button>
105
  </li>
106
- ))}
107
- </ul>
 
108
 
109
- {!showFlaggedOnly && totalPages > 1 && (
110
- <div className="mt-3 flex items-center text-xs">
111
- <button
112
- onClick={prevPage}
113
- className={`mr-2 rounded bg-slate-800 px-2 py-1 ${
114
- currentPage === 1 ? "cursor-not-allowed opacity-50" : ""
115
- }`}
116
- disabled={currentPage === 1}
117
- >
118
- « Prev
119
- </button>
120
- <span className="mr-2 font-mono">
121
- {currentPage} / {totalPages}
122
- </span>
123
- <button
124
- onClick={nextPage}
125
- className={`rounded bg-slate-800 px-2 py-1 ${
126
- currentPage === totalPages
127
- ? "cursor-not-allowed opacity-50"
128
- : ""
129
- }`}
130
- disabled={currentPage === totalPages}
131
- >
132
- Next »
133
- </button>
134
- </div>
135
- )}
136
- </div>
137
  </nav>
138
 
139
  <button
@@ -141,7 +174,7 @@ const Sidebar: React.FC<SidebarProps> = ({
141
  onClick={() => setMobileVisible((prev) => !prev)}
142
  title="Toggle sidebar"
143
  >
144
- <div className="h-10 w-2 rounded-full bg-slate-500" />
145
  </button>
146
  </div>
147
  );
 
42
  return (
43
  <div className="flex z-10 shrink-0">
44
  <nav
45
+ className={`shrink-0 overflow-y-auto bg-[var(--surface-0)] border-r border-white/5 p-4 break-words w-60 ${
46
  mobileVisible ? "block" : "hidden"
47
  } md:block`}
48
  aria-label="Sidebar navigation"
49
  >
50
+ <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs text-slate-400 tabular">
51
+ <dt className="uppercase tracking-wide text-[10px] text-slate-500">
52
+ Frames
53
+ </dt>
54
+ <dd className="text-slate-200">
55
+ {datasetInfo.total_frames.toLocaleString()}
56
+ </dd>
57
+ <dt className="uppercase tracking-wide text-[10px] text-slate-500">
58
+ Episodes
59
+ </dt>
60
+ <dd className="text-slate-200">
61
+ {datasetInfo.total_episodes.toLocaleString()}
62
+ </dd>
63
+ <dt className="uppercase tracking-wide text-[10px] text-slate-500">
64
+ FPS
65
+ </dt>
66
+ <dd className="text-slate-200">{datasetInfo.fps}</dd>
67
+ </dl>
68
 
69
+ <div className="mt-5 flex items-center justify-between">
70
+ <p className="text-[10px] uppercase tracking-wide text-slate-500">
71
+ Episodes
72
+ </p>
73
  {count > 0 && (
74
  <button
75
  onClick={() => onShowFlaggedOnlyChange(!showFlaggedOnly)}
76
+ className={`text-[10px] uppercase tracking-wide px-2 py-0.5 rounded-md transition-colors ${
77
  showFlaggedOnly
78
+ ? "bg-orange-500/15 text-orange-300 border border-orange-500/30"
79
+ : "text-slate-500 hover:text-slate-300 border border-white/10"
80
  }`}
81
  >
82
+ Flagged · {count}
83
  </button>
84
  )}
85
  </div>
86
 
87
+ <ul className="mt-2 space-y-px">
88
+ {displayEpisodes.map((episode) => {
89
+ const active = episode === episodeId;
90
+ const itemClass = `group flex items-center justify-between gap-2 px-2 py-1 rounded-md text-xs tabular transition-colors ${
91
+ active
92
+ ? "bg-cyan-400/10 text-cyan-300"
93
+ : "text-slate-300 hover:bg-white/5"
94
+ }`;
95
+ return (
96
+ <li key={episode}>
97
  {onEpisodeSelect ? (
98
+ <div className={itemClass}>
99
+ <button
100
+ onClick={() => onEpisodeSelect(episode)}
101
+ className="flex-1 text-left"
102
+ >
103
+ Episode {episode}
104
+ </button>
105
+ <button
106
+ onClick={() => toggle(episode)}
107
+ className={`text-xs leading-none transition-colors ${
108
+ flagged.has(episode)
109
+ ? "text-orange-400 hover:text-orange-300"
110
+ : "text-slate-600 hover:text-slate-400 opacity-0 group-hover:opacity-100"
111
+ }`}
112
+ title={flagged.has(episode) ? "Unflag" : "Flag"}
113
+ >
114
+
115
+ </button>
116
+ </div>
117
  ) : (
118
+ <div className={itemClass}>
119
+ <Link
120
+ href={`./episode_${episode}`}
121
+ className="flex-1 text-left"
122
+ >
123
+ Episode {episode}
124
+ </Link>
125
+ <button
126
+ onClick={() => toggle(episode)}
127
+ className={`text-xs leading-none transition-colors ${
128
+ flagged.has(episode)
129
+ ? "text-orange-400 hover:text-orange-300"
130
+ : "text-slate-600 hover:text-slate-400 opacity-0 group-hover:opacity-100"
131
+ }`}
132
+ title={flagged.has(episode) ? "Unflag" : "Flag"}
133
+ >
134
+
135
+ </button>
136
+ </div>
137
  )}
 
 
 
 
 
 
 
 
 
 
 
138
  </li>
139
+ );
140
+ })}
141
+ </ul>
142
 
143
+ {!showFlaggedOnly && totalPages > 1 && (
144
+ <div className="mt-3 flex items-center gap-2 text-[10px] uppercase tracking-wide text-slate-400">
145
+ <button
146
+ onClick={prevPage}
147
+ className={`px-2 py-1 rounded-md border border-white/10 transition-colors hover:bg-white/5 hover:text-slate-200 ${
148
+ currentPage === 1 ? "cursor-not-allowed opacity-40" : ""
149
+ }`}
150
+ disabled={currentPage === 1}
151
+ >
152
+ Prev
153
+ </button>
154
+ <span className="tabular text-slate-500">
155
+ {currentPage} / {totalPages}
156
+ </span>
157
+ <button
158
+ onClick={nextPage}
159
+ className={`ml-auto px-2 py-1 rounded-md border border-white/10 transition-colors hover:bg-white/5 hover:text-slate-200 ${
160
+ currentPage === totalPages
161
+ ? "cursor-not-allowed opacity-40"
162
+ : ""
163
+ }`}
164
+ disabled={currentPage === totalPages}
165
+ >
166
+ Next
167
+ </button>
168
+ </div>
169
+ )}
 
170
  </nav>
171
 
172
  <button
 
174
  onClick={() => setMobileVisible((prev) => !prev)}
175
  title="Toggle sidebar"
176
  >
177
+ <div className="h-10 w-1 rounded-full bg-white/20" />
178
  </button>
179
  </div>
180
  );
src/components/simple-videos-player.tsx CHANGED
@@ -224,20 +224,20 @@ export const SimpleVideosPlayer = ({
224
  {hiddenVideos.length > 0 && (
225
  <div className="relative mb-4">
226
  <button
227
- className="flex items-center gap-2 rounded bg-slate-800 px-3 py-2 text-sm text-slate-100 hover:bg-slate-700 border border-slate-500"
228
  onClick={() => setShowHiddenMenu(!showHiddenMenu)}
229
  >
230
- <FaEye /> Show Hidden Videos ({hiddenVideos.length})
231
  </button>
232
  {showHiddenMenu && (
233
- <div className="absolute left-0 mt-2 w-max rounded border border-slate-500 bg-slate-900 shadow-lg p-2 z-50">
234
- <div className="mb-2 text-xs text-slate-300">
235
- Restore hidden videos:
236
  </div>
237
  {hiddenVideos.map((filename) => (
238
  <button
239
  key={filename}
240
- className="block w-full text-left px-2 py-1 rounded hover:bg-slate-700 text-slate-100"
241
  onClick={() =>
242
  setHiddenVideos((prev) =>
243
  prev.filter((v) => v !== filename),
@@ -269,21 +269,25 @@ export const SimpleVideosPlayer = ({
269
  : "max-w-96"
270
  }`}
271
  >
272
- <p className="truncate w-full rounded-t-xl bg-gray-800 px-2 text-sm text-gray-300 flex items-center justify-between">
273
- <span>{info.filename}</span>
274
- <span className="flex gap-1">
275
  <button
276
  title={isEnlarged ? "Minimize" : "Enlarge"}
277
- className="ml-2 p-1 hover:bg-slate-700 rounded"
278
  onClick={() =>
279
  setEnlargedVideo(isEnlarged ? null : info.filename)
280
  }
281
  >
282
- {isEnlarged ? <FaCompress /> : <FaExpand />}
 
 
 
 
283
  </button>
284
  <button
285
  title="Hide Video"
286
- className="ml-1 p-1 hover:bg-slate-700 rounded"
287
  onClick={() =>
288
  setHiddenVideos((prev) => [...prev, info.filename])
289
  }
@@ -293,7 +297,7 @@ export const SimpleVideosPlayer = ({
293
  ).length === 1
294
  }
295
  >
296
- <FaTimes />
297
  </button>
298
  </span>
299
  </p>
 
224
  {hiddenVideos.length > 0 && (
225
  <div className="relative mb-4">
226
  <button
227
+ className="inline-flex items-center gap-2 h-8 rounded-md panel px-3 text-xs text-slate-300 hover:text-slate-100 hover:bg-white/5 transition-colors"
228
  onClick={() => setShowHiddenMenu(!showHiddenMenu)}
229
  >
230
+ <FaEye size={11} /> Show hidden · {hiddenVideos.length}
231
  </button>
232
  {showHiddenMenu && (
233
+ <div className="absolute left-0 mt-1.5 w-max panel-raised bg-[var(--surface-1)] shadow-xl p-1.5 z-50">
234
+ <div className="mb-1 px-2 text-[10px] uppercase tracking-wide text-slate-500">
235
+ Restore hidden videos
236
  </div>
237
  {hiddenVideos.map((filename) => (
238
  <button
239
  key={filename}
240
+ className="block w-full text-left px-2 py-1 rounded-md text-xs text-slate-300 hover:text-slate-100 hover:bg-white/5 transition-colors"
241
  onClick={() =>
242
  setHiddenVideos((prev) =>
243
  prev.filter((v) => v !== filename),
 
269
  : "max-w-96"
270
  }`}
271
  >
272
+ <p className="truncate w-full rounded-t-md bg-[var(--surface-1)] border border-b-0 border-white/5 px-2.5 py-1 text-[11px] text-slate-400 flex items-center justify-between gap-2">
273
+ <span className="truncate">{info.filename}</span>
274
+ <span className="flex gap-0.5 shrink-0">
275
  <button
276
  title={isEnlarged ? "Minimize" : "Enlarge"}
277
+ className="p-1 rounded text-slate-500 hover:text-slate-200 hover:bg-white/5 transition-colors"
278
  onClick={() =>
279
  setEnlargedVideo(isEnlarged ? null : info.filename)
280
  }
281
  >
282
+ {isEnlarged ? (
283
+ <FaCompress size={10} />
284
+ ) : (
285
+ <FaExpand size={10} />
286
+ )}
287
  </button>
288
  <button
289
  title="Hide Video"
290
+ className="p-1 rounded text-slate-500 hover:text-slate-200 hover:bg-white/5 transition-colors disabled:opacity-30 disabled:hover:bg-transparent"
291
  onClick={() =>
292
  setHiddenVideos((prev) => [...prev, info.filename])
293
  }
 
297
  ).length === 1
298
  }
299
  >
300
+ <FaTimes size={10} />
301
  </button>
302
  </span>
303
  </p>
src/components/stats-panel.tsx CHANGED
@@ -104,7 +104,7 @@ function EpisodeLengthHistogram({
104
 
105
  function Card({ label, value }: { label: string; value: string | number }) {
106
  return (
107
- <div className="bg-slate-800/60 rounded-lg p-4 border border-slate-700">
108
  <p className="text-xs text-slate-400 uppercase tracking-wide">{label}</p>
109
  <p className="text-xl font-bold tabular-nums mt-1">{value}</p>
110
  </div>
@@ -154,13 +154,16 @@ function StatsPanel({
154
 
155
  {/* Camera resolutions */}
156
  {datasetInfo.cameras.length > 0 && (
157
- <div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700">
158
  <h3 className="text-sm font-semibold text-slate-200 mb-3">
159
  Camera Resolutions
160
  </h3>
161
  <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
162
  {datasetInfo.cameras.map((cam: CameraInfo) => (
163
- <div key={cam.name} className="bg-slate-900/50 rounded-md p-3">
 
 
 
164
  <p
165
  className="text-xs text-slate-400 mb-1 truncate"
166
  title={cam.name}
@@ -201,7 +204,7 @@ function StatsPanel({
201
  {/* Episode length section */}
202
  {els && (
203
  <>
204
- <div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700">
205
  <h3 className="text-sm font-semibold text-slate-200 mb-4">
206
  Episode Lengths
207
  </h3>
@@ -221,7 +224,7 @@ function StatsPanel({
221
  </div>
222
 
223
  {els.episodeLengthHistogram.length > 0 && (
224
- <div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700">
225
  <h3 className="text-sm font-semibold text-slate-200 mb-4">
226
  Episode Length Distribution
227
  <span className="text-xs text-slate-500 ml-2 font-normal">
 
104
 
105
  function Card({ label, value }: { label: string; value: string | number }) {
106
  return (
107
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-4 border border-white/10">
108
  <p className="text-xs text-slate-400 uppercase tracking-wide">{label}</p>
109
  <p className="text-xl font-bold tabular-nums mt-1">{value}</p>
110
  </div>
 
154
 
155
  {/* Camera resolutions */}
156
  {datasetInfo.cameras.length > 0 && (
157
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-5 border border-white/10">
158
  <h3 className="text-sm font-semibold text-slate-200 mb-3">
159
  Camera Resolutions
160
  </h3>
161
  <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
162
  {datasetInfo.cameras.map((cam: CameraInfo) => (
163
+ <div
164
+ key={cam.name}
165
+ className="bg-[var(--surface-0)]/50 rounded-md p-3"
166
+ >
167
  <p
168
  className="text-xs text-slate-400 mb-1 truncate"
169
  title={cam.name}
 
204
  {/* Episode length section */}
205
  {els && (
206
  <>
207
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-5 border border-white/10">
208
  <h3 className="text-sm font-semibold text-slate-200 mb-4">
209
  Episode Lengths
210
  </h3>
 
224
  </div>
225
 
226
  {els.episodeLengthHistogram.length > 0 && (
227
+ <div className="bg-[var(--surface-1)]/60 rounded-lg p-5 border border-white/10">
228
  <h3 className="text-sm font-semibold text-slate-200 mb-4">
229
  Episode Length Distribution
230
  <span className="text-xs text-slate-500 ml-2 font-normal">
src/components/urdf-playback-bar.tsx CHANGED
@@ -37,7 +37,7 @@ export default function UrdfPlaybackBar({
37
  <button
38
  onClick={onPlayPause}
39
  disabled={disabled}
40
- className="w-8 h-8 flex items-center justify-center rounded bg-orange-600 hover:bg-orange-500 disabled:bg-slate-700 disabled:hover:bg-slate-700 disabled:cursor-not-allowed text-white transition-colors shrink-0"
41
  >
42
  {playing ? (
43
  <svg width="12" height="14" viewBox="0 0 12 14">
@@ -57,8 +57,8 @@ export default function UrdfPlaybackBar({
57
  disabled={disabled}
58
  className={`px-2 h-8 text-xs rounded transition-colors shrink-0 disabled:cursor-not-allowed ${
59
  trailEnabled
60
- ? "bg-orange-600/30 text-orange-400 border border-orange-500"
61
- : "bg-slate-700 text-slate-400 border border-slate-600"
62
  }`}
63
  title={trailEnabled ? "Hide trail" : "Show trail"}
64
  >
@@ -73,7 +73,7 @@ export default function UrdfPlaybackBar({
73
  value={frame}
74
  onChange={onFrameChange}
75
  disabled={disabled}
76
- className="flex-1 h-1.5 accent-orange-500 cursor-pointer disabled:cursor-not-allowed"
77
  />
78
  <span className="text-xs text-slate-400 tabular-nums w-28 text-right shrink-0">
79
  {currentTime}s / {totalTime}s
@@ -85,7 +85,7 @@ export default function UrdfPlaybackBar({
85
  {/* Keyboard hints */}
86
  <div className="text-xs text-slate-500 select-none hidden md:flex flex-col gap-y-0.5 ml-2 shrink-0">
87
  <p>
88
- <span className="px-1.5 py-0.5 rounded border border-slate-600 bg-slate-800 text-slate-400 text-xs">
89
  Space
90
  </span>{" "}
91
  pause/unpause
 
37
  <button
38
  onClick={onPlayPause}
39
  disabled={disabled}
40
+ className="w-8 h-8 flex items-center justify-center rounded-md bg-cyan-400/10 border border-cyan-400/30 text-cyan-300 hover:bg-cyan-400/15 disabled:bg-white/5 disabled:border-white/5 disabled:text-slate-500 disabled:cursor-not-allowed transition-colors shrink-0"
41
  >
42
  {playing ? (
43
  <svg width="12" height="14" viewBox="0 0 12 14">
 
57
  disabled={disabled}
58
  className={`px-2 h-8 text-xs rounded transition-colors shrink-0 disabled:cursor-not-allowed ${
59
  trailEnabled
60
+ ? "bg-cyan-400/15 text-cyan-300 border border-cyan-400/40"
61
+ : "bg-white/5 text-slate-400 border border-white/10"
62
  }`}
63
  title={trailEnabled ? "Hide trail" : "Show trail"}
64
  >
 
73
  value={frame}
74
  onChange={onFrameChange}
75
  disabled={disabled}
76
+ className="flex-1 h-1.5 accent-cyan-400 cursor-pointer disabled:cursor-not-allowed"
77
  />
78
  <span className="text-xs text-slate-400 tabular-nums w-28 text-right shrink-0">
79
  {currentTime}s / {totalTime}s
 
85
  {/* Keyboard hints */}
86
  <div className="text-xs text-slate-500 select-none hidden md:flex flex-col gap-y-0.5 ml-2 shrink-0">
87
  <p>
88
+ <span className="px-1.5 py-0.5 rounded border border-white/10 bg-[var(--surface-1)] text-slate-400 text-xs">
89
  Space
90
  </span>{" "}
91
  pause/unpause
src/components/urdf-viewer.tsx CHANGED
@@ -946,9 +946,9 @@ export default function URDFViewer({
946
  return (
947
  <div className="flex-1 flex flex-col overflow-hidden">
948
  {/* 3D Viewport */}
949
- <div className="flex-1 min-h-0 bg-slate-900 rounded-lg overflow-hidden border border-slate-700 relative">
950
  {(episodeLoading || urdfLoading) && (
951
- <div className="absolute inset-0 z-10 flex items-center justify-center bg-slate-900/70">
952
  <span className="text-white text-lg animate-pulse">
953
  {urdfLoading
954
  ? "Loading 3D model…"
@@ -1046,7 +1046,7 @@ export default function URDFViewer({
1046
  </div>
1047
 
1048
  {/* Controls */}
1049
- <div className="bg-slate-800/90 border-t border-slate-700 p-3 space-y-3 shrink-0">
1050
  <UrdfPlaybackBar
1051
  frame={frame}
1052
  totalFrames={totalFrames}
@@ -1087,8 +1087,8 @@ export default function URDFViewer({
1087
  onClick={() => setSelectedGroup(name)}
1088
  className={`px-2 py-1 text-xs rounded transition-colors ${
1089
  selectedGroup === name
1090
- ? "bg-orange-600 text-white"
1091
- : "bg-slate-700 text-slate-300 hover:bg-slate-600"
1092
  }`}
1093
  >
1094
  {name}
@@ -1099,7 +1099,7 @@ export default function URDFViewer({
1099
 
1100
  <div className="flex-1 overflow-x-auto max-h-48 overflow-y-auto">
1101
  <table className="w-full text-xs">
1102
- <thead className="sticky top-0 bg-slate-800">
1103
  <tr className="text-slate-500">
1104
  <th className="text-left font-normal px-1">URDF Joint</th>
1105
  <th className="text-left font-normal px-1">→</th>
@@ -1111,10 +1111,7 @@ export default function URDFViewer({
1111
  </thead>
1112
  <tbody>
1113
  {displayJointNames.map((jointName) => (
1114
- <tr
1115
- key={jointName}
1116
- className="border-t border-slate-700/50"
1117
- >
1118
  <td className="px-1 py-0.5 text-slate-300 font-mono">
1119
  {jointName}
1120
  </td>
@@ -1128,7 +1125,7 @@ export default function URDFViewer({
1128
  [jointName]: e.target.value,
1129
  }))
1130
  }
1131
- className="bg-slate-900 text-slate-200 text-xs rounded px-1 py-0.5 border border-slate-600 w-full max-w-[200px]"
1132
  >
1133
  <option value="">-- unmapped --</option>
1134
  {selectedColumns.map((col) => {
 
946
  return (
947
  <div className="flex-1 flex flex-col overflow-hidden">
948
  {/* 3D Viewport */}
949
+ <div className="flex-1 min-h-0 bg-[var(--surface-0)] rounded-lg overflow-hidden border border-white/10 relative">
950
  {(episodeLoading || urdfLoading) && (
951
+ <div className="absolute inset-0 z-10 flex items-center justify-center bg-[var(--bg)]/80">
952
  <span className="text-white text-lg animate-pulse">
953
  {urdfLoading
954
  ? "Loading 3D model…"
 
1046
  </div>
1047
 
1048
  {/* Controls */}
1049
+ <div className="bg-[var(--surface-1)]/90 border-t border-white/10 p-3 space-y-3 shrink-0">
1050
  <UrdfPlaybackBar
1051
  frame={frame}
1052
  totalFrames={totalFrames}
 
1087
  onClick={() => setSelectedGroup(name)}
1088
  className={`px-2 py-1 text-xs rounded transition-colors ${
1089
  selectedGroup === name
1090
+ ? "bg-cyan-500 text-white"
1091
+ : "bg-white/5 text-slate-300 hover:bg-white/5"
1092
  }`}
1093
  >
1094
  {name}
 
1099
 
1100
  <div className="flex-1 overflow-x-auto max-h-48 overflow-y-auto">
1101
  <table className="w-full text-xs">
1102
+ <thead className="sticky top-0 bg-[var(--surface-1)]">
1103
  <tr className="text-slate-500">
1104
  <th className="text-left font-normal px-1">URDF Joint</th>
1105
  <th className="text-left font-normal px-1">→</th>
 
1111
  </thead>
1112
  <tbody>
1113
  {displayJointNames.map((jointName) => (
1114
+ <tr key={jointName} className="border-t border-white/10/50">
 
 
 
1115
  <td className="px-1 py-0.5 text-slate-300 font-mono">
1116
  {jointName}
1117
  </td>
 
1125
  [jointName]: e.target.value,
1126
  }))
1127
  }
1128
+ className="bg-[var(--surface-0)] text-slate-200 text-xs rounded px-1 py-0.5 border border-white/10 w-full max-w-[200px]"
1129
  >
1130
  <option value="">-- unmapped --</option>
1131
  {selectedColumns.map((col) => {
src/components/videos-player.tsx CHANGED
@@ -274,7 +274,7 @@ export const VideosPlayer = ({
274
  <>
275
  {/* Error message */}
276
  {videoCodecError && (
277
- <div className="font-medium text-orange-700">
278
  <p>
279
  Videos could NOT play because{" "}
280
  <a
@@ -322,7 +322,7 @@ export const VideosPlayer = ({
322
  <div className="relative">
323
  <button
324
  ref={showHiddenBtnRef}
325
- className="flex items-center gap-2 rounded bg-slate-800 px-3 py-2 text-sm text-slate-100 hover:bg-slate-700 border border-slate-500"
326
  onClick={() => setShowHiddenMenu((prev) => !prev)}
327
  >
328
  <FaEye /> Show Hidden Videos ({hiddenVideos.length})
@@ -330,7 +330,7 @@ export const VideosPlayer = ({
330
  {showHiddenMenu && (
331
  <div
332
  ref={hiddenMenuRef}
333
- className="absolute left-0 mt-2 w-max rounded border border-slate-500 bg-slate-900 shadow-lg p-2 z-50"
334
  >
335
  <div className="mb-2 text-xs text-slate-300">
336
  Restore hidden videos:
@@ -338,7 +338,7 @@ export const VideosPlayer = ({
338
  {hiddenVideos.map((filename) => (
339
  <button
340
  key={filename}
341
- className="block w-full text-left px-2 py-1 rounded hover:bg-slate-700 text-slate-100"
342
  onClick={() =>
343
  setHiddenVideos((prev: string[]) =>
344
  prev.filter((v: string) => v !== filename),
@@ -373,7 +373,7 @@ export const VideosPlayer = ({
373
  <span className="flex gap-1">
374
  <button
375
  title={isEnlarged ? "Minimize" : "Enlarge"}
376
- className="ml-2 p-1 hover:bg-slate-700 rounded focus:outline-none focus:ring-0"
377
  onClick={() =>
378
  setEnlargedVideo(isEnlarged ? null : video.filename)
379
  }
@@ -382,7 +382,7 @@ export const VideosPlayer = ({
382
  </button>
383
  <button
384
  title="Hide Video"
385
- className="ml-1 p-1 hover:bg-slate-700 rounded focus:outline-none focus:ring-0"
386
  onClick={() =>
387
  setHiddenVideos((prev: string[]) => [
388
  ...prev,
 
274
  <>
275
  {/* Error message */}
276
  {videoCodecError && (
277
+ <div className="font-medium text-amber-400">
278
  <p>
279
  Videos could NOT play because{" "}
280
  <a
 
322
  <div className="relative">
323
  <button
324
  ref={showHiddenBtnRef}
325
+ className="flex items-center gap-2 rounded bg-[var(--surface-1)] px-3 py-2 text-sm text-slate-100 hover:bg-white/5 border border-white/10"
326
  onClick={() => setShowHiddenMenu((prev) => !prev)}
327
  >
328
  <FaEye /> Show Hidden Videos ({hiddenVideos.length})
 
330
  {showHiddenMenu && (
331
  <div
332
  ref={hiddenMenuRef}
333
+ className="absolute left-0 mt-2 w-max rounded border border-white/10 bg-[var(--surface-0)] shadow-lg p-2 z-50"
334
  >
335
  <div className="mb-2 text-xs text-slate-300">
336
  Restore hidden videos:
 
338
  {hiddenVideos.map((filename) => (
339
  <button
340
  key={filename}
341
+ className="block w-full text-left px-2 py-1 rounded hover:bg-white/5 text-slate-100"
342
  onClick={() =>
343
  setHiddenVideos((prev: string[]) =>
344
  prev.filter((v: string) => v !== filename),
 
373
  <span className="flex gap-1">
374
  <button
375
  title={isEnlarged ? "Minimize" : "Enlarge"}
376
+ className="ml-2 p-1 hover:bg-white/5 rounded focus:outline-none focus:ring-0"
377
  onClick={() =>
378
  setEnlargedVideo(isEnlarged ? null : video.filename)
379
  }
 
382
  </button>
383
  <button
384
  title="Hide Video"
385
+ className="ml-1 p-1 hover:bg-white/5 rounded focus:outline-none focus:ring-0"
386
  onClick={() =>
387
  setHiddenVideos((prev: string[]) => [
388
  ...prev,