"use client"; import { useState, useRef, useEffect } from "react"; import { Header } from "@/components/layout"; import { AppLayout } from "@/components/layout"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Card, CardContent, } from "@/components/ui/card"; import { apiClient, type JobInfo, type JobDetailsResponse, } from "@/lib/api"; import { Loader2, Download, X, Upload } from "lucide-react"; import { downloadImage, downloadAuthenticatedImage, fileToBase64, } from "@/lib/utils"; import { useLocalStorage, useGeneratedImages } from "@/lib/storage"; import { useModelSelection, useModelTypeSelection, } from "@/contexts/model-selection-context"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; // import { AutoSelectionStatus } from '@/components/features/models'; type UpscalerFormData = { upscale_factor: number; model: string; }; const defaultFormData: UpscalerFormData = { upscale_factor: 2, model: "", }; function UpscalerForm() { const { actions } = useModelSelection(); const { availableModels: upscalerModels, selectedModel: selectedUpscalerModel, setSelectedModel: setSelectedUpscalerModel, } = useModelTypeSelection("upscaler"); const [formData, setFormData] = useLocalStorage( "upscaler-form-data", defaultFormData, { excludeLargeData: true, maxSize: 512 * 1024 }, ); // Separate state for image data (not stored in localStorage) const [uploadedImage, setUploadedImage] = useState(""); const [previewImage, setPreviewImage] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [jobInfo, setJobInfo] = useState(null); const { images: storedImages, addImages, getLatestImages } = useGeneratedImages('upscaler'); const [generatedImages, setGeneratedImages] = useState(() => getLatestImages()); const [pollCleanup, setPollCleanup] = useState<(() => void) | null>(null); const fileInputRef = useRef(null); // Cleanup polling on unmount useEffect(() => { return () => { if (pollCleanup) { pollCleanup(); } }; }, [pollCleanup]); useEffect(() => { const loadModels = async () => { try { // Fetch all models with enhanced info const modelsData = await apiClient.getModels(); // Filter for upscaler models const upscalerModels = modelsData.models.filter( (m) => m.type.toLowerCase() === "upscaler", ); actions.setModels(modelsData.models); // Set first upscaler model as default if none selected if (upscalerModels.length > 0 && !formData.model) { setFormData((prev) => ({ ...prev, model: upscalerModels[0].name, })); } } catch (err) { console.error("Failed to load upscaler models:", err); } }; loadModels(); }, [actions, setFormData]); // Update form data when upscaler model changes useEffect(() => { if (selectedUpscalerModel) { setFormData((prev) => ({ ...prev, model: selectedUpscalerModel, })); } }, [selectedUpscalerModel, setFormData]); const handleImageUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; try { const base64 = await fileToBase64(file); setUploadedImage(base64); setPreviewImage(base64); setError(null); } catch { setError("Failed to load image"); } }; const pollJobStatus = async (jobId: string) => { const maxAttempts = 300; let attempts = 0; let isPolling = true; let timeoutId: NodeJS.Timeout | null = null; const poll = async () => { if (!isPolling) return; try { const status: JobDetailsResponse = await apiClient.getJobStatus(jobId); setJobInfo(status.job); if (status.job.status === "completed") { let imageUrls: string[] = []; // Handle both old format (result.images) and new format (outputs) if (status.job.outputs && status.job.outputs.length > 0) { // New format: convert output URLs to authenticated image URLs with cache-busting imageUrls = status.job.outputs.map((output: { filename: string }) => { const filename = output.filename; return apiClient.getImageUrl(jobId, filename); }); } else if ( status.job.result?.images && status.job.result.images.length > 0 ) { // Old format: convert image URLs to authenticated URLs imageUrls = status.job.result.images.map((imageUrl: string) => { // Extract filename from URL if it's already a full URL if (imageUrl.includes("/output/")) { const parts = imageUrl.split("/output/"); if (parts.length === 2) { const filename = parts[1].split("?")[0]; // Remove query params return apiClient.getImageUrl(jobId, filename); } } // If it's just a filename, convert it directly return apiClient.getImageUrl(jobId, imageUrl); }); } // Create a new array to trigger React re-render setGeneratedImages([...imageUrls]); addImages(imageUrls, jobId); setLoading(false); isPolling = false; } else if (status.job.status === "failed") { setError(status.job.error || "Upscaling failed"); setLoading(false); isPolling = false; } else if (status.job.status === "cancelled") { setError("Upscaling was cancelled"); setLoading(false); isPolling = false; } else if (attempts < maxAttempts) { attempts++; timeoutId = setTimeout(poll, 2000); } else { setError("Job polling timeout"); setLoading(false); isPolling = false; } } catch { if (isPolling) { setError("Failed to check job status"); setLoading(false); isPolling = false; } } }; poll(); // Return cleanup function return () => { isPolling = false; if (timeoutId) { clearTimeout(timeoutId); } }; }; const handleUpscale = async (e: React.FormEvent) => { e.preventDefault(); if (!uploadedImage) { setError("Please upload an image first"); return; } setLoading(true); setError(null); setGeneratedImages([]); setJobInfo(null); try { // Validate model selection if (!selectedUpscalerModel) { setError("Please select an upscaler model"); setLoading(false); return; } const job = await apiClient.upscale({ image: uploadedImage, model: selectedUpscalerModel, upscale_factor: formData.upscale_factor, }); setJobInfo(job); const jobId = job.request_id || job.id; if (jobId) { const cleanup = pollJobStatus(jobId); setPollCleanup(() => cleanup); } else { setError("No job ID returned from server"); setLoading(false); } } catch { setError("Failed to upscale image"); setLoading(false); } }; const handleCancel = async () => { const jobId = jobInfo?.request_id || jobInfo?.id; if (jobId) { try { await apiClient.cancelJob(jobId); setLoading(false); setError("Upscaling cancelled"); // Cleanup polling if (pollCleanup) { pollCleanup(); setPollCleanup(null); } } catch (err) { console.error("Failed to cancel job:", err); } } }; return (
{/* Left Panel - Form */}
{previewImage && (
Preview
)}
{loading && ( )}
{error && (
{error}
)}

Upscaled Image

{generatedImages.length === 0 ? (

{loading ? "Upscaling..." : "Upscaled image will appear here"}

) : (
{generatedImages.map((image, index) => (
{`Upscaled
))}
)}
); } export default function UpscalerPage() { return ; }