Skip to content

Commit 5a0ccdc

Browse files
committed
updating injected notebook tutorials
1 parent a2e03fa commit 5a0ccdc

6 files changed

Lines changed: 997 additions & 103 deletions

File tree

deploy/Dockerfile.workspace

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,14 @@ RUN mkdir -p /opt/conda/share/jupyter/lab/settings && \
3131
echo '{ "@jupyterlab/apputils-extension:themes": { "theme": "JupyterLab Dark" } }' \
3232
> /opt/conda/share/jupyter/lab/settings/overrides.json
3333

34-
# Create workspace directory and copy welcome notebook
34+
# Create workspace directory and copy notebooks
3535
RUN mkdir -p /workspace/models && chown -R ${NB_UID}:${NB_GID} /workspace
3636
COPY workspace/welcome.ipynb /workspace/welcome.ipynb
37-
RUN chown ${NB_UID}:${NB_GID} /workspace/welcome.ipynb
37+
COPY workspace/visualization.ipynb /workspace/visualization.ipynb
38+
COPY workspace/registry.ipynb /workspace/registry.ipynb
39+
RUN chown ${NB_UID}:${NB_GID} /workspace/welcome.ipynb \
40+
/workspace/visualization.ipynb \
41+
/workspace/registry.ipynb
3842

3943
USER ${NB_UID}
4044
WORKDIR /workspace

web/src/app/training/[id]/page.tsx

Lines changed: 79 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -118,87 +118,92 @@ export default function TrainingDetailPage() {
118118
return Math.max(0, Math.floor((end - start) / 1000));
119119
}, []);
120120

121-
useEffect(() => {
122-
let cancelled = false;
123-
124-
async function fetchAll() {
125-
try {
126-
const jobRes = await api.get<Job>(`/training/${jobId}`);
127-
if (cancelled) return;
128-
setJob(jobRes);
129-
setElapsedSec(computeElapsed(jobRes.started_at, jobRes.completed_at));
130-
131-
// Fetch metrics and artifacts in parallel
132-
const [metricsRes, artifactsRes] = await Promise.all([
133-
api.get<MetricRecord[]>(`/training/${jobId}/metrics`).catch(() => [] as MetricRecord[]),
134-
api.get<Artifact[]>(`/jobs/${jobId}/artifacts`).catch(() => [] as Artifact[]),
135-
]);
136-
137-
if (cancelled) return;
138-
139-
// Split metrics by metric_name
140-
const loss: { name: string; value: number }[] = [];
141-
const acc: { name: string; value: number }[] = [];
142-
143-
if (metricsRes && metricsRes.length > 0) {
144-
for (const record of metricsRes) {
145-
const point = {
146-
name: (record.step ?? record.epoch ?? 0).toString(),
147-
value: record.value,
148-
};
149-
if (record.metric_name === "loss") {
150-
loss.push(point);
151-
} else if (record.metric_name === "accuracy") {
152-
acc.push(point);
153-
}
121+
const fetchAll = useCallback(async (isInitial = false) => {
122+
try {
123+
const jobRes = await api.get<Job>(`/training/${jobId}`);
124+
setJob(jobRes);
125+
setElapsedSec(computeElapsed(jobRes.started_at, jobRes.completed_at));
126+
127+
// Fetch metrics and artifacts in parallel
128+
const [metricsRes, artifactsRes] = await Promise.all([
129+
api.get<MetricRecord[]>(`/training/${jobId}/metrics`).catch(() => [] as MetricRecord[]),
130+
api.get<Artifact[]>(`/jobs/${jobId}/artifacts`).catch(() => [] as Artifact[]),
131+
]);
132+
133+
// Split metrics by metric_name
134+
const loss: { name: string; value: number }[] = [];
135+
const acc: { name: string; value: number }[] = [];
136+
137+
if (metricsRes && metricsRes.length > 0) {
138+
for (const record of metricsRes) {
139+
const point = {
140+
name: (record.step ?? record.epoch ?? 0).toString(),
141+
value: record.value,
142+
};
143+
if (record.metric_name === "loss") {
144+
loss.push(point);
145+
} else if (record.metric_name === "accuracy") {
146+
acc.push(point);
154147
}
155148
}
149+
}
156150

157-
setLossData(loss);
158-
setAccData(acc);
159-
setArtifacts(artifactsRes ?? []);
160-
} catch (err) {
161-
if (!cancelled) {
162-
toast.error(err instanceof Error ? err.message : "Failed to load training job");
163-
164-
// Set a fallback job so the full UI always renders (e.g. for E2E tests)
165-
const fallbackJob: Job = {
166-
id: jobId,
167-
project_id: "",
168-
model_id: "",
169-
dataset_id: null,
170-
job_type: "Training Job",
171-
status: "unknown",
172-
k8s_job_name: null,
173-
hardware_tier: "N/A",
174-
hyperparameters: {},
175-
metrics: null,
176-
started_at: null,
177-
completed_at: null,
178-
error_message: null,
179-
created_by: "",
180-
created_at: new Date().toISOString(),
181-
updated_at: new Date().toISOString(),
182-
progress: 0,
183-
epoch_current: null,
184-
epoch_total: null,
185-
loss: null,
186-
learning_rate: null,
187-
gpu_config: null,
188-
};
189-
setJob(fallbackJob);
190-
setIsFallback(true);
191-
setElapsedSec(0);
192-
}
193-
} finally {
194-
if (!cancelled) setLoading(false);
151+
setLossData(loss);
152+
setAccData(acc);
153+
setArtifacts(artifactsRes ?? []);
154+
} catch (err) {
155+
if (isInitial) {
156+
toast.error(err instanceof Error ? err.message : "Failed to load training job");
157+
158+
// Set a fallback job so the full UI always renders (e.g. for E2E tests)
159+
const fallbackJob: Job = {
160+
id: jobId,
161+
project_id: "",
162+
model_id: "",
163+
dataset_id: null,
164+
job_type: "Training Job",
165+
status: "unknown",
166+
k8s_job_name: null,
167+
hardware_tier: "N/A",
168+
hyperparameters: {},
169+
metrics: null,
170+
started_at: null,
171+
completed_at: null,
172+
error_message: null,
173+
created_by: "",
174+
created_at: new Date().toISOString(),
175+
updated_at: new Date().toISOString(),
176+
progress: 0,
177+
epoch_current: null,
178+
epoch_total: null,
179+
loss: null,
180+
learning_rate: null,
181+
gpu_config: null,
182+
};
183+
setJob(fallbackJob);
184+
setIsFallback(true);
185+
setElapsedSec(0);
195186
}
187+
} finally {
188+
if (isInitial) setLoading(false);
196189
}
197-
198-
fetchAll();
199-
return () => { cancelled = true; };
200190
}, [jobId, computeElapsed]);
201191

192+
// Initial fetch
193+
useEffect(() => {
194+
fetchAll(true);
195+
}, [fetchAll]);
196+
197+
// Poll job + metrics every 3s while job is active
198+
useEffect(() => {
199+
if (!job) return;
200+
const isActive = job.status === "running" || job.status === "pending";
201+
if (!isActive) return;
202+
203+
const t = setInterval(() => fetchAll(false), 3000);
204+
return () => clearInterval(t);
205+
}, [job?.status, fetchAll]);
206+
202207
// Tick elapsed timer every second while job is running
203208
useEffect(() => {
204209
if (!job) return;

web/src/components/shared/notification-panel.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,11 @@ export function NotificationPanel() {
168168
</PopoverTrigger>
169169
<PopoverContent
170170
align="end"
171-
className="w-96 p-0 border bg-background/95 backdrop-blur-xl"
171+
className="w-96 p-0 border bg-popover shadow-xl"
172172
sideOffset={8}
173173
>
174174
{/* Header */}
175-
<div className="flex items-center justify-between border-b px-4 py-3">
175+
<div className="flex items-center justify-between border-b px-4 py-3 bg-popover rounded-t-md">
176176
<h3 className="text-sm font-semibold text-foreground">Notifications</h3>
177177
<div className="flex items-center gap-1">
178178
{unreadCount > 0 && (
@@ -197,15 +197,15 @@ export function NotificationPanel() {
197197
</div>
198198
</div>
199199

200-
{/* Body */}
201-
<ScrollArea className="max-h-[400px]">
200+
{/* Body — fixed max height, self-contained scrolling */}
201+
<ScrollArea className="h-[min(400px,60vh)]">
202202
{notifications.length === 0 ? (
203203
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
204204
<Bell className="h-8 w-8 mb-2 opacity-30" />
205205
<p className="text-sm">No notifications yet</p>
206206
</div>
207207
) : (
208-
<div className="divide-y">
208+
<div className="divide-y divide-border/50">
209209
{renderGroup("Today", groups.today, handleClick)}
210210
{renderGroup("This Week", groups.thisWeek, handleClick)}
211211
{renderGroup("Earlier", groups.earlier, handleClick)}
@@ -225,7 +225,7 @@ function renderGroup(
225225
if (items.length === 0) return null;
226226
return (
227227
<div>
228-
<div className="sticky top-0 bg-background/90 backdrop-blur px-4 py-1.5">
228+
<div className="sticky top-0 z-10 bg-popover px-4 py-1.5 border-b border-border/30">
229229
<span className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
230230
{label}
231231
</span>

0 commit comments

Comments
 (0)