Cache dataset loading and clarify uploads
This commit is contained in:
@@ -95,6 +95,11 @@ type UploadedDataset = {
|
|||||||
files?: Record<string, DatasetFileItem[]>;
|
files?: Record<string, DatasetFileItem[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DatasetUploadResponse = {
|
||||||
|
dataset: UploadedDataset;
|
||||||
|
saved: Array<{ name: string; relative_path: string; size: number; from_archive?: boolean }>;
|
||||||
|
};
|
||||||
|
|
||||||
type DatasetValidation = {
|
type DatasetValidation = {
|
||||||
dataset: string;
|
dataset: string;
|
||||||
counts: { images: number; labels: number; masks: number; annotations: number };
|
counts: { images: number; labels: number; masks: number; annotations: number };
|
||||||
@@ -212,6 +217,14 @@ type DeepAcceptancePayload = {
|
|||||||
checks?: Array<{ name: string; passed: boolean }>;
|
checks?: Array<{ name: string; passed: boolean }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DataLoadingState = {
|
||||||
|
refreshing: boolean;
|
||||||
|
phase: string;
|
||||||
|
progress: number;
|
||||||
|
cached: boolean;
|
||||||
|
updatedAt?: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
type GpuPayload = {
|
type GpuPayload = {
|
||||||
available: boolean;
|
available: boolean;
|
||||||
gpus: Array<{
|
gpus: Array<{
|
||||||
@@ -504,7 +517,35 @@ function fileName(path?: string | null) {
|
|||||||
return path.split("/").filter(Boolean).pop() ?? path;
|
return path.split("/").filter(Boolean).pop() ?? path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DATA_CACHE_KEY = "seg_data_server_net.datasets.v1";
|
||||||
|
|
||||||
|
type DatasetCachePayload = {
|
||||||
|
datasets: UploadedDataset[];
|
||||||
|
datasetValidations: Record<string, DatasetValidation>;
|
||||||
|
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() {
|
function useData() {
|
||||||
|
const cachedDatasetState = useMemo(() => readDatasetCache(), []);
|
||||||
const [catalog, setCatalog] = useState<Catalog | null>(null);
|
const [catalog, setCatalog] = useState<Catalog | null>(null);
|
||||||
const [gpus, setGpus] = useState<GpuPayload | null>(null);
|
const [gpus, setGpus] = useState<GpuPayload | null>(null);
|
||||||
const [condaEnvs, setCondaEnvs] = useState<CondaEnvPayload | null>(null);
|
const [condaEnvs, setCondaEnvs] = useState<CondaEnvPayload | null>(null);
|
||||||
@@ -512,8 +553,8 @@ function useData() {
|
|||||||
const [results, setResults] = useState<ResultItem[]>([]);
|
const [results, setResults] = useState<ResultItem[]>([]);
|
||||||
const [curves, setCurves] = useState<TrainingCurve[]>([]);
|
const [curves, setCurves] = useState<TrainingCurve[]>([]);
|
||||||
const [weightManifest, setWeightManifest] = useState<WeightManifest | null>(null);
|
const [weightManifest, setWeightManifest] = useState<WeightManifest | null>(null);
|
||||||
const [datasets, setDatasets] = useState<UploadedDataset[]>([]);
|
const [datasets, setDatasets] = useState<UploadedDataset[]>(cachedDatasetState?.datasets ?? []);
|
||||||
const [datasetValidations, setDatasetValidations] = useState<Record<string, DatasetValidation>>({});
|
const [datasetValidations, setDatasetValidations] = useState<Record<string, DatasetValidation>>(cachedDatasetState?.datasetValidations ?? {});
|
||||||
const [coverage, setCoverage] = useState<CoveragePayload | null>(null);
|
const [coverage, setCoverage] = useState<CoveragePayload | null>(null);
|
||||||
const [acceptance, setAcceptance] = useState<AcceptancePayload | null>(null);
|
const [acceptance, setAcceptance] = useState<AcceptancePayload | null>(null);
|
||||||
const [realAcceptance, setRealAcceptance] = useState<AcceptancePayload | null>(null);
|
const [realAcceptance, setRealAcceptance] = useState<AcceptancePayload | null>(null);
|
||||||
@@ -524,10 +565,46 @@ function useData() {
|
|||||||
const [agentEvaluation, setAgentEvaluation] = useState<EvaluationAgentPayload | null>(null);
|
const [agentEvaluation, setAgentEvaluation] = useState<EvaluationAgentPayload | null>(null);
|
||||||
const [userAgent, setUserAgent] = useState<UserAgentPayload | null>(null);
|
const [userAgent, setUserAgent] = useState<UserAgentPayload | null>(null);
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
const [dataLoading, setDataLoading] = useState<DataLoadingState>({
|
||||||
|
refreshing: false,
|
||||||
|
phase: cachedDatasetState ? "使用本地缓存" : "等待加载",
|
||||||
|
progress: cachedDatasetState ? 100 : 0,
|
||||||
|
cached: Boolean(cachedDatasetState),
|
||||||
|
updatedAt: cachedDatasetState?.savedAt ?? null
|
||||||
|
});
|
||||||
|
const refreshSeqRef = useRef(0);
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
|
const seq = refreshSeqRef.current + 1;
|
||||||
|
refreshSeqRef.current = seq;
|
||||||
|
const updateLoading = (patch: Partial<DataLoadingState>) => {
|
||||||
|
if (refreshSeqRef.current !== seq) return;
|
||||||
|
setDataLoading((current) => ({ ...current, ...patch }));
|
||||||
|
};
|
||||||
try {
|
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<UploadedDataset[]>("/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<DatasetValidation>(`/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<readonly [string, DatasetValidation]> => 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<Catalog>("/api/catalog"),
|
api<Catalog>("/api/catalog"),
|
||||||
api<GpuPayload>("/api/system/gpus"),
|
api<GpuPayload>("/api/system/gpus"),
|
||||||
api<CondaEnvPayload>("/api/system/envs"),
|
api<CondaEnvPayload>("/api/system/envs"),
|
||||||
@@ -537,7 +614,6 @@ function useData() {
|
|||||||
api<ResultItem[]>("/api/results?limit=1000"),
|
api<ResultItem[]>("/api/results?limit=1000"),
|
||||||
api<TrainingCurve[]>("/api/results/curves?limit=100"),
|
api<TrainingCurve[]>("/api/results/curves?limit=100"),
|
||||||
api<WeightManifest>("/api/weights"),
|
api<WeightManifest>("/api/weights"),
|
||||||
api<UploadedDataset[]>("/api/datasets"),
|
|
||||||
api<CoveragePayload>("/api/coverage"),
|
api<CoveragePayload>("/api/coverage"),
|
||||||
api<AcceptancePayload>("/api/acceptance/latest"),
|
api<AcceptancePayload>("/api/acceptance/latest"),
|
||||||
api<AcceptancePayload>("/api/acceptance/real/latest"),
|
api<AcceptancePayload>("/api/acceptance/real/latest"),
|
||||||
@@ -545,39 +621,35 @@ function useData() {
|
|||||||
api<DeepAcceptancePayload>("/api/acceptance/deep/latest"),
|
api<DeepAcceptancePayload>("/api/acceptance/deep/latest"),
|
||||||
api<EvaluationAgentPayload>("/api/agents/evaluate"),
|
api<EvaluationAgentPayload>("/api/agents/evaluate"),
|
||||||
api<UserAgentPayload>("/api/agents/user/latest")
|
api<UserAgentPayload>("/api/agents/user/latest")
|
||||||
]);
|
] as const);
|
||||||
setCatalog(catalogNext);
|
if (refreshSeqRef.current !== seq) return;
|
||||||
setGpus(gpusNext);
|
const secondaryErrors: string[] = [];
|
||||||
setCondaEnvs(envsNext);
|
if (catalogNext.status === "fulfilled") setCatalog(catalogNext.value); else secondaryErrors.push(String(catalogNext.reason));
|
||||||
setRuntimeReadiness(readinessNext);
|
if (gpusNext.status === "fulfilled") setGpus(gpusNext.value); else secondaryErrors.push(String(gpusNext.reason));
|
||||||
setCapabilities(capabilitiesNext);
|
if (envsNext.status === "fulfilled") setCondaEnvs(envsNext.value); else secondaryErrors.push(String(envsNext.reason));
|
||||||
setJobs(jobsNext);
|
if (readinessNext.status === "fulfilled") setRuntimeReadiness(readinessNext.value); else secondaryErrors.push(String(readinessNext.reason));
|
||||||
setResults(resultsNext);
|
if (capabilitiesNext.status === "fulfilled") setCapabilities(capabilitiesNext.value); else secondaryErrors.push(String(capabilitiesNext.reason));
|
||||||
setCurves(curvesNext);
|
if (jobsNext.status === "fulfilled") setJobs(jobsNext.value); else secondaryErrors.push(String(jobsNext.reason));
|
||||||
setWeightManifest(weightsNext);
|
if (resultsNext.status === "fulfilled") setResults(resultsNext.value); else secondaryErrors.push(String(resultsNext.reason));
|
||||||
setDatasets(datasetsNext);
|
if (curvesNext.status === "fulfilled") setCurves(curvesNext.value); else secondaryErrors.push(String(curvesNext.reason));
|
||||||
const validationEntries: Array<[string, DatasetValidation]> = [];
|
if (weightsNext.status === "fulfilled") setWeightManifest(weightsNext.value); else secondaryErrors.push(String(weightsNext.reason));
|
||||||
await Promise.all(
|
if (coverageNext.status === "fulfilled") setCoverage(coverageNext.value); else secondaryErrors.push(String(coverageNext.reason));
|
||||||
datasetsNext.map(async (dataset) => {
|
if (acceptanceNext.status === "fulfilled") setAcceptance(acceptanceNext.value); else secondaryErrors.push(String(acceptanceNext.reason));
|
||||||
try {
|
if (realAcceptanceNext.status === "fulfilled") setRealAcceptance(realAcceptanceNext.value); else secondaryErrors.push(String(realAcceptanceNext.reason));
|
||||||
const validation = await api<DatasetValidation>(`/api/datasets/${encodeURIComponent(dataset.name)}/validate`);
|
if (realTrainAcceptanceNext.status === "fulfilled") setRealTrainAcceptance(realTrainAcceptanceNext.value); else secondaryErrors.push(String(realTrainAcceptanceNext.reason));
|
||||||
validationEntries.push([dataset.name, validation]);
|
if (deepAcceptanceNext.status === "fulfilled") setDeepAcceptance(deepAcceptanceNext.value); else secondaryErrors.push(String(deepAcceptanceNext.reason));
|
||||||
} catch {
|
if (agentEvaluationNext.status === "fulfilled") setAgentEvaluation(agentEvaluationNext.value); else secondaryErrors.push(String(agentEvaluationNext.reason));
|
||||||
// Dataset validation is advisory; upload and job controls should remain usable.
|
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() });
|
||||||
);
|
|
||||||
setDatasetValidations(Object.fromEntries(validationEntries));
|
|
||||||
setCoverage(coverageNext);
|
|
||||||
setAcceptance(acceptanceNext);
|
|
||||||
setRealAcceptance(realAcceptanceNext);
|
|
||||||
setRealTrainAcceptance(realTrainAcceptanceNext);
|
|
||||||
setDeepAcceptance(deepAcceptanceNext);
|
|
||||||
setAgentEvaluation(agentEvaluationNext);
|
|
||||||
setUserAgent(userAgentNext);
|
|
||||||
setError("");
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(String(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 () => 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 }) {
|
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 (
|
||||||
|
<div className={`syncStrip ${loading.refreshing ? "loading" : ""}`} data-page-section="datasets">
|
||||||
|
<div className="syncMark">
|
||||||
|
<RefreshCcw size={15} />
|
||||||
|
</div>
|
||||||
|
<div className="syncText">
|
||||||
|
<strong>{loading.phase}</strong>
|
||||||
|
<span>{loading.cached ? "正在显示本地缓存,同步完成后会自动更新。" : "数据集列表、缩略图和配对状态会分阶段刷新。"}</span>
|
||||||
|
</div>
|
||||||
|
<div className="syncProgress" aria-label="数据同步进度">
|
||||||
|
<span style={{ width: `${percent}%` }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
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<PageId>(pageFromHash);
|
const [page, setPage] = useState<PageId>(pageFromHash);
|
||||||
const [taskType, setTaskType] = useState("mock.echo");
|
const [taskType, setTaskType] = useState("mock.echo");
|
||||||
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
|
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
|
||||||
@@ -965,11 +1056,12 @@ function App() {
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
setActionError("");
|
setActionError("");
|
||||||
await api("/api/datasets", {
|
const created = await api<UploadedDataset>("/api/datasets", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ name: datasetName, description: datasetDescription })
|
body: JSON.stringify({ name: datasetName, description: datasetDescription })
|
||||||
});
|
});
|
||||||
setSelectedDatasetName(datasetName);
|
setDatasetName(created.name);
|
||||||
|
setSelectedDatasetName(created.name);
|
||||||
setUploadModalOpen(false);
|
setUploadModalOpen(false);
|
||||||
await refresh();
|
await refresh();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -997,7 +1089,9 @@ function App() {
|
|||||||
throw new Error(`上传失败,无法连接后端 ${API_BASE}:${err instanceof Error ? err.message : String(err)}`);
|
throw new Error(`上传失败,无法连接后端 ${API_BASE}:${err instanceof Error ? err.message : String(err)}`);
|
||||||
}
|
}
|
||||||
if (!res.ok) throw new Error(await res.text());
|
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);
|
setUploadModalOpen(false);
|
||||||
setUploadFilesByKind((current) => ({ ...current, [uploadKind]: null }));
|
setUploadFilesByKind((current) => ({ ...current, [uploadKind]: null }));
|
||||||
await refresh();
|
await refresh();
|
||||||
@@ -1265,6 +1359,7 @@ function App() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{(error || actionError) && <div className="alert">{actionError || error}</div>}
|
{(error || actionError) && <div className="alert">{actionError || error}</div>}
|
||||||
|
<DataSyncStrip loading={dataLoading} />
|
||||||
|
|
||||||
<section className="metrics" data-page-section="overview">
|
<section className="metrics" data-page-section="overview">
|
||||||
<div className="metric">
|
<div className="metric">
|
||||||
@@ -1447,12 +1542,12 @@ function App() {
|
|||||||
{dataset.name} · {dataset.counts.images}/{dataset.counts.masks}
|
{dataset.name} · {dataset.counts.images}/{dataset.counts.masks}
|
||||||
</option>
|
</option>
|
||||||
)) : (
|
)) : (
|
||||||
<option value="">暂无数据集</option>
|
<option value="">{dataLoading.refreshing ? "正在加载数据集..." : "暂无数据集"}</option>
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<div className="datasetMiniList">
|
<div className="datasetMiniList">
|
||||||
{datasets.map((dataset) => (
|
{datasets.length ? datasets.map((dataset) => (
|
||||||
<div
|
<div
|
||||||
key={dataset.name}
|
key={dataset.name}
|
||||||
className={`datasetMiniItem ${selectedDataset?.name === dataset.name ? "selected" : ""}`}
|
className={`datasetMiniItem ${selectedDataset?.name === dataset.name ? "selected" : ""}`}
|
||||||
@@ -1473,7 +1568,12 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)) : (
|
||||||
|
<div className="datasetListEmpty">
|
||||||
|
<RefreshCcw size={17} />
|
||||||
|
<span>{dataLoading.refreshing ? dataLoading.phase : "暂无数据集"}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1503,6 +1603,12 @@ function App() {
|
|||||||
<>
|
<>
|
||||||
<DatasetDetail dataset={selectedDataset} validation={selectedValidation} />
|
<DatasetDetail dataset={selectedDataset} validation={selectedValidation} />
|
||||||
</>
|
</>
|
||||||
|
) : dataLoading.refreshing ? (
|
||||||
|
<div className="panelLoading">
|
||||||
|
<RefreshCcw size={20} />
|
||||||
|
<strong>{dataLoading.phase}</strong>
|
||||||
|
<span>正在读取数据集、缩略图和 mask 配对状态。</span>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="muted">左侧创建或选择一个数据集后,这里会显示图片、masks、配对和异常项。</p>
|
<p className="muted">左侧创建或选择一个数据集后,这里会显示图片、masks、配对和异常项。</p>
|
||||||
)}
|
)}
|
||||||
@@ -1555,8 +1661,12 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div className="modalActions">
|
<div className="modalActions">
|
||||||
<button className="primary" disabled={busy || datasetNameExists} onClick={createDataset}><Boxes size={17} />创建数据集</button>
|
<button className="primary" disabled={busy || !activeUploadFiles?.length} onClick={uploadDatasetFiles}>
|
||||||
<button className="primary secondary" disabled={busy || !activeUploadFiles?.length} onClick={uploadDatasetFiles}><UploadCloud size={17} />{datasetNameExists ? "追加文件" : "上传文件"}</button>
|
<UploadCloud size={17} />{datasetNameExists ? "追加文件" : "创建并上传"}
|
||||||
|
</button>
|
||||||
|
<button className="primary ghost" disabled={busy || datasetNameExists} onClick={createDataset}>
|
||||||
|
<Boxes size={17} />仅创建空数据集
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -198,6 +198,12 @@ h2 {
|
|||||||
background: var(--cyan);
|
background: var(--cyan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.primary.ghost {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: #101310;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
.primary.danger {
|
.primary.danger {
|
||||||
background: var(--red);
|
background: var(--red);
|
||||||
}
|
}
|
||||||
@@ -215,6 +221,74 @@ h2 {
|
|||||||
margin-bottom: 18px;
|
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 {
|
.metrics, .grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
@@ -953,6 +1027,33 @@ textarea {
|
|||||||
overflow: auto;
|
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 {
|
.datasetMiniItem {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user