Bump Assistent to 0.5: denser Krea packs, slash commands, and chips.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-21 21:15:46 +03:00
co-authored by Cursor
parent bb8cb990aa
commit b8145c6455
12 changed files with 741 additions and 88 deletions
+47
View File
@@ -691,6 +691,53 @@
line-height: 1.35;
}
.sa-chips {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
align-items: center;
}
.sa-chip {
appearance: none;
border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
background: color-mix(in srgb, currentColor 6%, transparent);
color: inherit;
border-radius: 999px;
padding: 0.18rem 0.55rem;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.02em;
cursor: pointer;
line-height: 1.3;
}
.sa-chip:hover {
background: color-mix(in srgb, currentColor 12%, transparent);
}
.sa-chip.sa-chip-active {
border-color: color-mix(in srgb, #6cf 55%, currentColor);
background: color-mix(in srgb, #6cf 22%, transparent);
}
.sa-chip-sep {
width: 1px;
height: 1.1rem;
background: color-mix(in srgb, currentColor 28%, transparent);
margin: 0 0.15rem;
}
.sa-pack-flash {
animation: sa-flash 0.85s ease;
}
.sa-system-note {
white-space: pre-wrap;
opacity: 0.92;
font-size: 0.92rem;
}
.sa-composer-actions {
display: flex;
flex-wrap: wrap;
+461 -15
View File
@@ -1,6 +1,6 @@
/**
* Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API).
* v0.4.0: multi-window board, look_at slots, latest-patch buttons, first-run welcome.
* v0.5.0: Krea knowledge packs, aspect/vary/lock_seed patches, slash commands, chips.
*/
(function () {
const LS_BASE = 'swarm_assistent_base_url';
@@ -17,16 +17,57 @@
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,
@@ -46,6 +87,7 @@
slots: [],
selectedSlotId: 'ref1',
refSeq: 1,
packUserTouched: false,
};
function $(id) {
@@ -1151,7 +1193,19 @@
obj.slot_to_init != null ||
obj.slot_to_mask != null ||
obj.snapshot_generate != null ||
obj.select_slot != 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
);
}
@@ -1176,6 +1230,120 @@
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;
@@ -1188,16 +1356,21 @@
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_mask_image ||
patch.clear_prompt_images
);
}
@@ -1210,6 +1383,14 @@
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) {
@@ -1263,11 +1444,17 @@
}
if (doParams) {
if (patch.width != null) {
setVal('input_width', String(patch.width));
}
if (patch.height != null) {
setVal('input_height', String(patch.height));
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));
@@ -1279,7 +1466,14 @@
setVal('input_cfg', String(patch.cfg));
}
}
if (patch.seed != null) {
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) {
@@ -1293,6 +1487,14 @@
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) {
@@ -1361,7 +1563,11 @@
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');
}
@@ -2019,6 +2225,199 @@
}
}
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) {
@@ -2058,17 +2457,39 @@
}
async function sendChat(opts = {}) {
if (state.busy) {
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;
}
const text = ($('sa_input')?.value || '').trim();
if (!text) {
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) {
@@ -2467,13 +2888,14 @@
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 on the board, or send Generate via Send to Assistent.</div>';
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('');
@@ -2487,7 +2909,28 @@
});
$('sa_base_url')?.addEventListener('change', saveSettings);
$('sa_model')?.addEventListener('change', saveSettings);
$('sa_pack')?.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();
@@ -2501,6 +2944,9 @@
$('sa_auto_critique')?.addEventListener('change', saveSettings);
$('sa_auto_download')?.addEventListener('change', saveSettings);
syncChipHighlight();
setInterval(syncChipHighlight, 2500);
setInterval(updateGate, 2000);
setInterval(syncGenerateSlot, 700);
setInterval(() => {