Files
HSAP/platform/web/src/pages/Iterate.tsx

263 lines
12 KiB
TypeScript
Raw Normal View History

import { useEffect, useMemo, useState } from "react";
import { useOutletContext } from "react-router-dom";
import { api, type CatalogReport, type JobRecord, type PendingReport } from "../api/client";
import { useToast } from "../components/Toast";
import { LANE_DATA_VIZ_ENABLED } from "../config/featureFlags";
type Ctx = { refreshMeta: () => void };
type IterAction = "train_dms" | "train_lane" | "eval_dms" | "eval_lane" | "visualize_dms" | "visualize_lane";
const ITERATE_ACTIONS = new Set<IterAction>([
"train_dms",
"train_lane",
"eval_dms",
"eval_lane",
"visualize_dms",
"visualize_lane",
]);
const ACTION_META: Record<IterAction, { label: string; project: "DMS" | "Lane"; kind: "训练" | "评估" | "可视化" }> = {
train_dms: { label: "DMS 训练", project: "DMS", kind: "训练" },
train_lane: { label: "Lane 训练", project: "Lane", kind: "训练" },
eval_dms: { label: "DMS 评估", project: "DMS", kind: "评估" },
eval_lane: { label: "Lane 评估", project: "Lane", kind: "评估" },
visualize_dms: { label: "DMS 可视化", project: "DMS", kind: "可视化" },
visualize_lane: { label: "Lane 可视化", project: "Lane", kind: "可视化" },
};
function statusBadgeClass(status: string): string {
if (status === "succeeded") return "badge-promoted";
if (status === "running") return "badge-training";
if (status === "failed") return "badge-pending";
return "badge-idle";
}
function fmtTime(ts?: string): string {
if (!ts) return "—";
const d = new Date(ts);
if (Number.isNaN(d.getTime())) return ts;
return d.toLocaleString("zh-CN", { hour12: false });
}
function extractWeight(job: JobRecord): string | null {
const res = (job.result || {}) as Record<string, unknown>;
const params = (job.params || {}) as Record<string, unknown>;
const candidates = [res.best_weights, res.candidate, res.model_path, res.weights, params.weights, params.model_path];
for (const v of candidates) {
if (typeof v === "string" && v.trim()) return v;
}
return null;
}
export function IteratePage() {
const { refreshMeta } = useOutletContext<Ctx>();
const toast = useToast();
const [pending, setPending] = useState<PendingReport | null>(null);
const [cat, setCat] = useState<CatalogReport | null>(null);
const [jobs, setJobs] = useState<JobRecord[]>([]);
const [dmsTask, setDmsTask] = useState("dam");
const [dmsTrack, setDmsTrack] = useState<"platform" | "local">("platform");
const [dmsWeights, setDmsWeights] = useState("");
const [laneTrack, setLaneTrack] = useState<"platform" | "local">("platform");
const [laneModelPath, setLaneModelPath] = useState("");
const [laneDataRoot, setLaneDataRoot] = useState("");
const [laneTestList, setLaneTestList] = useState("list/test_gt.txt");
const [selectedJobId, setSelectedJobId] = useState<string | null>(null);
const loadAll = async () => {
const [p, c, j] = await Promise.all([api.pending(), api.catalog(), api.listJobs()]);
setPending(p);
setCat(c);
setJobs(j.items || []);
};
useEffect(() => { loadAll(); }, []);
useEffect(() => {
const tasks = Object.keys(pending?.projects?.dms?.task_defs || {});
if (tasks.length > 0 && !tasks.includes(dmsTask)) {
setDmsTask(tasks[0]);
}
}, [pending]);
const submit = async (action: string, params: Record<string, unknown>) => {
try {
await api.submitApproval(action, params);
toast(`已提交 ${action}`);
refreshMeta();
await loadAll();
} catch (e) {
toast(String(e), true);
}
};
const packRows = [
...(pending?.projects?.dms?.active_packs || []).map((n) => ({ project: "dms", name: n, enabled: true })),
...(pending?.projects?.dms?.not_enabled || []).map((n) => ({ project: "dms", name: n, enabled: false })),
...Object.entries(cat?.lane || {}).map(([n, info]) => ({ project: "lane", name: n, enabled: info.enabled })),
];
const iterJobs = useMemo(() => {
return jobs.filter((j) => ITERATE_ACTIONS.has(j.action as IterAction)).slice(0, 30);
}, [jobs]);
const milestones = useMemo(() => [...iterJobs].reverse(), [iterJobs]);
const selectedJob = useMemo(
() => milestones.find((j) => j.id === selectedJobId) || milestones[milestones.length - 1] || null,
[milestones, selectedJobId]
);
useEffect(() => {
if (!selectedJobId && milestones.length > 0) {
setSelectedJobId(milestones[milestones.length - 1].id);
}
}, [milestones, selectedJobId]);
const dmsTasks = Object.keys(pending?.projects?.dms?.task_defs || {});
return (
<>
<div className="grid-2">
<div className="panel">
<div className="panel-header"><h2>DMSYOLO//</h2></div>
<div className="panel-body">
<div className="crud-form">
<label className="field">
<span></span>
<select value={dmsTask} onChange={(e) => setDmsTask(e.target.value)}>
{(dmsTasks.length > 0 ? dmsTasks : [dmsTask]).map((t) => <option key={t} value={t}>{t}</option>)}
</select>
</label>
<label className="field">
<span></span>
<select value={dmsTrack} onChange={(e) => setDmsTrack(e.target.value as "platform" | "local")}>
<option value="platform">platform</option>
<option value="local">local</option>
</select>
</label>
<label className="field" style={{ gridColumn: "1 / -1" }}>
<span>/</span>
<input value={dmsWeights} onChange={(e) => setDmsWeights(e.target.value)} placeholder="/path/to/best.pt" />
</label>
</div>
<div className="audit-quick">
<button type="button" className="btn btn-sm btn-primary" onClick={() => submit("train_dms", { task: dmsTask, mode: "full", track: dmsTrack })}></button>
<button type="button" className="btn btn-sm btn-ghost" onClick={() => submit("eval_dms", { task: dmsTask, ...(dmsWeights ? { weights: dmsWeights } : {}) })}></button>
<button type="button" className="btn btn-sm btn-ghost" onClick={() => submit("visualize_dms", { task: dmsTask, ...(dmsWeights ? { weights: dmsWeights } : {}) })}></button>
</div>
</div>
</div>
<div className="panel">
<div className="panel-header"><h2>LaneUFLD/{LANE_DATA_VIZ_ENABLED ? "/可视化" : ""}</h2></div>
<div className="panel-body">
<div className="crud-form">
<label className="field">
<span></span>
<select value={laneTrack} onChange={(e) => setLaneTrack(e.target.value as "platform" | "local")}>
<option value="platform">platform</option>
<option value="local">local</option>
</select>
</label>
<label className="field">
<span>test_list</span>
<input value={laneTestList} onChange={(e) => setLaneTestList(e.target.value)} placeholder="list/test_gt.txt" />
</label>
<label className="field" style={{ gridColumn: "1 / -1" }}>
<span>/</span>
<input value={laneModelPath} onChange={(e) => setLaneModelPath(e.target.value)} placeholder="/path/to/best.pth" />
</label>
<label className="field" style={{ gridColumn: "1 / -1" }}>
<span>data_root</span>
<input value={laneDataRoot} onChange={(e) => setLaneDataRoot(e.target.value)} placeholder="/path/to/lane/dataset/root" />
</label>
</div>
<div className="audit-quick">
<button type="button" className="btn btn-sm btn-primary" onClick={() => submit("train_lane", { track: laneTrack })}></button>
<button type="button" className="btn btn-sm btn-ghost" onClick={() => submit("eval_lane", { model_path: laneModelPath, ...(laneDataRoot ? { data_root: laneDataRoot } : {}), test_list: laneTestList })}></button>
{LANE_DATA_VIZ_ENABLED && (
<button type="button" className="btn btn-sm btn-ghost" onClick={() => submit("visualize_lane", { model_path: laneModelPath, ...(laneDataRoot ? { data_root: laneDataRoot } : {}), test_list: laneTestList })}></button>
)}
</div>
</div>
</div>
</div>
<div className="panel">
<div className="panel-header"><h2></h2></div>
<div className="panel-body pack-list">
{packRows.map((p) => (
<div key={`${p.project}/${p.name}`} className={`pack-row ${p.enabled ? "active-pack" : ""}`}>
<span className="pack-name">{p.project}/{p.name}</span>
<span className={`badge ${p.enabled ? "badge-staged" : "badge-idle"}`}>{p.enabled ? "已启用" : "未启用"}</span>
<code className="text-sm">python as.py enable {p.project} {p.name}</code>
</div>
))}
</div>
</div>
<div className="panel">
<div className="panel-header"><h2></h2></div>
<div className="panel-body">
{milestones.length === 0 ? (
<div className="empty-state">/</div>
) : (
<div className="milestone-layout">
<div className="milestone-track">
{milestones.map((j) => {
const action = j.action as IterAction;
const meta = ACTION_META[action];
const weight = extractWeight(j);
return (
<button
type="button"
key={j.id}
className={`milestone-node ${selectedJob?.id === j.id ? "active" : ""}`}
onClick={() => setSelectedJobId(j.id)}
>
<div className="milestone-dot" />
<div className="milestone-body">
<div className="milestone-title">{meta?.label || j.action}</div>
<div className="milestone-sub">{fmtTime(j.started_at || j.created_at)}</div>
<div className="milestone-sub">
<span className={`badge ${statusBadgeClass(j.status)}`}>{j.status}</span>
<span>{meta?.project || "未知"}</span>
<span>{meta?.kind || "动作"}</span>
</div>
<div className="milestone-sub mono">{weight ? `权重: ${weight}` : "权重: —"}</div>
</div>
</button>
);
})}
</div>
{selectedJob && (
<div className="milestone-detail">
<div className="milestone-detail-head">
<h3>{ACTION_META[selectedJob.action as IterAction]?.label || selectedJob.action}</h3>
<span className={`badge ${statusBadgeClass(selectedJob.status)}`}>{selectedJob.status}</span>
</div>
<dl className="detail-dl">
<dt>Job ID</dt>
<dd className="mono">{selectedJob.id}</dd>
<dt></dt>
<dd>{fmtTime(selectedJob.created_at)}</dd>
<dt></dt>
<dd>{fmtTime(selectedJob.started_at)}</dd>
<dt></dt>
<dd>{fmtTime(selectedJob.finished_at)}</dd>
<dt></dt>
<dd className="path-dd">{extractWeight(selectedJob) || "—"}</dd>
<dt></dt>
<dd><pre className="params-pre">{JSON.stringify(selectedJob.params || {}, null, 2)}</pre></dd>
<dt></dt>
<dd><pre className="params-pre milestone-result">{JSON.stringify(selectedJob.result || {}, null, 2)}</pre></dd>
</dl>
</div>
)}
</div>
)}
</div>
</div>
</>
);
}