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:
+165
-1
@@ -729,11 +729,172 @@
|
||||
}
|
||||
|
||||
.sa-settings {
|
||||
display: grid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.45rem;
|
||||
padding: 0.7rem 0.75rem;
|
||||
border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
||||
background: color-mix(in srgb, currentColor 5%, transparent);
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.sa-settings-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.sa-settings-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.sa-stab {
|
||||
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.2rem 0.55rem;
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.sa-stab:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sa-stab-active {
|
||||
opacity: 1;
|
||||
background: color-mix(in srgb, currentColor 12%, transparent);
|
||||
border-color: color-mix(in srgb, currentColor 35%, transparent);
|
||||
}
|
||||
|
||||
.sa-settings-panes {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sa-spane {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.sa-settings-hint {
|
||||
margin: 0;
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.7;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.sa-settings-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sa-settings-row.sa-knob-row label {
|
||||
flex: 1;
|
||||
min-width: 5rem;
|
||||
}
|
||||
|
||||
.sa-settings-health {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.sa-danger-btn {
|
||||
color: #e06c75 !important;
|
||||
}
|
||||
|
||||
.sa-persona-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(8rem, 11rem) 1fr;
|
||||
gap: 0.5rem;
|
||||
min-height: 10rem;
|
||||
}
|
||||
|
||||
.sa-persona-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
max-height: 14rem;
|
||||
overflow: auto;
|
||||
border: 1px solid color-mix(in srgb, currentColor 16%, transparent);
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.sa-persona-item {
|
||||
text-align: left;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: 0.35rem 0.4rem;
|
||||
border-radius: 0.3rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.sa-persona-item:hover,
|
||||
.sa-persona-item-active {
|
||||
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||
}
|
||||
|
||||
.sa-persona-item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.sa-persona-dot {
|
||||
width: 0.45rem;
|
||||
height: 0.45rem;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sa-persona-badge {
|
||||
font-size: 0.62rem;
|
||||
opacity: 0.55;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sa-persona-preview {
|
||||
border: 1px solid color-mix(in srgb, currentColor 16%, transparent);
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.45rem 0.55rem;
|
||||
max-height: 14rem;
|
||||
overflow: auto;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.35;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.sa-user-prefs-cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.sa-user-prefs-cols,
|
||||
.sa-persona-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.sa-mem-search {
|
||||
flex: 1;
|
||||
min-width: 6rem;
|
||||
max-width: 12rem;
|
||||
box-sizing: border-box;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.sa-settings label {
|
||||
@@ -744,6 +905,9 @@
|
||||
}
|
||||
|
||||
.sa-settings input[type="text"],
|
||||
.sa-settings input[type="number"],
|
||||
.sa-settings input[type="search"],
|
||||
.sa-settings input[type="range"],
|
||||
.sa-select {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
+715
-42
@@ -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();
|
||||
|
||||
@@ -14,6 +14,8 @@ window.SA = window.SA || {};
|
||||
'snapshot_generate', 'select_slot', 'aspect', 'images', 'batch', 'vary', 'lock_seed',
|
||||
'creativity', 'intensity', 'complexity', 'movement',
|
||||
'clear_prompt_images', 'slot_to_prompt_image', 'pack',
|
||||
'memories', 'memory_query', 'memory_kind', 'tag_query', 'user_prefs',
|
||||
'controls', 'persona_clone', 'persona_shelves', 'persona', 'notes',
|
||||
];
|
||||
|
||||
const FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||||
|
||||
Reference in New Issue
Block a user