Files
Seg_Data_Server_Net/frontend/src/main.tsx
2026-06-30 17:33:15 +08:00

1734 lines
67 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useMemo, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import {
Activity,
BarChart3,
Bot,
Boxes,
ClipboardCheck,
Cpu,
Database,
FileImage,
FileSearch,
Gauge,
HardDrive,
Layers3,
Play,
RefreshCcw,
ShieldCheck,
Square,
Terminal,
UploadCloud,
Wand2,
Zap
} from "lucide-react";
import "./styles.css";
const API_BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8010";
type JobProgress = {
percent: number | null;
label: string;
stage: string;
current: number | null;
total: number | null;
unit: string | null;
source: string;
};
type Job = {
id: string;
type: string;
status: string;
description: string;
command?: string[];
cwd?: string;
pid?: number | null;
exit_code?: number | null;
error?: string | null;
created_at: string;
started_at?: string;
finished_at?: string;
log_path?: string;
log_tail?: string;
log_size?: number;
params: Record<string, unknown>;
progress?: JobProgress;
};
type Catalog = {
task_types: string[];
task_defaults: Record<string, Record<string, unknown>>;
segmodel_architectures: string[];
yolo_models: string[];
mmseg_algorithms: string[];
datasets: Array<{ name: string; path: string; source: string }>;
weights: { count: number; total_bytes: number; updated_at?: string };
};
type UploadedDataset = {
name: string;
description?: string;
absolute_layout?: Record<"images" | "labels" | "masks", string>;
layout?: Record<"images" | "labels" | "masks", string>;
counts: { images: number; labels: number; masks: number };
samples: Record<string, Array<{ name: string; relative_path: string; size: number; previewable: boolean }>>;
};
type DatasetValidation = {
dataset: string;
counts: { images: number; labels: number; masks: number; annotations: number };
pairs: {
image_label: number;
image_mask: number;
images_without_labels: string[];
labels_without_images: string[];
images_without_masks: string[];
masks_without_images: string[];
};
classes: number[];
checks: Array<{ name: string; passed: boolean; count?: number; labels?: number; masks?: number; errors?: unknown[] }>;
ready: { yolo: boolean; mask: boolean; any: boolean };
};
type ResultItem = {
name: string;
path: string;
relative_path: string;
size: number;
modified: number;
kind: string;
family?: string;
role?: string;
previewable?: boolean;
};
type CurveSeries = {
name: string;
points: Array<{ x: number; y: number }>;
last: number;
min: number;
max: number;
};
type TrainingCurve = {
name: string;
file_name: string;
relative_path: string;
modified: number;
family: string;
x_key: string;
row_count: number;
series: CurveSeries[];
};
type DatasetYoloOutputsPayload = {
bestWeight?: ResultItem;
artifacts: ResultItem[];
curves: TrainingCurve[];
predictions: ResultItem[];
heatmaps: ResultItem[];
};
type WeightEntry = {
source_path: string;
stored_path: string;
size: number;
family: string;
role: string;
sha256?: string;
};
type WeightManifest = {
generated_at?: string | null;
updated_at?: string | null;
source_root?: string;
count: number;
total_bytes: number;
files: WeightEntry[];
};
type WeightVerifyPayload = {
count: number;
ok_count: number;
items: Array<{ stored_path: string; exists: boolean; size_ok: boolean; hash_ok?: boolean | null; ok: boolean }>;
};
type CoveragePayload = {
scripts_total: number;
user_scripts_total: number;
mapped_user_scripts: number;
unmapped_user_scripts: string[];
task_build_passed: boolean;
task_build_checks: Array<{ task: string; passed: boolean; script_exists?: boolean; error?: string }>;
};
type AcceptancePayload = {
available?: boolean;
passed?: boolean;
run_id?: string;
created_at?: string;
checks?: Array<{ name: string; passed: boolean }>;
model_family_readiness?: {
passed: boolean;
warnings: Array<{ name: string; passed: boolean }>;
checks: Array<{ name: string; passed: boolean; required: boolean }>;
};
};
type DeepAcceptancePayload = {
available?: boolean;
passed?: boolean;
run_id?: string;
created_at?: string;
checks?: Array<{ name: string; passed: boolean }>;
};
type GpuPayload = {
available: boolean;
gpus: Array<{
index: number;
name: string;
memory_total_mb: number;
memory_used_mb: number;
memory_free_mb: number;
utilization_gpu_percent: number;
temperature_c: number;
}>;
};
type CondaEnvPayload = {
available: boolean;
envs: Array<{ name: string; path: string; active?: boolean }>;
task_default?: string;
mmseg_default?: string;
};
type RuntimeCheck = {
module: string;
package?: string;
passed: boolean;
version?: string | null;
error?: string;
};
type RuntimeEnv = {
role: string;
name: string;
label: string;
env_file: string;
path?: string;
exists: boolean;
passed: boolean;
checks: RuntimeCheck[];
extra: Record<string, unknown>;
};
type RuntimeReadinessPayload = {
available: boolean;
passed: boolean;
generated_at: string;
cached: boolean;
cache_seconds: number;
envs: RuntimeEnv[];
specs: {
bootstrap_script: string;
verify_script: string;
env_files: string[];
task_default: string;
mmseg_default: string;
};
};
type CapabilityDomain = {
id: string;
label: string;
ready: boolean;
tasks: {
total: number;
required: number;
required_ready: number;
examples: string[];
missing_required: string[];
unbuildable_required: string[];
};
runtime: Array<{ role: string; name: string; passed: boolean }>;
evidence: {
count: number;
artifacts: Array<{ name: string; relative_path: string; role?: string; family?: string }>;
curves: Array<{ name: string; relative_path: string; family?: string; row_count?: number }>;
};
gaps: string[];
};
type CapabilityPayload = {
passed: boolean;
generated_at: string;
summary: {
ready_domains: number;
total_domains: number;
mapped_user_scripts: number;
user_scripts_total: number;
uploaded_datasets: number;
artifacts: number;
curves: number;
weights: number;
};
requirements: Array<{ id: string; label: string; passed: boolean; detail: string }>;
domains: CapabilityDomain[];
};
type AgentCheck = {
name: string;
passed: boolean;
detail?: unknown;
details?: unknown;
};
type EvaluationAgentPayload = {
agent: string;
score: number;
checks: AgentCheck[];
suggestions: string[];
};
type ValidationAgentPayload = {
agent: string;
passed: boolean;
checks: AgentCheck[];
};
async function api<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
headers: { "Content-Type": "application/json" },
...init
});
if (!res.ok) throw new Error(await res.text());
return res.json();
}
const defaultParams: Record<string, Record<string, unknown>> = {
"mock.echo": { message: "hello from Seg Data Server" },
"dataset.rename": { image_dir: "../DataSet_Own/ORI", label_dir: "../DataSet_Own/Label" },
"dataset.to_png": { input_dir: "../DataSet_Own/ORI", output_dir: "../DataSet_Own/ORI_PNG" },
"dataset.resize": { image_dir: "../DataSet_Own/ORI", label_dir: "../DataSet_Own/Label", width: 1920, height: 1080 },
"dataset.pair": { image_dir: "../DataSet_Own/ORI", label_dir: "../DataSet_Own/Label" },
"dataset.rebuild_labels": { label_dir: "../DataSet_Own/Label" },
"dataset.stack": { image_dir: "../DataSet_Own/ORI", label_dir: "../DataSet_Own/Label", result_dir: "../DataSet_Own/stacked", alpha: 0.3 },
"dataset.stitch": { image_dir: "../DataSet_Own/ORI", label_dir: "../DataSet_Own/Label", result_dir: "../DataSet_Own/stitch" },
"dataset.video_frames": { video: "../Seg_Predict_Own_Video_V2/LC_Video_1.mp4", interval: 0.5, resize: "1920x1080" },
"segmodel.train": { architecture: "Unet" },
"segmodel.predict": { architecture: "Unet", run_choice: 1 },
"yolo.train": { model: "YOLOv8n-seg" },
"yolo.train_custom": { model: "YOLO11n-seg", data: "var/uploads/datasets/example/dataset.yaml", epochs: 10, imgsz: 640, batch: 1, workers: 0, device: "cpu", exist_ok: true },
"yolo.predict": { model: "YOLOv8n-seg", pt_name: "best.pt", conf: 0.2, run_choice: 1 },
"yolo.predict_custom": { weights: "var/custom_yolo_runs/example/weights/best.pt", source: "var/uploads/datasets/example/images", imgsz: 640, conf: 0.25, device: "cpu", name: "example_predict", exist_ok: true },
"yolo.heatmap": { model: "YOLOv8n-seg", cam_method: "All", pt_name: "best.pt", run_choice: 1 },
"yolo.heatmap_custom": { weights: "var/custom_yolo_runs/example/weights/best.pt", source: "var/uploads/datasets/example/images", model_key: "YOLO11n-seg", cam_method: "GradCAM", target_layers: "model.model.model[9]", limit: 3, name: "example_heatmap" },
"mmseg.generate_alg": { dataset_choice: 1, gpu_count: 1, gpu_ids: [0], schedule_mode: 2, max_epochs: 300, algorithm_choice: 1 },
"mmseg.train": { config: "configs/example.py", work_dir: "../DataSet_Public_outputs/example" },
"mmseg.metrics": { input_dir: "../Hardisk", output_dir: "../BestMode_Predict_Results_DataSet_Public", dataset_choice: 1, algorithm_choice: 0 },
"mmseg.flops_fps": { input_dir: "../Hardisk", output_dir: "../BestMode_Predict_Results_DataSet_Public", repeat_times: 3, dataset_choice: 1, algorithm_choice: 0 },
"analysis.all": { input_dir: "../BestMode_Predict_Results_DataSet_Public", output_dir: "./", dataset_choice: 1 }
};
const taskLabels: Record<string, string> = {
"dataset.rename": "重命名",
"dataset.to_png": "转 PNG",
"dataset.resize": "Resize",
"dataset.pair": "图片/Label 配对",
"dataset.rebuild_labels": "重建 Label",
"dataset.stack": "透明叠加",
"dataset.stitch": "拼接检查",
"dataset.video_frames": "视频抽帧",
"dataset.yolo_check_pairs": "YOLO 配对",
"dataset.yolo_stack": "YOLO 叠加",
"dataset.yolo_rebuild_labels": "YOLO Label",
"dataset.yolo_txt_sort": "生成 TXT",
"dataset.yolo_convert_png": "批量 PNG",
"dataset.yolo_resize": "批量缩放"
};
function formatBytes(value?: number) {
if (!value) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
let next = value;
let unit = 0;
while (next >= 1024 && unit < units.length - 1) {
next /= 1024;
unit += 1;
}
return `${next.toFixed(unit > 1 ? 2 : 0)} ${units[unit]}`;
}
function useData() {
const [catalog, setCatalog] = useState<Catalog | null>(null);
const [gpus, setGpus] = useState<GpuPayload | null>(null);
const [condaEnvs, setCondaEnvs] = useState<CondaEnvPayload | null>(null);
const [jobs, setJobs] = useState<Job[]>([]);
const [results, setResults] = useState<ResultItem[]>([]);
const [curves, setCurves] = useState<TrainingCurve[]>([]);
const [weightManifest, setWeightManifest] = useState<WeightManifest | null>(null);
const [datasets, setDatasets] = useState<UploadedDataset[]>([]);
const [datasetValidations, setDatasetValidations] = useState<Record<string, DatasetValidation>>({});
const [coverage, setCoverage] = useState<CoveragePayload | null>(null);
const [acceptance, setAcceptance] = useState<AcceptancePayload | null>(null);
const [realAcceptance, setRealAcceptance] = useState<AcceptancePayload | null>(null);
const [deepAcceptance, setDeepAcceptance] = useState<DeepAcceptancePayload | null>(null);
const [runtimeReadiness, setRuntimeReadiness] = useState<RuntimeReadinessPayload | null>(null);
const [capabilities, setCapabilities] = useState<CapabilityPayload | null>(null);
const [agentEvaluation, setAgentEvaluation] = useState<EvaluationAgentPayload | null>(null);
const [error, setError] = useState<string>("");
async function refresh() {
try {
const [catalogNext, gpusNext, envsNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, weightsNext, datasetsNext, coverageNext, acceptanceNext, realAcceptanceNext, deepAcceptanceNext, agentEvaluationNext] = await Promise.all([
api<Catalog>("/api/catalog"),
api<GpuPayload>("/api/system/gpus"),
api<CondaEnvPayload>("/api/system/envs"),
api<RuntimeReadinessPayload>("/api/system/readiness"),
api<CapabilityPayload>("/api/capabilities"),
api<Job[]>("/api/jobs"),
api<ResultItem[]>("/api/results?limit=1000"),
api<TrainingCurve[]>("/api/results/curves?limit=100"),
api<WeightManifest>("/api/weights"),
api<UploadedDataset[]>("/api/datasets"),
api<CoveragePayload>("/api/coverage"),
api<AcceptancePayload>("/api/acceptance/latest"),
api<AcceptancePayload>("/api/acceptance/real/latest"),
api<DeepAcceptancePayload>("/api/acceptance/deep/latest"),
api<EvaluationAgentPayload>("/api/agents/evaluate")
]);
setCatalog(catalogNext);
setGpus(gpusNext);
setCondaEnvs(envsNext);
setRuntimeReadiness(readinessNext);
setCapabilities(capabilitiesNext);
setJobs(jobsNext);
setResults(resultsNext);
setCurves(curvesNext);
setWeightManifest(weightsNext);
setDatasets(datasetsNext);
const validationEntries: Array<[string, DatasetValidation]> = [];
await Promise.all(
datasetsNext.map(async (dataset) => {
try {
const validation = await api<DatasetValidation>(`/api/datasets/${encodeURIComponent(dataset.name)}/validate`);
validationEntries.push([dataset.name, validation]);
} catch {
// Dataset validation is advisory; upload and job controls should remain usable.
}
})
);
setDatasetValidations(Object.fromEntries(validationEntries));
setCoverage(coverageNext);
setAcceptance(acceptanceNext);
setRealAcceptance(realAcceptanceNext);
setDeepAcceptance(deepAcceptanceNext);
setAgentEvaluation(agentEvaluationNext);
setError("");
} catch (err) {
setError(String(err));
}
}
useEffect(() => {
refresh();
const timer = window.setInterval(refresh, 5000);
return () => window.clearInterval(timer);
}, []);
return { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, deepAcceptance, error, refresh };
}
function StatusPill({ status }: { status: string }) {
return <span className={`pill pill-${status}`}>{status}</span>;
}
function JobProgressBar({ progress }: { progress?: JobProgress }) {
const percent = typeof progress?.percent === "number" ? Math.max(0, Math.min(100, progress.percent)) : 0;
return (
<div className="progressBox" data-stage={progress?.stage ?? "unknown"}>
<div className="progressTrack" aria-label={progress?.label ?? "job progress"}>
<span style={{ width: `${percent}%` }} />
</div>
<div className="progressMeta">
<span>{progress?.label ?? "等待日志"}</span>
<strong>{typeof progress?.percent === "number" ? `${progress.percent.toFixed(progress.percent % 1 ? 1 : 0)}%` : "..."}</strong>
</div>
</div>
);
}
function App() {
const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, deepAcceptance, error, refresh } = useData();
const [taskType, setTaskType] = useState("mock.echo");
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
const [log, setLog] = useState("");
const [busy, setBusy] = useState(false);
const [datasetName, setDatasetName] = useState("demo_dataset");
const [datasetDescription, setDatasetDescription] = useState("");
const [selectedDatasetName, setSelectedDatasetName] = useState("");
const [selectedCurvePath, setSelectedCurvePath] = useState("");
const [resultFamilyFilter, setResultFamilyFilter] = useState("all");
const [resultRoleFilter, setResultRoleFilter] = useState("all");
const [selectedGpuIds, setSelectedGpuIds] = useState<number[]>([]);
const [selectedCondaEnv, setSelectedCondaEnv] = useState("auto");
const [uploadKind, setUploadKind] = useState<"images" | "labels" | "masks">("images");
const [uploadFiles, setUploadFiles] = useState<FileList | null>(null);
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
const [weightVerification, setWeightVerification] = useState<WeightVerifyPayload | null>(null);
const [agentBusy, setAgentBusy] = useState(false);
const eventSourceRef = useRef<EventSource | null>(null);
useEffect(() => () => {
eventSourceRef.current?.close();
}, []);
const runningCount = jobs.filter((job) => job.status === "running").length;
const successCount = jobs.filter((job) => job.status === "success").length;
const failedCount = jobs.filter((job) => job.status === "failed").length;
const taskGroups = useMemo<Record<string, string[]>>(() => {
const items = catalog?.task_types ?? [];
return {
dataset: items.filter((task) => task.startsWith("dataset.")),
segmodel: items.filter((task) => task.startsWith("segmodel.")),
yolo: items.filter((task) => task.startsWith("yolo.")),
visual: items.filter((task) => task.startsWith("visual.")),
mmseg: items.filter((task) => task.startsWith("mmseg.")),
analysis: items.filter((task) => task.startsWith("analysis.") || task.startsWith("system.") || task.startsWith("mock."))
};
}, [catalog]);
const datasetOps = taskGroups.dataset.filter((task) => task in taskLabels);
const selectedDataset = useMemo(
() => datasets.find((dataset) => dataset.name === selectedDatasetName) ?? datasets.find((dataset) => dataset.name === datasetName),
[datasetName, datasets, selectedDatasetName]
);
const selectedValidation = selectedDataset ? datasetValidations[selectedDataset.name] : undefined;
const selectedCurve = curves.find((curve) => curve.relative_path === selectedCurvePath) ?? curves[0];
const selectedYoloOutputs = useMemo<DatasetYoloOutputsPayload>(() => {
if (!selectedDataset) {
return { artifacts: [], curves: [], predictions: [], heatmaps: [] };
}
const prefixes = [
`var/custom_yolo_runs/${selectedDataset.name}`,
`var/custom_yolo_runs/${selectedDataset.name}_predict`,
`var/custom_yolo_runs/${selectedDataset.name}_heatmap`
];
const matches = (relativePath: string) => prefixes.some((prefix) => relativePath === prefix || relativePath.startsWith(`${prefix}/`));
const artifacts = results.filter((item) => matches(item.relative_path));
return {
bestWeight: artifacts.find((item) => item.relative_path.endsWith("/weights/best.pt")),
artifacts,
curves: curves.filter((curve) => matches(curve.relative_path)),
predictions: artifacts.filter((item) => item.role === "segmentation" && item.previewable),
heatmaps: artifacts.filter((item) => item.role === "heatmap" && item.previewable)
};
}, [curves, results, selectedDataset]);
const selectedYoloWeightReady = Boolean(selectedYoloOutputs.bestWeight);
const availableGpus = gpus?.gpus ?? [];
const condaEnvOptions = useMemo(() => ["auto", ...Array.from(new Set((condaEnvs?.envs ?? []).map((item) => item.name))).sort()], [condaEnvs]);
const selectedGpuDevice = selectedGpuIds.length ? selectedGpuIds.map((_, index) => index).join(",") : "cpu";
const resultFamilyOptions = useMemo(() => ["all", ...Array.from(new Set(results.map((item) => item.family ?? "artifact"))).sort()], [results]);
const resultRoleOptions = useMemo(() => ["all", ...Array.from(new Set(results.map((item) => item.role ?? "artifact"))).sort()], [results]);
const filteredResults = useMemo(
() => results.filter((item) =>
(resultFamilyFilter === "all" || (item.family ?? "artifact") === resultFamilyFilter) &&
(resultRoleFilter === "all" || (item.role ?? "artifact") === resultRoleFilter)
),
[resultFamilyFilter, resultRoleFilter, results]
);
function pickTask(next: string) {
setTaskType(next);
setParams(JSON.stringify(catalog?.task_defaults?.[next] ?? defaultParams[next] ?? {}, null, 2));
}
function pickDatasetTask(next: string) {
setTaskType(next);
setParams(JSON.stringify(datasetParamsForTask(next), null, 2));
window.location.hash = "jobs";
}
function toggleGpu(index: number) {
setSelectedGpuIds((current) => current.includes(index) ? current.filter((item) => item !== index) : [...current, index].sort((a, b) => a - b));
}
function paramsWithGpuSelection(type: string, rawParams: Record<string, unknown>) {
const next = { ...rawParams };
if (!selectedGpuIds.length) return next;
if (type.startsWith("yolo.") || type.startsWith("visual.")) {
next.device = selectedGpuDevice;
}
if (type.startsWith("mmseg.generate_alg")) {
next.gpu_count = selectedGpuIds.length;
next.gpu_ids = selectedGpuIds;
}
return next;
}
function jobPayload(type: string, rawParams: Record<string, unknown>) {
const payload: { type: string; params: Record<string, unknown>; gpus?: number[]; conda_env?: string } = {
type,
params: paramsWithGpuSelection(type, rawParams)
};
if (selectedGpuIds.length) payload.gpus = selectedGpuIds;
if (selectedCondaEnv !== "auto") payload.conda_env = selectedCondaEnv;
return payload;
}
function datasetParamsForTask(next: string): Record<string, unknown> {
const base = { ...(catalog?.task_defaults?.[next] ?? defaultParams[next] ?? {}) };
const layout = selectedDataset?.absolute_layout;
if (!layout) return base;
const resultDir = `${layout.images.replace(/\/images$/, "")}/results/${next.replace(".", "_")}`;
if (["dataset.pair", "dataset.resize", "dataset.stack", "dataset.stitch", "dataset.yolo_check_pairs", "dataset.yolo_stack"].includes(next)) {
return { ...base, image_dir: layout.images, label_dir: layout.labels, result_dir: resultDir };
}
if (["dataset.rebuild_labels", "dataset.yolo_rebuild_labels", "dataset.yolo_txt_sort"].includes(next)) {
return { ...base, label_dir: layout.labels, folder: layout.labels };
}
if (["dataset.to_png", "dataset.yolo_convert_png", "dataset.yolo_resize"].includes(next)) {
return { ...base, input_dir: layout.images, output_dir: resultDir, folder: layout.images };
}
return base;
}
async function createJob() {
setBusy(true);
try {
const job = await api<Job>("/api/jobs", {
method: "POST",
body: JSON.stringify(jobPayload(taskType, JSON.parse(params) as Record<string, unknown>))
});
await inspectJob(job);
await refresh();
} finally {
setBusy(false);
}
}
async function syncWeights() {
setBusy(true);
try {
await api("/api/weights/sync", {
method: "POST",
body: JSON.stringify({ mode: "copy", hash_files: true, skip_existing: true })
});
setWeightVerification(null);
await refresh();
} finally {
setBusy(false);
}
}
async function verifyWeights() {
setBusy(true);
try {
const result = await api<WeightVerifyPayload>("/api/weights/verify", { method: "POST" });
setWeightVerification(result);
} finally {
setBusy(false);
}
}
async function runAcceptanceSmoke() {
setBusy(true);
try {
await api("/api/acceptance/smoke", { method: "POST" });
await refresh();
} finally {
setBusy(false);
}
}
async function runDeepAcceptance() {
setBusy(true);
try {
await api("/api/acceptance/deep", { method: "POST" });
await refresh();
} finally {
setBusy(false);
}
}
async function runRealAcceptance() {
setBusy(true);
try {
await api("/api/acceptance/real", { method: "POST" });
await refresh();
} finally {
setBusy(false);
}
}
async function runAgentValidation() {
setAgentBusy(true);
try {
const result = await api<ValidationAgentPayload>("/api/agents/validate?run_build=false&run_acceptance=false&run_deep=false");
setAgentValidation(result);
} finally {
setAgentBusy(false);
}
}
async function createDataset() {
setBusy(true);
try {
await api("/api/datasets", {
method: "POST",
body: JSON.stringify({ name: datasetName, description: datasetDescription })
});
setSelectedDatasetName(datasetName);
await refresh();
} finally {
setBusy(false);
}
}
async function uploadDatasetFiles() {
if (!uploadFiles || uploadFiles.length === 0) return;
setBusy(true);
try {
const body = new FormData();
Array.from(uploadFiles).forEach((file) => body.append("files", file));
const res = await fetch(`${API_BASE}/api/datasets/${encodeURIComponent(datasetName)}/upload/${uploadKind}`, {
method: "POST",
body
});
if (!res.ok) throw new Error(await res.text());
await refresh();
} finally {
setBusy(false);
}
}
function customYoloWeightPath(dataset: UploadedDataset) {
const expected = `var/custom_yolo_runs/${dataset.name}/weights/best.pt`;
return selectedYoloOutputs.bestWeight?.relative_path ?? results.find((item) => item.relative_path === expected || item.relative_path.endsWith(`/custom_yolo_runs/${dataset.name}/weights/best.pt`))?.relative_path ?? expected;
}
async function createSelectedYoloYaml() {
if (!selectedDataset) return;
const classNames = selectedValidation?.classes.map((classId) => `class_${classId}`) ?? undefined;
return api<{ relative_path: string; path: string }>(`/api/datasets/${encodeURIComponent(selectedDataset.name)}/yolo-yaml`, {
method: "POST",
body: JSON.stringify({ class_names: classNames })
});
}
async function generateSelectedYoloYaml() {
if (!selectedDataset) return;
setBusy(true);
try {
const generated = await createSelectedYoloYaml();
if (!generated) return;
setTaskType("yolo.train_custom");
setParams(JSON.stringify(paramsWithGpuSelection("yolo.train_custom", { model: "YOLO11n-seg", data: generated.path, epochs: 10, imgsz: 640, batch: 1, workers: 0, device: selectedGpuDevice, project: "var/custom_yolo_runs", name: selectedDataset.name, exist_ok: true }), null, 2));
await refresh();
} finally {
setBusy(false);
}
}
async function startSelectedYoloTrain() {
if (!selectedDataset) return;
setBusy(true);
try {
const generated = await createSelectedYoloYaml();
if (!generated) return;
const job = await api<Job>("/api/jobs", {
method: "POST",
body: JSON.stringify(jobPayload("yolo.train_custom", { model: "YOLO11n-seg", data: generated.path, epochs: 10, imgsz: 640, batch: 1, workers: 0, device: selectedGpuDevice, project: "var/custom_yolo_runs", name: selectedDataset.name, exist_ok: true }))
});
await inspectJob(job);
window.location.hash = "jobs";
await refresh();
} finally {
setBusy(false);
}
}
async function startSelectedYoloPredict() {
if (!selectedDataset?.absolute_layout) return;
setBusy(true);
try {
const job = await api<Job>("/api/jobs", {
method: "POST",
body: JSON.stringify(jobPayload("yolo.predict_custom", { weights: customYoloWeightPath(selectedDataset), source: selectedDataset.absolute_layout.images, imgsz: 640, conf: 0.25, device: selectedGpuDevice, project: "var/custom_yolo_runs", name: `${selectedDataset.name}_predict`, exist_ok: true }))
});
await inspectJob(job);
window.location.hash = "jobs";
await refresh();
} finally {
setBusy(false);
}
}
async function startSelectedYoloHeatmap() {
if (!selectedDataset?.absolute_layout) return;
setBusy(true);
try {
const job = await api<Job>("/api/jobs", {
method: "POST",
body: JSON.stringify(jobPayload("yolo.heatmap_custom", { weights: customYoloWeightPath(selectedDataset), source: selectedDataset.absolute_layout.images, model_key: "YOLO11n-seg", cam_method: "GradCAM", target_layers: "model.model.model[9]", limit: 3, device: selectedGpuDevice, project: "var/custom_yolo_runs", name: `${selectedDataset.name}_heatmap` }))
});
await inspectJob(job);
window.location.hash = "jobs";
await refresh();
} finally {
setBusy(false);
}
}
async function inspectJob(job: Job) {
eventSourceRef.current?.close();
const detail = await api<Job>(`/api/jobs/${job.id}`);
setSelectedJob(detail);
setLog(detail.log_tail ?? "");
const source = new EventSource(`${API_BASE}/api/jobs/${job.id}/events?offset=${detail.log_size ?? 0}`);
eventSourceRef.current = source;
source.onmessage = (event) => {
const payload = JSON.parse(event.data);
if (payload.chunk) setLog((prev) => `${prev}${payload.chunk}`);
setSelectedJob(payload.job);
if (["success", "failed", "cancelled"].includes(payload.job.status)) {
source.close();
if (eventSourceRef.current === source) eventSourceRef.current = null;
}
};
source.onerror = () => {
source.close();
if (eventSourceRef.current === source) eventSourceRef.current = null;
};
}
async function cancelSelectedJob() {
if (!selectedJob) return;
await api(`/api/jobs/${selectedJob.id}/cancel`, { method: "POST" });
await refresh();
}
return (
<main className="shell">
<aside className="rail">
<div className="brand">
<div className="mark"><Layers3 size={24} /></div>
<div>
<strong>Seg Data Server</strong>
<span>Net Console</span>
</div>
</div>
<nav>
<a href="#jobs"><Terminal size={18} /></a>
<a href="#capabilities"><Gauge size={18} /></a>
<a href="#datasets"><Boxes size={18} /></a>
<a href="#gpu"><Cpu size={18} />GPU</a>
<a href="#runtime"><ShieldCheck size={18} /></a>
<a href="#coverage"><ClipboardCheck size={18} /></a>
<a href="#agents"><Bot size={18} />Agent</a>
<a href="#weights"><HardDrive size={18} /></a>
<a href="#results"><BarChart3 size={18} /></a>
</nav>
</aside>
<section className="workspace">
<header className="topbar">
<div>
<p className="eyebrow">Segmentation Operations</p>
<h1></h1>
</div>
<button className="iconButton" onClick={refresh} title="刷新">
<RefreshCcw size={18} />
</button>
</header>
{error && <div className="alert">{error}</div>}
<section className="metrics">
<div className="metric">
<Activity size={20} />
<span></span>
<strong>{runningCount}</strong>
</div>
<div className="metric">
<ShieldCheck size={20} />
<span></span>
<strong>{successCount}</strong>
</div>
<div className="metric">
<Zap size={20} />
<span></span>
<strong>{failedCount}</strong>
</div>
<div className="metric">
<Database size={20} />
<span></span>
<strong>{datasets.length}</strong>
</div>
<div className="metric">
<ClipboardCheck size={20} />
<span></span>
<strong>{catalog?.task_types.length ?? 0}</strong>
</div>
</section>
<section className="panel" id="capabilities">
<div className="panelHead">
<div>
<p className="eyebrow">Capability Matrix</p>
<h2></h2>
</div>
<StatusPill status={capabilities?.passed ? "success" : "queued"} />
</div>
<div className="capSummary">
<div><span></span><strong>{capabilities?.summary.ready_domains ?? 0}/{capabilities?.summary.total_domains ?? 0}</strong></div>
<div><span></span><strong>{capabilities?.summary.mapped_user_scripts ?? 0}/{capabilities?.summary.user_scripts_total ?? 0}</strong></div>
<div><span></span><strong>{capabilities?.summary.uploaded_datasets ?? 0}</strong></div>
<div><span></span><strong>{capabilities?.summary.artifacts ?? 0}</strong></div>
<div><span>线</span><strong>{capabilities?.summary.curves ?? 0}</strong></div>
<div><span></span><strong>{capabilities?.summary.weights ?? 0}</strong></div>
</div>
<div className="capabilityGrid">
{(capabilities?.domains ?? []).map((domain) => (
<div key={domain.id} className={`capCard ${domain.ready ? "ok" : "bad"}`}>
<div className="capHead">
<strong>{domain.label}</strong>
<span>{domain.ready ? "READY" : "CHECK"}</span>
</div>
<div className="capNumbers">
<span>{domain.tasks.required_ready}/{domain.tasks.required} required</span>
<span>{domain.evidence.count} evidence</span>
</div>
<div className="capTags">
{domain.tasks.examples.slice(0, 4).map((task) => <small key={task}>{task}</small>)}
</div>
</div>
))}
</div>
<div className="requirementStrip">
{(capabilities?.requirements ?? []).map((item) => (
<span key={item.id} className={item.passed ? "ok" : "bad"} title={item.detail}>
{item.label}
</span>
))}
</div>
</section>
<section className="grid two" id="jobs">
<div className="panel taskPanel">
<div className="panelHead">
<div>
<p className="eyebrow">Job Builder</p>
<h2></h2>
</div>
<button className="primary" disabled={busy} onClick={createJob}>
<Play size={17} />
</button>
</div>
<div className="taskColumns">
{Object.entries(taskGroups).map(([group, values]) => (
<div key={group} className="taskGroup">
<span>{group}</span>
{values.map((task) => (
<button key={task} className={task === taskType ? "selected" : ""} onClick={() => pickTask(task)}>
{task}
</button>
))}
</div>
))}
</div>
<div className="gpuSelector">
<div>
<span>GPU </span>
<strong>{selectedGpuIds.length ? selectedGpuIds.map((item) => `GPU ${item}`).join(", ") : "CPU"}</strong>
</div>
<div className="gpuPicker">
<button type="button" className={!selectedGpuIds.length ? "selected" : ""} onClick={() => setSelectedGpuIds([])}>
<Cpu size={14} />
<span>CPU</span>
</button>
{availableGpus.map((gpu) => (
<button key={gpu.index} type="button" className={selectedGpuIds.includes(gpu.index) ? "selected" : ""} onClick={() => toggleGpu(gpu.index)}>
<Cpu size={14} />
<span>GPU {gpu.index}</span>
<small>{gpu.memory_free_mb} MB</small>
</button>
))}
</div>
</div>
<div className="runtimeSelector">
<label>
<span>Conda </span>
<select value={selectedCondaEnv} onChange={(event) => setSelectedCondaEnv(event.target.value)} aria-label="conda environment">
{condaEnvOptions.map((name) => (
<option key={name} value={name}>
{name === "auto" ? `Auto (${taskType.startsWith("mmseg.") ? condaEnvs?.mmseg_default ?? "seg_mmcv" : condaEnvs?.task_default ?? "seg_smp"})` : name}
</option>
))}
</select>
</label>
</div>
<label className="field">
<span> JSON</span>
<textarea value={params} onChange={(event) => setParams(event.target.value)} />
</label>
</div>
<div className="panel">
<div className="panelHead">
<div>
<p className="eyebrow">Queue</p>
<h2></h2>
</div>
<Gauge size={22} />
</div>
<div className="jobList">
{jobs.slice(0, 12).map((job) => (
<button key={job.id} className="jobRow" onClick={() => inspectJob(job)}>
<div className="jobRowTop">
<span>{job.type}</span>
<StatusPill status={job.status} />
</div>
<small>{job.description || job.id.slice(0, 8)}</small>
<JobProgressBar progress={job.progress} />
</button>
))}
</div>
</div>
</section>
<section className="grid two" id="datasets">
<div className="panel">
<div className="panelHead">
<div>
<p className="eyebrow">Dataset Bench</p>
<h2>LabelMask </h2>
</div>
<Database size={22} />
</div>
<div className="datasetForm">
<label className="field compact">
<span></span>
<input value={datasetName} onChange={(event) => setDatasetName(event.target.value)} />
</label>
<label className="field compact">
<span></span>
<input value={datasetDescription} onChange={(event) => setDatasetDescription(event.target.value)} />
</label>
<div className="segmented">
{(["images", "labels", "masks"] as const).map((kind) => (
<button key={kind} className={uploadKind === kind ? "active" : ""} onClick={() => setUploadKind(kind)}>
{kind}
</button>
))}
</div>
<label className="drop">
<UploadCloud size={24} />
<span>{uploadFiles?.length ? `${uploadFiles.length} files selected` : "选择图片、label 或 mask 文件"}</span>
<input multiple type="file" accept="image/*,.txt,.json,.yaml,.yml" onChange={(event) => setUploadFiles(event.target.files)} />
</label>
<div className="buttonRow">
<button className="primary" disabled={busy} onClick={createDataset}><Boxes size={17} /></button>
<button className="primary secondary" disabled={busy || !uploadFiles?.length} onClick={uploadDatasetFiles}><UploadCloud size={17} /></button>
</div>
<div className="opGrid">
{datasetOps.map((task) => (
<button key={task} type="button" onClick={() => pickDatasetTask(task)}>
<Wand2 size={16} />
<span>{taskLabels[task]}</span>
</button>
))}
</div>
</div>
</div>
<div className="panel">
<div className="panelHead">
<div>
<p className="eyebrow">Files</p>
<h2></h2>
</div>
<div className="buttonRow compactButtons">
<button className="iconButton" disabled={busy || !selectedValidation?.ready.yolo} onClick={generateSelectedYoloYaml} title="生成 YOLO dataset.yaml">
<FileSearch size={18} />
</button>
<button className="iconButton" disabled={busy || !selectedValidation?.ready.yolo} onClick={startSelectedYoloTrain} title="启动自定义 YOLO 训练">
<Play size={18} />
</button>
<button className="iconButton" disabled={busy || !selectedDataset?.absolute_layout || !selectedYoloWeightReady} onClick={startSelectedYoloPredict} title={selectedYoloWeightReady ? "使用自定义 best.pt 预测" : "best.pt 尚未生成"}>
<FileImage size={18} />
</button>
<button className="iconButton" disabled={busy || !selectedDataset?.absolute_layout || !selectedYoloWeightReady} onClick={startSelectedYoloHeatmap} title={selectedYoloWeightReady ? "使用自定义 best.pt 生成热度图" : "best.pt 尚未生成"}>
<Zap size={18} />
</button>
<FileImage size={22} />
</div>
</div>
<div className="datasetList">
{datasets.map((dataset) => (
<div key={dataset.name}>
<div
className={`datasetCard ${selectedDataset?.name === dataset.name ? "selected" : ""}`}
role="button"
tabIndex={0}
onClick={() => {
setDatasetName(dataset.name);
setSelectedDatasetName(dataset.name);
}}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
setDatasetName(dataset.name);
setSelectedDatasetName(dataset.name);
}
}}
>
<div className="datasetCardHead">
<strong>{dataset.name}</strong>
<span>{dataset.counts.images} image · {dataset.counts.labels} label · {dataset.counts.masks} mask</span>
</div>
<div className="readinessLine">
<StatusPill status={datasetValidations[dataset.name]?.ready.yolo ? "success" : "queued"} />
<small>YOLO {datasetValidations[dataset.name]?.pairs.image_label ?? 0} pair · Mask {datasetValidations[dataset.name]?.pairs.image_mask ?? 0} pair</small>
</div>
<div className="sampleStrip">
{["images", "labels", "masks"].flatMap((kind) =>
(dataset.samples[kind] ?? []).slice(0, 4).map((sample) => (
<a key={`${kind}-${sample.relative_path}`} href={`${API_BASE}/api/artifacts/${sample.relative_path}`} target="_blank" rel="noreferrer">
<span>{kind}</span>
<small>{sample.name}</small>
</a>
))
)}
</div>
</div>
</div>
))}
</div>
{selectedValidation && <DatasetQuality validation={selectedValidation} />}
{selectedDataset && <DatasetYoloOutputs dataset={selectedDataset} outputs={selectedYoloOutputs} />}
</div>
</section>
<section className="grid two" id="coverage">
<div className="panel">
<div className="panelHead">
<div>
<p className="eyebrow">Coverage</p>
<h2>Seg </h2>
</div>
<div className="buttonRow compactButtons">
<button className="iconButton" disabled={busy} onClick={runAcceptanceSmoke} title="运行轻量验收">
<ClipboardCheck size={18} />
</button>
<button className="iconButton" disabled={busy} onClick={runRealAcceptance} title="运行真实数据验收">
<FileSearch size={18} />
</button>
<button className="iconButton" disabled={busy} onClick={runDeepAcceptance} title="运行深度训练验收">
<Activity size={18} />
</button>
</div>
</div>
<div className="coverageGrid">
<div>
<span></span>
<strong>{coverage?.mapped_user_scripts ?? 0}/{coverage?.user_scripts_total ?? 0}</strong>
</div>
<div>
<span></span>
<strong>{coverage?.scripts_total ?? 0}</strong>
</div>
<div>
<span></span>
<strong>{coverage?.task_build_passed ? "OK" : "Check"}</strong>
</div>
<div>
<span></span>
<strong>{acceptance?.available === false ? "New" : acceptance?.passed ? "OK" : "Check"}</strong>
</div>
<div>
<span></span>
<strong>{acceptance?.model_family_readiness?.passed ? "OK" : "Check"}</strong>
</div>
<div>
<span></span>
<strong>{realAcceptance?.available === false ? "New" : realAcceptance?.passed ? "OK" : "Check"}</strong>
</div>
<div>
<span></span>
<strong>{deepAcceptance?.available === false ? "New" : deepAcceptance?.passed ? "OK" : "Check"}</strong>
</div>
</div>
<div className="coverageStatus">
{(coverage?.unmapped_user_scripts.length ?? 0) === 0 ? (
<>
<span></span>
<span>{acceptance?.created_at ?? "尚未运行"} {acceptance?.run_id ? `#${acceptance.run_id}` : ""}</span>
<span>{realAcceptance?.created_at ?? "尚未运行"} {realAcceptance?.run_id ? `#${realAcceptance.run_id}` : ""} {realAcceptance?.checks?.filter((item) => item.passed).length ?? 0}/{realAcceptance?.checks?.length ?? 0}</span>
<span>{deepAcceptance?.created_at ?? "尚未运行"} {deepAcceptance?.run_id ? `#${deepAcceptance.run_id}` : ""} {deepAcceptance?.checks?.filter((item) => item.passed).length ?? 0}/{deepAcceptance?.checks?.length ?? 0}</span>
<span> readiness{acceptance?.model_family_readiness?.checks?.filter((item) => item.passed).length ?? 0}/{acceptance?.model_family_readiness?.checks?.length ?? 0}warnings {acceptance?.model_family_readiness?.warnings?.length ?? 0}</span>
</>
) : (
coverage?.unmapped_user_scripts.slice(0, 8).map((item) => <code key={item}>{item}</code>)
)}
</div>
</div>
<div className="panel">
<div className="panelHead">
<div>
<p className="eyebrow">Buildability</p>
<h2></h2>
</div>
<Terminal size={22} />
</div>
<div className="taskCheckList">
{(coverage?.task_build_checks ?? []).slice(0, 28).map((item) => (
<div key={item.task} className={item.passed ? "ok" : "bad"}>
<span>{item.task}</span>
<small>{item.passed ? "command ready" : item.error ?? "check failed"}</small>
</div>
))}
</div>
</div>
</section>
<section className="grid two" id="agents">
<div className="panel agentPanel">
<div className="panelHead">
<div>
<p className="eyebrow">Evaluation Agent</p>
<h2></h2>
</div>
<StatusPill status={(agentEvaluation?.score ?? 0) >= 1 ? "success" : "queued"} />
</div>
<div className="agentScore">
<strong>{Math.round((agentEvaluation?.score ?? 0) * 100)}%</strong>
<span>{agentEvaluation?.checks.filter((item) => item.passed).length ?? 0}/{agentEvaluation?.checks.length ?? 0} checks passed</span>
</div>
<div className="suggestionList">
{(agentEvaluation?.suggestions ?? ["等待评价 agent 返回建议。"]).slice(0, 6).map((item, index) => (
<div key={`${index}-${item}`}>{item}</div>
))}
</div>
<AgentCheckList checks={agentEvaluation?.checks ?? []} limit={14} />
</div>
<div className="panel agentPanel">
<div className="panelHead">
<div>
<p className="eyebrow">Validation Agent</p>
<h2></h2>
</div>
<button className="primary" disabled={agentBusy} onClick={runAgentValidation}>
<ClipboardCheck size={17} />
</button>
</div>
<div className="agentScore">
<strong>{agentValidation ? (agentValidation.passed ? "OK" : "Check") : "Ready"}</strong>
<span>{agentValidation ? `${agentValidation.checks.filter((item) => item.passed).length}/${agentValidation.checks.length} checks passed` : "轻量验证不会触发深度训练"}</span>
</div>
<AgentCheckList checks={agentValidation?.checks ?? []} limit={18} />
</div>
</section>
<section className="grid four">
<div className="panel" id="gpu">
<div className="panelHead">
<div>
<p className="eyebrow">Hardware</p>
<h2>GPU</h2>
</div>
<Cpu size={22} />
</div>
{(gpus?.gpus ?? []).map((gpu) => (
<div className="gpu" key={gpu.index}>
<div>
<strong>GPU {gpu.index}</strong>
<span>{gpu.name}</span>
</div>
<meter value={gpu.memory_used_mb} max={gpu.memory_total_mb} />
<small>{gpu.memory_free_mb} MB free · {gpu.utilization_gpu_percent}% · {gpu.temperature_c}C</small>
</div>
))}
</div>
<div className="panel" id="runtime">
<div className="panelHead">
<div>
<p className="eyebrow">Runtime</p>
<h2></h2>
</div>
<ShieldCheck size={22} />
</div>
<div className="envList">
{(runtimeReadiness?.envs ?? []).map((env) => (
<div key={env.role} className={`envCard ${env.passed ? "ok" : "bad"}`}>
<div className="envHead">
<div>
<strong>{env.name}</strong>
<small>{env.label}</small>
</div>
<span>{env.passed ? "READY" : env.exists ? "CHECK" : "MISSING"}</span>
</div>
<div className="envChecks">
{env.checks.slice(0, 8).map((check) => (
<span key={check.module} className={check.passed ? "ok" : "bad"} title={check.error ?? check.package}>
{check.module}{check.version ? ` ${check.version}` : ""}
</span>
))}
</div>
</div>
))}
</div>
<p className="muted">{runtimeReadiness?.passed ? "runtime imports ready" : "run scripts/bootstrap_conda_envs.sh"} · {runtimeReadiness?.generated_at ?? "not checked"}</p>
</div>
<WeightPanel
catalog={catalog}
manifest={weightManifest}
verification={weightVerification}
busy={busy}
onSync={syncWeights}
onVerify={verifyWeights}
/>
<div className="panel">
<div className="panelHead">
<div>
<p className="eyebrow">Catalog</p>
<h2></h2>
</div>
<FileSearch size={22} />
</div>
<p className="muted">SegModel {catalog?.segmodel_architectures.length ?? 0} · YOLO {catalog?.yolo_models.length ?? 0} · MMSeg {catalog?.mmseg_algorithms.length ?? 0}</p>
<div className="chips">
{(catalog?.segmodel_architectures ?? []).slice(0, 8).map((item) => <span key={item}>{item}</span>)}
</div>
</div>
</section>
<section className="grid three">
<div className="panel insight">
<div className="panelHead">
<div>
<p className="eyebrow">Segmentation</p>
<h2></h2>
</div>
<Wand2 size={22} />
</div>
<ResultPreview results={results.filter((item) => item.role === "segmentation" && ["png", "jpg", "jpeg", "svg"].includes(item.kind)).slice(0, 6)} />
</div>
<div className="panel insight">
<div className="panelHead">
<div>
<p className="eyebrow">Heatmap</p>
<h2>YOLO </h2>
</div>
<Zap size={22} />
</div>
<ResultPreview results={results.filter((item) => item.role === "heatmap" && ["png", "jpg", "jpeg", "svg"].includes(item.kind)).slice(0, 6)} />
</div>
<div className="panel insight">
<div className="panelHead">
<div>
<p className="eyebrow">Curves</p>
<h2>Loss / </h2>
</div>
<BarChart3 size={22} />
</div>
<div className="resultList tight">
<CurvePanel curves={curves} selected={selectedCurve} selectedPath={selectedCurvePath} onSelect={setSelectedCurvePath} />
</div>
</div>
</section>
<section className="grid two">
<div className="panel logPanel">
<div className="panelHead">
<div>
<p className="eyebrow">Live Log</p>
<h2>{selectedJob?.type ?? "选择一个任务"}</h2>
</div>
<button className="iconButton" disabled={!selectedJob} onClick={cancelSelectedJob} title="取消任务">
<Square size={18} />
</button>
</div>
{selectedJob && <JobProgressBar progress={selectedJob.progress} />}
{selectedJob && <JobDiagnostics job={selectedJob} />}
<pre>{log || "No log selected."}</pre>
</div>
<div className="panel" id="results">
<div className="panelHead">
<div>
<p className="eyebrow">Artifacts</p>
<h2></h2>
</div>
<BarChart3 size={22} />
</div>
<ResultBrowser
results={filteredResults}
total={results.length}
familyOptions={resultFamilyOptions}
roleOptions={resultRoleOptions}
familyFilter={resultFamilyFilter}
roleFilter={resultRoleFilter}
onFamilyFilter={setResultFamilyFilter}
onRoleFilter={setResultRoleFilter}
/>
</div>
</section>
</section>
</main>
);
}
function JobDiagnostics({ job }: { job: Job }) {
const command = job.command?.join(" ") ?? "";
return (
<div className="jobDiagnostics">
<div className="jobDetailGrid">
<div><span>ID</span><strong>{job.id.slice(0, 12)}</strong></div>
<div><span>PID</span><strong>{job.pid ?? "-"}</strong></div>
<div><span>Exit</span><strong>{job.exit_code ?? "-"}</strong></div>
<div><span>Log</span><strong>{formatBytes(job.log_size)}</strong></div>
</div>
{job.cwd && (
<div className="jobPath">
<span>CWD</span>
<code>{job.cwd}</code>
</div>
)}
{!!job.error && <div className="jobError">{job.error}</div>}
{!!command && (
<details className="jobDetailBlock" open>
<summary>Command</summary>
<pre>{command}</pre>
</details>
)}
<details className="jobDetailBlock">
<summary>Params</summary>
<pre>{JSON.stringify(job.params ?? {}, null, 2)}</pre>
</details>
</div>
);
}
function WeightPanel({
catalog,
manifest,
verification,
busy,
onSync,
onVerify
}: {
catalog: Catalog | null;
manifest: WeightManifest | null;
verification: WeightVerifyPayload | null;
busy: boolean;
onSync: () => void;
onVerify: () => void;
}) {
const files = manifest?.files ?? [];
const familyStats = Array.from(files.reduce((counts, item) => counts.set(item.family, (counts.get(item.family) ?? 0) + 1), new Map<string, number>()).entries())
.sort((a, b) => b[1] - a[1]);
return (
<div className="panel" id="weights">
<div className="panelHead">
<div>
<p className="eyebrow">Assets</p>
<h2></h2>
</div>
<div className="buttonRow compactButtons">
<button className="iconButton" disabled={busy || !files.length} onClick={onVerify} title="校验权重 manifest">
<ShieldCheck size={18} />
</button>
<button className="iconButton" disabled={busy} onClick={onSync} title="同步权重">
<UploadCloud size={18} />
</button>
</div>
</div>
<div className="bigNumber">{manifest?.count ?? catalog?.weights.count ?? 0}</div>
<p className="muted">{formatBytes(manifest?.total_bytes ?? catalog?.weights.total_bytes)} indexed</p>
<p className="muted">{manifest?.updated_at ?? catalog?.weights.updated_at ?? "manifest not generated"}</p>
{verification && (
<div className={verification.ok_count === verification.count ? "weightVerify ok" : "weightVerify bad"}>
{verification.ok_count}/{verification.count} verified
</div>
)}
<div className="weightFamilies">
{familyStats.slice(0, 4).map(([family, count]) => (
<span key={family}>{family} {count}</span>
))}
</div>
<div className="weightList">
{files.slice(0, 6).map((item) => (
<a key={item.stored_path} href={`${API_BASE}/api/artifacts/${item.stored_path}`} target="_blank" rel="noreferrer">
<span>{item.source_path}</span>
<small>{item.family} · {item.role} · {formatBytes(item.size)}</small>
</a>
))}
</div>
</div>
);
}
function AgentCheckList({ checks, limit }: { checks: AgentCheck[]; limit: number }) {
if (!checks.length) {
return <p className="muted"></p>;
}
return (
<div className="agentCheckList">
{checks.slice(0, limit).map((check) => (
<div key={check.name} className={check.passed ? "ok" : "bad"} title={check.name}>
<span>{check.name}</span>
<small>{check.passed ? "passed" : "needs attention"}</small>
</div>
))}
</div>
);
}
function ResultPreview({ results }: { results: ResultItem[] }) {
if (!results.length) {
return <p className="muted"></p>;
}
return (
<div className="previewGrid">
{results.map((item) => (
<a key={item.path} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
<img src={`${API_BASE}/api/artifacts/${item.relative_path}`} alt={item.name} />
<span>{item.name}</span>
</a>
))}
</div>
);
}
function ResultBrowser({
results,
total,
familyOptions,
roleOptions,
familyFilter,
roleFilter,
onFamilyFilter,
onRoleFilter
}: {
results: ResultItem[];
total: number;
familyOptions: string[];
roleOptions: string[];
familyFilter: string;
roleFilter: string;
onFamilyFilter: (value: string) => void;
onRoleFilter: (value: string) => void;
}) {
return (
<div className="resultBrowser">
<div className="resultFilters">
<label>
<span>Family</span>
<select value={familyFilter} onChange={(event) => onFamilyFilter(event.target.value)} aria-label="result family">
{familyOptions.map((option) => (
<option key={option} value={option}>{resultFilterLabel(option)}</option>
))}
</select>
</label>
<label>
<span>Role</span>
<select value={roleFilter} onChange={(event) => onRoleFilter(event.target.value)} aria-label="result role">
{roleOptions.map((option) => (
<option key={option} value={option}>{resultFilterLabel(option)}</option>
))}
</select>
</label>
</div>
<div className="resultSummary">
<strong>{results.length}</strong>
<span>/ {total} artifacts</span>
</div>
<div className="resultList">
{results.length ? (
results.map((item) => (
<a key={item.path} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
<span>{item.name}</span>
<small>{item.family ?? "artifact"} · {item.role ?? "artifact"} · {formatBytes(item.size)}</small>
</a>
))
) : (
<p className="muted"></p>
)}
</div>
</div>
);
}
function DatasetQuality({ validation }: { validation: DatasetValidation }) {
return (
<div className="qualityBox">
<div className="qualityHead">
<strong>{validation.dataset}</strong>
<span>{validation.ready.yolo ? "YOLO READY" : validation.ready.mask ? "MASK READY" : "CHECK"}</span>
</div>
<div className="qualityStats">
<div><span>Image/Label</span><strong>{validation.pairs.image_label}</strong></div>
<div><span>Image/Mask</span><strong>{validation.pairs.image_mask}</strong></div>
<div><span>Classes</span><strong>{validation.classes.length || 0}</strong></div>
<div><span>Annotations</span><strong>{validation.counts.annotations}</strong></div>
</div>
<div className="qualityChecks">
{validation.checks.map((check) => (
<div key={check.name} className={check.passed ? "ok" : "bad"}>
<span>{check.name}</span>
<small>{check.passed ? "ok" : `${check.errors?.length ?? 0} issue`}</small>
</div>
))}
</div>
</div>
);
}
function DatasetYoloOutputs({ dataset, outputs }: { dataset: UploadedDataset; outputs: DatasetYoloOutputsPayload }) {
const previewItems = [...outputs.heatmaps.slice(0, 3), ...outputs.predictions.slice(0, 3)].slice(0, 6);
const primaryCurve = outputs.curves[0];
const curveSeries = primaryCurve?.series.slice(0, 4) ?? [];
return (
<div className="datasetOutputBox">
<div className="qualityHead">
<strong>{dataset.name} · YOLO</strong>
<span>{outputs.bestWeight ? "BEST.PT READY" : "BEST.PT MISSING"}</span>
</div>
<div className="qualityStats">
<div><span>Weights</span><strong>{outputs.bestWeight ? 1 : 0}</strong></div>
<div><span>Predict</span><strong>{outputs.predictions.length}</strong></div>
<div><span>Heatmap</span><strong>{outputs.heatmaps.length}</strong></div>
<div><span>Curves</span><strong>{outputs.curves.length}</strong></div>
</div>
<div className="datasetOutputLinks">
{outputs.bestWeight && (
<a href={`${API_BASE}/api/artifacts/${outputs.bestWeight.relative_path}`} target="_blank" rel="noreferrer">
<span>best.pt</span>
<small>{formatBytes(outputs.bestWeight.size)}</small>
</a>
)}
{outputs.curves.slice(0, 2).map((curve) => (
<a key={curve.relative_path} href={`${API_BASE}/api/artifacts/${curve.relative_path}`} target="_blank" rel="noreferrer">
<span>{curve.name}</span>
<small>{curve.row_count} epochs</small>
</a>
))}
</div>
{primaryCurve && !!curveSeries.length && (
<div className="datasetOutputCurve">
<div>
<strong>{primaryCurve.name}</strong>
<span>{primaryCurve.row_count} epochs · {primaryCurve.family}</span>
</div>
<MiniCurvePlot series={curveSeries} />
</div>
)}
{!!previewItems.length && (
<div className="datasetOutputPreview">
{previewItems.map((item) => (
<a key={item.relative_path} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
<img src={`${API_BASE}/api/artifacts/${item.relative_path}`} alt={item.name} />
<span>{item.role}</span>
</a>
))}
</div>
)}
</div>
);
}
function CurvePanel({
curves,
selected,
selectedPath,
onSelect
}: {
curves: TrainingCurve[];
selected?: TrainingCurve;
selectedPath: string;
onSelect: (path: string) => void;
}) {
if (!curves.length || !selected) {
return <p className="muted">线</p>;
}
const visibleSeries = selected.series.slice(0, 5);
return (
<div className="curvePanel">
<select value={selectedPath || selected.relative_path} onChange={(event) => onSelect(event.target.value)} aria-label="curve">
{curves.map((curve) => (
<option key={curve.relative_path} value={curve.relative_path}>
{curve.family} · {curve.name} · {curve.row_count} epochs
</option>
))}
</select>
<MiniCurvePlot series={visibleSeries} />
<div className="curveLegend">
{visibleSeries.map((item, index) => (
<a key={item.name} href={`${API_BASE}/api/artifacts/${selected.relative_path}`} target="_blank" rel="noreferrer">
<i style={{ background: curveColor(index) }} />
<span>{item.name}</span>
<small>{item.last.toFixed(4)}</small>
</a>
))}
</div>
</div>
);
}
function MiniCurvePlot({ series }: { series: CurveSeries[] }) {
const points = series.flatMap((item) => item.points);
if (!points.length) return <div className="curveEmpty" />;
const minX = Math.min(...points.map((point) => point.x));
const maxX = Math.max(...points.map((point) => point.x));
const minY = Math.min(...points.map((point) => point.y));
const maxY = Math.max(...points.map((point) => point.y));
const width = 520;
const height = 190;
const pad = 16;
const scaleX = (x: number) => pad + ((x - minX) / Math.max(maxX - minX, 1)) * (width - pad * 2);
const scaleY = (y: number) => height - pad - ((y - minY) / Math.max(maxY - minY, 1)) * (height - pad * 2);
return (
<svg className="curveSvg" viewBox={`0 0 ${width} ${height}`} role="img" aria-label="training curve">
<path d={`M ${pad} ${height - pad} H ${width - pad} M ${pad} ${pad} V ${height - pad}`} className="axis" />
{[0.25, 0.5, 0.75].map((tick) => (
<path key={tick} d={`M ${pad} ${pad + tick * (height - pad * 2)} H ${width - pad}`} className="gridLine" />
))}
{series.map((item, index) => (
<polyline
key={item.name}
points={item.points.map((point) => `${scaleX(point.x).toFixed(2)},${scaleY(point.y).toFixed(2)}`).join(" ")}
fill="none"
stroke={curveColor(index)}
strokeWidth="2.4"
strokeLinejoin="round"
strokeLinecap="round"
/>
))}
</svg>
);
}
function curveColor(index: number) {
return ["#9de26f", "#73d2de", "#d3b35b", "#7aa2ff", "#f07167"][index % 5];
}
function resultFilterLabel(value: string) {
return value === "all" ? "All" : value;
}
createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);