2984 lines
109 KiB
JavaScript
2984 lines
109 KiB
JavaScript
/**
|
|
* Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API).
|
|
* v0.5.0: Krea knowledge packs, aspect/vary/lock_seed patches, slash commands, chips.
|
|
*/
|
|
(function () {
|
|
const LS_BASE = 'swarm_assistent_base_url';
|
|
const LS_MODEL = 'swarm_assistent_model';
|
|
const LS_PACK = 'swarm_assistent_pack';
|
|
const LS_AUTO_VISION = 'swarm_assistent_auto_vision';
|
|
const LS_AUTO_APPLY = 'swarm_assistent_auto_apply';
|
|
const LS_AUTO_GENERATE = 'swarm_assistent_auto_generate';
|
|
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 ASPECT_TABLE = {
|
|
'1:1': [1024, 1024],
|
|
'4:3': [1184, 896],
|
|
'3:2': [1248, 832],
|
|
'16:9': [1376, 768],
|
|
'2.35:1': [1568, 672],
|
|
'4:5': [928, 1152],
|
|
'2:3': [832, 1248],
|
|
'9:16': [768, 1376],
|
|
};
|
|
|
|
const PACK_ALIASES = {
|
|
write: 'write_prompt',
|
|
write_prompt: 'write_prompt',
|
|
critique: 'critique_image',
|
|
critique_image: 'critique_image',
|
|
compose: 'compose_scene',
|
|
compose_scene: 'compose_scene',
|
|
params: 'fix_params',
|
|
fix_params: 'fix_params',
|
|
inpaint: 'inpaint_edit',
|
|
inpaint_edit: 'inpaint_edit',
|
|
describe: 'describe_ref',
|
|
describe_ref: 'describe_ref',
|
|
};
|
|
|
|
const WELCOME_HTML = `
|
|
<div class="sa-welcome-title">Assistent · Krea 2</div>
|
|
<ul>
|
|
<li><strong>Generate</strong> слева — живой просмотр текущей генерации. В чат сам не уходит.</li>
|
|
<li><strong>Ref</strong> — референсы: drop / paste / Snapshot gen / Send to Assistent.</li>
|
|
<li>Глаз на окне — отправить это изображение мне в vision.</li>
|
|
<li>Чипсы aspect / seed / Vary — быстрые патчи. В чате: <code>/help</code>.</li>
|
|
<li>Кнопки патча только у последнего предложения. Пока идёт генерация, Apply + Generate крутит спиннер.</li>
|
|
</ul>
|
|
Напиши, что сгенерировать — или кинь референс и попроси правку.`;
|
|
|
|
const HELP_TEXT = `Slash-команды (без LLM):
|
|
/help — этот список
|
|
/gen — Generate сейчас
|
|
/look generate|refN — прикрепить окно к vision
|
|
/init /mask /clear — Init / Mask / Clear Init
|
|
/interrupt — остановить генерацию
|
|
/aspect 16:9 — размер из таблицы 1K
|
|
/seed lock|random — зафиксировать или рандомизировать seed
|
|
/vary — новый seed, тот же промпт
|
|
/pack write|critique|compose|params|inpaint|describe
|
|
/civitai <query> — поиск LoRA (Confirm в чате)
|
|
|
|
Чипсы над полем ввода делают то же для aspect / seed / vary.`;
|
|
|
|
const state = {
|
|
history: [],
|
|
packsLoaded: false,
|
|
busy: false,
|
|
generating: false,
|
|
lastImageDataUrl: null,
|
|
preferredModel: null,
|
|
dragDepth: 0,
|
|
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,
|
|
packUserTouched: false,
|
|
};
|
|
|
|
function $(id) {
|
|
return document.getElementById(id);
|
|
}
|
|
|
|
function modelShort(name) {
|
|
const s = String(name || '');
|
|
const slash = s.lastIndexOf('/');
|
|
return (slash >= 0 ? s.slice(slash + 1) : s) || 'model';
|
|
}
|
|
|
|
function fmtElapsed(ms) {
|
|
const s = Math.max(0, Math.floor(ms / 1000));
|
|
if (s < 60) {
|
|
return `${s}s`;
|
|
}
|
|
return `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`;
|
|
}
|
|
|
|
function hideChatEmpty() {
|
|
const empty = $('sa_chat_empty');
|
|
if (empty) {
|
|
empty.hidden = true;
|
|
}
|
|
}
|
|
|
|
function showChatEmptyIfIdle() {
|
|
const box = $('sa_messages');
|
|
const empty = $('sa_chat_empty');
|
|
if (!box || !empty) {
|
|
return;
|
|
}
|
|
const hasMsg = [...box.children].some((el) => el.id !== 'sa_chat_empty');
|
|
empty.hidden = hasMsg;
|
|
}
|
|
|
|
function setBusyPhase(phase) {
|
|
state.busyPhase = phase || 'thinking';
|
|
tickBusyUi();
|
|
syncPatchActionAvailability();
|
|
syncGenerateBusy();
|
|
}
|
|
|
|
function tickBusyUi() {
|
|
if (state.busyPhase === 'idle') {
|
|
return;
|
|
}
|
|
const elapsed = Date.now() - (state.busyStarted || Date.now());
|
|
if (!state.gotDelta && (state.busyPhase === 'thinking' || state.busyPhase === 'waiting') && elapsed > 1600) {
|
|
state.busyPhase = 'loading';
|
|
}
|
|
const model = modelShort($('sa_model')?.value);
|
|
const labels = {
|
|
encoding: 'Encoding image…',
|
|
waiting: 'Waiting for Ollama…',
|
|
loading: `Loading ${model} into GPU… first load can take a minute`,
|
|
thinking: 'Thinking…',
|
|
streaming: 'Writing…',
|
|
generating: 'Generating image…',
|
|
applying: 'Applying patch…',
|
|
refining: 'Civitai search done — refining…',
|
|
};
|
|
const text = labels[state.busyPhase] || 'Working…';
|
|
const barText = $('sa_livebar_text');
|
|
if (barText) {
|
|
barText.textContent = text;
|
|
}
|
|
const elapsedEl = $('sa_elapsed');
|
|
if (elapsedEl) {
|
|
elapsedEl.textContent = fmtElapsed(elapsed);
|
|
}
|
|
const status = $('sa_status');
|
|
if (status) {
|
|
status.textContent = text;
|
|
status.classList.add('sa-status-busy');
|
|
}
|
|
}
|
|
|
|
function startBusyUi(phase) {
|
|
state.busyStarted = Date.now();
|
|
state.gotDelta = false;
|
|
state.busyPhase = phase || 'thinking';
|
|
$('swarm_assistent_root')?.classList.add('sa-is-busy');
|
|
$('sa_composer')?.classList.add('sa-composer-busy');
|
|
const send = $('sa_btn_send');
|
|
if (send) {
|
|
send.disabled = true;
|
|
}
|
|
const input = $('sa_input');
|
|
if (input) {
|
|
input.classList.add('sa-input-busy');
|
|
}
|
|
const bar = $('sa_livebar');
|
|
if (bar) {
|
|
bar.hidden = false;
|
|
}
|
|
const dot = $('sa_live_dot');
|
|
if (dot) {
|
|
dot.hidden = false;
|
|
}
|
|
tickBusyUi();
|
|
syncPatchActionAvailability();
|
|
syncGenerateBusy();
|
|
if (state.busyTimer) {
|
|
clearInterval(state.busyTimer);
|
|
}
|
|
state.busyTimer = setInterval(tickBusyUi, 400);
|
|
}
|
|
|
|
function stopBusyUi(finalStatus) {
|
|
if (state.busyTimer) {
|
|
clearInterval(state.busyTimer);
|
|
state.busyTimer = null;
|
|
}
|
|
const elapsed = Date.now() - (state.busyStarted || Date.now());
|
|
state.busyPhase = 'idle';
|
|
$('swarm_assistent_root')?.classList.remove('sa-is-busy');
|
|
$('sa_composer')?.classList.remove('sa-composer-busy');
|
|
const send = $('sa_btn_send');
|
|
if (send) {
|
|
send.disabled = false;
|
|
}
|
|
const input = $('sa_input');
|
|
if (input) {
|
|
input.classList.remove('sa-input-busy');
|
|
}
|
|
const bar = $('sa_livebar');
|
|
if (bar) {
|
|
bar.hidden = true;
|
|
}
|
|
const dot = $('sa_live_dot');
|
|
if (dot) {
|
|
dot.hidden = true;
|
|
}
|
|
const status = $('sa_status');
|
|
if (status) {
|
|
status.classList.remove('sa-status-busy');
|
|
}
|
|
if (finalStatus != null) {
|
|
const suffix = elapsed >= 1000 ? ` · ${fmtElapsed(elapsed)}` : '';
|
|
setStatus(finalStatus + suffix);
|
|
}
|
|
syncPatchActionAvailability();
|
|
syncGenerateBusy();
|
|
}
|
|
|
|
function setStatus(text) {
|
|
const el = $('sa_status');
|
|
if (el) {
|
|
el.textContent = text || '';
|
|
}
|
|
}
|
|
|
|
function setInterruptVisible(on) {
|
|
const btn = $('sa_btn_interrupt');
|
|
if (btn) {
|
|
btn.hidden = !on;
|
|
btn.classList.toggle('sa-interrupt-active', !!on);
|
|
}
|
|
}
|
|
|
|
function looksLikeKrea(text) {
|
|
const s = String(text || '');
|
|
return /krea\s*2|krea2|krea-2/i.test(s) || /krea/i.test(s);
|
|
}
|
|
|
|
function resolveCurrentCheckpoint() {
|
|
const out = {
|
|
name: null,
|
|
architecture: null,
|
|
compat_class: null,
|
|
title: null,
|
|
class: null,
|
|
source: null,
|
|
};
|
|
try {
|
|
if (typeof currentModelHelper !== 'undefined' && currentModelHelper) {
|
|
out.name = currentModelHelper.curModel || null;
|
|
out.architecture = currentModelHelper.curArch || null;
|
|
out.compat_class = currentModelHelper.curCompatClass || null;
|
|
out.source = 'currentModelHelper';
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
|
|
try {
|
|
if (typeof getCurrentModel === 'function') {
|
|
const model = getCurrentModel();
|
|
if (model) {
|
|
out.name = out.name || model.name || null;
|
|
out.title = model.title || null;
|
|
out.architecture = out.architecture || model.architecture || null;
|
|
out.class = model.class || null;
|
|
out.compat_class = out.compat_class || model.compat_class || null;
|
|
out.source = out.source || 'getCurrentModel';
|
|
}
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
|
|
try {
|
|
const sel =
|
|
document.getElementById('current_model') ||
|
|
document.getElementById('input_model');
|
|
if (sel) {
|
|
const opt = sel.selectedOptions && sel.selectedOptions[0];
|
|
const hint = [
|
|
sel.value,
|
|
opt && opt.text,
|
|
opt && opt.dataset && opt.dataset.cleanname,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
if (!out.name && sel.value) {
|
|
out.name = sel.value;
|
|
out.source = out.source || 'dropdown';
|
|
}
|
|
if (hint && !out.architecture) {
|
|
out.title = out.title || hint;
|
|
}
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
|
|
return out;
|
|
}
|
|
|
|
function isKreaSelected() {
|
|
try {
|
|
const m = resolveCurrentCheckpoint();
|
|
const blob = [
|
|
m.architecture,
|
|
m.compat_class,
|
|
m.title,
|
|
m.name,
|
|
m.class,
|
|
].join(' ');
|
|
return looksLikeKrea(blob);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function updateGate() {
|
|
const ok = isKreaSelected();
|
|
const gate = $('sa_gate');
|
|
const layout = $('sa_layout');
|
|
if (gate) {
|
|
gate.hidden = ok;
|
|
if (!ok) {
|
|
const m = resolveCurrentCheckpoint();
|
|
const seen = [m.architecture, m.compat_class, m.name]
|
|
.filter(Boolean)
|
|
.join(' · ');
|
|
const p = gate.querySelector('p');
|
|
if (p) {
|
|
p.innerHTML = seen
|
|
? `Swarm Assistent is for <strong>Krea 2</strong> models only. Current: <code>${escapeHtml(
|
|
seen,
|
|
)}</code> — pick a checkpoint with architecture <code>krea-2</code>.`
|
|
: 'Swarm Assistent is for <strong>Krea 2</strong> models only. Select a Krea 2 checkpoint on Generate to enable the chat.';
|
|
}
|
|
}
|
|
}
|
|
if (layout) {
|
|
layout.classList.toggle('sa-disabled', !ok);
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return String(s)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function val(id) {
|
|
const el = document.getElementById(id);
|
|
return el ? el.value : '';
|
|
}
|
|
|
|
function setVal(id, value) {
|
|
const el = document.getElementById(id);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
el.value = value;
|
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
|
|
function openAssistentTab() {
|
|
const tab = document.getElementById(TAB_BUTTON_ID);
|
|
if (tab) {
|
|
tab.click();
|
|
return true;
|
|
}
|
|
const pane = document.getElementById('assistent');
|
|
if (pane && typeof bootstrap !== 'undefined' && bootstrap.Tab) {
|
|
try {
|
|
bootstrap.Tab.getOrCreateInstance(tab || pane).show();
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
return !!tab;
|
|
}
|
|
|
|
function flashImagePane(slotId) {
|
|
const el = document.querySelector(`.sa-slot[data-id="${slotId || state.selectedSlotId}"]`);
|
|
if (!el) {
|
|
return;
|
|
}
|
|
el.classList.remove('sa-flash');
|
|
void el.offsetWidth;
|
|
el.classList.add('sa-flash');
|
|
}
|
|
|
|
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 = src ? String(src).trim().split(/\s+/)[0] : null;
|
|
if (cleaned && cleaned.startsWith('#')) {
|
|
return false;
|
|
}
|
|
slot.src = cleaned || null;
|
|
if (attach != null) {
|
|
slot.attach = !!attach;
|
|
} else if (slot.type === 'ref' && slot.src) {
|
|
slot.attach = true;
|
|
}
|
|
if (select) {
|
|
state.selectedSlotId = slot.id;
|
|
}
|
|
syncLastImageAlias();
|
|
renderBoard();
|
|
flashImagePane(slot.id);
|
|
if (switchTab) {
|
|
openAssistentTab();
|
|
}
|
|
if (note) {
|
|
setStatus(note);
|
|
}
|
|
return 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;
|
|
}
|
|
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;
|
|
}
|
|
renderBoard();
|
|
return slot;
|
|
}
|
|
|
|
function clearSlot(id, { silent = false } = {}) {
|
|
const slot = slotById(id);
|
|
if (!slot) {
|
|
return;
|
|
}
|
|
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(`${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'
|
|
? '<div class="sa-empty-title">Generate</div><div class="sa-empty-hint">Live view of the current generation</div>'
|
|
: '<div class="sa-empty-title">Reference</div><div class="sa-empty-hint">Drop · paste · Snapshot gen</div>';
|
|
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 = '<span class="sa-spinner" aria-hidden="true"></span>';
|
|
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');
|
|
if (!box) {
|
|
return 0;
|
|
}
|
|
const text = box.value || '';
|
|
const matches = text.match(/<image(?:\s|\/|>)/gi) || text.match(/data:image\//gi);
|
|
return matches ? matches.length : 0;
|
|
} catch (e) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function triggerChangeForEl(el) {
|
|
if (!el) {
|
|
return;
|
|
}
|
|
if (typeof triggerChangeFor === 'function') {
|
|
triggerChangeFor(el);
|
|
return;
|
|
}
|
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
|
|
function openInitImageGroup() {
|
|
try {
|
|
const initEl = document.getElementById('input_initimage');
|
|
if (initEl && typeof toggleGroupOpen === 'function') {
|
|
toggleGroupOpen(initEl, true);
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
const toggler = document.getElementById('input_group_content_initimage_toggle');
|
|
if (toggler) {
|
|
toggler.checked = true;
|
|
triggerChangeForEl(toggler);
|
|
}
|
|
}
|
|
|
|
function hasFileParam(id) {
|
|
const el = document.getElementById(id);
|
|
return !!(el && el.files && el.files.length > 0);
|
|
}
|
|
|
|
function clearFileParam(id) {
|
|
const el = document.getElementById(id);
|
|
if (!el) {
|
|
return false;
|
|
}
|
|
try {
|
|
el.value = '';
|
|
if (el.files && typeof DataTransfer !== 'undefined') {
|
|
el.files = new DataTransfer().files;
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
triggerChangeForEl(el);
|
|
return true;
|
|
}
|
|
|
|
function guessImageMime(src) {
|
|
const s = String(src || '');
|
|
if (s.startsWith('data:image/')) {
|
|
const m = s.match(/^data:(image\/[a-z0-9.+-]+)/i);
|
|
return (m && m[1]) || 'image/png';
|
|
}
|
|
const path = s.split('?')[0];
|
|
const ext = path.substring(path.lastIndexOf('.') + 1).toLowerCase();
|
|
if (ext === 'jpg' || ext === 'jpeg') {
|
|
return 'image/jpeg';
|
|
}
|
|
if (ext === 'webp') {
|
|
return 'image/webp';
|
|
}
|
|
if (ext === 'gif') {
|
|
return 'image/gif';
|
|
}
|
|
return 'image/png';
|
|
}
|
|
|
|
async function srcToBlob(src) {
|
|
if (!src) {
|
|
return null;
|
|
}
|
|
const cleaned = String(src).trim().split(/\s+/)[0];
|
|
if (cleaned.startsWith('data:') || cleaned.startsWith('/') || cleaned.startsWith('View/') || cleaned.startsWith('http')) {
|
|
try {
|
|
const resp = await fetch(cleaned);
|
|
return await resp.blob();
|
|
} catch (e) {
|
|
console.warn('Assistent: fetch blob failed', e);
|
|
}
|
|
}
|
|
return await new Promise((resolve) => {
|
|
const tmpImg = new Image();
|
|
tmpImg.crossOrigin = 'Anonymous';
|
|
tmpImg.onload = () => {
|
|
try {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = tmpImg.naturalWidth;
|
|
canvas.height = tmpImg.naturalHeight;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.drawImage(tmpImg, 0, 0);
|
|
canvas.toBlob((blob) => resolve(blob), 'image/png');
|
|
} catch (e) {
|
|
resolve(null);
|
|
}
|
|
};
|
|
tmpImg.onerror = () => resolve(null);
|
|
tmpImg.src = cleaned;
|
|
});
|
|
}
|
|
|
|
async function setFileParamFromSrc(paramId, src, { filename = 'assistent.png' } = {}) {
|
|
const el = document.getElementById(paramId);
|
|
if (!el) {
|
|
setStatus(`Missing ${paramId} on Generate tab`);
|
|
return false;
|
|
}
|
|
const blob = await srcToBlob(src);
|
|
if (!blob) {
|
|
setStatus('Could not load image for init/mask');
|
|
return false;
|
|
}
|
|
const mime = blob.type || guessImageMime(src);
|
|
const file = new File([blob], filename, { type: mime });
|
|
const container = new DataTransfer();
|
|
container.items.add(file);
|
|
el.files = container.files;
|
|
triggerChangeForEl(el);
|
|
openInitImageGroup();
|
|
return true;
|
|
}
|
|
|
|
async function setInitFromSrc(src) {
|
|
const ok = await setFileParamFromSrc('input_initimage', src, { filename: 'assistent_init.png' });
|
|
if (ok) {
|
|
setStatus('Init Image set');
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
async function setMaskFromSrc(src) {
|
|
const ok = await setFileParamFromSrc('input_maskimage', src, { filename: 'assistent_mask.png' });
|
|
if (ok) {
|
|
setStatus('Mask Image set (white = edit)');
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
function clearInitAndMask() {
|
|
clearFileParam('input_initimage');
|
|
clearFileParam('input_maskimage');
|
|
const toggler = document.getElementById('input_group_content_initimage_toggle');
|
|
if (toggler) {
|
|
toggler.checked = false;
|
|
triggerChangeForEl(toggler);
|
|
}
|
|
setStatus('Init Image + Mask cleared');
|
|
}
|
|
|
|
function readInitContext() {
|
|
const creativityRaw = val('input_initimagecreativity');
|
|
const creativity = creativityRaw === '' ? null : parseFloat(creativityRaw);
|
|
return {
|
|
has_init_image: hasFileParam('input_initimage'),
|
|
has_mask_image: hasFileParam('input_maskimage'),
|
|
init_creativity: Number.isFinite(creativity) ? creativity : null,
|
|
mask_blur: parseFloat(val('input_maskblur') || '') || null,
|
|
mask_grow: parseInt(val('input_maskgrow') || val('input_maskshrinkgrow') || '', 10) || null,
|
|
init_group_on: !!(document.getElementById('input_group_content_initimage_toggle')?.checked),
|
|
};
|
|
}
|
|
|
|
function collectLiveContext() {
|
|
const inv = state.inventory || {};
|
|
const initCtx = readInitContext();
|
|
const ctx = {
|
|
architecture_ok: isKreaSelected(),
|
|
checkpoint: null,
|
|
prompt: val('alt_prompt_textbox') || val('input_prompt') || '',
|
|
negative: val('input_negativeprompt') || val('alt_negativeprompt_textbox') || '',
|
|
width: parseInt(val('input_width') || '0', 10) || null,
|
|
height: parseInt(val('input_height') || '0', 10) || null,
|
|
steps: parseInt(val('input_steps') || '0', 10) || null,
|
|
cfg: parseFloat(val('input_cfgscale') || val('input_cfg') || '') || null,
|
|
sigma_shift: parseFloat(val('input_sigmashift') || '') || null,
|
|
seed: val('input_seed') || null,
|
|
sampler: val('input_sampler') || val('input_samplerate') || null,
|
|
scheduler: val('input_scheduler') || null,
|
|
batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null,
|
|
prompt_image_count: countPromptImages(),
|
|
selected_loras: [],
|
|
available_loras: (inv.loras || []).slice(0, 100),
|
|
wildcards: (inv.wildcards || []).map((w) => w.name || w).slice(0, 60),
|
|
has_vision_image: 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,
|
|
...initCtx,
|
|
};
|
|
|
|
try {
|
|
const model = resolveCurrentCheckpoint();
|
|
if (model.name || model.architecture) {
|
|
ctx.checkpoint = {
|
|
name: model.name || model.title || null,
|
|
architecture: model.architecture || model.compat_class || model.class || null,
|
|
title: model.title || null,
|
|
};
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
|
|
try {
|
|
if (typeof loraHelper !== 'undefined' && loraHelper && Array.isArray(loraHelper.selected)) {
|
|
ctx.selected_loras = loraHelper.selected.map((l) => ({
|
|
name: l.name || l,
|
|
weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l]) || 1,
|
|
}));
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
|
|
// Fallback if inventory empty
|
|
if (!ctx.available_loras.length) {
|
|
try {
|
|
const models = (typeof allModels !== 'undefined' && allModels) || (typeof model_list !== 'undefined' && model_list) || [];
|
|
const list = Array.isArray(models) ? models : Object.values(models || {});
|
|
for (const m of list) {
|
|
if (!m) {
|
|
continue;
|
|
}
|
|
const folder = `${m.folder || m.path || ''}`;
|
|
const isLora = /lora/i.test(m.category || m.type || '') || (m.name && String(m.name).toLowerCase().includes('lora'));
|
|
const inLoraFolder = /lora/i.test(folder);
|
|
if (!(isLora || inLoraFolder)) {
|
|
continue;
|
|
}
|
|
ctx.available_loras.push({
|
|
name: m.name || m.title,
|
|
title: m.title || m.name,
|
|
trigger_phrase: m.trigger_phrase || m.trigger || (m.metadata && (m.metadata.trigger_phrase || m.metadata.trigger)) || null,
|
|
architecture: m.architecture || null,
|
|
});
|
|
}
|
|
if (ctx.available_loras.length > 80) {
|
|
ctx.available_loras = ctx.available_loras.slice(0, 80);
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
|
|
return ctx;
|
|
}
|
|
|
|
function isPatchObject(obj) {
|
|
if (!obj || typeof obj !== 'object') {
|
|
return false;
|
|
}
|
|
return (
|
|
obj.prompt != null ||
|
|
obj.loras ||
|
|
obj.width ||
|
|
obj.height ||
|
|
obj.steps ||
|
|
obj.cfg ||
|
|
obj.seed != null ||
|
|
obj.sigma_shift != null ||
|
|
obj.sampler ||
|
|
obj.actions ||
|
|
obj.search_query ||
|
|
obj.civitai_query ||
|
|
obj.use_init_image != null ||
|
|
obj.clear_init_image != null ||
|
|
obj.init_creativity != null ||
|
|
obj.denoise != null ||
|
|
obj.use_mask_image != null ||
|
|
obj.clear_mask_image != null ||
|
|
obj.mask_blur != null ||
|
|
obj.mask_grow != null ||
|
|
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 ||
|
|
obj.aspect != null ||
|
|
obj.images != null ||
|
|
obj.batch != null ||
|
|
obj.vary != null ||
|
|
obj.lock_seed != null ||
|
|
obj.creativity != null ||
|
|
obj.intensity != null ||
|
|
obj.complexity != null ||
|
|
obj.movement != null ||
|
|
obj.clear_prompt_images != null ||
|
|
obj.slot_to_prompt_image != null ||
|
|
obj.pack != null
|
|
);
|
|
}
|
|
|
|
function extractPatch(text) {
|
|
if (!text) {
|
|
return { prose: text || '', patch: null };
|
|
}
|
|
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
|
|
let match;
|
|
let lastPatch = null;
|
|
let prose = text;
|
|
while ((match = re.exec(text)) !== null) {
|
|
const raw = match[1].trim();
|
|
try {
|
|
const obj = JSON.parse(raw);
|
|
if (isPatchObject(obj)) {
|
|
lastPatch = obj;
|
|
prose = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trim();
|
|
}
|
|
} catch (e) { /* not json */ }
|
|
}
|
|
return { prose, patch: lastPatch };
|
|
}
|
|
|
|
function normalizeAspect(raw) {
|
|
if (raw == null) {
|
|
return null;
|
|
}
|
|
let s = String(raw).trim().toLowerCase().replace(/\s+/g, '');
|
|
if (!s) {
|
|
return null;
|
|
}
|
|
if (s === 'square') {
|
|
s = '1:1';
|
|
} else if (s === 'portrait' || s === 'vert') {
|
|
s = '2:3';
|
|
} else if (s === 'landscape' || s === 'horiz') {
|
|
s = '16:9';
|
|
} else if (s === 'cinematic' || s === 'ultrawide') {
|
|
s = '2.35:1';
|
|
}
|
|
return ASPECT_TABLE[s] ? s : null;
|
|
}
|
|
|
|
function sizeFromAspect(aspect) {
|
|
const key = normalizeAspect(aspect);
|
|
return key ? ASPECT_TABLE[key] : null;
|
|
}
|
|
|
|
function guessAspectFromSize(w, h) {
|
|
const width = parseInt(w, 10);
|
|
const height = parseInt(h, 10);
|
|
if (!width || !height) {
|
|
return null;
|
|
}
|
|
let best = null;
|
|
let bestDist = Infinity;
|
|
for (const [key, [aw, ah]] of Object.entries(ASPECT_TABLE)) {
|
|
const dist = Math.abs(width / height - aw / ah) + Math.abs(width - aw) / 4000 + Math.abs(height - ah) / 4000;
|
|
if (dist < bestDist) {
|
|
bestDist = dist;
|
|
best = key;
|
|
}
|
|
}
|
|
return bestDist < 0.12 ? best : null;
|
|
}
|
|
|
|
function clearPromptImagesInBox() {
|
|
const box = document.getElementById('alt_prompt_textbox') || document.getElementById('input_prompt');
|
|
if (!box) {
|
|
return false;
|
|
}
|
|
const before = box.value || '';
|
|
const next = before
|
|
.replace(/<image\b[^>]*>[\s\S]*?<\/image>/gi, '')
|
|
.replace(/<image\b[^>]*\/?>/gi, '')
|
|
.replace(/data:image\/[a-zA-Z0-9.+-]+;base64,[A-Za-z0-9+/=]+/g, '')
|
|
.replace(/\n{3,}/g, '\n\n')
|
|
.trim();
|
|
if (next === before.trim()) {
|
|
return false;
|
|
}
|
|
box.value = next;
|
|
box.dispatchEvent(new Event('input', { bubbles: true }));
|
|
box.dispatchEvent(new Event('change', { bubbles: true }));
|
|
return true;
|
|
}
|
|
|
|
function setPackValue(packName, { flash, user } = {}) {
|
|
const pack = $('sa_pack');
|
|
if (!pack || !packName) {
|
|
return false;
|
|
}
|
|
const resolved = PACK_ALIASES[String(packName).trim()] || String(packName).trim();
|
|
if (![...pack.options].some((o) => o.value === resolved)) {
|
|
return false;
|
|
}
|
|
if (pack.value !== resolved) {
|
|
pack.value = resolved;
|
|
saveSettings();
|
|
}
|
|
if (user) {
|
|
state.packUserTouched = true;
|
|
}
|
|
if (flash) {
|
|
pack.classList.add('sa-pack-flash');
|
|
setTimeout(() => pack.classList.remove('sa-pack-flash'), 900);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function autoSelectPack(text) {
|
|
if (state.packUserTouched) {
|
|
return null;
|
|
}
|
|
const t = String(text || '').toLowerCase();
|
|
if (!t.trim()) {
|
|
return null;
|
|
}
|
|
if (/\b(опиши\s+реф|опиши\s+изображ|prompt\s+from\s+image|describe\s+(this|the|ref|image)|reverse\s*prompt)\b/i.test(t)
|
|
|| /опиши\s+(этот|эту|картинк|референс)/i.test(t)) {
|
|
return 'describe_ref';
|
|
}
|
|
if (/\b(critique|что\s+не\s+так|посмотри|смотри|разбери|критик)\b/i.test(t)) {
|
|
return 'critique_image';
|
|
}
|
|
if (/\b(inpaint|замажь|закрась|руки|лицо|маск|mask|img2img|init\s*image)\b/i.test(t)) {
|
|
return 'inpaint_edit';
|
|
}
|
|
if (/\b(steps?|cfg|seed|sigma|размер|aspect|ширин|высот|resolution|batch)\b/i.test(t)) {
|
|
return 'fix_params';
|
|
}
|
|
if (/\b(сцен|moodboard|атмосфер|compose|scene|мизансцен)\b/i.test(t)) {
|
|
return 'compose_scene';
|
|
}
|
|
return 'write_prompt';
|
|
}
|
|
|
|
function patchHasGenTrigger(patch) {
|
|
if (!patch) {
|
|
return false;
|
|
}
|
|
if (Array.isArray(patch.actions) && patch.actions.map(String).includes('generate')) {
|
|
return true;
|
|
}
|
|
return (
|
|
patch.prompt != null ||
|
|
patch.loras ||
|
|
patch.width != null ||
|
|
patch.height != null ||
|
|
patch.aspect != null ||
|
|
patch.steps != null ||
|
|
patch.cfg != null ||
|
|
patch.seed != null ||
|
|
patch.sigma_shift != null ||
|
|
patch.images != null ||
|
|
patch.batch != null ||
|
|
patch.vary === true ||
|
|
patch.use_init_image ||
|
|
patch.clear_init_image ||
|
|
patch.init_creativity != null ||
|
|
patch.denoise != null ||
|
|
patch.use_mask_image ||
|
|
patch.clear_mask_image ||
|
|
patch.clear_prompt_images
|
|
);
|
|
}
|
|
|
|
async function applyPatch(patch, which) {
|
|
if (!patch) {
|
|
return;
|
|
}
|
|
const doPrompt = !which || which === 'all' || which === 'prompt';
|
|
const doLoras = !which || which === 'all' || which === 'loras';
|
|
const doParams = !which || which === 'all' || which === 'size' || which === 'params';
|
|
const doInit = !which || which === 'all' || which === 'params' || which === 'init';
|
|
|
|
if (patch.pack) {
|
|
setPackValue(patch.pack, { flash: true });
|
|
}
|
|
|
|
if (doPrompt && patch.clear_prompt_images) {
|
|
clearPromptImagesInBox();
|
|
}
|
|
|
|
if (doPrompt && patch.prompt != null) {
|
|
const box = document.getElementById('alt_prompt_textbox') || document.getElementById('input_prompt');
|
|
if (box) {
|
|
box.value = patch.prompt;
|
|
box.dispatchEvent(new Event('input', { bubbles: true }));
|
|
box.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
if (patch.negative != null) {
|
|
setVal('input_negativeprompt', patch.negative);
|
|
}
|
|
if (Array.isArray(patch.loras)) {
|
|
for (const l of patch.loras) {
|
|
const triggers = l.triggers || (l.trigger_phrase ? [l.trigger_phrase] : []);
|
|
for (const t of triggers) {
|
|
if (t && box && box.value && !box.value.includes(t)) {
|
|
box.value = `${box.value.trim()}, ${t}`;
|
|
box.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (doLoras && Array.isArray(patch.loras) && typeof loraHelper !== 'undefined' && loraHelper) {
|
|
try {
|
|
if (typeof loraHelper.clearLoras === 'function') {
|
|
loraHelper.clearLoras();
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
for (const l of patch.loras) {
|
|
const name = l.name;
|
|
if (!name) {
|
|
continue;
|
|
}
|
|
try {
|
|
if (typeof loraHelper.selectLora === 'function') {
|
|
loraHelper.selectLora(name);
|
|
}
|
|
if (loraHelper.loraWeightPref && l.weight != null) {
|
|
loraHelper.loraWeightPref[name] = l.weight;
|
|
}
|
|
} catch (e) {
|
|
console.warn('Assistent: selectLora failed', name, e);
|
|
}
|
|
}
|
|
try {
|
|
if (typeof loraHelper.rebuildUI === 'function') {
|
|
loraHelper.rebuildUI();
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
|
|
if (doParams) {
|
|
const aspectSize = sizeFromAspect(patch.aspect);
|
|
if (aspectSize) {
|
|
setVal('input_width', String(aspectSize[0]));
|
|
setVal('input_height', String(aspectSize[1]));
|
|
} else {
|
|
if (patch.width != null) {
|
|
setVal('input_width', String(patch.width));
|
|
}
|
|
if (patch.height != null) {
|
|
setVal('input_height', String(patch.height));
|
|
}
|
|
}
|
|
if (patch.steps != null) {
|
|
setVal('input_steps', String(patch.steps));
|
|
}
|
|
if (patch.cfg != null) {
|
|
if (document.getElementById('input_cfgscale')) {
|
|
setVal('input_cfgscale', String(patch.cfg));
|
|
} else {
|
|
setVal('input_cfg', String(patch.cfg));
|
|
}
|
|
}
|
|
if (patch.vary === true) {
|
|
setVal('input_seed', '-1');
|
|
} else if (patch.lock_seed === true) {
|
|
const cur = val('input_seed');
|
|
if (cur && String(cur) !== '-1') {
|
|
setVal('input_seed', cur);
|
|
}
|
|
} else if (patch.seed != null) {
|
|
setVal('input_seed', String(patch.seed));
|
|
}
|
|
if (patch.sigma_shift != null) {
|
|
setVal('input_sigmashift', String(patch.sigma_shift));
|
|
}
|
|
if (patch.sampler != null) {
|
|
if (document.getElementById('input_sampler')) {
|
|
setVal('input_sampler', String(patch.sampler));
|
|
}
|
|
}
|
|
if (patch.scheduler != null && document.getElementById('input_scheduler')) {
|
|
setVal('input_scheduler', String(patch.scheduler));
|
|
}
|
|
const batch = patch.images != null ? patch.images : patch.batch;
|
|
if (batch != null) {
|
|
if (document.getElementById('input_images')) {
|
|
setVal('input_images', String(batch));
|
|
} else if (document.getElementById('input_batchsize')) {
|
|
setVal('input_batchsize', String(batch));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (doInit) {
|
|
const creativity = patch.init_creativity != null ? patch.init_creativity : patch.denoise;
|
|
if (creativity != null && document.getElementById('input_initimagecreativity')) {
|
|
setVal('input_initimagecreativity', String(creativity));
|
|
openInitImageGroup();
|
|
}
|
|
if (patch.mask_blur != null && document.getElementById('input_maskblur')) {
|
|
setVal('input_maskblur', String(patch.mask_blur));
|
|
}
|
|
if (patch.mask_grow != null) {
|
|
if (document.getElementById('input_maskgrow')) {
|
|
setVal('input_maskgrow', String(patch.mask_grow));
|
|
} else if (document.getElementById('input_maskshrinkgrow')) {
|
|
setVal('input_maskshrinkgrow', String(patch.mask_grow));
|
|
}
|
|
}
|
|
if (patch.clear_init_image || patch.clear_mask_image) {
|
|
if (patch.clear_init_image) {
|
|
clearFileParam('input_initimage');
|
|
}
|
|
if (patch.clear_mask_image) {
|
|
clearFileParam('input_maskimage');
|
|
}
|
|
if (patch.clear_init_image && patch.clear_mask_image) {
|
|
const toggler = document.getElementById('input_group_content_initimage_toggle');
|
|
if (toggler) {
|
|
toggler.checked = false;
|
|
triggerChangeForEl(toggler);
|
|
}
|
|
}
|
|
}
|
|
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) {
|
|
const initSrc = resolveSlotSrc(initId) || src;
|
|
if (initSrc) {
|
|
await setInitFromSrc(initSrc);
|
|
} else {
|
|
setStatus('No image for Init — drop a ref or wait for Generate');
|
|
}
|
|
}
|
|
if (wantMask) {
|
|
const maskSrc = resolveSlotSrc(maskId) || src;
|
|
if (maskSrc) {
|
|
await setMaskFromSrc(maskSrc);
|
|
} else {
|
|
setStatus('No image for Mask — drop a mask (white=edit) first');
|
|
}
|
|
}
|
|
if (patch.slot_to_prompt_image) {
|
|
setStatus('Prompt Images: drop the ref into the Swarm prompt box (no auto helper yet)');
|
|
}
|
|
}
|
|
syncChipHighlight();
|
|
setStatus('Applied patch');
|
|
}
|
|
|
|
function triggerGenerate() {
|
|
try {
|
|
if (typeof mainGenHandler !== 'undefined' && mainGenHandler && typeof mainGenHandler.doGenerate === 'function') {
|
|
mainGenHandler.doGenerate();
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
console.warn('Assistent: doGenerate failed', e);
|
|
}
|
|
const btn =
|
|
document.getElementById('generate_button') ||
|
|
document.getElementById('alt_generate_button') ||
|
|
document.querySelector('button.generate-button') ||
|
|
document.querySelector('#generate_button, button[id*="generate"]');
|
|
if (btn) {
|
|
btn.click();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function doInterruptNow() {
|
|
try {
|
|
if (typeof doInterrupt === 'function') {
|
|
doInterrupt(false);
|
|
return;
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
if (typeof genericRequest === 'function') {
|
|
genericRequest('InterruptAll', { other_sessions: false }, () => {}, 0, () => {});
|
|
}
|
|
}
|
|
|
|
function waitForNewImage(prevSrc, timeoutMs = 180000) {
|
|
return new Promise((resolve) => {
|
|
const start = Date.now();
|
|
const timer = setInterval(() => {
|
|
const src = findCurrentGenerateSrc();
|
|
if (src && src !== prevSrc && !looksLikeModelPreview(src)) {
|
|
clearInterval(timer);
|
|
resolve(src);
|
|
} else if (Date.now() - start > timeoutMs) {
|
|
clearInterval(timer);
|
|
resolve(null);
|
|
}
|
|
}, 400);
|
|
});
|
|
}
|
|
|
|
async function runGenerateFromPatch(patch) {
|
|
if (!$('sa_auto_generate')?.checked || !patchHasGenTrigger(patch)) {
|
|
return null;
|
|
}
|
|
const prev = findCurrentGenerateSrc();
|
|
setStatus('Generating…');
|
|
startBusyUi('generating');
|
|
state.generating = true;
|
|
setInterruptVisible(true);
|
|
const ok = triggerGenerate();
|
|
if (!ok) {
|
|
state.generating = false;
|
|
setInterruptVisible(state.busy);
|
|
setStatus('Could not start Generate (UI hook missing)');
|
|
return null;
|
|
}
|
|
const src = await waitForNewImage(prev);
|
|
state.generating = false;
|
|
setInterruptVisible(state.busy);
|
|
if (!state.busy) {
|
|
stopBusyUi(src ? 'Generate done' : 'Generate finished (no new image detected)');
|
|
}
|
|
if (src) {
|
|
const gen = generateSlot();
|
|
if (gen) {
|
|
gen.src = src;
|
|
renderBoard();
|
|
}
|
|
setStatus('Generate done');
|
|
return src;
|
|
}
|
|
if (state.busy) {
|
|
setStatus('Generate finished (no new image detected)');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function maybeAutoCritique(imageSrc) {
|
|
if (!$('sa_auto_critique')?.checked || state.critiqueHopUsed || !imageSrc) {
|
|
return;
|
|
}
|
|
state.critiqueHopUsed = true;
|
|
const pack = $('sa_pack');
|
|
if (pack) {
|
|
pack.value = 'critique_image';
|
|
saveSettings();
|
|
}
|
|
if ($('sa_input')) {
|
|
$('sa_input').value = 'Critique this result and improve the prompt for the next generation.';
|
|
}
|
|
const gen = generateSlot();
|
|
if (gen) {
|
|
gen.attach = true;
|
|
if (imageSrc) {
|
|
gen.src = imageSrc;
|
|
}
|
|
renderBoard();
|
|
}
|
|
setStatus('Auto-critique…');
|
|
await sendChat({ fromAutoCritique: true, forceSlotIds: [GEN_ID] });
|
|
}
|
|
|
|
function appendMessage(role, text, patch, civitaiResults) {
|
|
const box = $('sa_messages');
|
|
if (!box) {
|
|
return null;
|
|
}
|
|
hideChatEmpty();
|
|
const div = document.createElement('div');
|
|
div.className = `sa-msg ${role}`;
|
|
const { prose, patch: extracted } = role === 'assistant' ? extractPatch(text) : { prose: text, patch: null };
|
|
const finalPatch = patch || extracted;
|
|
div.textContent = prose || text || '';
|
|
if (finalPatch) {
|
|
const wrap = document.createElement('div');
|
|
wrap.className = 'sa-patch';
|
|
const pre = document.createElement('pre');
|
|
pre.textContent = JSON.stringify(finalPatch, null, 2);
|
|
wrap.appendChild(pre);
|
|
mountPatchActions(wrap, finalPatch);
|
|
div.appendChild(wrap);
|
|
}
|
|
if (civitaiResults && civitaiResults.length) {
|
|
div.appendChild(buildCivitaiCards(civitaiResults));
|
|
}
|
|
box.appendChild(div);
|
|
box.scrollTop = box.scrollHeight;
|
|
return div;
|
|
}
|
|
|
|
function beginStreamMessage() {
|
|
const box = $('sa_messages');
|
|
if (!box) {
|
|
return null;
|
|
}
|
|
hideChatEmpty();
|
|
const div = document.createElement('div');
|
|
div.className = 'sa-msg assistant sa-streaming sa-typing';
|
|
div.innerHTML = '<span class="sa-dots" aria-hidden="true"><i></i><i></i><i></i></span><span class="sa-typing-label">Waiting for the model…</span>';
|
|
box.appendChild(div);
|
|
box.scrollTop = box.scrollHeight;
|
|
state.streamEl = div;
|
|
return div;
|
|
}
|
|
|
|
function appendStreamDelta(delta) {
|
|
if (!state.streamEl) {
|
|
beginStreamMessage();
|
|
}
|
|
if (state.streamEl) {
|
|
if (state.streamEl.classList.contains('sa-typing')) {
|
|
state.streamEl.classList.remove('sa-typing');
|
|
state.streamEl.textContent = '';
|
|
}
|
|
state.gotDelta = true;
|
|
if (state.busyPhase !== 'refining') {
|
|
setBusyPhase('streaming');
|
|
}
|
|
state.streamEl.textContent += delta;
|
|
const box = $('sa_messages');
|
|
if (box) {
|
|
box.scrollTop = box.scrollHeight;
|
|
}
|
|
}
|
|
}
|
|
|
|
function finalizeStreamMessage(fullReply, civitaiResults) {
|
|
const el = state.streamEl;
|
|
state.streamEl = null;
|
|
if (!el) {
|
|
appendMessage('assistant', fullReply, null, civitaiResults);
|
|
return;
|
|
}
|
|
el.classList.remove('sa-streaming', 'sa-typing');
|
|
const { prose, patch } = extractPatch(fullReply);
|
|
el.textContent = prose || fullReply || '';
|
|
if (patch) {
|
|
const wrap = document.createElement('div');
|
|
wrap.className = 'sa-patch';
|
|
const pre = document.createElement('pre');
|
|
pre.textContent = JSON.stringify(patch, null, 2);
|
|
wrap.appendChild(pre);
|
|
mountPatchActions(wrap, patch);
|
|
el.appendChild(wrap);
|
|
}
|
|
if (civitaiResults && civitaiResults.length) {
|
|
el.appendChild(buildCivitaiCards(civitaiResults));
|
|
}
|
|
const box = $('sa_messages');
|
|
if (box) {
|
|
box.scrollTop = box.scrollHeight;
|
|
}
|
|
}
|
|
|
|
function buildCivitaiCards(results) {
|
|
const list = document.createElement('div');
|
|
list.className = 'sa-civitai-list';
|
|
for (const r of results) {
|
|
const card = document.createElement('div');
|
|
card.className = 'sa-civitai-card' + (r.already_installed ? ' sa-installed' : '');
|
|
const title = document.createElement('div');
|
|
title.className = 'sa-civitai-title';
|
|
title.textContent = r.name || r.file_name || 'LoRA';
|
|
card.appendChild(title);
|
|
const meta = document.createElement('div');
|
|
meta.className = 'sa-civitai-meta';
|
|
const bits = [
|
|
r.base_model || '?',
|
|
r.krea_likely ? 'Krea?' : null,
|
|
r.already_installed ? 'already installed' : null,
|
|
(r.triggers || []).slice(0, 3).join(', ') || null,
|
|
].filter(Boolean);
|
|
meta.textContent = bits.join(' · ');
|
|
card.appendChild(meta);
|
|
const actions = document.createElement('div');
|
|
actions.className = 'sa-civitai-actions';
|
|
if (r.already_installed) {
|
|
const note = document.createElement('span');
|
|
note.textContent = 'Installed';
|
|
actions.appendChild(note);
|
|
} else if (r.download_url) {
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'basic-button sa-primary';
|
|
btn.textContent = 'Confirm download';
|
|
btn.addEventListener('click', () => downloadCivitaiLoRA(r, btn));
|
|
actions.appendChild(btn);
|
|
} else {
|
|
const note = document.createElement('span');
|
|
note.textContent = 'No download URL';
|
|
actions.appendChild(note);
|
|
}
|
|
card.appendChild(actions);
|
|
list.appendChild(card);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
function downloadCivitaiLoRA(card, btn) {
|
|
if (!card.download_url) {
|
|
return;
|
|
}
|
|
if (btn) {
|
|
btn.disabled = true;
|
|
btn.textContent = 'Downloading…';
|
|
}
|
|
setStatus(`Downloading ${card.file_name || card.name}…`);
|
|
setInterruptVisible(true);
|
|
const payload = {
|
|
url: card.download_url,
|
|
type: 'LoRA',
|
|
name: card.file_name || card.name || 'lora',
|
|
};
|
|
const onDone = (ok, msg) => {
|
|
setInterruptVisible(state.busy || state.generating);
|
|
if (ok) {
|
|
setStatus(`Downloaded ${payload.name}`);
|
|
if (btn) {
|
|
btn.textContent = 'Downloaded';
|
|
}
|
|
refreshInventory(() => {
|
|
if ($('sa_input')) {
|
|
$('sa_input').value = `LoRA "${payload.name}" is now installed. Enable it with its triggers and improve the prompt.`;
|
|
}
|
|
sendChat({ fromDownload: true });
|
|
});
|
|
} else {
|
|
setStatus(msg || 'Download failed');
|
|
if (btn) {
|
|
btn.disabled = false;
|
|
btn.textContent = 'Confirm download';
|
|
}
|
|
appendMessage('error', msg || 'Download failed');
|
|
}
|
|
};
|
|
if (typeof makeWSRequest === 'function') {
|
|
makeWSRequest(
|
|
'DoModelDownloadWS',
|
|
payload,
|
|
(data) => {
|
|
if (data.error) {
|
|
onDone(false, String(data.error));
|
|
return;
|
|
}
|
|
if (data.success || data.overall_percent >= 1 || data.current_percent >= 1) {
|
|
if (data.success || data.overall_percent >= 0.99) {
|
|
onDone(true);
|
|
} else if (data.current_percent != null) {
|
|
setStatus(`Download ${(data.current_percent * 100).toFixed(0)}%`);
|
|
}
|
|
}
|
|
},
|
|
0,
|
|
(err) => onDone(false, String(err || 'Download failed')),
|
|
);
|
|
} else {
|
|
onDone(false, 'makeWSRequest unavailable');
|
|
}
|
|
}
|
|
|
|
function wantsAutoVision() {
|
|
return !!$('sa_auto_vision')?.checked;
|
|
}
|
|
|
|
function looksLikeModelPreview(src) {
|
|
const s = String(src || '').toLowerCase();
|
|
if (!s) {
|
|
return false;
|
|
}
|
|
return s.includes('.preview.')
|
|
|| s.includes('placeholder')
|
|
|| /\/models\//i.test(s);
|
|
}
|
|
|
|
function findCurrentGenerateSrc({ allowPreview = false } = {}) {
|
|
let src = null;
|
|
try {
|
|
const cur = document.getElementById('current_image_img')
|
|
|| document.querySelector('#current_image img')
|
|
|| document.querySelector('.current-image img')
|
|
|| document.querySelector('#current_image_batch img');
|
|
if (cur) {
|
|
src = cur.dataset?.src || cur.src || null;
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
if (!src) {
|
|
try {
|
|
if (typeof currentMetadataMap !== 'undefined' && currentMetadataMap && currentMetadataMap.image) {
|
|
src = currentMetadataMap.image;
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
if (!src) {
|
|
return null;
|
|
}
|
|
if (!allowPreview && looksLikeModelPreview(src)) {
|
|
return null;
|
|
}
|
|
return src;
|
|
}
|
|
|
|
function refreshImagePreview() {
|
|
if (wantsAutoVision()) {
|
|
const gen = generateSlot();
|
|
if (gen) {
|
|
gen.attach = true;
|
|
const src = findCurrentGenerateSrc();
|
|
if (src) {
|
|
gen.src = src;
|
|
}
|
|
renderBoard();
|
|
}
|
|
}
|
|
syncGenerateSlot();
|
|
}
|
|
|
|
function fileToDataUrl(file) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(String(reader.result || ''));
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
async function acceptImageFile(file, slotId) {
|
|
if (!file || !String(file.type || '').startsWith('image/')) {
|
|
setStatus('Not an image file');
|
|
return false;
|
|
}
|
|
const dataUrl = await fileToDataUrl(file);
|
|
if (slotId) {
|
|
return setSlotSrc(slotId, dataUrl, { note: `Loaded ${file.name || 'image'}` });
|
|
}
|
|
return putImageOnBoard(dataUrl, { note: `Loaded ${file.name || 'image'}` });
|
|
}
|
|
|
|
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, slotId);
|
|
}
|
|
}
|
|
}
|
|
const uri = (dt.getData('text/uri-list') || dt.getData('text/plain') || '').trim();
|
|
if (uri) {
|
|
const first = uri.split('\n').map((l) => l.trim()).find((l) => l && !l.startsWith('#'));
|
|
if (first) {
|
|
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]) {
|
|
if (slotId) {
|
|
return setSlotSrc(slotId, m[1], { note: 'Image from drag' });
|
|
}
|
|
return putImageOnBoard(m[1], { note: 'Image from drag' });
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function imageToBase64ForOllama(src, maxEdge = 1024) {
|
|
if (!src) {
|
|
return null;
|
|
}
|
|
const dataUrl = await srcToDataUrl(src);
|
|
if (!dataUrl) {
|
|
return null;
|
|
}
|
|
try {
|
|
const img = await new Promise((resolve, reject) => {
|
|
const el = new Image();
|
|
el.onload = () => resolve(el);
|
|
el.onerror = reject;
|
|
el.src = dataUrl;
|
|
});
|
|
const w = img.naturalWidth || img.width || 0;
|
|
const h = img.naturalHeight || img.height || 0;
|
|
const edge = Math.max(w, h);
|
|
const canvas = document.createElement('canvas');
|
|
if (!edge || edge <= maxEdge) {
|
|
canvas.width = Math.max(w, 1);
|
|
canvas.height = Math.max(h, 1);
|
|
canvas.getContext('2d').drawImage(img, 0, 0);
|
|
} else {
|
|
const scale = maxEdge / edge;
|
|
canvas.width = Math.max(1, Math.round(w * scale));
|
|
canvas.height = Math.max(1, Math.round(h * scale));
|
|
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
}
|
|
const jpeg = canvas.toDataURL('image/jpeg', 0.85);
|
|
const i = jpeg.indexOf(',');
|
|
return i >= 0 ? jpeg.slice(i + 1) : null;
|
|
} catch (e) {
|
|
console.warn('Assistent: vision resize failed', e);
|
|
const i = dataUrl.indexOf(',');
|
|
return i >= 0 ? dataUrl.slice(i + 1) : null;
|
|
}
|
|
}
|
|
|
|
async function srcToDataUrl(src) {
|
|
if (src.startsWith('data:')) {
|
|
return src;
|
|
}
|
|
try {
|
|
const resp = await fetch(src);
|
|
const blob = await resp.blob();
|
|
return await new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(String(reader.result || ''));
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
} catch (e) {
|
|
console.warn('Assistent: vision fetch failed', e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function loadSettings() {
|
|
const base = localStorage.getItem(LS_BASE);
|
|
const model = localStorage.getItem(LS_MODEL);
|
|
const pack = localStorage.getItem(LS_PACK);
|
|
const auto = localStorage.getItem(LS_AUTO_VISION);
|
|
const autoApply = localStorage.getItem(LS_AUTO_APPLY);
|
|
const autoGen = localStorage.getItem(LS_AUTO_GENERATE);
|
|
const autoCrit = localStorage.getItem(LS_AUTO_CRITIQUE);
|
|
const autoDl = localStorage.getItem(LS_AUTO_DOWNLOAD);
|
|
const paneW = localStorage.getItem(LS_PANE_WIDTH);
|
|
if (base && $('sa_base_url')) {
|
|
$('sa_base_url').value = base;
|
|
}
|
|
if (pack && $('sa_pack')) {
|
|
$('sa_pack').value = pack;
|
|
}
|
|
if (auto != null && $('sa_auto_vision')) {
|
|
$('sa_auto_vision').checked = auto === '1';
|
|
}
|
|
if ($('sa_auto_apply')) {
|
|
$('sa_auto_apply').checked = autoApply == null ? true : autoApply === '1';
|
|
}
|
|
if ($('sa_auto_generate')) {
|
|
$('sa_auto_generate').checked = autoGen == null ? true : autoGen === '1';
|
|
}
|
|
if ($('sa_auto_critique') && autoCrit != null) {
|
|
$('sa_auto_critique').checked = autoCrit === '1';
|
|
}
|
|
if ($('sa_auto_download') && autoDl != null) {
|
|
$('sa_auto_download').checked = autoDl === '1';
|
|
}
|
|
if (model) {
|
|
state.preferredModel = model;
|
|
}
|
|
if (paneW) {
|
|
document.documentElement.style.setProperty('--sa-image-width', paneW);
|
|
}
|
|
}
|
|
|
|
function saveSettings() {
|
|
localStorage.setItem(LS_BASE, $('sa_base_url')?.value || '');
|
|
localStorage.setItem(LS_MODEL, $('sa_model')?.value || '');
|
|
localStorage.setItem(LS_PACK, $('sa_pack')?.value || 'write_prompt');
|
|
localStorage.setItem(LS_AUTO_VISION, $('sa_auto_vision')?.checked ? '1' : '0');
|
|
localStorage.setItem(LS_AUTO_APPLY, $('sa_auto_apply')?.checked ? '1' : '0');
|
|
localStorage.setItem(LS_AUTO_GENERATE, $('sa_auto_generate')?.checked ? '1' : '0');
|
|
localStorage.setItem(LS_AUTO_CRITIQUE, $('sa_auto_critique')?.checked ? '1' : '0');
|
|
localStorage.setItem(LS_AUTO_DOWNLOAD, $('sa_auto_download')?.checked ? '1' : '0');
|
|
}
|
|
|
|
function setModelOptions(models, { error } = {}) {
|
|
const sel = $('sa_model');
|
|
if (!sel) {
|
|
return;
|
|
}
|
|
const names = (models || []).map((n) => String(n || '').trim()).filter(Boolean);
|
|
sel.innerHTML = '';
|
|
if (error) {
|
|
const opt = document.createElement('option');
|
|
opt.value = '';
|
|
opt.textContent = `⚠ ${String(error).replace(/\s+/g, ' ').slice(0, 90)}`;
|
|
sel.appendChild(opt);
|
|
sel.disabled = true;
|
|
return;
|
|
}
|
|
sel.disabled = false;
|
|
if (!names.length) {
|
|
const opt = document.createElement('option');
|
|
opt.value = '';
|
|
opt.textContent = 'No Ollama models — pull / Refresh';
|
|
sel.appendChild(opt);
|
|
return;
|
|
}
|
|
for (const name of names) {
|
|
const opt = document.createElement('option');
|
|
opt.value = name;
|
|
opt.textContent = name;
|
|
sel.appendChild(opt);
|
|
}
|
|
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL);
|
|
if (prefer && names.includes(prefer)) {
|
|
sel.value = prefer;
|
|
}
|
|
}
|
|
|
|
function refreshModels() {
|
|
const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434';
|
|
setStatus('Loading models…');
|
|
if (typeof genericRequest !== 'function') {
|
|
setStatus('SwarmUI API not ready');
|
|
setModelOptions([], { error: 'SwarmUI API not ready' });
|
|
return;
|
|
}
|
|
genericRequest(
|
|
'AssistentListModels',
|
|
{ baseUrl },
|
|
(data) => {
|
|
const models = data.models || [];
|
|
setModelOptions(models);
|
|
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL);
|
|
if (prefer && models.includes(prefer) && $('sa_model')) {
|
|
$('sa_model').value = prefer;
|
|
}
|
|
setStatus(models.length ? `${models.length} models` : 'No Ollama models (gpu-rent: ollama pull)');
|
|
saveSettings();
|
|
},
|
|
0,
|
|
(err) => {
|
|
const msg = String(err || 'Ollama unreachable');
|
|
setStatus(msg);
|
|
setModelOptions([], { error: msg });
|
|
appendMessage('error', msg);
|
|
},
|
|
);
|
|
}
|
|
|
|
function refreshInventory(done) {
|
|
if (typeof genericRequest !== 'function') {
|
|
if (done) {
|
|
done();
|
|
}
|
|
return;
|
|
}
|
|
genericRequest(
|
|
'AssistentListInventory',
|
|
{},
|
|
(data) => {
|
|
state.inventory = {
|
|
loras: data.loras || [],
|
|
checkpoints: data.checkpoints || [],
|
|
wildcards: data.wildcards || [],
|
|
has_civitai_key: !!data.has_civitai_key,
|
|
};
|
|
setStatus(`Inventory: ${state.inventory.loras.length} LoRAs, ${state.inventory.wildcards.length} wildcards`);
|
|
if (done) {
|
|
done();
|
|
}
|
|
},
|
|
0,
|
|
(err) => {
|
|
console.warn('Assistent inventory', err);
|
|
if (done) {
|
|
done();
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
async function handleReplySideEffects(reply, civitaiResults, opts = {}) {
|
|
const { fromAutoCritique, fromVisionHop } = opts;
|
|
const { patch } = extractPatch(reply);
|
|
if (Array.isArray(patch?.actions) && patch.actions.map(String).includes('interrupt')) {
|
|
doInterruptNow();
|
|
}
|
|
if (civitaiResults && civitaiResults.length && $('sa_auto_download')?.checked) {
|
|
const pick = civitaiResults.find((r) => !r.already_installed && r.download_url && r.krea_likely)
|
|
|| civitaiResults.find((r) => !r.already_installed && r.download_url);
|
|
if (pick) {
|
|
downloadCivitaiLoRA(pick, null);
|
|
}
|
|
}
|
|
if (patch && !fromVisionHop && !fromAutoCritique) {
|
|
const hopped = await maybeVisionHop(patch, opts.attachedSlotIds || []);
|
|
if (hopped) {
|
|
return;
|
|
}
|
|
}
|
|
if (patch && $('sa_auto_apply')?.checked) {
|
|
await applyPatch(patch, 'all');
|
|
if (!fromAutoCritique) {
|
|
const src = await runGenerateFromPatch(patch);
|
|
if (src) {
|
|
await maybeAutoCritique(src);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function applyQuickPatch(patch, note) {
|
|
const withActions = { ...patch };
|
|
if (!Array.isArray(withActions.actions) && patchHasGenTrigger(withActions)) {
|
|
withActions.actions = ['generate'];
|
|
}
|
|
await applyPatch(withActions, 'all');
|
|
setStatus(note || 'Applied');
|
|
if ($('sa_auto_generate')?.checked) {
|
|
await runGenerateFromPatch(withActions);
|
|
}
|
|
syncChipHighlight();
|
|
}
|
|
|
|
function syncChipHighlight() {
|
|
const bar = $('sa_chips');
|
|
if (!bar) {
|
|
return;
|
|
}
|
|
const cur = guessAspectFromSize(val('input_width'), val('input_height'));
|
|
const seed = val('input_seed');
|
|
bar.querySelectorAll('[data-aspect]').forEach((btn) => {
|
|
btn.classList.toggle('sa-chip-active', btn.getAttribute('data-aspect') === cur);
|
|
});
|
|
bar.querySelectorAll('[data-seed]').forEach((btn) => {
|
|
const mode = btn.getAttribute('data-seed');
|
|
const active = (mode === 'lock' && seed && seed !== '-1') || (mode === 'random' && (!seed || seed === '-1'));
|
|
btn.classList.toggle('sa-chip-active', active);
|
|
});
|
|
}
|
|
|
|
function appendSystemNote(text) {
|
|
const box = $('sa_messages');
|
|
if (!box) {
|
|
return;
|
|
}
|
|
hideChatEmpty();
|
|
const div = document.createElement('div');
|
|
div.className = 'sa-msg assistant sa-system-note';
|
|
div.textContent = text;
|
|
box.appendChild(div);
|
|
box.scrollTop = box.scrollHeight;
|
|
}
|
|
|
|
async function handleSlashCommand(raw) {
|
|
const text = String(raw || '').trim();
|
|
if (!text.startsWith('/')) {
|
|
return false;
|
|
}
|
|
const parts = text.slice(1).split(/\s+/);
|
|
const cmd = (parts[0] || '').toLowerCase();
|
|
const arg = parts.slice(1).join(' ').trim();
|
|
|
|
if (cmd === 'help' || cmd === '?') {
|
|
appendSystemNote(HELP_TEXT);
|
|
setStatus('/help');
|
|
return true;
|
|
}
|
|
if (cmd === 'gen' || cmd === 'generate') {
|
|
const prev = findCurrentGenerateSrc();
|
|
startBusyUi('generating');
|
|
state.generating = true;
|
|
setInterruptVisible(true);
|
|
if (!triggerGenerate()) {
|
|
state.generating = false;
|
|
stopBusyUi('Could not start Generate');
|
|
return true;
|
|
}
|
|
const src = await waitForNewImage(prev);
|
|
state.generating = false;
|
|
setInterruptVisible(state.busy);
|
|
if (src) {
|
|
const gen = generateSlot();
|
|
if (gen) {
|
|
gen.src = src;
|
|
renderBoard();
|
|
}
|
|
stopBusyUi('Generate done');
|
|
} else {
|
|
stopBusyUi('Generate finished');
|
|
}
|
|
return true;
|
|
}
|
|
if (cmd === 'look') {
|
|
const id = normalizeSlotId(arg || GEN_ID) || GEN_ID;
|
|
const slot = slotById(id);
|
|
if (!slot) {
|
|
setStatus(`Unknown slot: ${arg || GEN_ID}`);
|
|
return true;
|
|
}
|
|
if (!slot.src && id === GEN_ID) {
|
|
const src = findCurrentGenerateSrc();
|
|
if (src) {
|
|
slot.src = src;
|
|
}
|
|
}
|
|
if (!slot.src) {
|
|
setStatus(`Slot ${id} is empty`);
|
|
return true;
|
|
}
|
|
slot.attach = true;
|
|
renderBoard();
|
|
if ($('sa_input')) {
|
|
$('sa_input').value = `Look at ${id} and describe what you see.`;
|
|
}
|
|
setPackValue('critique_image', { flash: true });
|
|
await sendChat({ forceSlotIds: [id], skipAutoPack: true });
|
|
return true;
|
|
}
|
|
if (cmd === 'init') {
|
|
const src = selectedSrc() || findCurrentGenerateSrc();
|
|
if (!src) {
|
|
setStatus('No image for Init');
|
|
return true;
|
|
}
|
|
await setInitFromSrc(src);
|
|
setPackValue('inpaint_edit', { flash: true });
|
|
return true;
|
|
}
|
|
if (cmd === 'mask') {
|
|
const src = selectedSrc();
|
|
if (!src) {
|
|
setStatus('Select a window with a mask image');
|
|
return true;
|
|
}
|
|
await setMaskFromSrc(src);
|
|
setPackValue('inpaint_edit', { flash: true });
|
|
return true;
|
|
}
|
|
if (cmd === 'clear') {
|
|
clearInitAndMask();
|
|
return true;
|
|
}
|
|
if (cmd === 'interrupt' || cmd === 'stop') {
|
|
doInterruptNow();
|
|
state.busy = false;
|
|
state.generating = false;
|
|
setInterruptVisible(false);
|
|
syncGenerateBusy();
|
|
syncPatchActionAvailability();
|
|
stopBusyUi('Interrupted');
|
|
return true;
|
|
}
|
|
if (cmd === 'aspect') {
|
|
const key = normalizeAspect(arg);
|
|
if (!key) {
|
|
setStatus(`Unknown aspect. Try: ${Object.keys(ASPECT_TABLE).join(', ')}`);
|
|
return true;
|
|
}
|
|
await applyQuickPatch({ aspect: key, actions: ['generate'] }, `Aspect ${key}`);
|
|
return true;
|
|
}
|
|
if (cmd === 'seed') {
|
|
const mode = (arg || 'random').toLowerCase();
|
|
if (mode === 'lock' || mode === 'keep') {
|
|
await applyQuickPatch({ lock_seed: true }, 'Seed locked');
|
|
} else {
|
|
await applyQuickPatch({ seed: -1, vary: true, actions: ['generate'] }, 'Seed random');
|
|
}
|
|
return true;
|
|
}
|
|
if (cmd === 'vary') {
|
|
await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary (new seed)');
|
|
return true;
|
|
}
|
|
if (cmd === 'pack') {
|
|
if (!setPackValue(arg, { flash: true, user: true })) {
|
|
setStatus('Pack: write|critique|compose|params|inpaint|describe');
|
|
} else {
|
|
setStatus(`Pack → ${$('sa_pack')?.value}`);
|
|
}
|
|
return true;
|
|
}
|
|
if (cmd === 'civitai') {
|
|
if (!arg) {
|
|
setStatus('/civitai <query>');
|
|
return true;
|
|
}
|
|
if ($('sa_input')) {
|
|
$('sa_input').value = `Find a Krea 2 LoRA for: ${arg}`;
|
|
}
|
|
setPackValue('write_prompt', { flash: true });
|
|
await sendChat({
|
|
skipAutoPack: true,
|
|
forcedUserText: `Search Civitai for Krea-compatible LoRA: ${arg}. Prefer actions search_civitai.`,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
appendSystemNote(`Unknown command /${cmd}.\n\n${HELP_TEXT}`);
|
|
setStatus(`Unknown /${cmd}`);
|
|
return true;
|
|
}
|
|
|
|
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 && !opts.fromVisionHop && !opts.fromAutoCritique) {
|
|
return;
|
|
}
|
|
const rawInput = ($('sa_input')?.value || '').trim();
|
|
const text = (opts.forcedUserText || rawInput).trim();
|
|
if (!text) {
|
|
return;
|
|
}
|
|
|
|
if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.skipSlash) {
|
|
if (rawInput.startsWith('/')) {
|
|
if ($('sa_input')) {
|
|
$('sa_input').value = '';
|
|
}
|
|
const handled = await handleSlashCommand(rawInput);
|
|
if (handled) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!updateGate()) {
|
|
setStatus('Select a Krea 2 model');
|
|
return;
|
|
}
|
|
|
|
if (!opts.skipAutoPack && !opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) {
|
|
const guessed = autoSelectPack(text);
|
|
if (guessed) {
|
|
setPackValue(guessed, { flash: true });
|
|
}
|
|
}
|
|
|
|
const pack = $('sa_pack')?.value || 'write_prompt';
|
|
const model = $('sa_model')?.value;
|
|
if (!model) {
|
|
setStatus('Pick an Ollama model in ⚙');
|
|
refreshModels();
|
|
return;
|
|
}
|
|
|
|
if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) {
|
|
state.critiqueHopUsed = false;
|
|
state.visionHopUsed = 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 });
|
|
appendMessage('user', text);
|
|
$('sa_input').value = '';
|
|
|
|
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;
|
|
}
|
|
|
|
state.busy = true;
|
|
setInterruptVisible(true);
|
|
startBusyUi('thinking');
|
|
saveSettings();
|
|
|
|
const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434';
|
|
// Flat fields: SwarmUI JObject params receive the whole request body.
|
|
const payload = {
|
|
baseUrl,
|
|
model,
|
|
pack,
|
|
includeBase: true,
|
|
messages,
|
|
context_json: JSON.stringify(context),
|
|
raw: {
|
|
messages,
|
|
context_json: JSON.stringify(context),
|
|
pack,
|
|
base_url: baseUrl,
|
|
model,
|
|
},
|
|
};
|
|
|
|
const finishOk = async (reply, civitaiResults) => {
|
|
state.busy = false;
|
|
setInterruptVisible(state.generating);
|
|
state.history.push({ role: 'assistant', content: reply });
|
|
stopBusyUi('Done');
|
|
await handleReplySideEffects(reply, civitaiResults, {
|
|
...opts,
|
|
attachedSlotIds: visionSlots.map((s) => s.id),
|
|
});
|
|
};
|
|
|
|
const finishErr = (msg) => {
|
|
state.busy = false;
|
|
setInterruptVisible(state.generating);
|
|
stopBusyUi(msg);
|
|
if (state.streamEl) {
|
|
state.streamEl.classList.remove('sa-streaming', 'sa-typing');
|
|
state.streamEl.classList.add('error');
|
|
state.streamEl.textContent = msg;
|
|
state.streamEl = null;
|
|
} else {
|
|
appendMessage('error', msg);
|
|
}
|
|
};
|
|
|
|
if (typeof makeWSRequest === 'function') {
|
|
beginStreamMessage();
|
|
makeWSRequest(
|
|
'AssistentChatWS',
|
|
payload,
|
|
(data) => {
|
|
if (data.phase === 'waiting_ollama') {
|
|
setBusyPhase('loading');
|
|
const label = state.streamEl?.querySelector('.sa-typing-label');
|
|
if (label) {
|
|
label.textContent = `Loading ${modelShort(model)} into GPU…`;
|
|
}
|
|
return;
|
|
}
|
|
if (data.error) {
|
|
finishErr(String(data.error));
|
|
return;
|
|
}
|
|
if (data.clear_stream) {
|
|
if (state.streamEl) {
|
|
state.streamEl.classList.add('sa-typing');
|
|
state.streamEl.innerHTML = '<span class="sa-dots" aria-hidden="true"><i></i><i></i><i></i></span><span class="sa-typing-label">Refining…</span>';
|
|
}
|
|
setBusyPhase('refining');
|
|
return;
|
|
}
|
|
if (data.delta) {
|
|
appendStreamDelta(data.delta);
|
|
return;
|
|
}
|
|
if (data.done || data.reply != null) {
|
|
const reply = data.reply || (state.streamEl && state.streamEl.textContent) || '';
|
|
const civitai = data.civitai_results || [];
|
|
finalizeStreamMessage(reply, civitai);
|
|
finishOk(reply, civitai);
|
|
}
|
|
},
|
|
0,
|
|
(err) => {
|
|
// Fallback to HTTP AssistentChat
|
|
console.warn('AssistentChatWS failed, falling back', err);
|
|
if (state.streamEl) {
|
|
state.streamEl.remove();
|
|
state.streamEl = null;
|
|
}
|
|
genericRequest(
|
|
'AssistentChat',
|
|
payload,
|
|
(data) => {
|
|
if (data.error) {
|
|
finishErr(String(data.error));
|
|
return;
|
|
}
|
|
const reply = data.reply || '';
|
|
appendMessage('assistant', reply, null, data.civitai_results || []);
|
|
finishOk(reply, data.civitai_results || []);
|
|
},
|
|
0,
|
|
(err2) => finishErr(String(err2 || err || 'Chat failed')),
|
|
);
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
|
|
genericRequest(
|
|
'AssistentChat',
|
|
payload,
|
|
(data) => {
|
|
if (data.error) {
|
|
finishErr(String(data.error));
|
|
return;
|
|
}
|
|
const reply = data.reply || '';
|
|
appendMessage('assistant', reply, null, data.civitai_results || []);
|
|
finishOk(reply, data.civitai_results || []);
|
|
},
|
|
0,
|
|
(err) => finishErr(String(err || 'Chat failed')),
|
|
);
|
|
}
|
|
|
|
function wireDropZone() {
|
|
const board = $('sa_board');
|
|
const layout = $('sa_layout');
|
|
layout?.addEventListener('dragover', (e) => {
|
|
if (e.dataTransfer?.types?.includes('Files') || e.dataTransfer?.types?.includes('text/uri-list')) {
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
layout?.addEventListener('drop', async (e) => {
|
|
if (!e.dataTransfer) {
|
|
return;
|
|
}
|
|
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');
|
|
if (!pane || !pane.classList.contains('active')) {
|
|
return;
|
|
}
|
|
if (e.target && (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT')) {
|
|
const items = e.clipboardData?.items;
|
|
let hasImage = false;
|
|
if (items) {
|
|
for (const item of items) {
|
|
if (item.type.startsWith('image/')) {
|
|
hasImage = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!hasImage) {
|
|
return;
|
|
}
|
|
}
|
|
const items = e.clipboardData?.items;
|
|
if (!items) {
|
|
return;
|
|
}
|
|
for (const item of items) {
|
|
if (item.type.startsWith('image/')) {
|
|
e.preventDefault();
|
|
const file = item.getAsFile();
|
|
if (file) {
|
|
const sel = selectedSlot();
|
|
await acceptImageFile(file, sel && sel.type === 'ref' ? sel.id : null);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function wireSplitter() {
|
|
const splitter = $('sa_splitter');
|
|
const layout = $('sa_layout');
|
|
const pane = $('sa_image_pane');
|
|
if (!splitter || !layout || !pane) {
|
|
return;
|
|
}
|
|
let dragging = false;
|
|
splitter.addEventListener('mousedown', (e) => {
|
|
e.preventDefault();
|
|
dragging = true;
|
|
splitter.classList.add('sa-dragging');
|
|
document.body.style.cursor = 'col-resize';
|
|
document.body.style.userSelect = 'none';
|
|
});
|
|
window.addEventListener('mousemove', (e) => {
|
|
if (!dragging) {
|
|
return;
|
|
}
|
|
const rect = layout.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const pct = Math.min(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);
|
|
});
|
|
window.addEventListener('mouseup', () => {
|
|
if (!dragging) {
|
|
return;
|
|
}
|
|
dragging = false;
|
|
splitter.classList.remove('sa-dragging');
|
|
document.body.style.cursor = '';
|
|
document.body.style.userSelect = '';
|
|
});
|
|
}
|
|
|
|
function registerSendButton() {
|
|
if (typeof registerMediaButton !== 'function') {
|
|
setTimeout(registerSendButton, 500);
|
|
return;
|
|
}
|
|
if (window.__swarmAssistentMediaRegistered) {
|
|
return;
|
|
}
|
|
window.__swarmAssistentMediaRegistered = true;
|
|
registerMediaButton(
|
|
'Send to Assistent',
|
|
(src) => {
|
|
putImageOnBoard(src, {
|
|
switchTab: true,
|
|
note: 'Image sent to Assistent',
|
|
preferSelected: false,
|
|
});
|
|
const pack = $('sa_pack');
|
|
if (pack && pack.value === 'write_prompt') {
|
|
pack.value = 'critique_image';
|
|
saveSettings();
|
|
}
|
|
},
|
|
'Open Assistent with this image (vision / critique / prompt help)',
|
|
['image'],
|
|
true,
|
|
true,
|
|
);
|
|
}
|
|
|
|
function wire() {
|
|
if (!$('swarm_assistent_root')) {
|
|
return;
|
|
}
|
|
if (typeof genericRequest !== 'function') {
|
|
setTimeout(wire, 300);
|
|
return;
|
|
}
|
|
if (window.__swarmAssistentWired) {
|
|
return;
|
|
}
|
|
window.__swarmAssistentWired = true;
|
|
loadSettings();
|
|
updateGate();
|
|
ensureBoard();
|
|
renderBoard();
|
|
syncGenerateSlot();
|
|
if (wantsAutoVision()) {
|
|
refreshImagePreview();
|
|
}
|
|
maybeWelcome();
|
|
refreshModels();
|
|
refreshInventory();
|
|
wireDropZone();
|
|
wireSplitter();
|
|
registerSendButton();
|
|
|
|
$('sa_btn_settings')?.addEventListener('click', () => {
|
|
const s = $('sa_settings');
|
|
if (s) {
|
|
s.hidden = !s.hidden;
|
|
}
|
|
});
|
|
$('sa_btn_refresh_models')?.addEventListener('click', () => {
|
|
saveSettings();
|
|
refreshModels();
|
|
});
|
|
$('sa_btn_refresh_inventory')?.addEventListener('click', () => refreshInventory());
|
|
$('sa_btn_add_ref')?.addEventListener('click', () => addRefSlot({ select: true }));
|
|
$('sa_btn_use_current')?.addEventListener('click', () => snapshotGenerateToRef());
|
|
$('sa_btn_as_init')?.addEventListener('click', async () => {
|
|
const src = selectedSrc() || findCurrentGenerateSrc();
|
|
if (!src) {
|
|
setStatus('No selected/Generate image for Init');
|
|
return;
|
|
}
|
|
await setInitFromSrc(src);
|
|
const pack = $('sa_pack');
|
|
if (pack && pack.value === 'write_prompt') {
|
|
pack.value = 'inpaint_edit';
|
|
saveSettings();
|
|
}
|
|
});
|
|
$('sa_btn_as_mask')?.addEventListener('click', async () => {
|
|
const src = selectedSrc();
|
|
if (!src) {
|
|
setStatus('Select a window with an image for Mask');
|
|
return;
|
|
}
|
|
await setMaskFromSrc(src);
|
|
const pack = $('sa_pack');
|
|
if (pack) {
|
|
pack.value = 'inpaint_edit';
|
|
saveSettings();
|
|
}
|
|
});
|
|
$('sa_btn_clear_init')?.addEventListener('click', clearInitAndMask);
|
|
$('sa_btn_clear_image')?.addEventListener('click', () => 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;
|
|
}
|
|
stopBusyUi('Interrupted');
|
|
});
|
|
$('sa_btn_clear')?.addEventListener('click', () => {
|
|
state.history = [];
|
|
state.critiqueHopUsed = false;
|
|
state.visionHopUsed = false;
|
|
state.packUserTouched = 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 = '<div class="sa-chat-empty-title">Collaborative Krea 2</div><div class="sa-chat-empty-hint">Write a prompt, drop refs, use aspect chips, or type <code>/help</code>.</div>';
|
|
box.appendChild(empty);
|
|
}
|
|
stopBusyUi('');
|
|
setStatus('');
|
|
});
|
|
$('sa_input')?.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey && !e.altKey) {
|
|
e.preventDefault();
|
|
sendChat();
|
|
}
|
|
});
|
|
$('sa_base_url')?.addEventListener('change', saveSettings);
|
|
$('sa_model')?.addEventListener('change', saveSettings);
|
|
$('sa_pack')?.addEventListener('change', () => {
|
|
state.packUserTouched = true;
|
|
saveSettings();
|
|
});
|
|
$('sa_chips')?.addEventListener('click', async (e) => {
|
|
const btn = e.target.closest('.sa-chip');
|
|
if (!btn || state.busy || state.generating) {
|
|
return;
|
|
}
|
|
const aspect = btn.getAttribute('data-aspect');
|
|
const seed = btn.getAttribute('data-seed');
|
|
const vary = btn.getAttribute('data-vary');
|
|
if (aspect) {
|
|
await applyQuickPatch({ aspect, actions: ['generate'] }, `Aspect ${aspect}`);
|
|
} else if (seed === 'lock') {
|
|
await applyQuickPatch({ lock_seed: true }, 'Seed locked');
|
|
} else if (seed === 'random') {
|
|
await applyQuickPatch({ seed: -1, actions: ['generate'] }, 'Seed random');
|
|
} else if (vary) {
|
|
await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary');
|
|
}
|
|
});
|
|
$('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);
|
|
|
|
syncChipHighlight();
|
|
setInterval(syncChipHighlight, 2500);
|
|
|
|
setInterval(updateGate, 2000);
|
|
setInterval(syncGenerateSlot, 700);
|
|
setInterval(() => {
|
|
if (!state.busy) {
|
|
refreshInventory();
|
|
}
|
|
}, 120000);
|
|
|
|
window.swarmAssistent = {
|
|
setImageFromSrc,
|
|
putImageOnBoard,
|
|
clearVisionImage,
|
|
snapshotGenerateToRef,
|
|
openAssistentTab,
|
|
sendToAssistent: (src) => putImageOnBoard(src, { switchTab: true, note: 'Image sent to Assistent', preferSelected: false }),
|
|
isKreaSelected,
|
|
resolveCurrentCheckpoint,
|
|
refreshInventory,
|
|
applyPatch,
|
|
triggerGenerate,
|
|
setInitFromSrc,
|
|
setMaskFromSrc,
|
|
clearInitAndMask,
|
|
slotById,
|
|
renderBoard,
|
|
};
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', wire);
|
|
} else {
|
|
wire();
|
|
}
|
|
})();
|