"use client"; import { useState, useEffect, useRef } 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 } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { apiClient, type GenerationRequest, type JobInfo, type JobDetailsResponse, } from "@/lib/api"; import { Loader2, Download, X, Trash2, RotateCcw, Power } from "lucide-react"; import { downloadImage, downloadAuthenticatedImage } from "@/lib/utils"; import { useLocalStorage, useGeneratedImages } from "@/lib/storage"; import { useModelTypeSelection } from "@/contexts/model-selection-context"; const defaultFormData: GenerationRequest = { prompt: "", negative_prompt: "", width: 512, height: 512, steps: 20, cfg_scale: 7.5, seed: "", sampling_method: "euler_a", scheduler: "default", batch_count: 1, }; function Text2ImgForm() { const { availableModels: vaeModels, selectedModel: selectedVae, setSelectedModel: setSelectedVae, } = useModelTypeSelection("vae"); const { availableModels: taesdModels, selectedModel: selectedTaesd, setSelectedModel: setSelectedTaesd, } = useModelTypeSelection("taesd"); const [formData, setFormData] = useLocalStorage( "text2img-form-data", defaultFormData, { excludeLargeData: true, maxSize: 512 * 1024 }, // 512KB limit ); const [loading, setLoading] = useState(false); const [jobInfo, setJobInfo] = useState(null); const { images: storedImages, addImages, getLatestImages } = useGeneratedImages('text2img'); const [generatedImages, setGeneratedImages] = useState(() => storedImages.map(img => img.url)); const [samplers, setSamplers] = useState< Array<{ name: string; description: string }> >([]); const [schedulers, setSchedulers] = useState< Array<{ name: string; description: string }> >([]); const [loraModels, setLoraModels] = useState([]); const [embeddings, setEmbeddings] = useState([]); const [error, setError] = useState(null); const pollCleanupRef = useRef<(() => void) | null>(null); // Cleanup polling on unmount useEffect(() => { return () => { if (pollCleanupRef.current) { pollCleanupRef.current(); pollCleanupRef.current = null; } }; }, []); useEffect(() => { const loadOptions = async () => { try { const [samplersData, schedulersData, loras, embeds] = await Promise.all( [ apiClient.getSamplers(), apiClient.getSchedulers(), apiClient.getModels("lora"), apiClient.getModels("embedding"), ], ); setSamplers(samplersData); setSchedulers(schedulersData); setLoraModels(loras.models.map((m) => m.name)); setEmbeddings(embeds.models.map((m) => m.name)); } catch (err) { console.error("Failed to load options:", err); } }; loadOptions(); }, []); 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" || name === "scheduler" ? value : Number(value), })); }; 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); console.log(`[DEBUG] Job ${jobId} status: ${status.job.status}, progress: ${status.job.progress}, outputs:`, status.job.outputs); 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) { console.log(`[DEBUG] Processing ${status.job.outputs.length} outputs`); // New format: convert output URLs to authenticated image URLs with cache-busting imageUrls = status.job.outputs.map((output: { filename: string }) => { const filename = output.filename; const imageUrl = apiClient.getImageUrl(jobId, filename); console.log(`[DEBUG] Generated URL for ${filename}: ${imageUrl}`); return imageUrl; }); } else if ( status.job.result?.images && status.job.result.images.length > 0 ) { console.log(`[DEBUG] Using old format with ${status.job.result.images.length} images`); // Old format: convert image URLs to authenticated URLs imageUrls = status.job.result.images.map((imageUrl: string) => { // Extract filename from URL if it's 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); }); } else { console.log(`[DEBUG] No outputs or images found in job response`); } console.log(`[DEBUG] Final image URLs:`, imageUrls); // Create a new array to trigger React re-render setGeneratedImages([...imageUrls]); addImages(imageUrls, jobId); setLoading(false); isPolling = false; } else if (status.job.status === "failed") { console.log(`[DEBUG] Job failed with error: ${status.job.error}`); setError(status.job.error || "Generation failed"); setLoading(false); isPolling = false; } else if (status.job.status === "cancelled") { console.log(`[DEBUG] Job was cancelled`); setError("Generation was cancelled"); setLoading(false); isPolling = false; } else if (attempts < maxAttempts) { attempts++; timeoutId = setTimeout(poll, 2000); } else { console.log(`[DEBUG] Job polling timeout after ${attempts} attempts`); setError("Job polling timeout"); setLoading(false); isPolling = false; } } catch (err) { console.log(`[DEBUG] Error polling job status:`, 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(); setLoading(true); setError(null); setGeneratedImages([]); setJobInfo(null); try { const requestData = { ...formData, vae: selectedVae || undefined, taesd: selectedTaesd || undefined, }; const job = await apiClient.text2img(requestData); setJobInfo(job); const jobId = job.request_id || job.id; if (jobId) { pollJobStatus(jobId).then((cleanup) => { pollCleanupRef.current = 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 (pollCleanupRef.current) { pollCleanupRef.current(); pollCleanupRef.current = null; } } catch (err) { console.error("Failed to cancel job:", err); } } }; const handleClearPrompts = () => { setFormData({ ...formData, prompt: "", negative_prompt: "" }); }; const handleResetToDefaults = () => { setFormData(defaultFormData); }; const handleServerRestart = async () => { if ( !confirm( "Are you sure you want to restart the server? This will cancel all running jobs.", ) ) { return; } try { setLoading(true); await apiClient.restartServer(); setError("Server restart initiated. Please wait..."); setTimeout(() => { window.location.reload(); }, 3000); } catch (err) { setError(err instanceof Error ? err.message : "Failed to restart server"); setLoading(false); } }; return (
{/* Left Panel - Form */}
setFormData({ ...formData, prompt: value }) } placeholder="a beautiful landscape with mountains and a lake, sunset, highly detailed..." rows={4} loras={loraModels} embeddings={embeddings} />

Tip: Use <lora:name:weight> for LoRAs (e.g., <lora:myLora:0.8>) and embedding names directly

setFormData({ ...formData, negative_prompt: value }) } placeholder="blurry, low quality, distorted..." rows={2} loras={loraModels} embeddings={embeddings} />
{/* Utility Buttons */}
{loading && ( )}
{error && (
{error}
)}
{/* Right Panel - Generated Images */}

Generated Images

{generatedImages.length === 0 ? (

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

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