import { useState, useEffect, useCallback } from "react"; import { HF_ORG } from "../../../config"; interface ImageViewerProps { datasetRepo: string; split?: string; onClose: () => void; } interface ImageRow { src: string; caption: string; rowIndex: number; } // Attempt to extract an image URL from a column value returned by the datasets-server API. // Image columns come back as { src: "..." }, but plain string URLs and path strings are also handled. function extractImageUrl(value: unknown): string | null { if (!value) return null; if (typeof value === "string") { const trimmed = value.trim(); if (trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("/")) { return trimmed; } return null; } if (typeof value === "object") { const obj = value as Record; if (typeof obj.src === "string") return obj.src; if (typeof obj.url === "string") return obj.url; if (typeof obj.path === "string" && (obj.path as string).startsWith("http")) return obj.path as string; } return null; } // Return the first string value that exists among the given keys in a row object. function pickCaption(row: Record, keys: string[]): string | null { for (const key of keys) { const v = row[key]; if (typeof v === "string" && v.trim()) return v.trim(); if (typeof v === "number") return String(v); } return null; } export default function ImageViewer({ datasetRepo, split = "train", onClose }: ImageViewerProps) { // Ensure dataset repo has org prefix for HF API calls const fullRepo = datasetRepo.includes("/") ? datasetRepo : `${HF_ORG}/${datasetRepo}`; const [images, setImages] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [noImageColumns, setNoImageColumns] = useState(false); // Lightbox state const [lightboxIndex, setLightboxIndex] = useState(null); // ---- Data loading ---- useEffect(() => { let cancelled = false; async function fetchImages() { setLoading(true); setError(null); setNoImageColumns(false); try { const url = `https://datasets-server.huggingface.co/rows?dataset=${encodeURIComponent(fullRepo)}&config=default&split=${encodeURIComponent(split)}&offset=0&length=20`; const res = await fetch(url); if (!res.ok) { throw new Error(`Datasets server returned ${res.status}: ${res.statusText}`); } const data = await res.json(); const rows: Record[] = data.rows?.map( (r: { row: Record }) => r.row ) ?? []; if (rows.length === 0) { if (!cancelled) { setImages([]); setLoading(false); } return; } // Find image column(s): any column whose value for the first row resolves to an image URL, // or whose key contains image/url/path hints. const firstRow = rows[0]; const columnNames = Object.keys(firstRow); // Rank columns: explicit image columns first, then url/path hints, then anything resolving const imageColumns: string[] = []; // Pass 1: columns that directly contain image objects or HTTP URLs for (const col of columnNames) { if (extractImageUrl(firstRow[col]) !== null) { imageColumns.push(col); } } // Pass 2: columns with suggestive names that we haven't already picked if (imageColumns.length === 0) { const hints = ["image", "img", "image_url", "url", "path", "file"]; for (const hint of hints) { const match = columnNames.find((c) => c.toLowerCase().includes(hint)); if (match && !imageColumns.includes(match)) { imageColumns.push(match); } } } if (imageColumns.length === 0) { if (!cancelled) { setNoImageColumns(true); setLoading(false); } return; } // Caption columns (first match wins per row) const captionKeys = ["caption", "description", "label", "title", "text", "name"]; const imageCol = imageColumns[0]; const extracted: ImageRow[] = []; rows.forEach((row, idx) => { const src = extractImageUrl(row[imageCol]); if (src) { const caption = pickCaption(row, captionKeys) ?? `Image ${idx + 1}`; extracted.push({ src, caption, rowIndex: idx }); } }); if (!cancelled) { setImages(extracted); setLoading(false); } } catch (err: unknown) { if (!cancelled) { setError(err instanceof Error ? err.message : "Failed to load images"); setLoading(false); } } } fetchImages(); return () => { cancelled = true; }; }, [fullRepo, split]); // ---- Lightbox keyboard navigation ---- const closeLightbox = useCallback(() => setLightboxIndex(null), []); const prevImage = useCallback(() => { setLightboxIndex((i) => (i === null || i === 0 ? images.length - 1 : i - 1)); }, [images.length]); const nextImage = useCallback(() => { setLightboxIndex((i) => (i === null ? 0 : (i + 1) % images.length)); }, [images.length]); useEffect(() => { if (lightboxIndex === null) return; const handler = (e: KeyboardEvent) => { if (e.key === "Escape") closeLightbox(); if (e.key === "ArrowLeft") prevImage(); if (e.key === "ArrowRight") nextImage(); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [lightboxIndex, closeLightbox, prevImage, nextImage]); const shortName = datasetRepo.split("/").pop() ?? datasetRepo; // ---- Render ---- return (
{/* Header */}
{shortName} {datasetRepo.includes("/") && ( {datasetRepo.split("/")[0]}/ )} {!loading && !error && !noImageColumns && ( {images.length} images )}
{/* Body */}
{loading && (
Loading images...
)} {!loading && error && (

Failed to load dataset

{error}

)} {!loading && !error && noImageColumns && (

No image columns found in this dataset.

)} {!loading && !error && !noImageColumns && images.length === 0 && (

No rows found in this split.

)} {!loading && !error && images.length > 0 && (
{images.map((img, idx) => ( ))}
)}
{/* Lightbox overlay */} {lightboxIndex !== null && images[lightboxIndex] && (
{/* Close */} {/* Prev arrow */} {images.length > 1 && ( )} {/* Image */}
e.stopPropagation()} > {images[lightboxIndex].caption}

{images[lightboxIndex].caption}

{images.length > 1 && (

{lightboxIndex + 1} / {images.length}

)}
{/* Next arrow */} {images.length > 1 && ( )}
)}
); }