Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"/> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"/> | |
| <title>imgGenPro — Free AI Image Generator</title> | |
| <!-- TailwindCSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Font Awesome --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"/> | |
| </head> | |
| <style> | |
| .blur-nsfw { | |
| filter: blur(12px); | |
| position: relative; | |
| } | |
| .blur-nsfw::after { | |
| content: "⚠️ NSFW — Click to reveal"; | |
| position: absolute; | |
| top:50%; left:50%; | |
| transform: translate(-50%, -50%); | |
| color:white; | |
| font-weight:bold; | |
| } | |
| </style> | |
| <body class="min-h-screen bg-black text-white selection:bg-fuchsia-400/30"> | |
| <!-- Topbar --> | |
| <header class="sticky top-0 z-50 backdrop-blur bg-black/40 border-b border-white/10"> | |
| <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-9 h-9 rounded-xl bg-gradient-to-br from-fuchsia-500 via-purple-500 to-sky-400 glow"></div> | |
| <span class="font-semibold tracking-wide">imgGenPro</span> | |
| </div> | |
| <nav class="hidden md:flex items-center gap-2"> | |
| <a class="px-3 py-2 rounded-lg hover:bg-white/5 text-sm" href="https://aniwall.free.nf" target="_blank" rel="noopener"> | |
| <i class="fa-solid fa-image mr-2"></i>Wallpapers | |
| </a> | |
| <button id="openSettings" class="px-3 py-2 rounded-lg hover:bg-white/5 text-sm"> | |
| <i class="fa-solid fa-gear mr-2"></i>Settings | |
| </button> | |
| <a class="px-3 py-2 rounded-lg hover:bg-white/5 text-sm" href="#" id="openHistory"> | |
| <i class="fa-solid fa-clock-rotate-left mr-2"></i>History | |
| </a> | |
| <a class="px-3 py-2 rounded-lg hover:bg-white/5 text-sm" href="#" id="clearGallery"> | |
| <i class="fa-solid fa-trash mr-2"></i>Clear | |
| </a> | |
| </nav> | |
| <button id="openSettingsSm" class="md:hidden px-3 py-2 rounded-lg hover:bg-white/5"> | |
| <i class="fa-solid fa-sliders"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main --> | |
| <main class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6"> | |
| <!-- Prompt card --> | |
| <section class="rounded-2xl border border-white/10 bg-white/5 p-4 md:p-6"> | |
| <div class="flex flex-col md:flex-row gap-3"> | |
| <div class="flex-1"> | |
| <label for="prompt" class="sr-only">Prompt</label> | |
| <div class="relative"> | |
| <textarea id="prompt" rows="2" placeholder="Describe what you want to create (e.g., 'a cyberpunk city alley with neon reflections, rainy night')" | |
| class="w-full resize-y rounded-xl bg-black/50 border border-white/10 focus:border-fuchsia-400/60 outline-none p-4 pr-12 placeholder-white/40"></textarea> | |
| <button id="enhanceToggle" class="absolute right-2 top-2 text-xs px-2 py-1 rounded-lg border border-white/10 hover:bg-white/5"> | |
| <i class="fa-solid fa-wand-magic-sparkles mr-1"></i>Enhance | |
| </button> | |
| </div> | |
| <!-- Quick styles --> | |
| <div class="mt-3 flex flex-wrap gap-2"> | |
| <button class="style-btn px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs hover:bg-white/10" data-style="">No style</button> | |
| <button class="style-btn px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs hover:bg-white/10" data-style="cinematic shot, moody tone, film still">Cinema</button> | |
| <button class="style-btn px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs hover:bg-white/10" data-style="photo, candid, natural look, unposed">Realistic</button> | |
| <button class="style-btn px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs hover:bg-white/10" data-style="studio portrait, soft tone, subject centered">Portrait</button> | |
| <button class="style-btn px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs hover:bg-white/10" data-style="anime style, vibrant cel shading">Anime</button> | |
| <button class="style-btn px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs hover:bg-white/10" data-style="fantasy concept art, painterly">Fantasy</button> | |
| </div> | |
| </div> | |
| <div class="w-full md:w-56 flex md:flex-col gap-2"> | |
| <button id="generateBtn" | |
| class="flex-1 md:h-[60px] p-2 rounded-xl bg-white text-black font-semibold hover:opacity-90 active:opacity-100 transition mb-2"> | |
| <span class="inline-flex items-center gap-2"><i class="fa-solid fa-bolt"></i> Generate</span> | |
| </button> | |
| <button id="randomPrompt" class="flex-1 md:h-[56px] rounded-xl bg-white/5 border border-white/10 hover:bg-white/10"> | |
| <i class="fa-solid fa-shuffle mr-2"></i>Random | |
| </button> | |
| <button id="downloadAll" class="hidden md:block h-[56px] rounded-xl bg-white/5 border border-white/10 hover:bg-white/10"> | |
| <i class="fa-solid fa-download mr-2"></i>Download All | |
| </button> | |
| </div> | |
| </div> | |
| <p class="mt-3 text-xs text-white/60"> | |
| Tip: NSFW may be filtered by models. | |
| </p> | |
| </section> | |
| <!-- Gallery --> | |
| <section class="mt-6"> | |
| <div id="gallery" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"></div> | |
| <div id="emptyState" class="text-center py-16 text-white/50"> | |
| <i class="fa-regular fa-images text-4xl mb-3 block"></i> | |
| <p>No images yet — start by generating something ✨</p> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- Settings Drawer --> | |
| <div id="settingsDrawer" class="fixed inset-0 hidden"> | |
| <div class="absolute inset-0 backdrop"></div> | |
| <aside class="ml-auto h-full w-full sm:w-[420px] bg-black/90 border-l border-white/10 p-5 overflow-y-auto"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h2 class="text-lg font-semibold"><i class="fa-solid fa-sliders mr-2"></i>Settings</h2> | |
| <button id="closeSettings" class="p-2 rounded-lg hover:bg-white/5"> | |
| <i class="fa-solid fa-xmark"></i> | |
| </button> | |
| </div> | |
| <div class="space-y-4"> | |
| <label class="block text-sm"> | |
| <span class="text-white/70">Model</span> | |
| <select id="model" class="mt-1 w-full rounded-lg bg-black/50 border border-white/10 p-2"> | |
| <!-- Popular, fast models --> | |
| <option value="black-forest-labs/FLUX.1-schnell">FLUX.1-schnell (fast)</option> | |
| <option value="stabilityai/stable-diffusion-xl-base-1.0">SDXL Base 1.0</option> | |
| <option value="runwayml/stable-diffusion-v1-5">SD 1.5</option> | |
| </select> | |
| </label> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <label class="block text-sm"> | |
| <span class="text-white/70">Width</span> | |
| <input id="width" type="number" min="64" max="2048" value="1024" | |
| class="mt-1 w-full rounded-lg bg-black/50 border border-white/10 p-2"> | |
| </label> | |
| <label class="block text-sm"> | |
| <span class="text-white/70">Height</span> | |
| <input id="height" type="number" min="64" max="2048" value="1024" | |
| class="mt-1 w-full rounded-lg bg-black/50 border border-white/10 p-2"> | |
| </label> | |
| </div> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <label class="block text-sm"> | |
| <span class="text-white/70">Seed</span> | |
| <input id="seed" type="number" value="42" | |
| class="mt-1 w-full rounded-lg bg-black/50 border border-white/10 p-2"> | |
| </label> | |
| <label class="flex items-center gap-2 text-sm mt-7"> | |
| <input id="randomSeed" type="checkbox" class="size-4 accent-fuchsia-500" checked> | |
| Random seed | |
| </label> | |
| </div> | |
| <label class="block text-sm"> | |
| <span class="text-white/70">Negative Prompt (optional)</span> | |
| <input id="negPrompt" type="text" placeholder="blurry, low quality, watermark" | |
| class="mt-1 w-full rounded-lg bg-black/50 border border-white/10 p-2"> | |
| </label> | |
| <div class="flex items-center justify-between"> | |
| <label class="flex items-center gap-2 text-sm"> | |
| <input id="enhance" type="checkbox" class="size-4 accent-fuchsia-500"> | |
| Enhance prompt (client-side) | |
| </label> | |
| <button id="saveSettings" class="px-3 py-2 rounded-lg border border-white/10 hover:bg-white/5"> | |
| <i class="fa-regular fa-floppy-disk mr-2"></i>Save | |
| </button> | |
| </div> | |
| </div> | |
| </aside> | |
| </div> | |
| <!-- Image Modal --> | |
| <div id="imageModal" class="fixed inset-0 hidden"> | |
| <div class="absolute inset-0 backdrop"></div> | |
| <div class="relative z-10 h-full w-full flex items-center justify-center p-4"> | |
| <div class="max-w-5xl w-full rounded-2xl bg-black/90 border border-white/10 p-4"> | |
| <div class="flex justify-between items-center mb-3"> | |
| <h3 class="font-semibold"><i class="fa-regular fa-image mr-2"></i>Preview</h3> | |
| <button id="closeModal" class="p-2 rounded-lg hover:bg-white/5"> | |
| <i class="fa-solid fa-xmark"></i> | |
| </button> | |
| </div> | |
| <img id="modalImg" src="" alt="Preview" class="w-full rounded-xl border border-white/10"> | |
| <div class="mt-3 flex gap-2"> | |
| <button id="downloadBtn" class="px-3 py-2 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10"> | |
| <i class="fa-solid fa-download mr-2"></i>Download | |
| </button> | |
| <button id="copyPromptBtn" class="px-3 py-2 rounded-lg bg-white/5 border border-white/10 hover:bg-white/10"> | |
| <i class="fa-solid fa-quote-left mr-2"></i>Copy Prompt | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Toast --> | |
| <div id="toast" class="fixed bottom-4 left-1/2 -translate-x-1/2 hidden"> | |
| <div class="rounded-xl bg-white/10 border border-white/10 px-4 py-2 text-sm"> | |
| <span id="toastMsg">Saved</span> | |
| </div> | |
| </div> | |
| <footer class="text-center text-white-600 pt-8 border-t border-gray-200"><p>ImgGenPro © 2025 - All rights reserved</p><p class="mt-2">Created by <a href="https://souravs.great-site.net"> SouRav </a></p></footer></div> | |
| <!-- JS --> | |
| <script> | |
| // ------------ State ------------ | |
| const el = { | |
| prompt: document.getElementById('prompt'), | |
| model: document.getElementById('model'), | |
| width: document.getElementById('width'), | |
| height: document.getElementById('height'), | |
| seed: document.getElementById('seed'), | |
| randomSeed: document.getElementById('randomSeed'), | |
| negPrompt: document.getElementById('negPrompt'), | |
| enhance: document.getElementById('enhance'), | |
| enhanceToggle: document.getElementById('enhanceToggle'), | |
| generateBtn: document.getElementById('generateBtn'), | |
| randomPrompt: document.getElementById('randomPrompt'), | |
| downloadAll: document.getElementById('downloadAll'), | |
| gallery: document.getElementById('gallery'), | |
| emptyState: document.getElementById('emptyState'), | |
| openSettings: document.getElementById('openSettings'), | |
| openSettingsSm: document.getElementById('openSettingsSm'), | |
| settingsDrawer: document.getElementById('settingsDrawer'), | |
| closeSettings: document.getElementById('closeSettings'), | |
| saveSettings: document.getElementById('saveSettings'), | |
| openHistory: document.getElementById('openHistory'), | |
| clearGallery: document.getElementById('clearGallery'), | |
| imageModal: document.getElementById('imageModal'), | |
| modalImg: document.getElementById('modalImg'), | |
| closeModal: document.getElementById('closeModal'), | |
| downloadBtn: document.getElementById('downloadBtn'), | |
| copyPromptBtn: document.getElementById('copyPromptBtn'), | |
| toast: document.getElementById('toast'), | |
| toastMsg: document.getElementById('toastMsg'), | |
| }; | |
| let currentStyle = ""; | |
| let history = JSON.parse(localStorage.getItem('img_history') || '[]'); | |
| // ------------ Helpers ------------ | |
| function showToast(msg){ | |
| el.toastMsg.textContent = msg; | |
| el.toast.classList.remove('hidden'); | |
| setTimeout(()=>el.toast.classList.add('hidden'), 1600); | |
| } | |
| function randInt(min,max){return Math.floor(Math.random()*(max-min+1))+min;} | |
| function saveSettings(){ | |
| const s = { | |
| model: el.model.value, | |
| width: +el.width.value || 1024, | |
| height: +el.height.value || 1024, | |
| seed: +el.seed.value || 42, | |
| randomSeed: el.randomSeed.checked, | |
| negPrompt: el.negPrompt.value || '', | |
| enhance: el.enhance.checked | |
| }; | |
| localStorage.setItem('img_settings', JSON.stringify(s)); | |
| showToast('Settings saved'); | |
| } | |
| function loadSettings(){ | |
| try{ | |
| const s = JSON.parse(localStorage.getItem('img_settings')||'{}'); | |
| if(s.model) el.model.value = s.model; | |
| if(s.width) el.width.value = s.width; | |
| if(s.height) el.height.value = s.height; | |
| if(s.seed !== undefined) el.seed.value = s.seed; | |
| if(s.randomSeed !== undefined) el.randomSeed.checked = s.randomSeed; | |
| if(s.negPrompt !== undefined) el.negPrompt.value = s.negPrompt; | |
| if(s.enhance !== undefined) el.enhance.checked = s.enhance; | |
| }catch(e){} | |
| } | |
| loadSettings(); | |
| function promptVariants(){ | |
| const arr = [ | |
| // Cozy / Realistic | |
| "a cozy reading nook by a window with rain outside", | |
| "a serene beach at sunset with bioluminescent waves", | |
| "a cozy coffee shop interior with warm lighting and people reading", | |
| "a retro-futuristic living room, 80s aesthetic meets sci-fi", | |
| "a quiet snowy village at night with glowing lanterns, cinematic", | |
| "a sunlit forest clearing with wildflowers and butterflies", | |
| "a rustic kitchen with fresh bread on the table, morning light", | |
| // Fantasy / Concept Art | |
| "a majestic dragon soaring above snow-capped mountains, fantasy concept", | |
| "a mystical castle on a cliff during a thunderstorm", | |
| "an ancient forest with glowing magical mushrooms, misty atmosphere", | |
| "a surreal dreamscape with floating islands and waterfalls", | |
| "a steampunk airship flying above the clouds, dramatic lighting", | |
| "a magical library with floating books and enchanted candles", | |
| "a crystal cave glowing with colorful lights, fantasy setting", | |
| // Cyberpunk / Sci-fi | |
| "a neon-lit ramen shop alley in Tokyo at night, reflections on wet ground", | |
| "a bustling cyberpunk city street, rain-soaked, neon reflections", | |
| "a futuristic laboratory with floating holograms and robots", | |
| "a sci-fi spaceship bridge overlooking a distant galaxy", | |
| "a robotic marketplace with android vendors and glowing signs", | |
| "a high-tech futuristic cityscape at sunset with flying cars", | |
| // Whimsical / Cute / Anime-style | |
| "a cute corgi astronaut floating in space", | |
| "a portrait of a wise owl wearing a wizard hat, digital painting", | |
| "an anime girl sitting under cherry blossoms, soft lighting", | |
| "a chibi character baking cupcakes in a magical kitchen", | |
| "a young mage casting a spell in a magical garden, anime style", | |
| "a playful fox spirit dancing in a moonlit forest, anime illustration", | |
| "an anime-style futuristic city with floating platforms and lights", | |
| "a cat café full of cats wearing tiny costumes, cute anime style", | |
| "a magical girl soaring through the skies with sparkling trails", | |
| "a dragon and a young adventurer sharing tea in a forest clearing" | |
| ]; | |
| return arr[randInt(0, arr.length-1)]; | |
| } | |
| function buildPrompt(){ | |
| let p = el.prompt.value.trim(); | |
| if(currentStyle) p += `, ${currentStyle}`; | |
| const neg = el.negPrompt.value.trim(); | |
| return { prompt: p, negative_prompt: neg }; | |
| } | |
| // Simple NSFW keyword detection | |
| const nsfwKeywords = ["bikini","nude","sex","erotic","porn"]; | |
| function isNSFW(prompt){ | |
| const lower = prompt.toLowerCase(); | |
| return nsfwKeywords.some(k => lower.includes(k)); | |
| } | |
| // ------------ Cards ------------ | |
| function addCardSkeleton(){ | |
| el.emptyState.classList.add('hidden'); | |
| const card = document.createElement('div'); | |
| card.className = "group relative rounded-2xl border border-white/10 bg-white/5 overflow-hidden"; | |
| card.innerHTML = ` | |
| <div class="aspect-[4/3] skeleton"></div> | |
| <div class="absolute inset-0 flex items-center justify-center"> | |
| <div class="w-14 h-14 rounded-full skeleton"></div> | |
| </div> | |
| <div class="p-3 text-xs text-white/50">Generating...</div> | |
| `; | |
| el.gallery.prepend(card); | |
| return card; | |
| } | |
| function addCard(imgUrl, meta){ | |
| el.emptyState.classList.add('hidden'); | |
| const card = document.createElement('div'); | |
| card.className = "group relative rounded-2xl border border-white/10 bg-white/5 overflow-hidden hover:border-fuchsia-400/40 transition"; | |
| card.innerHTML = ` | |
| <img src="${imgUrl}" alt="AI image" class="w-full h-auto block"> | |
| <div class="absolute top-2 right-2 flex gap-1 opacity-0 group-hover:opacity-100 transition"> | |
| <button class="px-2 py-1 rounded-lg bg-black/60 hover:bg-black/80 border border-white/10 text-xs" data-act="download"><i class="fa-solid fa-download"></i></button> | |
| <button class="px-2 py-1 rounded-lg bg-black/60 hover:bg-black/80 border border-white/10 text-xs" data-act="open"><i class="fa-regular fa-image"></i></button> | |
| <button class="px-2 py-1 rounded-lg bg-black/60 hover:bg-black/80 border border-white/10 text-xs" data-act="copy"><i class="fa-solid fa-quote-left"></i></button> | |
| </div> | |
| <div class="p-3 text-[11px] text-white/60 line-clamp-2">${meta.prompt}</div> | |
| `; | |
| const img = card.querySelector('img'); | |
| if(isNSFW(meta.prompt)){ | |
| img.classList.add("blur-nsfw"); | |
| img.title = "⚠️ NSFW — Click to reveal"; | |
| img.addEventListener('click', ()=> img.classList.remove("blur-nsfw")); | |
| } | |
| card.addEventListener('click', e=>{ | |
| const btn = e.target.closest('button'); | |
| if(!btn) return; | |
| const act = btn.dataset.act; | |
| if(act==='download'){ | |
| const a = document.createElement('a'); a.href = imgUrl; a.download = 'imggenpro.png'; a.click(); | |
| }else if(act==='open'){ | |
| openModal(imgUrl, meta.prompt); | |
| }else if(act==='copy'){ | |
| navigator.clipboard.writeText(meta.prompt); | |
| showToast('Prompt copied'); | |
| } | |
| }); | |
| el.gallery.prepend(card); | |
| return card; | |
| } | |
| // ------------ Modal ------------ | |
| function openModal(src, prompt){ | |
| el.modalImg.src = src; | |
| el.copyPromptBtn.dataset.prompt = prompt; | |
| el.imageModal.classList.remove('hidden'); | |
| } | |
| function closeModal(){ el.imageModal.classList.add('hidden'); } | |
| // ------------ Events ------------ | |
| document.querySelectorAll('.style-btn').forEach(b=>{ | |
| b.addEventListener('click', ()=>{ | |
| document.querySelectorAll('.style-btn').forEach(x=>x.classList.remove('ring-2','ring-fuchsia-400/60')); | |
| b.classList.add('ring-2','ring-fuchsia-400/60'); | |
| currentStyle = b.dataset.style || ""; | |
| }); | |
| }); | |
| el.enhanceToggle.addEventListener('click', ()=>{ | |
| el.enhance.checked = !el.enhance.checked; | |
| el.enhanceToggle.classList.toggle('bg-white/10', el.enhance.checked); | |
| }); | |
| el.openSettings?.addEventListener('click', ()=> el.settingsDrawer.classList.remove('hidden')); | |
| el.openSettingsSm?.addEventListener('click', ()=> el.settingsDrawer.classList.remove('hidden')); | |
| el.closeSettings.addEventListener('click', ()=> el.settingsDrawer.classList.add('hidden')); | |
| el.saveSettings.addEventListener('click', ()=>{ saveSettings(); el.settingsDrawer.classList.add('hidden'); }); | |
| el.openHistory?.addEventListener('click', ()=>{ | |
| if(!history.length){ showToast('No history yet'); return; } | |
| el.gallery.innerHTML = ''; | |
| el.emptyState.classList.add('hidden'); | |
| history.forEach(h=> addCard(h.url, {prompt: h.prompt})); | |
| }); | |
| el.clearGallery?.addEventListener('click', ()=>{ | |
| el.gallery.innerHTML = ''; | |
| el.emptyState.classList.remove('hidden'); | |
| }); | |
| el.randomPrompt.addEventListener('click', ()=>{ el.prompt.value = promptVariants(); }); | |
| el.downloadAll.addEventListener('click', ()=>{ | |
| const imgs = el.gallery.querySelectorAll('img'); | |
| if(!imgs.length){ showToast('Nothing to download'); return; } | |
| imgs.forEach((img,i)=>{ | |
| const a = document.createElement('a'); | |
| a.href = img.src; a.download = `imggenpro_${i+1}.png`; a.click(); | |
| }); | |
| }); | |
| el.closeModal.addEventListener('click', closeModal); | |
| el.downloadBtn.addEventListener('click', ()=>{ | |
| const a = document.createElement('a'); a.href = el.modalImg.src; a.download='imggenpro.png'; a.click(); | |
| }); | |
| el.copyPromptBtn.addEventListener('click', ()=>{ | |
| const p = el.copyPromptBtn.dataset.prompt || ''; | |
| navigator.clipboard.writeText(p); | |
| showToast('Prompt copied'); | |
| }); | |
| // ------------ Prompt Enhance (fake client-side) ------------ | |
| async function enhancePrompt(text){ | |
| if(text.length < 10) return text; | |
| return text.replace(/\s+/g,' ').concat(', high detail focus, cohesive composition'); | |
| } | |
| // ------------ Generate Flow ------------ | |
| async function doGenerate(){ | |
| const card = addCardSkeleton(); | |
| try{ | |
| el.generateBtn.disabled = true; | |
| el.generateBtn.innerHTML = '<span class="inline-flex items-center gap-2"><i class="fa-solid fa-spinner animate-spin"></i> Generating…</span>'; | |
| let { prompt, negative_prompt } = buildPrompt(); | |
| if(!prompt){ throw new Error('Please enter a prompt'); } | |
| if(el.enhance.checked){ prompt = await enhancePrompt(prompt); } | |
| const model = el.model.value; | |
| const width = Math.min(2048, Math.max(64, +el.width.value||1024)); | |
| const height = Math.min(2048, Math.max(64, +el.height.value||1024)); | |
| const seed = el.randomSeed.checked ? randInt(0, 999999) : (+el.seed.value||42); | |
| const res = await fetch('/generate', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ prompt, negative_prompt, model, width, height, seed }) | |
| }); | |
| if(!res.ok){ throw new Error('Generation failed'); } | |
| const contentType = res.headers.get("content-type"); | |
| if (contentType.includes("application/json")) { | |
| const data = await res.json(); | |
| throw new Error(data.error || "Model returned JSON instead of image"); | |
| } else { | |
| const blob = await res.blob(); | |
| const url = URL.createObjectURL(blob); | |
| card.remove(); | |
| addCard(url, { prompt }); | |
| history.unshift({ url, prompt }); | |
| history = history.slice(0, 30); | |
| localStorage.setItem('img_history', JSON.stringify(history)); | |
| } | |
| }catch(err){ | |
| console.error(err); | |
| card.querySelector('.p-3')?.remove(); | |
| card.innerHTML = `<div class="p-4 text-red-300 text-sm">Error: ${err.message || 'Unknown error'}</div>`; | |
| }finally{ | |
| el.generateBtn.disabled = false; | |
| el.generateBtn.innerHTML = '<span class="inline-flex items-center gap-2"><i class="fa-solid fa-bolt"></i> Generate</span>'; | |
| } | |
| } | |
| // Only generate with button click now | |
| el.generateBtn.addEventListener('click', doGenerate); | |
| // Remove Ctrl+Enter trigger for UX clarity | |
| </script | |
| </body> | |
| </html> | |