From 3690e64b184a50520e5fb8f45a0b50c2b4e04fd8 Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Wed, 1 Jul 2026 17:13:02 +0800 Subject: [PATCH] Cache dataset loading and clarify uploads --- frontend/src/main.tsx | 200 +++++++++++++++++++++++++++++++--------- frontend/src/styles.css | 101 ++++++++++++++++++++ 2 files changed, 256 insertions(+), 45 deletions(-) diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 4ae3f28..887cd74 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -95,6 +95,11 @@ type UploadedDataset = { files?: Record; }; +type DatasetUploadResponse = { + dataset: UploadedDataset; + saved: Array<{ name: string; relative_path: string; size: number; from_archive?: boolean }>; +}; + type DatasetValidation = { dataset: string; counts: { images: number; labels: number; masks: number; annotations: number }; @@ -212,6 +217,14 @@ type DeepAcceptancePayload = { checks?: Array<{ name: string; passed: boolean }>; }; +type DataLoadingState = { + refreshing: boolean; + phase: string; + progress: number; + cached: boolean; + updatedAt?: number | null; +}; + type GpuPayload = { available: boolean; gpus: Array<{ @@ -504,7 +517,35 @@ function fileName(path?: string | null) { return path.split("/").filter(Boolean).pop() ?? path; } +const DATA_CACHE_KEY = "seg_data_server_net.datasets.v1"; + +type DatasetCachePayload = { + datasets: UploadedDataset[]; + datasetValidations: Record; + savedAt: number; +}; + +function readDatasetCache(): DatasetCachePayload | null { + if (typeof window === "undefined") return null; + try { + const raw = window.localStorage.getItem(DATA_CACHE_KEY); + return raw ? JSON.parse(raw) as DatasetCachePayload : null; + } catch { + return null; + } +} + +function writeDatasetCache(payload: DatasetCachePayload) { + if (typeof window === "undefined") return; + try { + window.localStorage.setItem(DATA_CACHE_KEY, JSON.stringify(payload)); + } catch { + // Cache is only a UI convenience; storage failures should never block work. + } +} + function useData() { + const cachedDatasetState = useMemo(() => readDatasetCache(), []); const [catalog, setCatalog] = useState(null); const [gpus, setGpus] = useState(null); const [condaEnvs, setCondaEnvs] = useState(null); @@ -512,8 +553,8 @@ function useData() { const [results, setResults] = useState([]); const [curves, setCurves] = useState([]); const [weightManifest, setWeightManifest] = useState(null); - const [datasets, setDatasets] = useState([]); - const [datasetValidations, setDatasetValidations] = useState>({}); + const [datasets, setDatasets] = useState(cachedDatasetState?.datasets ?? []); + const [datasetValidations, setDatasetValidations] = useState>(cachedDatasetState?.datasetValidations ?? {}); const [coverage, setCoverage] = useState(null); const [acceptance, setAcceptance] = useState(null); const [realAcceptance, setRealAcceptance] = useState(null); @@ -524,10 +565,46 @@ function useData() { const [agentEvaluation, setAgentEvaluation] = useState(null); const [userAgent, setUserAgent] = useState(null); const [error, setError] = useState(""); + const [dataLoading, setDataLoading] = useState({ + refreshing: false, + phase: cachedDatasetState ? "使用本地缓存" : "等待加载", + progress: cachedDatasetState ? 100 : 0, + cached: Boolean(cachedDatasetState), + updatedAt: cachedDatasetState?.savedAt ?? null + }); + const refreshSeqRef = useRef(0); async function refresh() { + const seq = refreshSeqRef.current + 1; + refreshSeqRef.current = seq; + const updateLoading = (patch: Partial) => { + if (refreshSeqRef.current !== seq) return; + setDataLoading((current) => ({ ...current, ...patch })); + }; try { - const [catalogNext, gpusNext, envsNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, weightsNext, datasetsNext, coverageNext, acceptanceNext, realAcceptanceNext, realTrainAcceptanceNext, deepAcceptanceNext, agentEvaluationNext, userAgentNext] = await Promise.all([ + updateLoading({ refreshing: true, phase: "读取数据集", progress: 8, cached: datasets.length > 0 }); + const datasetsNext = await api("/api/datasets"); + if (refreshSeqRef.current !== seq) return; + setDatasets(datasetsNext); + writeDatasetCache({ datasets: datasetsNext, datasetValidations, savedAt: Date.now() }); + + updateLoading({ phase: "校验图片和 masks", progress: 32 }); + const validationResults = await Promise.allSettled( + datasetsNext.map(async (dataset) => { + const validation = await api(`/api/datasets/${encodeURIComponent(dataset.name)}/validate`); + return [dataset.name, validation] as const; + }) + ); + if (refreshSeqRef.current !== seq) return; + const validationEntries = validationResults + .filter((item): item is PromiseFulfilledResult => item.status === "fulfilled") + .map((item) => item.value); + const validationsNext = Object.fromEntries(validationEntries); + setDatasetValidations(validationsNext); + writeDatasetCache({ datasets: datasetsNext, datasetValidations: validationsNext, savedAt: Date.now() }); + + updateLoading({ phase: "同步任务、结果和系统状态", progress: 58 }); + const [catalogNext, gpusNext, envsNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, weightsNext, coverageNext, acceptanceNext, realAcceptanceNext, realTrainAcceptanceNext, deepAcceptanceNext, agentEvaluationNext, userAgentNext] = await Promise.allSettled([ api("/api/catalog"), api("/api/system/gpus"), api("/api/system/envs"), @@ -537,7 +614,6 @@ function useData() { api("/api/results?limit=1000"), api("/api/results/curves?limit=100"), api("/api/weights"), - api("/api/datasets"), api("/api/coverage"), api("/api/acceptance/latest"), api("/api/acceptance/real/latest"), @@ -545,39 +621,35 @@ function useData() { api("/api/acceptance/deep/latest"), api("/api/agents/evaluate"), api("/api/agents/user/latest") - ]); - 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(`/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); - setRealTrainAcceptance(realTrainAcceptanceNext); - setDeepAcceptance(deepAcceptanceNext); - setAgentEvaluation(agentEvaluationNext); - setUserAgent(userAgentNext); - setError(""); + ] as const); + if (refreshSeqRef.current !== seq) return; + const secondaryErrors: string[] = []; + if (catalogNext.status === "fulfilled") setCatalog(catalogNext.value); else secondaryErrors.push(String(catalogNext.reason)); + if (gpusNext.status === "fulfilled") setGpus(gpusNext.value); else secondaryErrors.push(String(gpusNext.reason)); + if (envsNext.status === "fulfilled") setCondaEnvs(envsNext.value); else secondaryErrors.push(String(envsNext.reason)); + if (readinessNext.status === "fulfilled") setRuntimeReadiness(readinessNext.value); else secondaryErrors.push(String(readinessNext.reason)); + if (capabilitiesNext.status === "fulfilled") setCapabilities(capabilitiesNext.value); else secondaryErrors.push(String(capabilitiesNext.reason)); + if (jobsNext.status === "fulfilled") setJobs(jobsNext.value); else secondaryErrors.push(String(jobsNext.reason)); + if (resultsNext.status === "fulfilled") setResults(resultsNext.value); else secondaryErrors.push(String(resultsNext.reason)); + if (curvesNext.status === "fulfilled") setCurves(curvesNext.value); else secondaryErrors.push(String(curvesNext.reason)); + if (weightsNext.status === "fulfilled") setWeightManifest(weightsNext.value); else secondaryErrors.push(String(weightsNext.reason)); + if (coverageNext.status === "fulfilled") setCoverage(coverageNext.value); else secondaryErrors.push(String(coverageNext.reason)); + if (acceptanceNext.status === "fulfilled") setAcceptance(acceptanceNext.value); else secondaryErrors.push(String(acceptanceNext.reason)); + if (realAcceptanceNext.status === "fulfilled") setRealAcceptance(realAcceptanceNext.value); else secondaryErrors.push(String(realAcceptanceNext.reason)); + if (realTrainAcceptanceNext.status === "fulfilled") setRealTrainAcceptance(realTrainAcceptanceNext.value); else secondaryErrors.push(String(realTrainAcceptanceNext.reason)); + if (deepAcceptanceNext.status === "fulfilled") setDeepAcceptance(deepAcceptanceNext.value); else secondaryErrors.push(String(deepAcceptanceNext.reason)); + if (agentEvaluationNext.status === "fulfilled") setAgentEvaluation(agentEvaluationNext.value); else secondaryErrors.push(String(agentEvaluationNext.reason)); + if (userAgentNext.status === "fulfilled") setUserAgent(userAgentNext.value); else secondaryErrors.push(String(userAgentNext.reason)); + setError(secondaryErrors.length ? `部分系统信息同步失败,数据集仍可用:${secondaryErrors[0]}` : ""); + updateLoading({ refreshing: false, phase: "加载完成", progress: 100, cached: false, updatedAt: Date.now() }); } catch (err) { setError(String(err)); + updateLoading({ + refreshing: false, + phase: datasets.length ? "同步失败,继续显示缓存" : "同步失败", + progress: datasets.length ? 100 : 0, + cached: datasets.length > 0 + }); } } @@ -587,7 +659,7 @@ function useData() { return () => window.clearInterval(timer); }, []); - return { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, userAgent, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, realTrainAcceptance, deepAcceptance, error, refresh }; + return { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, userAgent, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, realTrainAcceptance, deepAcceptance, error, dataLoading, refresh }; } function StatusPill({ status }: { status: string }) { @@ -609,8 +681,27 @@ function JobProgressBar({ progress }: { progress?: JobProgress }) { ); } +function DataSyncStrip({ loading }: { loading: DataLoadingState }) { + if (!loading.refreshing && !loading.cached) return null; + const percent = Math.max(0, Math.min(100, loading.progress)); + return ( +
+
+ +
+
+ {loading.phase} + {loading.cached ? "正在显示本地缓存,同步完成后会自动更新。" : "数据集列表、缩略图和配对状态会分阶段刷新。"} +
+
+ +
+
+ ); +} + function App() { - const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, userAgent, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, realTrainAcceptance, deepAcceptance, error, refresh } = useData(); + const { catalog, gpus, condaEnvs, runtimeReadiness, capabilities, agentEvaluation, userAgent, jobs, results, curves, weightManifest, datasets, datasetValidations, coverage, acceptance, realAcceptance, realTrainAcceptance, deepAcceptance, error, dataLoading, refresh } = useData(); const [page, setPage] = useState(pageFromHash); const [taskType, setTaskType] = useState("mock.echo"); const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2)); @@ -965,11 +1056,12 @@ function App() { setBusy(true); try { setActionError(""); - await api("/api/datasets", { + const created = await api("/api/datasets", { method: "POST", body: JSON.stringify({ name: datasetName, description: datasetDescription }) }); - setSelectedDatasetName(datasetName); + setDatasetName(created.name); + setSelectedDatasetName(created.name); setUploadModalOpen(false); await refresh(); } catch (err) { @@ -997,7 +1089,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); + const uploadResult = await res.json() as DatasetUploadResponse; + setDatasetName(uploadResult.dataset.name); + setSelectedDatasetName(uploadResult.dataset.name); setUploadModalOpen(false); setUploadFilesByKind((current) => ({ ...current, [uploadKind]: null })); await refresh(); @@ -1265,6 +1359,7 @@ function App() { {(error || actionError) &&
{actionError || error}
} +
@@ -1447,12 +1542,12 @@ function App() { {dataset.name} · {dataset.counts.images}/{dataset.counts.masks} )) : ( - + )}
- {datasets.map((dataset) => ( + {datasets.length ? datasets.map((dataset) => (
- ))} + )) : ( +
+ + {dataLoading.refreshing ? dataLoading.phase : "暂无数据集"} +
+ )}
@@ -1503,6 +1603,12 @@ function App() { <> + ) : dataLoading.refreshing ? ( +
+ + {dataLoading.phase} + 正在读取数据集、缩略图和 mask 配对状态。 +
) : (

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

)} @@ -1555,8 +1661,12 @@ function App() { />
- - + +
diff --git a/frontend/src/styles.css b/frontend/src/styles.css index bab6a02..ee7b8cf 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -198,6 +198,12 @@ h2 { background: var(--cyan); } +.primary.ghost { + border: 1px solid var(--line); + background: #101310; + color: var(--ink); +} + .primary.danger { background: var(--red); } @@ -215,6 +221,74 @@ h2 { margin-bottom: 18px; } +.syncStrip { + min-width: 0; + display: grid; + grid-template-columns: auto minmax(0, 1fr) minmax(180px, 260px); + align-items: center; + gap: 12px; + padding: 10px 12px; + margin-bottom: 16px; + border: 1px solid rgba(115, 210, 222, 0.35); + border-radius: 8px; + background: rgba(14, 22, 20, 0.94); +} + +.syncMark { + width: 30px; + height: 30px; + display: grid; + place-items: center; + border-radius: 6px; + color: #0b0d0b; + background: var(--cyan); +} + +.syncStrip.loading .syncMark svg, +.panelLoading svg, +.datasetListEmpty svg { + animation: syncSpin 1s linear infinite; +} + +.syncText { + min-width: 0; + display: grid; + gap: 2px; +} + +.syncText strong, +.syncText span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.syncText span { + color: var(--muted); + font-size: 12px; +} + +.syncProgress { + height: 8px; + overflow: hidden; + border-radius: 999px; + border: 1px solid rgba(238, 242, 232, 0.12); + background: #080a08; +} + +.syncProgress span { + display: block; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, var(--green), var(--cyan)); + transition: width 220ms ease; +} + +@keyframes syncSpin { + to { transform: rotate(360deg); } +} + .metrics, .grid { display: grid; gap: 16px; @@ -953,6 +1027,33 @@ textarea { overflow: auto; } +.datasetListEmpty, +.panelLoading { + min-width: 0; + display: grid; + place-items: center; + gap: 8px; + padding: 18px; + border: 1px dashed var(--line); + border-radius: 7px; + background: rgba(8, 10, 8, 0.42); + color: var(--muted); + text-align: center; +} + +.panelLoading { + min-height: 260px; +} + +.panelLoading strong { + color: var(--ink); +} + +.panelLoading span, +.datasetListEmpty span { + font-size: 12px; +} + .datasetMiniItem { min-width: 0; display: grid;