Some checks are pending
build-and-push / image (push) Waiting to run
- All 14 panes rewritten with rich controls: KPIs, sparklines, meter gauges, presets, history sidebars, search/filter, bulk actions, schema-driven forms, copy buttons, auto-refresh. - Backend /api/system enriched with cpu_count, rss_bytes, mem_total, mem_used_pct, loadavg[], user/system CPU times, max_rss_kb. - CSS additions: .meter/.gauge, .seg, .chip(s), .spark, .slider, .dl, .link-card, details.card-d plus utility classes. - ui.js helpers: copyToClipboard, withCopy/bindCopyButtons, sparkline, meterRow, fmtBytes/fmtNum, historyStore, inputFromSchema, collectSchemaForm. - Files pane added; iCloud duplicate '*2' files removed.
175 lines
9.8 KiB
JavaScript
175 lines
9.8 KiB
JavaScript
// 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 = `
|
|
<div class="pane-head">
|
|
<div><div class="title">Diffusion</div><div class="sub">Generate via the diffusion sidecar.</div></div>
|
|
<div class="grow"></div>
|
|
<span class="pill" id="diff-pill"><span class="dot"></span><span id="diff-pill-text">—</span></span>
|
|
</div>
|
|
|
|
<div class="grid cols-2 mb-3">
|
|
<div class="card">
|
|
<div class="card-title"><h2>Prompt</h2></div>
|
|
<div class="chips mb-3" id="diff-presets"></div>
|
|
<form id="diff-form" class="grid gap-3">
|
|
<label class="field"><span class="lbl">Prompt</span><textarea class="input" id="diff-prompt" rows="3" required></textarea></label>
|
|
<label class="field"><span class="lbl">Negative prompt</span><textarea class="input" id="diff-neg" rows="2"></textarea></label>
|
|
<div class="grid cols-3 gap-3">
|
|
<label class="field"><span class="lbl">Steps</span><input class="input" id="diff-steps" type="number" value="20" min="1"/></label>
|
|
<label class="field"><span class="lbl">CFG</span><input class="input" id="diff-cfg" type="number" value="7" step="0.5"/></label>
|
|
<label class="field"><span class="lbl">Seed</span><input class="input" id="diff-seed" type="number" value="-1" title="-1 = random"/></label>
|
|
</div>
|
|
<div class="grid cols-3 gap-3">
|
|
<label class="field"><span class="lbl">Width</span><input class="input" id="diff-w" type="number" value="512" step="64"/></label>
|
|
<label class="field"><span class="lbl">Height</span><input class="input" id="diff-h" type="number" value="512" step="64"/></label>
|
|
<label class="field"><span class="lbl">Batch</span><input class="input" id="diff-batch" type="number" value="1" min="1" max="8"/></label>
|
|
</div>
|
|
<div class="grid cols-2 gap-3">
|
|
<label class="field"><span class="lbl">Model</span><select class="input" id="diff-model"><option value="">(default)</option></select></label>
|
|
<label class="field"><span class="lbl">Sampler</span><select class="input" id="diff-sampler"><option value="">(default)</option></select></label>
|
|
</div>
|
|
<div class="btn-row">
|
|
<button class="btn btn-primary" type="submit" id="diff-submit">Generate</button>
|
|
<button class="btn btn-secondary" type="button" id="diff-progress">Progress</button>
|
|
<button class="btn btn-ghost" type="reset">Reset</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title"><h2>Output</h2><span class="muted mono xsmall" id="diff-status">—</span></div>
|
|
<div class="gallery" id="diff-gallery" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:8px"></div>
|
|
<div class="divider"></div>
|
|
<div class="card-title"><h2 style="font-size:14px">Meta</h2><button class="btn btn-ghost" id="diff-copy-meta">Copy</button></div>
|
|
<pre id="diff-meta" class="code" style="max-height:18vh">—</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title"><h2>Recent generations</h2><button class="btn btn-ghost" id="diff-clear-hist">Clear</button></div>
|
|
<div id="diff-history" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:8px"></div>
|
|
</div>
|
|
`;
|
|
|
|
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', `<option value="${escapeHtml(String(name))}">${escapeHtml(String(name))}</option>`);
|
|
});
|
|
} 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', `<option value="${escapeHtml(String(n))}">${escapeHtml(String(n))}</option>`);
|
|
});
|
|
} catch {}
|
|
}
|
|
|
|
function renderHistory(host) {
|
|
const items = galleryHist.list();
|
|
host.querySelector('#diff-history').innerHTML = items.length
|
|
? items.map((g, i) => `<button class="card no-pad" data-hi="${i}" title="${escapeHtml(g.prompt)}" style="border:1px solid var(--border);cursor:pointer;padding:0">
|
|
<img src="data:image/png;base64,${g.b64}" style="width:100%;display:block;border-radius:4px"/>
|
|
<div class="p-2 truncate muted xsmall">${escapeHtml(g.prompt)}</div>
|
|
</button>`).join('')
|
|
: '<div class="muted xsmall p-2">no history yet</div>';
|
|
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) =>
|
|
`<button class="chip" data-pi="${i}" type="button">${escapeHtml(p.label)}</button>`).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) =>
|
|
`<a download="cxai-diff-${Date.now()}-${i}.png" href="data:image/png;base64,${b64}"><img src="data:image/png;base64,${b64}" alt="generated" style="width:100%;border-radius:4px;display:block"/></a>`
|
|
).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);
|
|
});
|
|
},
|
|
});
|