diff --git a/Assets/assistent.css b/Assets/assistent.css index 3c24deb..ee7166c 100644 --- a/Assets/assistent.css +++ b/Assets/assistent.css @@ -24,12 +24,46 @@ } .sa-image-pane { - flex: 0 0 var(--sa-image-width, 34%); - max-width: 40%; - min-width: 11rem; + flex: 0 0 var(--sa-image-width, 42%); + max-width: 56%; + min-width: 14rem; display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.45rem; +} + +.sa-board-head { + display: flex; + align-items: baseline; + gap: 0.45rem; + min-height: 1.6rem; +} + +.sa-board-title { + font-weight: 650; + letter-spacing: 0.04em; + text-transform: uppercase; + font-size: 0.78rem; +} + +.sa-board-hint { + flex: 1; + font-size: 0.75rem; + opacity: 0.55; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sa-board { + flex: 1; + display: grid; + grid-template-columns: 1fr 1fr; + grid-auto-rows: minmax(7.5rem, 1fr); + gap: 0.4rem; + min-height: 16rem; + outline: none; } .sa-splitter { @@ -47,6 +81,123 @@ background: color-mix(in srgb, currentColor 35%, transparent); } +.sa-slot { + position: relative; + display: flex; + align-items: center; + justify-content: center; + border: 1px dashed color-mix(in srgb, currentColor 28%, transparent); + border-radius: 0.5rem; + overflow: hidden; + background: + radial-gradient(ellipse at 30% 20%, color-mix(in srgb, currentColor 8%, transparent), transparent 55%), + color-mix(in srgb, currentColor 4%, transparent); + min-height: 7.5rem; + cursor: pointer; + outline: none; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.sa-slot.sa-has-image { + border-style: solid; +} + +.sa-slot.sa-selected { + box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 45%, transparent); + border-color: color-mix(in srgb, currentColor 55%, transparent); +} + +.sa-slot.sa-dragover { + border-color: color-mix(in srgb, currentColor 70%, transparent); + box-shadow: inset 0 0 0 2px color-mix(in srgb, currentColor 25%, transparent); +} + +.sa-slot.sa-slot-gen { + grid-column: 1 / -1; + min-height: 10rem; +} + +.sa-board.sa-board-many .sa-slot.sa-slot-gen { + grid-column: auto; + min-height: 7.5rem; +} + +.sa-slot img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + display: block; +} + +.sa-slot-bar { + position: absolute; + top: 0.3rem; + left: 0.3rem; + right: 0.3rem; + display: flex; + align-items: center; + gap: 0.25rem; + z-index: 2; + pointer-events: none; +} + +.sa-slot-bar > * { + pointer-events: auto; +} + +.sa-slot-chip { + padding: 0.12rem 0.4rem; + border-radius: 999px; + font-size: 0.68rem; + font-weight: 650; + letter-spacing: 0.04em; + text-transform: uppercase; + background: color-mix(in srgb, currentColor 18%, transparent); + border: 1px solid color-mix(in srgb, currentColor 28%, transparent); +} + +.sa-slot-chip.sa-live { + background: color-mix(in srgb, #6ee7a8 28%, transparent); +} + +.sa-slot-attach { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.2rem; + font-size: 0.7rem; + opacity: 0.85; + background: color-mix(in srgb, currentColor 14%, transparent); + border-radius: 999px; + padding: 0.08rem 0.35rem; +} + +.sa-slot-busy { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: color-mix(in srgb, #000 35%, transparent); + z-index: 3; +} + +.sa-add-cell { + border: 1px dashed color-mix(in srgb, currentColor 22%, transparent); + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + opacity: 0.55; + cursor: pointer; + font-size: 0.85rem; + font-weight: 600; +} + +.sa-add-cell:hover { + opacity: 0.9; +} + .sa-image-frame { position: relative; flex: 1; @@ -427,6 +578,51 @@ margin-top: 0.45rem; } +.sa-patch-stale { + margin-top: 0.4rem; + font-size: 0.78rem; + opacity: 0.5; + font-style: italic; +} + +.sa-btn-gen { + display: inline-flex; + align-items: center; + gap: 0.4rem; +} + +.sa-btn-gen:disabled { + opacity: 0.65; + pointer-events: none; +} + +.sa-spinner-btn { + width: 0.7rem; + height: 0.7rem; + border-width: 2px; +} + +.sa-msg.sa-welcome { + align-self: stretch; + max-width: 100%; + background: color-mix(in srgb, currentColor 6%, transparent); + border: 1px solid color-mix(in srgb, currentColor 16%, transparent); +} + +.sa-welcome-title { + font-weight: 650; + margin-bottom: 0.35rem; +} + +.sa-welcome ul { + margin: 0.2rem 0 0; + padding-left: 1.1rem; +} + +.sa-welcome li { + margin: 0.15rem 0; +} + .sa-danger { opacity: 0.85; } @@ -534,9 +730,12 @@ .sa-image-pane { flex: 0 0 auto; max-width: none; - max-height: 38%; + max-height: 46%; width: 100% !important; } + .sa-slot.sa-slot-gen { + grid-column: auto; + } .sa-splitter { display: none; } diff --git a/Assets/assistent.js b/Assets/assistent.js index d52b92d..9214f4b 100644 --- a/Assets/assistent.js +++ b/Assets/assistent.js @@ -1,6 +1,6 @@ /** * Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API). - * v0.3.4: do not auto-fill Vision from the checkpoint preview. + * v0.4.0: multi-window board, look_at slots, latest-patch buttons, first-run welcome. */ (function () { const LS_BASE = 'swarm_assistent_base_url'; @@ -12,7 +12,20 @@ const LS_AUTO_CRITIQUE = 'swarm_assistent_auto_critique'; const LS_AUTO_DOWNLOAD = 'swarm_assistent_auto_download'; const LS_PANE_WIDTH = 'swarm_assistent_pane_width'; + const LS_WELCOMED = 'swarm_assistent_welcomed'; const TAB_BUTTON_ID = 'maintab_assistent'; + const GEN_ID = 'generate'; + const MAX_REF_SLOTS = 4; + + const WELCOME_HTML = ` +
Assistent · Krea 2
+ + Напиши, что сгенерировать — или кинь референс и попроси правку.`; const state = { history: [], @@ -25,10 +38,14 @@ inventory: { loras: [], checkpoints: [], wildcards: [], has_civitai_key: false }, streamEl: null, critiqueHopUsed: false, + visionHopUsed: false, busyPhase: 'idle', busyStarted: 0, gotDelta: false, busyTimer: null, + slots: [], + selectedSlotId: 'ref1', + refSeq: 1, }; function $(id) { @@ -69,6 +86,8 @@ function setBusyPhase(phase) { state.busyPhase = phase || 'thinking'; tickBusyUi(); + syncPatchActionAvailability(); + syncGenerateBusy(); } function tickBusyUi() { @@ -129,6 +148,8 @@ dot.hidden = false; } tickBusyUi(); + syncPatchActionAvailability(); + syncGenerateBusy(); if (state.busyTimer) { clearInterval(state.busyTimer); } @@ -168,6 +189,8 @@ const suffix = elapsed >= 1000 ? ` · ${fmtElapsed(elapsed)}` : ''; setStatus(finalStatus + suffix); } + syncPatchActionAvailability(); + syncGenerateBusy(); } function setStatus(text) { @@ -329,76 +352,519 @@ return !!tab; } - function flashImagePane() { - const frame = $('sa_image_frame'); - if (!frame) { + function flashImagePane(slotId) { + const el = document.querySelector(`.sa-slot[data-id="${slotId || state.selectedSlotId}"]`); + if (!el) { return; } - frame.classList.remove('sa-flash'); - void frame.offsetWidth; - frame.classList.add('sa-flash'); + el.classList.remove('sa-flash'); + void el.offsetWidth; + el.classList.add('sa-flash'); } - function setImageFromSrc(src, { switchTab = false, note = null } = {}) { - if (!src) { + function ensureBoard() { + if (state.slots.length) { + return; + } + state.slots = [ + { id: GEN_ID, type: 'generate', label: 'Generate', src: null, attach: false }, + { id: 'ref1', type: 'ref', label: 'Ref 1', src: null, attach: true }, + ]; + state.refSeq = 1; + state.selectedSlotId = 'ref1'; + } + + function slotById(id) { + ensureBoard(); + const key = normalizeSlotId(id); + return state.slots.find((s) => s.id === key) || null; + } + + function generateSlot() { + return slotById(GEN_ID); + } + + function refSlots() { + ensureBoard(); + return state.slots.filter((s) => s.type === 'ref'); + } + + function normalizeSlotId(id) { + const raw = String(id || '').trim().toLowerCase(); + if (!raw) { + return ''; + } + if (raw === 'gen' || raw === 'current' || raw === 'live' || raw === 'generation') { + return GEN_ID; + } + if (raw === 'selected' || raw === 'sel') { + return state.selectedSlotId; + } + const m = raw.match(/^ref\s*[_-]?\s*(\d+)$/); + if (m) { + return `ref${m[1]}`; + } + return raw; + } + + function selectedSlot() { + return slotById(state.selectedSlotId) || generateSlot(); + } + + function selectedSrc() { + return selectedSlot()?.src || null; + } + + function syncLastImageAlias() { + const attached = attachableSlots(); + state.lastImageDataUrl = (attached[0] || selectedSlot() || generateSlot())?.src || null; + } + + function attachableSlots() { + ensureBoard(); + return state.slots.filter((s) => s.attach && s.src); + } + + function setSlotSrc(id, src, { select = true, attach = null, note = null, switchTab = false } = {}) { + const slot = slotById(id); + if (!slot) { return false; } - const cleaned = String(src).trim().split(/\s+/)[0]; - if (!cleaned || cleaned.startsWith('#')) { + const cleaned = src ? String(src).trim().split(/\s+/)[0] : null; + 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; + slot.src = cleaned || null; + if (attach != null) { + slot.attach = !!attach; + } else if (slot.type === 'ref' && slot.src) { + slot.attach = true; } - if (empty) { - empty.hidden = true; - } - if (chip) { - chip.hidden = false; - } - if (frame) { - frame.classList.add('sa-has-image'); + if (select) { + state.selectedSlotId = slot.id; } + syncLastImageAlias(); + renderBoard(); + flashImagePane(slot.id); 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; + function addRefSlot({ src = null, select = true } = {}) { + ensureBoard(); + if (refSlots().length >= MAX_REF_SLOTS) { + setStatus(`Max ${MAX_REF_SLOTS} reference windows`); + const empty = refSlots().find((s) => !s.src); + if (empty && src) { + return setSlotSrc(empty.id, src, { select, note: `Loaded into ${empty.label}` }); + } + return empty || null; } - if (empty) { - empty.hidden = false; + state.refSeq += 1; + const id = `ref${state.refSeq}`; + const slot = { + id, + type: 'ref', + label: `Ref ${state.refSeq}`, + src: src || null, + attach: !!src, + }; + state.slots.push(slot); + if (select) { + state.selectedSlotId = id; } - if (chip) { - chip.hidden = true; + renderBoard(); + return slot; + } + + function clearSlot(id, { silent = false } = {}) { + const slot = slotById(id); + if (!slot) { + return; } - if (frame) { - frame.classList.remove('sa-has-image'); + if (slot.type === 'generate') { + if (!silent) { + setStatus('Generate window is live — use Snapshot gen to copy it'); + } + return; } + slot.src = null; + slot.attach = true; + syncLastImageAlias(); + renderBoard(); if (!silent) { - setStatus('Vision image cleared'); + setStatus(`${slot.label} cleared`); } } + function snapshotGenerateToRef() { + const src = generateSlot()?.src || findCurrentGenerateSrc({ allowPreview: true }); + if (!src) { + setStatus('No current Generate image'); + return false; + } + const empty = refSlots().find((s) => !s.src); + if (empty) { + return setSlotSrc(empty.id, src, { note: `Snapshot → ${empty.label}` }); + } + const created = addRefSlot({ src, select: true }); + if (created?.src) { + setStatus(`Snapshot → ${created.label}`); + flashImagePane(created.id); + return true; + } + const last = refSlots()[refSlots().length - 1]; + if (last) { + return setSlotSrc(last.id, src, { note: `Snapshot → ${last.label} (replaced)` }); + } + return false; + } + + function putImageOnBoard(src, { note = null, switchTab = false, preferSelected = true } = {}) { + if (!src) { + return false; + } + ensureBoard(); + const sel = selectedSlot(); + if (preferSelected && sel && sel.type === 'ref') { + return setSlotSrc(sel.id, src, { note: note || `Loaded into ${sel.label}`, switchTab }); + } + const empty = refSlots().find((s) => !s.src); + if (empty) { + return setSlotSrc(empty.id, src, { note: note || `Loaded into ${empty.label}`, switchTab }); + } + const created = addRefSlot({ src, select: true }); + if (created) { + if (switchTab) { + openAssistentTab(); + } + if (note) { + setStatus(note); + } + return true; + } + return false; + } + + function setImageFromSrc(src, opts = {}) { + return putImageOnBoard(src, opts); + } + + function clearVisionImage(opts) { + clearSlot(state.selectedSlotId, opts); + } + + function slotCatalog() { + ensureBoard(); + return state.slots.map((s) => ({ + id: s.id, + type: s.type, + label: s.label, + has_image: !!s.src, + attach: !!s.attach, + selected: s.id === state.selectedSlotId, + })); + } + + function lookAtIdsFromPatch(patch) { + if (!patch) { + return []; + } + const raw = patch.look_at || patch.vision_from || patch.vision_slots; + const list = Array.isArray(raw) ? raw : (raw ? [raw] : []); + if (Array.isArray(patch.actions)) { + for (const a of patch.actions.map(String)) { + const m = a.match(/^look_at[_:]?(generate|ref\d+|selected)$/i); + if (m) { + list.push(m[1]); + } + } + } + return [...new Set(list.map(normalizeSlotId).filter(Boolean))]; + } + + function resolveSlotSrc(id) { + if (!id) { + return selectedSrc() || generateSlot()?.src || findCurrentGenerateSrc(); + } + const slot = slotById(id); + if (slot?.src) { + return slot.src; + } + if (normalizeSlotId(id) === GEN_ID) { + return findCurrentGenerateSrc(); + } + return null; + } + + function isGenerateUnavailable() { + if (state.generating || state.busy) { + return true; + } + try { + if (typeof mainGenHandler !== 'undefined' && mainGenHandler) { + if (mainGenHandler.isGenerating === true || mainGenHandler.running === true) { + return true; + } + } + } catch (e) { /* ignore */ } + const interrupt = document.getElementById('interrupt_button') + || document.getElementById('alt_interrupt_button'); + if (interrupt && !interrupt.hidden && interrupt.offsetParent !== null) { + return true; + } + const genBtn = document.getElementById('generate_button') || document.getElementById('alt_generate_button'); + if (genBtn && (genBtn.disabled || /interrupt/i.test(genBtn.textContent || ''))) { + return true; + } + return false; + } + + function syncGenerateBusy() { + const overlay = document.querySelector('.sa-slot-gen .sa-slot-busy'); + if (overlay) { + overlay.hidden = !state.generating && state.busyPhase !== 'generating'; + } + } + + function syncPatchActionAvailability() { + const bar = document.querySelector('.sa-patch-actions.sa-patch-current'); + if (!bar) { + return; + } + const locked = isGenerateUnavailable(); + bar.querySelectorAll('.sa-btn-gen').forEach((btn) => { + btn.disabled = locked; + let spin = btn.querySelector('.sa-spinner'); + if (locked) { + if (!spin) { + spin = document.createElement('span'); + spin.className = 'sa-spinner sa-spinner-btn'; + spin.setAttribute('aria-hidden', 'true'); + btn.prepend(spin); + } + } else if (spin) { + spin.remove(); + } + }); + } + + function retireStalePatchActions() { + document.querySelectorAll('.sa-patch-actions').forEach((el) => { + const note = document.createElement('div'); + note.className = 'sa-patch-stale'; + note.textContent = 'Superseded — use the latest proposal'; + el.replaceWith(note); + }); + } + + function mountPatchActions(parent, patch) { + if (!parent || !patch) { + return; + } + retireStalePatchActions(); + const wrap = parent.classList.contains('sa-patch') ? parent : null; + const host = wrap || parent; + const actions = document.createElement('div'); + actions.className = 'sa-patch-actions sa-patch-current'; + 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 sa-btn-gen'; + genBtn.textContent = 'Apply + Generate'; + genBtn.addEventListener('click', async () => { + if (isGenerateUnavailable()) { + return; + } + await applyPatch(patch, 'all'); + await runGenerateFromPatch({ ...patch, actions: ['generate'] }); + }); + actions.appendChild(genBtn); + host.appendChild(actions); + syncPatchActionAvailability(); + } + + function renderBoard() { + const board = $('sa_board'); + if (!board) { + return; + } + ensureBoard(); + const refs = refSlots(); + board.classList.toggle('sa-board-many', refs.some((s) => s.src) || refs.length > 1); + board.innerHTML = ''; + for (const slot of state.slots) { + board.appendChild(buildSlotEl(slot)); + } + if (refs.length < MAX_REF_SLOTS) { + const add = document.createElement('div'); + add.className = 'sa-add-cell'; + add.textContent = '+ Ref'; + add.title = 'Add a reference window'; + add.addEventListener('click', (e) => { + e.stopPropagation(); + addRefSlot({ select: true }); + }); + add.addEventListener('dragover', (e) => { + e.preventDefault(); + e.stopPropagation(); + }); + add.addEventListener('drop', async (e) => { + e.preventDefault(); + e.stopPropagation(); + const created = addRefSlot({ select: true }); + if (created) { + state.selectedSlotId = created.id; + await handleDropDataTransfer(e.dataTransfer, created.id); + } + }); + board.appendChild(add); + } + syncGenerateBusy(); + syncLastImageAlias(); + } + + function buildSlotEl(slot) { + const el = document.createElement('div'); + el.className = `sa-slot${slot.type === 'generate' ? ' sa-slot-gen' : ''}`; + el.dataset.id = slot.id; + if (slot.src) { + el.classList.add('sa-has-image'); + } + if (slot.id === state.selectedSlotId) { + el.classList.add('sa-selected'); + } + const bar = document.createElement('div'); + bar.className = 'sa-slot-bar'; + const chip = document.createElement('span'); + chip.className = `sa-slot-chip${slot.type === 'generate' ? ' sa-live' : ''}`; + chip.textContent = slot.type === 'generate' ? 'Generate' : slot.label; + bar.appendChild(chip); + const attachLab = document.createElement('label'); + attachLab.className = 'sa-slot-attach'; + attachLab.title = 'Attach this window to the next chat (vision)'; + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.checked = !!slot.attach; + cb.addEventListener('click', (e) => e.stopPropagation()); + cb.addEventListener('change', (e) => { + e.stopPropagation(); + slot.attach = cb.checked; + syncLastImageAlias(); + }); + attachLab.appendChild(cb); + attachLab.appendChild(document.createTextNode(' vision')); + bar.appendChild(attachLab); + el.appendChild(bar); + + if (slot.src) { + const img = document.createElement('img'); + img.alt = slot.label; + img.src = slot.src; + el.appendChild(img); + } else { + const empty = document.createElement('div'); + empty.className = 'sa-image-empty'; + empty.innerHTML = slot.type === 'generate' + ? '
Generate
Live view of the current generation
' + : '
Reference
Drop · paste · Snapshot gen
'; + el.appendChild(empty); + } + + const busy = document.createElement('div'); + busy.className = 'sa-slot-busy'; + busy.hidden = !(slot.type === 'generate' && (state.generating || state.busyPhase === 'generating')); + busy.innerHTML = ''; + el.appendChild(busy); + + el.addEventListener('click', () => { + state.selectedSlotId = slot.id; + renderBoard(); + }); + el.addEventListener('dragover', (e) => { + e.preventDefault(); + e.stopPropagation(); + el.classList.add('sa-dragover'); + if (e.dataTransfer) { + e.dataTransfer.dropEffect = 'copy'; + } + }); + el.addEventListener('dragleave', () => el.classList.remove('sa-dragover')); + el.addEventListener('drop', async (e) => { + e.preventDefault(); + e.stopPropagation(); + el.classList.remove('sa-dragover'); + const targetId = slot.type === 'generate' ? null : slot.id; + if (slot.type === 'generate') { + const created = addRefSlot({ select: true }); + await handleDropDataTransfer(e.dataTransfer, created?.id); + } else { + await handleDropDataTransfer(e.dataTransfer, targetId); + } + }); + return el; + } + + function syncGenerateSlot() { + const src = findCurrentGenerateSrc(); + const slot = generateSlot(); + if (!slot) { + return; + } + if (src && src !== slot.src) { + slot.src = src; + const img = document.querySelector('.sa-slot-gen img'); + const empty = document.querySelector('.sa-slot-gen .sa-image-empty'); + const frame = document.querySelector('.sa-slot-gen'); + if (img) { + img.src = src; + } else if (frame) { + renderBoard(); + return; + } + if (empty) { + empty.hidden = true; + } + frame?.classList.add('sa-has-image'); + } + syncGenerateBusy(); + syncPatchActionAvailability(); + } + + function maybeWelcome() { + if (localStorage.getItem(LS_WELCOMED) === '1') { + return; + } + if (!$('sa_messages')) { + return; + } + localStorage.setItem(LS_WELCOMED, '1'); + const box = $('sa_messages'); + hideChatEmpty(); + const div = document.createElement('div'); + div.className = 'sa-msg assistant sa-welcome'; + div.innerHTML = WELCOME_HTML; + box.appendChild(div); + box.scrollTop = box.scrollHeight; + } + function countPromptImages() { try { const box = document.getElementById('alt_prompt_textbox') || document.getElementById('input_prompt'); @@ -594,7 +1060,9 @@ 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_vision_image: attachableSlots().length > 0, + image_slots: slotCatalog(), + attached_slot_ids: attachableSlots().map((s) => s.id), has_civitai_key: !!inv.has_civitai_key, auto_apply: !!$('sa_auto_apply')?.checked, auto_generate: !!$('sa_auto_generate')?.checked, @@ -676,7 +1144,14 @@ obj.use_mask_image != null || obj.clear_mask_image != null || obj.mask_blur != null || - obj.mask_grow != null + obj.mask_grow != null || + obj.look_at != null || + obj.vision_from != null || + obj.vision_slots != null || + obj.slot_to_init != null || + obj.slot_to_mask != null || + obj.snapshot_generate != null || + obj.select_slot != null ); } @@ -851,21 +1326,37 @@ } } } - const src = state.lastImageDataUrl || findCurrentGenerateSrc(); + if (patch.select_slot) { + const id = normalizeSlotId(patch.select_slot); + if (slotById(id)) { + state.selectedSlotId = id; + renderBoard(); + } + } + if (patch.snapshot_generate) { + snapshotGenerateToRef(); + } + const initId = patch.slot_to_init || (patch.use_init_image || (Array.isArray(patch.actions) && patch.actions.map(String).includes('use_init')) ? state.selectedSlotId : null); + const maskId = patch.slot_to_mask || null; + const src = resolveSlotSrc(patch.slot_to_init) || selectedSrc() || findCurrentGenerateSrc(); const wantInit = patch.use_init_image === true + || !!patch.slot_to_init || (Array.isArray(patch.actions) && patch.actions.map(String).includes('use_init')); const wantMask = patch.use_mask_image === true + || !!patch.slot_to_mask || (Array.isArray(patch.actions) && patch.actions.map(String).includes('use_mask')); if (wantInit) { - if (src) { - await setInitFromSrc(src); + const initSrc = resolveSlotSrc(initId) || src; + if (initSrc) { + await setInitFromSrc(initSrc); } else { - setStatus('No image for Init — drop/Send to Assistent first'); + setStatus('No image for Init — drop a ref or wait for Generate'); } } if (wantMask) { - if (src) { - await setMaskFromSrc(src); + const maskSrc = resolveSlotSrc(maskId) || src; + if (maskSrc) { + await setMaskFromSrc(maskSrc); } else { setStatus('No image for Mask — drop a mask (white=edit) first'); } @@ -946,7 +1437,12 @@ stopBusyUi(src ? 'Generate done' : 'Generate finished (no new image detected)'); } if (src) { - setImageFromSrc(src, { note: 'Result → vision' }); + const gen = generateSlot(); + if (gen) { + gen.src = src; + renderBoard(); + } + setStatus('Generate done'); return src; } if (state.busy) { @@ -968,8 +1464,16 @@ if ($('sa_input')) { $('sa_input').value = 'Critique this result and improve the prompt for the next generation.'; } + const gen = generateSlot(); + if (gen) { + gen.attach = true; + if (imageSrc) { + gen.src = imageSrc; + } + renderBoard(); + } setStatus('Auto-critique…'); - await sendChat({ fromAutoCritique: true }); + await sendChat({ fromAutoCritique: true, forceSlotIds: [GEN_ID] }); } function appendMessage(role, text, patch, civitaiResults) { @@ -989,31 +1493,7 @@ 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); + mountPatchActions(wrap, finalPatch); div.appendChild(wrap); } if (civitaiResults && civitaiResults.length) { @@ -1076,31 +1556,7 @@ 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); + mountPatchActions(wrap, patch); el.appendChild(wrap); } if (civitaiResults && civitaiResults.length) { @@ -1259,17 +1715,19 @@ return src; } - function refreshImagePreview({ onlyIfEmpty = false } = {}) { - if (!wantsAutoVision()) { - return; - } - if (onlyIfEmpty && state.lastImageDataUrl) { - return; - } - const src = findCurrentGenerateSrc(); - if (src) { - setImageFromSrc(src); + function refreshImagePreview() { + if (wantsAutoVision()) { + const gen = generateSlot(); + if (gen) { + gen.attach = true; + const src = findCurrentGenerateSrc(); + if (src) { + gen.src = src; + } + renderBoard(); + } } + syncGenerateSlot(); } function fileToDataUrl(file) { @@ -1281,23 +1739,26 @@ }); } - async function acceptImageFile(file) { + async function acceptImageFile(file, slotId) { 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'}` }); + if (slotId) { + return setSlotSrc(slotId, dataUrl, { note: `Loaded ${file.name || 'image'}` }); + } + return putImageOnBoard(dataUrl, { note: `Loaded ${file.name || 'image'}` }); } - async function handleDropDataTransfer(dt) { + async function handleDropDataTransfer(dt, slotId) { 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); + return acceptImageFile(file, slotId); } } } @@ -1305,13 +1766,19 @@ 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' }); + if (slotId) { + return setSlotSrc(slotId, first, { note: 'Image from drag' }); + } + return putImageOnBoard(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' }); + if (slotId) { + return setSlotSrc(slotId, m[1], { note: 'Image from drag' }); + } + return putImageOnBoard(m[1], { note: 'Image from drag' }); } return false; } @@ -1522,7 +1989,8 @@ ); } - async function handleReplySideEffects(reply, civitaiResults, { fromAutoCritique } = {}) { + async function handleReplySideEffects(reply, civitaiResults, opts = {}) { + const { fromAutoCritique, fromVisionHop } = opts; const { patch } = extractPatch(reply); if (Array.isArray(patch?.actions) && patch.actions.map(String).includes('interrupt')) { doInterruptNow(); @@ -1534,6 +2002,12 @@ downloadCivitaiLoRA(pick, null); } } + if (patch && !fromVisionHop && !fromAutoCritique) { + const hopped = await maybeVisionHop(patch, opts.attachedSlotIds || []); + if (hopped) { + return; + } + } if (patch && $('sa_auto_apply')?.checked) { await applyPatch(patch, 'all'); if (!fromAutoCritique) { @@ -1545,6 +2019,44 @@ } } + async function maybeVisionHop(patch, attachedSlotIds) { + const ids = lookAtIdsFromPatch(patch); + if (!ids.length || state.visionHopUsed) { + return false; + } + const have = ids.map((id) => slotById(id)).filter((s) => s && s.src); + if (!have.length) { + const genSrc = findCurrentGenerateSrc(); + if (ids.includes(GEN_ID) && genSrc) { + const gen = generateSlot(); + if (gen) { + gen.src = genSrc; + have.push(gen); + } + } + } + if (!have.length) { + setStatus('look_at: those windows are empty'); + return false; + } + const already = new Set(attachedSlotIds || []); + const need = have.filter((s) => !already.has(s.id)); + if (!need.length) { + return false; + } + state.visionHopUsed = true; + for (const s of need) { + s.attach = true; + } + renderBoard(); + if ($('sa_input')) { + $('sa_input').value = `Look at board slots: ${need.map((s) => s.id).join(', ')}. Continue using these images.`; + } + setStatus(`Vision hop ← ${need.map((s) => s.label).join(', ')}`); + await sendChat({ fromVisionHop: true, forceSlotIds: need.map((s) => s.id) }); + return true; + } + async function sendChat(opts = {}) { if (state.busy) { return; @@ -1565,22 +2077,33 @@ 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 && !opts.fromVisionHop) { + state.critiqueHopUsed = false; + state.visionHopUsed = false; } - if (!opts.fromAutoCritique && !opts.fromDownload) { - state.critiqueHopUsed = false; + let wantedIds = (opts.forceSlotIds || []).map(normalizeSlotId).filter(Boolean); + if (!wantedIds.length) { + wantedIds = attachableSlots().map((s) => s.id); + } + if (wantsAutoVision() && generateSlot()?.src && !wantedIds.includes(GEN_ID)) { + wantedIds.push(GEN_ID); + } + const visionSlots = wantedIds.map((id) => slotById(id)).filter((s) => s && s.src); + let images = null; + if (visionSlots.length) { + startBusyUi('encoding'); + setStatus('Encoding image…'); + images = []; + for (const slot of visionSlots) { + const b64 = await imageToBase64ForOllama(slot.src); + if (b64) { + images.push(b64); + } + } + if (!images.length) { + images = null; + } } state.history.push({ role: 'user', content: text }); @@ -1589,6 +2112,7 @@ const context = collectLiveContext(); context.has_vision_image = !!images; + context.attached_slot_ids = visionSlots.map((s) => s.id); const messages = state.history.slice(-12).map((m) => ({ role: m.role, content: m.content })); if (images && messages.length) { messages[messages.length - 1].images = images; @@ -1622,7 +2146,10 @@ setInterruptVisible(state.generating); state.history.push({ role: 'assistant', content: reply }); stopBusyUi('Done'); - await handleReplySideEffects(reply, civitaiResults, opts); + await handleReplySideEffects(reply, civitaiResults, { + ...opts, + attachedSlotIds: visionSlots.map((s) => s.id), + }); }; const finishErr = (msg) => { @@ -1722,45 +2249,7 @@ } 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 board = $('sa_board'); const layout = $('sa_layout'); layout?.addEventListener('dragover', (e) => { if (e.dataTransfer?.types?.includes('Files') || e.dataTransfer?.types?.includes('text/uri-list')) { @@ -1771,9 +2260,21 @@ if (!e.dataTransfer) { return; } + if (e.target && e.target.closest && e.target.closest('.sa-slot, .sa-add-cell')) { + return; + } e.preventDefault(); await handleDropDataTransfer(e.dataTransfer); }); + board?.addEventListener('keydown', (e) => { + if (e.key === 'Delete' || e.key === 'Backspace') { + if (e.target && (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT')) { + return; + } + e.preventDefault(); + clearSlot(state.selectedSlotId); + } + }); document.addEventListener('paste', async (e) => { const pane = document.getElementById('assistent'); @@ -1804,7 +2305,8 @@ e.preventDefault(); const file = item.getAsFile(); if (file) { - await acceptImageFile(file); + const sel = selectedSlot(); + await acceptImageFile(file, sel && sel.type === 'ref' ? sel.id : null); } return; } @@ -1833,7 +2335,7 @@ } const rect = layout.getBoundingClientRect(); const x = e.clientX - rect.left; - const pct = Math.min(45, Math.max(18, (x / rect.width) * 100)); + const pct = Math.min(56, Math.max(22, (x / rect.width) * 100)); const value = `${pct}%`; document.documentElement.style.setProperty('--sa-image-width', value); localStorage.setItem(LS_PANE_WIDTH, value); @@ -1861,9 +2363,10 @@ registerMediaButton( 'Send to Assistent', (src) => { - setImageFromSrc(src, { + putImageOnBoard(src, { switchTab: true, note: 'Image sent to Assistent', + preferSelected: false, }); const pack = $('sa_pack'); if (pack && pack.value === 'write_prompt') { @@ -1892,9 +2395,13 @@ window.__swarmAssistentWired = true; loadSettings(); updateGate(); + ensureBoard(); + renderBoard(); + syncGenerateSlot(); if (wantsAutoVision()) { refreshImagePreview(); } + maybeWelcome(); refreshModels(); refreshInventory(); wireDropZone(); @@ -1912,23 +2419,14 @@ 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_add_ref')?.addEventListener('click', () => addRefSlot({ select: true })); + $('sa_btn_use_current')?.addEventListener('click', () => snapshotGenerateToRef()); $('sa_btn_as_init')?.addEventListener('click', async () => { - const src = state.lastImageDataUrl || findCurrentGenerateSrc(); + const src = selectedSrc() || findCurrentGenerateSrc(); if (!src) { - setStatus('No vision/Generate image for Init'); + setStatus('No selected/Generate image for Init'); return; } - if (!state.lastImageDataUrl) { - setImageFromSrc(src); - } await setInitFromSrc(src); const pack = $('sa_pack'); if (pack && pack.value === 'write_prompt') { @@ -1937,9 +2435,9 @@ } }); $('sa_btn_as_mask')?.addEventListener('click', async () => { - const src = state.lastImageDataUrl || findCurrentGenerateSrc(); + const src = selectedSrc(); if (!src) { - setStatus('No vision image for Mask'); + setStatus('Select a window with an image for Mask'); return; } await setMaskFromSrc(src); @@ -1950,13 +2448,15 @@ } }); $('sa_btn_clear_init')?.addEventListener('click', clearInitAndMask); - $('sa_btn_clear_image')?.addEventListener('click', clearVisionImage); + $('sa_btn_clear_image')?.addEventListener('click', () => clearSlot(state.selectedSlotId)); $('sa_btn_send')?.addEventListener('click', () => sendChat()); $('sa_btn_interrupt')?.addEventListener('click', () => { doInterruptNow(); state.busy = false; state.generating = false; setInterruptVisible(false); + syncGenerateBusy(); + syncPatchActionAvailability(); if (state.streamEl?.classList.contains('sa-typing')) { state.streamEl.remove(); state.streamEl = null; @@ -1966,13 +2466,14 @@ $('sa_btn_clear')?.addEventListener('click', () => { state.history = []; state.critiqueHopUsed = false; + state.visionHopUsed = 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.
'; + empty.innerHTML = '
Collaborative Krea 2
Write a prompt, drop refs on the board, or send Generate via Send to Assistent.
'; box.appendChild(empty); } stopBusyUi(''); @@ -1987,14 +2488,21 @@ $('sa_base_url')?.addEventListener('change', saveSettings); $('sa_model')?.addEventListener('change', saveSettings); $('sa_pack')?.addEventListener('change', saveSettings); - $('sa_auto_vision')?.addEventListener('change', saveSettings); + $('sa_auto_vision')?.addEventListener('change', () => { + saveSettings(); + const gen = generateSlot(); + if (gen) { + gen.attach = wantsAutoVision(); + renderBoard(); + } + }); $('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(syncGenerateSlot, 700); setInterval(() => { if (!state.busy) { refreshInventory(); @@ -2003,9 +2511,11 @@ window.swarmAssistent = { setImageFromSrc, + putImageOnBoard, clearVisionImage, + snapshotGenerateToRef, openAssistentTab, - sendToAssistent: (src) => setImageFromSrc(src, { switchTab: true, note: 'Image sent to Assistent' }), + sendToAssistent: (src) => putImageOnBoard(src, { switchTab: true, note: 'Image sent to Assistent', preferSelected: false }), isKreaSelected, resolveCurrentCheckpoint, refreshInventory, @@ -2014,6 +2524,8 @@ setInitFromSrc, setMaskFromSrc, clearInitAndMask, + slotById, + renderBoard, }; } diff --git a/Prompts/base_krea2.md b/Prompts/base_krea2.md index 2e58099..080e5ce 100644 --- a/Prompts/base_krea2.md +++ b/Prompts/base_krea2.md @@ -21,7 +21,8 @@ A JSON block named "Live SwarmUI context" is attached. Treat it as ground truth: - Respect current width/height/steps/cfg/seed/sigma_shift/sampler unless the user asks to change them or the pack is `fix_params`. - `wildcards` lists installed wildcard names (`__name__` syntax in prompts). - `prompt_image_count` > 0 means Prompt Images are attached — warn if they may dominate. -- **Init / inpaint:** `has_init_image`, `has_mask_image`, `init_creativity` (aka denoise, 0–1), `mask_blur`, `mask_grow`. `has_vision_image` is the Assistent pane reference (can become Init/Mask). +- **Init / inpaint:** `has_init_image`, `has_mask_image`, `init_creativity` (aka denoise, 0–1), `mask_blur`, `mask_grow`. +- **Board (image windows):** `image_slots` lists windows. `generate` is the live current generation (view only unless attached). `ref1`… are user references. `attached_slot_ids` / `has_vision_image` = which windows are actually sent as vision this turn. To look at a window that was not attached, emit `look_at`. ## Output contract (mandatory) @@ -47,6 +48,11 @@ A JSON block named "Live SwarmUI context" is attached. Treat it as ground truth: "clear_mask_image": false, "mask_blur": null, "mask_grow": null, + "look_at": ["generate"], + "slot_to_init": null, + "slot_to_mask": null, + "snapshot_generate": false, + "select_slot": null, "actions": ["generate"], "search_query": null, "notes": "one-line why" @@ -63,8 +69,8 @@ A JSON block named "Live SwarmUI context" is attached. Treat it as ground truth: ### Init image / inpaint -- **img2img:** set `use_init_image: true` (uses Assistent vision / current Generate image) and `init_creativity` (0 = almost copy, 1 = almost new). Typical edits: **0.25–0.45**; restyle: **0.5–0.7**. Alias `denoise` is accepted. -- **Inpaint:** needs Init + Mask. Set `use_init_image: true` and `use_mask_image: true` only when the vision pane holds a proper mask (white = edit, black = keep). If the user has not painted a mask, tell them to use Swarm **Edit Image** / paint a mask, or press **As Mask** with a prepared mask — do not invent pixel masks. +- **img2img:** set `use_init_image: true` and optionally `slot_to_init: "generate"` or `"ref1"` (uses that board window) plus `init_creativity` (0 = almost copy, 1 = almost new). Typical edits: **0.25–0.45**; restyle: **0.5–0.7**. Alias `denoise` is accepted. +- **Inpaint:** needs Init + Mask. Set `use_init_image: true` and `use_mask_image: true` only when a board window holds a proper mask (white = edit, black = keep). Point to it with `slot_to_mask`. If the user has not painted a mask, tell them to use Swarm **Edit Image** / paint a mask, or press **As Mask** — do not invent pixel masks. - `clear_init_image` / `clear_mask_image` to leave img2img mode. - Prompt Images ≠ Init Image. Prefer Init for structural edits; Prompt Images for style refs (and warn they can dominate). @@ -74,6 +80,9 @@ A JSON block named "Live SwarmUI context" is attached. Treat it as ground truth: - `"use_init"` / `"use_mask"` — same as the boolean flags (optional). - `"search_civitai"` — Civitai search; user must **Confirm** downloads. - `"interrupt"` — stop generation. +- `look_at: ["generate", "ref1"]` — ask the UI to send those board windows as vision (a follow-up hop). Use when you need to see a slot that was not attached. +- `slot_to_init` / `slot_to_mask` — which board id to copy into Swarm Init / Mask. +- `snapshot_generate: true` — copy the live Generate window into a new/empty Ref. - Pure Q&A with no prompt/param change: omit the JSON patch entirely (do not burn GPU). ### Auto-apply note diff --git a/Prompts/critique_image.md b/Prompts/critique_image.md index 8163e15..0385f15 100644 --- a/Prompts/critique_image.md +++ b/Prompts/critique_image.md @@ -1,6 +1,8 @@ # Mode: critique_image -Goal: look at the attached image (vision) and improve the next generation for **Krea 2**. +Goal: look at the attached / requested board image(s) and improve the next generation for **Krea 2**. + +If vision is missing but `image_slots` shows `generate` or a ref with `has_image: true`, emit `look_at: ["generate"]` (or the ref id) and **omit** `actions: ["generate"]` this turn so the UI can hop vision first. ## How to critique diff --git a/README.md b/README.md index 9b7c90e..f45c2bf 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,20 @@ # Swarm Assistent -SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + vision, LoRA/trigger awareness, applyable patches, **img2img / inpaint**, auto Generate, and Civitai search with Confirm. +SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + multi-window board (live Generate + refs), LoRA/trigger awareness, applyable patches, **img2img / inpaint**, auto Generate, and Civitai search with Confirm. ## Layout -- **Left:** vision reference (drop / paste / *Send to Assistent* from Generate) +- **Left — Board:** live **Generate** window + **Ref** windows (drop / paste / *Send to Assistent* / Snapshot gen). Per-window **vision** checkbox attaches that image to the next chat. - **Splitter:** drag to resize panes -- **Right (wider):** chat +- **Right (wider):** chat — first visit shows a short how-to. Only the **latest** JSON proposal keeps action buttons; Apply + Generate spins/disables while a generation is running. - **Top-right:** prompt pack + settings (Ollama URL, model, auto-apply / auto-generate) ## UX -- **Send to Assistent** under the current Generate image (and History) — loads vision + opens the Assistent tab -- Drag images from Generate/History or the OS onto the left pane -- Paste (`Ctrl+V`) while the Assistent tab is open -- **Use current** / **Clear** for the vision reference -- **As Init** / **As Mask** / **Clear Init** — wire the vision image into Swarm `Init Image` / `Mask Image` (img2img & inpaint) +- **Send to Assistent** under the current Generate image (and History) — adds a Ref window and opens the Assistent tab +- Drag / paste onto a Ref (drop on Generate snapshots into a new Ref) +- **Snapshot gen** copies the live Generate window into a Ref +- **As Init** / **As Mask** / **Clear Init** — selected window → Swarm `Init Image` / `Mask Image` - Enter sends; Shift+Enter newline - **Auto-apply** + **Auto-generate** (default on): patch from the LLM is applied and Generate runs when the patch changes prompt/params or includes `actions: ["generate"]` - Pure Q&A without a patch does **not** start Generate @@ -67,13 +66,18 @@ Live context (checkpoint, server inventory LoRAs + triggers, wildcards, current "use_init_image": true, "init_creativity": 0.45, "use_mask_image": false, + "look_at": ["generate"], + "slot_to_init": "generate", + "snapshot_generate": false, "actions": ["generate"], "search_query": null } ``` - `generate` — auto-generate after apply (when enabled) -- `use_init` / `use_mask` — set Assistent vision (or current Generate) as Init / Mask +- `look_at` — hop vision from named board windows (`generate`, `ref1`, …) +- `slot_to_init` / `slot_to_mask` — copy that window into Swarm Init / Mask +- `snapshot_generate` — copy live Generate into a Ref - `search_civitai` + `search_query` — server searches Civitai, second LLM hop, Confirm cards in UI - `interrupt` — stop current generation diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs index f296d99..8885bcd 100644 --- a/SwarmAssistentExtension.cs +++ b/SwarmAssistentExtension.cs @@ -53,9 +53,9 @@ public class SwarmAssistentExtension : Extension ScriptFiles.Add("Assets/assistent.js"); StyleSheetFiles.Add("Assets/assistent.css"); ExtensionAuthor = "mrleo1nid"; - Description = "Collaborative Krea 2 assistant via Ollama: chat, vision, img2img/inpaint, Generate loop, Civitai Confirm."; + Description = "Collaborative Krea 2 assistant: Ollama chat, multi-window board, img2img/inpaint, Generate loop, Civitai Confirm."; License = "MIT"; - Version = "0.3.4"; + Version = "0.4.0"; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint"]; } @@ -449,7 +449,10 @@ public class SwarmAssistentExtension : Extension || 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)) + || obj["mask_blur"] != null || obj["mask_grow"] != null + || obj["look_at"] != null || obj["vision_from"] != null || obj["vision_slots"] != null + || obj["slot_to_init"] != null || obj["slot_to_mask"] != null + || obj["snapshot_generate"] != null || obj["select_slot"] != null)) { return obj; } diff --git a/Tabs/Text2Image/Assistent.html b/Tabs/Text2Image/Assistent.html index 4c2ec72..6cf47bb 100644 --- a/Tabs/Text2Image/Assistent.html +++ b/Tabs/Text2Image/Assistent.html @@ -4,22 +4,18 @@
@@ -47,7 +43,7 @@ - + @@ -56,7 +52,7 @@
Collaborative Krea 2
-
Write a prompt, drop a reference on the left, or send the current Generate image.
+
Write a prompt, drop refs on the board, or send Generate via Send to Assistent.