Allow edit of captioning job

This commit is contained in:
Jaret Burkett
2026-04-30 16:00:16 -06:00
parent eadc9a58af
commit 0d91fcee9e
5 changed files with 88 additions and 12 deletions

View File

@@ -8,7 +8,6 @@ import { Button } from '@headlessui/react';
import AddImagesModal, { openImagesModal, useOpenImagesModalOnDrag } from '@/components/AddImagesModal'; import AddImagesModal, { openImagesModal, useOpenImagesModalOnDrag } from '@/components/AddImagesModal';
import { TopBar, MainContent } from '@/components/layout'; import { TopBar, MainContent } from '@/components/layout';
import { apiClient } from '@/utils/api'; import { apiClient } from '@/utils/api';
import { CaptionDatasetModal, openCaptionDatasetModal } from '@/components/CaptionDatasetModal';
import useSettings from '@/hooks/useSettings'; import useSettings from '@/hooks/useSettings';
import { pathJoin } from '@/utils/basic'; import { pathJoin } from '@/utils/basic';
import AutoCaptionButton from '@/components/AutoCaptionButton'; import AutoCaptionButton from '@/components/AutoCaptionButton';
@@ -140,7 +139,6 @@ export default function DatasetPage({ params }: { params: { datasetName: string
)} )}
</MainContent> </MainContent>
<AddImagesModal /> <AddImagesModal />
<CaptionDatasetModal />
</> </>
); );
} }

View File

@@ -8,6 +8,7 @@ import { Suspense } from 'react';
import AuthWrapper from '@/components/AuthWrapper'; import AuthWrapper from '@/components/AuthWrapper';
import DocModal from '@/components/DocModal'; import DocModal from '@/components/DocModal';
import os from 'os'; import os from 'os';
import { CaptionDatasetModal } from '@/components/CaptionDatasetModal';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -53,6 +54,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</ThemeProvider> </ThemeProvider>
<ConfirmModal /> <ConfirmModal />
<DocModal /> <DocModal />
<CaptionDatasetModal />
</body> </body>
</html> </html>
); );

View File

@@ -1,3 +1,4 @@
'use client';
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Modal } from '@/components/Modal'; import { Modal } from '@/components/Modal';
import { createGlobalState } from 'react-global-hooks'; import { createGlobalState } from 'react-global-hooks';
@@ -15,38 +16,89 @@ import { startQueue } from '@/utils/queue';
import CaptionSimpleJob from '@/components/CaptionSimpleJob'; import CaptionSimpleJob from '@/components/CaptionSimpleJob';
import AdvancedConfigEditor from '@/components/AdvancedConfigEditor'; import AdvancedConfigEditor from '@/components/AdvancedConfigEditor';
import { SelectInput } from '@/components/formInputs'; import { SelectInput } from '@/components/formInputs';
import { Loader2 } from 'lucide-react';
export interface CaptionDatasetModalState { export interface CaptionDatasetModalState {
datasetPath: string; datasetPath: string;
jobId?: string | null;
cloneId?: string | null;
onClose?: () => void; onClose?: () => void;
} }
export const captionDatasetModalState = createGlobalState<CaptionDatasetModalState | null>(null); export const captionDatasetModalState = createGlobalState<CaptionDatasetModalState | null>(null);
export const openCaptionDatasetModal = (datasetPath: string, onClose?: () => void) => { export const openCaptionDatasetModal = (
captionDatasetModalState.set({ datasetPath, onClose }); 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 = () => { export const CaptionDatasetModal: React.FC = () => {
const [modalInfo, setModalInfo] = captionDatasetModalState.use(); const [modalInfo, setModalInfo] = captionDatasetModalState.use();
const [jobConfig, setJobConfig] = useNestedState<CaptionJobConfig>(objectCopy(defaultCaptionJobConfig)); const [jobConfig, setJobConfig] = useNestedState<CaptionJobConfig>(objectCopy(defaultCaptionJobConfig));
const [gpuIDs, setGpuIDs] = useState<string | null>(null); const [gpuIDs, setGpuIDs] = useState<string | null>(null);
const [existingJobName, setExistingJobName] = useState<string | null>(null);
const [hasLoadedExistingJob, setHasLoadedExistingJob] = useState(false);
const { gpuList, isGPUInfoLoaded } = useGPUInfo(); const { gpuList, isGPUInfoLoaded } = useGPUInfo();
const [activeTab, setActiveTab] = useState<'simple' | 'advanced'>('simple'); const [activeTab, setActiveTab] = useState<'simple' | 'advanced'>('simple');
const open = modalInfo !== null; const open = modalInfo !== null;
const isSavingRef = useRef(false); const isSavingRef = useRef(false);
const [isSaving, setIsSaving] = useState(false);
const showGPUSelect = !isMac(); const showGPUSelect = !isMac();
const isLoadingExistingJob = !!(modalInfo?.jobId || modalInfo?.cloneId) && !hasLoadedExistingJob;
const showLoadingOverlay = isLoadingExistingJob || isSaving;
useFromNull(() => { useFromNull(() => {
// reset the state // reset the state
setJobConfig(objectCopy(defaultCaptionJobConfig)); setJobConfig(objectCopy(defaultCaptionJobConfig));
setActiveTab('simple'); setActiveTab('simple');
setExistingJobName(null);
// set the path_to_caption // set the path_to_caption
if (modalInfo?.datasetPath) { if (modalInfo?.datasetPath) {
setJobConfig(modalInfo.datasetPath, 'config.process[0].caption.path_to_caption'); setJobConfig(modalInfo.datasetPath, 'config.process[0].caption.path_to_caption');
} }
}, [modalInfo]); }, [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(() => { useEffect(() => {
if (isGPUInfoLoaded) { if (isGPUInfoLoaded) {
if (gpuIDs === null && gpuList.length > 0) { if (gpuIDs === null && gpuList.length > 0) {
@@ -59,6 +111,7 @@ export const CaptionDatasetModal: React.FC = () => {
if (modalInfo?.onClose) { if (modalInfo?.onClose) {
modalInfo.onClose(); modalInfo.onClose();
} }
setHasLoadedExistingJob(false);
setModalInfo(null); setModalInfo(null);
}; };
@@ -69,11 +122,14 @@ export const CaptionDatasetModal: React.FC = () => {
return; return;
} }
isSavingRef.current = true; isSavingRef.current = true;
setIsSaving(true);
const isEdit = !!modalInfo.jobId;
apiClient apiClient
.post('/api/jobs', { .post('/api/jobs', {
id: null, id: isEdit ? modalInfo.jobId : null,
name: uuidv4(), name: isEdit && existingJobName ? existingJobName : uuidv4(),
gpu_ids: gpuIDs, gpu_ids: gpuIDs,
job_config: jobConfig, job_config: jobConfig,
job_type: 'caption', job_type: 'caption',
@@ -85,6 +141,7 @@ export const CaptionDatasetModal: React.FC = () => {
// start the queue as well // start the queue as well
await startQueue(gpuIDs || ''); await startQueue(gpuIDs || '');
isSavingRef.current = false; isSavingRef.current = false;
setIsSaving(false);
handleClose(); handleClose();
}) })
.catch(error => { .catch(error => {
@@ -95,6 +152,7 @@ export const CaptionDatasetModal: React.FC = () => {
} }
console.log('Error saving training:', error); console.log('Error saving training:', error);
isSavingRef.current = false; isSavingRef.current = false;
setIsSaving(false);
}); });
}; };
@@ -107,7 +165,12 @@ export const CaptionDatasetModal: React.FC = () => {
return ( return (
<Modal isOpen={open} onClose={handleClose} title="Caption Dataset" size={activeTab === 'advanced' ? 'xl' : 'lg'}> <Modal isOpen={open} onClose={handleClose} title="Caption Dataset" size={activeTab === 'advanced' ? 'xl' : 'lg'}>
<div className="space-y-4 text-gray-200"> <div className="relative space-y-4 text-gray-200">
{showLoadingOverlay && (
<div className="absolute -left-6 -right-6 -top-4 -bottom-4 z-10 flex items-center justify-center backdrop-blur-sm bg-gray-900/40">
<Loader2 className="w-10 h-10 text-blue-400 animate-spin" />
</div>
)}
<div className="flex items-center border-b border-gray-700 -mt-2"> <div className="flex items-center border-b border-gray-700 -mt-2">
<button type="button" className={tabButtonClass('simple')} onClick={() => setActiveTab('simple')}> <button type="button" className={tabButtonClass('simple')} onClick={() => setActiveTab('simple')}>
Simple Simple

View File

@@ -7,6 +7,7 @@ import { startJob, stopJob, deleteJob, getAvaliableJobActions, markJobAsStopped
import { startQueue } from '@/utils/queue'; import { startQueue } from '@/utils/queue';
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'; import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { openCaptionDatasetModal } from '@/components/CaptionDatasetModal';
interface JobActionBarProps { interface JobActionBarProps {
job: Job; job: Job;
@@ -84,7 +85,23 @@ export default function JobActionBar({
<Eye /> <Eye />
</Link> </Link>
)} )}
{canEdit && ( {job.job_type === 'caption' && canEdit && (
<div
className="ml-2 hover:text-gray-100 inline-block cursor-pointer"
onClick={() =>
openCaptionDatasetModal(
job.job_ref || '',
() => {
if (onRefresh) onRefresh();
},
{ jobId: job.id },
)
}
>
<Pen />
</div>
)}
{job.job_type === 'train' && canEdit && (
<Link href={`/jobs/new?id=${job.id}`} className="ml-2 hover:text-gray-100 inline-block"> <Link href={`/jobs/new?id=${job.id}`} className="ml-2 hover:text-gray-100 inline-block">
<Pen /> <Pen />
</Link> </Link>

View File

@@ -82,10 +82,6 @@ export const getAvaliableJobActions = (job: Job) => {
if (job.status === 'completed' && (jobConfig.config.process[0].train?.steps || 0) > job.step && !isStopping) { if (job.status === 'completed' && (jobConfig.config.process[0].train?.steps || 0) > job.step && !isStopping) {
canStart = true; canStart = true;
} }
if (job.job_type !== 'train') {
// for non-train jobs, allow editing unless it's currently running
canEdit = false;
}
return { canDelete, canEdit, canStop, canStart, canRemoveFromQueue }; return { canDelete, canEdit, canStop, canStart, canRemoveFromQueue };
}; };