+
{source && }
- } />
+ } />
-
+
{rows.map((r, i) => (
-
+
{JSON.stringify(r)}
))}
-
+
>
);
}
@@ -385,8 +414,8 @@ function PreviewCard(props: {
total: number | null | undefined; rows: Record
[];
}) {
return (
-
-
{props.title}
+
);
@@ -394,9 +423,9 @@ function PreviewCard(props: {
function Meta({ label, value }: { label: string; value: React.ReactNode }) {
return (
-
-
{label}:
-
{value}
+
+ {label}
+ {value}
);
}
diff --git a/frontend/src/pages/Machines.tsx b/frontend/src/pages/Machines.tsx
index 0bcd090..a8088c6 100644
--- a/frontend/src/pages/Machines.tsx
+++ b/frontend/src/pages/Machines.tsx
@@ -1,20 +1,25 @@
// Machines — every device serving this hub via `shadowlm worker`.
-import { useEffect, useState } from "react";
-import { Check, Copy, KeyRound, MonitorSmartphone, Trash2 } from "lucide-react";
-import { createToken, getTokens, getWorkers, revokeToken } from "../api";
-import type { MachineToken, WorkerInfo } from "../api";
-import { PageHeader } from "../ui";
+import { Fragment, useEffect, useState } from "react";
+import { Check, ChevronDown, ChevronRight, Copy, KeyRound, MonitorSmartphone, Trash2 } from "lucide-react";
+
+import { createToken, getTokens, getWorkers, revokeToken } from "@/api";
+import type { MachineToken, WorkerInfo } from "@/api";
+import { EmptyState, Mono, PageHeader, SectionHeader } from "@/components/common";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
+import { cn } from "@/lib/utils";
function CopyBtn({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
return (
-
navigator.clipboard.writeText(text).then(() => {
setCopied(true); setTimeout(() => setCopied(false), 1500);
- })}
- className="shrink-0 p-2 rounded-md border border-border text-muted-foreground hover:text-foreground hover:bg-accent/40">
- {copied ? : }
-
+ })}>
+ {copied ?
:
}
+
);
}
@@ -43,50 +48,49 @@ function ConnectCmd() {
: null;
return (
-
+
- setName(e.target.value)}
+ setName(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && mint()}
- placeholder="machine name — e.g. macbook"
- className="flex-1 font-mono text-sm" />
-
- Create machine token
-
+ placeholder="Machine name, e.g. macbook"
+ className="flex-1 font-mono" />
+
+ Create machine token
+
- {err &&
{err}
}
+ {err &&
{err}
}
{cmd && (
-
+
-
- long-lived token, shown once — copy it now. Revoke it here any time.
+
+ A long-lived token, shown once: copy it now. Revoke it here any time.
)}
{tokens.length > 0 && (
-
+
{tokens.map((t) => (
-
-
- {t.name}
-
+
+
+ {t.name}
+
created {new Date(t.created * 1000).toLocaleDateString()}
- revokeToken(t.name).then(refresh)}
- className="p-1 rounded text-muted-foreground hover:text-red-500">
-
-
-
+ revokeToken(t.name).then(refresh)}>
+
+
+
))}
-
+
)}
);
@@ -129,97 +133,94 @@ export default function Machines() {
return (
<>
Devices serving this hub over one outbound socket. They appear here the moment shadowlm worker connects, and any of them can be picked as the training target.>}
/>
-
- {loaded && workers.length === 0 ? (
-
-
- No machines connected
-
- On any machine with shadowlm installed
- (a MacBook, an office GPU box — NAT is fine, it dials out):
-
-
-
- ) : (
-
-
-
Connected machines
-
+
+ {loaded && workers.length === 0 ? (
+
+
+ On any machine with shadowlm installed (a MacBook, an office GPU box; NAT is
+ fine, it dials out), create a token and run the command it gives you.
+
+
+
+ ) : (
+ <>
+
{workers.filter((w) => w.online).length}/{workers.length} online
-
-
-
-
- Machine
- Backend
- Platform
- Compute
- Queue
- Status
-
-
-
+ }
+ />
+
+
+
+
+ Machine
+ Backend
+ Platform
+ Compute
+ Queue
+ Status
+
+
+
{workers.map((w) => (
- <>
- setOpen(open === w.name ? null : w.name)}>
-
-
-
- {w.name}
+
+ setOpen(open === w.name ? null : w.name)}>
+
+
+
+ {w.name}
{w.models.length > 0 && (
-
- {open === w.name ? "▾" : "▸"} {w.models.length} models
+
+ {open === w.name ? : }
+ {w.models.length} models
)}
-
- {w.backend}
- {w.device}
- {compute(w)}
- {w.queued || "—"}
-
+
+ {w.backend}
+ {w.device}
+ {compute(w)}
+ {w.queued || "—"}
+
{w.online ? (w.queued ? "busy" : "idle") : `last seen ${ago(w.last_seen)}`}
-
-
+
+
{open === w.name && w.models.length > 0 && (
-
-
-
- models on {w.name}
-
+
+
+ Models on {w.name}
{w.models.map((m) => (
-
+
{m.id}
- · {m.size_gb} GB
-
+ · {m.size_gb} GB
+
))}
-
-
+
+
)}
- >
+
))}
-
-
-
- )}
-
- {workers.length > 0 && (
-
-
Add another machine:
-
+
+
- )}
-
+
+ {workers.length > 0 && (
+
+ )}
+ >
+ )}
>
);
}
diff --git a/frontend/src/pages/Models.tsx b/frontend/src/pages/Models.tsx
index dc9517c..e021a1d 100644
--- a/frontend/src/pages/Models.tsx
+++ b/frontend/src/pages/Models.tsx
@@ -1,11 +1,16 @@
// Model library — catalog + recently trained, search and family filters.
import { useCallback, useEffect, useMemo, useState } from "react";
-import { Check, Cpu, Download, Loader2, Search, X } from "lucide-react";
+import { Box, Check, Download, LoaderCircle, Plus, Search, TriangleAlert, X } from "lucide-react";
+
import {
addCustomModel, downloadModel, getDownloads, getModels, removeCustomModel,
-} from "../api";
-import type { CatalogModel, DownloadStatus } from "../api";
-import { PageHeader, btnGhost, btnPrimary } from "../ui";
+} from "@/api";
+import type { CatalogModel, DownloadStatus } from "@/api";
+import { EmptyState, Mono, PageHeader } from "@/components/common";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
const fmtGB = (b?: number) => (b ? `${(b / 1e9).toFixed(b < 1e9 ? 2 : 1)} GB` : "");
@@ -77,104 +82,114 @@ export default function Models() {
return (
<>
{ e.preventDefault(); addModel(free); }}>
+ setFree(e.target.value)}
+ placeholder="org/model, any HF id" className="w-56 font-mono" />
+ Add
+
+ }
/>
-
-
-
-
- setSearch(e.target.value)}
- placeholder="Search models…" className="w-full pl-9 pr-3 py-2 text-sm" />
-
-
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search models…" className="pl-8" />
+
+
+
{families.map((f) => (
- setFamily(f)}
- className={`px-2.5 py-1 text-[11px] font-mono rounded transition-colors ${
- family === f ? "bg-card text-foreground shadow-sm" : "text-muted-foreground"}`}>
- {f}
-
+ {f}
))}
-
-
-
+
+
+
-
+ {filtered.length === 0 ? (
+
+ Clear the search, or add any Hugging Face model id above.
+
+ ) : (
+
{filtered.map((m) => {
const dl = downloads[m.id];
const onDisk = m.cached || dl?.state === "ready";
const downloading = dl?.state === "downloading";
return (
-
-
-
-
+
+
+
+
+
+
+ {onDisk && (
+
+ On disk
+
+ )}
+ {m.dev && Dev pick }
+ {m.gated && (
+
+ HF token
+
+ )}
+ {m.custom && (
+ removeModel(m.id)}>
+
+
+ )}
+
-
- {onDisk && on disk }
- {m.dev && dev pick }
- {m.gated && HF token }
- {m.custom && (
- removeModel(m.id)} title="remove from library"
- className="text-muted-foreground hover:text-destructive">
-
-
- )}
-
-
-
{m.id.split("/").pop()}
-
- {m.id}{m.params ? ` · ${m.params}` : ""}
-
- {m.note &&
{m.note}
}
+
{m.id.split("/").pop()}
+
+ {m.id} {m.params ? · {m.params} : ""}
+
+ {m.note &&
{m.note}
}
- {downloading && (
-
-
-
+ {downloading && (
+
+
+
+
+ {dl.pct != null
+ ? `${dl.pct}% · ${fmtGB(dl.downloaded)} / ${fmtGB(dl.total)}`
+ : "downloading…"}
+
-
-
- {dl.pct != null
- ? `${dl.pct}% · ${fmtGB(dl.downloaded)} / ${fmtGB(dl.total)}`
- : "downloading…"}
+ )}
+ {dl?.state === "error" && (
+
+ {dl.error}
-
- )}
- {dl?.state === "error" && (
-
- ⚠ {dl.error}
-
- )}
-
-
-
pick("model", m.id, "#train")}
- className={`${btnPrimary} flex-1 justify-center`}>
- Fine-tune
-
-
pick("model", m.id, "#playground")} className={btnGhost}>
- Try
-
- {!onDisk && !downloading && (
-
startDownload(m.id)} className={btnGhost} title="prefetch weights to disk">
-
-
)}
+
+
+ pick("model", m.id, "#train")}>
+ Fine-tune
+
+ pick("model", m.id, "#playground")}>
+ Try
+
+ {!onDisk && !downloading && (
+ startDownload(m.id)}>
+
+
+ )}
+
-
);
})}
-
+ )}
>
);
}
diff --git a/frontend/src/pages/Playground.tsx b/frontend/src/pages/Playground.tsx
index fee563c..301e2fd 100644
--- a/frontend/src/pages/Playground.tsx
+++ b/frontend/src/pages/Playground.tsx
@@ -1,11 +1,14 @@
-// Playground — a clean chat. The model picker is a command palette (slm❯): one
+// Playground — a clean chat. The model picker is a command palette: one
// search across base open models AND your shadows, grouped. Pick a shadow and a
// quiet "shadow mode" toggle appears — the shadow answers next to its base.
import { useEffect, useMemo, useRef, useState } from "react";
-import { ArrowUp, ChevronDown } from "lucide-react";
-import { chat, getCheckpoints, getJobs, getModels, prewarm } from "../api";
-import type { CatalogModel, Checkpoint, JobSummary } from "../api";
-import { Dots } from "../ui";
+import { ArrowUp, Check, ChevronDown, ChevronRight, LoaderCircle, MessagesSquare, RotateCcw, Search, TriangleAlert } from "lucide-react";
+import { chat, getCheckpoints, getJobs, getModels, prewarm } from "@/api";
+import type { CatalogModel, Checkpoint, JobSummary } from "@/api";
+import { Dots } from "@/components/common";
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog";
+import { cn } from "@/lib/utils";
type Msg = { role: "user" | "assistant"; content: string };
@@ -143,32 +146,33 @@ export default function Playground() {
const empty = msgs.length === 0;
return (
-
+
{/* model selector */}
-
+
+
{ setPop((v) => !v); setQ(""); }}
- className="flex items-center gap-2 text-base font-semibold hover:text-primary transition-colors">
- {adapter ? "shadow❯" : "base❯"}
- {label}
-
+ className="flex min-w-0 items-center gap-2 rounded-md px-1.5 py-1 text-sm font-semibold transition-colors hover:bg-surface-2">
+ {adapter ? "Shadow" : "Base"}
+ {label}
+
{adapter && (
-
-
+ setCompare(e.target.checked)} />
- shadow mode (base ↔ shadow)
+ Shadow mode (base ↔ shadow)
)}
{adapter && ckpts.length > 1 && (
-
+
ckpt
{ckpts.map((c) => {
const on = c.final ? ckptStep === null : ckptStep === c.step;
return (
setCkptStep(c.final ? null : c.step)}
- className={`rounded-full px-2 py-0.5 transition-colors ${
- on ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"}`}>
+ className={cn("rounded-md px-2 py-0.5 transition-colors",
+ on ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground")}>
{c.final ? "final" : c.step}
);
@@ -176,125 +180,122 @@ export default function Playground() {
)}
{msgs.length > 0 && (
-
{ setMsgs([]); setBase([]); }}>clear
+
{ setMsgs([]); setBase([]); }}>
+ Clear
+
)}
-
{/* model picker — a command palette, not a dropdown: one search across
- base models AND your shadows, grouped, terminal-styled. */}
- {pop && (
-
setPop(false)}>
-
e.stopPropagation()}
- className="mt-[11vh] w-full max-w-2xl rounded-xl border border-border bg-card shadow-[0_32px_80px_#0004] overflow-hidden">
- {/* prompt line */}
-
- slm❯
- setQ(e.target.value)}
- onKeyDown={(e) => {
- if (e.key === "Escape") setPop(false);
- if (e.key === "Enter") {
- if (hubRows[0] && (!tunedRows.length || q)) pickHub(hubRows[0].id);
- else if (tunedRows[0]) pickTuned(tunedRows[0]);
- }
- }}
- placeholder="filter base models or your shadows · paste any HF id"
- className="flex-1 border-0 bg-transparent p-0 text-sm focus:outline-none focus:ring-0" />
- esc
-
+ base models AND your shadows, grouped. */}
+
+
+ Pick a model
+ Filter base models or your shadows, or paste any Hugging Face id.
+ {/* prompt line */}
+
+
+ setQ(e.target.value)} data-slot="palette-input"
+ onKeyDown={(e) => {
+ if (e.key === "Escape") setPop(false);
+ if (e.key === "Enter") {
+ if (hubRows[0] && (!tunedRows.length || q)) pickHub(hubRows[0].id);
+ else if (tunedRows[0]) pickTuned(tunedRows[0]);
+ }
+ }}
+ placeholder="Filter base models or your shadows · paste any HF id"
+ className="flex-1 border-0 bg-transparent p-0 text-sm outline-none placeholder:text-muted-foreground" />
+ esc
+
-
- {/* base models */}
-
- base · open models
-
-
- {hubRows.map((m) => {
- const on = m.id === model && !adapter;
- return (
-
pickHub(m.id)}
- className="group w-full flex items-center gap-3 px-4 py-2 text-sm hover:bg-accent/40 text-left font-mono">
-
- {on ? "●" : "›"}
-
- {m.id}
-
- {m.dev ? "dev pick" : m.gated ? "HF token" : m.params ?? m.note ?? ""}
-
-
- );
- })}
+
+ {/* base models */}
+
Base · open models
+ {hubRows.map((m) => {
+ const on = m.id === model && !adapter;
+ return (
+
pickHub(m.id)}
+ className="group flex w-full items-center gap-3 px-4 py-2 text-left text-sm hover:bg-subtle">
+
+ {on ?
+ : }
+
+ {m.id}
+
+ {m.dev ? "dev pick" : m.gated ? "HF token" : m.params ?? m.note ?? ""}
+
+
+ );
+ })}
- {/* shadows */}
-
-
shadow · your runs
-
+ {/* shadows */}
+
Shadows · your runs
+ {tunedRows.length ? tunedRows.map((j) => {
+ const on = j.job_id === adapter;
+ return (
+
pickTuned(j)}
+ className="group flex w-full items-center gap-3 px-4 py-2 text-left text-sm hover:bg-subtle">
+
+ {on ?
+ : }
+
+
+ {j.name?.trim() || {j.job_id.slice(0, 10)} }
+ · {j.method}
+
+
+ shadows {(j.base_model || "").split("/").pop()}
+
+
+ );
+ }) : (
+
- {tunedRows.length ? tunedRows.map((j) => {
- const on = j.job_id === adapter;
- return (
-
pickTuned(j)}
- className="group w-full flex items-center gap-3 px-4 py-2 text-sm hover:bg-accent/40 text-left font-mono">
-
- {on ? "●" : "›"}
-
-
- {j.name?.trim() || j.job_id.slice(0, 10)} · {j.method}
-
-
- shadows {(j.base_model || "").split("/").pop()}
-
-
- );
- }) : (
-
- no shadows yet —
train one to see it here
-
- )}
-
+ )}
-
- )}
+
+
{/* transcript or greeting */}
-
pop && setPop(false)}>
{empty ? (
-
+
+ {/* The brain: red on the light theme, white on the dark one. */}
-
-
slm♥ playground
-
- {adapter ? "Does it cast the same shadow?" : "Talk to a model"}
-
-
-
- base {model.split("/").pop()}
- {adapter && <> · shadow {adapter.slice(0, 8)} >}
+ className="mb-2 size-28 dark:hidden [filter:drop-shadow(0_0_40px_#e5484d55)_drop-shadow(0_0_14px_#e5484d44)]" />
+
+
+ {adapter ? "Does it cast the same shadow?" : "Talk to a model"}
+
+
+ Base {model.split("/").pop()}
+ {adapter && <> · shadow {adapter.slice(0, 8)} >}
-
from Lyzr Research Labs
) : compare && adapter ? (
-
+
{msgs.map((m, i) => m.role === "user" ? (
) : (
))}
- {busy &&
}
+ {busy &&
}
) : (
-
+
{msgs.map((m, i) => m.role === "user"
?
- :
-
slm♥ › {m.content}
+ :
)}
{busy &&
}
@@ -302,24 +303,22 @@ export default function Playground() {
{/* input */}
-
-
@@ -329,17 +328,17 @@ export default function Playground() {
function UserBubble({ text }: { text: string }) {
return (
);
}
function Pane({ tone, label, text }: { tone: "tuned" | "base"; label: string; text?: string }) {
return (
-
-
{label}
+
+
{label}
{text == null ? : text}
);
diff --git a/frontend/src/pages/Runs.tsx b/frontend/src/pages/Runs.tsx
index 1fd3ac4..e08afb0 100644
--- a/frontend/src/pages/Runs.tsx
+++ b/frontend/src/pages/Runs.tsx
@@ -1,9 +1,15 @@
// Runs — master-detail: searchable run list left, live detail right.
import { useEffect, useRef, useState } from "react";
-import { Download, MessagesSquare, Search, Square } from "lucide-react";
-import { apiKey, cancelJob, getCheckpoints, getJob, getJobs, getLogs, getMetrics } from "../api";
-import type { Checkpoint, JobDetail, JobSummary, StepMetric } from "../api";
-import { ChartLegend, LossChart, PageHeader, Sparkline, StatTile, StatusBadge, btnGhost } from "../ui";
+import type { ReactNode } from "react";
+import { CircleAlert, Download, History, MessagesSquare, PackageOpen, Search, Square, TerminalSquare } from "lucide-react";
+import { apiFetch, cancelJob, getCheckpoints, getJob, getJobs, getLogs, getMetrics } from "@/api";
+import type { Checkpoint, JobDetail, JobSummary, StepMetric } from "@/api";
+import { ChartLegend, LossChart, Sparkline } from "@/components/charts";
+import { EmptyState, Mono, PageHeader, Stat, StatStrip, StatusBadge } from "@/components/common";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { cn } from "@/lib/utils";
export default function Runs({ initialId }: { initialId?: string }) {
const [jobs, setJobs] = useState
([]);
@@ -45,21 +51,20 @@ export default function Runs({ initialId }: { initialId?: string }) {
return (
<>
-
-
-
+
+
+
-
+
{filtered.length === 0 && (
-
- no runs yet — start one in
Train
+
)}
{filtered.map((r) => (
{ setSelectedId(r.job_id); window.location.hash = `#runs/${r.job_id}`; }}
- className={`w-full text-left px-5 py-3.5 hover:bg-accent/30 transition-colors ${
+ aria-current={selectedId === r.job_id ? "true" : undefined}
+ className={cn(
+ "w-full border-l-2 px-4 py-3 text-left transition-colors",
selectedId === r.job_id
- ? "bg-accent/50 border-l-2 border-l-primary"
- : "border-l-2 border-l-transparent"}`}>
-
-
+ ? "border-l-primary bg-primary/5"
+ : "border-l-transparent hover:bg-subtle")}>
+
+
{r.name?.trim() || r.job_id.slice(0, 12)}
- {r.base_model}
-
-
+
{r.base_model}
+
+
{r.method ?? "?"} · {r.steps} steps
@@ -101,12 +110,30 @@ export default function Runs({ initialId }: { initialId?: string }) {
{selected
?
- : select a run
}
+ : Select a run to see its curves, logs and artifact.
}
>
);
}
+// Panel is one framed block of the run detail: a title line, then its body.
+function Panel({ title, sub, actions, children, className }: {
+ title: string; sub?: string; actions?: ReactNode; children: ReactNode; className?: string;
+}) {
+ return (
+
+
+
+
{title}
+ {sub &&
{sub}
}
+
+ {actions}
+
+ {children}
+
+ );
+}
+
function RunDetail({ run }: { run: JobSummary }) {
const [job, setJob] = useState
(null);
const [steps, setSteps] = useState([]);
@@ -135,9 +162,7 @@ function RunDetail({ run }: { run: JobSummary }) {
const last = steps[steps.length - 1];
async function downloadAdapter() {
- const headers: Record = {};
- if (apiKey.get()) headers["Authorization"] = `Bearer ${apiKey.get()}`;
- const r = await fetch(`/v1/finetunes/${run.job_id}/artifact`, { headers });
+ const r = await apiFetch(`/v1/finetunes/${run.job_id}/artifact`);
if (!r.ok) return alert("artifact not ready");
const blob = await r.blob();
const a = document.createElement("a");
@@ -147,179 +172,157 @@ function RunDetail({ run }: { run: JobSummary }) {
URL.revokeObjectURL(a.href);
}
+ const status = job?.status ?? run.status;
+ const toPlayground = () => {
+ sessionStorage.setItem("pick.adapter", run.job_id);
+ sessionStorage.setItem("pick.model", run.base_model);
+ window.location.hash = "#playground";
+ };
+
return (
-
-
-
-
-
+
+
+
+
+
{run.name?.trim() && {run.name.trim()} }
- {run.job_id}
-
+ {run.job_id}
+
-
{run.base_model}
-
+
{run.base_model}
+
{run.method ?? "?"}
{last?.tokens_per_s ? ` · ${Math.round(last.tokens_per_s)} tok/s` : ""}
{(job?.status === "running" || job?.status === "pending") && (
- cancelJob(run.job_id)}
- className="inline-flex items-center gap-1.5 rounded-md border border-destructive/40 bg-destructive/10 text-destructive px-3 py-2 text-xs font-medium hover:bg-destructive/20 transition-colors">
- Cancel
-
+ cancelJob(run.job_id)}>
+ Cancel
+
)}
{job?.status === "succeeded" && (
<>
- {
- sessionStorage.setItem("pick.adapter", run.job_id);
- sessionStorage.setItem("pick.model", run.base_model);
- window.location.hash = "#playground";
- }} className={btnGhost}>
- Playground
-
-
- Adapter
-
+
+ Playground
+
+
+ Adapter
+
>
)}
-
-
-
-
-
-
+
+
+
+
+
+
- {/* tabs */}
-
- {(["loss", "logs", "artifact"] as const).map((t) => (
- setTab(t)}
- className={`px-4 py-2.5 text-sm border-b-2 -mb-px transition-colors capitalize ${
- tab === t ? "border-primary text-foreground"
- : "border-transparent text-muted-foreground hover:text-foreground"}`}>
- {t === "loss" ? "Loss curves" : t === "logs" ? "Training logs" : "Artifact"}
- {t === "logs" && job?.status === "running" &&
- }
-
- ))}
-
+
setTab(v as typeof tab)} className="gap-4">
+
+ Loss curves
+
+ Training logs
+ {job?.status === "running" && }
+
+ Artifact
+
- {tab === "loss" && (
-
+
{/* combined: train curve with eval overlaid */}
-
+ }>
+
+
{/* separate: train and eval on their own axes */}
-
-
-
- Training loss
- raw + EMA overlay
-
-
-
-
-
- Eval loss
- held-out validation
-
-
+
+
+
+
+
+
{evals.length
?
- :
+ :
-
No eval data
-
Train with a held-out eval split to see this
+
No eval data
+
Train with a held-out eval split to see this
}
-
+
-
- )}
+
- {tab === "logs" && (
- logs.length
- ?
- :
- no console output captured for this run
-
- )}
+
+ {logs.length
+ ?
+ :
+ Nothing was captured for this run.
+ }
+
- {tab === "artifact" && (
-
+
{job?.error && (
-
- Error
- {job.error}
+
+
+ Error
+
+ {job.error}
)}
{job?.checkpoint ? (
-
-
-
+
+
-
{job.checkpoint}
-
- load it back:
slm.load("{run.base_model}", adapter="…")
+
{job.checkpoint}
+
+ Load it back: slm.load("{run.base_model}", adapter="…")
-
- tar.gz
-
+
+ tar.gz
+
{ckpts.length > 1 && (
-
- saved versions · {ckpts.length}
+
+ Saved versions · {ckpts.length}
{[...ckpts].reverse().map((c) => (
-
-
+
+
{c.final ? {c.label} : `step ${c.step}`}
- {
sessionStorage.setItem("pick.adapter", run.job_id);
sessionStorage.setItem("pick.model", run.base_model);
if (c.final) sessionStorage.removeItem("pick.checkpoint");
else sessionStorage.setItem("pick.checkpoint", String(c.step));
window.location.hash = "#playground";
- }}
- className="text-xs text-primary hover:underline inline-flex items-center gap-1">
- test this version
-
+ }}>
+ Test this version
+
))}
-
- trained with
save_steps — each is a point you can roll back to or A/B in the playground.
+
+ Trained with save_steps: each is a point you can roll back to or A/B in the playground.
)}
-
+
) : !job?.error && (
-
- no artifact yet — finishes when training succeeds
-
+
+ It appears when training succeeds.
+
)}
-
- )}
+
+
);
@@ -331,20 +334,19 @@ function TerminalPanel({ logs, live }: { logs: string[]; live: boolean }) {
useEffect(() => { if (stick && ref.current) ref.current.scrollTop = ref.current.scrollHeight; },
[logs, stick]);
return (
-