Download frontend/src/JobsView.tsx from finerobotics/annotation-studio: direct link, hf CLI and curl.
- Browser
- Download file 17 kB
-
https://huggingface.co/spaces/finerobotics/annotation-studio/resolve/main/frontend/src/JobsView.tsx
- Command line
-
hf download hf://spaces/finerobotics/annotation-studio/frontend/src/JobsView.tsx
-
curl -L -o JobsView.tsx https://huggingface.co/spaces/finerobotics/annotation-studio/resolve/main/frontend/src/JobsView.tsx
17 kB
| import { useState, type ReactNode } from "react"; | |
| import { | |
| ArrowUpRight, | |
| ChevronDown, | |
| LoaderCircle, | |
| Plus, | |
| RefreshCw, | |
| } from "lucide-react"; | |
| import "./jobs.css"; | |
| import SubtaskViews from "./SubtaskViews"; | |
| export type Job = { | |
| id: string; | |
| name?: string; | |
| stage: string; | |
| created_at: string; | |
| owner: string; | |
| output?: string; | |
| }; | |
| export type Progress = { | |
| model?: string; | |
| captioner?: string; | |
| status: string; | |
| expected: number; | |
| completed: number; | |
| exported?: number; | |
| export_pending?: number; | |
| failed: number; | |
| failed_recordings?: { | |
| source_episode_index: number; | |
| recording: string; | |
| hand: string; | |
| stage: string; | |
| error: string; | |
| }[]; | |
| stopped?: number; | |
| pending?: number; | |
| usage: { | |
| measured_inference_usd?: number; | |
| successful_calls?: number; | |
| input_tokens?: number; | |
| output_tokens?: number; | |
| rate_limited_requests?: number; | |
| media_usd?: number; | |
| completed_media_requests?: number; | |
| }; | |
| cost?: { | |
| inference_usd: number | null; | |
| cpu_usd: number | null; | |
| total_usd: number | null; | |
| uncertain_inference_usd: number; | |
| running_seconds: number | null; | |
| basis: string; | |
| billing_url: string; | |
| }; | |
| annotation_url?: string | null; | |
| hand_visualizer_url?: string | null; | |
| output_mode?: string; | |
| subtask_views?: "primary" | "per-camera" | "per-hand"; | |
| subtask_cameras?: string[]; | |
| human_videos?: Record<string, number> | null; | |
| }; | |
| type HumanVideo = { | |
| episode_index?: number | null; | |
| source_episode_index: number; | |
| segment_index?: number; | |
| subtask?: string; | |
| caption?: string | null; | |
| status: string; | |
| robot_frame?: string; | |
| first_frame?: string; | |
| video?: string; | |
| }; | |
| function HumanVideos({ jobId }: { jobId: string }) { | |
| const [videos, setVideos] = useState<HumanVideo[] | null>(null); | |
| const [error, setError] = useState(""); | |
| const [loading, setLoading] = useState(false); | |
| async function load() { | |
| setLoading(true); | |
| setError(""); | |
| try { | |
| const response = await fetch(`/api/jobs/${jobId}/human-videos`, { | |
| credentials: "same-origin", | |
| }); | |
| if (!response.ok) throw new Error("No human demo videos saved yet."); | |
| setVideos(await response.json()); | |
| } catch (e) { | |
| setError((e as Error).message); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| if (!videos) | |
| return ( | |
| <div className="human-videos"> | |
| <button className="text-action" onClick={load} disabled={loading}> | |
| {loading ? "Loading videos…" : "Preview human demo videos"} | |
| </button> | |
| {error && <p className="job-cost-note">{error}</p>} | |
| </div> | |
| ); | |
| const ready = videos.filter((v) => v.status === "completed" && v.video); | |
| return ( | |
| <div className="human-videos"> | |
| {ready.length === 0 && <p>No completed videos yet.</p>} | |
| <div className="human-video-grid"> | |
| {ready.map((v) => ( | |
| <figure key={`${v.source_episode_index}-${v.segment_index}`}> | |
| <div className="human-video-pair"> | |
| {v.robot_frame && ( | |
| <img src={v.robot_frame} alt="Robot view" loading="lazy" /> | |
| )} | |
| <video | |
| src={v.video} | |
| poster={v.first_frame} | |
| controls | |
| muted | |
| loop | |
| playsInline | |
| preload="none" | |
| /> | |
| </div> | |
| <figcaption> | |
| <strong> | |
| Episode {v.episode_index ?? v.source_episode_index} | |
| </strong>{" "} | |
| · {v.subtask} | |
| </figcaption> | |
| </figure> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| const terminal = new Set(["COMPLETED", "ERROR", "CANCELED", "DELETED"]); | |
| const usd = (value: number | null | undefined) => | |
| value == null | |
| ? "—" | |
| : new Intl.NumberFormat("en-US", { | |
| style: "currency", | |
| currency: "USD", | |
| minimumFractionDigits: value > 0 && value < 1 ? 4 : 2, | |
| maximumFractionDigits: 4, | |
| }).format(value); | |
| function status(job: Job, progress?: Progress) { | |
| if (job.stage === "ERROR") return "Failed"; | |
| if (job.stage === "COMPLETED") | |
| return progress?.status === "partial" ? "Partial" : "Completed"; | |
| if (job.stage === "CANCELED") return "Canceled"; | |
| if (["PENDING", "QUEUED"].includes(job.stage)) return "Queued"; | |
| if ( | |
| progress?.expected && | |
| progress.completed === progress.expected && | |
| progress.export_pending | |
| ) | |
| return "Exporting"; | |
| return job.stage[0] + job.stage.slice(1).toLowerCase(); | |
| } | |
| export default function JobsView({ | |
| jobs, | |
| progress, | |
| models, | |
| signedIn, | |
| loading, | |
| signIn, | |
| onRefresh, | |
| onNew, | |
| onCancel, | |
| }: { | |
| jobs: Job[]; | |
| progress: Record<string, Progress>; | |
| models: { route: string; name: string; provider_name: string }[]; | |
| signedIn: boolean; | |
| loading: boolean; | |
| signIn: ReactNode; | |
| onRefresh: () => void; | |
| onNew: () => void; | |
| onCancel: (id: string) => void; | |
| }) { | |
| return ( | |
| <div className="jobs-page"> | |
| <div className="page-heading"> | |
| <h1>Jobs</h1> | |
| <div className="heading-actions"> | |
| <button | |
| className="icon-button" | |
| onClick={onRefresh} | |
| disabled={!signedIn || loading} | |
| aria-label="Refresh jobs" | |
| > | |
| <RefreshCw size={17} className={loading ? "spin" : ""} /> | |
| </button> | |
| <button className="button primary" onClick={onNew}> | |
| <Plus size={15} /> New job | |
| </button> | |
| </div> | |
| </div> | |
| {!signedIn ? ( | |
| <div className="jobs-empty"> | |
| <p>Sign in to see your jobs.</p> | |
| {signIn} | |
| </div> | |
| ) : jobs.length === 0 ? ( | |
| <div className="jobs-empty"> | |
| {loading ? ( | |
| <> | |
| <LoaderCircle size={20} className="spin" /> | |
| <p>Loading jobs…</p> | |
| </> | |
| ) : ( | |
| <> | |
| <p>No jobs yet.</p> | |
| <button className="button secondary" onClick={onNew}> | |
| New annotation | |
| </button> | |
| </> | |
| )} | |
| </div> | |
| ) : ( | |
| <div className="compact-jobs"> | |
| <div className="jobs-columns" aria-hidden="true"> | |
| <span>Job</span> | |
| <span>Status</span> | |
| <span>Episodes</span> | |
| <span>Usage cost</span> | |
| <span /> | |
| </div> | |
| {jobs.map((job) => { | |
| const p = progress[job.id]; | |
| const ended = terminal.has(job.stage); | |
| const state = status(job, p); | |
| const profile = models.find((m) => m.route === p?.model); | |
| const model = | |
| p?.captioner === "flora" | |
| ? "Flora · TempSeg + Captioner" | |
| : profile?.name || p?.model?.split(":")[0].split("/").pop(); | |
| const provider = | |
| p?.captioner === "flora" | |
| ? "H200 GPU" | |
| : profile?.provider_name || p?.model?.split(":")[1]; | |
| const name = | |
| job.output?.split("/")[1] || job.name || "Annotation job"; | |
| const logs = `https://huggingface.co/jobs/${job.owner}/${job.id}`; | |
| const results = | |
| job.output && p?.completed | |
| ? `https://huggingface.co/datasets/${job.output}` | |
| : null; | |
| const annotations = p?.annotation_url; | |
| const hands = p?.hand_visualizer_url; | |
| const action = hands | |
| ? "View 3D hands" | |
| : annotations | |
| ? "Show annotations" | |
| : results | |
| ? "Results" | |
| : "Logs"; | |
| const knownCost = p?.cost?.total_usd ?? p?.cost?.cpu_usd; | |
| const incompleteCost = | |
| p?.cost && | |
| (p.cost.total_usd == null || | |
| p.cost.uncertain_inference_usd > 0.000001); | |
| return ( | |
| <details className="compact-job" key={job.id}> | |
| <summary className="compact-job-row"> | |
| <span className="job-identity"> | |
| <strong title={job.output || name}>{name}</strong> | |
| <span> | |
| <time dateTime={job.created_at}> | |
| {new Date(job.created_at).toLocaleString(undefined, { | |
| month: "short", | |
| day: "numeric", | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| })} | |
| </time> | |
| {model && ( | |
| <> | |
| <span className="job-meta-separator">·</span> | |
| <span className="job-model">{model}</span> | |
| </> | |
| )} | |
| </span> | |
| </span> | |
| <span className={`compact-status ${state.toLowerCase()}`}> | |
| <i /> | |
| {state} | |
| </span> | |
| <span className="compact-progress"> | |
| <span> | |
| {p?.expected | |
| ? `${p.completed.toLocaleString()} / ${p.expected.toLocaleString()} labeled` | |
| : "—"} | |
| </span> | |
| {p?.exported != null && ( | |
| <small className="export-progress"> | |
| {p.exported.toLocaleString()} ready to view | |
| </small> | |
| )} | |
| {!ended && !!p?.expected && ( | |
| <progress | |
| max={p.expected} | |
| value={p.completed} | |
| aria-label={`${name} episodes completed`} | |
| /> | |
| )} | |
| </span> | |
| <span | |
| className="compact-cost" | |
| title={ | |
| incompleteCost | |
| ? "Some usage is unconfirmed. Expand for details." | |
| : "Usage-derived cost" | |
| } | |
| > | |
| <span className="mobile-label">Cost </span> | |
| {usd(knownCost)} | |
| {incompleteCost && knownCost != null && <sup>*</sup>} | |
| </span> | |
| <span className="compact-job-action"> | |
| <a | |
| href={hands || annotations || results || logs} | |
| target="_blank" | |
| rel="noreferrer" | |
| onClick={(e) => e.stopPropagation()} | |
| > | |
| {action} | |
| <ArrowUpRight size={13} /> | |
| </a> | |
| <ChevronDown | |
| size={15} | |
| className="job-expand" | |
| aria-hidden="true" | |
| /> | |
| </span> | |
| </summary> | |
| <div className="compact-job-details"> | |
| <dl> | |
| <div> | |
| <dt>Model</dt> | |
| <dd> | |
| {model | |
| ? `${model}${provider ? ` · ${provider}` : ""}` | |
| : "—"} | |
| </dd> | |
| </div> | |
| <div> | |
| <dt>Episodes</dt> | |
| <dd> | |
| {p | |
| ? `${p.completed} labeled · ${p.exported ?? 0} ready to view${p.failed ? ` · ${p.failed} failed` : ""}${p.pending ? ` · ${p.pending} pending` : ""}` | |
| : "—"} | |
| </dd> | |
| </div> | |
| <div> | |
| <dt>Inference</dt> | |
| <dd>{usd(p?.cost?.inference_usd)}</dd> | |
| </div> | |
| <div> | |
| <dt>Compute</dt> | |
| <dd>{usd(p?.cost?.cpu_usd)}</dd> | |
| </div> | |
| <div> | |
| <dt>Model calls</dt> | |
| <dd> | |
| {p?.usage.successful_calls?.toLocaleString() ?? "—"} | |
| </dd> | |
| </div> | |
| {p?.human_videos && ( | |
| <div> | |
| <dt>Human videos</dt> | |
| <dd> | |
| {(p.human_videos.completed || 0).toLocaleString()} | |
| {p.human_videos.failed | |
| ? ` · ${p.human_videos.failed} failed` | |
| : ""} | |
| {p.usage.media_usd | |
| ? ` · ${usd(p.usage.media_usd)}` | |
| : ""} | |
| </dd> | |
| </div> | |
| )} | |
| </dl> | |
| {p?.subtask_views === "per-camera" && !!p.completed && ( | |
| <SubtaskViews jobId={job.id} /> | |
| )} | |
| {p?.subtask_views === "per-hand" && !!p.completed && ( | |
| <SubtaskViews jobId={job.id} actors /> | |
| )} | |
| {!!p?.human_videos?.completed && ( | |
| <HumanVideos jobId={job.id} /> | |
| )} | |
| {p && | |
| (p.failed > 0 || | |
| (p.stopped || 0) > 0 || | |
| (p.pending || 0) > 0) && ( | |
| <p className="job-outcomes"> | |
| {[ | |
| p.failed ? `${p.failed} failed` : "", | |
| p.stopped ? `${p.stopped} stopped` : "", | |
| p.pending ? `${p.pending} not processed` : "", | |
| ] | |
| .filter(Boolean) | |
| .join(" · ")} | |
| </p> | |
| )} | |
| {!!p?.failed_recordings?.length && ( | |
| <details className="job-recording-failures"> | |
| <summary> | |
| Failed recordings ({p.failed_recordings.length}) | |
| </summary> | |
| <ul> | |
| {p.failed_recordings.map((r) => ( | |
| <li key={r.source_episode_index}> | |
| <strong> | |
| {r.recording}/{r.hand} | |
| </strong>{" "} | |
| · source episode {r.source_episode_index} ·{" "} | |
| {r.stage.replace(/_/g, " ")} | |
| <p>{r.error.split(": {")[0]}</p> | |
| </li> | |
| ))} | |
| </ul> | |
| {job.output && ( | |
| <a | |
| href={`https://huggingface.co/datasets/${job.output}/blob/main/annotation_studio/failures.json`} | |
| target="_blank" | |
| rel="noreferrer" | |
| > | |
| Full failure report <ArrowUpRight size={12} /> | |
| </a> | |
| )} | |
| </details> | |
| )} | |
| {incompleteCost && ( | |
| <p className="job-cost-note"> | |
| {p.cost!.uncertain_inference_usd > 0.000001 | |
| ? `Unconfirmed inference: up to ${usd(p.cost!.uncertain_inference_usd)} extra.` | |
| : "Some usage has not been reported yet."} | |
| </p> | |
| )} | |
| <div className="job-detail-actions"> | |
| {hands && annotations && ( | |
| <a href={annotations} target="_blank" rel="noreferrer"> | |
| Language annotations <ArrowUpRight size={12} /> | |
| </a> | |
| )} | |
| <a href={logs} target="_blank" rel="noreferrer"> | |
| Logs <ArrowUpRight size={12} /> | |
| </a> | |
| {results && ( | |
| <a href={results} target="_blank" rel="noreferrer"> | |
| Output files <ArrowUpRight size={12} /> | |
| </a> | |
| )} | |
| {p?.cost && ( | |
| <a | |
| href={p.cost.billing_url} | |
| target="_blank" | |
| rel="noreferrer" | |
| > | |
| Billing <ArrowUpRight size={12} /> | |
| </a> | |
| )} | |
| {!ended && ( | |
| <button | |
| className="text-danger" | |
| onClick={() => onCancel(job.id)} | |
| > | |
| Cancel job | |
| </button> | |
| )} | |
| </div> | |
| <p className="job-billing-note"> | |
| Usage estimates; final billing may differ. | |
| </p> | |
| </div> | |
| </details> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |