'use client'; import React, { useState, useEffect, useRef } from 'react'; import { Modal } from '@/components/Modal'; import { createGlobalState } from 'react-global-hooks'; import { useFromNull } from '@/hooks/useFromNull'; import { CaptionJobConfig } from '@/types'; import { defaultCaptionJobConfig } from '@/helpers/captionJobConfig'; import { objectCopy } from '@/utils/basic'; import { useNestedState } from '@/utils/hooks'; import { isMac } from '@/helpers/basic'; import useGPUInfo from '@/hooks/useGPUInfo'; import { apiClient } from '@/utils/api'; import { v4 as uuidv4 } from 'uuid'; import { startJob } from '@/utils/jobs'; import { startQueue } from '@/utils/queue'; import CaptionSimpleJob from '@/components/CaptionSimpleJob'; import AdvancedConfigEditor from '@/components/AdvancedConfigEditor'; import { SelectInput } from '@/components/formInputs'; import { Loader2 } from 'lucide-react'; export interface CaptionDatasetModalState { datasetPath: string; jobId?: string | null; cloneId?: string | null; onClose?: () => void; } export const captionDatasetModalState = createGlobalState(null); export const openCaptionDatasetModal = ( datasetPath: string, onClose?: () => void, options?: { jobId?: string | null; cloneId?: string | null }, ) => { captionDatasetModalState.set({ datasetPath, onClose, jobId: options?.jobId ?? null, cloneId: options?.cloneId ?? null, }); }; export const CaptionDatasetModal: React.FC = () => { const [modalInfo, setModalInfo] = captionDatasetModalState.use(); const [jobConfig, setJobConfig] = useNestedState(objectCopy(defaultCaptionJobConfig)); const [gpuIDs, setGpuIDs] = useState(null); const [existingJobName, setExistingJobName] = useState(null); const [hasLoadedExistingJob, setHasLoadedExistingJob] = useState(false); const { gpuList, isGPUInfoLoaded } = useGPUInfo(); const [activeTab, setActiveTab] = useState<'simple' | 'advanced'>('simple'); const open = modalInfo !== null; const isSavingRef = useRef(false); const [isSaving, setIsSaving] = useState(false); const showGPUSelect = !isMac(); const isLoadingExistingJob = !!(modalInfo?.jobId || modalInfo?.cloneId) && !hasLoadedExistingJob; const showLoadingOverlay = isLoadingExistingJob || isSaving; useFromNull(() => { // reset the state setJobConfig(objectCopy(defaultCaptionJobConfig)); setActiveTab('simple'); setExistingJobName(null); // set the path_to_caption if (modalInfo?.datasetPath) { setJobConfig(modalInfo.datasetPath, 'config.process[0].caption.path_to_caption'); } }, [modalInfo]); // clone existing caption job useEffect(() => { if (modalInfo?.cloneId) { apiClient .get(`/api/jobs?id=${modalInfo.cloneId}`) .then(res => res.data) .then(data => { setGpuIDs(data.gpu_ids); const newJobConfig = JSON.parse(data.job_config); newJobConfig.config.name = `${newJobConfig.config.name}_copy`; setJobConfig(newJobConfig); }) .catch(error => console.error('Error fetching caption job:', error)) .finally(() => setHasLoadedExistingJob(true)); } }, [modalInfo?.cloneId]); // load existing caption job for editing useEffect(() => { if (modalInfo?.jobId) { apiClient .get(`/api/jobs?id=${modalInfo.jobId}`) .then(res => res.data) .then(data => { setGpuIDs(data.gpu_ids); setExistingJobName(data.name); setJobConfig(JSON.parse(data.job_config)); }) .catch(error => console.error('Error fetching caption job:', error)) .finally(() => setHasLoadedExistingJob(true)); } }, [modalInfo?.jobId]); useEffect(() => { if (isGPUInfoLoaded) { if (gpuIDs === null && gpuList.length > 0) { setGpuIDs(`${gpuList[0].index}`); } } }, [gpuList, isGPUInfoLoaded]); const handleClose = () => { if (modalInfo?.onClose) { modalInfo.onClose(); } setHasLoadedExistingJob(false); setModalInfo(null); }; const saveJob = async () => { if (isSavingRef.current) return; if (!modalInfo?.datasetPath) { alert('Dataset path is missing. Please try again.'); return; } isSavingRef.current = true; setIsSaving(true); const isEdit = !!modalInfo.jobId; apiClient .post('/api/jobs', { id: isEdit ? modalInfo.jobId : null, name: isEdit && existingJobName ? existingJobName : uuidv4(), gpu_ids: gpuIDs, job_config: jobConfig, job_type: 'caption', job_ref: modalInfo.datasetPath, }) .then(async res => { const jobId = res.data.id; await startJob(jobId); // start the queue as well await startQueue(gpuIDs || ''); isSavingRef.current = false; setIsSaving(false); handleClose(); }) .catch(error => { if (error.response?.status === 409) { alert('A caption job for this dataset already exists. Please check your jobs list.'); } else { alert('Failed to save job. Please try again.'); } console.log('Error saving training:', error); isSavingRef.current = false; setIsSaving(false); }); }; const tabButtonClass = (tab: 'simple' | 'advanced') => `px-4 py-2 text-sm font-medium border-b-2 transition-colors ${ activeTab === tab ? 'border-blue-500 text-blue-400' : 'border-transparent text-gray-400 hover:text-gray-200 hover:border-gray-600' }`; return (
{showLoadingOverlay && (
)}
{activeTab === 'advanced' && showGPUSelect && (
setGpuIDs(value)} options={gpuList.map((gpu: any) => ({ value: `${gpu.index}`, label: `GPU #${gpu.index}` }))} />
)}
{ e.preventDefault(); saveJob(); }} > {activeTab === 'simple' ? ( ) : (
)}
); };