diff --git a/ui/src/app/datasets/[datasetName]/page.tsx b/ui/src/app/datasets/[datasetName]/page.tsx index 29dbaa6..147f9c1 100644 --- a/ui/src/app/datasets/[datasetName]/page.tsx +++ b/ui/src/app/datasets/[datasetName]/page.tsx @@ -13,6 +13,7 @@ import { apiClient } from '@/utils/api'; import useSettings from '@/hooks/useSettings'; import { pathJoin } from '@/utils/basic'; import AutoCaptionButton from '@/components/AutoCaptionButton'; +import CaptionMonitor from '@/components/CaptionMonitor'; import { CreatableSelectInput } from '@/components/formInputs'; export default function DatasetPage({ params }: { params: { datasetName: string } }) { @@ -26,6 +27,7 @@ export default function DatasetPage({ params }: { params: { datasetName: string const [captionExt, setCaptionExt] = useState('txt'); const [captionRefreshKeys, setCaptionRefreshKeys] = useState>({}); const [scrollParent, setScrollParent] = useState(null); + const [captionBarHeight, setCaptionBarHeight] = useState(0); const scrollParentCallback = useCallback((el: HTMLDivElement | null) => setScrollParent(el), []); const refreshImageList = (dbName: string) => { @@ -174,8 +176,17 @@ export default function DatasetPage({ params }: { params: { datasetName: string computeItemKey={index => imgList[index]?.img_path ?? index} /> )} + {/* Spacer so the last cards stay accessible above the floating caption bar. + Always keeps a baseline gap, plus the bar height when it is showing. */} +
+ {isSettingsLoaded && ( + + )}
diff --git a/ui/src/components/CaptionMonitor.tsx b/ui/src/components/CaptionMonitor.tsx new file mode 100644 index 0000000..d2e1e02 --- /dev/null +++ b/ui/src/components/CaptionMonitor.tsx @@ -0,0 +1,156 @@ +'use client'; + +import { useEffect, useMemo, useRef, useState } from 'react'; +import { ChevronDown, ChevronUp, Cpu } from 'lucide-react'; +import useJobByRef from '@/hooks/useJobByRef'; +import useJobLog from '@/hooks/useJobLog'; +import useGPUInfo from '@/hooks/useGPUInfo'; +import GPUWidget from '@/components/GPUWidget'; +import JobActionBar from '@/components/JobActionBar'; +import { getTotalSteps } from '@/utils/jobs'; + +interface CaptionMonitorProps { + datasetPath: string; + // Reports the current docked height (px) so the page can pad content to match. + onHeightChange?: (height: number) => void; +} + +// A small floating module that pops up from the bottom while a dataset is being +// auto-captioned. It is a simplified version of the job page: action bar, GPU +// info, and the live log. +export default function CaptionMonitor({ datasetPath, onHeightChange }: CaptionMonitorProps) { + const { job, status, refreshJob } = useJobByRef(datasetPath, 3000); + const [collapsed, setCollapsed] = useState(false); + + const isActive = !!(job && (job.status === 'running' || job.status === 'queued')); + + // Reset the collapsed state whenever a new captioning run starts. + useEffect(() => { + if (isActive) setCollapsed(false); + }, [job?.id, isActive]); + + const gpuIds = useMemo(() => { + if (!job) return []; + if (job.gpu_ids === 'mps') return [0]; + return job.gpu_ids.split(',').map(id => parseInt(id)); + }, [job?.gpu_ids]); + + const { gpuList, isGPUInfoLoaded } = useGPUInfo(gpuIds, 5000); + const { log, status: statusLog } = useJobLog(job?.id ?? '', isActive ? 2000 : null); + + const totalSteps = job ? getTotalSteps(job) : 0; + const progress = totalSteps > 0 ? (job!.step / totalSteps) * 100 : 0; + + const logRef = useRef(null); + const [isScrolledToBottom, setIsScrolledToBottom] = useState(true); + + const logLines: string[] = useMemo(() => { + let splits: string[] = log.split(/\n|\r\n/); + splits = splits.map(line => line.split(/\r/).pop()) as string[]; + const maxLines = 1000; + if (splits.length > maxLines) { + splits = splits.slice(splits.length - maxLines); + } + return splits; + }, [log]); + + const handleScroll = () => { + if (logRef.current) { + const { scrollTop, scrollHeight, clientHeight } = logRef.current; + setIsScrolledToBottom(scrollHeight - scrollTop - clientHeight < 10); + } + }; + + useEffect(() => { + if (logRef.current && isScrolledToBottom) { + logRef.current.scrollTop = logRef.current.scrollHeight; + } + }, [log, isScrolledToBottom]); + + // Animate the docked height instead of translating, so the collapsed panel + // never extends below the container and adds scroll. Inactive -> 0 (hidden), + // collapsed -> just the header bar, expanded -> full panel. + const HEADER_HEIGHT = 44; + const PANEL_HEIGHT = 300; + let height = 0; + if (isActive) height = collapsed ? HEADER_HEIGHT : PANEL_HEIGHT; + + useEffect(() => { + onHeightChange?.(height); + }, [height, onHeightChange]); + + return ( +
+
+ {/* Header / action bar */} +
+ + {job?.status ?? '...'} + +

{job?.info || 'Captioning...'}

+ {totalSteps > 0 && ( +
+
+
+
+ + {job!.step} / {totalSteps} + +
+ )} +
+ {job && } + +
+
+ + {/* Body: log + GPU info */} +
+
+
+
+ {statusLog === 'loading' && 'Loading log...'} + {statusLog === 'error' && 'Error loading log'} + {['success', 'refreshing'].includes(statusLog) && ( +
+ {logLines.map((line, index) => ( +
{line}
+ ))} +
+ )} +
+
+
+
+ {isGPUInfoLoaded && gpuList.length > 0 ? ( + + ) : ( +
+ Loading GPU info... +
+ )} +
+
+
+
+ ); +} diff --git a/ui/src/components/DatasetImageViewer.tsx b/ui/src/components/DatasetImageViewer.tsx index e8873c3..c3dcd06 100644 --- a/ui/src/components/DatasetImageViewer.tsx +++ b/ui/src/components/DatasetImageViewer.tsx @@ -392,12 +392,12 @@ export default function DatasetImageViewer({ if (!mounted) return null; return createPortal( - + -
+
- + {job.job_type === 'train' && ( )} - {canStop && ( + {job.job_type === 'train' && canStop && (
{ let title = row.name; + let href = `/jobs/${row.id}`; // if (row.job_type === 'train') title = `Train: ${title}`; if (row.job_type === 'caption') { let splits = row.job_ref.split(/[/\\]/); const datasetPath = `${splits[splits.length - 1]}`; + href = `/datasets/${datasetPath}`; title = ( <> CAPTION: {datasetPath} @@ -137,7 +139,7 @@ export default function JobsTable({ onlyActive = false, job_type = null }: JobsT ); } return ( - + {['running', 'stopping'].includes(row.status) ? ( ) : null}