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 [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, 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<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);
      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, 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, 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 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>数据集、Label、Mask 上传</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={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>{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>深度验收：{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>
);
