'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 { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { apiClient, type JobInfo, type ModelInfo } from '@/lib/api'; import { Loader2, Download, X, Upload } from 'lucide-react'; import { downloadImage, downloadAuthenticatedImage, fileToBase64 } from '@/lib/utils'; import { useLocalStorage } from '@/lib/storage'; import { ModelSelectionProvider, useModelSelection, useModelTypeSelection } from '@/contexts/model-selection-context'; import { EnhancedModelSelect } from '@/components/features/models'; // import { AutoSelectionStatus } from '@/components/features/models'; type UpscalerFormData = { upscale_factor: number; model: string; }; const defaultFormData: UpscalerFormData = { upscale_factor: 2, model: '', }; function UpscalerForm() { const { state, actions } = useModelSelection(); const { availableModels: upscalerModels, selectedModel: selectedUpscalerModel, isUserOverride: isUpscalerUserOverride, isAutoSelected: isUpscalerAutoSelected, setSelectedModel: setSelectedUpscalerModel, setUserOverride: setUpscalerUserOverride, clearUserOverride: clearUpscalerUserOverride, } = 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 [generatedImages, setGeneratedImages] = useState([]); 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 (ESRGAN and upscaler types) const allUpscalerModels = [ ...modelsData.models.filter(m => m.type.toLowerCase() === 'esrgan'), ...modelsData.models.filter(m => m.type.toLowerCase() === 'upscaler') ]; actions.setModels(modelsData.models); // Set first model as default if none selected if (allUpscalerModels.length > 0 && !formData.model) { setFormData(prev => ({ ...prev, model: allUpscalerModels[0].name })); } } catch (err) { console.error('Failed to load upscaler models:', err); } }; loadModels(); }, [actions, formData.model, setFormData]); // Update form data when upscaler model changes useEffect(() => { if (selectedUpscalerModel) { setFormData(prev => ({ ...prev, model: selectedUpscalerModel, })); } }, [selectedUpscalerModel, setFormData]); const handleInputChange = ( e: React.ChangeEvent ) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: name === 'upscale_factor' ? Number(value) : value, })); }; 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 (err) { setError('Failed to load image'); } }; const pollJobStatus = async (jobId: string) => { const maxAttempts = 300; let attempts = 0; let isPolling = true; const poll = async () => { if (!isPolling) return; try { const status = await apiClient.getJobStatus(jobId); setJobInfo(status); if (status.status === 'completed') { let imageUrls: string[] = []; // Handle both old format (result.images) and new format (outputs) if (status.outputs && status.outputs.length > 0) { // New format: convert output URLs to authenticated image URLs with cache-busting imageUrls = status.outputs.map((output: any) => { const filename = output.filename; return apiClient.getImageUrl(jobId, filename); }); } else if (status.result?.images && status.result.images.length > 0) { // Old format: convert image URLs to authenticated URLs imageUrls = status.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]); setLoading(false); isPolling = false; } else if (status.status === 'failed') { setError(status.error || 'Upscaling failed'); setLoading(false); isPolling = false; } else if (status.status === 'cancelled') { setError('Upscaling was cancelled'); setLoading(false); isPolling = false; } else if (attempts < maxAttempts) { attempts++; setTimeout(poll, 2000); } else { setError('Job polling timeout'); setLoading(false); isPolling = false; } } catch (err) { if (isPolling) { setError(err instanceof Error ? err.message : 'Failed to check job status'); setLoading(false); isPolling = false; } } }; poll(); // Return cleanup function return () => { isPolling = false; }; }; 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; } // Note: You may need to adjust the API endpoint based on your backend implementation const job = await apiClient.generateImage({ prompt: `upscale ${formData.upscale_factor}x`, model: selectedUpscalerModel, image: uploadedImage, // Add upscale-specific parameters here based on your API } as any); 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 (err) { setError(err instanceof Error ? err.message : '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
)}
setFormData(prev => ({ ...prev, model: value }))} onSetUserOverride={(value) => {/* User override logic */}} onClearOverride={clearUpscalerUserOverride} availableModels={upscalerModels} isAutoSelected={isUpscalerAutoSelected} isUserOverride={isUpscalerUserOverride} placeholder="Select an upscaler model" />
{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 ; }