Ship Assistent 0.10: settings panel and UserPrefs with prompt weight.

Separate About-the-user memory (global + per-persona) from craft RAG, add tabbed settings with persona export/import and craft clear APIs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-22 02:05:12 +03:00
co-authored by Cursor
parent cf89348f85
commit fa73158e1c
18 changed files with 2081 additions and 191 deletions
+715 -42
View File
@@ -160,6 +160,9 @@
slashIndex: 0,
llmParked: false,
memoryRows: [],
userPrefs: [],
settingsTab: 'behavior',
settingsPersonaId: null,
wanted: { count: 0, items: [] },
wantedKeys: new Set(),
ollamaHealth: 'unknown',
@@ -2152,6 +2155,18 @@
fillEmptyParamsFromExact();
renderPersonaControls(data?.controls || {}, data?.control_values || data?.exact?.controls || {});
syncPersonaDeleteButton(data?.persona_source || data?.personas?.find((p) => p.id === id)?.source);
const settings = $('sa_settings');
if (settings && !settings.hidden) {
if (state.settingsTab === 'user') {
refreshUserPrefs();
}
if (state.settingsTab === 'craft') {
renderMemoryList();
}
if (state.settingsTab === 'more') {
fillKnobsFromConfig(data);
}
}
});
}
@@ -2371,7 +2386,7 @@
auto_generate: !!$('sa_auto_generate')?.checked,
persona: $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral',
model_cards: [],
taste_profile: summarizeTaste(),
user_prefs_count: 0,
...initCtx,
};
@@ -2696,6 +2711,34 @@
taste.updated = Date.now();
state.taste = taste;
saveTaste();
syncTasteHintsToUserPrefs(taste);
}
function syncTasteHintsToUserPrefs(taste) {
if (typeof genericRequest !== 'function' || !taste) {
return;
}
const upsert = (key, text) => {
if (!text) {
return;
}
genericRequest(
'AssistentUpsertUserPref',
{ key, text: String(text).slice(0, 240), scope: 'global', source: 'migrated_taste', pinned: false },
() => {},
0,
() => {},
);
};
if (taste.avoid?.[0]) {
upsert('avoid_hint', `Avoid: ${taste.avoid.slice(0, 4).join('; ')}`);
}
if (taste.styles?.[0]) {
upsert('style_hint', `Styles: ${taste.styles.slice(0, 4).join('; ')}`);
}
if (taste.likes?.[0]) {
upsert('like_hint', `Often uses: ${taste.likes.slice(0, 4).join('; ')}`);
}
}
// Fallback key list — only used if assistent.patch.js failed to load.
@@ -2703,6 +2746,7 @@
'prompt', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler',
'actions', 'search_query', 'civitai_query', 'init_creativity', 'denoise',
'look_at', 'vision_from', 'vision_slots', 'aspect', 'batch', 'vary', 'lock_seed', 'pack',
'memories', 'user_prefs',
];
function isPatchObject(obj) {
@@ -4265,6 +4309,7 @@
if (asst.context_prompt_max != null) {
CONTEXT_PROMPT_MAX = Math.max(200, Number(asst.context_prompt_max) || 2000);
}
fillKnobsFromConfig(data);
if (applyDefaults || data.exact) {
fillEmptyParamsFromExact();
}
@@ -4537,37 +4582,42 @@
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;
}
const sel2 = $('sa_settings_chat_model');
const apply = (target) => {
if (!target) {
return;
}
const names = (models || []).map((n) => String(n || '').trim()).filter(Boolean);
target.innerHTML = '';
if (error) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = `${String(error).replace(/\s+/g, ' ').slice(0, 90)}`;
target.appendChild(opt);
target.disabled = true;
return;
}
target.disabled = false;
if (!names.length) {
const opt = document.createElement('option');
opt.value = '';
opt.textContent = 'No Ollama models — pull / Refresh';
target.appendChild(opt);
return;
}
for (const name of names) {
const opt = document.createElement('option');
opt.value = name;
opt.textContent = name;
target.appendChild(opt);
}
const prefer = state.preferredModel || localStorage.getItem(LS_MODEL);
if (prefer && names.includes(prefer)) {
target.value = prefer;
}
};
apply(sel);
apply(sel2);
}
function setEmbedModelOptions(models) {
@@ -4691,6 +4741,14 @@
return $('sa_mem_kind')?.value || 'all';
}
function memoryScopeFilter() {
return $('sa_mem_scope')?.value || 'all';
}
function memorySearchFilter() {
return ($('sa_mem_search')?.value || '').trim().toLowerCase();
}
function renderMemoryKinds(kinds) {
const sel = $('sa_mem_kind');
if (!sel) {
@@ -4713,16 +4771,40 @@
}
}
function filteredMemoryRows() {
const filter = memoryKindFilter();
const scope = memoryScopeFilter();
const q = memorySearchFilter();
const persona = $('sa_persona')?.value || 'neutral';
return (state.memoryRows || []).filter((m) => {
if (filter !== 'all' && m.kind !== filter) {
return false;
}
if (scope === 'shared' && m.scope !== 'shared') {
return false;
}
if (scope === 'personal' && !(m.scope === 'personal' && (m.persona === persona || !m.persona))) {
return false;
}
if (q) {
const hay = `${m.kind || ''} ${m.key || ''} ${m.text || ''}`.toLowerCase();
if (!hay.includes(q)) {
return false;
}
}
return true;
});
}
function renderMemoryList() {
const root = $('sa_mem_list');
if (!root) {
return;
}
const filter = memoryKindFilter();
const rows = (state.memoryRows || []).filter((m) => filter === 'all' || m.kind === filter);
const rows = filteredMemoryRows();
root.innerHTML = '';
if (!rows.length) {
root.innerHTML = '<div class="sa-mem-empty">Память пуста — она наполняется из карточек, seed и патчей <code>memory_upsert</code>.</div>';
root.innerHTML = '<div class="sa-mem-empty">Крафт-память пуста — карточки, seed и патчи <code>memory_upsert</code>.</div>';
return;
}
for (const row of rows) {
@@ -4800,6 +4882,533 @@
);
}
function clearCraftMemory(opts = {}) {
if (typeof genericRequest !== 'function') {
return;
}
const label = opts.label || 'крафт-память';
if (!window.confirm(`Очистить ${label}? Bundled seed останется.`)) {
return;
}
const body = {
scope: opts.scope || '',
kind: opts.kind || '',
persona: opts.persona || '',
};
genericRequest(
'AssistentClearMemory',
body,
(data) => {
setStatus(`Удалено крафт-записей: ${data?.deleted ?? 0}`);
refreshMemoryList();
},
0,
(err) => setStatus(String(err || 'Очистка не удалась')),
);
}
function setSettingsTab(id) {
state.settingsTab = id || 'behavior';
document.querySelectorAll('#sa_settings .sa-stab').forEach((btn) => {
const on = btn.getAttribute('data-stab') === state.settingsTab;
btn.classList.toggle('sa-stab-active', on);
btn.setAttribute('aria-selected', on ? 'true' : 'false');
});
document.querySelectorAll('#sa_settings .sa-spane').forEach((pane) => {
pane.hidden = pane.getAttribute('data-spane') !== state.settingsTab;
});
if (state.settingsTab === 'craft') {
refreshMemoryList();
refreshWantedQueue();
}
if (state.settingsTab === 'user') {
refreshUserPrefs();
}
if (state.settingsTab === 'personas') {
renderPersonaSettingsList();
}
if (state.settingsTab === 'models') {
syncSettingsHealthLine();
const m = $('sa_model')?.value;
if (m && $('sa_settings_chat_model')) {
$('sa_settings_chat_model').value = m;
}
}
if (state.settingsTab === 'more') {
fillKnobsFromConfig(state.config);
}
}
function openSettings(tab) {
const s = $('sa_settings');
if (!s) {
return;
}
s.hidden = false;
setSettingsTab(tab || state.settingsTab || 'behavior');
}
function closeSettings() {
const s = $('sa_settings');
if (s) {
s.hidden = true;
}
}
function fillKnobsFromConfig(data) {
const asst = data?.assistant || state.config?.assistant || {};
const exact = data?.exact || state.config?.exact || state.exact || {};
const setNum = (id, v) => {
const el = $(id);
if (el && v != null && Number.isFinite(Number(v))) {
el.value = String(v);
}
};
setNum('sa_num_ctx', asst.num_ctx);
setNum('sa_history_keep', asst.history_keep_turns);
setNum('sa_memory_top_k', asst.memory_top_k);
const w = asst.user_prefs_weight != null ? Number(asst.user_prefs_weight) : 1;
const weightEl = $('sa_user_prefs_weight');
if (weightEl) {
weightEl.value = String(Math.max(0, Math.min(1.5, w)));
const lab = $('sa_user_prefs_weight_val');
if (lab) {
lab.textContent = Number(weightEl.value).toFixed(1);
}
}
const turbo = exact.profiles?.turbo || {};
const raw = exact.profiles?.raw || {};
setNum('sa_exact_turbo_steps', turbo.steps);
setNum('sa_exact_turbo_cfg', turbo.cfg);
setNum('sa_exact_turbo_sigma', turbo.sigma_shift);
setNum('sa_exact_raw_steps', raw.steps);
setNum('sa_exact_raw_cfg', raw.cfg);
setNum('sa_exact_raw_sigma', raw.sigma_shift);
}
function saveKnobs() {
if (typeof genericRequest !== 'function') {
return;
}
const num = (id) => {
const v = parseFloat($(id)?.value);
return Number.isFinite(v) ? v : null;
};
const assistant = {
num_ctx: num('sa_num_ctx'),
history_keep_turns: num('sa_history_keep'),
memory_top_k: num('sa_memory_top_k'),
user_prefs_weight: num('sa_user_prefs_weight'),
};
Object.keys(assistant).forEach((k) => {
if (assistant[k] == null) {
delete assistant[k];
}
});
const exact = {
profiles: {
turbo: {
steps: num('sa_exact_turbo_steps'),
cfg: num('sa_exact_turbo_cfg'),
sigma_shift: num('sa_exact_turbo_sigma'),
},
raw: {
steps: num('sa_exact_raw_steps'),
cfg: num('sa_exact_raw_cfg'),
sigma_shift: num('sa_exact_raw_sigma'),
},
},
};
genericRequest(
'AssistentSaveKnobs',
{ assistant, exact },
(data) => {
if (data?.assistant || data?.exact) {
applyConfigPayload({
...state.config,
assistant: data.assistant || state.config?.assistant,
exact: data.exact || state.config?.exact,
});
}
setStatus('Knobs сохранены в overlay');
},
0,
(err) => setStatus(String(err || 'Не удалось сохранить knobs')),
);
}
function syncSettingsHealthLine() {
const line = $('sa_settings_health_line');
const badge = $('sa_ollama_health');
if (line && badge) {
line.textContent = badge.textContent || 'Ollama · …';
line.className = 'sa-settings-health ' + (badge.className || '').replace('sa-health', '').trim();
}
}
function personaSourceLabel(source) {
if (source === 'overlay') {
return 'моя';
}
if (source === 'overlay+bundled') {
return 'встроено+правка';
}
return 'встроено';
}
function renderPersonaSettingsList() {
const root = $('sa_persona_list');
if (!root) {
return;
}
const list = state.personas || state.config?.personas || [];
const cur = state.settingsPersonaId || $('sa_persona')?.value || list[0]?.id;
state.settingsPersonaId = cur;
root.innerHTML = '';
for (const p of list) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'sa-persona-item' + (p.id === cur ? ' sa-persona-item-active' : '');
const accent = p.accent || 'currentColor';
btn.innerHTML = `<div class="sa-persona-item-title"><span class="sa-persona-dot" style="background:${escapeHtml(accent)}"></span><span>${escapeHtml(p.title || p.id)}</span></div><div class="sa-persona-badge">${escapeHtml(personaSourceLabel(p.source))}</div>`;
btn.addEventListener('click', () => {
state.settingsPersonaId = p.id;
renderPersonaSettingsList();
loadPersonaPreview(p.id);
});
root.appendChild(btn);
}
syncPersonaPanelActions();
if (cur) {
loadPersonaPreview(cur);
}
}
function syncPersonaPanelActions() {
const id = state.settingsPersonaId;
const p = (state.personas || []).find((x) => x.id === id);
const canDelete = p && (p.source === 'overlay' || p.source === 'overlay+bundled');
const del = $('sa_btn_persona_delete_panel');
if (del) {
del.disabled = !canDelete;
}
}
function loadPersonaPreview(id) {
const box = $('sa_persona_preview');
if (!box || typeof genericRequest !== 'function') {
return;
}
box.innerHTML = '<div class="sa-mem-empty">Загрузка…</div>';
genericRequest(
'AssistentGetPersonaShelves',
{ persona: id },
(data) => {
const summary = data?.identity_summary || '';
const src = data?.source || '';
box.textContent = `${id} · ${personaSourceLabel(src)}\n\n${summary || '(пусто)'}`;
syncPersonaPanelActions();
},
0,
(err) => {
box.innerHTML = `<div class="sa-mem-empty">${escapeHtml(String(err || 'ошибка'))}</div>`;
},
);
}
function exportSelectedPersona() {
const id = state.settingsPersonaId || $('sa_persona')?.value;
if (!id || typeof genericRequest !== 'function') {
return;
}
genericRequest(
'AssistentExportPersona',
{ persona: id },
(data) => {
const pack = data?.pack;
if (!pack) {
setStatus('Пустой экспорт');
return;
}
const blob = new Blob([JSON.stringify(pack, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${pack.id || id}.assistent-persona.json`;
a.click();
URL.revokeObjectURL(a.href);
setStatus(`Экспорт: ${a.download}`);
},
0,
(err) => setStatus(String(err || 'Экспорт не удался')),
);
}
function importPersonaFile(file) {
if (!file || typeof genericRequest !== 'function') {
return;
}
const reader = new FileReader();
reader.onload = () => {
let pack;
try {
pack = JSON.parse(String(reader.result || ''));
} catch (e) {
setStatus('Невалидный JSON');
return;
}
let newId = pack?.id || '';
if ((state.personas || []).some((p) => p.id === newId && (p.source === 'bundled' || p.source === 'overlay+bundled'))) {
newId = window.prompt('Id занят bundled — новый id:', `${newId}_import`) || '';
}
genericRequest(
'AssistentImportPersona',
{ pack, new_id: newId || null, overwrite: false },
(data) => {
if (Array.isArray(data?.personas)) {
state.personas = data.personas;
renderPersonaOptions(data.personas, data.persona?.id);
}
state.settingsPersonaId = data?.persona?.id || newId;
renderPersonaSettingsList();
setStatus(`Импорт: ${data?.persona?.id || newId}`);
},
0,
(err) => setStatus(String(err || 'Импорт не удался')),
);
};
reader.readAsText(file);
}
function cloneSelectedPersona() {
const from = state.settingsPersonaId || $('sa_persona')?.value;
if (!from) {
return;
}
const to = window.prompt('Новый id личности:', `${from}_copy`);
if (!to) {
return;
}
genericRequest(
'AssistentClonePersona',
{ from, to, title: to },
(data) => {
if (Array.isArray(data?.personas)) {
state.personas = data.personas;
renderPersonaOptions(data.personas, to);
}
state.settingsPersonaId = to;
renderPersonaSettingsList();
setStatus(`Клон: ${to}`);
},
0,
(err) => setStatus(String(err || 'Клон не удался')),
);
}
function deleteSelectedOverlayPersona() {
const id = state.settingsPersonaId;
const p = (state.personas || []).find((x) => x.id === id);
if (!p || (p.source !== 'overlay' && p.source !== 'overlay+bundled')) {
setStatus('Можно удалить только overlay');
return;
}
if (!window.confirm(`Удалить overlay-личность «${id}»?`)) {
return;
}
genericRequest(
'AssistentDeletePersona',
{ persona: id },
(data) => {
if (Array.isArray(data?.personas)) {
state.personas = data.personas;
renderPersonaOptions(data.personas, data.default_persona);
}
state.settingsPersonaId = data?.default_persona || null;
renderPersonaSettingsList();
setStatus(`Удалено: ${id}`);
},
0,
(err) => setStatus(String(err || 'Удаление не удалось')),
);
}
function refreshUserPrefs() {
if (typeof genericRequest !== 'function') {
return;
}
const persona = $('sa_persona')?.value || 'neutral';
genericRequest(
'AssistentListUserPrefs',
{ persona, limit: 200 },
(data) => {
state.userPrefs = Array.isArray(data?.prefs) ? data.prefs : [];
renderUserPrefsLists();
},
0,
(err) => setStatus(String(err || 'User prefs недоступны')),
);
}
function renderUserPrefsLists() {
const persona = $('sa_persona')?.value || 'neutral';
const global = (state.userPrefs || []).filter((p) => p.scope === 'global');
const personal = (state.userPrefs || []).filter((p) => p.scope === 'persona' && (p.persona_id === persona || p.persona === persona));
const fill = (rootId, rows) => {
const root = $(rootId);
if (!root) {
return;
}
root.innerHTML = '';
if (!rows.length) {
root.innerHTML = '<div class="sa-mem-empty">Пусто</div>';
return;
}
for (const row of rows) {
const el = document.createElement('div');
el.className = 'sa-mem-row';
const pin = row.pinned ? ' ★' : '';
el.innerHTML = `<div class="sa-mem-row-body"><div class="sa-mem-row-head"><span class="sa-mem-row-key">${escapeHtml(row.key || '')}${pin}</span></div><div class="sa-mem-row-text">${escapeHtml(clipDebug(row.text, 200))}</div></div>`;
el.querySelector('.sa-mem-row-body')?.addEventListener('click', () => editUserPref(row));
el.querySelector('.sa-mem-row-body')?.setAttribute('title', 'Клик — редактировать');
const pinBtn = document.createElement('button');
pinBtn.type = 'button';
pinBtn.className = 'basic-button sa-mem-forget';
pinBtn.textContent = row.pinned ? '★' : '☆';
pinBtn.title = row.pinned ? 'Unpin' : 'Pin';
pinBtn.addEventListener('click', (e) => {
e.stopPropagation();
toggleUserPrefPin(row);
});
el.appendChild(pinBtn);
const forget = document.createElement('button');
forget.type = 'button';
forget.className = 'basic-button sa-mem-forget';
forget.textContent = '×';
forget.title = 'Забыть';
forget.addEventListener('click', (e) => {
e.stopPropagation();
forgetUserPref(row);
});
el.appendChild(forget);
root.appendChild(el);
}
};
fill('sa_prefs_global', global);
fill('sa_prefs_persona', personal);
}
function editUserPref(row) {
const text = window.prompt('Текст факта:', row.text || '');
if (text == null || !String(text).trim()) {
return;
}
genericRequest(
'AssistentUpsertUserPref',
{
key: row.key,
text: String(text).trim(),
scope: row.scope || 'global',
persona: row.persona_id || row.persona || $('sa_persona')?.value || 'neutral',
source: 'user',
pinned: !!row.pinned,
},
() => refreshUserPrefs(),
0,
(err) => setStatus(String(err || 'Не удалось сохранить')),
);
}
function toggleUserPrefPin(row) {
genericRequest(
'AssistentUpsertUserPref',
{
key: row.key,
text: row.text,
scope: row.scope || 'global',
persona: row.persona_id || row.persona || $('sa_persona')?.value || 'neutral',
source: row.source || 'user',
pinned: !row.pinned,
},
() => refreshUserPrefs(),
0,
(err) => setStatus(String(err || 'Не удалось pin')),
);
}
function addUserPref(scope) {
const key = window.prompt('Ключ (stable-id):', scope === 'global' ? 'prefer' : 'tone');
if (!key) {
return;
}
const text = window.prompt('Текст факта:', '');
if (!text) {
return;
}
genericRequest(
'AssistentUpsertUserPref',
{
key: key.trim(),
text: text.trim(),
scope,
persona: $('sa_persona')?.value || 'neutral',
source: 'user',
pinned: false,
},
() => {
refreshUserPrefs();
setStatus('Сохранено');
},
0,
(err) => setStatus(String(err || 'Не удалось сохранить')),
);
}
function forgetUserPref(row) {
genericRequest(
'AssistentForgetUserPref',
{
key: row.key,
scope: row.scope || 'global',
persona: row.persona_id || row.persona || $('sa_persona')?.value,
},
() => refreshUserPrefs(),
0,
(err) => setStatus(String(err || 'Не удалось забыть')),
);
}
function clearUserPrefs(scope) {
const labels = { global: 'общие prefs', persona: 'prefs этой личности', all: 'все prefs о пользователе' };
if (!window.confirm(`Очистить ${labels[scope] || scope}?`)) {
return;
}
genericRequest(
'AssistentClearUserPrefs',
{ scope, persona: $('sa_persona')?.value || 'neutral' },
(data) => {
setStatus(`Удалено: ${data?.deleted ?? 0}`);
refreshUserPrefs();
},
0,
(err) => setStatus(String(err || 'Очистка не удалась')),
);
}
function resetUiState() {
if (!window.confirm('Сбросить UI-state (local + disk)? Настройки Ollama и prefs останутся.')) {
return;
}
const keys = Object.keys(localStorage).filter((k) => k.startsWith('swarm_assistent_'));
for (const k of keys) {
if (k === LS_TASTE) {
continue;
}
localStorage.removeItem(k);
}
diskPersist()?.saveUiState?.({});
setStatus('UI-state сброшен — обнови страницу');
}
function modelKeyLeaf(name) {
return String(name || '')
.replace(/\\/g, '/')
@@ -4877,6 +5486,7 @@
el.title = title || text;
el.classList.remove('sa-health-ok', 'sa-health-warn', 'sa-health-down');
el.classList.add(`sa-health-${level}`);
syncSettingsHealthLine();
}
function probeOllamaHealth() {
@@ -5871,7 +6481,7 @@
} else {
why.push('последнего патча Assistent ещё нет');
}
why.push('приоритет: user → session_exact → exact(+persona) → live UI → memory_hits (shared+personal)');
why.push('приоритет: user → About the user → session_exact → exact(+persona) → live UI → craft memory_hits');
const lines = [
'### Debug Assistent',
@@ -6725,18 +7335,75 @@
$('sa_btn_settings')?.addEventListener('click', () => {
const s = $('sa_settings');
if (s) {
s.hidden = !s.hidden;
if (!s.hidden) {
refreshMemoryList();
refreshWantedQueue();
if (s.hidden) {
openSettings(state.settingsTab || 'behavior');
} else {
closeSettings();
}
}
});
$('sa_settings_close')?.addEventListener('click', () => closeSettings());
document.querySelectorAll('#sa_settings .sa-stab').forEach((btn) => {
btn.addEventListener('click', () => setSettingsTab(btn.getAttribute('data-stab')));
});
$('sa_btn_mem_refresh')?.addEventListener('click', () => {
refreshMemoryList();
refreshWantedQueue();
});
$('sa_mem_kind')?.addEventListener('change', renderMemoryList);
$('sa_mem_scope')?.addEventListener('change', renderMemoryList);
$('sa_mem_search')?.addEventListener('input', () => renderMemoryList());
$('sa_btn_mem_clear_kind')?.addEventListener('click', () => {
const kind = memoryKindFilter();
clearCraftMemory({ kind: kind === 'all' ? '' : kind, label: kind === 'all' ? 'весь крафт (фильтр типа)' : `тип ${kind}` });
});
$('sa_btn_mem_clear_persona')?.addEventListener('click', () => {
clearCraftMemory({ scope: 'personal', persona: $('sa_persona')?.value || 'neutral', label: 'крафт этой личности' });
});
$('sa_btn_mem_clear_shared')?.addEventListener('click', () => {
clearCraftMemory({ scope: 'shared', label: 'общую крафт-память' });
});
$('sa_btn_mem_clear_all')?.addEventListener('click', () => {
clearCraftMemory({ label: 'весь крафт (non-bundled)' });
});
$('sa_btn_prefs_refresh')?.addEventListener('click', () => refreshUserPrefs());
$('sa_btn_pref_add_global')?.addEventListener('click', () => addUserPref('global'));
$('sa_btn_pref_add_persona')?.addEventListener('click', () => addUserPref('persona'));
$('sa_btn_prefs_clear_global')?.addEventListener('click', () => clearUserPrefs('global'));
$('sa_btn_prefs_clear_persona')?.addEventListener('click', () => clearUserPrefs('persona'));
$('sa_btn_prefs_clear_all')?.addEventListener('click', () => clearUserPrefs('all'));
$('sa_user_prefs_weight')?.addEventListener('input', () => {
const lab = $('sa_user_prefs_weight_val');
if (lab) {
lab.textContent = Number($('sa_user_prefs_weight').value).toFixed(1);
}
});
$('sa_user_prefs_weight')?.addEventListener('change', () => saveKnobs());
$('sa_memory_top_k')?.addEventListener('change', () => saveKnobs());
$('sa_btn_knobs_save')?.addEventListener('click', () => saveKnobs());
$('sa_btn_reset_ui')?.addEventListener('click', () => resetUiState());
$('sa_btn_persona_export')?.addEventListener('click', () => exportSelectedPersona());
$('sa_btn_persona_import')?.addEventListener('click', () => $('sa_persona_import_file')?.click());
$('sa_persona_import_file')?.addEventListener('change', (e) => {
const file = e.target?.files?.[0];
if (file) {
importPersonaFile(file);
}
e.target.value = '';
});
$('sa_btn_persona_clone')?.addEventListener('click', () => cloneSelectedPersona());
$('sa_btn_persona_delete_panel')?.addEventListener('click', () => deleteSelectedOverlayPersona());
$('sa_btn_settings_health')?.addEventListener('click', () => {
probeOllamaHealth();
setTimeout(syncSettingsHealthLine, 400);
});
$('sa_settings_chat_model')?.addEventListener('change', () => {
const v = $('sa_settings_chat_model')?.value;
if (v && $('sa_model')) {
$('sa_model').value = v;
saveSettings();
}
});
$('sa_btn_look_result')?.addEventListener('click', () => askLookAtResult());
$('sa_ollama_health')?.addEventListener('click', () => probeOllamaHealth());
document.addEventListener('keydown', (e) => {
@@ -6746,7 +7413,7 @@
let closed = false;
const settings = $('sa_settings');
if (settings && !settings.hidden) {
settings.hidden = true;
closeSettings();
closed = true;
}
if (state.chatsPanelOpen) {
@@ -6854,7 +7521,13 @@
$('sa_board_more_menu')?.addEventListener('click', (e) => e.stopPropagation());
$('sa_clear_more_menu')?.addEventListener('click', (e) => e.stopPropagation());
$('sa_base_url')?.addEventListener('change', saveSettings);
$('sa_model')?.addEventListener('change', saveSettings);
$('sa_model')?.addEventListener('change', () => {
const v = $('sa_model')?.value;
if (v && $('sa_settings_chat_model')) {
$('sa_settings_chat_model').value = v;
}
saveSettings();
});
$('sa_embed_model')?.addEventListener('change', () => {
state.preferredEmbed = $('sa_embed_model')?.value || '';
saveSettings();