// panes/diffusion.js — Stable Diffusion proxy with prompt presets, model picker, gallery history. import { registerPane } from '../app.js'; import { jget, jpost, escapeHtml } from '../lib/api.js'; import { fmtJSON, ok, err, copyToClipboard, historyStore } from '../lib/ui.js'; const galleryHist = historyStore('cx_diff_gallery', 12); const PROMPT_PRESETS = [ { label: 'Portrait', prompt: 'a cinematic portrait of a person, soft natural light, 50mm lens, bokeh, photorealistic, sharp focus', neg: 'blurry, deformed, text, watermark' }, { label: 'Landscape', prompt: 'sweeping mountain landscape at golden hour, dramatic clouds, ultra-detailed, 8k, photoreal', neg: 'low quality, jpeg artifacts, signature' }, { label: 'Anime', prompt: 'anime illustration, vibrant colors, detailed face, dynamic pose, by Makoto Shinkai, masterpiece', neg: 'realistic, blurry, lowres, bad anatomy' }, { label: 'Photoreal', prompt: 'photoreal product shot, studio lighting, white background, high detail, hyperreal', neg: 'illustration, painting, low quality' }, { label: 'Cyberpunk', prompt: 'cyberpunk street, neon signs, rain, reflective puddles, futuristic, cinematic lighting', neg: 'daylight, bright, cartoon' }, ]; const TPL = `
Diffusion
Generate via the diffusion sidecar.

Prompt

Output

Meta

Recent generations

`; async function probe(host) { const pill = host.querySelector('#diff-pill'); try { const h = await jget('/api/diffusion/healthz'); pill.classList.remove('err'); pill.classList.add('ok'); host.querySelector('#diff-pill-text').textContent = `up · ${h.backend ?? '?'}`; } catch (e) { pill.classList.remove('ok'); pill.classList.add('err'); host.querySelector('#diff-pill-text').textContent = 'down'; } } async function loadModels(host) { try { const m = await jget('/api/diffusion/v1/models'); const sel = host.querySelector('#diff-model'); (m.models || m || []).forEach(x => { const name = x.model_name || x.name || x.id || x; sel.insertAdjacentHTML('beforeend', ``); }); } catch {} try { const s = await jget('/api/diffusion/v1/samplers'); const sel = host.querySelector('#diff-sampler'); (s.samplers || s || []).forEach(x => { const n = x.name || x; sel.insertAdjacentHTML('beforeend', ``); }); } catch {} } function renderHistory(host) { const items = galleryHist.list(); host.querySelector('#diff-history').innerHTML = items.length ? items.map((g, i) => ``).join('') : '
no history yet
'; host.querySelectorAll('#diff-history [data-hi]').forEach(b => b.addEventListener('click', () => { const g = galleryHist.list()[+b.dataset.hi]; if (!g) return; host.querySelector('#diff-prompt').value = g.prompt; host.querySelector('#diff-neg').value = g.neg || ''; if (g.seed != null) host.querySelector('#diff-seed').value = g.seed; })); } registerPane('diffusion', { label: 'Diffusion', init(host) { host.innerHTML = TPL; host.querySelector('#diff-presets').innerHTML = PROMPT_PRESETS.map((p, i) => ``).join(''); host.querySelectorAll('#diff-presets .chip').forEach(b => b.addEventListener('click', () => { const p = PROMPT_PRESETS[+b.dataset.pi]; host.querySelector('#diff-prompt').value = p.prompt; host.querySelector('#diff-neg').value = p.neg; })); probe(host); loadModels(host); renderHistory(host); host.querySelector('#diff-form').addEventListener('submit', async (e) => { e.preventDefault(); const btn = host.querySelector('#diff-submit'); btn.disabled = true; host.querySelector('#diff-status').textContent = 'generating…'; const seed = +host.querySelector('#diff-seed').value; const body = { prompt: host.querySelector('#diff-prompt').value, negative_prompt: host.querySelector('#diff-neg').value, steps: +host.querySelector('#diff-steps').value || 20, width: +host.querySelector('#diff-w').value || 512, height: +host.querySelector('#diff-h').value || 512, cfg_scale: +host.querySelector('#diff-cfg').value || 7, batch_size: +host.querySelector('#diff-batch').value || 1, }; if (seed >= 0) body.seed = seed; const sm = host.querySelector('#diff-sampler').value; if (sm) body.sampler_name = sm; const md = host.querySelector('#diff-model').value; if (md) body.model = md; try { const r = await jpost('/api/diffusion/v1/generate', body); host.querySelector('#diff-gallery').innerHTML = (r.images || []).map((b64, i) => `generated` ).join(''); host.querySelector('#diff-meta').textContent = fmtJSON({ seeds: r.seeds, duration_s: r.duration_s, model: r.model, sampler: r.sampler }); host.querySelector('#diff-status').textContent = `ok · ${r.duration_s?.toFixed?.(2) ?? '?'}s · ${(r.images || []).length} img`; (r.images || []).forEach((b64, i) => galleryHist.push({ b64, prompt: body.prompt, neg: body.negative_prompt, seed: r.seeds?.[i], })); renderHistory(host); ok('Generated'); } catch (e) { host.querySelector('#diff-status').textContent = `error ${e.status ?? ''}`; host.querySelector('#diff-meta').textContent = fmtJSON(e.body ?? { error: e.message }); err(e.message); } finally { btn.disabled = false; } }); host.querySelector('#diff-progress').addEventListener('click', async () => { try { host.querySelector('#diff-meta').textContent = fmtJSON(await jget('/api/diffusion/v1/progress')); } catch (e) { host.querySelector('#diff-meta').textContent = e.message; } }); host.querySelector('#diff-copy-meta').addEventListener('click', () => copyToClipboard(host.querySelector('#diff-meta').textContent)); host.querySelector('#diff-clear-hist').addEventListener('click', () => { if (!confirm('Clear gallery history?')) return; galleryHist.clear(); renderHistory(host); }); }, });