/** * 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 LS_PANE_WIDTH = 'swarm_assistent_pane_width'; const TAB_BUTTON_ID = 'maintab_assistent'; const state = { history: [], packsLoaded: false, busy: false, lastImageDataUrl: null, preferredModel: null, dragDepth: 0, }; function $(id) { return document.getElementById(id); } function setStatus(text) { const el = $('sa_status'); if (el) { el.textContent = text || ''; } } function looksLikeKrea(text) { const s = String(text || ''); return /krea\s*2|krea2|krea-2/i.test(s) || /krea/i.test(s); } /** Resolve current checkpoint from modern SwarmUI APIs (getCurrentModel is gone). */ function resolveCurrentCheckpoint() { const out = { name: null, architecture: null, compat_class: null, title: null, class: null, source: null, }; try { if (typeof currentModelHelper !== 'undefined' && currentModelHelper) { out.name = currentModelHelper.curModel || null; out.architecture = currentModelHelper.curArch || null; out.compat_class = currentModelHelper.curCompatClass || null; out.source = 'currentModelHelper'; } } catch (e) { /* ignore */ } try { if (typeof getCurrentModel === 'function') { const model = getCurrentModel(); if (model) { out.name = out.name || model.name || null; out.title = model.title || null; out.architecture = out.architecture || model.architecture || null; out.class = model.class || null; out.compat_class = out.compat_class || model.compat_class || null; out.source = out.source || 'getCurrentModel'; } } } catch (e) { /* ignore */ } try { const sel = document.getElementById('current_model') || document.getElementById('input_model'); if (sel) { const opt = sel.selectedOptions && sel.selectedOptions[0]; const hint = [ sel.value, opt && opt.text, opt && opt.dataset && opt.dataset.cleanname, ] .filter(Boolean) .join(' '); if (!out.name && sel.value) { out.name = sel.value; out.source = out.source || 'dropdown'; } if (hint && !out.architecture) { out.title = out.title || hint; } } } catch (e) { /* ignore */ } return out; } function isKreaSelected() { try { const m = resolveCurrentCheckpoint(); const blob = [ m.architecture, m.compat_class, m.title, m.name, m.class, ].join(' '); return looksLikeKrea(blob); } catch (e) { return false; } } function updateGate() { const ok = isKreaSelected(); const gate = $('sa_gate'); const layout = $('sa_layout'); if (gate) { gate.hidden = ok; if (!ok) { const m = resolveCurrentCheckpoint(); const seen = [m.architecture, m.compat_class, m.name] .filter(Boolean) .join(' · '); const p = gate.querySelector('p'); if (p) { p.innerHTML = seen ? `Swarm Assistent is for Krea 2 models only. Current: ${escapeHtml( seen, )} — pick a checkpoint with architecture krea-2.` : 'Swarm Assistent is for Krea 2 models only. Select a Krea 2 checkpoint on Generate to enable the chat.'; } } } if (layout) { layout.classList.toggle('sa-disabled', !ok); } return ok; } function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } 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 openAssistentTab() { const tab = document.getElementById(TAB_BUTTON_ID); if (tab) { tab.click(); return true; } // Fallback: try hash / pane const pane = document.getElementById('assistent'); if (pane && typeof bootstrap !== 'undefined' && bootstrap.Tab) { try { bootstrap.Tab.getOrCreateInstance(tab || pane).show(); } catch (e) { /* ignore */ } } return !!tab; } function flashImagePane() { const frame = $('sa_image_frame'); if (!frame) { return; } frame.classList.remove('sa-flash'); // reflow void frame.offsetWidth; frame.classList.add('sa-flash'); } function setImageFromSrc(src, { switchTab = false, note = null } = {}) { if (!src) { return false; } const cleaned = String(src).trim().split(/\s+/)[0]; if (!cleaned || cleaned.startsWith('#')) { return false; } state.lastImageDataUrl = cleaned; const img = $('sa_image_preview'); const empty = $('sa_image_empty'); const chip = $('sa_vision_chip'); const frame = $('sa_image_frame'); if (img) { img.src = cleaned; img.hidden = false; } if (empty) { empty.hidden = true; } if (chip) { chip.hidden = false; } if (frame) { frame.classList.add('sa-has-image'); } if (switchTab) { openAssistentTab(); } flashImagePane(); if (note) { setStatus(note); } return true; } function clearVisionImage({ silent = false } = {}) { state.lastImageDataUrl = null; const img = $('sa_image_preview'); const empty = $('sa_image_empty'); const chip = $('sa_vision_chip'); const frame = $('sa_image_frame'); if (img) { img.removeAttribute('src'); img.hidden = true; } if (empty) { empty.hidden = false; } if (chip) { chip.hidden = true; } if (frame) { frame.classList.remove('sa-has-image'); } if (!silent) { setStatus('Vision image cleared'); } } 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 = 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 */ } 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)) { if (!/lora/i.test(JSON.stringify(m).slice(0, 200))) { 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 */ } 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); } 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 findCurrentGenerateSrc() { 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) { return cur.dataset?.src || cur.src || null; } } catch (e) { /* ignore */ } try { if (typeof currentMetadataMap !== 'undefined' && currentMetadataMap && currentMetadataMap.image) { return currentMetadataMap.image; } } catch (e) { /* ignore */ } return null; } function refreshImagePreview({ onlyIfEmpty = false } = {}) { if (onlyIfEmpty && state.lastImageDataUrl) { return; } const src = findCurrentGenerateSrc(); if (src) { setImageFromSrc(src); } else if (!state.lastImageDataUrl) { clearVisionImage({ silent: true }); } } 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' }); } } // HTML img 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) { if (!src) { return null; } 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); 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 (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'); } 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; } if (!state.lastImageDataUrl) { 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]; } } 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; 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 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); }); // Also accept drops on the whole layout (easier target) 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); }); frame.addEventListener('paste', async (e) => { 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; } } }); // Global paste when Assistent tab is visible 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')) { // Still allow image paste over text fields when clipboard has image 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') { // SwarmUI core not ready yet — retry briefly setTimeout(registerSendButton, 500); return; } // Avoid duplicate registrations on hot-reload if (window.__swarmAssistentMediaRegistered) { return; } window.__swarmAssistentMediaRegistered = true; registerMediaButton( 'Send to Assistent', (src) => { setImageFromSrc(src, { switchTab: true, note: 'Image sent to Assistent', }); // Prefer critique pack when coming from an image 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; } loadSettings(); updateGate(); refreshImagePreview(); refreshModels(); 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_use_current')?.addEventListener('click', () => { const src = findCurrentGenerateSrc(); if (src) { setImageFromSrc(src, { note: 'Using current Generate image' }); } else { setStatus('No current Generate image'); } }); $('sa_btn_clear_image')?.addEventListener('click', clearVisionImage); $('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.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); setInterval(updateGate, 2000); // Soft sync from Generate only when we have no pinned vision image setInterval(() => refreshImagePreview({ onlyIfEmpty: true }), 5000); // Expose for console / other extensions window.swarmAssistent = { setImageFromSrc, clearVisionImage, openAssistentTab, sendToAssistent: (src) => setImageFromSrc(src, { switchTab: true, note: 'Image sent to Assistent' }), isKreaSelected, resolveCurrentCheckpoint, }; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', wire); } else { wire(); } })();