import { useEffect, useMemo, useRef, useState } from "react"; type JobStatus = "queued" | "running" | "done" | "error" | "cancelled"; type ModelInfo = { id: string; name: string; available: boolean; description: string; }; type JobInfo = { job_id: string; status: JobStatus; progress: number; message: string; image_paths: string[]; output_dir: string | null; error: string | null; }; type HistoryItem = { prompt: string; negative_prompt: string; timestamp: string; }; type Preset = { name: string; prompt: string; negative_prompt: string; model: string; size: string; count: number; steps: number; guidance: number; image_type: string; style_preset: string; style_strength: number; updated_at: string; }; type DashboardStats = { queued: number; running: number; done: number; error: number; cancelled: number; total: number; last_24h: number; }; type AdminSettings = { content_profile: string; rate_limit_per_minute: number; output_retention_days: number; adult_enabled: boolean; }; const API_BASE = "http://127.0.0.1:8008"; function toImageUrl(path: string): string { return `${API_BASE}/image?path=${encodeURIComponent(path)}`; } type ResolvedImageProps = { path: string; alt: string; }; function ResolvedImage({ path, alt }: ResolvedImageProps) { const [src, setSrc] = useState(() => toImageUrl(path)); useEffect(() => { let active = true; setSrc(toImageUrl(path)); void window.imageForge.readImageDataUrl(path).then((dataUrl) => { if (active && dataUrl) { setSrc(dataUrl); } }); return () => { active = false; }; }, [path]); return {alt}; } async function api(path: string, init?: RequestInit): Promise { const apiKey = localStorage.getItem("imageforge_api_key") || ""; const response = await fetch(`${API_BASE}${path}`, { ...init, headers: { "Content-Type": "application/json", "X-ImageForge-Api-Key": apiKey, ...(init?.headers || {}), }, }); if (!response.ok) { const text = await response.text(); throw new Error(text || `HTTP ${response.status}`); } return (await response.json()) as T; } function App() { const [activeTab, setActiveTab] = useState<"studio" | "dashboard" | "presets" | "settings">("studio"); const [apiKeyInput, setApiKeyInput] = useState(localStorage.getItem("imageforge_api_key") || ""); const [adminSettings, setAdminSettings] = useState(null); const lastJobLogSignatureRef = useRef(""); const [models, setModels] = useState([]); const [history, setHistory] = useState([]); const [presets, setPresets] = useState([]); const [stats, setStats] = useState(null); const [prompt, setPrompt] = useState(""); const [batchPrompts, setBatchPrompts] = useState(""); const [negativePrompt, setNegativePrompt] = useState(""); const [model, setModel] = useState("dummy"); const [modelVariant, setModelVariant] = useState(""); const [size, setSize] = useState("512x512"); const [count, setCount] = useState(1); const [randomSeed, setRandomSeed] = useState(true); const [seed, setSeed] = useState(42); const [steps, setSteps] = useState(18); const [guidance, setGuidance] = useState(6.5); const [imageType, setImageType] = useState("general"); const [stylePreset, setStylePreset] = useState("auto"); const [styleStrength, setStyleStrength] = useState(60); const [initImagePath, setInitImagePath] = useState(null); const [img2imgStrength, setImg2imgStrength] = useState(0.45); const [currentJobId, setCurrentJobId] = useState(null); const [jobs, setJobs] = useState([]); const [jobInfo, setJobInfo] = useState(null); const [logs, setLogs] = useState([]); const [selectedImage, setSelectedImage] = useState(null); const [compareImages, setCompareImages] = useState([]); async function refreshModels() { const response = await api<{ value: ModelInfo[]; Count: number }>("/models"); const data = Array.isArray(response) ? response : response.value || []; setModels(data); const preferred = ["a1111", "localai", "diffusion", "dummy"]; const available = preferred .map((id) => data.find((m) => m.id === id && m.available)) .find(Boolean) || data.find((m) => m.available) || data[0]; if (available) { setModel((prev) => { const current = data.find((m) => m.id === prev); if (!current || !current.available || prev === "dummy") { return available.id; } return prev; }); } } async function refreshHistory() { const data = await api("/history"); setHistory(data); } async function refreshJobs() { const data = await api("/jobs"); setJobs(data); } async function refreshPresets() { const data = await api("/presets"); setPresets(data); } async function refreshStats() { const data = await api("/dashboard/stats"); setStats(data); } async function refreshAdminSettings() { try { const data = await api("/admin/settings"); setAdminSettings(data); } catch { setAdminSettings(null); } } useEffect(() => { void Promise.all([refreshModels(), refreshHistory(), refreshJobs(), refreshPresets(), refreshStats(), refreshAdminSettings()]).catch((err: Error) => { void window.imageForge.showError("Startup Error", err.message); }); }, []); useEffect(() => { const timer = window.setInterval(() => { void refreshJobs().catch(() => {}); void refreshStats().catch(() => {}); }, 2000); return () => window.clearInterval(timer); }, []); useEffect(() => { if (!currentJobId) { return; } const timer = window.setInterval(() => { void api(`/jobs/${currentJobId}`) .then((info) => { setJobInfo(info); const signature = `${info.status}|${info.progress}|${info.message}`; if (lastJobLogSignatureRef.current !== signature) { lastJobLogSignatureRef.current = signature; setLogs((prev) => { const next = [...prev, `${new Date().toLocaleTimeString()} | ${info.message}`]; return next.slice(-200); }); } if (info.status === "done") { setSelectedImage(info.image_paths[0] || null); setCurrentJobId(null); void refreshHistory().catch(() => {}); void refreshJobs().catch(() => {}); } if (info.status === "error" || info.status === "cancelled") { setCurrentJobId(null); if (info.error) { void window.imageForge.showError("Generation Error", info.error); } } }) .catch((err: Error) => { setLogs((prev) => [...prev, `${new Date().toLocaleTimeString()} | Poll failed: ${err.message}`]); }); }, 1000); return () => window.clearInterval(timer); }, [currentJobId]); const isGenerating = useMemo( () => Boolean(currentJobId) || jobInfo?.status === "queued" || jobInfo?.status === "running", [currentJobId, jobInfo?.status] ); function buildPayload(basePrompt: string) { return { prompt: basePrompt, negative_prompt: negativePrompt, model, model_variant: modelVariant.trim() || null, size, count, random_seed: randomSeed, seed: randomSeed ? null : seed, steps, guidance, image_type: imageType, style_preset: stylePreset, style_strength: styleStrength, init_image_path: initImagePath, img2img_strength: img2imgStrength, }; } async function submitOne(basePrompt: string) { const response = await api<{ job_id: string }>("/generate", { method: "POST", body: JSON.stringify(buildPayload(basePrompt)), }); return response.job_id; } async function handleGenerate() { if (!prompt.trim()) { await window.imageForge.showError("Validation", "Prompt darf nicht leer sein."); return; } try { setLogs((prev) => [...prev, `${new Date().toLocaleTimeString()} | Submit generation`]); setJobInfo(null); lastJobLogSignatureRef.current = ""; setSelectedImage(null); const jobId = await submitOne(prompt.trim()); setCurrentJobId(jobId); await refreshJobs(); } catch (err) { const message = err instanceof Error ? err.message : String(err); await window.imageForge.showError("Generate Failed", message); } } async function handleBatchGenerate() { const lines = batchPrompts .split("\n") .map((line) => line.trim()) .filter(Boolean); if (lines.length === 0) { await window.imageForge.showError("Validation", "Batch-Prompts sind leer."); return; } try { for (const line of lines) { await submitOne(line); } setLogs((prev) => [...prev, `${new Date().toLocaleTimeString()} | Batch queued (${lines.length})`]); await refreshJobs(); await refreshStats(); } catch (err) { const message = err instanceof Error ? err.message : String(err); await window.imageForge.showError("Batch Failed", message); } } async function handleCancel() { if (!currentJobId) { return; } await api(`/jobs/${currentJobId}/cancel`, { method: "POST" }); setLogs((prev) => [...prev, `${new Date().toLocaleTimeString()} | Cancel requested`]); await refreshJobs(); } async function handleCancelById(jobId: string) { await api(`/jobs/${jobId}/cancel`, { method: "POST" }); await refreshJobs(); } async function handleRetry(jobId: string) { const out = await api<{ new_job_id: string }>(`/jobs/${jobId}/retry`, { method: "POST" }); setCurrentJobId(out.new_job_id); await refreshJobs(); } async function handleSaveImage() { if (!selectedImage) { return; } const ok = await window.imageForge.saveImage(selectedImage, "imageforge_output.png"); if (ok) { setLogs((prev) => [...prev, `${new Date().toLocaleTimeString()} | Image saved`]); } } async function handleExportImage(format: "png" | "jpg" | "webp") { if (!selectedImage) { return; } const out = await api<{ output_path: string }>("/export", { method: "POST", body: JSON.stringify({ source_path: selectedImage, format, quality: 92 }), }); await window.imageForge.saveImage(out.output_path, `imageforge_export.${format}`); } async function handleOpenFolder() { if (jobInfo?.output_dir) { await window.imageForge.openFolder(jobInfo.output_dir); } } async function handlePickInitImage() { const path = await window.imageForge.pickImage(); if (path) { setInitImagePath(path); } } function toggleCompare(path: string) { setCompareImages((prev) => (prev.includes(path) ? prev.filter((p) => p !== path) : [...prev.slice(-3), path])); } async function savePreset() { const name = prompt.slice(0, 40) || `preset-${Date.now()}`; const payload = { name, prompt, negative_prompt: negativePrompt, model, size, count, steps, guidance, image_type: imageType, style_preset: stylePreset, style_strength: styleStrength, }; await api("/presets", { method: "POST", body: JSON.stringify(payload) }); await refreshPresets(); } function applyHyperrealPortraitPreset() { setImageType("portrait"); setStylePreset("photorealistic"); setStyleStrength(90); setSteps(48); setGuidance(7.0); setModel((prev) => prev || "localai"); setNegativePrompt( "low quality, blurry, bad anatomy, extra fingers, waxy skin, overprocessed face, watermark" ); } function applyHyperrealProductPreset() { setImageType("product"); setStylePreset("photorealistic"); setStyleStrength(85); setSteps(42); setGuidance(6.5); setModel((prev) => prev || "localai"); setNegativePrompt( "low quality, blurry, noisy, distorted geometry, warped label, watermark, text clutter" ); } async function applyPreset(p: Preset) { setPrompt(p.prompt); setNegativePrompt(p.negative_prompt); setModel(p.model); setSize(p.size); setCount(p.count); setSteps(p.steps); setGuidance(p.guidance); setImageType(p.image_type); setStylePreset(p.style_preset); setStyleStrength(p.style_strength); setActiveTab("studio"); } async function deletePreset(name: string) { await api(`/presets/${encodeURIComponent(name)}`, { method: "DELETE" }); await refreshPresets(); } async function saveAdminSettings() { if (!adminSettings) { return; } const out = await api("/admin/settings", { method: "PUT", body: JSON.stringify(adminSettings), }); setAdminSettings(out); setLogs((prev) => [...prev, `${new Date().toLocaleTimeString()} | Admin settings updated`]); } return (
{activeTab === "studio" && ( <>

ImageForge Studio