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:
@@ -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
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user