"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 */}
);
}
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 (
);
}
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 */}
{/* 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 ;
}