diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 555633e..14152c0 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -14,6 +14,7 @@ import { Gauge, HardDrive, Layers3, + Plus, Play, RefreshCcw, ShieldCheck, @@ -22,6 +23,7 @@ import { Trash2, UploadCloud, PackageOpen, + X, Wand2, Zap } from "lucide-react"; @@ -616,6 +618,7 @@ function App() { const [log, setLog] = useState(""); const [busy, setBusy] = useState(false); const [actionError, setActionError] = useState(""); + const [uploadModalOpen, setUploadModalOpen] = useState(false); const [datasetName, setDatasetName] = useState("demo_dataset"); const [datasetDescription, setDatasetDescription] = useState(""); const [copyDatasetName, setCopyDatasetName] = useState(""); @@ -964,6 +967,7 @@ function App() { body: JSON.stringify({ name: datasetName, description: datasetDescription }) }); setSelectedDatasetName(datasetName); + setUploadModalOpen(false); await refresh(); } catch (err) { setActionError(String(err)); @@ -989,6 +993,9 @@ function App() { throw new Error(`上传失败,无法连接后端 ${API_BASE}:${err instanceof Error ? err.message : String(err)}`); } if (!res.ok) throw new Error(await res.text()); + setSelectedDatasetName(datasetName); + setUploadModalOpen(false); + setUploadFiles(null); await refresh(); } catch (err) { setActionError(String(err)); @@ -1394,37 +1401,11 @@ function App() {

Dataset Bench

-

上传与管理

-
- -
-
- - - {datasetNameExists && 已存在同名数据集,只能上传追加,不能重复创建。} -
- {(["images", "masks"] as const).map((kind) => ( - - ))} -
- -
- - +

数据集管理

+
@@ -1521,8 +1502,6 @@ function App() { {selectedDataset ? ( <> - {selectedValidation && } - ) : (

左侧创建或选择一个数据集后,这里会显示图片、masks、配对和异常项。

@@ -1530,6 +1509,50 @@ function App() {
+ {uploadModalOpen && ( +
+
+
+
+

Upload

+

新建或追加数据集

+
+ +
+
+ + + {datasetNameExists && 已存在同名数据集,不能重复创建,但可以继续上传追加文件。} +
+ {(["images", "masks"] as const).map((kind) => ( + + ))} +
+ +
+ + +
+
+
+
+ )} +
@@ -2159,76 +2182,73 @@ function AcceptanceArtifactLinks({ artifacts }: { artifacts?: AcceptancePayload[ function DatasetDetail({ dataset, validation }: { dataset: UploadedDataset; validation?: DatasetValidation }) { const rows = datasetPairRows(dataset); - const images = datasetFiles(dataset, "images"); - const masks = datasetFiles(dataset, "masks"); const problemRows = rows.filter((row) => row.status !== "paired"); const shapeErrors = validation?.checks.find((check) => check.name === "mask_shapes_match")?.errors ?? []; const ready = validation?.ready.mask; return (
-
-
images / video{dataset.counts.images}
-
masks{dataset.counts.masks}
-
paired{validation?.pairs.image_mask ?? rows.filter((row) => row.status === "paired").length}
-
status{ready ? "MASK READY" : "CHECK"}
+
+ {dataset.counts.images} images / video + {dataset.counts.masks} masks + {validation?.pairs.image_mask ?? rows.filter((row) => row.status === "paired").length} paired + {ready ? "MASK READY" : "CHECK"}
-
- {[...images.slice(0, 4), ...masks.slice(0, 4)].slice(0, 8).map((item) => ( - - {item.previewable ? {item.name} : } - {item.name} - - ))} -
- -
-
- 配对 - images / video - 大小 - masks - 大小 - 状态 -
-
- {rows.length ? rows.slice(0, 500).map((row) => ( -
- {row.stem} - - {row.image?.name ?? "-"} - - {row.image ? formatBytes(row.image.size) : "-"} - - {row.mask?.name ?? "-"} - - {row.mask ? formatBytes(row.mask.size) : "-"} - {row.status === "paired" ? "已配对" : row.status === "missing_mask" ? "缺少 mask" : "多余 mask"} +
+ {rows.length ? rows.slice(0, 500).map((row) => ( +
+ {row.status === "paired" ? "已配对" : row.status === "missing_mask" ? "缺少 mask" : "多余 mask"} +
+ +
- )) : ( -

暂无文件。

- )} -
+
+ {row.stem} + {row.image?.name ?? "-"} / {row.mask?.name ?? "-"} + {row.image ? formatBytes(row.image.size) : "-"} · {row.mask ? formatBytes(row.mask.size) : "-"} +
+ + )) : ( +

暂无文件。

+ )}
-
-
- 多余或无法匹配 - {problemRows.length ? problemRows.slice(0, 80).map((row) => ( - {row.status === "missing_mask" ? row.image?.name : row.mask?.name} - )) : 当前没有未配对文件。} + {(problemRows.length > 0 || shapeErrors.length > 0) && ( +
+ {problemRows.slice(0, 80).map((row) => ( + {row.status === "missing_mask" ? `缺少 mask: ${row.image?.name}` : `多余 mask: ${row.mask?.name}`} + ))} + {shapeErrors.slice(0, 20).map((item, index) => ( + 尺寸问题: {JSON.stringify(item)} + ))}
-
- 尺寸问题 - {shapeErrors.length ? shapeErrors.slice(0, 20).map((item, index) => ( - {JSON.stringify(item)} - )) : 当前抽样未发现 image/mask 尺寸不一致。} -
-
+ )}
); } +function DatasetWallPane({ item, label, emptyText }: { item?: DatasetFileItem; label: string; emptyText: string }) { + if (!item) { + return ( +
+ + {emptyText} +
+ ); + } + return ( + + {item.previewable ? {item.name} : ( +
+ + {item.media_type ?? "file"} +
+ )} + {label} +
+ ); +} + function DatasetQuality({ validation }: { validation: DatasetValidation }) { return (
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 40730e6..111a697 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -740,6 +740,12 @@ textarea { gap: 16px; } +.addDatasetButton { + color: #0b0d0b; + background: var(--green); + border-color: transparent; +} + .datasetManager, .preprocessBox, .datasetDetailStack { @@ -759,6 +765,44 @@ textarea { line-height: 1.35; } +.modalOverlay { + position: fixed; + inset: 0; + z-index: 50; + display: grid; + place-items: center; + padding: 28px; + background: rgba(4, 7, 5, 0.72); + backdrop-filter: blur(10px); +} + +.datasetUploadModal { + width: min(620px, 100%); + max-height: calc(100vh - 56px); + overflow: auto; + padding: 18px; + border: 1px solid rgba(157, 226, 111, 0.32); + border-radius: 8px; + background: + linear-gradient(135deg, rgba(157, 226, 111, 0.06), transparent 42%), + #151815; + box-shadow: 0 28px 80px rgba(0, 0, 0, 0.55); +} + +.modalHead { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; +} + +.modalActions { + display: flex; + justify-content: flex-end; + gap: 10px; +} + .segmented { display: grid; grid-template-columns: repeat(2, 1fr); @@ -960,6 +1004,177 @@ textarea { font-size: 20px; } +.datasetWallStats { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.datasetWallStats span, +.datasetWallStats strong { + padding: 7px 9px; + border: 1px solid var(--line); + border-radius: 999px; + background: #101310; + color: var(--muted); + font-size: 12px; +} + +.datasetWallStats strong { + color: var(--green); + border-color: rgba(157, 226, 111, 0.35); +} + +.imageWall { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); + gap: 12px; + align-items: start; +} + +.wallCard { + position: relative; + min-width: 0; + overflow: hidden; + border: 1px solid var(--line); + border-radius: 8px; + background: #101310; +} + +.wallCard.paired { + border-color: rgba(157, 226, 111, 0.28); +} + +.wallCard.missing_mask, +.wallCard.extra_mask { + border-color: rgba(211, 179, 91, 0.52); +} + +.wallBadge { + position: absolute; + top: 8px; + right: 8px; + z-index: 2; + max-width: calc(100% - 16px); + padding: 4px 7px; + border-radius: 999px; + background: rgba(8, 10, 8, 0.78); + color: var(--green); + border: 1px solid rgba(157, 226, 111, 0.38); + font-size: 11px; + font-weight: 760; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.wallCard.missing_mask .wallBadge, +.wallCard.extra_mask .wallBadge { + color: var(--amber); + border-color: rgba(211, 179, 91, 0.55); +} + +.wallMedia { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1px; + background: var(--line); +} + +.wallPane { + position: relative; + min-width: 0; + display: grid; + place-items: center; + aspect-ratio: 1 / 0.82; + background: #070907; + color: var(--muted); + text-decoration: none; + overflow: hidden; +} + +.wallPane img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.wallPane i { + position: absolute; + left: 7px; + bottom: 7px; + padding: 3px 5px; + border-radius: 4px; + background: rgba(8, 10, 8, 0.75); + color: var(--green); + font-style: normal; + font-size: 10px; + text-transform: uppercase; +} + +.emptyPane { + gap: 6px; + border: 1px dashed rgba(211, 179, 91, 0.45); + color: var(--amber); +} + +.emptyPane span { + font-size: 12px; +} + +.mediaPlaceholder { + display: grid; + place-items: center; + gap: 5px; + color: var(--muted); +} + +.mediaPlaceholder span { + font-size: 11px; +} + +.wallCaption { + display: grid; + gap: 3px; + padding: 9px; +} + +.wallCaption strong, +.wallCaption span, +.wallCaption small { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.wallCaption span, +.wallCaption small { + color: var(--muted); + font-size: 11px; +} + +.wallIssueRail { + display: flex; + flex-wrap: wrap; + gap: 7px; +} + +.wallIssueRail span { + max-width: 280px; + padding: 6px 8px; + border: 1px solid rgba(211, 179, 91, 0.45); + border-radius: 999px; + background: rgba(211, 179, 91, 0.08); + color: var(--amber); + font-size: 11px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .datasetPreviewBand { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); @@ -2189,11 +2404,20 @@ meter { .inferencePreview, .datasetSummaryStrip, .datasetPreviewBand, + .imageWall, .issueGrid, .userAgentDataset { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .modalActions { + justify-content: stretch; + } + + .modalActions .primary { + flex: 1 1 0; + } + .fileTableHead, .fileTableRow { grid-template-columns: minmax(80px, 0.8fr) minmax(120px, 1.3fr) 74px minmax(120px, 1.3fr) 74px 82px; @@ -2254,6 +2478,7 @@ meter { .opGrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .datasetWorkbench { grid-template-columns: 1fr; } .datasetPreviewBand { grid-template-columns: repeat(4, minmax(0, 1fr)); } + .imageWall { grid-template-columns: repeat(3, minmax(0, 1fr)); } .coverageGrid, .taskCheckList { grid-template-columns: 1fr; } .grid.three { grid-template-columns: 1fr; }