Spaces:
Paused
Paused
'use client'; | |
import { createGlobalState } from 'react-global-hooks'; | |
import { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'; | |
import { FaUpload } from 'react-icons/fa'; | |
import { useCallback, useState } from 'react'; | |
import { useDropzone } from 'react-dropzone'; | |
import { apiClient } from '@/utils/api'; | |
export interface AddImagesModalState { | |
datasetName: string; | |
onComplete?: () => void; | |
} | |
export const addImagesModalState = createGlobalState<AddImagesModalState | null>(null); | |
export const openImagesModal = (datasetName: string, onComplete: () => void) => { | |
addImagesModalState.set({ datasetName, onComplete }); | |
}; | |
export default function AddImagesModal() { | |
const [addImagesModalInfo, setAddImagesModalInfo] = addImagesModalState.use(); | |
const [uploadProgress, setUploadProgress] = useState<number>(0); | |
const [isUploading, setIsUploading] = useState<boolean>(false); | |
const open = addImagesModalInfo !== null; | |
const onCancel = () => { | |
if (!isUploading) { | |
setAddImagesModalInfo(null); | |
} | |
}; | |
const onDone = () => { | |
if (addImagesModalInfo?.onComplete && !isUploading) { | |
addImagesModalInfo.onComplete(); | |
setAddImagesModalInfo(null); | |
} | |
}; | |
const onDrop = useCallback( | |
async (acceptedFiles: File[]) => { | |
if (acceptedFiles.length === 0) return; | |
setIsUploading(true); | |
setUploadProgress(0); | |
const formData = new FormData(); | |
acceptedFiles.forEach(file => { | |
formData.append('files', file); | |
}); | |
formData.append('datasetName', addImagesModalInfo?.datasetName || ''); | |
try { | |
await apiClient.post(`/api/datasets/upload`, formData, { | |
headers: { | |
'Content-Type': 'multipart/form-data', | |
}, | |
onUploadProgress: progressEvent => { | |
const percentCompleted = Math.round((progressEvent.loaded * 100) / (progressEvent.total || 100)); | |
setUploadProgress(percentCompleted); | |
}, | |
timeout: 0, // Disable timeout | |
}); | |
onDone(); | |
} catch (error) { | |
console.error('Upload failed:', error); | |
} finally { | |
setIsUploading(false); | |
setUploadProgress(0); | |
} | |
}, | |
[addImagesModalInfo], | |
); | |
const { getRootProps, getInputProps, isDragActive } = useDropzone({ | |
onDrop, | |
accept: { | |
'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'], | |
'video/*': ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.m4v', '.flv'], | |
'text/*': ['.txt'], | |
}, | |
multiple: true, | |
}); | |
return ( | |
<Dialog open={open} onClose={onCancel} className="relative z-10"> | |
<DialogBackdrop | |
transition | |
className="fixed inset-0 bg-gray-900/75 transition-opacity data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in" | |
/> | |
<div className="fixed inset-0 z-10 w-screen overflow-y-auto"> | |
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"> | |
<DialogPanel | |
transition | |
className="relative transform overflow-hidden rounded-lg bg-gray-800 text-left shadow-xl transition-all data-closed:translate-y-4 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in sm:my-8 sm:w-full sm:max-w-lg data-closed:sm:translate-y-0 data-closed:sm:scale-95" | |
> | |
<div className="bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4"> | |
<div className="text-center"> | |
<DialogTitle as="h3" className="text-base font-semibold text-gray-200 mb-4"> | |
Add Images to: {addImagesModalInfo?.datasetName} | |
</DialogTitle> | |
<div className="w-full"> | |
<div | |
{...getRootProps()} | |
className={`h-40 w-full flex flex-col items-center justify-center border-2 border-dashed rounded-lg cursor-pointer transition-colors duration-200 | |
${isDragActive ? 'border-blue-500 bg-blue-50/10' : 'border-gray-600'}`} | |
> | |
<input {...getInputProps()} /> | |
<FaUpload className="size-8 mb-3 text-gray-400" /> | |
<p className="text-sm text-gray-200 text-center"> | |
{isDragActive ? 'Drop the files here...' : 'Drag & drop files here, or click to select files'} | |
</p> | |
</div> | |
{isUploading && ( | |
<div className="mt-4"> | |
<div className="w-full bg-gray-700 rounded-full h-2.5"> | |
<div className="bg-blue-600 h-2.5 rounded-full" style={{ width: `${uploadProgress}%` }}></div> | |
</div> | |
<p className="text-sm text-gray-300 mt-2 text-center">Uploading... {uploadProgress}%</p> | |
</div> | |
)} | |
</div> | |
</div> | |
</div> | |
<div className="bg-gray-700 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"> | |
<button | |
type="button" | |
onClick={onDone} | |
disabled={isUploading} | |
className={`inline-flex w-full justify-center rounded-md bg-slate-600 px-3 py-2 text-sm font-semibold text-white shadow-xs sm:ml-3 sm:w-auto | |
${isUploading ? 'opacity-50 cursor-not-allowed' : ''}`} | |
> | |
Done | |
</button> | |
<button | |
type="button" | |
data-autofocus | |
onClick={onCancel} | |
disabled={isUploading} | |
className={`mt-3 inline-flex w-full justify-center rounded-md bg-gray-800 px-3 py-2 text-sm font-semibold text-gray-200 hover:bg-gray-800 sm:mt-0 sm:w-auto ring-0 | |
${isUploading ? 'opacity-50 cursor-not-allowed' : ''}`} | |
> | |
Cancel | |
</button> | |
</div> | |
</DialogPanel> | |
</div> | |
</div> | |
</Dialog> | |
); | |
} | |