)/gi) || text.match(/data:image\//gi);
return matches ? matches.length : 0;
} catch (e) {
return 0;
}
}
function triggerChangeForEl(el) {
if (!el) {
return;
}
if (typeof triggerChangeFor === 'function') {
triggerChangeFor(el);
return;
}
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
function openInitImageGroup() {
try {
const initEl = document.getElementById('input_initimage');
if (initEl && typeof toggleGroupOpen === 'function') {
toggleGroupOpen(initEl, true);
}
} catch (e) { /* ignore */ }
const toggler = document.getElementById('input_group_content_initimage_toggle');
if (toggler) {
toggler.checked = true;
triggerChangeForEl(toggler);
}
}
function hasFileParam(id) {
const el = document.getElementById(id);
return !!(el && el.files && el.files.length > 0);
}
function clearFileParam(id) {
const el = document.getElementById(id);
if (!el) {
return false;
}
try {
el.value = '';
if (el.files && typeof DataTransfer !== 'undefined') {
el.files = new DataTransfer().files;
}
} catch (e) { /* ignore */ }
triggerChangeForEl(el);
return true;
}
function guessImageMime(src) {
const s = String(src || '');
if (s.startsWith('data:image/')) {
const m = s.match(/^data:(image\/[a-z0-9.+-]+)/i);
return (m && m[1]) || 'image/png';
}
const path = s.split('?')[0];
const ext = path.substring(path.lastIndexOf('.') + 1).toLowerCase();
if (ext === 'jpg' || ext === 'jpeg') {
return 'image/jpeg';
}
if (ext === 'webp') {
return 'image/webp';
}
if (ext === 'gif') {
return 'image/gif';
}
return 'image/png';
}
async function srcToBlob(src) {
if (!src) {
return null;
}
const cleaned = String(src).trim().split(/\s+/)[0];
if (cleaned.startsWith('data:') || cleaned.startsWith('/') || cleaned.startsWith('View/') || cleaned.startsWith('http')) {
try {
const resp = await fetch(cleaned);
return await resp.blob();
} catch (e) {
console.warn('Assistent: fetch blob failed', e);
}
}
return await new Promise((resolve) => {
const tmpImg = new Image();
tmpImg.crossOrigin = 'Anonymous';
tmpImg.onload = () => {
try {
const canvas = document.createElement('canvas');
canvas.width = tmpImg.naturalWidth;
canvas.height = tmpImg.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(tmpImg, 0, 0);
canvas.toBlob((blob) => resolve(blob), 'image/png');
} catch (e) {
resolve(null);
}
};
tmpImg.onerror = () => resolve(null);
tmpImg.src = cleaned;
});
}
async function setFileParamFromSrc(paramId, src, { filename = 'assistent.png' } = {}) {
const el = document.getElementById(paramId);
if (!el) {
setStatus(`Missing ${paramId} on Generate tab`);
return false;
}
const blob = await srcToBlob(src);
if (!blob) {
setStatus('Could not load image for init/mask');
return false;
}
const mime = blob.type || guessImageMime(src);
const file = new File([blob], filename, { type: mime });
const container = new DataTransfer();
container.items.add(file);
el.files = container.files;
triggerChangeForEl(el);
openInitImageGroup();
return true;
}
async function setInitFromSrc(src) {
const ok = await setFileParamFromSrc('input_initimage', src, { filename: 'assistent_init.png' });
if (ok) {
setStatus('Init Image set');
}
return ok;
}
async function setMaskFromSrc(src) {
const ok = await setFileParamFromSrc('input_maskimage', src, { filename: 'assistent_mask.png' });
if (ok) {
setStatus('Mask Image set (white = edit)');
}
return ok;
}
function clearInitAndMask() {
clearFileParam('input_initimage');
clearFileParam('input_maskimage');
const toggler = document.getElementById('input_group_content_initimage_toggle');
if (toggler) {
toggler.checked = false;
triggerChangeForEl(toggler);
}
setStatus('Init Image + Mask cleared');
}
function readInitContext() {
const creativityRaw = val('input_initimagecreativity');
const creativity = creativityRaw === '' ? null : parseFloat(creativityRaw);
return {
has_init_image: hasFileParam('input_initimage'),
has_mask_image: hasFileParam('input_maskimage'),
init_creativity: Number.isFinite(creativity) ? creativity : null,
mask_blur: parseFloat(val('input_maskblur') || '') || null,
mask_grow: parseInt(val('input_maskgrow') || val('input_maskshrinkgrow') || '', 10) || null,
init_group_on: !!(document.getElementById('input_group_content_initimage_toggle')?.checked),
};
}
function collectLiveContext() {
const inv = state.inventory || {};
const initCtx = readInitContext();
const ctx = {
architecture_ok: isKreaSelected(),
checkpoint: null,
prompt: val('alt_prompt_textbox') || val('input_prompt') || '',
negative: val('input_negativeprompt') || val('alt_negativeprompt_textbox') || '',
width: parseInt(val('input_width') || '0', 10) || null,
height: parseInt(val('input_height') || '0', 10) || null,
steps: parseInt(val('input_steps') || '0', 10) || null,
cfg: parseFloat(val('input_cfgscale') || val('input_cfg') || '') || null,
sigma_shift: parseFloat(val('input_sigmashift') || '') || null,
seed: val('input_seed') || null,
sampler: val('input_sampler') || val('input_samplerate') || null,
scheduler: val('input_scheduler') || null,
batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null,
prompt_image_count: countPromptImages(),
selected_loras: [],
available_loras: (inv.loras || []).slice(0, 100),
wildcards: (inv.wildcards || []).map((w) => w.name || w).slice(0, 60),
has_vision_image: !!state.lastImageDataUrl,
has_civitai_key: !!inv.has_civitai_key,
auto_apply: !!$('sa_auto_apply')?.checked,
auto_generate: !!$('sa_auto_generate')?.checked,
...initCtx,
};
try {
const model = resolveCurrentCheckpoint();
if (model.name || model.architecture) {
ctx.checkpoint = {
name: model.name || model.title || null,
architecture: model.architecture || model.compat_class || model.class || null,
title: model.title || null,
};
}
} catch (e) { /* ignore */ }
try {
if (typeof loraHelper !== 'undefined' && loraHelper && Array.isArray(loraHelper.selected)) {
ctx.selected_loras = loraHelper.selected.map((l) => ({
name: l.name || l,
weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l]) || 1,
}));
}
} catch (e) { /* ignore */ }
// Fallback if inventory empty
if (!ctx.available_loras.length) {
try {
const models = (typeof allModels !== 'undefined' && allModels) || (typeof model_list !== 'undefined' && model_list) || [];
const list = Array.isArray(models) ? models : Object.values(models || {});
for (const m of list) {
if (!m) {
continue;
}
const folder = `${m.folder || m.path || ''}`;
const isLora = /lora/i.test(m.category || m.type || '') || (m.name && String(m.name).toLowerCase().includes('lora'));
const inLoraFolder = /lora/i.test(folder);
if (!(isLora || inLoraFolder)) {
continue;
}
ctx.available_loras.push({
name: m.name || m.title,
title: m.title || m.name,
trigger_phrase: m.trigger_phrase || m.trigger || (m.metadata && (m.metadata.trigger_phrase || m.metadata.trigger)) || null,
architecture: m.architecture || null,
});
}
if (ctx.available_loras.length > 80) {
ctx.available_loras = ctx.available_loras.slice(0, 80);
}
} catch (e) { /* ignore */ }
}
return ctx;
}
function isPatchObject(obj) {
if (!obj || typeof obj !== 'object') {
return false;
}
return (
obj.prompt != null ||
obj.loras ||
obj.width ||
obj.height ||
obj.steps ||
obj.cfg ||
obj.seed != null ||
obj.sigma_shift != null ||
obj.sampler ||
obj.actions ||
obj.search_query ||
obj.civitai_query ||
obj.use_init_image != null ||
obj.clear_init_image != null ||
obj.init_creativity != null ||
obj.denoise != null ||
obj.use_mask_image != null ||
obj.clear_mask_image != null ||
obj.mask_blur != null ||
obj.mask_grow != null
);
}
function extractPatch(text) {
if (!text) {
return { prose: text || '', patch: null };
}
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
let match;
let lastPatch = null;
let prose = text;
while ((match = re.exec(text)) !== null) {
const raw = match[1].trim();
try {
const obj = JSON.parse(raw);
if (isPatchObject(obj)) {
lastPatch = obj;
prose = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trim();
}
} catch (e) { /* not json */ }
}
return { prose, patch: lastPatch };
}
function patchHasGenTrigger(patch) {
if (!patch) {
return false;
}
if (Array.isArray(patch.actions) && patch.actions.map(String).includes('generate')) {
return true;
}
return (
patch.prompt != null ||
patch.loras ||
patch.width != null ||
patch.height != null ||
patch.steps != null ||
patch.cfg != null ||
patch.seed != null ||
patch.sigma_shift != null ||
patch.use_init_image ||
patch.clear_init_image ||
patch.init_creativity != null ||
patch.denoise != null ||
patch.use_mask_image ||
patch.clear_mask_image
);
}
async function applyPatch(patch, which) {
if (!patch) {
return;
}
const doPrompt = !which || which === 'all' || which === 'prompt';
const doLoras = !which || which === 'all' || which === 'loras';
const doParams = !which || which === 'all' || which === 'size' || which === 'params';
const doInit = !which || which === 'all' || which === 'params' || which === 'init';
if (doPrompt && patch.prompt != null) {
const box = document.getElementById('alt_prompt_textbox') || document.getElementById('input_prompt');
if (box) {
box.value = patch.prompt;
box.dispatchEvent(new Event('input', { bubbles: true }));
box.dispatchEvent(new Event('change', { bubbles: true }));
}
if (patch.negative != null) {
setVal('input_negativeprompt', patch.negative);
}
if (Array.isArray(patch.loras)) {
for (const l of patch.loras) {
const triggers = l.triggers || (l.trigger_phrase ? [l.trigger_phrase] : []);
for (const t of triggers) {
if (t && box && box.value && !box.value.includes(t)) {
box.value = `${box.value.trim()}, ${t}`;
box.dispatchEvent(new Event('input', { bubbles: true }));
}
}
}
}
}
if (doLoras && Array.isArray(patch.loras) && typeof loraHelper !== 'undefined' && loraHelper) {
try {
if (typeof loraHelper.clearLoras === 'function') {
loraHelper.clearLoras();
}
} catch (e) { /* ignore */ }
for (const l of patch.loras) {
const name = l.name;
if (!name) {
continue;
}
try {
if (typeof loraHelper.selectLora === 'function') {
loraHelper.selectLora(name);
}
if (loraHelper.loraWeightPref && l.weight != null) {
loraHelper.loraWeightPref[name] = l.weight;
}
} catch (e) {
console.warn('Assistent: selectLora failed', name, e);
}
}
try {
if (typeof loraHelper.rebuildUI === 'function') {
loraHelper.rebuildUI();
}
} catch (e) { /* ignore */ }
}
if (doParams) {
if (patch.width != null) {
setVal('input_width', String(patch.width));
}
if (patch.height != null) {
setVal('input_height', String(patch.height));
}
if (patch.steps != null) {
setVal('input_steps', String(patch.steps));
}
if (patch.cfg != null) {
if (document.getElementById('input_cfgscale')) {
setVal('input_cfgscale', String(patch.cfg));
} else {
setVal('input_cfg', String(patch.cfg));
}
}
if (patch.seed != null) {
setVal('input_seed', String(patch.seed));
}
if (patch.sigma_shift != null) {
setVal('input_sigmashift', String(patch.sigma_shift));
}
if (patch.sampler != null) {
if (document.getElementById('input_sampler')) {
setVal('input_sampler', String(patch.sampler));
}
}
if (patch.scheduler != null && document.getElementById('input_scheduler')) {
setVal('input_scheduler', String(patch.scheduler));
}
}
if (doInit) {
const creativity = patch.init_creativity != null ? patch.init_creativity : patch.denoise;
if (creativity != null && document.getElementById('input_initimagecreativity')) {
setVal('input_initimagecreativity', String(creativity));
openInitImageGroup();
}
if (patch.mask_blur != null && document.getElementById('input_maskblur')) {
setVal('input_maskblur', String(patch.mask_blur));
}
if (patch.mask_grow != null) {
if (document.getElementById('input_maskgrow')) {
setVal('input_maskgrow', String(patch.mask_grow));
} else if (document.getElementById('input_maskshrinkgrow')) {
setVal('input_maskshrinkgrow', String(patch.mask_grow));
}
}
if (patch.clear_init_image || patch.clear_mask_image) {
if (patch.clear_init_image) {
clearFileParam('input_initimage');
}
if (patch.clear_mask_image) {
clearFileParam('input_maskimage');
}
if (patch.clear_init_image && patch.clear_mask_image) {
const toggler = document.getElementById('input_group_content_initimage_toggle');
if (toggler) {
toggler.checked = false;
triggerChangeForEl(toggler);
}
}
}
const src = state.lastImageDataUrl || findCurrentGenerateSrc();
const wantInit = patch.use_init_image === true
|| (Array.isArray(patch.actions) && patch.actions.map(String).includes('use_init'));
const wantMask = patch.use_mask_image === true
|| (Array.isArray(patch.actions) && patch.actions.map(String).includes('use_mask'));
if (wantInit) {
if (src) {
await setInitFromSrc(src);
} else {
setStatus('No image for Init — drop/Send to Assistent first');
}
}
if (wantMask) {
if (src) {
await setMaskFromSrc(src);
} else {
setStatus('No image for Mask — drop a mask (white=edit) first');
}
}
}
setStatus('Applied patch');
}
function triggerGenerate() {
try {
if (typeof mainGenHandler !== 'undefined' && mainGenHandler && typeof mainGenHandler.doGenerate === 'function') {
mainGenHandler.doGenerate();
return true;
}
} catch (e) {
console.warn('Assistent: doGenerate failed', e);
}
const btn =
document.getElementById('generate_button') ||
document.getElementById('alt_generate_button') ||
document.querySelector('button.generate-button') ||
document.querySelector('#generate_button, button[id*="generate"]');
if (btn) {
btn.click();
return true;
}
return false;
}
function doInterruptNow() {
try {
if (typeof doInterrupt === 'function') {
doInterrupt(false);
return;
}
} catch (e) { /* ignore */ }
if (typeof genericRequest === 'function') {
genericRequest('InterruptAll', { other_sessions: false }, () => {}, 0, () => {});
}
}
function waitForNewImage(prevSrc, timeoutMs = 180000) {
return new Promise((resolve) => {
const start = Date.now();
const timer = setInterval(() => {
const src = findCurrentGenerateSrc();
if (src && src !== prevSrc && !looksLikeModelPreview(src)) {
clearInterval(timer);
resolve(src);
} else if (Date.now() - start > timeoutMs) {
clearInterval(timer);
resolve(null);
}
}, 400);
});
}
async function runGenerateFromPatch(patch) {
if (!$('sa_auto_generate')?.checked || !patchHasGenTrigger(patch)) {
return null;
}
const prev = findCurrentGenerateSrc();
setStatus('Generating…');
startBusyUi('generating');
state.generating = true;
setInterruptVisible(true);
const ok = triggerGenerate();
if (!ok) {
state.generating = false;
setInterruptVisible(state.busy);
setStatus('Could not start Generate (UI hook missing)');
return null;
}
const src = await waitForNewImage(prev);
state.generating = false;
setInterruptVisible(state.busy);
if (!state.busy) {
stopBusyUi(src ? 'Generate done' : 'Generate finished (no new image detected)');
}
if (src) {
setImageFromSrc(src, { note: 'Result → vision' });
return src;
}
if (state.busy) {
setStatus('Generate finished (no new image detected)');
}
return null;
}
async function maybeAutoCritique(imageSrc) {
if (!$('sa_auto_critique')?.checked || state.critiqueHopUsed || !imageSrc) {
return;
}
state.critiqueHopUsed = true;
const pack = $('sa_pack');
if (pack) {
pack.value = 'critique_image';
saveSettings();
}
if ($('sa_input')) {
$('sa_input').value = 'Critique this result and improve the prompt for the next generation.';
}
setStatus('Auto-critique…');
await sendChat({ fromAutoCritique: true });
}
function appendMessage(role, text, patch, civitaiResults) {
const box = $('sa_messages');
if (!box) {
return null;
}
hideChatEmpty();
const div = document.createElement('div');
div.className = `sa-msg ${role}`;
const { prose, patch: extracted } = role === 'assistant' ? extractPatch(text) : { prose: text, patch: null };
const finalPatch = patch || extracted;
div.textContent = prose || text || '';
if (finalPatch) {
const wrap = document.createElement('div');
wrap.className = 'sa-patch';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify(finalPatch, null, 2);
wrap.appendChild(pre);
const actions = document.createElement('div');
actions.className = 'sa-patch-actions';
for (const [label, which] of [
['Apply all', 'all'],
['Prompt', 'prompt'],
['LoRAs', 'loras'],
['Params', 'params'],
]) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'basic-button';
btn.textContent = label;
btn.addEventListener('click', () => applyPatch(finalPatch, which));
actions.appendChild(btn);
}
const genBtn = document.createElement('button');
genBtn.type = 'button';
genBtn.className = 'basic-button';
genBtn.textContent = 'Apply + Generate';
genBtn.addEventListener('click', async () => {
await applyPatch(finalPatch, 'all');
await runGenerateFromPatch({ ...finalPatch, actions: ['generate'] });
});
actions.appendChild(genBtn);
wrap.appendChild(actions);
div.appendChild(wrap);
}
if (civitaiResults && civitaiResults.length) {
div.appendChild(buildCivitaiCards(civitaiResults));
}
box.appendChild(div);
box.scrollTop = box.scrollHeight;
return div;
}
function beginStreamMessage() {
const box = $('sa_messages');
if (!box) {
return null;
}
hideChatEmpty();
const div = document.createElement('div');
div.className = 'sa-msg assistant sa-streaming sa-typing';
div.innerHTML = 'Waiting for the model…';
box.appendChild(div);
box.scrollTop = box.scrollHeight;
state.streamEl = div;
return div;
}
function appendStreamDelta(delta) {
if (!state.streamEl) {
beginStreamMessage();
}
if (state.streamEl) {
if (state.streamEl.classList.contains('sa-typing')) {
state.streamEl.classList.remove('sa-typing');
state.streamEl.textContent = '';
}
state.gotDelta = true;
if (state.busyPhase !== 'refining') {
setBusyPhase('streaming');
}
state.streamEl.textContent += delta;
const box = $('sa_messages');
if (box) {
box.scrollTop = box.scrollHeight;
}
}
}
function finalizeStreamMessage(fullReply, civitaiResults) {
const el = state.streamEl;
state.streamEl = null;
if (!el) {
appendMessage('assistant', fullReply, null, civitaiResults);
return;
}
el.classList.remove('sa-streaming', 'sa-typing');
const { prose, patch } = extractPatch(fullReply);
el.textContent = prose || fullReply || '';
if (patch) {
const wrap = document.createElement('div');
wrap.className = 'sa-patch';
const pre = document.createElement('pre');
pre.textContent = JSON.stringify(patch, null, 2);
wrap.appendChild(pre);
const actions = document.createElement('div');
actions.className = 'sa-patch-actions';
for (const [label, which] of [
['Apply all', 'all'],
['Prompt', 'prompt'],
['LoRAs', 'loras'],
['Params', 'params'],
]) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'basic-button';
btn.textContent = label;
btn.addEventListener('click', () => applyPatch(patch, which));
actions.appendChild(btn);
}
const genBtn = document.createElement('button');
genBtn.type = 'button';
genBtn.className = 'basic-button';
genBtn.textContent = 'Apply + Generate';
genBtn.addEventListener('click', async () => {
await applyPatch(patch, 'all');
await runGenerateFromPatch({ ...patch, actions: ['generate'] });
});
actions.appendChild(genBtn);
wrap.appendChild(actions);
el.appendChild(wrap);
}
if (civitaiResults && civitaiResults.length) {
el.appendChild(buildCivitaiCards(civitaiResults));
}
const box = $('sa_messages');
if (box) {
box.scrollTop = box.scrollHeight;
}
}
function buildCivitaiCards(results) {
const list = document.createElement('div');
list.className = 'sa-civitai-list';
for (const r of results) {
const card = document.createElement('div');
card.className = 'sa-civitai-card' + (r.already_installed ? ' sa-installed' : '');
const title = document.createElement('div');
title.className = 'sa-civitai-title';
title.textContent = r.name || r.file_name || 'LoRA';
card.appendChild(title);
const meta = document.createElement('div');
meta.className = 'sa-civitai-meta';
const bits = [
r.base_model || '?',
r.krea_likely ? 'Krea?' : null,
r.already_installed ? 'already installed' : null,
(r.triggers || []).slice(0, 3).join(', ') || null,
].filter(Boolean);
meta.textContent = bits.join(' · ');
card.appendChild(meta);
const actions = document.createElement('div');
actions.className = 'sa-civitai-actions';
if (r.already_installed) {
const note = document.createElement('span');
note.textContent = 'Installed';
actions.appendChild(note);
} else if (r.download_url) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'basic-button sa-primary';
btn.textContent = 'Confirm download';
btn.addEventListener('click', () => downloadCivitaiLoRA(r, btn));
actions.appendChild(btn);
} else {
const note = document.createElement('span');
note.textContent = 'No download URL';
actions.appendChild(note);
}
card.appendChild(actions);
list.appendChild(card);
}
return list;
}
function downloadCivitaiLoRA(card, btn) {
if (!card.download_url) {
return;
}
if (btn) {
btn.disabled = true;
btn.textContent = 'Downloading…';
}
setStatus(`Downloading ${card.file_name || card.name}…`);
setInterruptVisible(true);
const payload = {
url: card.download_url,
type: 'LoRA',
name: card.file_name || card.name || 'lora',
};
const onDone = (ok, msg) => {
setInterruptVisible(state.busy || state.generating);
if (ok) {
setStatus(`Downloaded ${payload.name}`);
if (btn) {
btn.textContent = 'Downloaded';
}
refreshInventory(() => {
if ($('sa_input')) {
$('sa_input').value = `LoRA "${payload.name}" is now installed. Enable it with its triggers and improve the prompt.`;
}
sendChat({ fromDownload: true });
});
} else {
setStatus(msg || 'Download failed');
if (btn) {
btn.disabled = false;
btn.textContent = 'Confirm download';
}
appendMessage('error', msg || 'Download failed');
}
};
if (typeof makeWSRequest === 'function') {
makeWSRequest(
'DoModelDownloadWS',
payload,
(data) => {
if (data.error) {
onDone(false, String(data.error));
return;
}
if (data.success || data.overall_percent >= 1 || data.current_percent >= 1) {
if (data.success || data.overall_percent >= 0.99) {
onDone(true);
} else if (data.current_percent != null) {
setStatus(`Download ${(data.current_percent * 100).toFixed(0)}%`);
}
}
},
0,
(err) => onDone(false, String(err || 'Download failed')),
);
} else {
onDone(false, 'makeWSRequest unavailable');
}
}
function wantsAutoVision() {
return !!$('sa_auto_vision')?.checked;
}
function looksLikeModelPreview(src) {
const s = String(src || '').toLowerCase();
if (!s) {
return false;
}
return s.includes('.preview.')
|| s.includes('placeholder')
|| /\/models\//i.test(s);
}
function findCurrentGenerateSrc({ allowPreview = false } = {}) {
let src = null;
try {
const cur = document.getElementById('current_image_img')
|| document.querySelector('#current_image img')
|| document.querySelector('.current-image img')
|| document.querySelector('#current_image_batch img');
if (cur) {
src = cur.dataset?.src || cur.src || null;
}
} catch (e) { /* ignore */ }
if (!src) {
try {
if (typeof currentMetadataMap !== 'undefined' && currentMetadataMap && currentMetadataMap.image) {
src = currentMetadataMap.image;
}
} catch (e) { /* ignore */ }
}
if (!src) {
return null;
}
if (!allowPreview && looksLikeModelPreview(src)) {
return null;
}
return src;
}
function refreshImagePreview({ onlyIfEmpty = false } = {}) {
if (!wantsAutoVision()) {
return;
}
if (onlyIfEmpty && state.lastImageDataUrl) {
return;
}
const src = findCurrentGenerateSrc();
if (src) {
setImageFromSrc(src);
}
}
function fileToDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ''));
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
async function acceptImageFile(file) {
if (!file || !String(file.type || '').startsWith('image/')) {
setStatus('Not an image file');
return false;
}
const dataUrl = await fileToDataUrl(file);
return setImageFromSrc(dataUrl, { note: `Loaded ${file.name || 'image'}` });
}
async function handleDropDataTransfer(dt) {
if (!dt) {
return false;
}
if (dt.files && dt.files.length) {
for (const file of dt.files) {
if (String(file.type || '').startsWith('image/')) {
return acceptImageFile(file);
}
}
}
const uri = (dt.getData('text/uri-list') || dt.getData('text/plain') || '').trim();
if (uri) {
const first = uri.split('\n').map((l) => l.trim()).find((l) => l && !l.startsWith('#'));
if (first) {
return setImageFromSrc(first, { note: 'Image from drag' });
}
}
const html = dt.getData('text/html') || '';
const m = html.match(/src=["']([^"']+)["']/i);
if (m && m[1]) {
return setImageFromSrc(m[1], { note: 'Image from drag' });
}
return false;
}
async function imageToBase64ForOllama(src, maxEdge = 1024) {
if (!src) {
return null;
}
const dataUrl = await srcToDataUrl(src);
if (!dataUrl) {
return null;
}
try {
const img = await new Promise((resolve, reject) => {
const el = new Image();
el.onload = () => resolve(el);
el.onerror = reject;
el.src = dataUrl;
});
const w = img.naturalWidth || img.width || 0;
const h = img.naturalHeight || img.height || 0;
const edge = Math.max(w, h);
const canvas = document.createElement('canvas');
if (!edge || edge <= maxEdge) {
canvas.width = Math.max(w, 1);
canvas.height = Math.max(h, 1);
canvas.getContext('2d').drawImage(img, 0, 0);
} else {
const scale = maxEdge / edge;
canvas.width = Math.max(1, Math.round(w * scale));
canvas.height = Math.max(1, Math.round(h * scale));
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
}
const jpeg = canvas.toDataURL('image/jpeg', 0.85);
const i = jpeg.indexOf(',');
return i >= 0 ? jpeg.slice(i + 1) : null;
} catch (e) {
console.warn('Assistent: vision resize failed', e);
const i = dataUrl.indexOf(',');
return i >= 0 ? dataUrl.slice(i + 1) : null;
}
}
async function srcToDataUrl(src) {
if (src.startsWith('data:')) {
return src;
}
try {
const resp = await fetch(src);
const blob = await resp.blob();
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ''));
reader.onerror = reject;
reader.readAsDataURL(blob);
});
} catch (e) {
console.warn('Assistent: vision fetch failed', e);
return null;
}
}
function loadSettings() {
const base = localStorage.getItem(LS_BASE);
const model = localStorage.getItem(LS_MODEL);
const pack = localStorage.getItem(LS_PACK);
const auto = localStorage.getItem(LS_AUTO_VISION);
const autoApply = localStorage.getItem(LS_AUTO_APPLY);
const autoGen = localStorage.getItem(LS_AUTO_GENERATE);
const autoCrit = localStorage.getItem(LS_AUTO_CRITIQUE);
const autoDl = localStorage.getItem(LS_AUTO_DOWNLOAD);
const paneW = localStorage.getItem(LS_PANE_WIDTH);
if (base && $('sa_base_url')) {
$('sa_base_url').value = base;
}
if (pack && $('sa_pack')) {
$('sa_pack').value = pack;
}
if (auto != null && $('sa_auto_vision')) {
$('sa_auto_vision').checked = auto === '1';
}
if ($('sa_auto_apply')) {
$('sa_auto_apply').checked = autoApply == null ? true : autoApply === '1';
}
if ($('sa_auto_generate')) {
$('sa_auto_generate').checked = autoGen == null ? true : autoGen === '1';
}
if ($('sa_auto_critique') && autoCrit != null) {
$('sa_auto_critique').checked = autoCrit === '1';
}
if ($('sa_auto_download') && autoDl != null) {
$('sa_auto_download').checked = autoDl === '1';
}
if (model) {
state.preferredModel = model;
}
if (paneW) {
document.documentElement.style.setProperty('--sa-image-width', paneW);
}
}
function saveSettings() {
localStorage.setItem(LS_BASE, $('sa_base_url')?.value || '');
localStorage.setItem(LS_MODEL, $('sa_model')?.value || '');
localStorage.setItem(LS_PACK, $('sa_pack')?.value || 'write_prompt');
localStorage.setItem(LS_AUTO_VISION, $('sa_auto_vision')?.checked ? '1' : '0');
localStorage.setItem(LS_AUTO_APPLY, $('sa_auto_apply')?.checked ? '1' : '0');
localStorage.setItem(LS_AUTO_GENERATE, $('sa_auto_generate')?.checked ? '1' : '0');
localStorage.setItem(LS_AUTO_CRITIQUE, $('sa_auto_critique')?.checked ? '1' : '0');
localStorage.setItem(LS_AUTO_DOWNLOAD, $('sa_auto_download')?.checked ? '1' : '0');
}
function setModelOptions(models, { error } = {}) {
const sel = $('sa_model');
if (!sel) {
return;
}
const names = (models || []).map((n) => String(n || '').trim()).filter(Boolean);
sel.innerHTML = '';
if (error) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = `⚠ ${String(error).replace(/\s+/g, ' ').slice(0, 90)}`;
sel.appendChild(opt);
sel.disabled = true;
return;
}
sel.disabled = false;
if (!names.length) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'No Ollama models — pull / Refresh';
sel.appendChild(opt);
return;
}
for (const name of names) {
const opt = document.createElement('option');
opt.value = name;
opt.textContent = name;
sel.appendChild(opt);
}
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL);
if (prefer && names.includes(prefer)) {
sel.value = prefer;
}
}
function refreshModels() {
const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434';
setStatus('Loading models…');
if (typeof genericRequest !== 'function') {
setStatus('SwarmUI API not ready');
setModelOptions([], { error: 'SwarmUI API not ready' });
return;
}
genericRequest(
'AssistentListModels',
{ baseUrl },
(data) => {
const models = data.models || [];
setModelOptions(models);
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL);
if (prefer && models.includes(prefer) && $('sa_model')) {
$('sa_model').value = prefer;
}
setStatus(models.length ? `${models.length} models` : 'No Ollama models (gpu-rent: ollama pull)');
saveSettings();
},
0,
(err) => {
const msg = String(err || 'Ollama unreachable');
setStatus(msg);
setModelOptions([], { error: msg });
appendMessage('error', msg);
},
);
}
function refreshInventory(done) {
if (typeof genericRequest !== 'function') {
if (done) {
done();
}
return;
}
genericRequest(
'AssistentListInventory',
{},
(data) => {
state.inventory = {
loras: data.loras || [],
checkpoints: data.checkpoints || [],
wildcards: data.wildcards || [],
has_civitai_key: !!data.has_civitai_key,
};
setStatus(`Inventory: ${state.inventory.loras.length} LoRAs, ${state.inventory.wildcards.length} wildcards`);
if (done) {
done();
}
},
0,
(err) => {
console.warn('Assistent inventory', err);
if (done) {
done();
}
},
);
}
async function handleReplySideEffects(reply, civitaiResults, { fromAutoCritique } = {}) {
const { patch } = extractPatch(reply);
if (Array.isArray(patch?.actions) && patch.actions.map(String).includes('interrupt')) {
doInterruptNow();
}
if (civitaiResults && civitaiResults.length && $('sa_auto_download')?.checked) {
const pick = civitaiResults.find((r) => !r.already_installed && r.download_url && r.krea_likely)
|| civitaiResults.find((r) => !r.already_installed && r.download_url);
if (pick) {
downloadCivitaiLoRA(pick, null);
}
}
if (patch && $('sa_auto_apply')?.checked) {
await applyPatch(patch, 'all');
if (!fromAutoCritique) {
const src = await runGenerateFromPatch(patch);
if (src) {
await maybeAutoCritique(src);
}
}
}
}
async function sendChat(opts = {}) {
if (state.busy) {
return;
}
if (!updateGate()) {
setStatus('Select a Krea 2 model');
return;
}
const text = ($('sa_input')?.value || '').trim();
if (!text) {
return;
}
const pack = $('sa_pack')?.value || 'write_prompt';
const model = $('sa_model')?.value;
if (!model) {
setStatus('Pick an Ollama model in ⚙');
refreshModels();
return;
}
if (!state.lastImageDataUrl && wantsAutoVision()) {
refreshImagePreview();
}
const attach = ($('sa_attach_vision')?.checked || wantsAutoVision()) && state.lastImageDataUrl;
let images = null;
if (attach) {
startBusyUi('encoding');
setStatus('Encoding image…');
const b64 = await imageToBase64ForOllama(state.lastImageDataUrl);
if (b64) {
images = [b64];
}
}
if (!opts.fromAutoCritique && !opts.fromDownload) {
state.critiqueHopUsed = false;
}
state.history.push({ role: 'user', content: text });
appendMessage('user', text);
$('sa_input').value = '';
const context = collectLiveContext();
context.has_vision_image = !!images;
const messages = state.history.slice(-12).map((m) => ({ role: m.role, content: m.content }));
if (images && messages.length) {
messages[messages.length - 1].images = images;
}
state.busy = true;
setInterruptVisible(true);
startBusyUi('thinking');
saveSettings();
const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434';
// Flat fields: SwarmUI JObject params receive the whole request body.
const payload = {
baseUrl,
model,
pack,
includeBase: true,
messages,
context_json: JSON.stringify(context),
raw: {
messages,
context_json: JSON.stringify(context),
pack,
base_url: baseUrl,
model,
},
};
const finishOk = async (reply, civitaiResults) => {
state.busy = false;
setInterruptVisible(state.generating);
state.history.push({ role: 'assistant', content: reply });
stopBusyUi('Done');
await handleReplySideEffects(reply, civitaiResults, opts);
};
const finishErr = (msg) => {
state.busy = false;
setInterruptVisible(state.generating);
stopBusyUi(msg);
if (state.streamEl) {
state.streamEl.classList.remove('sa-streaming', 'sa-typing');
state.streamEl.classList.add('error');
state.streamEl.textContent = msg;
state.streamEl = null;
} else {
appendMessage('error', msg);
}
};
if (typeof makeWSRequest === 'function') {
beginStreamMessage();
makeWSRequest(
'AssistentChatWS',
payload,
(data) => {
if (data.phase === 'waiting_ollama') {
setBusyPhase('loading');
const label = state.streamEl?.querySelector('.sa-typing-label');
if (label) {
label.textContent = `Loading ${modelShort(model)} into GPU…`;
}
return;
}
if (data.error) {
finishErr(String(data.error));
return;
}
if (data.clear_stream) {
if (state.streamEl) {
state.streamEl.classList.add('sa-typing');
state.streamEl.innerHTML = 'Refining…';
}
setBusyPhase('refining');
return;
}
if (data.delta) {
appendStreamDelta(data.delta);
return;
}
if (data.done || data.reply != null) {
const reply = data.reply || (state.streamEl && state.streamEl.textContent) || '';
const civitai = data.civitai_results || [];
finalizeStreamMessage(reply, civitai);
finishOk(reply, civitai);
}
},
0,
(err) => {
// Fallback to HTTP AssistentChat
console.warn('AssistentChatWS failed, falling back', err);
if (state.streamEl) {
state.streamEl.remove();
state.streamEl = null;
}
genericRequest(
'AssistentChat',
payload,
(data) => {
if (data.error) {
finishErr(String(data.error));
return;
}
const reply = data.reply || '';
appendMessage('assistant', reply, null, data.civitai_results || []);
finishOk(reply, data.civitai_results || []);
},
0,
(err2) => finishErr(String(err2 || err || 'Chat failed')),
);
},
);
return;
}
genericRequest(
'AssistentChat',
payload,
(data) => {
if (data.error) {
finishErr(String(data.error));
return;
}
const reply = data.reply || '';
appendMessage('assistant', reply, null, data.civitai_results || []);
finishOk(reply, data.civitai_results || []);
},
0,
(err) => finishErr(String(err || 'Chat failed')),
);
}
function wireDropZone() {
const frame = $('sa_image_frame');
const overlay = $('sa_drop_overlay');
if (!frame) {
return;
}
const setDrag = (on) => {
frame.classList.toggle('sa-dragover', on);
if (overlay) {
overlay.hidden = !on;
}
};
['dragenter', 'dragover'].forEach((ev) => {
frame.addEventListener(ev, (e) => {
e.preventDefault();
e.stopPropagation();
state.dragDepth += ev === 'dragenter' ? 1 : 0;
setDrag(true);
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'copy';
}
});
});
frame.addEventListener('dragleave', (e) => {
e.preventDefault();
state.dragDepth = Math.max(0, state.dragDepth - 1);
if (state.dragDepth === 0) {
setDrag(false);
}
});
frame.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
state.dragDepth = 0;
setDrag(false);
await handleDropDataTransfer(e.dataTransfer);
});
const layout = $('sa_layout');
layout?.addEventListener('dragover', (e) => {
if (e.dataTransfer?.types?.includes('Files') || e.dataTransfer?.types?.includes('text/uri-list')) {
e.preventDefault();
}
});
layout?.addEventListener('drop', async (e) => {
if (!e.dataTransfer) {
return;
}
e.preventDefault();
await handleDropDataTransfer(e.dataTransfer);
});
document.addEventListener('paste', async (e) => {
const pane = document.getElementById('assistent');
if (!pane || !pane.classList.contains('active')) {
return;
}
if (e.target && (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT')) {
const items = e.clipboardData?.items;
let hasImage = false;
if (items) {
for (const item of items) {
if (item.type.startsWith('image/')) {
hasImage = true;
break;
}
}
}
if (!hasImage) {
return;
}
}
const items = e.clipboardData?.items;
if (!items) {
return;
}
for (const item of items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
const file = item.getAsFile();
if (file) {
await acceptImageFile(file);
}
return;
}
}
});
}
function wireSplitter() {
const splitter = $('sa_splitter');
const layout = $('sa_layout');
const pane = $('sa_image_pane');
if (!splitter || !layout || !pane) {
return;
}
let dragging = false;
splitter.addEventListener('mousedown', (e) => {
e.preventDefault();
dragging = true;
splitter.classList.add('sa-dragging');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
});
window.addEventListener('mousemove', (e) => {
if (!dragging) {
return;
}
const rect = layout.getBoundingClientRect();
const x = e.clientX - rect.left;
const pct = Math.min(45, Math.max(18, (x / rect.width) * 100));
const value = `${pct}%`;
document.documentElement.style.setProperty('--sa-image-width', value);
localStorage.setItem(LS_PANE_WIDTH, value);
});
window.addEventListener('mouseup', () => {
if (!dragging) {
return;
}
dragging = false;
splitter.classList.remove('sa-dragging');
document.body.style.cursor = '';
document.body.style.userSelect = '';
});
}
function registerSendButton() {
if (typeof registerMediaButton !== 'function') {
setTimeout(registerSendButton, 500);
return;
}
if (window.__swarmAssistentMediaRegistered) {
return;
}
window.__swarmAssistentMediaRegistered = true;
registerMediaButton(
'Send to Assistent',
(src) => {
setImageFromSrc(src, {
switchTab: true,
note: 'Image sent to Assistent',
});
const pack = $('sa_pack');
if (pack && pack.value === 'write_prompt') {
pack.value = 'critique_image';
saveSettings();
}
},
'Open Assistent with this image (vision / critique / prompt help)',
['image'],
true,
true,
);
}
function wire() {
if (!$('swarm_assistent_root')) {
return;
}
if (typeof genericRequest !== 'function') {
setTimeout(wire, 300);
return;
}
if (window.__swarmAssistentWired) {
return;
}
window.__swarmAssistentWired = true;
loadSettings();
updateGate();
if (wantsAutoVision()) {
refreshImagePreview();
}
refreshModels();
refreshInventory();
wireDropZone();
wireSplitter();
registerSendButton();
$('sa_btn_settings')?.addEventListener('click', () => {
const s = $('sa_settings');
if (s) {
s.hidden = !s.hidden;
}
});
$('sa_btn_refresh_models')?.addEventListener('click', () => {
saveSettings();
refreshModels();
});
$('sa_btn_refresh_inventory')?.addEventListener('click', () => refreshInventory());
$('sa_btn_use_current')?.addEventListener('click', () => {
const src = findCurrentGenerateSrc({ allowPreview: true });
if (src) {
setImageFromSrc(src, { note: 'Using current Generate image' });
} else {
setStatus('No current Generate image');
}
});
$('sa_btn_as_init')?.addEventListener('click', async () => {
const src = state.lastImageDataUrl || findCurrentGenerateSrc();
if (!src) {
setStatus('No vision/Generate image for Init');
return;
}
if (!state.lastImageDataUrl) {
setImageFromSrc(src);
}
await setInitFromSrc(src);
const pack = $('sa_pack');
if (pack && pack.value === 'write_prompt') {
pack.value = 'inpaint_edit';
saveSettings();
}
});
$('sa_btn_as_mask')?.addEventListener('click', async () => {
const src = state.lastImageDataUrl || findCurrentGenerateSrc();
if (!src) {
setStatus('No vision image for Mask');
return;
}
await setMaskFromSrc(src);
const pack = $('sa_pack');
if (pack) {
pack.value = 'inpaint_edit';
saveSettings();
}
});
$('sa_btn_clear_init')?.addEventListener('click', clearInitAndMask);
$('sa_btn_clear_image')?.addEventListener('click', clearVisionImage);
$('sa_btn_send')?.addEventListener('click', () => sendChat());
$('sa_btn_interrupt')?.addEventListener('click', () => {
doInterruptNow();
state.busy = false;
state.generating = false;
setInterruptVisible(false);
if (state.streamEl?.classList.contains('sa-typing')) {
state.streamEl.remove();
state.streamEl = null;
}
stopBusyUi('Interrupted');
});
$('sa_btn_clear')?.addEventListener('click', () => {
state.history = [];
state.critiqueHopUsed = false;
const box = $('sa_messages');
if (box) {
box.innerHTML = '';
const empty = document.createElement('div');
empty.className = 'sa-chat-empty';
empty.id = 'sa_chat_empty';
empty.innerHTML = 'Collaborative Krea 2
Write a prompt, drop a reference on the left, or send the current Generate image.
';
box.appendChild(empty);
}
stopBusyUi('');
setStatus('');
});
$('sa_input')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey && !e.altKey) {
e.preventDefault();
sendChat();
}
});
$('sa_base_url')?.addEventListener('change', saveSettings);
$('sa_model')?.addEventListener('change', saveSettings);
$('sa_pack')?.addEventListener('change', saveSettings);
$('sa_auto_vision')?.addEventListener('change', saveSettings);
$('sa_auto_apply')?.addEventListener('change', saveSettings);
$('sa_auto_generate')?.addEventListener('change', saveSettings);
$('sa_auto_critique')?.addEventListener('change', saveSettings);
$('sa_auto_download')?.addEventListener('change', saveSettings);
setInterval(updateGate, 2000);
setInterval(() => refreshImagePreview({ onlyIfEmpty: true }), 5000);
setInterval(() => {
if (!state.busy) {
refreshInventory();
}
}, 120000);
window.swarmAssistent = {
setImageFromSrc,
clearVisionImage,
openAssistentTab,
sendToAssistent: (src) => setImageFromSrc(src, { switchTab: true, note: 'Image sent to Assistent' }),
isKreaSelected,
resolveCurrentCheckpoint,
refreshInventory,
applyPatch,
triggerGenerate,
setInitFromSrc,
setMaskFromSrc,
clearInitAndMask,
};
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', wire);
} else {
wire();
}
})();