Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Sync from GitHub via hub-sync
Browse files- src/app/[org]/[dataset]/[episode]/episode-viewer.tsx +62 -115
- src/app/explore/explore-grid.tsx +13 -11
- src/app/globals.css +71 -18
- src/app/page.tsx +8 -8
- src/components/action-insights-panel.tsx +36 -39
- src/components/data-recharts.tsx +5 -5
- src/components/filtering-panel.tsx +19 -19
- src/components/loading-component.tsx +11 -9
- src/components/overview-panel.tsx +12 -12
- src/components/playback-bar.tsx +27 -35
- src/components/side-nav.tsx +106 -73
- src/components/simple-videos-player.tsx +17 -13
- src/components/stats-panel.tsx +8 -5
- src/components/urdf-playback-bar.tsx +5 -5
- src/components/urdf-viewer.tsx +8 -11
- src/components/videos-player.tsx +6 -6
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-
|
| 97 |
-
<div className="max-w-xl p-
|
| 98 |
-
<h2 className="text-
|
| 99 |
-
<p className="text-
|
|
|
|
|
|
|
| 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-
|
| 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-
|
| 485 |
{/* Top tab bar */}
|
| 486 |
-
<div className="flex items-center border-b border-
|
| 487 |
-
<
|
| 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 |
-
<
|
| 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 |
-
</
|
| 529 |
-
<
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 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
|
| 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-
|
| 628 |
/>
|
| 629 |
</a>
|
| 630 |
|
| 631 |
-
<div>
|
| 632 |
<a
|
| 633 |
href={`https://huggingface.co/datasets/${datasetInfo.repoId}`}
|
| 634 |
target="_blank"
|
|
|
|
| 635 |
>
|
| 636 |
-
<p className="text-
|
| 637 |
{datasetInfo.repoId}
|
| 638 |
</p>
|
| 639 |
</a>
|
| 640 |
-
|
| 641 |
-
|
| 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
|
| 658 |
-
<p className="text-slate-
|
| 659 |
-
|
| 660 |
-
Language Instruction:
|
| 661 |
-
</span>
|
| 662 |
</p>
|
| 663 |
-
<div className="mt-
|
| 664 |
{task
|
| 665 |
.split("\n")
|
| 666 |
.map((instruction: string, index: number) => (
|
| 667 |
-
<p key={index}
|
| 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="
|
| 30 |
-
<h1 className="text-
|
| 31 |
-
|
|
|
|
|
|
|
| 32 |
{datasets.map((ds, idx) => (
|
| 33 |
<Link
|
| 34 |
key={ds.id}
|
| 35 |
href={`/${ds.id}`}
|
| 36 |
-
className="relative
|
| 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
|
| 68 |
-
<div className="relative z-20
|
| 69 |
{ds.id}
|
| 70 |
</div>
|
| 71 |
</Link>
|
| 72 |
))}
|
| 73 |
</div>
|
| 74 |
-
<div className="flex justify-center mt-8 gap-
|
| 75 |
{currentPage > 1 && (
|
| 76 |
<button
|
| 77 |
-
className="px-
|
| 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-
|
| 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 |
-
|
| 5 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
}
|
| 7 |
|
| 8 |
@theme inline {
|
| 9 |
-
--color-
|
| 10 |
-
--color-
|
|
|
|
|
|
|
|
|
|
| 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(--
|
| 29 |
-
color: var(--
|
| 30 |
-
font-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 45 |
}
|
| 46 |
|
| 47 |
@keyframes fadeInUp {
|
| 48 |
from {
|
| 49 |
opacity: 0;
|
| 50 |
-
transform: translateY(
|
| 51 |
}
|
| 52 |
to {
|
| 53 |
opacity: 1;
|
|
@@ -56,5 +109,5 @@ body {
|
|
| 56 |
}
|
| 57 |
|
| 58 |
.animate-fade-in-up {
|
| 59 |
-
animation: fadeInUp 0.
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 250 |
-
: "text-slate-200 hover:bg-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 106 |
<button
|
| 107 |
onClick={() => setFs(false)}
|
| 108 |
-
className="fixed top-4 right-4 z-50 p-2 rounded bg-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 372 |
-
<span className="text-
|
| 373 |
{suggestedChunk}
|
| 374 |
</span>
|
| 375 |
<div>
|
| 376 |
-
<p className="text-sm text-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
<br />
|
| 1157 |
<span className="text-slate-500">
|
| 1158 |
Based on "Is Diversity All You Need" (AGI-Bot, 2025)
|
|
@@ -1234,7 +1231,7 @@ function SpeedVarianceSection({
|
|
| 1234 |
</div>
|
| 1235 |
</div>
|
| 1236 |
|
| 1237 |
-
<div className="bg-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 1465 |
-
<span className="text-
|
| 1466 |
{meanPeakLag}
|
| 1467 |
</span>
|
| 1468 |
<div>
|
| 1469 |
-
<p className="text-sm text-
|
| 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-
|
| 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-
|
| 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 "Is Diversity All You Need" (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-
|
| 87 |
-
: "bg-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 172 |
<div
|
| 173 |
-
className="absolute top-1/2 -translate-y-1/2 h-1 rounded bg-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 258 |
<div className="flex items-center justify-between">
|
| 259 |
-
<h3 className="text-sm font-semibold text-
|
| 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-
|
| 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-
|
| 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-
|
| 340 |
>
|
| 341 |
LeRobot CLI
|
| 342 |
</a>{" "}
|
| 343 |
— delete flagged episodes:
|
| 344 |
</p>
|
| 345 |
-
<pre className="text-xs text-slate-300 bg-
|
| 346 |
-
<pre className="text-xs text-slate-300 bg-
|
| 347 |
</div>
|
| 348 |
</div>
|
| 349 |
);
|
|
@@ -377,7 +377,7 @@ function FilteringPanel({
|
|
| 377 |
)}
|
| 378 |
|
| 379 |
{crossEpisodeLoading && (
|
| 380 |
-
<div className="bg-
|
| 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-
|
| 7 |
tabIndex={-1}
|
| 8 |
aria-modal="true"
|
| 9 |
role="dialog"
|
| 10 |
>
|
| 11 |
<svg
|
| 12 |
-
className="animate-spin mb-
|
| 13 |
-
width="
|
| 14 |
-
height="
|
| 15 |
viewBox="0 0 24 24"
|
| 16 |
fill="none"
|
| 17 |
xmlns="http://www.w3.org/2000/svg"
|
| 18 |
>
|
| 19 |
<circle
|
| 20 |
-
className="opacity-
|
| 21 |
cx="12"
|
| 22 |
cy="12"
|
| 23 |
r="10"
|
| 24 |
stroke="currentColor"
|
| 25 |
-
strokeWidth="
|
| 26 |
/>
|
| 27 |
<path
|
| 28 |
-
className="opacity-
|
| 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-
|
| 34 |
-
|
|
|
|
|
|
|
| 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 & 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-
|
| 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-
|
| 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-
|
| 82 |
-
: "opacity-0 group-hover:opacity-100 text-slate-400 hover:text-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 232 |
-
: "text-slate-400 hover:text-slate-200 border border-
|
| 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-
|
| 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-
|
| 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-
|
| 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="
|
| 52 |
<button
|
| 53 |
title="Jump backward 5 seconds"
|
| 54 |
onClick={() => setCurrentTime(Math.max(0, currentTime - 5))}
|
| 55 |
-
className="
|
| 56 |
>
|
| 57 |
-
<FaBackward size={
|
| 58 |
</button>
|
| 59 |
<button
|
| 60 |
-
className=
|
| 61 |
-
title=
|
| 62 |
-
|
| 63 |
-
|
|
|
|
| 64 |
>
|
| 65 |
-
<FaPlay size={
|
| 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="
|
| 79 |
>
|
| 80 |
-
<FaForward size={
|
| 81 |
</button>
|
| 82 |
<button
|
| 83 |
title="Rewind from start"
|
| 84 |
onClick={() => setCurrentTime(0)}
|
| 85 |
-
className="
|
| 86 |
>
|
| 87 |
-
<FaUndoAlt size={
|
| 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-
|
| 101 |
aria-label="Seek video"
|
| 102 |
/>
|
| 103 |
-
<span className="w-16 text-right tabular
|
| 104 |
{Math.floor(sliderValue)} / {Math.floor(duration)}
|
| 105 |
</span>
|
| 106 |
|
| 107 |
-
<div className="
|
| 108 |
-
<p>
|
| 109 |
-
<
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
</span>{" "}
|
| 114 |
-
to pause/unpause
|
| 115 |
</p>
|
| 116 |
-
<p>
|
| 117 |
-
<span className="inline-flex items-center gap-
|
| 118 |
-
<FaArrowUp size={
|
| 119 |
-
|
| 120 |
-
|
|
|
|
| 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-
|
| 46 |
mobileVisible ? "block" : "hidden"
|
| 47 |
} md:block`}
|
| 48 |
aria-label="Sidebar navigation"
|
| 49 |
>
|
| 50 |
-
<
|
| 51 |
-
<
|
| 52 |
-
|
| 53 |
-
<
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
<div className="mt-
|
| 57 |
-
<p className="text-
|
|
|
|
|
|
|
| 58 |
{count > 0 && (
|
| 59 |
<button
|
| 60 |
onClick={() => onShowFlaggedOnlyChange(!showFlaggedOnly)}
|
| 61 |
-
className={`text-
|
| 62 |
showFlaggedOnly
|
| 63 |
-
? "bg-orange-500/
|
| 64 |
-
: "text-slate-500 hover:text-slate-300 border border-
|
| 65 |
}`}
|
| 66 |
>
|
| 67 |
-
Flagged
|
| 68 |
</button>
|
| 69 |
)}
|
| 70 |
</div>
|
| 71 |
|
| 72 |
-
<
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
| 79 |
{onEpisodeSelect ? (
|
| 80 |
-
<
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
) : (
|
| 87 |
-
<
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 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-
|
| 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
|
| 228 |
onClick={() => setShowHiddenMenu(!showHiddenMenu)}
|
| 229 |
>
|
| 230 |
-
<FaEye /> Show
|
| 231 |
</button>
|
| 232 |
{showHiddenMenu && (
|
| 233 |
-
<div className="absolute left-0 mt-
|
| 234 |
-
<div className="mb-2 text-
|
| 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
|
| 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-
|
| 273 |
-
<span>{info.filename}</span>
|
| 274 |
-
<span className="flex gap-
|
| 275 |
<button
|
| 276 |
title={isEnlarged ? "Minimize" : "Enlarge"}
|
| 277 |
-
className="
|
| 278 |
onClick={() =>
|
| 279 |
setEnlargedVideo(isEnlarged ? null : info.filename)
|
| 280 |
}
|
| 281 |
>
|
| 282 |
-
{isEnlarged ?
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
</button>
|
| 284 |
<button
|
| 285 |
title="Hide Video"
|
| 286 |
-
className="
|
| 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-
|
| 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-
|
| 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 |
<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-
|
| 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-
|
| 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-
|
| 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-
|
| 61 |
-
: "bg-
|
| 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-
|
| 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-
|
| 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-
|
| 950 |
{(episodeLoading || urdfLoading) && (
|
| 951 |
-
<div className="absolute inset-0 z-10 flex items-center justify-center bg-
|
| 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-
|
| 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-
|
| 1091 |
-
: "bg-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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-
|
| 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,
|