@proofhound/web-ui 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/i18n/index.d.ts +44 -14
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +44 -14
- package/dist/i18n/index.js.map +1 -1
- package/dist/screens/datasets/dataset-import-runner.d.ts +23 -2
- package/dist/screens/datasets/dataset-import-runner.d.ts.map +1 -1
- package/dist/screens/datasets/dataset-import-runner.js +106 -0
- package/dist/screens/datasets/dataset-import-runner.js.map +1 -1
- package/dist/screens/datasets/dataset-upload-page.d.ts +17 -0
- package/dist/screens/datasets/dataset-upload-page.d.ts.map +1 -1
- package/dist/screens/datasets/dataset-upload-page.js +242 -46
- package/dist/screens/datasets/dataset-upload-page.js.map +1 -1
- package/dist/screens/datasets/dataset-upload-parser.d.ts +7 -2
- package/dist/screens/datasets/dataset-upload-parser.d.ts.map +1 -1
- package/dist/screens/datasets/dataset-upload-parser.js +163 -67
- package/dist/screens/datasets/dataset-upload-parser.js.map +1 -1
- package/package.json +12 -10
|
@@ -4,15 +4,15 @@ import { Link } from '../../components/navigation/link';
|
|
|
4
4
|
import { useRouter } from '../../hooks/use-router';
|
|
5
5
|
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
6
6
|
import { datasetImportClient } from '@proofhound/api-client';
|
|
7
|
-
import { AlertTriangle, Check, ChevronLeft, ChevronRight, FileText, Loader2, Upload } from 'lucide-react';
|
|
8
|
-
import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Progress, formatProgressLabel, cn, } from '@proofhound/ui';
|
|
7
|
+
import { AlertTriangle, Check, ChevronLeft, ChevronRight, Download, FileText, Info, Loader2, Upload, } from 'lucide-react';
|
|
8
|
+
import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Progress, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, formatProgressLabel, cn, } from '@proofhound/ui';
|
|
9
9
|
import { Main } from '@proofhound/ui/layout';
|
|
10
10
|
import { useCreateDataset } from '../../hooks';
|
|
11
11
|
import { useI18n } from '../../i18n';
|
|
12
|
-
import { runDatasetImport } from './dataset-import-runner';
|
|
12
|
+
import { projectSampleRowsToBatches, runDatasetImport, runRawDatasetImport } from './dataset-import-runner';
|
|
13
13
|
import { DatasetTransferProgressPanel, useDatasetTransferProgress } from './dataset-transfer-progress';
|
|
14
14
|
import { RoleArrowLabel, RolePill } from './dataset-ui';
|
|
15
|
-
import { FORMAT_CHIPS, PREVIEW_LIMIT, getDatasetNameFromFile, getDisplayValue, getUploadFilePath, inferRole,
|
|
15
|
+
import { FORMAT_CHIPS, PREVIEW_LIMIT, getDatasetNameFromFile, getDisplayValue, getUploadFilePath, inferRole, isStreamingImportFile, parseDatasetFile, parseStreamingPrefix, projectSamplesToColumns, selectDatasetFile, streamDatasetRows, } from './dataset-upload-parser';
|
|
16
16
|
const ROLE_OPTIONS = [
|
|
17
17
|
{ role: 'id', labelKey: 'datasets.role.id' },
|
|
18
18
|
{ role: 'text', labelKey: 'datasets.role.text' },
|
|
@@ -21,6 +21,28 @@ const ROLE_OPTIONS = [
|
|
|
21
21
|
{ role: 'metadata', labelKey: 'datasets.role.metadata' },
|
|
22
22
|
];
|
|
23
23
|
const directoryInputProps = { webkitdirectory: '', directory: '' };
|
|
24
|
+
export const DATASET_IMAGE_SAMPLE_DOWNLOADS = [
|
|
25
|
+
{
|
|
26
|
+
labelKey: 'datasets.upload.imageSamples.urlFields',
|
|
27
|
+
href: '/examples/datasets/images/image-url-fields.csv',
|
|
28
|
+
fileName: 'proofhound-image-url-fields.csv',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
labelKey: 'datasets.upload.imageSamples.urlArray',
|
|
32
|
+
href: '/examples/datasets/images/image-url-array.csv',
|
|
33
|
+
fileName: 'proofhound-image-url-array.csv',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
labelKey: 'datasets.upload.imageSamples.base64',
|
|
37
|
+
href: '/examples/datasets/images/image-base64.jsonl',
|
|
38
|
+
fileName: 'proofhound-image-base64.jsonl',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
labelKey: 'datasets.upload.imageSamples.zip',
|
|
42
|
+
href: '/examples/datasets/images/image-zip-relative-paths.zip',
|
|
43
|
+
fileName: 'proofhound-image-zip-relative-paths.zip',
|
|
44
|
+
},
|
|
45
|
+
];
|
|
24
46
|
function normalizeExpectedRoles(roles, preferredColumn) {
|
|
25
47
|
let expectedColumn = preferredColumn && roles[preferredColumn] === 'expected' ? preferredColumn : null;
|
|
26
48
|
if (!expectedColumn) {
|
|
@@ -37,12 +59,22 @@ function SectionNumber({ value }) {
|
|
|
37
59
|
function Section({ number, title, hint, children, className, }) {
|
|
38
60
|
return (_jsxs("section", { className: cn('rounded-lg border bg-card', className), children: [_jsxs("div", { className: "flex items-center gap-2 border-b px-4 py-3", children: [_jsx(SectionNumber, { value: number }), _jsx("h2", { className: "text-[14.5px] font-semibold", children: title }), _jsx("span", { className: "ml-auto text-[11.5px] text-muted-foreground", children: hint })] }), _jsx("div", { className: "p-4", children: children })] }));
|
|
39
61
|
}
|
|
40
|
-
function formatFileSize(bytes) {
|
|
62
|
+
export function formatFileSize(bytes) {
|
|
41
63
|
if (bytes < 1024)
|
|
42
64
|
return `${bytes} B`;
|
|
43
65
|
if (bytes < 1024 * 1024)
|
|
44
66
|
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
45
|
-
|
|
67
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
68
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
69
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
|
70
|
+
}
|
|
71
|
+
function formatByteLimit(bytes) {
|
|
72
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
73
|
+
return formatFileSize(bytes);
|
|
74
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
|
|
75
|
+
}
|
|
76
|
+
function formatTemplate(template, values) {
|
|
77
|
+
return Object.entries(values).reduce((output, [key, value]) => output.replaceAll(`{${key}}`, String(value)), template);
|
|
46
78
|
}
|
|
47
79
|
function withRelativePath(file, relativePath) {
|
|
48
80
|
Object.defineProperty(file, 'proofhoundRelativePath', {
|
|
@@ -96,38 +128,111 @@ async function getDroppedFiles(dataTransfer) {
|
|
|
96
128
|
function getParseErrorKey(parseError) {
|
|
97
129
|
if (parseError === 'unsupported_file_type')
|
|
98
130
|
return 'datasets.upload.unsupportedFile';
|
|
99
|
-
if (parseError === '
|
|
100
|
-
return 'datasets.upload.
|
|
131
|
+
if (parseError === 'large_requires_streaming_format')
|
|
132
|
+
return 'datasets.upload.largeRequiresStreamingFormat';
|
|
101
133
|
return 'datasets.upload.parseFailed';
|
|
102
134
|
}
|
|
103
|
-
function
|
|
104
|
-
return
|
|
135
|
+
export function estimateUploadProgressBytes(sourceFile) {
|
|
136
|
+
return Math.max(1, sourceFile.fileSizeBytes);
|
|
105
137
|
}
|
|
106
|
-
// Files larger than this are not parsed whole on drop: only a head prefix is read for preview
|
|
107
|
-
//
|
|
108
|
-
const SYNC_MAX_FILE_BYTES =
|
|
138
|
+
// Files larger than this are not parsed whole on drop: only a head prefix is read for preview.
|
|
139
|
+
// They prefer raw upload + server-side import when object storage supports browser upload sessions.
|
|
140
|
+
const SYNC_MAX_FILE_BYTES = 1024 * 1024;
|
|
109
141
|
// Below the file-size threshold a parsed dataset still routes through the import session once it exceeds
|
|
110
142
|
// this many samples, because the synchronous POST /datasets path is capped server-side.
|
|
111
143
|
const SYNC_MAX_SAMPLES = 5000;
|
|
112
144
|
const IMPORT_BATCH_SIZE = 1000;
|
|
145
|
+
const DEFAULT_RAW_UPLOAD_MAX_BYTES = 2 * 1024 * 1024 * 1024;
|
|
146
|
+
const RAW_BUFFERED_FORMAT_MAX_BYTES = 64 * 1024 * 1024;
|
|
113
147
|
function toImportSourceFormat(fileName) {
|
|
114
148
|
const lower = fileName.toLowerCase();
|
|
115
149
|
if (lower.endsWith('.csv'))
|
|
116
150
|
return 'csv';
|
|
117
151
|
if (lower.endsWith('.tsv'))
|
|
118
152
|
return 'tsv';
|
|
153
|
+
if (lower.endsWith('.json'))
|
|
154
|
+
return 'json';
|
|
155
|
+
if (lower.endsWith('.zip'))
|
|
156
|
+
return 'zip';
|
|
119
157
|
return 'jsonl';
|
|
120
158
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
159
|
+
function isStreamingImportFileName(fileName) {
|
|
160
|
+
const lower = fileName.toLowerCase();
|
|
161
|
+
return lower.endsWith('.jsonl') || lower.endsWith('.csv') || lower.endsWith('.tsv');
|
|
162
|
+
}
|
|
163
|
+
function isRawImportFileName(fileName) {
|
|
164
|
+
const lower = fileName.toLowerCase();
|
|
165
|
+
return (lower.endsWith('.jsonl') ||
|
|
166
|
+
lower.endsWith('.csv') ||
|
|
167
|
+
lower.endsWith('.tsv') ||
|
|
168
|
+
lower.endsWith('.json') ||
|
|
169
|
+
lower.endsWith('.zip'));
|
|
170
|
+
}
|
|
171
|
+
function canUseRawImportForFile(file, capabilities) {
|
|
172
|
+
if (capabilities?.supported !== true)
|
|
173
|
+
return false;
|
|
174
|
+
if (!isRawImportFileName(file.name) || file.size > capabilities.maxBytes)
|
|
175
|
+
return false;
|
|
176
|
+
return isStreamingImportFileName(file.name) || file.size <= RAW_BUFFERED_FORMAT_MAX_BYTES;
|
|
177
|
+
}
|
|
178
|
+
export function selectDatasetUploadImportPath({ file, isLargeFile, parsedSampleCount, rawImportCapabilities, }) {
|
|
179
|
+
if (file.size <= SYNC_MAX_FILE_BYTES && parsedSampleCount <= SYNC_MAX_SAMPLES) {
|
|
180
|
+
return 'sync';
|
|
124
181
|
}
|
|
182
|
+
if (canUseRawImportForFile(file, rawImportCapabilities)) {
|
|
183
|
+
return 'raw';
|
|
184
|
+
}
|
|
185
|
+
if (isLargeFile) {
|
|
186
|
+
return 'streaming';
|
|
187
|
+
}
|
|
188
|
+
return 'buffered';
|
|
189
|
+
}
|
|
190
|
+
function yieldToBrowser() {
|
|
191
|
+
return new Promise((resolve) => {
|
|
192
|
+
if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') {
|
|
193
|
+
setTimeout(resolve, 0);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
window.requestAnimationFrame(() => window.requestAnimationFrame(() => resolve()));
|
|
197
|
+
});
|
|
125
198
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
199
|
+
function throwIfAborted(signal) {
|
|
200
|
+
if (!signal?.aborted)
|
|
201
|
+
return;
|
|
202
|
+
throw new DOMException('aborted', 'AbortError');
|
|
203
|
+
}
|
|
204
|
+
export async function* projectBufferedSampleBatches(samples, columns, size, signal) {
|
|
205
|
+
async function* rows() {
|
|
206
|
+
for (let index = 0; index < samples.length; index += 1) {
|
|
207
|
+
throwIfAborted(signal);
|
|
208
|
+
if (index > 0 && index % size === 0)
|
|
209
|
+
await yieldToBrowser();
|
|
210
|
+
throwIfAborted(signal);
|
|
211
|
+
yield samples[index] ?? {};
|
|
212
|
+
}
|
|
130
213
|
}
|
|
214
|
+
yield* projectSampleRowsToBatches(rows(), columns, { maxRows: size, signal });
|
|
215
|
+
}
|
|
216
|
+
// Streams a large JSONL/CSV/TSV file off disk, projecting each batch to the selected columns before upload.
|
|
217
|
+
async function* projectedStreamingFileBatches(file, columns, size, onBytes, signal) {
|
|
218
|
+
yield* projectSampleRowsToBatches(streamDatasetRows(file, onBytes, signal), columns, { maxRows: size, signal });
|
|
219
|
+
}
|
|
220
|
+
function UploadLimitInfoIcon({ rawMaxBytes }) {
|
|
221
|
+
const { t } = useI18n();
|
|
222
|
+
const rawLimit = formatByteLimit(rawMaxBytes);
|
|
223
|
+
const syncLimit = formatByteLimit(SYNC_MAX_FILE_BYTES);
|
|
224
|
+
return (_jsx(TooltipProvider, { delayDuration: 140, children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx("button", { type: "button", className: "inline-flex size-5 items-center justify-center rounded-full text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "aria-label": t('datasets.upload.limitInfoLabel'), "data-testid": "dataset-upload-limit-info", children: _jsx(Info, { className: "size-3.5" }) }) }), _jsxs(TooltipContent, { side: "top", className: "max-w-[340px] text-left", children: [_jsx("div", { className: "text-[11.5px] font-semibold", children: t('datasets.upload.limitInfoTitle') }), _jsxs("div", { className: "mt-1.5 space-y-1 text-[11px] leading-relaxed", children: [_jsx("p", { children: formatTemplate(t('datasets.upload.limitInfoSmall'), {
|
|
225
|
+
syncLimit,
|
|
226
|
+
}) }), _jsx("p", { children: t('datasets.upload.limitInfoStreaming') }), _jsx("p", { children: formatTemplate(t('datasets.upload.limitInfoRaw'), {
|
|
227
|
+
rawLimit,
|
|
228
|
+
}) }), _jsx("p", { children: t('datasets.upload.limitInfoJsonZip') })] })] })] }) }));
|
|
229
|
+
}
|
|
230
|
+
function ImageSampleDownloads() {
|
|
231
|
+
const { t } = useI18n();
|
|
232
|
+
return (_jsxs("div", { className: "border-t pt-3", "data-testid": "dataset-upload-image-samples", children: [_jsxs("div", { className: "flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between", children: [_jsx("div", { className: "text-[12px] font-semibold", children: t('datasets.upload.imageSamples.title') }), _jsx("div", { className: "text-[11.5px] text-muted-foreground", children: t('datasets.upload.imageSamples.hint') })] }), _jsx("div", { className: "mt-2 flex flex-wrap gap-2", children: DATASET_IMAGE_SAMPLE_DOWNLOADS.map((sample) => {
|
|
233
|
+
const label = t(sample.labelKey);
|
|
234
|
+
return (_jsxs("a", { className: "inline-flex h-8 items-center gap-1.5 rounded-md border bg-background px-2.5 text-[11.5px] font-medium text-foreground transition-colors hover:bg-muted", href: sample.href, download: sample.fileName, "aria-label": formatTemplate(t('datasets.upload.imageSamples.downloadAria'), { name: label }), "data-testid": `dataset-image-sample-${sample.fileName}`, children: [_jsx(Download, { className: "size-3.5" }), label] }, sample.href));
|
|
235
|
+
}) })] }));
|
|
131
236
|
}
|
|
132
237
|
export function DatasetUploadPage({ projectId }) {
|
|
133
238
|
const { t } = useI18n();
|
|
@@ -146,15 +251,37 @@ export function DatasetUploadPage({ projectId }) {
|
|
|
146
251
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
147
252
|
const [isImporting, setIsImporting] = useState(false);
|
|
148
253
|
const [isLargeFile, setIsLargeFile] = useState(false);
|
|
254
|
+
const [rawImportCapabilities, setRawImportCapabilities] = useState(null);
|
|
149
255
|
const importAbortRef = useRef(null);
|
|
150
256
|
const importIdRef = useRef(null);
|
|
257
|
+
const abortOnLeaveRef = useRef(false);
|
|
151
258
|
const leaveActionRef = useRef(null);
|
|
152
259
|
const [leaveDialogOpen, setLeaveDialogOpen] = useState(false);
|
|
153
|
-
|
|
154
|
-
|
|
260
|
+
const [serverImportContinues, setServerImportContinues] = useState(false);
|
|
261
|
+
// Before raw upload is finalized, leaving can cancel the transfer. After server-side import starts, it continues.
|
|
262
|
+
useEffect(() => () => {
|
|
263
|
+
if (abortOnLeaveRef.current)
|
|
264
|
+
importAbortRef.current?.abort();
|
|
265
|
+
}, []);
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
let cancelled = false;
|
|
268
|
+
datasetImportClient
|
|
269
|
+
.getRawImportCapabilities(projectId)
|
|
270
|
+
.then((capabilities) => {
|
|
271
|
+
if (!cancelled)
|
|
272
|
+
setRawImportCapabilities(capabilities);
|
|
273
|
+
})
|
|
274
|
+
.catch(() => {
|
|
275
|
+
if (!cancelled)
|
|
276
|
+
setRawImportCapabilities({ supported: false, maxBytes: 1 });
|
|
277
|
+
});
|
|
278
|
+
return () => {
|
|
279
|
+
cancelled = true;
|
|
280
|
+
};
|
|
281
|
+
}, [projectId]);
|
|
155
282
|
// While an import is in flight, guard every way to leave so the user is warned before losing it.
|
|
156
283
|
useEffect(() => {
|
|
157
|
-
if (!isImporting)
|
|
284
|
+
if (!isImporting || serverImportContinues)
|
|
158
285
|
return undefined;
|
|
159
286
|
// Tab close / refresh / hard URL change: only the browser's native prompt is possible here.
|
|
160
287
|
const onBeforeUnload = (event) => {
|
|
@@ -208,10 +335,11 @@ export function DatasetUploadPage({ projectId }) {
|
|
|
208
335
|
document.removeEventListener('click', onClickCapture, true);
|
|
209
336
|
window.removeEventListener('popstate', onPopState);
|
|
210
337
|
};
|
|
211
|
-
}, [isImporting, router, projectId]);
|
|
338
|
+
}, [isImporting, serverImportContinues, router, projectId]);
|
|
212
339
|
const confirmLeaveImport = () => {
|
|
213
340
|
setLeaveDialogOpen(false);
|
|
214
|
-
|
|
341
|
+
if (abortOnLeaveRef.current)
|
|
342
|
+
importAbortRef.current?.abort();
|
|
215
343
|
setIsImporting(false);
|
|
216
344
|
const action = leaveActionRef.current;
|
|
217
345
|
leaveActionRef.current = null;
|
|
@@ -248,12 +376,13 @@ export function DatasetUploadPage({ projectId }) {
|
|
|
248
376
|
try {
|
|
249
377
|
const file = await selectDatasetFile(files);
|
|
250
378
|
const large = file.size > SYNC_MAX_FILE_BYTES;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
379
|
+
const canRawImportWholeFile = canUseRawImportForFile(file, rawImportCapabilities);
|
|
380
|
+
if (large && !isStreamingImportFile(file) && !canRawImportWholeFile) {
|
|
381
|
+
// Large JSON arrays / ZIPs are only previewed with a bounded parser before raw import.
|
|
382
|
+
throw new Error('large_requires_streaming_format');
|
|
254
383
|
}
|
|
255
|
-
//
|
|
256
|
-
const parsed = large ? await
|
|
384
|
+
// Streaming formats read only a head prefix; bounded JSON/ZIP raw imports may parse the small whole file for preview.
|
|
385
|
+
const parsed = large && isStreamingImportFile(file) ? await parseStreamingPrefix(file) : await parseDatasetFile(file);
|
|
257
386
|
setSelectedFile(file);
|
|
258
387
|
setParsedFile(parsed);
|
|
259
388
|
setIsLargeFile(large);
|
|
@@ -283,21 +412,70 @@ export function DatasetUploadPage({ projectId }) {
|
|
|
283
412
|
description: description.trim() || null,
|
|
284
413
|
fieldMappings,
|
|
285
414
|
sourceFile,
|
|
286
|
-
sourceFormat:
|
|
415
|
+
sourceFormat: toImportSourceFormat(sourceFile.fileName),
|
|
287
416
|
};
|
|
288
417
|
const controller = new AbortController();
|
|
289
418
|
importAbortRef.current = controller;
|
|
419
|
+
abortOnLeaveRef.current = true;
|
|
420
|
+
setServerImportContinues(false);
|
|
290
421
|
setIsImporting(true);
|
|
291
422
|
uploadProgress.start(t('datasets.transfer.uploadTitle'), totalBytes);
|
|
423
|
+
await yieldToBrowser();
|
|
292
424
|
try {
|
|
293
425
|
await runDatasetImport({
|
|
294
426
|
projectId,
|
|
295
427
|
createBody,
|
|
296
|
-
batches:
|
|
428
|
+
batches: projectedStreamingFileBatches(file, selectedColumns, IMPORT_BATCH_SIZE, (readBytes) => uploadProgress.update({ loadedBytes: readBytes, totalBytes }), controller.signal),
|
|
429
|
+
signal: controller.signal,
|
|
430
|
+
onCreated: (id) => {
|
|
431
|
+
importIdRef.current = id;
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
uploadProgress.complete(totalBytes);
|
|
435
|
+
router.push(`/datasets`);
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
uploadProgress.fail();
|
|
439
|
+
}
|
|
440
|
+
finally {
|
|
441
|
+
setIsImporting(false);
|
|
442
|
+
importAbortRef.current = null;
|
|
443
|
+
importIdRef.current = null;
|
|
444
|
+
abortOnLeaveRef.current = false;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
const importRawDataset = async (fieldMappings, sourceFile, file) => {
|
|
448
|
+
const totalBytes = file.size;
|
|
449
|
+
const createBody = {
|
|
450
|
+
name: datasetName.trim(),
|
|
451
|
+
description: description.trim() || null,
|
|
452
|
+
fieldMappings,
|
|
453
|
+
sourceFile,
|
|
454
|
+
sourceFormat: toImportSourceFormat(sourceFile.fileName),
|
|
455
|
+
};
|
|
456
|
+
const controller = new AbortController();
|
|
457
|
+
importAbortRef.current = controller;
|
|
458
|
+
abortOnLeaveRef.current = true;
|
|
459
|
+
setServerImportContinues(false);
|
|
460
|
+
setIsImporting(true);
|
|
461
|
+
uploadProgress.start(t('datasets.transfer.uploadTitle'), totalBytes);
|
|
462
|
+
await yieldToBrowser();
|
|
463
|
+
try {
|
|
464
|
+
await runRawDatasetImport({
|
|
465
|
+
projectId,
|
|
466
|
+
createBody,
|
|
467
|
+
file,
|
|
297
468
|
signal: controller.signal,
|
|
298
469
|
onCreated: (id) => {
|
|
299
470
|
importIdRef.current = id;
|
|
300
471
|
},
|
|
472
|
+
onUploaded: () => uploadProgress.update({ loadedBytes: totalBytes, totalBytes }),
|
|
473
|
+
onProgress: ({ phase }) => {
|
|
474
|
+
if (phase === 'queued' || phase === 'parsing' || phase === 'importing') {
|
|
475
|
+
abortOnLeaveRef.current = false;
|
|
476
|
+
setServerImportContinues(true);
|
|
477
|
+
}
|
|
478
|
+
},
|
|
301
479
|
});
|
|
302
480
|
uploadProgress.complete(totalBytes);
|
|
303
481
|
router.push(`/datasets`);
|
|
@@ -309,11 +487,13 @@ export function DatasetUploadPage({ projectId }) {
|
|
|
309
487
|
setIsImporting(false);
|
|
310
488
|
importAbortRef.current = null;
|
|
311
489
|
importIdRef.current = null;
|
|
490
|
+
abortOnLeaveRef.current = false;
|
|
491
|
+
setServerImportContinues(false);
|
|
312
492
|
}
|
|
313
493
|
};
|
|
314
|
-
const importBufferedDataset = async (fieldMappings, sourceFile, samples) => {
|
|
494
|
+
const importBufferedDataset = async (fieldMappings, sourceFile, samples, columns) => {
|
|
315
495
|
const totalRows = samples.length;
|
|
316
|
-
const estimatedBytes =
|
|
496
|
+
const estimatedBytes = estimateUploadProgressBytes(sourceFile);
|
|
317
497
|
const createBody = {
|
|
318
498
|
name: datasetName.trim(),
|
|
319
499
|
description: description.trim() || null,
|
|
@@ -324,13 +504,16 @@ export function DatasetUploadPage({ projectId }) {
|
|
|
324
504
|
};
|
|
325
505
|
const controller = new AbortController();
|
|
326
506
|
importAbortRef.current = controller;
|
|
507
|
+
abortOnLeaveRef.current = true;
|
|
508
|
+
setServerImportContinues(false);
|
|
327
509
|
setIsImporting(true);
|
|
328
510
|
uploadProgress.start(t('datasets.transfer.uploadTitle'), estimatedBytes);
|
|
511
|
+
await yieldToBrowser();
|
|
329
512
|
try {
|
|
330
513
|
await runDatasetImport({
|
|
331
514
|
projectId,
|
|
332
515
|
createBody,
|
|
333
|
-
batches:
|
|
516
|
+
batches: projectBufferedSampleBatches(samples, columns, IMPORT_BATCH_SIZE, controller.signal),
|
|
334
517
|
signal: controller.signal,
|
|
335
518
|
onCreated: (id) => {
|
|
336
519
|
importIdRef.current = id;
|
|
@@ -350,6 +533,7 @@ export function DatasetUploadPage({ projectId }) {
|
|
|
350
533
|
setIsImporting(false);
|
|
351
534
|
importAbortRef.current = null;
|
|
352
535
|
importIdRef.current = null;
|
|
536
|
+
abortOnLeaveRef.current = false;
|
|
353
537
|
}
|
|
354
538
|
};
|
|
355
539
|
const importDataset = async () => {
|
|
@@ -364,15 +548,25 @@ export function DatasetUploadPage({ projectId }) {
|
|
|
364
548
|
fileSizeBytes: selectedFile.size,
|
|
365
549
|
contentType: selectedFile.type || undefined,
|
|
366
550
|
};
|
|
367
|
-
|
|
551
|
+
const importPath = selectDatasetUploadImportPath({
|
|
552
|
+
file: selectedFile,
|
|
553
|
+
isLargeFile,
|
|
554
|
+
parsedSampleCount: parsedFile.samples.length,
|
|
555
|
+
rawImportCapabilities,
|
|
556
|
+
});
|
|
557
|
+
if (importPath === 'raw') {
|
|
558
|
+
await importRawDataset(fieldMappings, sourceFile, selectedFile);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (importPath === 'streaming') {
|
|
368
562
|
await importStreamingDataset(fieldMappings, sourceFile, selectedFile);
|
|
369
563
|
return;
|
|
370
564
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
await importBufferedDataset(fieldMappings, sourceFile, samples);
|
|
565
|
+
if (importPath === 'buffered') {
|
|
566
|
+
await importBufferedDataset(fieldMappings, sourceFile, parsedFile.samples, selectedColumns);
|
|
374
567
|
return;
|
|
375
568
|
}
|
|
569
|
+
const samples = projectSamplesToColumns(parsedFile.samples, selectedColumns);
|
|
376
570
|
const body = {
|
|
377
571
|
name: datasetName.trim(),
|
|
378
572
|
description: description.trim() || null,
|
|
@@ -380,7 +574,7 @@ export function DatasetUploadPage({ projectId }) {
|
|
|
380
574
|
fieldMappings,
|
|
381
575
|
samples,
|
|
382
576
|
};
|
|
383
|
-
const estimatedBytes =
|
|
577
|
+
const estimatedBytes = estimateUploadProgressBytes(sourceFile);
|
|
384
578
|
uploadProgress.start(t('datasets.transfer.uploadTitle'), estimatedBytes);
|
|
385
579
|
try {
|
|
386
580
|
await createDataset.mutateAsync({
|
|
@@ -394,7 +588,11 @@ export function DatasetUploadPage({ projectId }) {
|
|
|
394
588
|
uploadProgress.fail();
|
|
395
589
|
}
|
|
396
590
|
};
|
|
397
|
-
return (_jsxs(Main, { className: "gap-0 bg-muted/35 p-0", children: [_jsxs("div", { className: "mx-auto w-full max-w-[1440px] px-4 py-6 pb-36 sm:px-6 sm:pb-28 lg:px-8", "data-testid": "dataset-upload-page", children: [_jsxs("div", { className: "mb-1 font-mono text-[11.5px] text-muted-foreground", children: [_jsx(Link, { className: "hover:text-foreground", href: `/datasets`, children: t('datasets.title') }), _jsx("span", { className: "px-1.5", children: "/" }), _jsx("span", { className: "text-foreground", children: t('datasets.upload.title') })] }), _jsx("div", { className: "mb-5", children: _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-[26px] font-semibold", children: t('datasets.upload.title') }), _jsx("div", { className: "mt-1 text-[12.5px] text-muted-foreground", children: t('datasets.upload.subtitle') })] }) }), createDataset.isError && (_jsxs("div", { className: "mb-4 flex gap-2 rounded-md border border-destructive/35 bg-destructive/10 p-3 text-sm text-destructive", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-4 shrink-0" }), t('datasets.upload.importFailed')] })), isImporting && (_jsxs("div", { className: "mb-4 flex gap-2 rounded-md border border-[var(--status-pending-bd)] bg-[var(--status-pending-bg)] p-3 text-sm text-[var(--status-pending-fg)]", role: "alert", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-4 shrink-0" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium", children: t(
|
|
591
|
+
return (_jsxs(Main, { className: "gap-0 bg-muted/35 p-0", children: [_jsxs("div", { className: "mx-auto w-full max-w-[1440px] px-4 py-6 pb-36 sm:px-6 sm:pb-28 lg:px-8", "data-testid": "dataset-upload-page", children: [_jsxs("div", { className: "mb-1 font-mono text-[11.5px] text-muted-foreground", children: [_jsx(Link, { className: "hover:text-foreground", href: `/datasets`, children: t('datasets.title') }), _jsx("span", { className: "px-1.5", children: "/" }), _jsx("span", { className: "text-foreground", children: t('datasets.upload.title') })] }), _jsx("div", { className: "mb-5", children: _jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-[26px] font-semibold", children: t('datasets.upload.title') }), _jsx("div", { className: "mt-1 text-[12.5px] text-muted-foreground", children: t('datasets.upload.subtitle') })] }) }), createDataset.isError && (_jsxs("div", { className: "mb-4 flex gap-2 rounded-md border border-destructive/35 bg-destructive/10 p-3 text-sm text-destructive", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-4 shrink-0" }), t('datasets.upload.importFailed')] })), isImporting && (_jsxs("div", { className: "mb-4 flex gap-2 rounded-md border border-[var(--status-pending-bd)] bg-[var(--status-pending-bg)] p-3 text-sm text-[var(--status-pending-fg)]", role: "alert", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-4 shrink-0" }), _jsxs("div", { children: [_jsx("div", { className: "font-medium", children: t(serverImportContinues
|
|
592
|
+
? 'datasets.upload.backgroundImportNoticeTitle'
|
|
593
|
+
: 'datasets.upload.importingNoticeTitle') }), _jsx("div", { className: "mt-0.5 text-[12.5px]", children: t(serverImportContinues
|
|
594
|
+
? 'datasets.upload.backgroundImportNoticeBody'
|
|
595
|
+
: 'datasets.upload.importingNoticeBody') })] })] })), _jsx(DatasetTransferProgressPanel, { progress: uploadProgress.progress, className: "mb-4" }), _jsxs("div", { className: "grid gap-4 xl:grid-cols-[1.2fr_1fr]", children: [_jsx(Section, { number: 1, title: t('datasets.upload.file'), hint: t('datasets.upload.fileHint'), children: _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: cn('block rounded-lg border border-dashed border-[var(--status-running-bd)] bg-[var(--status-running-bg)]/45 p-4 transition-colors hover:bg-[var(--status-running-bg)]/65', isDragOver && 'border-primary bg-primary/10'), onDragEnter: (event) => {
|
|
398
596
|
event.preventDefault();
|
|
399
597
|
setIsDragOver(true);
|
|
400
598
|
}, onDragOver: (event) => event.preventDefault(), onDragLeave: () => setIsDragOver(false), onDrop: handleDrop, children: [_jsx("input", { id: fileInputId, type: "file", accept: FORMAT_CHIPS.join(','), className: "sr-only", onChange: updateFileInput }), _jsx("input", { id: folderInputId, type: "file", multiple: true, className: "sr-only", onChange: updateFileInput, ...directoryInputProps }), _jsxs("div", { className: "flex items-start gap-3", children: [_jsx("div", { className: "flex size-10 shrink-0 items-center justify-center rounded-md bg-[var(--status-running-bg)] text-[var(--status-running-fg)]", children: selectedFile ? _jsx(FileText, { className: "size-5" }) : _jsx(Upload, { className: "size-5" }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "text-[13.5px] font-semibold", children: selectedFile ? getUploadFilePath(selectedFile) : t('datasets.upload.chooseFile') }), selectedFile && (_jsxs("span", { className: "font-mono text-[11px] text-muted-foreground", children: [formatFileSize(selectedFile.size), " \u00B7 ", selectedFile.type || t('datasets.upload.unknownType')] })), parsedFile && (_jsxs("span", { className: "status-running ml-auto inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] font-medium", children: [_jsx("span", { className: "dot-running size-1.5 rounded-full" }), t('datasets.upload.parsed')] }))] }), _jsx("div", { className: "mt-0.5 font-mono text-[11.5px] text-muted-foreground", children: parsedFile
|
|
@@ -403,11 +601,9 @@ export function DatasetUploadPage({ projectId }) {
|
|
|
403
601
|
? t('datasets.upload.uploadReady')
|
|
404
602
|
: isDragOver
|
|
405
603
|
? t('datasets.upload.dropHere')
|
|
406
|
-
: t('datasets.upload.waitingForFile') }), _jsxs("div", { className: "flex items-center gap-2 text-[11.5px]", children: [_jsx("label", { className: "cursor-pointer text-muted-foreground hover:text-foreground", htmlFor: fileInputId, children: selectedFile ? t('datasets.action.replaceFile') : t('datasets.upload.browse') }), _jsx("span", { className: "text-muted-foreground", children: "\u00B7" }), _jsx("label", { className: "cursor-pointer text-muted-foreground hover:text-foreground", htmlFor: folderInputId, children: t('datasets.upload.browseFolder') })] })] })] })] })] }), parseError && (_jsxs("div", { className: "flex gap-2 rounded-md border border-destructive/35 bg-destructive/10 p-3 text-[12px] text-destructive", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-4 shrink-0" }), _jsx("div", { children: t(parseErrorKey) })] })), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "font-mono text-[11px] text-muted-foreground", children: t('datasets.upload.supportedFormats') }), FORMAT_CHIPS.map((format) => (_jsx("span", { className: "inline-flex rounded-[5px] border bg-muted px-2 py-0.5 font-mono text-[11px]", children: format }, format)))] })] }) }), _jsx(Section, { number: 2, title: t('datasets.upload.basicInfo'), hint: t('datasets.upload.basicInfoHint'), children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsxs("label", { className: "mb-1.5 block text-xs font-medium", children: [t('datasets.upload.name'), " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx("input", { className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring", value: datasetName, onChange: (event) => setDatasetName(event.target.value), placeholder: "risk-eval-v4" }), _jsx("div", { className: "mt-1 text-[11px] text-muted-foreground", children: t('datasets.upload.nameHelp') })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1.5 block text-xs font-medium", children: t('datasets.upload.description') }), _jsx("textarea", { className: "min-h-24 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring", value: description, onChange: (event) => setDescription(event.target.value), placeholder: t('datasets.upload.descriptionPlaceholder') })] })] }) }), _jsx(Section, { number: 3, title: t('datasets.upload.previewAndMapping'), hint: parsedFile
|
|
604
|
+
: t('datasets.upload.waitingForFile') }), _jsxs("div", { className: "flex items-center gap-2 text-[11.5px]", children: [_jsx("label", { className: "cursor-pointer text-muted-foreground hover:text-foreground", htmlFor: fileInputId, children: selectedFile ? t('datasets.action.replaceFile') : t('datasets.upload.browse') }), _jsx("span", { className: "text-muted-foreground", children: "\u00B7" }), _jsx("label", { className: "cursor-pointer text-muted-foreground hover:text-foreground", htmlFor: folderInputId, children: t('datasets.upload.browseFolder') })] })] })] })] })] }), parseError && (_jsxs("div", { className: "flex gap-2 rounded-md border border-destructive/35 bg-destructive/10 p-3 text-[12px] text-destructive", children: [_jsx(AlertTriangle, { className: "mt-0.5 size-4 shrink-0" }), _jsx("div", { children: t(parseErrorKey) })] })), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "font-mono text-[11px] text-muted-foreground", children: t('datasets.upload.supportedFormats') }), _jsx(UploadLimitInfoIcon, { rawMaxBytes: rawImportCapabilities?.maxBytes ?? DEFAULT_RAW_UPLOAD_MAX_BYTES }), FORMAT_CHIPS.map((format) => (_jsx("span", { className: "inline-flex rounded-[5px] border bg-muted px-2 py-0.5 font-mono text-[11px]", children: format }, format)))] }), _jsx(ImageSampleDownloads, {})] }) }), _jsx(Section, { number: 2, title: t('datasets.upload.basicInfo'), hint: t('datasets.upload.basicInfoHint'), children: _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { children: [_jsxs("label", { className: "mb-1.5 block text-xs font-medium", children: [t('datasets.upload.name'), " ", _jsx("span", { className: "text-destructive", children: "*" })] }), _jsx("input", { className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring", value: datasetName, onChange: (event) => setDatasetName(event.target.value), placeholder: "risk-eval-v4" }), _jsx("div", { className: "mt-1 text-[11px] text-muted-foreground", children: t('datasets.upload.nameHelp') })] }), _jsxs("div", { children: [_jsx("label", { className: "mb-1.5 block text-xs font-medium", children: t('datasets.upload.description') }), _jsx("textarea", { className: "min-h-24 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring", value: description, onChange: (event) => setDescription(event.target.value), placeholder: t('datasets.upload.descriptionPlaceholder') })] })] }) }), _jsx(Section, { number: 3, title: t('datasets.upload.previewAndMapping'), hint: parsedFile
|
|
407
605
|
? `${parsedFile.columns.length} ${t('datasets.detail.fields')} · ${sampleCountLabel}`
|
|
408
|
-
: t('datasets.upload.previewAndMappingHint'), className: "xl:col-span-2", children: !parsedFile ? (_jsx("div", { className: "rounded-md border border-dashed bg-muted/30 p-8 text-center text-sm text-muted-foreground", children: t('datasets.upload.noPreview') })) : (_jsxs("div", { className: "-m-4", children: [_jsxs("div", { className: "border-b", children: [_jsxs("div", { className: "flex flex-col gap-2 bg-muted/30 px-4 py-2.5 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs font-semibold", children: t('datasets.upload.samplePreview') }), _jsx("span", { className: "text-[11px] text-muted-foreground", children: t('datasets.upload.samplePreviewHint') })] }), _jsx("span", { className: "font-mono text-[11px] text-muted-foreground", children: t('datasets.upload.fieldRoleHint') })] }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[880px] text-sm", children: [_jsx("thead", { children: _jsx("tr", { className: "border-b bg-muted/60 text-left text-xs font-medium text-muted-foreground", children: parsedFile.columns.map((column) => (_jsx("th", { className: cn('px-3 py-3', !selectedFields[column] && 'opacity-45'), children: _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { children: column }), selectedFields[column] ? (_jsx(RoleArrowLabel, { role: fieldRoles[column] ?? 'metadata' })) : (_jsxs("span", { className: "font-mono text-[10px] font-normal text-muted-foreground", children: ['->', " ", t('datasets.upload.notImported')] }))] }) }, column))) }) }), _jsx("tbody", { children: previewRows.map((row, index) => (_jsx("tr", { className: "border-b last:border-b-0 hover:bg-muted/35", children: parsedFile.columns.map((column) => (_jsx("td", { className: "max-w-[280px] truncate px-3 py-3 font-mono text-[12px]", children: getDisplayValue(row[column]) }, column))) }, index))) })] }) }), _jsxs("div", { className: "flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7", "aria-label": t('common.previousPage'), disabled: true, children: _jsx(ChevronLeft, { className: "size-3.5" }) }), _jsxs("span", { className: "font-mono", children: ["1-", previewRows.length, ' ', isLargeFile
|
|
409
|
-
? `· ${t('datasets.upload.previewPrefixOnly')}`
|
|
410
|
-
: `/ ${parsedFile.samples.length}`] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7", "aria-label": t('common.nextPage'), disabled: true, children: _jsx(ChevronRight, { className: "size-3.5" }) })] }), _jsxs("span", { className: "font-mono text-[11.5px]", children: [sampleCountLabel, " \u00B7 ", selectedColumns.length, ' ', t('datasets.detail.fields'), ' ', selectedColumns.length > 0
|
|
606
|
+
: t('datasets.upload.previewAndMappingHint'), className: "xl:col-span-2", children: !parsedFile ? (_jsx("div", { className: "rounded-md border border-dashed bg-muted/30 p-8 text-center text-sm text-muted-foreground", children: t('datasets.upload.noPreview') })) : (_jsxs("div", { className: "-m-4", children: [_jsxs("div", { className: "border-b", children: [_jsxs("div", { className: "flex flex-col gap-2 bg-muted/30 px-4 py-2.5 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs font-semibold", children: t('datasets.upload.samplePreview') }), _jsx("span", { className: "text-[11px] text-muted-foreground", children: t('datasets.upload.samplePreviewHint') })] }), _jsx("span", { className: "font-mono text-[11px] text-muted-foreground", children: t('datasets.upload.fieldRoleHint') })] }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full min-w-[880px] text-sm", children: [_jsx("thead", { children: _jsx("tr", { className: "border-b bg-muted/60 text-left text-xs font-medium text-muted-foreground", children: parsedFile.columns.map((column) => (_jsx("th", { className: cn('px-3 py-3', !selectedFields[column] && 'opacity-45'), children: _jsxs("div", { className: "flex flex-col", children: [_jsx("span", { children: column }), selectedFields[column] ? (_jsx(RoleArrowLabel, { role: fieldRoles[column] ?? 'metadata' })) : (_jsxs("span", { className: "font-mono text-[10px] font-normal text-muted-foreground", children: ['->', " ", t('datasets.upload.notImported')] }))] }) }, column))) }) }), _jsx("tbody", { children: previewRows.map((row, index) => (_jsx("tr", { className: "border-b last:border-b-0 hover:bg-muted/35", children: parsedFile.columns.map((column) => (_jsx("td", { className: "max-w-[280px] truncate px-3 py-3 font-mono text-[12px]", children: getDisplayValue(row[column]) }, column))) }, index))) })] }) }), _jsxs("div", { className: "flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7", "aria-label": t('common.previousPage'), disabled: true, children: _jsx(ChevronLeft, { className: "size-3.5" }) }), _jsxs("span", { className: "font-mono", children: ["1-", previewRows.length, ' ', isLargeFile ? `· ${t('datasets.upload.previewPrefixOnly')}` : `/ ${parsedFile.samples.length}`] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-7", "aria-label": t('common.nextPage'), disabled: true, children: _jsx(ChevronRight, { className: "size-3.5" }) })] }), _jsxs("span", { className: "font-mono text-[11.5px]", children: [sampleCountLabel, " \u00B7 ", selectedColumns.length, " ", t('datasets.detail.fields'), ' ', selectedColumns.length > 0
|
|
411
607
|
? t('datasets.upload.readyToImport')
|
|
412
608
|
: t('datasets.upload.noSelectedFields')] })] })] }), _jsxs("div", { children: [_jsxs("div", { className: "flex flex-col gap-2 bg-muted/30 px-4 py-2.5 sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-xs font-semibold", children: t('datasets.upload.fieldMapping') }), _jsx("span", { className: "text-[11px] text-muted-foreground", children: t('datasets.upload.fieldMappingHint') })] }), _jsxs("span", { className: "font-mono text-[11px] text-muted-foreground", children: [t('datasets.upload.selectedFields'), ": ", selectedColumns.length, " / ", parsedFile.columns.length] }), _jsx("div", { className: "flex flex-wrap items-center gap-1.5", children: ROLE_OPTIONS.map((option) => (_jsx(RolePill, { role: option.role }, option.role))) })] }), _jsxs("div", { className: "grid grid-cols-[44px_96px_minmax(0,1fr)_minmax(0,1.2fr)_200px] border-t bg-muted/60 px-4 py-2.5 text-xs font-medium text-muted-foreground", children: [_jsx("div", { children: "#" }), _jsx("div", { children: t('datasets.upload.importField') }), _jsx("div", { children: t('datasets.upload.originalColumn') }), _jsx("div", { children: t('datasets.upload.firstRow') }), _jsx("div", { children: t('datasets.upload.role') })] }), parsedFile.columns.map((column, index) => (_jsxs("div", { className: cn('grid grid-cols-[44px_96px_minmax(0,1fr)_minmax(0,1.2fr)_200px] items-center border-t px-4 py-3 text-sm', !selectedFields[column] && 'bg-muted/25 text-muted-foreground'), children: [_jsx("span", { className: "flex size-6 items-center justify-center rounded bg-muted font-mono text-[11px] text-muted-foreground", children: index + 1 }), _jsxs("label", { className: "inline-flex items-center gap-2 text-xs font-medium", children: [_jsx("input", { type: "checkbox", checked: selectedFields[column] ?? false, onChange: (event) => setSelectedFields((current) => ({
|
|
413
609
|
...current,
|
|
@@ -415,7 +611,7 @@ export function DatasetUploadPage({ projectId }) {
|
|
|
415
611
|
})), className: "size-4 accent-primary", "aria-label": `${t('datasets.upload.importField')}: ${column}` }), selectedFields[column] ? t('datasets.upload.importField') : t('datasets.upload.notImported')] }), _jsx("div", { className: "min-w-0", children: _jsx("div", { className: "truncate font-mono text-[12.5px] font-semibold", children: column }) }), _jsx("div", { className: "truncate rounded-md bg-muted/45 px-2 py-1 font-mono text-[11.5px] text-muted-foreground", children: getDisplayValue(parsedFile.samples[0]?.[column]) }), _jsx("select", { value: fieldRoles[column] ?? 'metadata', onChange: (event) => setFieldRoles((current) => normalizeExpectedRoles({
|
|
416
612
|
...current,
|
|
417
613
|
[column]: event.target.value,
|
|
418
|
-
}, column)), disabled: !selectedFields[column], className: "h-8 rounded-md border bg-background px-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring", "aria-label": `${t('datasets.upload.role')}: ${column}`, children: ROLE_OPTIONS.map((option) => (_jsx("option", { value: option.role, children: t(option.labelKey) }, option.role))) })] }, column)))] })] })) })] })] }), _jsx("div", { className: "fixed bottom-0 left-0 right-0 z-20 border-t bg-background/95 px-4 py-3 shadow-lg backdrop-blur supports-[backdrop-filter]:bg-background/75 md:left-[var(--sidebar-width)]", children: _jsxs("div", { className: "mx-auto flex w-full max-w-[1440px] flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [_jsx("div", { className: "min-w-0 truncate font-mono text-[11.5px] text-muted-foreground", children: parsedFile ? (_jsxs("span", { children: [sampleCountLabel, " \u00B7 ", selectedColumns.length,
|
|
614
|
+
}, column)), disabled: !selectedFields[column], className: "h-8 rounded-md border bg-background px-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring", "aria-label": `${t('datasets.upload.role')}: ${column}`, children: ROLE_OPTIONS.map((option) => (_jsx("option", { value: option.role, children: t(option.labelKey) }, option.role))) })] }, column)))] })] })) })] })] }), _jsx("div", { className: "fixed bottom-0 left-0 right-0 z-20 border-t bg-background/95 px-4 py-3 shadow-lg backdrop-blur supports-[backdrop-filter]:bg-background/75 md:left-[var(--sidebar-width)]", children: _jsxs("div", { className: "mx-auto flex w-full max-w-[1440px] flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", children: [_jsx("div", { className: "min-w-0 truncate font-mono text-[11.5px] text-muted-foreground", children: parsedFile ? (_jsxs("span", { children: [sampleCountLabel, " \u00B7 ", selectedColumns.length, " ", t('datasets.detail.fields'), " \u00B7", ' ', selectedColumns.length > 0
|
|
419
615
|
? t('datasets.upload.readyToImport')
|
|
420
616
|
: t('datasets.upload.noSelectedFields')] })) : (_jsx("span", { children: t('datasets.upload.waitingForFile') })) }), _jsxs("div", { className: "flex w-full flex-col-reverse gap-2 sm:w-auto sm:flex-row sm:items-center", children: [_jsx(Button, { asChild: true, variant: "outline", size: "sm", className: "h-9 w-full sm:w-auto", children: _jsx(Link, { href: `/datasets`, children: t('common.cancel') }) }), _jsxs(Button, { type: "button", size: "sm", className: "h-9 w-full sm:w-auto", disabled: !canImport, "aria-busy": isSubmitting, onClick: () => void importDataset(), children: [isSubmitting ? _jsx(Loader2, { className: "size-4 animate-spin" }) : _jsx(Check, { className: "size-4" }), importButtonLabel] })] })] }) }), _jsx(Dialog, { open: leaveDialogOpen, onOpenChange: (open) => {
|
|
421
617
|
if (!open)
|