"use client"; import { useState, 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 { PromptTextarea } from "@/components/forms"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from "@/components/ui/card"; import { InpaintingCanvas } from "@/components/features/image-generation"; import { apiClient, type JobInfo, type JobDetailsResponse } from "@/lib/api"; import { Loader2, X, Download } from "lucide-react"; import { downloadAuthenticatedImage } from "@/lib/utils"; import { useLocalStorage, useGeneratedImages } from "@/lib/storage"; import { useModelTypeSelection, } from "@/contexts/model-selection-context"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; // import { AutoSelectionStatus } from '@/components/features/models'; type InpaintingFormData = { prompt: string; negative_prompt: string; steps: number; cfg_scale: number; seed: string; sampling_method: string; strength: number; width?: number; height?: number; }; const defaultFormData: InpaintingFormData = { prompt: "", negative_prompt: "", steps: 20, cfg_scale: 7.5, seed: "", sampling_method: "euler_a", strength: 0.75, width: 512, height: 512, }; function InpaintingForm() { const { availableModels: vaeModels, selectedModel: selectedVae, isAutoSelected: isVaeAutoSelected, setSelectedModel: setSelectedVae, setUserOverride: setVaeUserOverride, clearUserOverride: clearVaeUserOverride, } = useModelTypeSelection("vae"); const { availableModels: taesdModels, selectedModel: selectedTaesd, setSelectedModel: setSelectedTaesd, } = useModelTypeSelection("taesd"); const [formData, setFormData] = useLocalStorage( "inpainting-form-data", defaultFormData, { excludeLargeData: true, maxSize: 512 * 1024 }, ); // Separate state for image data (not stored in localStorage) const [sourceImage, setSourceImage] = useState(""); const [maskImage, setMaskImage] = useState(""); const [loading, setLoading] = useState(false); const [jobInfo, setJobInfo] = useState(null); const { images: storedImages, addImages, getLatestImages } = useGeneratedImages('inpainting'); const [generatedImages, setGeneratedImages] = useState(() => getLatestImages()); const [loraModels, setLoraModels] = useState([]); const [embeddings, setEmbeddings] = useState([]); const [error, setError] = useState(null); const [pollCleanup, setPollCleanup] = useState<(() => void) | null>(null); // Cleanup polling on unmount useEffect(() => { return () => { if (pollCleanup) { pollCleanup(); } }; }, [pollCleanup]); useEffect(() => { const loadModels = async () => { try { const [loras, embeds] = await Promise.all([ apiClient.getModels("lora"), apiClient.getModels("embedding"), ]); setLoraModels(loras.models.map((m) => m.name)); setEmbeddings(embeds.models.map((m) => m.name)); } catch (err) { console.error("Failed to load models:", err); } }; loadModels(); }, []); const handleInputChange = ( e: React.ChangeEvent< HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement >, ) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: name === "prompt" || name === "negative_prompt" || name === "seed" || name === "sampling_method" ? value : Number(value), })); }; const handleSourceImageChange = (image: string) => { setSourceImage(image); setError(null); }; const handleMaskImageChange = (image: string) => { setMaskImage(image); setError(null); }; const pollJobStatus = async (jobId: string) => { const maxAttempts = 300; // 5 minutes with 2 second interval 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); } } return imageUrl; // Already a full URL }); } // 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 || "Generation failed"); setLoading(false); isPolling = false; } else if (status.job.status === "cancelled") { setError("Generation 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 (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; if (timeoutId) { clearTimeout(timeoutId); } }; }; const handleGenerate = async (e: React.FormEvent) => { e.preventDefault(); if (!sourceImage) { setError("Please upload a source image first"); return; } if (!maskImage) { setError("Please create a mask first"); return; } setLoading(true); setError(null); setGeneratedImages([]); setJobInfo(null); try { const requestData = { ...formData, source_image: sourceImage, mask_image: maskImage, vae: selectedVae || undefined, taesd: selectedTaesd || undefined, }; const job = await apiClient.inpainting(requestData); 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 generate image"); setLoading(false); } }; const handleCancel = async () => { const jobId = jobInfo?.request_id || jobInfo?.id; if (jobId) { try { await apiClient.cancelJob(jobId); setLoading(false); setError("Generation cancelled"); // Cleanup polling if (pollCleanup) { pollCleanup(); setPollCleanup(null); } } catch (err) { console.error("Failed to cancel job:", err); } } }; return (
{/* Left Panel - Form Parameters */}
setFormData({ ...formData, prompt: value }) } placeholder="Describe what to generate in the masked areas..." rows={3} loras={loraModels} embeddings={embeddings} />

Tip: Use {""} for LoRAs and embedding names directly

setFormData({ ...formData, negative_prompt: value }) } placeholder="What to avoid in the generated areas..." rows={2} loras={loraModels} embeddings={embeddings} />
{/* Additional Models Section */} Additional Models Select additional models for generation
{jobInfo && ( )}
{error && (
{error}
)}
{/* Right Panel - Source Image and Results */}

Generated Images

{generatedImages.length === 0 ? (

{loading ? "Generating..." : "Generated images will appear here"}

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