/** * Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API). */ (function () { const LS_BASE = 'swarm_assistent_base_url'; const LS_MODEL = 'swarm_assistent_model'; const LS_PACK = 'swarm_assistent_pack'; const LS_AUTO_VISION = 'swarm_assistent_auto_vision'; const state = { history: [], packsLoaded: false, busy: false, lastImageDataUrl: null, }; function $(id) { return document.getElementById(id); } function setStatus(text) { const el = $('sa_status'); if (el) { el.textContent = text || ''; } } function isKreaSelected() { try { const model = getCurrentModel && getCurrentModel(); if (!model) { return false; } const arch = `${model.architecture || ''} ${model.title || ''} ${model.name || ''} ${model.class || ''}`; return /krea\s*2|krea2/i.test(arch) || /krea/i.test(arch); } catch (e) { return false; } } function updateGate() { const ok = isKreaSelected(); const gate = $('sa_gate'); const layout = $('sa_layout'); if (gate) { gate.hidden = ok; } if (layout) { layout.classList.toggle('sa-disabled', !ok); } return ok; } function val(id) { const el = document.getElementById(id); return el ? el.value : ''; } function setVal(id, value) { const el = document.getElementById(id); if (!el) { return; } el.value = value; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } function collectLiveContext() { 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, selected_loras: [], available_loras: [], has_vision_image: !!state.lastImageDataUrl, }; try { const model = getCurrentModel && getCurrentModel(); if (model) { ctx.checkpoint = { name: model.name || model.title || null, architecture: model.architecture || 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 */ } 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 arch = `${m.architecture || ''} ${m.class || ''} ${m.name || ''} ${m.title || ''}`; const isLora = /lora/i.test(m.category || m.type || '') || (m.name && String(m.name).toLowerCase().includes('lora')); const folder = `${m.folder || m.path || ''}`; const inLoraFolder = /lora/i.test(folder); if (!(isLora || inLoraFolder)) { // Still include if metadata says lora if (!/lora/i.test(JSON.stringify(m).slice(0, 200))) { continue; } } // Prefer Krea-tagged or unknown; skip obvious FLUX/SDXL-only names when tagged 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, }); } // Cap list size for context window if (ctx.available_loras.length > 80) { ctx.available_loras = ctx.available_loras.slice(0, 80); } } catch (e) { /* ignore */ } // Fallback: parse multi-select input_loras options as available names try { const sel = document.getElementById('input_loras'); if (sel && sel.options && ctx.available_loras.length === 0) { for (const opt of sel.options) { if (opt.value) { ctx.available_loras.push({ name: opt.value, title: opt.text || opt.value, trigger_phrase: null }); } } } } catch (e) { /* ignore */ } return ctx; } 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 (obj && typeof obj === 'object' && (obj.prompt != null || obj.loras || obj.width || obj.height || obj.steps || obj.cfg)) { 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 applyPatch(patch, which) { if (!patch) { return; } const doPrompt = !which || which === 'all' || which === 'prompt'; const doLoras = !which || which === 'all' || which === 'loras'; const doSize = !which || which === 'all' || which === 'size'; 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); } // Ensure triggers present 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 (doSize) { 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)); } } } setStatus('Applied patch'); } function appendMessage(role, text, patch) { const box = $('sa_messages'); if (!box) { return; } 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'], ['Size', 'size'], ]) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'basic-button'; btn.textContent = label; btn.addEventListener('click', () => applyPatch(finalPatch, which)); actions.appendChild(btn); } wrap.appendChild(actions); div.appendChild(wrap); } box.appendChild(div); box.scrollTop = box.scrollHeight; } function refreshImagePreview() { let src = null; try { const cur = document.getElementById('current_image_img') || document.querySelector('#current_image img') || document.querySelector('.current-image img'); if (cur && cur.src) { src = cur.src; } } catch (e) { /* ignore */ } try { if (!src && typeof currentMetadataMap !== 'undefined' && currentMetadataMap && currentMetadataMap.image) { src = currentMetadataMap.image; } } catch (e) { /* ignore */ } const img = $('sa_image_preview'); const empty = $('sa_image_empty'); if (src) { state.lastImageDataUrl = src; if (img) { img.src = src; img.hidden = false; } if (empty) { empty.hidden = true; } } else { state.lastImageDataUrl = null; if (img) { img.hidden = true; } if (empty) { empty.hidden = false; } } } async function imageToBase64ForOllama(src) { if (!src) { return null; } // Already data URL if (src.startsWith('data:')) { const i = src.indexOf(','); return i >= 0 ? src.slice(i + 1) : null; } try { const resp = await fetch(src); const blob = await resp.blob(); return await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const data = String(reader.result || ''); const i = data.indexOf(','); resolve(i >= 0 ? data.slice(i + 1) : null); }; 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); 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 (model) { state.preferredModel = model; } } 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'); } function refreshModels() { const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434'; setStatus('Loading models…'); genericRequest('AssistentListModels', { baseUrl }, (data) => { if (data.error) { setStatus(data.error); appendMessage('error', data.error); return; } const sel = $('sa_model'); if (!sel) { return; } sel.innerHTML = ''; const models = data.models || []; for (const name of models) { if (!name) { continue; } const opt = document.createElement('option'); opt.value = name; opt.textContent = name; sel.appendChild(opt); } const prefer = state.preferredModel || localStorage.getItem(LS_MODEL); if (prefer && models.includes(prefer)) { sel.value = prefer; } setStatus(models.length ? `${models.length} models` : 'No Ollama models'); saveSettings(); }); } async function sendChat() { 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; } refreshImagePreview(); const attach = ($('sa_attach_vision')?.checked || $('sa_auto_vision')?.checked) && state.lastImageDataUrl; let images = null; if (attach) { setStatus('Encoding image…'); const b64 = await imageToBase64ForOllama(state.lastImageDataUrl); if (b64) { images = [b64]; } } const userMsg = { role: 'user', content: text }; if (images) { userMsg.images = images; } state.history.push({ role: 'user', content: text }); appendMessage('user', text); $('sa_input').value = ''; const context = collectLiveContext(); context.has_vision_image = !!images; // Strip huge fields from history replay — only send recent turns without images in history payload const messages = state.history.slice(-12).map((m) => ({ role: m.role, content: m.content })); // Last user message may include images if (images && messages.length) { messages[messages.length - 1].images = images; } state.busy = true; setStatus('Thinking…'); saveSettings(); const payload = { baseUrl: $('sa_base_url')?.value || 'http://127.0.0.1:11434', model, pack, includeBase: true, raw: { messages, context_json: JSON.stringify(context), pack, base_url: $('sa_base_url')?.value || 'http://127.0.0.1:11434', model, }, }; genericRequest('AssistentChat', payload, (data) => { state.busy = false; if (data.error) { setStatus(data.error); appendMessage('error', data.error); return; } const reply = data.reply || ''; state.history.push({ role: 'assistant', content: reply }); appendMessage('assistant', reply); setStatus('Done'); }); } function wire() { if (!$('swarm_assistent_root')) { return; } loadSettings(); updateGate(); refreshImagePreview(); refreshModels(); $('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_image')?.addEventListener('click', refreshImagePreview); $('sa_btn_send')?.addEventListener('click', () => sendChat()); $('sa_btn_clear')?.addEventListener('click', () => { state.history = []; const box = $('sa_messages'); if (box) { box.innerHTML = ''; } setStatus(''); }); $('sa_input')?.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { 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); // Re-check Krea gate when user may swap models setInterval(updateGate, 2000); setInterval(refreshImagePreview, 4000); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', wire); } else { wire(); } })();