Files
HSAP/platform/web/src/modules/models/pages/DatasetVersionsPage.tsx
Chengfang Lu e72bc061c5 feat: HSAP platform v2 — modular navigation, quality review, audit log, world model simulation
Major changes:
- New frontend (platform/web/): Vite + React 18 + TypeScript + Tailwind
- 4-module navigation: 数据送标 / 模型管理 / 车队管理 / 系统管理
- Data catalog with charts (DMS/ADAS/Lane 3-tab view)
- Quality review workflow (标注质检): Good/Fine/Bad scoring with auto-advance
- Audit enhancements: batch operations, rejection categories, Feishu notifications
- Operation audit log (操作日志)
- World model simulation studio (仿真工坊)
- Dataset version management with snapshots and diff
- ADAS 7-class dataset integration (138K images organized + compressed)
- User management with Feishu integration and pagination
- CRUD/search/filter on all pages, card layout redesign
- PIL-optimized image overlay rendering
- Auto-snapshot on build, in_review workflow stage
- Removed embedded algorithm code (now in workspace)
2026-06-03 11:40:21 +08:00

205 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, useState, useCallback } from "react";
import { Link } from "react-router-dom";
import { hsapApi } from "@/app/hsap-api";
import { Button } from "@/components/ui/Button";
import { Badge } from "@/components/ui/Badge";
import { PageQueryState } from "@/components/PageQueryState";
type VersionEntry = Record<string, unknown>;
export const DatasetVersionsPage: React.FC = () => {
const [versions, setVersions] = useState<VersionEntry[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [project, setProject] = useState("dms");
const [diffA, setDiffA] = useState("");
const [diffB, setDiffB] = useState("");
const [diffResult, setDiffResult] = useState<Record<string, unknown> | null>(null);
const [expandedId, setExpandedId] = useState<string | null>(null);
const [search, setSearch] = useState("");
const filtered = versions.filter((v) => !search || String(v.description || v.version_id || "").toLowerCase().includes(search.toLowerCase()));
const load = useCallback(async () => {
setLoading(true); setError(null);
try {
const res = await hsapApi.listDatasetVersions(project);
setVersions((res.items || []) as VersionEntry[]);
} catch (e) { setError(String(e)); }
setLoading(false);
}, [project]);
useEffect(() => { load(); }, [load]);
const handleDiff = async () => {
if (!diffA || !diffB) return;
try {
const res = await hsapApi.diffDatasetVersions(diffA, diffB, project);
setDiffResult(res);
} catch (e) { setError(String(e)); }
};
return (
<div className="page-container">
<div className="page-header flex items-center justify-between">
<div>
<h1></h1>
<p>build </p>
</div>
<div className="flex gap-2">
<select className="form-input w-auto" value={project} onChange={(e) => setProject(e.target.value)}>
<option value="dms">DMS</option>
<option value="lane">Lane</option>
</select>
<Button variant="default" size="small" onClick={load}></Button>
</div>
</div>
<div className="flex gap-2 mb-3">
<input className="form-input w-48" placeholder="搜索版本描述..." value={search} onChange={(e) => setSearch(e.target.value)} />
<span className="text-xs text-gray-400 self-center"> {filtered.length} </span>
</div>
{/* Version diff */}
<div className="card mb-4">
<div className="card-header"></div>
<div className="flex gap-2 items-end">
<div className="form-group flex-1">
<label className="form-label"> A ()</label>
<select className="form-input" value={diffA} onChange={(e) => setDiffA(e.target.value)}>
<option value="">...</option>
{versions.map((v) => <option key={v._id as string} value={v._id as string}>{v.version_id as string} - {v.description as string || "无描述"}</option>)}
</select>
</div>
<div className="form-group flex-1">
<label className="form-label"> B ()</label>
<select className="form-input" value={diffB} onChange={(e) => setDiffB(e.target.value)}>
<option value="">...</option>
{versions.map((v) => <option key={v._id as string} value={v._id as string}>{v.version_id as string} - {v.description as string || "无描述"}</option>)}
</select>
</div>
<Button variant="default" onClick={handleDiff} disabled={!diffA || !diffB}></Button>
</div>
{diffResult && (
<div className="mt-4 bg-gray-50 rounded-lg p-4">
<div className="grid grid-cols-3 gap-4 mb-3 text-sm">
<div>
<span className="font-semibold">{(diffResult.v1 as Record<string,unknown>)?.id as string || "—"}</span>
<p className="text-xs text-gray-500">: {(diffResult.v1 as Record<string,unknown>)?.total as number || 0}</p>
</div>
<div className="text-center text-2xl text-gray-400"></div>
<div>
<span className="font-semibold">{(diffResult.v2 as Record<string,unknown>)?.id as string || "—"}</span>
<p className="text-xs text-gray-500">: {(diffResult.v2 as Record<string,unknown>)?.total as number || 0}</p>
</div>
</div>
{diffResult.image_delta != null && (
<p className="text-sm mb-2">
: <span className={Number(diffResult.image_delta) >= 0 ? "text-green-600" : "text-red-600"}>
{Number(diffResult.image_delta) >= 0 ? "+" : ""}{diffResult.image_delta as number}
</span>
</p>
)}
{((diffResult.added_packs as string[])?.length > 0) && (
<p className="text-sm text-green-600">+ : {(diffResult.added_packs as string[]).join(", ")}</p>
)}
{((diffResult.removed_packs as string[])?.length > 0) && (
<p className="text-sm text-red-600">- : {(diffResult.removed_packs as string[]).join(", ")}</p>
)}
{((diffResult.pack_changes as unknown[])?.length > 0) && (
<div className="mt-2">
<p className="text-xs font-semibold text-gray-500 mb-1">:</p>
{(diffResult.pack_changes as Record<string, unknown>[]).map((pc, i) => (
<div key={i} className="text-xs text-gray-600 ml-2">
{pc.pack as string}: train {(pc.v1 as Record<string,number>).train}{(pc.v2 as Record<string,number>).train}, val {(pc.v1 as Record<string,number>).val}{(pc.v2 as Record<string,number>).val}
</div>
))}
</div>
)}
</div>
)}
</div>
{/* Version list */}
<PageQueryState loading={loading} error={error} empty={filtered.length === 0} emptyMessage="暂无数据集版本,请创建第一个快照">
<div className="space-y-4">
{filtered.map((v) => {
const summary = v.summary as Record<string, unknown> | undefined;
const diff = v.diff as Record<string, unknown> | undefined;
const isExpanded = expandedId === v._id;
return (
<div key={v._id as string} className="card">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<Badge variant="info">{v.version_id as string}</Badge>
<span className="text-sm font-medium">{v.description as string || "无描述"}</span>
{(v.parent_version as string) && (
<span className="text-xs text-gray-400">
<span className="font-mono">{v.parent_version as string}</span>
</span>
)}
</div>
<div className="flex items-center gap-3 text-xs text-gray-500">
<span>{v.created_at as string}</span>
<span>{v.author as string || "—"}</span>
<button onClick={() => setExpandedId(isExpanded ? null : (v._id as string))}
className="text-blue-600 hover:underline">{isExpanded ? "收起" : "详情"}</button>
</div>
</div>
{/* Summary stats */}
<div className="flex gap-4 text-sm">
<span>📦 {summary?.packs_count as number || 0} </span>
<span>🖼 {summary?.total_images as number || 0} </span>
<span>🏷 {summary?.total_labels as number || 0} </span>
<span>📋 {summary?.batches_count as number || 0} </span>
</div>
{/* Expanded detail */}
{isExpanded && (
<div className="mt-3 pt-3 border-t border-gray-100">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="font-semibold text-xs text-gray-500 mb-1"></p>
{Object.entries((v.packs || {}) as Record<string, Record<string, number>>).map(([name, info]) => (
<div key={name} className="text-xs font-mono text-gray-600">
{name}: train={info.train_images} val={info.val_images} test={info.test_images}
</div>
))}
</div>
<div>
<p className="font-semibold text-xs text-gray-500 mb-1"></p>
<div className="flex flex-wrap gap-1">
{((v.batches || []) as string[]).map((b) => (
<Badge key={b} size="small">{b}</Badge>
))}
</div>
{diff && (
<div className="mt-3">
<p className="font-semibold text-xs text-gray-500 mb-1"></p>
{((diff.added_packs as string[])?.length > 0) && (
<p className="text-xs text-green-600">+ : {(diff.added_packs as string[]).join(", ")}</p>
)}
{((diff.removed_packs as string[])?.length > 0) && (
<p className="text-xs text-red-600">- : {(diff.removed_packs as string[]).join(", ")}</p>
)}
{((diff.added_batches as string[])?.length > 0) && (
<p className="text-xs text-green-600">+ : {(diff.added_batches as string[]).join(", ")}</p>
)}
</div>
)}
</div>
</div>
<div className="mt-2 flex gap-2">
<Link to="/labeling/catalog" className="text-blue-600 text-xs hover:underline"> </Link>
<Link to="/models/training/records" className="text-blue-600 text-xs hover:underline"> </Link>
</div>
</div>
)}
</div>
);
})}
</div>
</PageQueryState>
</div>
);
};