import { Download, Eye, ImageOff, Package } from 'lucide-react' import { useState } from 'react' import { api } from '../api/client' import { useToast } from '../store/toast' import HtmlPreviewModal from './HtmlPreviewModal' interface Props { /** * Display-only; not used to construct URLs (files already contain any * batch-folder prefix). Kept in the props so callers don't need to be * edited. */ screenshotFolder?: string files: string[] title?: string } export default function ScreenshotGallery(props: Props) { const { files, title = 'Screenshots' } = props // props.screenshotFolder is intentionally unused — see Props docs. const [preview, setPreview] = useState(null) const [zipping, setZipping] = useState(false) const toast = useToast() if (files.length === 0) { return (
No screenshots yet
Start a run and finished screenshots will appear here.
) } const downloadZip = async () => { setZipping(true) try { // `files` are already paths relative to OUTPUT_FOLDER (e.g. "batch 3/5(1).png" // or "5(1).png"). The backend /download-zip handler resolves them under // OUTPUT_FOLDER itself, so we MUST NOT prepend screenshotFolder again — // doing so produced "batch 3/batch 3/5(1).png" and empty ZIPs. const paths = files const blob = await api.downloadZip(paths, 'screenshots') const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = 'screenshots.zip' document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) } catch (err) { toast.push({ variant: 'error', title: 'Download failed', message: err instanceof Error ? err.message : String(err), }) } finally { setZipping(false) } } return (

{title} ({files.length})

{files.map((filename) => { const url = api.screenshotUrl(filename) return (
{/* `object-contain` so we never crop content; the checkered */} {/* background makes letterboxed bars look intentional. */}
{filename} setPreview(url)} />
{filename.split('/').pop()}
) })}
{preview && ( setPreview(null)} /> )}
) }