@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.
@@ -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, isJsonlFile, parseDatasetFile, parseJsonlPrefix, projectSamplesToColumns, selectDatasetFile, streamJsonlBatches, } from './dataset-upload-parser';
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
- return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
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 === 'large_requires_jsonl')
100
- return 'datasets.upload.largeRequiresJsonl';
131
+ if (parseError === 'large_requires_streaming_format')
132
+ return 'datasets.upload.largeRequiresStreamingFormat';
101
133
  return 'datasets.upload.parseFailed';
102
134
  }
103
- function estimatePayloadBytes(body) {
104
- return new TextEncoder().encode(JSON.stringify(body)).length;
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
- // and on import they stream off disk through the dataset-import session.
108
- const SYNC_MAX_FILE_BYTES = 10 * 1024 * 1024;
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
- async function* chunkSamples(samples, size) {
122
- for (let index = 0; index < samples.length; index += size) {
123
- yield samples.slice(index, index + size);
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
- // Streams a large JSONL file off disk, projecting each batch to the selected columns before upload.
127
- async function* projectedJsonlBatches(file, columns, size, onBytes, signal) {
128
- for await (const batch of streamJsonlBatches(file, size, onBytes, signal)) {
129
- yield projectSamplesToColumns(batch, columns);
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
- // Leaving the page mid-import aborts the session so the server clears its staging rows (中断即删干净).
154
- useEffect(() => () => importAbortRef.current?.abort(), []);
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
- importAbortRef.current?.abort();
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
- if (large && !isJsonlFile(file)) {
252
- // Streaming import currently supports JSONL only; large non-JSONL files are not parsed whole on drop.
253
- throw new Error('large_requires_jsonl');
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
- // Large files: read only a head prefix for preview/mapping, never the whole file.
256
- const parsed = large ? await parseJsonlPrefix(file) : await parseDatasetFile(file);
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: 'jsonl',
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: projectedJsonlBatches(file, selectedColumns, IMPORT_BATCH_SIZE, (readBytes) => uploadProgress.update({ loadedBytes: readBytes, totalBytes }), controller.signal),
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 = new TextEncoder().encode(JSON.stringify(samples)).length;
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: chunkSamples(samples, IMPORT_BATCH_SIZE),
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
- if (isLargeFile) {
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
- const samples = projectSamplesToColumns(parsedFile.samples, selectedColumns);
372
- if (samples.length > SYNC_MAX_SAMPLES) {
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 = estimatePayloadBytes(body);
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('datasets.upload.importingNoticeTitle') }), _jsx("div", { className: "mt-0.5 text-[12.5px]", children: t('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) => {
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, ' ', t('datasets.detail.fields'), " \u00B7", ' ', selectedColumns.length > 0
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)