Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
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 |
+
}
|