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; export const DatasetVersionsPage: React.FC = () => { const [versions, setVersions] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [project, setProject] = useState("dms"); const [diffA, setDiffA] = useState(""); const [diffB, setDiffB] = useState(""); const [diffResult, setDiffResult] = useState | null>(null); const [expandedId, setExpandedId] = useState(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 (

数据集版本

build 入库后自动生成快照,追踪数据演化历史

setSearch(e.target.value)} /> 共 {filtered.length} 条
{/* Version diff */}
版本对比
{diffResult && (
{(diffResult.v1 as Record)?.id as string || "—"}

图片: {(diffResult.v1 as Record)?.total as number || 0}

{(diffResult.v2 as Record)?.id as string || "—"}

图片: {(diffResult.v2 as Record)?.total as number || 0}

{diffResult.image_delta != null && (

图片变化: = 0 ? "text-green-600" : "text-red-600"}> {Number(diffResult.image_delta) >= 0 ? "+" : ""}{diffResult.image_delta as number} 张

)} {((diffResult.added_packs as string[])?.length > 0) && (

+ 新增包: {(diffResult.added_packs as string[]).join(", ")}

)} {((diffResult.removed_packs as string[])?.length > 0) && (

- 移除包: {(diffResult.removed_packs as string[]).join(", ")}

)} {((diffResult.pack_changes as unknown[])?.length > 0) && (

数据包变化详情:

{(diffResult.pack_changes as Record[]).map((pc, i) => (
{pc.pack as string}: train {(pc.v1 as Record).train}→{(pc.v2 as Record).train}, val {(pc.v1 as Record).val}→{(pc.v2 as Record).val}
))}
)}
)}
{/* Version list */}
{filtered.map((v) => { const summary = v.summary as Record | undefined; const diff = v.diff as Record | undefined; const isExpanded = expandedId === v._id; return (
{v.version_id as string} {v.description as string || "无描述"} {(v.parent_version as string) && ( 基于 {v.parent_version as string} )}
{v.created_at as string} {v.author as string || "—"}
{/* Summary stats */}
📦 {summary?.packs_count as number || 0} 个包 🖼️ {summary?.total_images as number || 0} 张图 🏷️ {summary?.total_labels as number || 0} 个标注 📋 {summary?.batches_count as number || 0} 个批次
{/* Expanded detail */} {isExpanded && (

数据包

{Object.entries((v.packs || {}) as Record>).map(([name, info]) => (
{name}: train={info.train_images} val={info.val_images} test={info.test_images}
))}

批次列表

{((v.batches || []) as string[]).map((b) => ( {b} ))}
{diff && (

相对父版本变化

{((diff.added_packs as string[])?.length > 0) && (

+ 包: {(diff.added_packs as string[]).join(", ")}

)} {((diff.removed_packs as string[])?.length > 0) && (

- 包: {(diff.removed_packs as string[]).join(", ")}

)} {((diff.added_batches as string[])?.length > 0) && (

+ 批次: {(diff.added_batches as string[]).join(", ")}

)}
)}
查看数据目录 → 查看训练记录 →
)}
); })}
); };