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
+
+ - Generate слева — живой просмотр текущей генерации. В чат сам не уходит.
+ - Ref — референсы: drop / paste / Snapshot gen / Send to Assistent.
+ - Глаз на окне — отправить это изображение мне в vision.
+ - Кнопки патча только у последнего предложения. Пока идёт генерация, Apply + Generate крутит спиннер.
+
+ Напиши, что сгенерировать — или кинь референс и попроси правку.`;
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 @@