Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ yarn-error.log*

# Local dev / tool state
.claude/
.dev/
.playwright-mcp/
.superpowers/
.worktrees/
Expand Down
4,277 changes: 4,277 additions & 0 deletions frontend/dist/assets/index-DD3QRr1z.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/dist/assets/index-Ds5-5MuP.css

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions frontend/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
<meta property="og:title" content="LeLab" />
<meta property="og:description" content="LeRobot but on your browser." />
<meta property="og:type" content="website" />
<script type="module" crossorigin src="/assets/index-BYEP-1Zk.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DmcGvCXo.css">
<script type="module" crossorigin src="/assets/index-DD3QRr1z.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Ds5-5MuP.css">
</head>

<body>
Expand Down
53 changes: 34 additions & 19 deletions frontend/src/components/jobs/HubJobCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {

interface Props {
job: HubJob;
onOpen?: (job: HubJob) => void;
busy?: boolean;
}

function relativeTime(iso: string | null): string {
Expand Down Expand Up @@ -45,7 +47,7 @@ const stagePresentation: Record<string, StagePresentation> = {
CANCELLED: { label: "Cancelled", color: "text-amber-400", Icon: AlertTriangle },
};

const HubJobCard: React.FC<Props> = ({ job }) => {
const HubJobCard: React.FC<Props> = ({ job, onOpen, busy = false }) => {
const stage = job.status?.stage?.toUpperCase() ?? "";
const present: StagePresentation = stagePresentation[stage] ?? {
label: stage || "Unknown",
Expand All @@ -55,34 +57,47 @@ const HubJobCard: React.FC<Props> = ({ job }) => {
const Icon = present.Icon;
const title =
job.docker_image ?? job.space_id ?? `Job ${job.id.slice(0, 12)}…`;
const hasUrl = Boolean(job.url);
const canOpen = Boolean(onOpen || hasUrl);

return (
<Card
onClick={() => window.open(job.url, "_blank", "noopener,noreferrer")}
className="bg-slate-800/50 border-slate-700 rounded-xl cursor-pointer hover:border-slate-500 transition-colors"
onClick={() => {
if (busy) return;
if (onOpen) {
onOpen(job);
return;
}
if (hasUrl) window.open(job.url, "_blank", "noopener,noreferrer");
}}
className={`bg-slate-800/50 border-slate-700 rounded-xl hover:border-slate-500 transition-colors ${
canOpen ? "cursor-pointer" : ""
}`}
>
<CardContent className="p-4 space-y-3">
<div className="flex items-start justify-between gap-2">
<div className={`flex items-center gap-1.5 text-xs font-semibold ${present.color}`}>
<Icon className={`w-3.5 h-3.5 ${present.spin ? "animate-spin" : ""}`} />
<Icon className={`w-3.5 h-3.5 ${present.spin || busy ? "animate-spin" : ""}`} />
{present.label}
</div>
<Button
variant="ghost"
size="icon"
asChild
className="h-7 w-7 text-slate-400 hover:text-white"
aria-label="View on Hub"
>
<a
href={job.url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
{hasUrl ? (
<Button
variant="ghost"
size="icon"
asChild
className="h-7 w-7 text-slate-400 hover:text-white"
aria-label="View provider job"
>
<ExternalLink className="w-3.5 h-3.5" />
</a>
</Button>
<a
href={job.url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<ExternalLink className="w-3.5 h-3.5" />
</a>
</Button>
) : null}
</div>
<div>
<div className="text-white font-semibold truncate" title={title}>
Expand Down
57 changes: 52 additions & 5 deletions frontend/src/components/jobs/JobsSection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useApi } from "@/contexts/ApiContext";
Expand All @@ -9,6 +10,7 @@ import {
HubModel,
JobProgressSnapshot,
JobRecord,
attachProviderJob,
deleteJob,
listHubJobs,
listJobs,
Expand Down Expand Up @@ -42,13 +44,15 @@ const isHubJobActive = (h: HubJob) =>
const JobsSection: React.FC = () => {
const { baseUrl, fetchWithHeaders } = useApi();
const { toast } = useToast();
const navigate = useNavigate();

const [jobs, setJobs] = useState<JobRecord[]>([]);
const [hubJobs, setHubJobs] = useState<HubJob[]>([]);
const [hubModels, setHubModels] = useState<HubModel[]>([]);
const [hubAuthenticated, setHubAuthenticated] = useState(false);
const [error, setError] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [attachingJobId, setAttachingJobId] = useState<string | null>(null);

const { selectedRecord } = useRobots();
const [inferenceModalOpen, setInferenceModalOpen] = useState(false);
Expand Down Expand Up @@ -148,6 +152,30 @@ const JobsSection: React.FC = () => {
}
};

const handleHubJobOpen = async (job: HubJob) => {
if (job.provider !== "seeed_cloud") {
if (job.url) window.open(job.url, "_blank", "noopener,noreferrer");
return;
}
try {
setAttachingJobId(job.id);
const record = await attachProviderJob(baseUrl, fetchWithHeaders, "seeed_cloud", job.id);
setJobs((prev) => {
if (prev.some((item) => item.id === record.id)) return prev;
return [record, ...prev];
});
navigate(`/training/${record.id}`);
} catch (e) {
toast({
title: "Attach failed",
description: e instanceof Error ? e.message : String(e),
variant: "destructive",
});
} finally {
setAttachingJobId(null);
}
};

const query = search.trim().toLowerCase();
const matchesQuery = useCallback(
(text: string | null | undefined) =>
Expand Down Expand Up @@ -176,7 +204,7 @@ const JobsSection: React.FC = () => {
[filteredJobs],
);
const trackedCloudJobs = useMemo(
() => filteredJobs.filter((j) => j.runner === "hf_cloud"),
() => filteredJobs.filter((j) => j.runner === "hf_cloud" || j.runner === "seeed_cloud"),
[filteredJobs],
);
const importedJobs = useMemo(
Expand All @@ -195,8 +223,17 @@ const JobsSection: React.FC = () => {
[trackedCloudJobs],
);
const untrackedHubJobs = useMemo(
() => filteredHubJobs.filter((h) => !trackedHfJobIds.has(h.id)),
[filteredHubJobs, trackedHfJobIds],
() =>
filteredHubJobs.filter((h) => {
if (h.provider === "hf_cloud") return !trackedHfJobIds.has(h.id);
if (h.provider === "seeed_cloud") {
return !trackedCloudJobs.some(
(j) => j.external_provider === "seeed_cloud" && j.external_job_id === h.id,
);
}
return true;
}),
[filteredHubJobs, trackedCloudJobs, trackedHfJobIds],
);
// Hide model repos that map 1-to-1 to a tracked cloud job (those already
// appear via JobCard); the remainder are past trainings the registry no
Expand Down Expand Up @@ -369,7 +406,12 @@ const JobsSection: React.FC = () => {
/>
))}
{untrackedHubActive.map((job) => (
<HubJobCard key={job.id} job={job} />
<HubJobCard
key={job.id}
job={job}
onOpen={handleHubJobOpen}
busy={attachingJobId === job.id}
/>
))}
{untrackedHubModels.map((model) => (
<HubModelCard key={model.repo_id} model={model} />
Expand Down Expand Up @@ -406,7 +448,12 @@ const JobsSection: React.FC = () => {
/>
))}
{untrackedHubInactive.map((job) => (
<HubJobCard key={job.id} job={job} />
<HubJobCard
key={job.id}
job={job}
onOpen={handleHubJobOpen}
busy={attachingJobId === job.id}
/>
))}
</div>
</CollapsibleContent>
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/components/training/ConfigurationTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import AdvancedCard from './config/AdvancedCard';
import TargetCard from './config/TargetCard';
import { ConfigComponentProps } from './types';
import { DatasetItem } from '@/lib/replayApi';
import { RunnerFlavor } from '@/lib/jobsApi';
import { RunnerFlavor, RunnerProvider } from '@/lib/jobsApi';

interface ConfigurationTabProps extends ConfigComponentProps {
datasets: DatasetItem[];
datasetsLoading: boolean;
authenticated: boolean;
flavors: RunnerFlavor[];
providers: RunnerProvider[];
hardwareLoading: boolean;
seeedConnecting: boolean;
onConnectSeeedCloud: () => void;
}

const ConfigurationTab: React.FC<ConfigurationTabProps> = ({
Expand All @@ -21,7 +24,10 @@ const ConfigurationTab: React.FC<ConfigurationTabProps> = ({
datasetsLoading,
authenticated,
flavors,
providers,
hardwareLoading,
seeedConnecting,
onConnectSeeedCloud,
}) => {
return (
<div className="max-w-3xl mx-auto space-y-6">
Expand All @@ -30,7 +36,10 @@ const ConfigurationTab: React.FC<ConfigurationTabProps> = ({
updateConfig={updateConfig}
authenticated={authenticated}
flavors={flavors}
providers={providers}
loading={hardwareLoading}
seeedConnecting={seeedConnecting}
onConnectSeeedCloud={onConnectSeeedCloud}
/>
<EssentialsCard
config={config}
Expand Down
66 changes: 66 additions & 0 deletions frontend/src/components/training/config/AdvancedCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { Separator } from '@/components/ui/separator';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { ConfigComponentProps } from '../types';
import { policyAdvancedCapabilities } from '../trainingPolicies';

const SectionHeading: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<h4 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">
Expand All @@ -23,6 +24,7 @@ const SectionHeading: React.FC<{ children: React.ReactNode }> = ({ children }) =

const AdvancedCard: React.FC<ConfigComponentProps> = ({ config, updateConfig }) => {
const [expanded, setExpanded] = useState(false);
const policyCapabilities = policyAdvancedCapabilities(config.policy_type);

return (
<Card className="bg-slate-800/50 border-slate-700 rounded-xl">
Expand Down Expand Up @@ -84,6 +86,70 @@ const AdvancedCard: React.FC<ConfigComponentProps> = ({ config, updateConfig })
Use Automatic Mixed Precision
</Label>
</div>
{policyCapabilities.dtype && (
<div>
<Label htmlFor="policy_dtype" className="text-slate-300">
Policy dtype
</Label>
<Select
value={config.policy_dtype || 'default'}
onValueChange={(value) =>
updateConfig('policy_dtype', value === 'default' ? undefined : value)
}
>
<SelectTrigger id="policy_dtype" className="bg-slate-900 border-slate-600 text-white rounded-lg">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-600 text-white">
<SelectItem value="default">Policy default</SelectItem>
<SelectItem value="bfloat16">bfloat16</SelectItem>
<SelectItem value="float32">float32</SelectItem>
</SelectContent>
</Select>
</div>
)}
{policyCapabilities.gradientCheckpointing && (
<div className="flex items-center space-x-3 pt-6">
<Switch
id="policy_gradient_checkpointing"
checked={Boolean(config.policy_gradient_checkpointing)}
onCheckedChange={(checked) =>
updateConfig('policy_gradient_checkpointing', checked)
}
/>
<Label htmlFor="policy_gradient_checkpointing" className="text-slate-300">
Gradient checkpointing
</Label>
</div>
)}
{policyCapabilities.freezeVisionEncoder && (
<div className="flex items-center space-x-3 pt-6">
<Switch
id="policy_freeze_vision_encoder"
checked={Boolean(config.policy_freeze_vision_encoder)}
onCheckedChange={(checked) =>
updateConfig('policy_freeze_vision_encoder', checked)
}
/>
<Label htmlFor="policy_freeze_vision_encoder" className="text-slate-300">
Freeze vision encoder
</Label>
</div>
)}
{policyCapabilities.trainExpertOnly && (
<div className="flex items-center space-x-3 pt-6">
<Switch
id="policy_train_expert_only"
checked={Boolean(config.policy_train_expert_only)}
onCheckedChange={(checked) =>
updateConfig('policy_train_expert_only', checked)
}
/>
<Label htmlFor="policy_train_expert_only" className="text-slate-300">
Train expert only
</Label>
</div>
)}
</div>
</section>

Expand Down
20 changes: 10 additions & 10 deletions frontend/src/components/training/config/EssentialsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import DatasetCombobox from '@/components/replay/DatasetCombobox';
import { DatasetItem } from '@/lib/replayApi';
import WandbInstallDialog from '../WandbInstallDialog';
import { useApi } from '@/contexts/ApiContext';
import { OFFLINE_TRAINING_POLICY_OPTIONS } from '../trainingPolicies';

interface EssentialsCardProps extends ConfigComponentProps {
datasets: DatasetItem[];
Expand All @@ -26,6 +27,7 @@ const EssentialsCard: React.FC<EssentialsCardProps> = ({ config, updateConfig, d
const { baseUrl, fetchWithHeaders } = useApi();
const [wandbDialogOpen, setWandbDialogOpen] = useState(false);
const [wandbInstallHint, setWandbInstallHint] = useState('pip install wandb');
const isExternalTarget = config.target.runner === 'seeed_cloud' || config.target.runner === 'external';

const handleWandbToggle = async (checked: boolean) => {
if (!checked) {
Expand Down Expand Up @@ -71,7 +73,9 @@ const EssentialsCard: React.FC<EssentialsCardProps> = ({ config, updateConfig, d
/>
</div>
<p className="text-xs text-slate-500 mt-1">
HuggingFace Hub dataset repository ID
{isExternalTarget
? 'LeLab dataset repository ID; Seeed Cloud also accepts an archive URL or SEEED_CLOUD_DATASET_URL override'
: 'HuggingFace Hub dataset repository ID'}
</p>
</div>

Expand All @@ -88,15 +92,11 @@ const EssentialsCard: React.FC<EssentialsCardProps> = ({ config, updateConfig, d
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-600 text-white">
<SelectItem value="act">ACT (Action Chunking Transformer)</SelectItem>
<SelectItem value="diffusion">Diffusion Policy</SelectItem>
<SelectItem value="pi0">PI0</SelectItem>
<SelectItem value="smolvla">SmolVLA</SelectItem>
<SelectItem value="tdmpc">TD-MPC</SelectItem>
<SelectItem value="vqbet">VQ-BeT</SelectItem>
<SelectItem value="pi0_fast">PI0 Fast</SelectItem>
<SelectItem value="sac">SAC</SelectItem>
<SelectItem value="reward_classifier">Reward Classifier</SelectItem>
{OFFLINE_TRAINING_POLICY_OPTIONS.map((policy) => (
<SelectItem key={policy.value} value={policy.value}>
{policy.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
Expand Down
Loading