Bump Assistent to 0.7.0: Config personas, skills, and vector memory.

Move prompts into Config/_base and persona folders; seed model facts into SQLite via Ollama embed and retrieve as memory_hits each turn.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-21 22:47:04 +03:00
co-authored by Cursor
parent 46264d7227
commit fe4d8d3a3a
72 changed files with 2593 additions and 507 deletions
+17
View File
@@ -556,6 +556,23 @@
font-size: 0.9rem;
}
.sa-skills-label {
font-size: 0.85rem;
opacity: 0.85;
margin-top: 0.25rem;
}
.sa-skills-box {
display: flex;
flex-wrap: wrap;
gap: 0.35rem 0.75rem;
margin-bottom: 0.25rem;
}
.sa-settings .sa-select {
width: 100%;
}
.sa-messages {
flex: 1;
overflow: auto;
+277 -48
View File
@@ -1,10 +1,11 @@
/**
* Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API).
* v0.6.0: board tabs, Cards form+Civitai fetch, LoRA chips, taste on disk, reliability fixes.
* v0.7.0: Config presets, persona folders, vector memory, chat|memory model roles.
*/
(function () {
const LS_BASE = 'swarm_assistent_base_url';
const LS_MODEL = 'swarm_assistent_model';
const LS_EMBED = 'swarm_assistent_embed_model';
const LS_PACK = 'swarm_assistent_pack';
const LS_PERSONA = 'swarm_assistent_persona';
const LS_VIEW = 'swarm_assistent_view';
@@ -23,7 +24,7 @@
const MAX_REF_SLOTS = 4;
const CONTEXT_PROMPT_MAX = 2000;
const ASPECT_TABLE = {
let ASPECT_TABLE = {
'1:1': [1024, 1024],
'4:3': [1184, 896],
'3:2': [1248, 832],
@@ -34,7 +35,7 @@
'9:16': [768, 1376],
};
const PACK_ALIASES = {
let PACK_ALIASES = {
write: 'write_prompt',
write_prompt: 'write_prompt',
critique: 'critique_image',
@@ -52,7 +53,7 @@
catalog_card: 'catalog_card',
};
const WELCOME_HTML = `
let WELCOME_HTML = `
<div class="sa-welcome-title">Assistent · Krea 2</div>
<ul>
<li><strong>Generate</strong> слева — живой просмотр. В чат сам не уходит.</li>
@@ -63,7 +64,7 @@
</ul>
Напиши, что сгенерировать — или кинь референс и попроси правку.`;
const HELP_TEXT = `Slash-команды (без LLM):
let HELP_TEXT = `Slash-команды (без LLM):
/help — этот список
/gen — Generate сейчас
/look generate|refN — прикрепить окно к vision
@@ -78,7 +79,7 @@
Чипсы над полем ввода делают то же для aspect / seed / vary / Turbo·RAW.`;
const SLASH_COMMANDS = [
let SLASH_COMMANDS = [
{ cmd: '/help', hint: 'список команд' },
{ cmd: '/gen', hint: 'Generate сейчас' },
{ cmd: '/look ', hint: 'generate|refN' },
@@ -97,6 +98,10 @@
const state = {
history: [],
packsLoaded: false,
config: null,
enabledSkills: [],
kreaProfiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 }, raw: { steps: 28, cfg: 4.5 } },
preferredEmbed: null,
busy: false,
generating: false,
chatEpoch: 0,
@@ -1138,11 +1143,23 @@
function onPersonaChanged() {
const id = $('sa_persona')?.value || 'neutral';
const info = (state.personas || []).find((p) => p.id === id);
const title = info?.title || id;
saveSettings();
appendSystemNote(`Тон → ${title}`);
state.pendingPersonaNote = `Persona is now ${id} (${title}). Adopt this voice from now on.`;
loadConfig(id, (data) => {
const title = data?.personas?.find((p) => p.id === id)?.title
|| (state.personas || []).find((p) => p.id === id)?.title
|| id;
if (data?.personas) {
state.personas = data.personas;
}
appendSystemNote(`Тон → ${title}`);
state.pendingPersonaNote = `Persona is now ${id} (${title}). Adopt this voice from now on.`;
if (data?.assistant?.default_pack && $('sa_pack') && !state.packUserTouched) {
const packId = data.assistant.default_pack;
if ([...($('sa_pack').options || [])].some((o) => o.value === packId)) {
$('sa_pack').value = packId;
}
}
});
}
function countPromptImages() {
@@ -2765,6 +2782,10 @@
if (model) {
state.preferredModel = model;
}
const embed = localStorage.getItem(LS_EMBED);
if (embed) {
state.preferredEmbed = embed;
}
if (paneW) {
document.documentElement.style.setProperty('--sa-image-width', paneW);
}
@@ -2780,6 +2801,7 @@
function saveSettings() {
localStorage.setItem(LS_BASE, $('sa_base_url')?.value || '');
localStorage.setItem(LS_MODEL, $('sa_model')?.value || '');
localStorage.setItem(LS_EMBED, $('sa_embed_model')?.value || state.preferredEmbed || '');
localStorage.setItem(LS_PACK, $('sa_pack')?.value || 'write_prompt');
localStorage.setItem(LS_PERSONA, $('sa_persona')?.value || 'neutral');
localStorage.setItem(LS_VIEW, state.view || 'chat');
@@ -2788,6 +2810,193 @@
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');
persistServerSettings();
}
function persistServerSettings() {
if (typeof genericRequest !== 'function') {
return;
}
const skills = {};
document.querySelectorAll('#sa_skills_box input[data-skill]')?.forEach((el) => {
skills[el.getAttribute('data-skill')] = !!el.checked;
});
const persona = $('sa_persona')?.value || 'neutral';
const settings = {
embed_model: $('sa_embed_model')?.value || state.preferredEmbed || '',
base_url: $('sa_base_url')?.value || '',
[persona]: { skills },
};
genericRequest('AssistentSaveSettings', { settings }, () => {}, 0, () => {});
}
function applyConfigPayload(data, { applyDefaults = false } = {}) {
if (!data || data.error) {
return;
}
state.config = data;
if (data.model?.aspect_table && typeof data.model.aspect_table === 'object') {
const next = {};
for (const [k, v] of Object.entries(data.model.aspect_table)) {
if (Array.isArray(v) && v.length >= 2) {
next[k] = [Number(v[0]), Number(v[1])];
}
}
if (Object.keys(next).length) {
ASPECT_TABLE = next;
}
}
if (data.model?.profiles) {
state.kreaProfiles = data.model.profiles;
}
if (data.ui?.pack_aliases) {
PACK_ALIASES = { ...PACK_ALIASES, ...data.ui.pack_aliases };
}
if (data.ui?.welcome_html) {
WELCOME_HTML = data.ui.welcome_html;
}
if (data.ui?.help_text) {
HELP_TEXT = data.ui.help_text;
}
if (Array.isArray(data.ui?.slash) && data.ui.slash.length) {
SLASH_COMMANDS = data.ui.slash.map((s) => ({
cmd: s.cmd || '',
hint: s.hint || '',
action: s.action || '',
}));
}
state.enabledSkills = Array.isArray(data.enabled_skills) ? data.enabled_skills.slice() : [];
if (Array.isArray(data.personas)) {
state.personas = data.personas;
}
renderPersonaOptions(data.personas || [], data.persona || data.default_persona);
renderPackOptions(data.packs || [], applyDefaults ? data.assistant?.default_pack : null);
renderChips(data.ui?.chips || []);
renderSkillChecks(data.skills || [], state.enabledSkills);
if (applyDefaults && data.assistant?.default_pack && $('sa_pack') && !localStorage.getItem(LS_PACK)) {
$('sa_pack').value = data.assistant.default_pack;
}
if (data.assistant?.embed_model && !state.preferredEmbed) {
state.preferredEmbed = data.assistant.embed_model;
}
}
function renderPersonaOptions(personas, selected) {
const sel = $('sa_persona');
if (!sel) {
return;
}
const cur = selected || sel.value || localStorage.getItem(LS_PERSONA) || 'neutral';
sel.innerHTML = '';
for (const p of personas) {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.title || p.id;
if (p.accent) {
opt.dataset.accent = p.accent;
}
sel.appendChild(opt);
}
if ([...sel.options].some((o) => o.value === cur)) {
sel.value = cur;
}
}
function renderPackOptions(packs, preferred) {
const sel = $('sa_pack');
if (!sel) {
return;
}
const cur = preferred || sel.value || localStorage.getItem(LS_PACK) || 'write_prompt';
sel.innerHTML = '';
const list = (packs || []).slice().sort((a, b) => (a.order || 100) - (b.order || 100));
for (const p of list) {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.title || p.id;
sel.appendChild(opt);
}
if ([...sel.options].some((o) => o.value === cur)) {
sel.value = cur;
}
}
function renderChips(chips) {
const box = $('sa_chips');
if (!box || !Array.isArray(chips) || !chips.length) {
return;
}
box.innerHTML = '';
for (const c of chips) {
if (c.sep) {
const sep = document.createElement('span');
sep.className = 'sa-chip-sep';
sep.setAttribute('aria-hidden', 'true');
box.appendChild(sep);
continue;
}
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'sa-chip';
btn.textContent = c.label || c.value || '';
if (c.title) {
btn.title = c.title;
}
const action = c.action || '';
const value = c.value ?? '';
if (action === 'aspect') {
btn.setAttribute('data-aspect', value);
} else if (action === 'seed') {
btn.setAttribute('data-seed', value);
} else if (action === 'vary') {
btn.setAttribute('data-vary', value || '1');
} else if (action === 'krea_profile') {
btn.setAttribute('data-krea-profile', value);
}
box.appendChild(btn);
}
}
function renderSkillChecks(skills, enabled) {
const box = $('sa_skills_box');
if (!box) {
return;
}
const on = new Set(enabled || []);
box.innerHTML = '';
for (const s of skills || []) {
const label = document.createElement('label');
label.className = 'sa-check';
const input = document.createElement('input');
input.type = 'checkbox';
input.setAttribute('data-skill', s.id);
input.checked = on.has(s.id) || (!enabled?.length && !!s.default);
input.addEventListener('change', () => {
state.enabledSkills = [...document.querySelectorAll('#sa_skills_box input[data-skill]:checked')].map((el) => el.getAttribute('data-skill'));
saveSettings();
});
label.appendChild(input);
label.appendChild(document.createTextNode(` ${s.title || s.id}`));
box.appendChild(label);
}
state.enabledSkills = [...document.querySelectorAll('#sa_skills_box input[data-skill]:checked')].map((el) => el.getAttribute('data-skill'));
}
function loadConfig(persona, done) {
if (typeof genericRequest !== 'function') {
done?.(null);
return;
}
genericRequest(
'AssistentGetConfig',
{ persona: persona || $('sa_persona')?.value || 'neutral' },
(data) => {
applyConfigPayload(data, { applyDefaults: true });
done?.(data);
},
0,
() => done?.(null),
);
}
function setModelOptions(models, { error } = {}) {
@@ -2825,6 +3034,38 @@
}
}
function setEmbedModelOptions(models) {
const sel = $('sa_embed_model');
if (!sel) {
return;
}
const names = (models || []).map((n) => String(n || '').trim()).filter(Boolean);
sel.innerHTML = '';
if (!names.length) {
const opt = document.createElement('option');
opt.value = state.preferredEmbed || 'nomic-embed-text';
opt.textContent = opt.value + ' (ожидается pull)';
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.preferredEmbed || localStorage.getItem(LS_EMBED) || state.config?.assistant?.embed_model;
if (prefer && names.includes(prefer)) {
sel.value = prefer;
} else if (prefer && !names.includes(prefer)) {
const opt = document.createElement('option');
opt.value = prefer;
opt.textContent = prefer;
sel.appendChild(opt);
sel.value = prefer;
}
}
function refreshModels() {
const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434';
setStatus('Loading models…');
@@ -2838,12 +3079,14 @@
{ baseUrl },
(data) => {
const models = data.models || [];
const memoryModels = data.memory_models || [];
setModelOptions(models);
setEmbedModelOptions(memoryModels);
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)');
setStatus(models.length ? `${models.length} chat · ${memoryModels.length} memory` : 'No Ollama models (gpu-rent: ollama pull)');
saveSettings();
},
0,
@@ -2928,36 +3171,11 @@
}
function refreshPersonas() {
if (typeof genericRequest !== 'function') {
return;
}
genericRequest(
'AssistentListPersonas',
{},
(data) => {
const list = data.personas || [];
state.personas = list;
const sel = $('sa_persona');
if (!sel) {
return;
}
const prefer = localStorage.getItem(LS_PERSONA) || data.default || 'neutral';
sel.innerHTML = '';
for (const p of list) {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.title || p.id;
sel.appendChild(opt);
}
if ([...sel.options].some((o) => o.value === prefer)) {
sel.value = prefer;
} else if (data.default) {
sel.value = data.default;
}
},
0,
(err) => console.warn('Assistent personas', err),
);
loadConfig($('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral', (data) => {
if (data?.personas) {
state.personas = data.personas;
}
});
}
function prefetchCard(kind, name) {
@@ -4132,6 +4350,8 @@
includeBase: true,
messages,
context_json: JSON.stringify(context),
skills: state.enabledSkills || [],
embed_model: $('sa_embed_model')?.value || state.preferredEmbed || '',
raw: {
messages,
context_json: JSON.stringify(context),
@@ -4139,6 +4359,8 @@
persona,
base_url: baseUrl,
model,
skills: state.enabledSkills || [],
embed_model: $('sa_embed_model')?.value || state.preferredEmbed || '',
},
};
@@ -4431,11 +4653,12 @@
}
restoreHistory();
maybeWelcome();
refreshModels();
refreshPersonas();
refreshInventory(() => {
renderCardsList();
renderLoraChips();
loadConfig(localStorage.getItem(LS_PERSONA) || 'neutral', () => {
refreshModels();
refreshInventory(() => {
renderCardsList();
renderLoraChips();
});
});
wireDropZone();
wireSplitter();
@@ -4537,6 +4760,10 @@
});
$('sa_base_url')?.addEventListener('change', saveSettings);
$('sa_model')?.addEventListener('change', saveSettings);
$('sa_embed_model')?.addEventListener('change', () => {
state.preferredEmbed = $('sa_embed_model')?.value || '';
saveSettings();
});
$('sa_pack')?.addEventListener('change', () => {
state.packUserTouched = true;
saveSettings();
@@ -4559,9 +4786,11 @@
} else if (vary) {
await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary');
} else if (profile === 'turbo') {
await applyQuickPatch({ steps: 8, cfg: 1, sigma_shift: 1.15, actions: ['generate'] }, 'Turbo 8/1');
const p = state.kreaProfiles?.turbo || { steps: 8, cfg: 1, sigma_shift: 1.15 };
await applyQuickPatch({ steps: p.steps ?? 8, cfg: p.cfg ?? 1, sigma_shift: p.sigma_shift ?? 1.15, actions: ['generate'] }, 'Turbo');
} else if (profile === 'raw') {
await applyQuickPatch({ steps: 28, cfg: 4.5, actions: ['generate'] }, 'RAW 28/4.5');
const p = state.kreaProfiles?.raw || { steps: 28, cfg: 4.5 };
await applyQuickPatch({ steps: p.steps ?? 28, cfg: p.cfg ?? 4.5, actions: ['generate'] }, 'RAW');
}
renderLoraChips();
});