diff --git a/ui/src/app/datasets/[datasetName]/page.tsx b/ui/src/app/datasets/[datasetName]/page.tsx index 9deb7c7..ebcb47e 100644 --- a/ui/src/app/datasets/[datasetName]/page.tsx +++ b/ui/src/app/datasets/[datasetName]/page.tsx @@ -4,6 +4,7 @@ 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 DatasetImageViewer from '@/components/DatasetImageViewer'; import { Button } from '@headlessui/react'; import AddImagesModal, { openImagesModal, useOpenImagesModalOnDrag } from '@/components/AddImagesModal'; import { TopBar, MainContent } from '@/components/layout'; @@ -19,6 +20,8 @@ export default function DatasetPage({ params }: { params: { datasetName: string const datasetName = usableParams.datasetName; const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); const { settings, isSettingsLoaded } = useSettings(); + const [selectedImgPath, setSelectedImgPath] = useState(null); + const [captionRefreshKeys, setCaptionRefreshKeys] = useState>({}); const refreshImageList = (dbName: string) => { setStatus('loading'); @@ -137,12 +140,21 @@ export default function DatasetPage({ params }: { params: { datasetName: string isAutoCaptioning={isAutoCaptioning} imageUrl={img.img_path} onDelete={() => refreshImageList(datasetName)} + onImageClick={() => setSelectedImgPath(img.img_path)} + captionRefreshKey={captionRefreshKeys[img.img_path] || 0} /> ))} )} + img.img_path)} + onChange={setSelectedImgPath} + refreshImages={() => refreshImageList(datasetName)} + onCaptionSaved={path => setCaptionRefreshKeys(prev => ({ ...prev, [path]: (prev[path] || 0) + 1 }))} + /> ); } diff --git a/ui/src/components/DatasetImageCard.tsx b/ui/src/components/DatasetImageCard.tsx index 8a4360a..8a8112c 100644 --- a/ui/src/components/DatasetImageCard.tsx +++ b/ui/src/components/DatasetImageCard.tsx @@ -13,6 +13,8 @@ interface DatasetImageCardProps { children?: ReactNode; className?: string; onDelete?: () => void; + onImageClick?: () => void; + captionRefreshKey?: number; } const DatasetImageCard: React.FC = ({ @@ -22,6 +24,8 @@ const DatasetImageCard: React.FC = ({ children, className = '', onDelete = () => {}, + onImageClick, + captionRefreshKey = 0, }) => { const cardRef = useRef(null); const [isVisible, setIsVisible] = useState(false); @@ -46,7 +50,7 @@ const DatasetImageCard: React.FC = ({ data = `${data}`; } setCaption(data || ''); - setSavedCaption(data || ''); + setSavedCaption((data || '').trim()); setIsCaptionLoaded(true); }) .catch(error => { @@ -92,6 +96,12 @@ const DatasetImageCard: React.FC = ({ return () => clearInterval(interval); }, [isAutoCaptioning, inViewport, isVisible]); + // External trigger (e.g. caption edited in the full-screen viewer) — re-fetch + useEffect(() => { + if (captionRefreshKey === 0) return; + setIsCaptionLoaded(false); + }, [captionRefreshKey]); + useEffect(() => { // Create intersection observer to check viewport visibility const observer = new IntersectionObserver( @@ -194,9 +204,12 @@ const DatasetImageCard: React.FC = ({ src={`/api/img/${encodeURIComponent(imageUrl)}`} alt={alt} onLoad={handleLoad} - className={`w-full h-full object-contain transition-opacity duration-300 ${ - loaded ? 'opacity-100' : 'opacity-0' - }`} + onClick={onImageClick} + className={classNames('w-full h-full object-contain transition-opacity duration-300', { + 'opacity-100': loaded, + 'opacity-0': !loaded, + 'cursor-zoom-in': !!onImageClick, + })} /> )} @@ -250,7 +263,7 @@ const DatasetImageCard: React.FC = ({ onBlur={saveCaption} >