Simplify annotation setup and add compact expandable job rows
Browse files- assets/studio-cover.png +2 -2
- assets/studio-cover.svg +1 -1
- deploy.py +1 -1
- frontend/src/JobsView.tsx +315 -0
- frontend/src/jobs.css +314 -0
- frontend/src/main.tsx +20 -413
assets/studio-cover.png
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
assets/studio-cover.svg
CHANGED
|
|
|
|
deploy.py
CHANGED
|
@@ -57,7 +57,7 @@ def main():
|
|
| 57 |
"frontend/dist/**",
|
| 58 |
],
|
| 59 |
delete_patterns=["app.py"],
|
| 60 |
-
commit_message="
|
| 61 |
)
|
| 62 |
print(f"https://huggingface.co/spaces/{repo}\nRevision: {result.oid}")
|
| 63 |
|
|
|
|
| 57 |
"frontend/dist/**",
|
| 58 |
],
|
| 59 |
delete_patterns=["app.py"],
|
| 60 |
+
commit_message="Simplify annotation setup and add compact expandable job rows",
|
| 61 |
)
|
| 62 |
print(f"https://huggingface.co/spaces/{repo}\nRevision: {result.oid}")
|
| 63 |
|
frontend/src/JobsView.tsx
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ReactNode } from "react";
|
| 2 |
+
import {
|
| 3 |
+
ArrowUpRight,
|
| 4 |
+
ChevronDown,
|
| 5 |
+
LoaderCircle,
|
| 6 |
+
Plus,
|
| 7 |
+
RefreshCw,
|
| 8 |
+
} from "lucide-react";
|
| 9 |
+
import "./jobs.css";
|
| 10 |
+
|
| 11 |
+
export type Job = {
|
| 12 |
+
id: string;
|
| 13 |
+
name?: string;
|
| 14 |
+
stage: string;
|
| 15 |
+
created_at: string;
|
| 16 |
+
owner: string;
|
| 17 |
+
output?: string;
|
| 18 |
+
};
|
| 19 |
+
export type Progress = {
|
| 20 |
+
model?: string;
|
| 21 |
+
status: string;
|
| 22 |
+
expected: number;
|
| 23 |
+
completed: number;
|
| 24 |
+
failed: number;
|
| 25 |
+
stopped?: number;
|
| 26 |
+
pending?: number;
|
| 27 |
+
usage: {
|
| 28 |
+
measured_inference_usd?: number;
|
| 29 |
+
successful_calls?: number;
|
| 30 |
+
input_tokens?: number;
|
| 31 |
+
output_tokens?: number;
|
| 32 |
+
rate_limited_requests?: number;
|
| 33 |
+
};
|
| 34 |
+
cost?: {
|
| 35 |
+
inference_usd: number | null;
|
| 36 |
+
cpu_usd: number | null;
|
| 37 |
+
total_usd: number | null;
|
| 38 |
+
uncertain_inference_usd: number;
|
| 39 |
+
running_seconds: number | null;
|
| 40 |
+
basis: string;
|
| 41 |
+
billing_url: string;
|
| 42 |
+
};
|
| 43 |
+
annotation_url?: string | null;
|
| 44 |
+
output_mode?: string;
|
| 45 |
+
};
|
| 46 |
+
const terminal = new Set(["COMPLETED", "ERROR", "CANCELED", "DELETED"]);
|
| 47 |
+
const usd = (value: number | null | undefined) =>
|
| 48 |
+
value == null
|
| 49 |
+
? "—"
|
| 50 |
+
: new Intl.NumberFormat("en-US", {
|
| 51 |
+
style: "currency",
|
| 52 |
+
currency: "USD",
|
| 53 |
+
minimumFractionDigits: value > 0 && value < 1 ? 4 : 2,
|
| 54 |
+
maximumFractionDigits: 4,
|
| 55 |
+
}).format(value);
|
| 56 |
+
|
| 57 |
+
function status(job: Job, progress?: Progress) {
|
| 58 |
+
if (job.stage === "ERROR") return "Failed";
|
| 59 |
+
if (job.stage === "COMPLETED")
|
| 60 |
+
return progress?.status === "partial" ? "Partial" : "Completed";
|
| 61 |
+
if (job.stage === "CANCELED") return "Canceled";
|
| 62 |
+
if (["PENDING", "QUEUED"].includes(job.stage)) return "Queued";
|
| 63 |
+
return job.stage[0] + job.stage.slice(1).toLowerCase();
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
export default function JobsView({
|
| 67 |
+
jobs,
|
| 68 |
+
progress,
|
| 69 |
+
models,
|
| 70 |
+
signedIn,
|
| 71 |
+
loading,
|
| 72 |
+
signIn,
|
| 73 |
+
onRefresh,
|
| 74 |
+
onNew,
|
| 75 |
+
onCancel,
|
| 76 |
+
}: {
|
| 77 |
+
jobs: Job[];
|
| 78 |
+
progress: Record<string, Progress>;
|
| 79 |
+
models: { route: string; name: string; provider_name: string }[];
|
| 80 |
+
signedIn: boolean;
|
| 81 |
+
loading: boolean;
|
| 82 |
+
signIn: ReactNode;
|
| 83 |
+
onRefresh: () => void;
|
| 84 |
+
onNew: () => void;
|
| 85 |
+
onCancel: (id: string) => void;
|
| 86 |
+
}) {
|
| 87 |
+
return (
|
| 88 |
+
<div className="jobs-page">
|
| 89 |
+
<div className="page-heading">
|
| 90 |
+
<h1>Jobs</h1>
|
| 91 |
+
<div className="heading-actions">
|
| 92 |
+
<button
|
| 93 |
+
className="icon-button"
|
| 94 |
+
onClick={onRefresh}
|
| 95 |
+
disabled={!signedIn || loading}
|
| 96 |
+
aria-label="Refresh jobs"
|
| 97 |
+
>
|
| 98 |
+
<RefreshCw size={17} className={loading ? "spin" : ""} />
|
| 99 |
+
</button>
|
| 100 |
+
<button className="button primary" onClick={onNew}>
|
| 101 |
+
<Plus size={15} /> New job
|
| 102 |
+
</button>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
{!signedIn ? (
|
| 106 |
+
<div className="jobs-empty">
|
| 107 |
+
<p>Sign in to see your jobs.</p>
|
| 108 |
+
{signIn}
|
| 109 |
+
</div>
|
| 110 |
+
) : jobs.length === 0 ? (
|
| 111 |
+
<div className="jobs-empty">
|
| 112 |
+
{loading ? (
|
| 113 |
+
<>
|
| 114 |
+
<LoaderCircle size={20} className="spin" />
|
| 115 |
+
<p>Loading jobs…</p>
|
| 116 |
+
</>
|
| 117 |
+
) : (
|
| 118 |
+
<>
|
| 119 |
+
<p>No jobs yet.</p>
|
| 120 |
+
<button className="button secondary" onClick={onNew}>
|
| 121 |
+
New annotation
|
| 122 |
+
</button>
|
| 123 |
+
</>
|
| 124 |
+
)}
|
| 125 |
+
</div>
|
| 126 |
+
) : (
|
| 127 |
+
<div className="compact-jobs">
|
| 128 |
+
<div className="jobs-columns" aria-hidden="true">
|
| 129 |
+
<span>Job</span>
|
| 130 |
+
<span>Status</span>
|
| 131 |
+
<span>Episodes</span>
|
| 132 |
+
<span>Usage cost</span>
|
| 133 |
+
<span />
|
| 134 |
+
</div>
|
| 135 |
+
{jobs.map((job) => {
|
| 136 |
+
const p = progress[job.id];
|
| 137 |
+
const ended = terminal.has(job.stage);
|
| 138 |
+
const state = status(job, p);
|
| 139 |
+
const profile = models.find((m) => m.route === p?.model);
|
| 140 |
+
const model =
|
| 141 |
+
profile?.name || p?.model?.split(":")[0].split("/").pop();
|
| 142 |
+
const provider = profile?.provider_name || p?.model?.split(":")[1];
|
| 143 |
+
const name =
|
| 144 |
+
job.output?.split("/")[1] || job.name || "Annotation job";
|
| 145 |
+
const logs = `https://huggingface.co/jobs/${job.owner}/${job.id}`;
|
| 146 |
+
const results =
|
| 147 |
+
job.output && p?.completed
|
| 148 |
+
? `https://huggingface.co/datasets/${job.output}`
|
| 149 |
+
: null;
|
| 150 |
+
const annotations = ended ? p?.annotation_url : null;
|
| 151 |
+
const action = annotations
|
| 152 |
+
? "Show annotations"
|
| 153 |
+
: results
|
| 154 |
+
? "Results"
|
| 155 |
+
: "Logs";
|
| 156 |
+
const knownCost = p?.cost?.total_usd ?? p?.cost?.cpu_usd;
|
| 157 |
+
const incompleteCost =
|
| 158 |
+
p?.cost &&
|
| 159 |
+
(p.cost.total_usd == null ||
|
| 160 |
+
p.cost.uncertain_inference_usd > 0.000001);
|
| 161 |
+
return (
|
| 162 |
+
<details className="compact-job" key={job.id}>
|
| 163 |
+
<summary className="compact-job-row">
|
| 164 |
+
<span className="job-identity">
|
| 165 |
+
<strong title={job.output || name}>{name}</strong>
|
| 166 |
+
<span>
|
| 167 |
+
<time dateTime={job.created_at}>
|
| 168 |
+
{new Date(job.created_at).toLocaleString(undefined, {
|
| 169 |
+
month: "short",
|
| 170 |
+
day: "numeric",
|
| 171 |
+
hour: "2-digit",
|
| 172 |
+
minute: "2-digit",
|
| 173 |
+
})}
|
| 174 |
+
</time>
|
| 175 |
+
{model && (
|
| 176 |
+
<>
|
| 177 |
+
<span className="job-meta-separator">·</span>
|
| 178 |
+
<span className="job-model">{model}</span>
|
| 179 |
+
</>
|
| 180 |
+
)}
|
| 181 |
+
</span>
|
| 182 |
+
</span>
|
| 183 |
+
<span className={`compact-status ${state.toLowerCase()}`}>
|
| 184 |
+
<i />
|
| 185 |
+
{state}
|
| 186 |
+
</span>
|
| 187 |
+
<span className="compact-progress">
|
| 188 |
+
<span>
|
| 189 |
+
{p?.expected
|
| 190 |
+
? `${p.completed.toLocaleString()} / ${p.expected.toLocaleString()}`
|
| 191 |
+
: "—"}
|
| 192 |
+
<span className="mobile-label"> episodes</span>
|
| 193 |
+
</span>
|
| 194 |
+
{!ended && !!p?.expected && (
|
| 195 |
+
<progress
|
| 196 |
+
max={p.expected}
|
| 197 |
+
value={p.completed}
|
| 198 |
+
aria-label={`${name} episodes completed`}
|
| 199 |
+
/>
|
| 200 |
+
)}
|
| 201 |
+
</span>
|
| 202 |
+
<span
|
| 203 |
+
className="compact-cost"
|
| 204 |
+
title={
|
| 205 |
+
incompleteCost
|
| 206 |
+
? "Some usage is unconfirmed. Expand for details."
|
| 207 |
+
: "Usage-derived cost"
|
| 208 |
+
}
|
| 209 |
+
>
|
| 210 |
+
<span className="mobile-label">Cost </span>
|
| 211 |
+
{usd(knownCost)}
|
| 212 |
+
{incompleteCost && knownCost != null && <sup>*</sup>}
|
| 213 |
+
</span>
|
| 214 |
+
<span className="compact-job-action">
|
| 215 |
+
<a
|
| 216 |
+
href={annotations || results || logs}
|
| 217 |
+
target="_blank"
|
| 218 |
+
rel="noreferrer"
|
| 219 |
+
onClick={(e) => e.stopPropagation()}
|
| 220 |
+
>
|
| 221 |
+
{action}
|
| 222 |
+
<ArrowUpRight size={13} />
|
| 223 |
+
</a>
|
| 224 |
+
<ChevronDown
|
| 225 |
+
size={15}
|
| 226 |
+
className="job-expand"
|
| 227 |
+
aria-hidden="true"
|
| 228 |
+
/>
|
| 229 |
+
</span>
|
| 230 |
+
</summary>
|
| 231 |
+
<div className="compact-job-details">
|
| 232 |
+
<dl>
|
| 233 |
+
<div>
|
| 234 |
+
<dt>Model</dt>
|
| 235 |
+
<dd>
|
| 236 |
+
{model
|
| 237 |
+
? `${model}${provider ? ` · ${provider}` : ""}`
|
| 238 |
+
: "—"}
|
| 239 |
+
</dd>
|
| 240 |
+
</div>
|
| 241 |
+
<div>
|
| 242 |
+
<dt>Inference</dt>
|
| 243 |
+
<dd>{usd(p?.cost?.inference_usd)}</dd>
|
| 244 |
+
</div>
|
| 245 |
+
<div>
|
| 246 |
+
<dt>Compute</dt>
|
| 247 |
+
<dd>{usd(p?.cost?.cpu_usd)}</dd>
|
| 248 |
+
</div>
|
| 249 |
+
<div>
|
| 250 |
+
<dt>Model calls</dt>
|
| 251 |
+
<dd>
|
| 252 |
+
{p?.usage.successful_calls?.toLocaleString() ?? "—"}
|
| 253 |
+
</dd>
|
| 254 |
+
</div>
|
| 255 |
+
</dl>
|
| 256 |
+
{p &&
|
| 257 |
+
(p.failed > 0 ||
|
| 258 |
+
(p.stopped || 0) > 0 ||
|
| 259 |
+
(p.pending || 0) > 0) && (
|
| 260 |
+
<p className="job-outcomes">
|
| 261 |
+
{[
|
| 262 |
+
p.failed ? `${p.failed} failed` : "",
|
| 263 |
+
p.stopped ? `${p.stopped} stopped` : "",
|
| 264 |
+
p.pending ? `${p.pending} not processed` : "",
|
| 265 |
+
]
|
| 266 |
+
.filter(Boolean)
|
| 267 |
+
.join(" · ")}
|
| 268 |
+
</p>
|
| 269 |
+
)}
|
| 270 |
+
{incompleteCost && (
|
| 271 |
+
<p className="job-cost-note">
|
| 272 |
+
{p.cost!.uncertain_inference_usd > 0.000001
|
| 273 |
+
? `Unconfirmed inference: up to ${usd(p.cost!.uncertain_inference_usd)} extra.`
|
| 274 |
+
: "Some usage has not been reported yet."}
|
| 275 |
+
</p>
|
| 276 |
+
)}
|
| 277 |
+
<div className="job-detail-actions">
|
| 278 |
+
<a href={logs} target="_blank" rel="noreferrer">
|
| 279 |
+
Logs <ArrowUpRight size={12} />
|
| 280 |
+
</a>
|
| 281 |
+
{results && (
|
| 282 |
+
<a href={results} target="_blank" rel="noreferrer">
|
| 283 |
+
Output files <ArrowUpRight size={12} />
|
| 284 |
+
</a>
|
| 285 |
+
)}
|
| 286 |
+
{p?.cost && (
|
| 287 |
+
<a
|
| 288 |
+
href={p.cost.billing_url}
|
| 289 |
+
target="_blank"
|
| 290 |
+
rel="noreferrer"
|
| 291 |
+
>
|
| 292 |
+
Billing <ArrowUpRight size={12} />
|
| 293 |
+
</a>
|
| 294 |
+
)}
|
| 295 |
+
{!ended && (
|
| 296 |
+
<button
|
| 297 |
+
className="text-danger"
|
| 298 |
+
onClick={() => onCancel(job.id)}
|
| 299 |
+
>
|
| 300 |
+
Cancel job
|
| 301 |
+
</button>
|
| 302 |
+
)}
|
| 303 |
+
</div>
|
| 304 |
+
<p className="job-billing-note">
|
| 305 |
+
Usage estimates; final billing may differ.
|
| 306 |
+
</p>
|
| 307 |
+
</div>
|
| 308 |
+
</details>
|
| 309 |
+
);
|
| 310 |
+
})}
|
| 311 |
+
</div>
|
| 312 |
+
)}
|
| 313 |
+
</div>
|
| 314 |
+
);
|
| 315 |
+
}
|
frontend/src/jobs.css
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.jobs-page {
|
| 2 |
+
max-width: 1100px;
|
| 3 |
+
margin: 0 auto 56px;
|
| 4 |
+
}
|
| 5 |
+
.jobs-page h1 {
|
| 6 |
+
font-size: 27px;
|
| 7 |
+
font-weight: 600;
|
| 8 |
+
}
|
| 9 |
+
.jobs-page .page-heading {
|
| 10 |
+
margin-bottom: 30px;
|
| 11 |
+
}
|
| 12 |
+
.jobs-empty {
|
| 13 |
+
display: flex;
|
| 14 |
+
flex-direction: column;
|
| 15 |
+
align-items: center;
|
| 16 |
+
gap: 20px;
|
| 17 |
+
padding: 100px 20px;
|
| 18 |
+
color: var(--fg-muted);
|
| 19 |
+
font-size: 14px;
|
| 20 |
+
}
|
| 21 |
+
.jobs-columns,
|
| 22 |
+
.compact-job-row {
|
| 23 |
+
display: grid;
|
| 24 |
+
grid-template-columns: minmax(180px, 1fr) 92px 85px 90px 155px;
|
| 25 |
+
gap: 20px;
|
| 26 |
+
align-items: center;
|
| 27 |
+
}
|
| 28 |
+
.jobs-columns {
|
| 29 |
+
padding: 0 21px 12px;
|
| 30 |
+
color: var(--fg-muted);
|
| 31 |
+
font-size: 11px;
|
| 32 |
+
}
|
| 33 |
+
.compact-jobs {
|
| 34 |
+
min-width: 0;
|
| 35 |
+
}
|
| 36 |
+
.compact-job {
|
| 37 |
+
background: var(--surface-0);
|
| 38 |
+
border: 1px solid var(--line);
|
| 39 |
+
border-bottom: 0;
|
| 40 |
+
}
|
| 41 |
+
.compact-job:nth-child(2) {
|
| 42 |
+
border-radius: 10px 10px 0 0;
|
| 43 |
+
}
|
| 44 |
+
.compact-job:last-child {
|
| 45 |
+
border-bottom: 1px solid var(--line);
|
| 46 |
+
border-radius: 0 0 10px 10px;
|
| 47 |
+
}
|
| 48 |
+
.compact-job:nth-child(2):last-child {
|
| 49 |
+
border-radius: 10px;
|
| 50 |
+
}
|
| 51 |
+
.compact-job-row {
|
| 52 |
+
list-style: none;
|
| 53 |
+
cursor: pointer;
|
| 54 |
+
padding: 21px;
|
| 55 |
+
font-size: 12px;
|
| 56 |
+
}
|
| 57 |
+
.compact-job-row::-webkit-details-marker {
|
| 58 |
+
display: none;
|
| 59 |
+
}
|
| 60 |
+
.compact-job-row:hover {
|
| 61 |
+
background: var(--surface-1);
|
| 62 |
+
}
|
| 63 |
+
.compact-job-row:focus-visible {
|
| 64 |
+
outline: 2px solid #66aafc;
|
| 65 |
+
outline-offset: -2px;
|
| 66 |
+
border-radius: 7px;
|
| 67 |
+
}
|
| 68 |
+
.job-identity {
|
| 69 |
+
min-width: 0;
|
| 70 |
+
}
|
| 71 |
+
.job-identity > strong {
|
| 72 |
+
display: block;
|
| 73 |
+
font-size: 13px;
|
| 74 |
+
font-weight: 550;
|
| 75 |
+
overflow: hidden;
|
| 76 |
+
white-space: nowrap;
|
| 77 |
+
text-overflow: ellipsis;
|
| 78 |
+
line-height: 1.6;
|
| 79 |
+
}
|
| 80 |
+
.job-identity > span {
|
| 81 |
+
display: flex;
|
| 82 |
+
align-items: center;
|
| 83 |
+
gap: 7px;
|
| 84 |
+
min-width: 0;
|
| 85 |
+
color: var(--fg-muted);
|
| 86 |
+
font-size: 11px;
|
| 87 |
+
margin-top: 5px;
|
| 88 |
+
}
|
| 89 |
+
.job-identity time {
|
| 90 |
+
flex-shrink: 0;
|
| 91 |
+
}
|
| 92 |
+
.job-model {
|
| 93 |
+
overflow: hidden;
|
| 94 |
+
text-overflow: ellipsis;
|
| 95 |
+
white-space: nowrap;
|
| 96 |
+
}
|
| 97 |
+
.compact-status {
|
| 98 |
+
display: inline-flex;
|
| 99 |
+
align-items: center;
|
| 100 |
+
gap: 6px;
|
| 101 |
+
font-size: 11px;
|
| 102 |
+
color: var(--fg-muted);
|
| 103 |
+
}
|
| 104 |
+
.compact-status i {
|
| 105 |
+
width: 5px;
|
| 106 |
+
height: 5px;
|
| 107 |
+
border-radius: 50%;
|
| 108 |
+
background: currentColor;
|
| 109 |
+
}
|
| 110 |
+
.compact-status.completed {
|
| 111 |
+
color: var(--green);
|
| 112 |
+
}
|
| 113 |
+
.compact-status.running {
|
| 114 |
+
color: #5176cb;
|
| 115 |
+
}
|
| 116 |
+
.compact-status.partial {
|
| 117 |
+
color: #a97c22;
|
| 118 |
+
}
|
| 119 |
+
.compact-status.failed {
|
| 120 |
+
color: #c35757;
|
| 121 |
+
}
|
| 122 |
+
.dark .compact-status.failed {
|
| 123 |
+
color: #ef9797;
|
| 124 |
+
}
|
| 125 |
+
.dark .compact-status.running {
|
| 126 |
+
color: #99b7f5;
|
| 127 |
+
}
|
| 128 |
+
.dark .compact-status.partial {
|
| 129 |
+
color: #e7bf70;
|
| 130 |
+
}
|
| 131 |
+
.compact-progress {
|
| 132 |
+
font-variant-numeric: tabular-nums;
|
| 133 |
+
line-height: 1.5;
|
| 134 |
+
}
|
| 135 |
+
.compact-progress progress {
|
| 136 |
+
display: block;
|
| 137 |
+
width: 100%;
|
| 138 |
+
height: 3px;
|
| 139 |
+
margin-top: 7px;
|
| 140 |
+
border: 0;
|
| 141 |
+
border-radius: 5px;
|
| 142 |
+
overflow: hidden;
|
| 143 |
+
background: var(--surface-2);
|
| 144 |
+
accent-color: var(--fg-muted);
|
| 145 |
+
}
|
| 146 |
+
.compact-progress progress::-webkit-progress-bar {
|
| 147 |
+
background: var(--surface-2);
|
| 148 |
+
}
|
| 149 |
+
.compact-progress progress::-webkit-progress-value {
|
| 150 |
+
background: var(--fg-muted);
|
| 151 |
+
}
|
| 152 |
+
.compact-cost {
|
| 153 |
+
font-variant-numeric: tabular-nums;
|
| 154 |
+
}
|
| 155 |
+
.compact-cost sup {
|
| 156 |
+
color: var(--fg-muted);
|
| 157 |
+
padding-left: 2px;
|
| 158 |
+
}
|
| 159 |
+
.compact-job-action {
|
| 160 |
+
display: flex;
|
| 161 |
+
align-items: center;
|
| 162 |
+
justify-content: flex-end;
|
| 163 |
+
gap: 18px;
|
| 164 |
+
}
|
| 165 |
+
.compact-job-action a {
|
| 166 |
+
display: inline-flex;
|
| 167 |
+
align-items: center;
|
| 168 |
+
gap: 5px;
|
| 169 |
+
font-size: 12px;
|
| 170 |
+
font-weight: 500;
|
| 171 |
+
white-space: nowrap;
|
| 172 |
+
padding: 8px 0;
|
| 173 |
+
}
|
| 174 |
+
.compact-job-action a:hover,
|
| 175 |
+
.job-detail-actions a:hover {
|
| 176 |
+
text-decoration: underline;
|
| 177 |
+
text-underline-offset: 3px;
|
| 178 |
+
}
|
| 179 |
+
.job-expand {
|
| 180 |
+
color: var(--fg-muted);
|
| 181 |
+
}
|
| 182 |
+
.compact-job[open] .job-expand {
|
| 183 |
+
transform: rotate(180deg);
|
| 184 |
+
}
|
| 185 |
+
.compact-job-details {
|
| 186 |
+
padding: 0 21px 21px;
|
| 187 |
+
}
|
| 188 |
+
.compact-job-details dl {
|
| 189 |
+
margin: 0;
|
| 190 |
+
padding-top: 20px;
|
| 191 |
+
border-top: 1px solid var(--line);
|
| 192 |
+
display: grid;
|
| 193 |
+
grid-template-columns: 2fr 1fr 1fr 1fr;
|
| 194 |
+
gap: 20px;
|
| 195 |
+
}
|
| 196 |
+
.compact-job-details dt {
|
| 197 |
+
font-size: 11px;
|
| 198 |
+
color: var(--fg-muted);
|
| 199 |
+
margin-bottom: 6px;
|
| 200 |
+
}
|
| 201 |
+
.compact-job-details dd {
|
| 202 |
+
font-size: 12px;
|
| 203 |
+
margin: 0;
|
| 204 |
+
line-height: 1.6;
|
| 205 |
+
overflow-wrap: anywhere;
|
| 206 |
+
}
|
| 207 |
+
.job-outcomes,
|
| 208 |
+
.job-cost-note {
|
| 209 |
+
color: var(--fg-muted);
|
| 210 |
+
font-size: 12px;
|
| 211 |
+
margin-top: 17px;
|
| 212 |
+
}
|
| 213 |
+
.job-cost-note {
|
| 214 |
+
color: var(--fg-soft);
|
| 215 |
+
}
|
| 216 |
+
.job-detail-actions {
|
| 217 |
+
display: flex;
|
| 218 |
+
align-items: center;
|
| 219 |
+
flex-wrap: wrap;
|
| 220 |
+
gap: 20px;
|
| 221 |
+
margin-top: 22px;
|
| 222 |
+
}
|
| 223 |
+
.job-detail-actions a,
|
| 224 |
+
.job-detail-actions button {
|
| 225 |
+
display: inline-flex;
|
| 226 |
+
align-items: center;
|
| 227 |
+
gap: 4px;
|
| 228 |
+
font-size: 12px;
|
| 229 |
+
padding: 0;
|
| 230 |
+
}
|
| 231 |
+
.job-detail-actions .text-danger {
|
| 232 |
+
margin-left: auto;
|
| 233 |
+
}
|
| 234 |
+
.job-billing-note {
|
| 235 |
+
font-size: 11px;
|
| 236 |
+
color: var(--fg-faint);
|
| 237 |
+
margin-top: 14px;
|
| 238 |
+
}
|
| 239 |
+
.mobile-label {
|
| 240 |
+
display: none;
|
| 241 |
+
}
|
| 242 |
+
@media (max-width: 1200px) {
|
| 243 |
+
.jobs-columns,
|
| 244 |
+
.compact-job-row {
|
| 245 |
+
grid-template-columns: minmax(160px, 1fr) 80px 65px 75px 142px;
|
| 246 |
+
gap: 12px;
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
@media (max-width: 800px) {
|
| 250 |
+
.jobs-columns {
|
| 251 |
+
display: none;
|
| 252 |
+
}
|
| 253 |
+
.compact-job-row {
|
| 254 |
+
grid-template-columns: minmax(0, 1fr) auto;
|
| 255 |
+
gap: 15px 12px;
|
| 256 |
+
}
|
| 257 |
+
.job-identity {
|
| 258 |
+
grid-column: 1;
|
| 259 |
+
grid-row: 1;
|
| 260 |
+
}
|
| 261 |
+
.compact-status {
|
| 262 |
+
grid-column: 2;
|
| 263 |
+
grid-row: 1;
|
| 264 |
+
justify-self: end;
|
| 265 |
+
}
|
| 266 |
+
.compact-progress {
|
| 267 |
+
grid-column: 1;
|
| 268 |
+
grid-row: 2;
|
| 269 |
+
font-size: 11px;
|
| 270 |
+
color: var(--fg-muted);
|
| 271 |
+
}
|
| 272 |
+
.compact-cost {
|
| 273 |
+
grid-column: 1;
|
| 274 |
+
grid-row: 3;
|
| 275 |
+
font-size: 11px;
|
| 276 |
+
}
|
| 277 |
+
.compact-job-action {
|
| 278 |
+
grid-column: 2;
|
| 279 |
+
grid-row: 2 / 4;
|
| 280 |
+
align-self: end;
|
| 281 |
+
}
|
| 282 |
+
.compact-progress progress {
|
| 283 |
+
width: 110px;
|
| 284 |
+
}
|
| 285 |
+
.mobile-label {
|
| 286 |
+
display: inline;
|
| 287 |
+
}
|
| 288 |
+
.compact-job-details dl {
|
| 289 |
+
grid-template-columns: 1fr 1fr;
|
| 290 |
+
}
|
| 291 |
+
.compact-job-row,
|
| 292 |
+
.compact-job-details {
|
| 293 |
+
padding-left: 17px;
|
| 294 |
+
padding-right: 17px;
|
| 295 |
+
}
|
| 296 |
+
.job-identity > span {
|
| 297 |
+
flex-wrap: wrap;
|
| 298 |
+
gap: 2px 7px;
|
| 299 |
+
}
|
| 300 |
+
.job-model {
|
| 301 |
+
flex-basis: 100%;
|
| 302 |
+
}
|
| 303 |
+
.job-meta-separator {
|
| 304 |
+
display: none;
|
| 305 |
+
}
|
| 306 |
+
.compact-job-action {
|
| 307 |
+
gap: 12px;
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.jobs-page .page-heading .heading-actions {
|
| 312 |
+
flex-direction: row;
|
| 313 |
+
align-items: center;
|
| 314 |
+
}
|
frontend/src/main.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
import { useEffect, useRef, useState } from "react";
|
| 2 |
import { createRoot } from "react-dom/client";
|
| 3 |
import {
|
| 4 |
-
ArrowDownToLine,
|
| 5 |
ArrowLeft,
|
| 6 |
ArrowRight,
|
| 7 |
ArrowUpRight,
|
|
@@ -12,13 +11,10 @@ import {
|
|
| 12 |
Check,
|
| 13 |
CheckCheck,
|
| 14 |
ChevronDown,
|
| 15 |
-
ChevronRight,
|
| 16 |
Coins,
|
| 17 |
Database,
|
| 18 |
FileJson2,
|
| 19 |
FolderOpen,
|
| 20 |
-
Gauge,
|
| 21 |
-
Github,
|
| 22 |
Globe,
|
| 23 |
HelpCircle,
|
| 24 |
Layers3,
|
|
@@ -34,9 +30,7 @@ import {
|
|
| 34 |
Rocket,
|
| 35 |
Search,
|
| 36 |
Settings2,
|
| 37 |
-
ShieldCheck,
|
| 38 |
Sun,
|
| 39 |
-
Terminal,
|
| 40 |
Video,
|
| 41 |
WandSparkles,
|
| 42 |
X,
|
|
@@ -44,6 +38,7 @@ import {
|
|
| 44 |
import "./style.css";
|
| 45 |
import "./wizard.css";
|
| 46 |
import AnnotationExamples from "./AnnotationExamples";
|
|
|
|
| 47 |
|
| 48 |
type Feature =
|
| 49 |
| "Subtasks"
|
|
@@ -127,41 +122,6 @@ type Session = {
|
|
| 127 |
csrf: string;
|
| 128 |
auth_available: boolean;
|
| 129 |
};
|
| 130 |
-
type Job = {
|
| 131 |
-
id: string;
|
| 132 |
-
name?: string;
|
| 133 |
-
stage: string;
|
| 134 |
-
created_at: string;
|
| 135 |
-
owner: string;
|
| 136 |
-
output?: string;
|
| 137 |
-
};
|
| 138 |
-
type Progress = {
|
| 139 |
-
model?: string;
|
| 140 |
-
status: string;
|
| 141 |
-
expected: number;
|
| 142 |
-
completed: number;
|
| 143 |
-
failed: number;
|
| 144 |
-
stopped?: number;
|
| 145 |
-
pending?: number;
|
| 146 |
-
usage: {
|
| 147 |
-
measured_inference_usd?: number;
|
| 148 |
-
successful_calls?: number;
|
| 149 |
-
input_tokens?: number;
|
| 150 |
-
output_tokens?: number;
|
| 151 |
-
rate_limited_requests?: number;
|
| 152 |
-
};
|
| 153 |
-
cost?: {
|
| 154 |
-
inference_usd: number | null;
|
| 155 |
-
cpu_usd: number | null;
|
| 156 |
-
total_usd: number | null;
|
| 157 |
-
uncertain_inference_usd: number;
|
| 158 |
-
running_seconds: number | null;
|
| 159 |
-
basis: string;
|
| 160 |
-
billing_url: string;
|
| 161 |
-
};
|
| 162 |
-
annotation_url?: string | null;
|
| 163 |
-
output_mode?: string;
|
| 164 |
-
};
|
| 165 |
const EXPORT = "Annotation files only",
|
| 166 |
COPY = "Annotated dataset copy";
|
| 167 |
const defaults: Options = {
|
|
@@ -242,7 +202,6 @@ const money = (n: number) =>
|
|
| 242 |
const num = (n: number) => new Intl.NumberFormat("en-US").format(n);
|
| 243 |
const duration = (s: number) =>
|
| 244 |
s >= 3600 ? `${(s / 3600).toFixed(1)} hours` : `${(s / 60).toFixed(1)} min`;
|
| 245 |
-
const terminal = new Set(["COMPLETED", "ERROR", "CANCELED", "DELETED"]);
|
| 246 |
function initial() {
|
| 247 |
try {
|
| 248 |
return {
|
|
@@ -741,16 +700,6 @@ function App() {
|
|
| 741 |
<BookOpen size={18} /> How it works
|
| 742 |
</button>
|
| 743 |
</nav>
|
| 744 |
-
<div className="sidebar-bottom">
|
| 745 |
-
<a
|
| 746 |
-
className="source-link"
|
| 747 |
-
href="https://huggingface.co/spaces/lerobot/annotation-studio/tree/main"
|
| 748 |
-
target="_blank"
|
| 749 |
-
rel="noreferrer"
|
| 750 |
-
>
|
| 751 |
-
<Github size={16} /> Open source <ArrowUpRight size={13} />
|
| 752 |
-
</a>
|
| 753 |
-
</div>
|
| 754 |
</aside>
|
| 755 |
<div className="workspace">
|
| 756 |
<header className="topbar">
|
|
@@ -762,8 +711,6 @@ function App() {
|
|
| 762 |
>
|
| 763 |
<Menu size={20} />
|
| 764 |
</button>
|
| 765 |
-
<span>Workspace</span>
|
| 766 |
-
<ChevronRight size={14} />
|
| 767 |
<strong>
|
| 768 |
{page === "new"
|
| 769 |
? "New annotation"
|
|
@@ -826,7 +773,6 @@ function App() {
|
|
| 826 |
<div className="page-heading">
|
| 827 |
<div>
|
| 828 |
<h1>New annotation</h1>
|
| 829 |
-
<p>A little more context for your robot data.</p>
|
| 830 |
</div>
|
| 831 |
</div>
|
| 832 |
<nav className="wizard-steps" aria-label="Annotation setup">
|
|
@@ -858,7 +804,6 @@ function App() {
|
|
| 858 |
))}
|
| 859 |
</nav>
|
| 860 |
<div className="step-intro">
|
| 861 |
-
<span>STEP {step + 1} OF 4</span>
|
| 862 |
<h2 ref={stepHeading} tabIndex={-1}>
|
| 863 |
{
|
| 864 |
[
|
|
@@ -869,16 +814,6 @@ function App() {
|
|
| 869 |
][step]
|
| 870 |
}
|
| 871 |
</h2>
|
| 872 |
-
<p>
|
| 873 |
-
{
|
| 874 |
-
[
|
| 875 |
-
"Select a dataset, episode range and camera view.",
|
| 876 |
-
"Start with subtasks, or add more context to each episode.",
|
| 877 |
-
"Use the default, or choose another vision model.",
|
| 878 |
-
"Check the estimate and choose where to save your annotations.",
|
| 879 |
-
][step]
|
| 880 |
-
}
|
| 881 |
-
</p>
|
| 882 |
</div>
|
| 883 |
{step === 0 && (
|
| 884 |
<section
|
|
@@ -886,7 +821,7 @@ function App() {
|
|
| 886 |
aria-label="Dataset settings"
|
| 887 |
>
|
| 888 |
<label className="field-label" htmlFor="dataset">
|
| 889 |
-
Dataset ID
|
| 890 |
</label>
|
| 891 |
<div className="dataset-input">
|
| 892 |
<Database size={17} />
|
|
@@ -901,8 +836,7 @@ function App() {
|
|
| 901 |
{metadata && <Check size={18} className="success-text" />}
|
| 902 |
</div>
|
| 903 |
<div className="field-help">
|
| 904 |
-
|
| 905 |
-
gated datasets.
|
| 906 |
</div>
|
| 907 |
<div className="fields-row">
|
| 908 |
<label>
|
|
@@ -1112,9 +1046,6 @@ function App() {
|
|
| 1112 |
|
| 1113 |
{selectedModel && (
|
| 1114 |
<>
|
| 1115 |
-
<p className="model-description">
|
| 1116 |
-
{selectedModel.description}
|
| 1117 |
-
</p>
|
| 1118 |
<div className="model-overview">
|
| 1119 |
<span>{selectedModel.provider_name}</span>
|
| 1120 |
<span>
|
|
@@ -1271,14 +1202,6 @@ function App() {
|
|
| 1271 |
</label>
|
| 1272 |
))}
|
| 1273 |
</div>
|
| 1274 |
-
{options.mode === COPY && (
|
| 1275 |
-
<p className="field-help">
|
| 1276 |
-
Only successfully annotated episodes are copied. Videos
|
| 1277 |
-
from all RGB cameras are trimmed to those episodes;
|
| 1278 |
-
skipped and failed episodes are excluded. Storage
|
| 1279 |
-
charges are separate.
|
| 1280 |
-
</p>
|
| 1281 |
-
)}
|
| 1282 |
<fieldset className="visibility-control">
|
| 1283 |
<legend>Repository visibility</legend>
|
| 1284 |
<div className="visibility-options">
|
|
@@ -1310,15 +1233,6 @@ function App() {
|
|
| 1310 |
</label>
|
| 1311 |
))}
|
| 1312 |
</div>
|
| 1313 |
-
{visibility === "public" && (
|
| 1314 |
-
<p className="field-help">
|
| 1315 |
-
{options.mode === COPY
|
| 1316 |
-
? "Copied episodes, including their videos and annotations, will be public."
|
| 1317 |
-
: "Annotations, source references and run details will be public. Source videos are not copied."}
|
| 1318 |
-
{quote?.private &&
|
| 1319 |
-
" Your source dataset is private; this publishes content derived from it."}
|
| 1320 |
-
</p>
|
| 1321 |
-
)}
|
| 1322 |
</fieldset>
|
| 1323 |
<button
|
| 1324 |
className="disclosure"
|
|
@@ -1361,14 +1275,6 @@ function App() {
|
|
| 1361 |
</select>
|
| 1362 |
</label>
|
| 1363 |
</div>
|
| 1364 |
-
<p>
|
| 1365 |
-
The tuned preset is experimental, not finetuned
|
| 1366 |
-
weights. Its small held-out benchmark did not
|
| 1367 |
-
establish a quality improvement. Image and JSON
|
| 1368 |
-
compatibility checks do not establish annotation
|
| 1369 |
-
quality. Provider limits and smaller budgets can
|
| 1370 |
-
reduce parallelism.
|
| 1371 |
-
</p>
|
| 1372 |
</div>
|
| 1373 |
)}
|
| 1374 |
</section>
|
|
@@ -1387,9 +1293,6 @@ function App() {
|
|
| 1387 |
<span>–</span>
|
| 1388 |
{money(quote.costs.high)}
|
| 1389 |
</div>
|
| 1390 |
-
<p className="price-description">
|
| 1391 |
-
Based on your selected video and annotations.
|
| 1392 |
-
</p>
|
| 1393 |
<details className="progressive-details cost-breakdown">
|
| 1394 |
<summary>
|
| 1395 |
Cost breakdown <ChevronDown size={15} />
|
|
@@ -1487,8 +1390,7 @@ function App() {
|
|
| 1487 |
/>
|
| 1488 |
</div>
|
| 1489 |
<p className="microcopy">
|
| 1490 |
-
|
| 1491 |
-
results are kept if the target or timeout is reached.
|
| 1492 |
</p>
|
| 1493 |
{quote && budget < quote.costs.high && (
|
| 1494 |
<div className="budget-hint">
|
|
@@ -1525,27 +1427,11 @@ function App() {
|
|
| 1525 |
<Rocket size={16} /> Review & launch{" "}
|
| 1526 |
<ArrowRight size={16} />
|
| 1527 |
</button>
|
| 1528 |
-
<div className="billed-to">
|
| 1529 |
-
Billed to <strong>{session.user.name}</strong>
|
| 1530 |
-
</div>
|
| 1531 |
</>
|
| 1532 |
) : (
|
| 1533 |
-
<>
|
| 1534 |
-
{signIn}
|
| 1535 |
-
<div className="billed-to">
|
| 1536 |
-
To launch jobs in your own account.
|
| 1537 |
-
</div>
|
| 1538 |
-
</>
|
| 1539 |
)}
|
| 1540 |
</div>
|
| 1541 |
-
<div className="summary-fineprint">
|
| 1542 |
-
<ShieldCheck size={14} />
|
| 1543 |
-
<p>
|
| 1544 |
-
Your dataset is preserved. Run costs are billed to you
|
| 1545 |
-
through Hugging Face. This spending guard is not a
|
| 1546 |
-
billing guarantee.
|
| 1547 |
-
</p>
|
| 1548 |
-
</div>
|
| 1549 |
</section>
|
| 1550 |
</div>
|
| 1551 |
)}
|
|
@@ -1559,9 +1445,7 @@ function App() {
|
|
| 1559 |
<ArrowLeft size={15} /> Back
|
| 1560 |
</button>
|
| 1561 |
) : (
|
| 1562 |
-
<span
|
| 1563 |
-
Your source dataset stays unchanged.
|
| 1564 |
-
</span>
|
| 1565 |
)}
|
| 1566 |
{step < 3 && (
|
| 1567 |
<button
|
|
@@ -1593,271 +1477,17 @@ function App() {
|
|
| 1593 |
</div>
|
| 1594 |
)}
|
| 1595 |
{page === "jobs" && (
|
| 1596 |
-
<
|
| 1597 |
-
|
| 1598 |
-
|
| 1599 |
-
|
| 1600 |
-
|
| 1601 |
-
|
| 1602 |
-
|
| 1603 |
-
|
| 1604 |
-
|
| 1605 |
-
|
| 1606 |
-
|
| 1607 |
-
<button
|
| 1608 |
-
className="button secondary"
|
| 1609 |
-
onClick={loadJobs}
|
| 1610 |
-
disabled={!session?.user || jobsLoading}
|
| 1611 |
-
>
|
| 1612 |
-
<RefreshCw
|
| 1613 |
-
size={15}
|
| 1614 |
-
className={jobsLoading ? "spin" : ""}
|
| 1615 |
-
/>{" "}
|
| 1616 |
-
Refresh
|
| 1617 |
-
</button>
|
| 1618 |
-
<button
|
| 1619 |
-
className="button primary"
|
| 1620 |
-
onClick={() => navigate("new")}
|
| 1621 |
-
>
|
| 1622 |
-
<Plus size={16} /> New job
|
| 1623 |
-
</button>
|
| 1624 |
-
</div>
|
| 1625 |
-
</div>
|
| 1626 |
-
{!session?.user ? (
|
| 1627 |
-
<div className="empty-state card">
|
| 1628 |
-
<div className="empty-icon">
|
| 1629 |
-
<FolderOpen size={30} />
|
| 1630 |
-
</div>
|
| 1631 |
-
<h2>Your runs, in one place.</h2>
|
| 1632 |
-
<p>
|
| 1633 |
-
Sign in to see the annotation jobs launched from your
|
| 1634 |
-
account.
|
| 1635 |
-
</p>
|
| 1636 |
-
{signIn}
|
| 1637 |
-
</div>
|
| 1638 |
-
) : jobs.length === 0 ? (
|
| 1639 |
-
<div className="empty-state card">
|
| 1640 |
-
<div className="empty-icon">
|
| 1641 |
-
<Rocket size={30} />
|
| 1642 |
-
</div>
|
| 1643 |
-
<h2>
|
| 1644 |
-
{jobsLoading
|
| 1645 |
-
? "Loading your jobs…"
|
| 1646 |
-
: "Your first annotation starts here."}
|
| 1647 |
-
</h2>
|
| 1648 |
-
<p>
|
| 1649 |
-
Configure a dataset, review the estimate, and launch your
|
| 1650 |
-
first job.
|
| 1651 |
-
</p>
|
| 1652 |
-
<button
|
| 1653 |
-
className="button primary"
|
| 1654 |
-
onClick={() => navigate("new")}
|
| 1655 |
-
>
|
| 1656 |
-
<Plus size={16} /> Create an annotation job
|
| 1657 |
-
</button>
|
| 1658 |
-
</div>
|
| 1659 |
-
) : (
|
| 1660 |
-
<div className="jobs-list">
|
| 1661 |
-
{jobs.map((j) => (
|
| 1662 |
-
<article className="card job" key={j.id}>
|
| 1663 |
-
<div className="job-head">
|
| 1664 |
-
<span className="section-icon">
|
| 1665 |
-
<Layers3 size={19} />
|
| 1666 |
-
</span>
|
| 1667 |
-
<div>
|
| 1668 |
-
<h2>
|
| 1669 |
-
{j.output?.split("/")[1] ||
|
| 1670 |
-
j.name ||
|
| 1671 |
-
"Annotation job"}
|
| 1672 |
-
</h2>
|
| 1673 |
-
<p>
|
| 1674 |
-
{new Date(j.created_at).toLocaleString()} ·{" "}
|
| 1675 |
-
<code>{j.id.slice(0, 8)}</code>
|
| 1676 |
-
</p>
|
| 1677 |
-
{progress[j.id]?.model && (
|
| 1678 |
-
<p>
|
| 1679 |
-
{modelCatalog?.models.find(
|
| 1680 |
-
(m) => m.route === progress[j.id].model,
|
| 1681 |
-
)?.name ||
|
| 1682 |
-
progress[j.id].model
|
| 1683 |
-
?.split(":")[0]
|
| 1684 |
-
.split("/")
|
| 1685 |
-
.pop()}{" "}
|
| 1686 |
-
·{" "}
|
| 1687 |
-
{modelCatalog?.models.find(
|
| 1688 |
-
(m) => m.route === progress[j.id].model,
|
| 1689 |
-
)?.provider_name ||
|
| 1690 |
-
progress[j.id].model?.split(":")[1]}
|
| 1691 |
-
</p>
|
| 1692 |
-
)}
|
| 1693 |
-
</div>
|
| 1694 |
-
<span
|
| 1695 |
-
className={`job-status ${progress[j.id]?.status === "partial" ? "partial" : j.stage.toLowerCase()}`}
|
| 1696 |
-
>
|
| 1697 |
-
{!terminal.has(j.stage) && (
|
| 1698 |
-
<LoaderCircle size={12} className="spin" />
|
| 1699 |
-
)}
|
| 1700 |
-
{progress[j.id]?.status === "partial"
|
| 1701 |
-
? "Partial"
|
| 1702 |
-
: j.stage.toLowerCase()}
|
| 1703 |
-
</span>
|
| 1704 |
-
</div>
|
| 1705 |
-
{progress[j.id] && (
|
| 1706 |
-
<div className="job-progress">
|
| 1707 |
-
<div>
|
| 1708 |
-
<span>
|
| 1709 |
-
{num(progress[j.id].completed)} /{" "}
|
| 1710 |
-
{num(progress[j.id].expected)} episodes saved
|
| 1711 |
-
</span>
|
| 1712 |
-
<strong>{progress[j.id].status}</strong>
|
| 1713 |
-
</div>
|
| 1714 |
-
<progress
|
| 1715 |
-
max={progress[j.id].expected}
|
| 1716 |
-
value={progress[j.id].completed}
|
| 1717 |
-
/>
|
| 1718 |
-
<small>
|
| 1719 |
-
{progress[j.id].failed} failed ·{" "}
|
| 1720 |
-
{progress[j.id].stopped || 0} stopped ·{" "}
|
| 1721 |
-
{progress[j.id].pending || 0} not processed ·
|
| 1722 |
-
measured inference{" "}
|
| 1723 |
-
{money(
|
| 1724 |
-
progress[j.id].usage.measured_inference_usd || 0,
|
| 1725 |
-
)}
|
| 1726 |
-
</small>
|
| 1727 |
-
</div>
|
| 1728 |
-
)}
|
| 1729 |
-
{progress[j.id]?.cost && (
|
| 1730 |
-
<div className="job-cost">
|
| 1731 |
-
<div className="job-cost-total">
|
| 1732 |
-
<div>
|
| 1733 |
-
<span>Usage cost</span>
|
| 1734 |
-
<strong>
|
| 1735 |
-
{progress[j.id].cost!.total_usd == null
|
| 1736 |
-
? "Pending"
|
| 1737 |
-
: new Intl.NumberFormat("en-US", {
|
| 1738 |
-
style: "currency",
|
| 1739 |
-
currency: "USD",
|
| 1740 |
-
minimumFractionDigits: 4,
|
| 1741 |
-
}).format(progress[j.id].cost!.total_usd!)}
|
| 1742 |
-
</strong>
|
| 1743 |
-
</div>
|
| 1744 |
-
<span className="quiet-badge">
|
| 1745 |
-
{terminal.has(j.stage)
|
| 1746 |
-
? "Run ended"
|
| 1747 |
-
: "Updates every 15 s"}
|
| 1748 |
-
</span>
|
| 1749 |
-
</div>
|
| 1750 |
-
<div className="job-cost-breakdown">
|
| 1751 |
-
<span>
|
| 1752 |
-
Model inference{" "}
|
| 1753 |
-
<b>
|
| 1754 |
-
{progress[j.id].cost!.inference_usd == null
|
| 1755 |
-
? "Pending"
|
| 1756 |
-
: "$" +
|
| 1757 |
-
progress[j.id].cost!.inference_usd!.toFixed(
|
| 1758 |
-
4,
|
| 1759 |
-
)}
|
| 1760 |
-
</b>
|
| 1761 |
-
</span>
|
| 1762 |
-
<span>
|
| 1763 |
-
CPU runtime{" "}
|
| 1764 |
-
<b>
|
| 1765 |
-
{progress[j.id].cost!.cpu_usd == null
|
| 1766 |
-
? "Pending"
|
| 1767 |
-
: "$" +
|
| 1768 |
-
progress[j.id].cost!.cpu_usd!.toFixed(4)}
|
| 1769 |
-
</b>
|
| 1770 |
-
</span>
|
| 1771 |
-
<span>
|
| 1772 |
-
Model calls{" "}
|
| 1773 |
-
<b>
|
| 1774 |
-
{num(
|
| 1775 |
-
progress[j.id].usage.successful_calls || 0,
|
| 1776 |
-
)}
|
| 1777 |
-
</b>
|
| 1778 |
-
</span>
|
| 1779 |
-
</div>
|
| 1780 |
-
{progress[j.id].cost!.uncertain_inference_usd >
|
| 1781 |
-
0.000001 && (
|
| 1782 |
-
<p className="cost-uncertain">
|
| 1783 |
-
Additional uncertain inference charges: up to $
|
| 1784 |
-
{progress[
|
| 1785 |
-
j.id
|
| 1786 |
-
].cost!.uncertain_inference_usd.toFixed(4)}
|
| 1787 |
-
. These are reserved separately.
|
| 1788 |
-
</p>
|
| 1789 |
-
)}
|
| 1790 |
-
<p>
|
| 1791 |
-
{progress[j.id].cost!.basis}{" "}
|
| 1792 |
-
<a
|
| 1793 |
-
href={progress[j.id].cost!.billing_url}
|
| 1794 |
-
target="_blank"
|
| 1795 |
-
rel="noreferrer"
|
| 1796 |
-
>
|
| 1797 |
-
View HF billing <ArrowUpRight size={10} />
|
| 1798 |
-
</a>
|
| 1799 |
-
</p>
|
| 1800 |
-
</div>
|
| 1801 |
-
)}
|
| 1802 |
-
<div className="job-actions">
|
| 1803 |
-
{terminal.has(j.stage) &&
|
| 1804 |
-
progress[j.id]?.annotation_url && (
|
| 1805 |
-
<a
|
| 1806 |
-
className="button primary"
|
| 1807 |
-
href={progress[j.id].annotation_url!}
|
| 1808 |
-
target="_blank"
|
| 1809 |
-
rel="noreferrer"
|
| 1810 |
-
>
|
| 1811 |
-
<Layers3 size={14} /> Show annotations{" "}
|
| 1812 |
-
<ArrowUpRight size={13} />
|
| 1813 |
-
</a>
|
| 1814 |
-
)}
|
| 1815 |
-
<button
|
| 1816 |
-
className="button secondary"
|
| 1817 |
-
onClick={() => getProgress(j)}
|
| 1818 |
-
>
|
| 1819 |
-
<Gauge size={14} /> Check progress
|
| 1820 |
-
</button>
|
| 1821 |
-
<a
|
| 1822 |
-
className="button secondary"
|
| 1823 |
-
href={`https://huggingface.co/jobs/${j.owner}/${j.id}`}
|
| 1824 |
-
target="_blank"
|
| 1825 |
-
rel="noreferrer"
|
| 1826 |
-
>
|
| 1827 |
-
<Terminal size={14} /> Logs <ArrowUpRight size={13} />
|
| 1828 |
-
</a>
|
| 1829 |
-
{j.output && (
|
| 1830 |
-
<a
|
| 1831 |
-
className="button secondary"
|
| 1832 |
-
href={`https://huggingface.co/datasets/${j.output}`}
|
| 1833 |
-
target="_blank"
|
| 1834 |
-
rel="noreferrer"
|
| 1835 |
-
>
|
| 1836 |
-
<ArrowDownToLine size={14} /> Results{" "}
|
| 1837 |
-
<ArrowUpRight size={13} />
|
| 1838 |
-
</a>
|
| 1839 |
-
)}
|
| 1840 |
-
{!terminal.has(j.stage) && (
|
| 1841 |
-
<button
|
| 1842 |
-
className="text-danger"
|
| 1843 |
-
onClick={() => setCancelId(j.id)}
|
| 1844 |
-
>
|
| 1845 |
-
Cancel job
|
| 1846 |
-
</button>
|
| 1847 |
-
)}
|
| 1848 |
-
</div>
|
| 1849 |
-
</article>
|
| 1850 |
-
))}
|
| 1851 |
-
</div>
|
| 1852 |
-
)}
|
| 1853 |
-
<p className="section-footnote">
|
| 1854 |
-
Show annotations opens the visualizer’s Annotations tab for an
|
| 1855 |
-
annotated dataset copy. Sign in there to access private results.
|
| 1856 |
-
Annotation-file exports can be downloaded from Results. Only
|
| 1857 |
-
jobs launched from Annotation Studio appear here. A completed
|
| 1858 |
-
job can contain partial results; check the saved run report.
|
| 1859 |
-
</p>
|
| 1860 |
-
</>
|
| 1861 |
)}
|
| 1862 |
{page === "guide" && (
|
| 1863 |
<>
|
|
@@ -1939,15 +1569,6 @@ function App() {
|
|
| 1939 |
</div>
|
| 1940 |
</>
|
| 1941 |
)}
|
| 1942 |
-
<footer>
|
| 1943 |
-
<span>
|
| 1944 |
-
<Bot size={14} /> LeRobot Annotation Studio
|
| 1945 |
-
</span>
|
| 1946 |
-
<span>
|
| 1947 |
-
Built on Hugging Face <span className="footer-dot">·</span> Open
|
| 1948 |
-
source robotics
|
| 1949 |
-
</span>
|
| 1950 |
-
</footer>
|
| 1951 |
</main>
|
| 1952 |
</div>
|
| 1953 |
<dialog
|
|
@@ -2072,9 +1693,7 @@ function App() {
|
|
| 2072 |
</button>
|
| 2073 |
</div>
|
| 2074 |
<div className="modal-body">
|
| 2075 |
-
<div className="overline">READY WHEN YOU ARE</div>
|
| 2076 |
<h2>Review your annotation job</h2>
|
| 2077 |
-
<p>Confirm the details before starting paid compute.</p>
|
| 2078 |
{error && (
|
| 2079 |
<div className="message error" role="alert">
|
| 2080 |
{error}
|
|
@@ -2151,23 +1770,11 @@ function App() {
|
|
| 2151 |
onChange={(e) => setConsent(e.target.checked)}
|
| 2152 |
/>
|
| 2153 |
<span>
|
| 2154 |
-
I
|
| 2155 |
-
|
| 2156 |
-
{quote?.model_profile.provider_name || "the selected provider"}. I
|
| 2157 |
-
have permission to process this dataset.
|
| 2158 |
-
{visibility === "public" &&
|
| 2159 |
-
(options.mode === COPY
|
| 2160 |
-
? " I also authorize publishing the copied episodes, including their videos and annotations, publicly."
|
| 2161 |
-
: " I also authorize publishing the annotation export, including its source reference and run details, publicly.")}
|
| 2162 |
</span>
|
| 2163 |
</label>
|
| 2164 |
-
<p className="microcopy">
|
| 2165 |
-
The spending guard may stop a run early. Storage charges are
|
| 2166 |
-
separate.{" "}
|
| 2167 |
-
{options.mode === COPY
|
| 2168 |
-
? "Only copied episodes use your dataset storage quota."
|
| 2169 |
-
: ""}
|
| 2170 |
-
</p>
|
| 2171 |
<button
|
| 2172 |
className="button primary full"
|
| 2173 |
disabled={!consent || submitting}
|
|
|
|
| 1 |
import { useEffect, useRef, useState } from "react";
|
| 2 |
import { createRoot } from "react-dom/client";
|
| 3 |
import {
|
|
|
|
| 4 |
ArrowLeft,
|
| 5 |
ArrowRight,
|
| 6 |
ArrowUpRight,
|
|
|
|
| 11 |
Check,
|
| 12 |
CheckCheck,
|
| 13 |
ChevronDown,
|
|
|
|
| 14 |
Coins,
|
| 15 |
Database,
|
| 16 |
FileJson2,
|
| 17 |
FolderOpen,
|
|
|
|
|
|
|
| 18 |
Globe,
|
| 19 |
HelpCircle,
|
| 20 |
Layers3,
|
|
|
|
| 30 |
Rocket,
|
| 31 |
Search,
|
| 32 |
Settings2,
|
|
|
|
| 33 |
Sun,
|
|
|
|
| 34 |
Video,
|
| 35 |
WandSparkles,
|
| 36 |
X,
|
|
|
|
| 38 |
import "./style.css";
|
| 39 |
import "./wizard.css";
|
| 40 |
import AnnotationExamples from "./AnnotationExamples";
|
| 41 |
+
import JobsView, { type Job, type Progress } from "./JobsView";
|
| 42 |
|
| 43 |
type Feature =
|
| 44 |
| "Subtasks"
|
|
|
|
| 122 |
csrf: string;
|
| 123 |
auth_available: boolean;
|
| 124 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
const EXPORT = "Annotation files only",
|
| 126 |
COPY = "Annotated dataset copy";
|
| 127 |
const defaults: Options = {
|
|
|
|
| 202 |
const num = (n: number) => new Intl.NumberFormat("en-US").format(n);
|
| 203 |
const duration = (s: number) =>
|
| 204 |
s >= 3600 ? `${(s / 3600).toFixed(1)} hours` : `${(s / 60).toFixed(1)} min`;
|
|
|
|
| 205 |
function initial() {
|
| 206 |
try {
|
| 207 |
return {
|
|
|
|
| 700 |
<BookOpen size={18} /> How it works
|
| 701 |
</button>
|
| 702 |
</nav>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
</aside>
|
| 704 |
<div className="workspace">
|
| 705 |
<header className="topbar">
|
|
|
|
| 711 |
>
|
| 712 |
<Menu size={20} />
|
| 713 |
</button>
|
|
|
|
|
|
|
| 714 |
<strong>
|
| 715 |
{page === "new"
|
| 716 |
? "New annotation"
|
|
|
|
| 773 |
<div className="page-heading">
|
| 774 |
<div>
|
| 775 |
<h1>New annotation</h1>
|
|
|
|
| 776 |
</div>
|
| 777 |
</div>
|
| 778 |
<nav className="wizard-steps" aria-label="Annotation setup">
|
|
|
|
| 804 |
))}
|
| 805 |
</nav>
|
| 806 |
<div className="step-intro">
|
|
|
|
| 807 |
<h2 ref={stepHeading} tabIndex={-1}>
|
| 808 |
{
|
| 809 |
[
|
|
|
|
| 814 |
][step]
|
| 815 |
}
|
| 816 |
</h2>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 817 |
</div>
|
| 818 |
{step === 0 && (
|
| 819 |
<section
|
|
|
|
| 821 |
aria-label="Dataset settings"
|
| 822 |
>
|
| 823 |
<label className="field-label" htmlFor="dataset">
|
| 824 |
+
Dataset ID
|
| 825 |
</label>
|
| 826 |
<div className="dataset-input">
|
| 827 |
<Database size={17} />
|
|
|
|
| 836 |
{metadata && <Check size={18} className="success-text" />}
|
| 837 |
</div>
|
| 838 |
<div className="field-help">
|
| 839 |
+
Sign in to access private datasets.
|
|
|
|
| 840 |
</div>
|
| 841 |
<div className="fields-row">
|
| 842 |
<label>
|
|
|
|
| 1046 |
|
| 1047 |
{selectedModel && (
|
| 1048 |
<>
|
|
|
|
|
|
|
|
|
|
| 1049 |
<div className="model-overview">
|
| 1050 |
<span>{selectedModel.provider_name}</span>
|
| 1051 |
<span>
|
|
|
|
| 1202 |
</label>
|
| 1203 |
))}
|
| 1204 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1205 |
<fieldset className="visibility-control">
|
| 1206 |
<legend>Repository visibility</legend>
|
| 1207 |
<div className="visibility-options">
|
|
|
|
| 1233 |
</label>
|
| 1234 |
))}
|
| 1235 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1236 |
</fieldset>
|
| 1237 |
<button
|
| 1238 |
className="disclosure"
|
|
|
|
| 1275 |
</select>
|
| 1276 |
</label>
|
| 1277 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1278 |
</div>
|
| 1279 |
)}
|
| 1280 |
</section>
|
|
|
|
| 1293 |
<span>–</span>
|
| 1294 |
{money(quote.costs.high)}
|
| 1295 |
</div>
|
|
|
|
|
|
|
|
|
|
| 1296 |
<details className="progressive-details cost-breakdown">
|
| 1297 |
<summary>
|
| 1298 |
Cost breakdown <ChevronDown size={15} />
|
|
|
|
| 1390 |
/>
|
| 1391 |
</div>
|
| 1392 |
<p className="microcopy">
|
| 1393 |
+
Stops near this target; completed results are kept.
|
|
|
|
| 1394 |
</p>
|
| 1395 |
{quote && budget < quote.costs.high && (
|
| 1396 |
<div className="budget-hint">
|
|
|
|
| 1427 |
<Rocket size={16} /> Review & launch{" "}
|
| 1428 |
<ArrowRight size={16} />
|
| 1429 |
</button>
|
|
|
|
|
|
|
|
|
|
| 1430 |
</>
|
| 1431 |
) : (
|
| 1432 |
+
<>{signIn}</>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1433 |
)}
|
| 1434 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1435 |
</section>
|
| 1436 |
</div>
|
| 1437 |
)}
|
|
|
|
| 1445 |
<ArrowLeft size={15} /> Back
|
| 1446 |
</button>
|
| 1447 |
) : (
|
| 1448 |
+
<span />
|
|
|
|
|
|
|
| 1449 |
)}
|
| 1450 |
{step < 3 && (
|
| 1451 |
<button
|
|
|
|
| 1477 |
</div>
|
| 1478 |
)}
|
| 1479 |
{page === "jobs" && (
|
| 1480 |
+
<JobsView
|
| 1481 |
+
jobs={jobs}
|
| 1482 |
+
progress={progress}
|
| 1483 |
+
models={modelCatalog?.models || []}
|
| 1484 |
+
signedIn={!!session?.user}
|
| 1485 |
+
loading={jobsLoading}
|
| 1486 |
+
signIn={signIn}
|
| 1487 |
+
onRefresh={loadJobs}
|
| 1488 |
+
onNew={() => navigate("new")}
|
| 1489 |
+
onCancel={setCancelId}
|
| 1490 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1491 |
)}
|
| 1492 |
{page === "guide" && (
|
| 1493 |
<>
|
|
|
|
| 1569 |
</div>
|
| 1570 |
</>
|
| 1571 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1572 |
</main>
|
| 1573 |
</div>
|
| 1574 |
<dialog
|
|
|
|
| 1693 |
</button>
|
| 1694 |
</div>
|
| 1695 |
<div className="modal-body">
|
|
|
|
| 1696 |
<h2>Review your annotation job</h2>
|
|
|
|
| 1697 |
{error && (
|
| 1698 |
<div className="message error" role="alert">
|
| 1699 |
{error}
|
|
|
|
| 1770 |
onChange={(e) => setConsent(e.target.checked)}
|
| 1771 |
/>
|
| 1772 |
<span>
|
| 1773 |
+
I approve this run and its charges, using{" "}
|
| 1774 |
+
{quote?.model_profile.provider_name || "the selected provider"}.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1775 |
</span>
|
| 1776 |
</label>
|
| 1777 |
+
<p className="microcopy">Storage is billed separately.</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1778 |
<button
|
| 1779 |
className="button primary full"
|
| 1780 |
disabled={!consent || submitting}
|