Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
24372b5
Add toggles to the UI to add flipped versions of the datasets, X, Y o…
jaretburkett Aug 24, 2025
f48d21c
Upgrade a LoRA rank if the new one is larger so users can increase th…
jaretburkett Aug 24, 2025
ea01a1c
Fixed a bug where samples would fail if merging in lora on sampling f…
jaretburkett Aug 25, 2025
119653c
Force width, height, and num frames to always be the proper sizes for…
jaretburkett Aug 25, 2025
37eda7b
Add a tab to the UI to show the config file for the job. Read only.
jaretburkett Aug 25, 2025
d0338b8
Allow dropping images directly into dataset folder without having to …
jaretburkett Aug 25, 2025
5ad190b
Improve UI for sample images when there are no samples
jaretburkett Aug 27, 2025
fd13bd7
Add a Download button on samples to download all the samples as a zip…
jaretburkett Aug 27, 2025
1f541bc
Changes to handle a different DFE arch
jaretburkett Aug 27, 2025
fc5b416
Switch order to save first, then sample.
jaretburkett Aug 27, 2025
9ef425a
Fixed issue with training qwen with cached text embeds with a batch s…
jaretburkett Aug 28, 2025
e334941
Updated runpod docs
jaretburkett Aug 28, 2025
056711d
Fix issue with wan22 14b that woudl load both transformers temporaril…
jaretburkett Aug 28, 2025
6fc9ec1
Added example config for training wan22 14b 24GB on images
jaretburkett Aug 28, 2025
193c1b2
Add a watcher to constantly check for stop signal from the UI. This w…
jaretburkett Aug 31, 2025
0f2239c
Add force sample toggle to the ui
jaretburkett Aug 31, 2025
7040d8d
Preperation for audio
jaretburkett Sep 2, 2025
85dcae6
Set full size control images to default true
jaretburkett Sep 2, 2025
f699f4b
Add ability to set transparent color for control images
jaretburkett Sep 2, 2025
6450467
Comment out fast stop watcher. Could potentiallty be causing some wei…
jaretburkett Sep 4, 2025
af6fdaa
Add ability to train a full rank LoRA. (experimental)
jaretburkett Sep 9, 2025
b95c17d
Add initial support for chroma radiance
jaretburkett Sep 10, 2025
3666b11
DEF for fake vae and adjust scaling
jaretburkett Sep 13, 2025
218f673
Added support for new concept slider training script to CLI and UI
jaretburkett Sep 16, 2025
24a576a
Regularize the slider targets.
jaretburkett Sep 17, 2025
2120dc5
Upgrade job to new ui trainer to fix issue with slider config showing…
jaretburkett Sep 17, 2025
e4ae97e
add dataset-level distillation-style regularization
squewel Sep 17, 2025
e27e229
add prior_reg flag to FileItemDTO
squewel Sep 17, 2025
3cdf50c
Merge pull request #426 from squewel/prior_reg
jaretburkett Sep 18, 2025
390e21b
Integrate dataset level trigger words and allow them to be cached. De…
jaretburkett Sep 18, 2025
20dfe1b
Small double tap of detach on qwen just for good measure
jaretburkett Sep 18, 2025
28728a1
Added experimental dfe 5
jaretburkett Sep 21, 2025
f744751
Add stepped loss type
jaretburkett Sep 22, 2025
454be09
Initial support for qwen image edit plus
jaretburkett Sep 24, 2025
1069dee
Added ui sopport for multi control samples and datasets. Added qwen i…
jaretburkett Sep 25, 2025
0eaa3d2
Merge pull request #434 from ostris/qwen_image_edit_plus
jaretburkett Sep 25, 2025
e04f55c
Fixed scaling issue with control images
jaretburkett Sep 26, 2025
be99063
Remove dropout from cached text embeddings even if used specifies it …
jaretburkett Sep 26, 2025
6da4172
Add extra detachments just to be sure on qiep
jaretburkett Sep 27, 2025
3b1f7b0
Allow user to set the attention backend. Add method to recomver from …
jaretburkett Sep 27, 2025
98d35f3
Add hidream ARA
jaretburkett Sep 27, 2025
f0646a0
Reworked ui sample image modal to show more information and function …
jaretburkett Sep 27, 2025
4e207d9
Add seed to the sample image modal
jaretburkett Sep 28, 2025
c20240b
Add advanced menu on job to allow user to do things like make a job a…
jaretburkett Sep 28, 2025
c233a80
Reqorked visibility toggle on samples, should help when dealing with …
jaretburkett Sep 28, 2025
ebadb32
On samples page, auto scroll to bottom on load. Added a floating butt…
jaretburkett Sep 29, 2025
2e9de5e
Add ability to delete samples from the ui
jaretburkett Sep 29, 2025
6a77b0c
Merge branch 'main' into gs/merge-main
gschoeni Sep 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Allow dropping images directly into dataset folder without having to …
…open the add images modal. Improve ui flow of dataset messaging.
  • Loading branch information
jaretburkett committed Aug 25, 2025
commit d0338b8b0b6eee63a6491c2551a809641126e5aa
64 changes: 59 additions & 5 deletions ui/src/app/datasets/[datasetName]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
'use client';

import { useEffect, useState, use } from 'react';
import { useEffect, useState, use, useMemo } from 'react';
import { LuImageOff, LuLoader, LuBan } from 'react-icons/lu';
import { FaChevronLeft } from 'react-icons/fa';
import DatasetImageCard from '@/components/DatasetImageCard';
import { Button } from '@headlessui/react';
import AddImagesModal, { openImagesModal } from '@/components/AddImagesModal';
import { TopBar, MainContent } from '@/components/layout';
import { apiClient } from '@/utils/api';
import FullscreenDropOverlay from '@/components/FullscreenDropOverlay';

export default function DatasetPage({ params }: { params: { datasetName: string } }) {
const [imgList, setImgList] = useState<{ img_path: string }[]>([]);
Expand Down Expand Up @@ -38,6 +40,56 @@ export default function DatasetPage({ params }: { params: { datasetName: string
}
}, [datasetName]);

const PageInfoContent = useMemo(() => {
let icon = null;
let text = '';
let subtitle = '';
let showIt = false;
let bgColor = '';
let textColor = '';
let iconColor = '';

if (status == 'loading') {
icon = <LuLoader className="animate-spin w-8 h-8" />;
text = 'Loading Images';
subtitle = 'Please wait while we fetch your dataset images...';
showIt = true;
bgColor = 'bg-gray-50 dark:bg-gray-800/50';
textColor = 'text-gray-900 dark:text-gray-100';
iconColor = 'text-gray-500 dark:text-gray-400';
}
if (status == 'error') {
icon = <LuBan className="w-8 h-8" />;
text = 'Error Loading Images';
subtitle = 'There was a problem fetching the images. Please try refreshing the page.';
showIt = true;
bgColor = 'bg-red-50 dark:bg-red-950/20';
textColor = 'text-red-900 dark:text-red-100';
iconColor = 'text-red-600 dark:text-red-400';
}
if (status == 'success' && imgList.length === 0) {
icon = <LuImageOff className="w-8 h-8" />;
text = 'No Images Found';
subtitle = 'This dataset is empty. Click "Add Images" to get started.';
showIt = true;
bgColor = 'bg-gray-50 dark:bg-gray-800/50';
textColor = 'text-gray-900 dark:text-gray-100';
iconColor = 'text-gray-500 dark:text-gray-400';
}

if (!showIt) return null;

return (
<div
className={`mt-10 flex flex-col items-center justify-center py-16 px-8 rounded-xl border-2 border-gray-700 border-dashed ${bgColor} ${textColor} mx-auto max-w-md text-center`}
>
<div className={`${iconColor} mb-4`}>{icon}</div>
<h3 className="text-lg font-semibold mb-2">{text}</h3>
<p className="text-sm opacity-75 leading-relaxed">{subtitle}</p>
</div>
);
}, [status, imgList.length]);

return (
<>
{/* Fixed top bar */}
Expand All @@ -61,11 +113,9 @@ export default function DatasetPage({ params }: { params: { datasetName: string
</div>
</TopBar>
<MainContent>
{status === 'loading' && <p>Loading...</p>}
{status === 'error' && <p>Error fetching images</p>}
{status === 'success' && (
{PageInfoContent}
{status === 'success' && imgList.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{imgList.length === 0 && <p>No images found</p>}
{imgList.map(img => (
<DatasetImageCard
key={img.img_path}
Expand All @@ -78,6 +128,10 @@ export default function DatasetPage({ params }: { params: { datasetName: string
)}
</MainContent>
<AddImagesModal />
<FullscreenDropOverlay
datasetName={datasetName}
onComplete={() => refreshImageList(datasetName)}
/>
</>
);
}
182 changes: 182 additions & 0 deletions ui/src/components/FullscreenDropOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
'use client';

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { FaUpload } from 'react-icons/fa';
import { apiClient } from '@/utils/api';

type AcceptMap = {
[mime: string]: string[];
};

interface FullscreenDropOverlayProps {
datasetName: string; // where to upload
onComplete?: () => void; // called after successful upload
accept?: AcceptMap; // optional override
multiple?: boolean; // default true
}

export default function FullscreenDropOverlay({
datasetName,
onComplete,
accept,
multiple = true,
}: FullscreenDropOverlayProps) {
const [visible, setVisible] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const dragDepthRef = useRef(0); // drag-enter/leave tracking

// Only show the overlay for real file drags (not text, images from page, etc)
const isFileDrag = (e: DragEvent) => {
const types = e?.dataTransfer?.types;
return !!types && Array.from(types).includes('Files');
};

// Window-level drag listeners to toggle visibility
useEffect(() => {
const onDragEnter = (e: DragEvent) => {
if (!isFileDrag(e)) return;
dragDepthRef.current += 1;
setVisible(true);
e.preventDefault();
};
const onDragOver = (e: DragEvent) => {
if (!isFileDrag(e)) return;
// Must preventDefault to allow dropping in the browser
e.preventDefault();
if (!visible) setVisible(true);
};
const onDragLeave = (e: DragEvent) => {
if (!isFileDrag(e)) return;
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
if (dragDepthRef.current === 0 && !isUploading) {
setVisible(false);
}
};
const onDrop = (e: DragEvent) => {
if (!isFileDrag(e)) return;
// Prevent browser from opening the file
e.preventDefault();
dragDepthRef.current = 0;
// We do NOT hide here; the dropzone onDrop will handle workflow visibility.
};

window.addEventListener('dragenter', onDragEnter);
window.addEventListener('dragover', onDragOver);
window.addEventListener('dragleave', onDragLeave);
window.addEventListener('drop', onDrop);

return () => {
window.removeEventListener('dragenter', onDragEnter);
window.removeEventListener('dragover', onDragOver);
window.removeEventListener('dragleave', onDragLeave);
window.removeEventListener('drop', onDrop);
};
}, [visible, isUploading]);

const onDrop = useCallback(
async (acceptedFiles: File[]) => {
if (acceptedFiles.length === 0) {
// no accepted files; hide overlay cleanly
setVisible(false);
return;
}

setIsUploading(true);
setUploadProgress(0);

const formData = new FormData();
acceptedFiles.forEach(file => formData.append('files', file));
formData.append('datasetName', datasetName || '');

try {
await apiClient.post(`/api/datasets/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: pe => {
const percent = Math.round(((pe.loaded || 0) * 100) / (pe.total || pe.loaded || 1));
setUploadProgress(percent);
},
timeout: 0,
});
onComplete?.();
} catch (err) {
console.error('Upload failed:', err);
} finally {
setIsUploading(false);
setUploadProgress(0);
setVisible(false);
}
},
[datasetName, onComplete],
);

const dropAccept = useMemo<AcceptMap>(
() =>
accept || {
'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'],
'video/*': ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.m4v', '.flv'],
'text/*': ['.txt'],
},
[accept],
);

const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: dropAccept,
multiple,
noClick: true,
noKeyboard: true,
// Prevent "folder opens" by browser if someone drags outside the overlay mid-drop:
preventDropOnDocument: true,
});

return (
<div
// When hidden: opacity-0 + pointer-events-none so the page is fully interactive
// When visible or uploading: fade in and capture the drop
className={`fixed inset-0 z-[9999] transition-opacity duration-200 ${
visible || isUploading ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'
}`}
aria-hidden={!visible && !isUploading}
{...getRootProps()}
>
{/* Fullscreen capture layer */}
<input {...getInputProps()} />

{/* Backdrop: keep it subtle so context remains visible */}
<div className={`absolute inset-0 ${isUploading ? 'bg-gray-900/70' : 'bg-gray-900/40'}`} />

{/* Center drop target UI */}
<div className="absolute inset-0 flex items-center justify-center p-6">
<div
className={`w-full max-w-2xl rounded-2xl border-2 border-dashed px-8 py-10 text-center shadow-2xl backdrop-blur-sm
${isDragActive ? 'border-blue-400 bg-white/10' : 'border-white/30 bg-white/5'}`}
>
<div className="flex flex-col items-center gap-4">
<FaUpload className="size-10 opacity-80" />
{!isUploading ? (
<>
<p className="text-lg font-semibold">Drop files to upload</p>
<p className="text-sm opacity-80">
Destination:&nbsp;<span className="font-mono">{datasetName || 'unknown'}</span>
</p>
<p className="text-xs opacity-70 mt-1">Images, videos, or .txt supported</p>
</>
) : (
<>
<p className="text-lg font-semibold">Uploading… {uploadProgress}%</p>
<div className="w-full h-2.5 bg-white/20 rounded-full overflow-hidden">
<div
className="h-2.5 bg-blue-500 rounded-full transition-[width] duration-150 ease-linear"
style={{ width: `${uploadProgress}%` }}
/>
</div>
</>
)}
</div>
</div>
</div>
</div>
);
}