fix: training panel stuck button, stale runs on startup, add delete model
This commit is contained in:
parent
d34dc9d729
commit
6ef102cf21
4 changed files with 240 additions and 24 deletions
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import { ChevronDown, Trash2 } from 'lucide-react';
|
||||
|
||||
interface DatasetInfo {
|
||||
path: string;
|
||||
|
|
@ -68,6 +68,7 @@ export default function TrainingPanel() {
|
|||
const [statusMessage, setStatusMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
const [isLoadingDataset, setIsLoadingDataset] = useState(false);
|
||||
const [isLoadingRuns, setIsLoadingRuns] = useState(false);
|
||||
const [deletingRunIds, setDeletingRunIds] = useState<Set<string>>(new Set());
|
||||
const pollIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const fetchDatasetInfo = useCallback(async () => {
|
||||
|
|
@ -95,39 +96,53 @@ export default function TrainingPanel() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
// Load data when panel expands
|
||||
useEffect(() => {
|
||||
if (expanded) {
|
||||
fetchDatasetInfo();
|
||||
setIsLoadingRuns(true);
|
||||
fetchRuns().finally(() => setIsLoadingRuns(false));
|
||||
// Fetch the authoritative active run from the backend
|
||||
const fetchActiveRun = useCallback(async (): Promise<string | null> => {
|
||||
try {
|
||||
const res = await fetch('/api/training/active');
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
return data.active ? (data.run_id ?? null) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, [expanded, fetchDatasetInfo, fetchRuns]);
|
||||
}, []);
|
||||
|
||||
// Check if there's an active training run on expand
|
||||
// Load data when panel expands; check backend for truly active run
|
||||
useEffect(() => {
|
||||
if (expanded && runs.length > 0) {
|
||||
const running = runs.find((r) => r.status === 'running');
|
||||
if (running && !activeRunId) {
|
||||
setActiveRunId(running.run_id);
|
||||
if (!expanded) return;
|
||||
|
||||
fetchDatasetInfo();
|
||||
setIsLoadingRuns(true);
|
||||
|
||||
Promise.all([fetchRuns(), fetchActiveRun()]).then(([_runs, serverActiveRunId]) => {
|
||||
if (serverActiveRunId) {
|
||||
setActiveRunId(serverActiveRunId);
|
||||
setIsTraining(true);
|
||||
} else {
|
||||
// Ensure we are not stuck in training state
|
||||
setIsTraining(false);
|
||||
setActiveRunId(null);
|
||||
}
|
||||
}
|
||||
}, [expanded, runs, activeRunId]);
|
||||
}).finally(() => setIsLoadingRuns(false));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [expanded]);
|
||||
|
||||
// Poll while training is active
|
||||
useEffect(() => {
|
||||
if (isTraining && activeRunId) {
|
||||
pollIntervalRef.current = setInterval(async () => {
|
||||
const updatedRuns = await fetchRuns();
|
||||
const activeRun = updatedRuns.find((r) => r.run_id === activeRunId);
|
||||
if (activeRun && activeRun.status !== 'running') {
|
||||
const [updatedRuns, serverActiveRunId] = await Promise.all([fetchRuns(), fetchActiveRun()]);
|
||||
|
||||
// If the server says nothing is active, training is done
|
||||
if (!serverActiveRunId) {
|
||||
setIsTraining(false);
|
||||
const finishedRun = updatedRuns.find((r) => r.run_id === activeRunId);
|
||||
setActiveRunId(null);
|
||||
if (pollIntervalRef.current) clearInterval(pollIntervalRef.current);
|
||||
|
||||
if (activeRun.status === 'completed') {
|
||||
const metrics = activeRun.metrics_summary;
|
||||
if (finishedRun?.status === 'completed') {
|
||||
const metrics = finishedRun.metrics_summary;
|
||||
const metricStr = metrics
|
||||
? Object.entries(metrics)
|
||||
.map(([k, v]) => `${k}: ${typeof v === 'number' ? (v * 100).toFixed(1) + '%' : v}`)
|
||||
|
|
@ -137,10 +152,10 @@ export default function TrainingPanel() {
|
|||
type: 'success',
|
||||
text: `Training complete!${metricStr ? ' ' + metricStr : ''}`,
|
||||
});
|
||||
} else {
|
||||
} else if (finishedRun?.status === 'failed') {
|
||||
setStatusMessage({
|
||||
type: 'error',
|
||||
text: activeRun.error || 'Training failed',
|
||||
text: finishedRun.error || 'Training failed',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -155,7 +170,7 @@ export default function TrainingPanel() {
|
|||
return () => {
|
||||
if (pollIntervalRef.current) clearInterval(pollIntervalRef.current);
|
||||
};
|
||||
}, [isTraining, activeRunId, fetchRuns]);
|
||||
}, [isTraining, activeRunId, fetchRuns, fetchActiveRun]);
|
||||
|
||||
const handleStartTraining = async () => {
|
||||
setStatusMessage(null);
|
||||
|
|
@ -192,6 +207,27 @@ export default function TrainingPanel() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleDeleteRun = async (runId: string) => {
|
||||
setDeletingRunIds((prev) => new Set(prev).add(runId));
|
||||
try {
|
||||
const res = await fetch(`/api/training/runs/${runId}`, { method: 'DELETE' });
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
setStatusMessage({ type: 'error', text: data.error || data.detail || 'Failed to delete run' });
|
||||
return;
|
||||
}
|
||||
await fetchRuns();
|
||||
} catch {
|
||||
setStatusMessage({ type: 'error', text: 'Failed to delete run' });
|
||||
} finally {
|
||||
setDeletingRunIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(runId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const datasetMissing = datasetInfo !== null && !datasetInfo.exists;
|
||||
const canTrain = !isTraining && !datasetMissing && datasetInfo !== null;
|
||||
|
||||
|
|
@ -301,7 +337,19 @@ export default function TrainingPanel() {
|
|||
<span className="text-foreground font-medium capitalize">
|
||||
{run.model_type.replace('_', ' ')}
|
||||
</span>
|
||||
<StatusBadge status={run.status} />
|
||||
<div className="flex items-center gap-1">
|
||||
<StatusBadge status={run.status} />
|
||||
{run.status !== 'running' && (
|
||||
<button
|
||||
onClick={() => handleDeleteRun(run.run_id)}
|
||||
disabled={deletingRunIds.has(run.run_id)}
|
||||
title="Delete run"
|
||||
className="text-muted-foreground hover:text-destructive transition-colors disabled:opacity-40"
|
||||
>
|
||||
<Trash2 className="w-2.5 h-2.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-muted-foreground">{formatDate(run.created_at)}</div>
|
||||
{run.status === 'completed' && run.metrics_summary && (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue