"use client"; import { useState, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; import { Header } from "@/components/layout"; import { AppLayout } from "@/components/layout"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { apiClient, type JobDetailsResponse } from "@/lib/api"; import { Loader2, Download, RefreshCw, Calendar, Image as ImageIcon, Maximize2, ArrowUp, } from "lucide-react"; import { downloadAuthenticatedImage } from "@/lib/utils"; interface GalleryImage { jobId: string; filename: string; url: string; thumbnailUrl: string; prompt?: string; negativePrompt?: string; width?: number; height?: number; steps?: number; cfgScale?: number; seed?: string; model?: string; createdAt: string; status: string; } interface ImageModalProps { image: GalleryImage | null; isOpen: boolean; onClose: () => void; } function ImageModal({ image, isOpen, onClose }: ImageModalProps) { if (!isOpen || !image) return null; const handleBackdropClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { onClose(); } }; return (
{/* Image container with responsive sizing */}
Generated image
); } function GalleryGrid() { const router = useRouter(); const [images, setImages] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedImage, setSelectedImage] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [sortBy, setSortBy] = useState<"newest" | "oldest">("newest"); const loadGalleryImages = useCallback(async () => { try { setLoading(true); setError(null); console.log("Gallery: Loading images..."); // Get queue status to find all completed jobs const queueStatus = await apiClient.getQueueStatus(); console.log("Gallery: Queue status:", queueStatus); const completedJobs = queueStatus.jobs.filter( (job) => job.status === "completed", ); console.log("Gallery: Completed jobs:", completedJobs); const galleryImages: GalleryImage[] = []; // Fetch individual job details to get output information for (const job of completedJobs) { const jobId = job.request_id || job.id || ""; if (!jobId) continue; try { // Get detailed job information including outputs console.log(`Gallery: Fetching details for job ${jobId}`); const jobDetails: JobDetailsResponse = await apiClient.getJobStatus(jobId); console.log(`Gallery: Job ${jobId} details:`, jobDetails); // API response has outputs nested in job object if ( jobDetails.job && jobDetails.job.outputs && jobDetails.job.outputs.length > 0 ) { for (const output of jobDetails.job.outputs) { const filename = output.filename; const url = apiClient.getImageUrl(jobId, filename); // Create thumbnail URL (we'll use the same URL but let the browser handle scaling) const thumbnailUrl = url; galleryImages.push({ jobId, filename, url, thumbnailUrl, prompt: jobDetails.job.prompt || "", // Get prompt from job details negativePrompt: "", // Job info doesn't include negative prompt width: undefined, // Job info doesn't include request details height: undefined, // Job info doesn't include request details steps: undefined, // Job info doesn't include request details cfgScale: undefined, // Job info doesn't include request details seed: undefined, // Job info doesn't include request details model: "Unknown", // Job info doesn't include request details createdAt: jobDetails.job.created_at || new Date().toISOString(), status: jobDetails.job.status, }); } } } catch (err) { console.warn(`Failed to fetch details for job ${jobId}:`, err); // Continue with other jobs even if one fails } } // Sort images galleryImages.sort((a, b) => { const dateA = new Date(a.createdAt).getTime(); const dateB = new Date(b.createdAt).getTime(); return sortBy === "newest" ? dateB - dateA : dateA - dateB; }); console.log("Gallery: Final images array:", galleryImages); setImages(galleryImages); } catch (err) { console.error("Gallery: Error loading images:", err); setError( err instanceof Error ? err.message : "Failed to load gallery images", ); } finally { setLoading(false); } }, [sortBy]); useEffect(() => { loadGalleryImages(); }, [sortBy, loadGalleryImages]); const handleImageClick = (image: GalleryImage) => { setSelectedImage(image); setIsModalOpen(true); }; const handleModalClose = () => { setIsModalOpen(false); setSelectedImage(null); }; const handleDownload = (image: GalleryImage, e: React.MouseEvent) => { e.stopPropagation(); const authToken = localStorage.getItem("auth_token"); const unixUser = localStorage.getItem("unix_user"); downloadAuthenticatedImage( image.url, `gallery-${image.jobId}-${image.filename}`, authToken || undefined, unixUser || undefined, ); }; const handleUpscale = (image: GalleryImage, e: React.MouseEvent) => { e.stopPropagation(); // Navigate to upscaler page with image URL as query parameter router.push(`/upscaler?imageUrl=${encodeURIComponent(image.url)}`); }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); }; if (loading) { return (
Loading gallery...
); } if (error) { return (

Error loading gallery

{error}

); } return (
{/* Controls */}

{images.length} {images.length === 1 ? "Image" : "Images"}

Sort by:
{/* Gallery Grid */} {images.length === 0 ? (

No images found

Generate some images first using the Text to Image, Image to Image, or Inpainting tools.

) : (
{images.map((image: GalleryImage, index: number) => ( handleImageClick(image)} >
{/* Thumbnail with hover effect */}
{`Generated
{/* Date badge */}
{formatDate(image.createdAt)}
{/* Model indicator */} {image.model && image.model !== "Unknown" && (
{image.model}
)}
{/* Image info */}
{image.width && image.height ? `${image.width}×${image.height}` : "Unknown size"}
{new Date(image.createdAt).toLocaleDateString()}
{image.prompt && (

{image.prompt}

)} {/* Action buttons */}
))}
)} {/* Image Modal */}
); } export default function GalleryPage() { return ; }