diff --git a/ui/src/app/datasets/[datasetName]/page.tsx b/ui/src/app/datasets/[datasetName]/page.tsx
index d3bf68fc..dcba4736 100644
--- a/ui/src/app/datasets/[datasetName]/page.tsx
+++ b/ui/src/app/datasets/[datasetName]/page.tsx
@@ -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 }[]>([]);
@@ -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 = ;
+ 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 = ;
+ 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 = ;
+ 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 (
+
+
{icon}
+
{text}
+
{subtitle}
+
+ );
+ }, [status, imgList.length]);
+
return (
<>
{/* Fixed top bar */}
@@ -61,11 +113,9 @@ export default function DatasetPage({ params }: { params: { datasetName: string
- {status === 'loading' && Loading...
}
- {status === 'error' && Error fetching images
}
- {status === 'success' && (
+ {PageInfoContent}
+ {status === 'success' && imgList.length > 0 && (
- {imgList.length === 0 &&
No images found
}
{imgList.map(img => (
+ refreshImageList(datasetName)}
+ />
>
);
}
diff --git a/ui/src/components/FullscreenDropOverlay.tsx b/ui/src/components/FullscreenDropOverlay.tsx
new file mode 100644
index 00000000..425b7aef
--- /dev/null
+++ b/ui/src/components/FullscreenDropOverlay.tsx
@@ -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(
+ () =>
+ 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 (
+
+ {/* Fullscreen capture layer */}
+
+
+ {/* Backdrop: keep it subtle so context remains visible */}
+
+
+ {/* Center drop target UI */}
+
+
+
+
+ {!isUploading ? (
+ <>
+
Drop files to upload
+
+ Destination: {datasetName || 'unknown'}
+
+
Images, videos, or .txt supported
+ >
+ ) : (
+ <>
+
Uploading… {uploadProgress}%
+
+ >
+ )}
+
+
+
+
+ );
+}