Upload all selected dataset files with progress
This commit is contained in:
@@ -32,6 +32,7 @@ import "./styles.css";
|
|||||||
const DEFAULT_API_BASE =
|
const DEFAULT_API_BASE =
|
||||||
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8010` : "http://127.0.0.1:8010";
|
typeof window !== "undefined" ? `${window.location.protocol}//${window.location.hostname}:8010` : "http://127.0.0.1:8010";
|
||||||
const API_BASE = (import.meta.env.VITE_API_BASE ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
const API_BASE = (import.meta.env.VITE_API_BASE ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
||||||
|
const DATASET_UPLOAD_KINDS: UploadKind[] = ["images", "masks"];
|
||||||
|
|
||||||
type JobProgress = {
|
type JobProgress = {
|
||||||
percent: number | null;
|
percent: number | null;
|
||||||
@@ -225,6 +226,24 @@ type DataLoadingState = {
|
|||||||
updatedAt?: number | null;
|
updatedAt?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UploadKind = "images" | "masks";
|
||||||
|
type UploadStepStatus = "idle" | "queued" | "uploading" | "processing" | "done" | "failed";
|
||||||
|
|
||||||
|
type UploadKindProgress = {
|
||||||
|
status: UploadStepStatus;
|
||||||
|
percent: number;
|
||||||
|
files: number;
|
||||||
|
saved?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UploadProgressState = {
|
||||||
|
active: boolean;
|
||||||
|
phase: string;
|
||||||
|
overall: number;
|
||||||
|
currentKind?: UploadKind;
|
||||||
|
kinds: Record<UploadKind, UploadKindProgress>;
|
||||||
|
};
|
||||||
|
|
||||||
type GpuPayload = {
|
type GpuPayload = {
|
||||||
available: boolean;
|
available: boolean;
|
||||||
gpus: Array<{
|
gpus: Array<{
|
||||||
@@ -405,6 +424,39 @@ async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uploadDatasetKind(
|
||||||
|
datasetName: string,
|
||||||
|
kind: UploadKind,
|
||||||
|
files: File[],
|
||||||
|
onProgress: (loaded: number, total: number, status: "uploading" | "processing") => void
|
||||||
|
): Promise<DatasetUploadResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const body = new FormData();
|
||||||
|
files.forEach((file) => body.append("files", file));
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("POST", `${API_BASE}/api/datasets/${encodeURIComponent(datasetName)}/upload/${kind}`);
|
||||||
|
xhr.upload.onprogress = (event) => {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
onProgress(event.loaded, event.total, "uploading");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.upload.onload = () => onProgress(1, 1, "processing");
|
||||||
|
xhr.onerror = () => reject(new Error(`上传失败,无法连接后端 ${API_BASE}`));
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (xhr.status < 200 || xhr.status >= 300) {
|
||||||
|
reject(new Error(xhr.responseText || `上传失败:HTTP ${xhr.status}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(xhr.responseText) as DatasetUploadResponse);
|
||||||
|
} catch (err) {
|
||||||
|
reject(new Error(`上传结果解析失败:${err instanceof Error ? err.message : String(err)}`));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function artifactUrl(path: string) {
|
function artifactUrl(path: string) {
|
||||||
return `${API_BASE}/api/artifacts/${path.split("/").map(encodeURIComponent).join("/")}`;
|
return `${API_BASE}/api/artifacts/${path.split("/").map(encodeURIComponent).join("/")}`;
|
||||||
}
|
}
|
||||||
@@ -544,6 +596,18 @@ function writeDatasetCache(payload: DatasetCachePayload) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function emptyUploadProgress(): UploadProgressState {
|
||||||
|
return {
|
||||||
|
active: false,
|
||||||
|
phase: "",
|
||||||
|
overall: 0,
|
||||||
|
kinds: {
|
||||||
|
images: { status: "idle", percent: 0, files: 0 },
|
||||||
|
masks: { status: "idle", percent: 0, files: 0 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function useData() {
|
function useData() {
|
||||||
const cachedDatasetState = useMemo(() => readDatasetCache(), []);
|
const cachedDatasetState = useMemo(() => readDatasetCache(), []);
|
||||||
const [catalog, setCatalog] = useState<Catalog | null>(null);
|
const [catalog, setCatalog] = useState<Catalog | null>(null);
|
||||||
@@ -573,6 +637,16 @@ function useData() {
|
|||||||
updatedAt: cachedDatasetState?.savedAt ?? null
|
updatedAt: cachedDatasetState?.savedAt ?? null
|
||||||
});
|
});
|
||||||
const refreshSeqRef = useRef(0);
|
const refreshSeqRef = useRef(0);
|
||||||
|
const datasetsRef = useRef<UploadedDataset[]>(cachedDatasetState?.datasets ?? []);
|
||||||
|
const datasetValidationsRef = useRef<Record<string, DatasetValidation>>(cachedDatasetState?.datasetValidations ?? {});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
datasetsRef.current = datasets;
|
||||||
|
}, [datasets]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
datasetValidationsRef.current = datasetValidations;
|
||||||
|
}, [datasetValidations]);
|
||||||
|
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
const seq = refreshSeqRef.current + 1;
|
const seq = refreshSeqRef.current + 1;
|
||||||
@@ -582,11 +656,11 @@ function useData() {
|
|||||||
setDataLoading((current) => ({ ...current, ...patch }));
|
setDataLoading((current) => ({ ...current, ...patch }));
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
updateLoading({ refreshing: true, phase: "读取数据集", progress: 8, cached: datasets.length > 0 });
|
updateLoading({ refreshing: true, phase: "读取数据集", progress: 8, cached: datasetsRef.current.length > 0 });
|
||||||
const datasetsNext = await api<UploadedDataset[]>("/api/datasets");
|
const datasetsNext = await api<UploadedDataset[]>("/api/datasets");
|
||||||
if (refreshSeqRef.current !== seq) return;
|
if (refreshSeqRef.current !== seq) return;
|
||||||
setDatasets(datasetsNext);
|
setDatasets(datasetsNext);
|
||||||
writeDatasetCache({ datasets: datasetsNext, datasetValidations, savedAt: Date.now() });
|
writeDatasetCache({ datasets: datasetsNext, datasetValidations: datasetValidationsRef.current, savedAt: Date.now() });
|
||||||
|
|
||||||
updateLoading({ phase: "校验图片和 masks", progress: 32 });
|
updateLoading({ phase: "校验图片和 masks", progress: 32 });
|
||||||
const validationResults = await Promise.allSettled(
|
const validationResults = await Promise.allSettled(
|
||||||
@@ -646,9 +720,9 @@ function useData() {
|
|||||||
setError(String(err));
|
setError(String(err));
|
||||||
updateLoading({
|
updateLoading({
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
phase: datasets.length ? "同步失败,继续显示缓存" : "同步失败",
|
phase: datasetsRef.current.length ? "同步失败,继续显示缓存" : "同步失败",
|
||||||
progress: datasets.length ? 100 : 0,
|
progress: datasetsRef.current.length ? 100 : 0,
|
||||||
cached: datasets.length > 0
|
cached: datasetsRef.current.length > 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -700,6 +774,44 @@ function DataSyncStrip({ loading }: { loading: DataLoadingState }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function UploadProgressPanel({ progress }: { progress: UploadProgressState }) {
|
||||||
|
if (!progress.phase) return null;
|
||||||
|
const visibleKinds = DATASET_UPLOAD_KINDS.filter((kind) => progress.kinds[kind].files > 0 || progress.kinds[kind].status !== "idle");
|
||||||
|
const labels: Record<UploadStepStatus, string> = {
|
||||||
|
idle: "未选择",
|
||||||
|
queued: "等待",
|
||||||
|
uploading: "上传中",
|
||||||
|
processing: "保存中",
|
||||||
|
done: "完成",
|
||||||
|
failed: "失败"
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={`uploadProgressPanel ${progress.active ? "active" : ""}`}>
|
||||||
|
<div className="uploadProgressHead">
|
||||||
|
<strong>{progress.phase}</strong>
|
||||||
|
<span>{Math.max(0, Math.min(100, progress.overall))}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="uploadProgressBar" aria-label="上传总进度">
|
||||||
|
<span style={{ width: `${Math.max(0, Math.min(100, progress.overall))}%` }} />
|
||||||
|
</div>
|
||||||
|
<div className="uploadProgressRows">
|
||||||
|
{visibleKinds.map((kind) => {
|
||||||
|
const item = progress.kinds[kind];
|
||||||
|
return (
|
||||||
|
<div key={kind} className={`uploadProgressRow ${item.status}`}>
|
||||||
|
<div>
|
||||||
|
<strong>{kind}</strong>
|
||||||
|
<small>{item.files} 个文件{typeof item.saved === "number" ? ` · 已保存 ${item.saved}` : ""}</small>
|
||||||
|
</div>
|
||||||
|
<span>{labels[item.status]} · {item.percent}%</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</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, dataLoading, 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);
|
||||||
@@ -727,8 +839,9 @@ function App() {
|
|||||||
const [resultQuery, setResultQuery] = useState("");
|
const [resultQuery, setResultQuery] = useState("");
|
||||||
const [selectedGpuIds, setSelectedGpuIds] = useState<number[]>([]);
|
const [selectedGpuIds, setSelectedGpuIds] = useState<number[]>([]);
|
||||||
const [selectedCondaEnv, setSelectedCondaEnv] = useState("auto");
|
const [selectedCondaEnv, setSelectedCondaEnv] = useState("auto");
|
||||||
const [uploadKind, setUploadKind] = useState<"images" | "masks">("images");
|
const [uploadKind, setUploadKind] = useState<UploadKind>("images");
|
||||||
const [uploadFilesByKind, setUploadFilesByKind] = useState<Record<"images" | "masks", FileList | null>>({ images: null, masks: null });
|
const [uploadFilesByKind, setUploadFilesByKind] = useState<Record<UploadKind, FileList | null>>({ images: null, masks: null });
|
||||||
|
const [uploadProgress, setUploadProgress] = useState<UploadProgressState>(() => emptyUploadProgress());
|
||||||
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
|
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
|
||||||
const [weightVerification, setWeightVerification] = useState<WeightVerifyPayload | null>(null);
|
const [weightVerification, setWeightVerification] = useState<WeightVerifyPayload | null>(null);
|
||||||
const [agentBusy, setAgentBusy] = useState(false);
|
const [agentBusy, setAgentBusy] = useState(false);
|
||||||
@@ -853,6 +966,11 @@ function App() {
|
|||||||
? "image/*,video/*,.zip,.tar,.tar.gz,.tgz"
|
? "image/*,video/*,.zip,.tar,.tar.gz,.tgz"
|
||||||
: "image/*,.txt,.json,.yaml,.yml,.zip,.tar,.tar.gz,.tgz";
|
: "image/*,.txt,.json,.yaml,.yml,.zip,.tar,.tar.gz,.tgz";
|
||||||
const activeUploadFiles = uploadFilesByKind[uploadKind];
|
const activeUploadFiles = uploadFilesByKind[uploadKind];
|
||||||
|
const uploadKindsWithFiles = DATASET_UPLOAD_KINDS.filter((kind) => (uploadFilesByKind[kind]?.length ?? 0) > 0);
|
||||||
|
const totalUploadFiles = uploadKindsWithFiles.reduce((total, kind) => total + (uploadFilesByKind[kind]?.length ?? 0), 0);
|
||||||
|
const uploadSelectionLabel = uploadKindsWithFiles.length
|
||||||
|
? uploadKindsWithFiles.map((kind) => `${kind} ${uploadFilesByKind[kind]?.length ?? 0}`).join(" / ")
|
||||||
|
: `当前 ${uploadKind}`;
|
||||||
const inferenceOutputs = useMemo(() => {
|
const inferenceOutputs = useMemo(() => {
|
||||||
const predictions = results.filter((item) => item.role === "segmentation" && item.previewable).slice(0, 8);
|
const predictions = results.filter((item) => item.role === "segmentation" && item.previewable).slice(0, 8);
|
||||||
const heatmaps = results.filter((item) => item.role === "heatmap" && item.previewable).slice(0, 8);
|
const heatmaps = results.filter((item) => item.role === "heatmap" && item.previewable).slice(0, 8);
|
||||||
@@ -1072,31 +1190,95 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function uploadDatasetFiles() {
|
async function uploadDatasetFiles() {
|
||||||
const files = uploadFilesByKind[uploadKind];
|
const pendingUploads = DATASET_UPLOAD_KINDS
|
||||||
if (!files || files.length === 0) return;
|
.map((kind) => ({ kind, files: Array.from(uploadFilesByKind[kind] ?? []) }))
|
||||||
|
.filter((item) => item.files.length > 0);
|
||||||
|
if (!pendingUploads.length) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
|
let runningKind: UploadKind | undefined;
|
||||||
try {
|
try {
|
||||||
setActionError("");
|
setActionError("");
|
||||||
const body = new FormData();
|
setUploadProgress({
|
||||||
Array.from(files).forEach((file) => body.append("files", file));
|
active: true,
|
||||||
let res: Response;
|
phase: "准备上传",
|
||||||
try {
|
overall: 0,
|
||||||
res = await fetch(`${API_BASE}/api/datasets/${encodeURIComponent(datasetName)}/upload/${uploadKind}`, {
|
kinds: {
|
||||||
method: "POST",
|
images: {
|
||||||
body
|
status: uploadFilesByKind.images?.length ? "queued" : "idle",
|
||||||
|
percent: 0,
|
||||||
|
files: uploadFilesByKind.images?.length ?? 0
|
||||||
|
},
|
||||||
|
masks: {
|
||||||
|
status: uploadFilesByKind.masks?.length ? "queued" : "idle",
|
||||||
|
percent: 0,
|
||||||
|
files: uploadFilesByKind.masks?.length ?? 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let targetDatasetName = datasetName;
|
||||||
|
let latestDatasetName = datasetName;
|
||||||
|
for (let index = 0; index < pendingUploads.length; index += 1) {
|
||||||
|
const { kind, files } = pendingUploads[index];
|
||||||
|
runningKind = kind;
|
||||||
|
setUploadProgress((current) => ({
|
||||||
|
...current,
|
||||||
|
currentKind: kind,
|
||||||
|
phase: `上传 ${kind}`,
|
||||||
|
kinds: {
|
||||||
|
...current.kinds,
|
||||||
|
[kind]: { ...current.kinds[kind], status: "uploading", percent: 0 }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const uploadResult = await uploadDatasetKind(targetDatasetName, kind, files, (loaded, total, status) => {
|
||||||
|
const rawPercent = total > 0 ? Math.round((loaded / total) * 100) : 0;
|
||||||
|
const percent = status === "processing" ? Math.max(rawPercent, 96) : rawPercent;
|
||||||
|
const overall = Math.round(((index + percent / 100) / pendingUploads.length) * 100);
|
||||||
|
setUploadProgress((current) => ({
|
||||||
|
...current,
|
||||||
|
currentKind: kind,
|
||||||
|
phase: status === "processing" ? `保存 ${kind}` : `上传 ${kind}`,
|
||||||
|
overall,
|
||||||
|
kinds: {
|
||||||
|
...current.kinds,
|
||||||
|
[kind]: { ...current.kinds[kind], status, percent }
|
||||||
|
}
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
} catch (err) {
|
latestDatasetName = uploadResult.dataset.name;
|
||||||
throw new Error(`上传失败,无法连接后端 ${API_BASE}:${err instanceof Error ? err.message : String(err)}`);
|
targetDatasetName = uploadResult.dataset.name;
|
||||||
|
setUploadProgress((current) => ({
|
||||||
|
...current,
|
||||||
|
overall: Math.round(((index + 1) / pendingUploads.length) * 100),
|
||||||
|
kinds: {
|
||||||
|
...current.kinds,
|
||||||
|
[kind]: { ...current.kinds[kind], status: "done", percent: 100, saved: uploadResult.saved.length }
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
if (!res.ok) throw new Error(await res.text());
|
setDatasetName(latestDatasetName);
|
||||||
const uploadResult = await res.json() as DatasetUploadResponse;
|
setSelectedDatasetName(latestDatasetName);
|
||||||
setDatasetName(uploadResult.dataset.name);
|
setUploadProgress((current) => ({ ...current, active: true, phase: "刷新图片墙", overall: 100 }));
|
||||||
setSelectedDatasetName(uploadResult.dataset.name);
|
setUploadFilesByKind((current) => {
|
||||||
setUploadModalOpen(false);
|
const next = { ...current };
|
||||||
setUploadFilesByKind((current) => ({ ...current, [uploadKind]: null }));
|
for (const { kind } of pendingUploads) next[kind] = null;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
await refresh();
|
await refresh();
|
||||||
|
setUploadModalOpen(false);
|
||||||
|
window.setTimeout(() => setUploadProgress(emptyUploadProgress()), 500);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setActionError(String(err));
|
setActionError(String(err));
|
||||||
|
setUploadProgress((current) => ({
|
||||||
|
...current,
|
||||||
|
active: false,
|
||||||
|
phase: "上传失败",
|
||||||
|
kinds: runningKind
|
||||||
|
? {
|
||||||
|
...current.kinds,
|
||||||
|
[runningKind]: { ...current.kinds[runningKind], status: "failed" }
|
||||||
|
}
|
||||||
|
: current.kinds
|
||||||
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -1521,7 +1703,15 @@ function App() {
|
|||||||
<p className="eyebrow">Dataset Bench</p>
|
<p className="eyebrow">Dataset Bench</p>
|
||||||
<h2>数据集管理</h2>
|
<h2>数据集管理</h2>
|
||||||
</div>
|
</div>
|
||||||
<button className="iconButton addDatasetButton" type="button" onClick={() => setUploadModalOpen(true)} title="新建或上传数据集">
|
<button
|
||||||
|
className="iconButton addDatasetButton"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setUploadProgress(emptyUploadProgress());
|
||||||
|
setUploadModalOpen(true);
|
||||||
|
}}
|
||||||
|
title="新建或上传数据集"
|
||||||
|
>
|
||||||
<Plus size={19} />
|
<Plus size={19} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1623,23 +1813,33 @@ function App() {
|
|||||||
<p className="eyebrow">Upload</p>
|
<p className="eyebrow">Upload</p>
|
||||||
<h2>新建或追加数据集</h2>
|
<h2>新建或追加数据集</h2>
|
||||||
</div>
|
</div>
|
||||||
<button className="iconButton" type="button" onClick={() => setUploadModalOpen(false)} title="关闭">
|
<button
|
||||||
|
className="iconButton"
|
||||||
|
type="button"
|
||||||
|
disabled={uploadProgress.active}
|
||||||
|
onClick={() => {
|
||||||
|
setUploadModalOpen(false);
|
||||||
|
setUploadProgress(emptyUploadProgress());
|
||||||
|
}}
|
||||||
|
title="关闭"
|
||||||
|
>
|
||||||
<X size={18} />
|
<X size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="datasetForm">
|
<div className="datasetForm">
|
||||||
<label className="field compact">
|
<label className="field compact">
|
||||||
<span>数据集名称</span>
|
<span>数据集名称</span>
|
||||||
<input value={datasetName} onChange={(event) => setDatasetName(event.target.value)} />
|
<input disabled={uploadProgress.active} value={datasetName} onChange={(event) => setDatasetName(event.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<label className="field compact">
|
<label className="field compact">
|
||||||
<span>说明</span>
|
<span>说明</span>
|
||||||
<input value={datasetDescription} onChange={(event) => setDatasetDescription(event.target.value)} />
|
<input disabled={uploadProgress.active} value={datasetDescription} onChange={(event) => setDatasetDescription(event.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
{datasetNameExists && <small className="formWarn">将追加到现有数据集的 {uploadKind},不会新建同名数据集。</small>}
|
{datasetNameExists && <small className="formWarn">将追加所有已选择文件到现有数据集:{uploadSelectionLabel}。</small>}
|
||||||
|
{!datasetNameExists && totalUploadFiles > 0 && <small className="formHint">将创建数据集并上传:{uploadSelectionLabel}。</small>}
|
||||||
<div className="segmented">
|
<div className="segmented">
|
||||||
{(["images", "masks"] as const).map((kind) => (
|
{DATASET_UPLOAD_KINDS.map((kind) => (
|
||||||
<button key={kind} type="button" className={uploadKind === kind ? "active" : ""} onClick={() => setUploadKind(kind)}>
|
<button key={kind} type="button" disabled={uploadProgress.active} className={uploadKind === kind ? "active" : ""} onClick={() => setUploadKind(kind)}>
|
||||||
{kind}
|
{kind}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -1656,15 +1856,17 @@ function App() {
|
|||||||
key={uploadKind}
|
key={uploadKind}
|
||||||
multiple
|
multiple
|
||||||
type="file"
|
type="file"
|
||||||
|
disabled={uploadProgress.active}
|
||||||
accept={uploadAccept}
|
accept={uploadAccept}
|
||||||
onChange={(event) => setUploadFilesByKind((current) => ({ ...current, [uploadKind]: event.target.files }))}
|
onChange={(event) => setUploadFilesByKind((current) => ({ ...current, [uploadKind]: event.target.files }))}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<UploadProgressPanel progress={uploadProgress} />
|
||||||
<div className="modalActions">
|
<div className="modalActions">
|
||||||
<button className="primary" disabled={busy || !activeUploadFiles?.length} onClick={uploadDatasetFiles}>
|
<button className="primary" disabled={busy || totalUploadFiles === 0} onClick={uploadDatasetFiles}>
|
||||||
<UploadCloud size={17} />{datasetNameExists ? "追加文件" : "创建并上传"}
|
<UploadCloud size={17} />{uploadProgress.active ? "上传中..." : datasetNameExists ? "追加已选择文件" : "创建并上传全部"}
|
||||||
</button>
|
</button>
|
||||||
<button className="primary ghost" disabled={busy || datasetNameExists} onClick={createDataset}>
|
<button className="primary ghost" disabled={busy || datasetNameExists || uploadProgress.active} onClick={createDataset}>
|
||||||
<Boxes size={17} />仅创建空数据集
|
<Boxes size={17} />仅创建空数据集
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -652,6 +652,12 @@ textarea {
|
|||||||
background: var(--field);
|
background: var(--field);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field input:disabled,
|
||||||
|
.field select:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.pathStack,
|
.pathStack,
|
||||||
.inferenceForm,
|
.inferenceForm,
|
||||||
.modelList,
|
.modelList,
|
||||||
@@ -831,12 +837,20 @@ textarea {
|
|||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
.formWarn {
|
.formWarn,
|
||||||
|
.formHint {
|
||||||
display: block;
|
display: block;
|
||||||
color: var(--amber);
|
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.formWarn {
|
||||||
|
color: var(--amber);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formHint {
|
||||||
|
color: var(--cyan);
|
||||||
|
}
|
||||||
|
|
||||||
.modalOverlay {
|
.modalOverlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@@ -913,6 +927,11 @@ textarea {
|
|||||||
font-weight: 760;
|
font-weight: 760;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.segmented button:disabled {
|
||||||
|
opacity: 0.62;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.uploadKindCounts {
|
.uploadKindCounts {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -966,6 +985,119 @@ textarea {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drop input:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressPanel {
|
||||||
|
display: grid;
|
||||||
|
gap: 9px;
|
||||||
|
padding: 11px;
|
||||||
|
border: 1px solid rgba(115, 210, 222, 0.32);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(8, 12, 12, 0.74);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressPanel.active {
|
||||||
|
border-color: rgba(115, 210, 222, 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressHead {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressHead strong,
|
||||||
|
.uploadProgressHead span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressHead span {
|
||||||
|
color: var(--cyan);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 760;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressBar {
|
||||||
|
height: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #080a08;
|
||||||
|
border: 1px solid rgba(238, 242, 232, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressBar span {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(90deg, var(--green), var(--cyan));
|
||||||
|
transition: width 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressRows {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressRow {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 7px 8px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #101310;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressRow > div {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressRow strong,
|
||||||
|
.uploadProgressRow small,
|
||||||
|
.uploadProgressRow > span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressRow small,
|
||||||
|
.uploadProgressRow > span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressRow.uploading,
|
||||||
|
.uploadProgressRow.processing {
|
||||||
|
border-color: rgba(115, 210, 222, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressRow.done {
|
||||||
|
border-color: rgba(157, 226, 111, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressRow.failed {
|
||||||
|
border-color: rgba(240, 113, 103, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressRow.done > span {
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgressRow.failed > span {
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
.buttonRow {
|
.buttonRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user