apolinario commited on
Commit
51089d0
·
1 Parent(s): cdc26bb

add dataset UI (hopefully)

Browse files
ui/src/app/jobs/new/SimplifiedJob.tsx CHANGED
@@ -13,6 +13,7 @@ import { Button } from '@headlessui/react';
13
  import { ChevronDown, ChevronUp, Upload } from 'lucide-react';
14
  import { apiClient } from '@/utils/api';
15
  import { addUserDataset, updateUserDatasetPath } from '@/utils/storage/datasetStorage';
 
16
 
17
  type DatasetMode = 'upload' | 'existing';
18
 
@@ -152,6 +153,8 @@ export default function SimplifiedJob({
152
  const [controlUploading, setControlUploading] = useState(false);
153
  const [controlUploadInfo, setControlUploadInfo] = useState<string | null>(null);
154
  const [controlUploadError, setControlUploadError] = useState<string | null>(null);
 
 
155
 
156
  const modelArch = useMemo(() => {
157
  return modelArchs.find(arch => arch.name === process.model.arch);
@@ -359,6 +362,7 @@ export default function SimplifiedJob({
359
  setTrainDatasetNameTouched(true);
360
  setTrainUploadInfo(`Uploaded ${files.length} file${files.length > 1 ? 's' : ''} to ${datasetPath}`);
361
  setTrainUploadError(null);
 
362
  } else {
363
  setJobConfig(datasetPath, 'config.process[0].datasets[0].control_path');
364
  updateUserDatasetPath(resolvedName, datasetPath);
@@ -370,6 +374,7 @@ export default function SimplifiedJob({
370
  setControlDatasetNameTouched(true);
371
  setControlUploadInfo(`Uploaded ${files.length} file${files.length > 1 ? 's' : ''} to ${datasetPath}`);
372
  setControlUploadError(null);
 
373
  }
374
  },
375
  [
@@ -377,10 +382,12 @@ export default function SimplifiedJob({
377
  controlDatasetPath,
378
  controlDatasetResolvedName,
379
  ensureDataset,
 
380
  setJobConfig,
381
  trainDatasetName,
382
  trainDatasetPath,
383
  trainDatasetResolvedName,
 
384
  ],
385
  );
386
 
@@ -486,6 +493,34 @@ export default function SimplifiedJob({
486
  return datasetOptions;
487
  }, [datasetOptions]);
488
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  return (
490
  <form onSubmit={handleSubmit} className="space-y-8">
491
  <Card title="Training Setup">
@@ -667,6 +702,9 @@ export default function SimplifiedJob({
667
  setJobConfig(value, 'config.process[0].datasets[0].folder_path');
668
  setTrainModeTouched(true);
669
  setTrainDatasetPath(value);
 
 
 
670
  }}
671
  options={existingOptions}
672
  />
@@ -678,6 +716,10 @@ export default function SimplifiedJob({
678
  setJobConfig(value, 'config.process[0].datasets[0].folder_path');
679
  setTrainModeTouched(true);
680
  setTrainDatasetPath(value);
 
 
 
 
681
  }}
682
  placeholder="/path/to/your/dataset"
683
  required
@@ -685,6 +727,15 @@ export default function SimplifiedJob({
685
  </div>
686
  )}
687
 
 
 
 
 
 
 
 
 
 
688
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
689
  <NumberInput
690
  label="Dataset Weight"
@@ -754,6 +805,9 @@ export default function SimplifiedJob({
754
  setJobConfig(value, 'config.process[0].datasets[0].control_path');
755
  setControlModeTouched(true);
756
  setControlDatasetPath(value);
 
 
 
757
  }}
758
  options={existingOptions}
759
  />
@@ -766,11 +820,25 @@ export default function SimplifiedJob({
766
  setJobConfig(normalized, 'config.process[0].datasets[0].control_path');
767
  setControlModeTouched(true);
768
  setControlDatasetPath(normalized || null);
 
 
 
 
769
  }}
770
  placeholder="/path/to/control/images"
771
  />
772
  </div>
773
  )}
 
 
 
 
 
 
 
 
 
 
774
  </div>
775
  )}
776
  </Card>
 
13
  import { ChevronDown, ChevronUp, Upload } from 'lucide-react';
14
  import { apiClient } from '@/utils/api';
15
  import { addUserDataset, updateUserDatasetPath } from '@/utils/storage/datasetStorage';
16
+ import DatasetPreviewGrid from '@/components/DatasetPreviewGrid';
17
 
18
  type DatasetMode = 'upload' | 'existing';
19
 
 
153
  const [controlUploading, setControlUploading] = useState(false);
154
  const [controlUploadInfo, setControlUploadInfo] = useState<string | null>(null);
155
  const [controlUploadError, setControlUploadError] = useState<string | null>(null);
156
+ const [trainPreviewRefresh, setTrainPreviewRefresh] = useState(0);
157
+ const [controlPreviewRefresh, setControlPreviewRefresh] = useState(0);
158
 
159
  const modelArch = useMemo(() => {
160
  return modelArchs.find(arch => arch.name === process.model.arch);
 
362
  setTrainDatasetNameTouched(true);
363
  setTrainUploadInfo(`Uploaded ${files.length} file${files.length > 1 ? 's' : ''} to ${datasetPath}`);
364
  setTrainUploadError(null);
365
+ setTrainPreviewRefresh(prev => prev + 1);
366
  } else {
367
  setJobConfig(datasetPath, 'config.process[0].datasets[0].control_path');
368
  updateUserDatasetPath(resolvedName, datasetPath);
 
374
  setControlDatasetNameTouched(true);
375
  setControlUploadInfo(`Uploaded ${files.length} file${files.length > 1 ? 's' : ''} to ${datasetPath}`);
376
  setControlUploadError(null);
377
+ setControlPreviewRefresh(prev => prev + 1);
378
  }
379
  },
380
  [
 
382
  controlDatasetPath,
383
  controlDatasetResolvedName,
384
  ensureDataset,
385
+ setControlPreviewRefresh,
386
  setJobConfig,
387
  trainDatasetName,
388
  trainDatasetPath,
389
  trainDatasetResolvedName,
390
+ setTrainPreviewRefresh,
391
  ],
392
  );
393
 
 
493
  return datasetOptions;
494
  }, [datasetOptions]);
495
 
496
+ useEffect(() => {
497
+ if (trainDatasetResolvedName) {
498
+ return;
499
+ }
500
+ const currentFolder = dataset.folder_path;
501
+ if (!currentFolder) {
502
+ return;
503
+ }
504
+ const match = existingOptions.find(option => option.value === currentFolder);
505
+ if (match) {
506
+ setTrainDatasetResolvedName(match.label);
507
+ }
508
+ }, [existingOptions, dataset.folder_path, trainDatasetResolvedName]);
509
+
510
+ useEffect(() => {
511
+ if (controlDatasetResolvedName) {
512
+ return;
513
+ }
514
+ const controlPathValue = typeof dataset.control_path === 'string' ? dataset.control_path : null;
515
+ if (!controlPathValue) {
516
+ return;
517
+ }
518
+ const match = existingOptions.find(option => option.value === controlPathValue);
519
+ if (match) {
520
+ setControlDatasetResolvedName(match.label);
521
+ }
522
+ }, [existingOptions, dataset.control_path, controlDatasetResolvedName]);
523
+
524
  return (
525
  <form onSubmit={handleSubmit} className="space-y-8">
526
  <Card title="Training Setup">
 
702
  setJobConfig(value, 'config.process[0].datasets[0].folder_path');
703
  setTrainModeTouched(true);
704
  setTrainDatasetPath(value);
705
+ const selected = existingOptions.find(option => option.value === value);
706
+ setTrainDatasetResolvedName(selected?.label || null);
707
+ setTrainPreviewRefresh(prev => prev + 1);
708
  }}
709
  options={existingOptions}
710
  />
 
716
  setJobConfig(value, 'config.process[0].datasets[0].folder_path');
717
  setTrainModeTouched(true);
718
  setTrainDatasetPath(value);
719
+ const trimmed = value?.trim();
720
+ const matched = existingOptions.find(option => option.value === trimmed);
721
+ setTrainDatasetResolvedName(matched?.label || null);
722
+ setTrainPreviewRefresh(prev => prev + 1);
723
  }}
724
  placeholder="/path/to/your/dataset"
725
  required
 
727
  </div>
728
  )}
729
 
730
+ <div className="mt-6">
731
+ <h3 className="text-sm text-gray-200 font-semibold mb-3">Dataset Preview</h3>
732
+ <DatasetPreviewGrid
733
+ datasetName={trainDatasetResolvedName}
734
+ refreshKey={trainPreviewRefresh}
735
+ compact
736
+ />
737
+ </div>
738
+
739
  <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
740
  <NumberInput
741
  label="Dataset Weight"
 
805
  setJobConfig(value, 'config.process[0].datasets[0].control_path');
806
  setControlModeTouched(true);
807
  setControlDatasetPath(value);
808
+ const selected = existingOptions.find(option => option.value === value);
809
+ setControlDatasetResolvedName(selected?.label || null);
810
+ setControlPreviewRefresh(prev => prev + 1);
811
  }}
812
  options={existingOptions}
813
  />
 
820
  setJobConfig(normalized, 'config.process[0].datasets[0].control_path');
821
  setControlModeTouched(true);
822
  setControlDatasetPath(normalized || null);
823
+ const trimmed = value?.trim();
824
+ const matched = existingOptions.find(option => option.value === trimmed);
825
+ setControlDatasetResolvedName(matched?.label || null);
826
+ setControlPreviewRefresh(prev => prev + 1);
827
  }}
828
  placeholder="/path/to/control/images"
829
  />
830
  </div>
831
  )}
832
+
833
+ <div className="mt-6">
834
+ <h3 className="text-sm text-gray-200 font-semibold mb-3">Control Dataset Preview</h3>
835
+ <DatasetPreviewGrid
836
+ datasetName={controlDatasetResolvedName}
837
+ refreshKey={controlPreviewRefresh}
838
+ compact
839
+ emptyMessage="No control media uploaded yet. Add files to preview them here."
840
+ />
841
+ </div>
842
  </div>
843
  )}
844
  </Card>
ui/src/components/DatasetPreviewGrid.tsx ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { LuImageOff, LuLoader, LuBan } from 'react-icons/lu';
3
+ import DatasetImageCard from './DatasetImageCard';
4
+ import classNames from 'classnames';
5
+ import { apiClient } from '@/utils/api';
6
+
7
+ type LoadState = 'idle' | 'loading' | 'success' | 'error' | 'empty';
8
+
9
+ interface DatasetPreviewGridProps {
10
+ datasetName?: string | null;
11
+ refreshKey?: number;
12
+ className?: string;
13
+ emptyMessage?: string;
14
+ compact?: boolean;
15
+ }
16
+
17
+ export default function DatasetPreviewGrid({
18
+ datasetName,
19
+ refreshKey = 0,
20
+ className = '',
21
+ emptyMessage = 'No files uploaded yet. Add images or videos to see them here.',
22
+ compact = false,
23
+ }: DatasetPreviewGridProps) {
24
+ const [status, setStatus] = useState<LoadState>('idle');
25
+ const [items, setItems] = useState<{ img_path: string }[]>([]);
26
+
27
+ const fetchMedia = useCallback(async () => {
28
+ const trimmedName = datasetName?.trim();
29
+ if (!trimmedName) {
30
+ setItems([]);
31
+ setStatus('idle');
32
+ return;
33
+ }
34
+
35
+ setStatus('loading');
36
+ try {
37
+ const response = await apiClient
38
+ .post('/api/datasets/listImages', { datasetName: trimmedName })
39
+ .then(res => res.data);
40
+
41
+ const sorted = Array.isArray(response?.images)
42
+ ? [...response.images].sort((a, b) => a.img_path.localeCompare(b.img_path))
43
+ : [];
44
+
45
+ setItems(sorted);
46
+ setStatus(sorted.length === 0 ? 'empty' : 'success');
47
+ } catch (error) {
48
+ console.error('Failed to fetch dataset preview:', error);
49
+ setItems([]);
50
+ setStatus('error');
51
+ }
52
+ }, [datasetName]);
53
+
54
+ useEffect(() => {
55
+ fetchMedia();
56
+ }, [fetchMedia, refreshKey]);
57
+
58
+ const content = useMemo(() => {
59
+ switch (status) {
60
+ case 'idle':
61
+ return (
62
+ <div className="text-sm text-gray-500 bg-gray-900/60 border border-gray-800 rounded-md px-4 py-6 text-center">
63
+ Select or upload a dataset to preview its contents.
64
+ </div>
65
+ );
66
+ case 'loading':
67
+ return (
68
+ <div className="flex flex-col items-center justify-center gap-3 py-8 text-sm text-gray-300">
69
+ <LuLoader className="animate-spin w-6 h-6" />
70
+ <span>Loading dataset contents…</span>
71
+ </div>
72
+ );
73
+ case 'error':
74
+ return (
75
+ <div className="flex flex-col items-center justify-center gap-3 py-8 text-sm text-red-300 bg-red-950/30 border border-red-800 rounded-md">
76
+ <LuBan className="w-6 h-6" />
77
+ <span>Unable to load dataset media. Verify the dataset exists and try again.</span>
78
+ </div>
79
+ );
80
+ case 'empty':
81
+ return (
82
+ <div className="flex flex-col items-center justify-center gap-3 py-8 text-sm text-gray-400 bg-gray-900/60 border border-dashed border-gray-700 rounded-md">
83
+ <LuImageOff className="w-6 h-6" />
84
+ <span>{emptyMessage}</span>
85
+ </div>
86
+ );
87
+ case 'success':
88
+ return (
89
+ <div
90
+ className={classNames(
91
+ 'grid gap-4',
92
+ compact
93
+ ? 'grid-cols-1 sm:grid-cols-2'
94
+ : 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
95
+ )}
96
+ >
97
+ {items.map(item => (
98
+ <DatasetImageCard
99
+ key={item.img_path}
100
+ imageUrl={item.img_path}
101
+ alt="Dataset media"
102
+ onDelete={fetchMedia}
103
+ />
104
+ ))}
105
+ </div>
106
+ );
107
+ default:
108
+ return null;
109
+ }
110
+ }, [status, items, emptyMessage, compact, fetchMedia]);
111
+
112
+ return <div className={classNames('space-y-4', className)}>{content}</div>;
113
+ }