Ship persona shelves, Exact controls, and overlay author pipeline.
Add Leonid as a shelf-based example with preference_bias slider; support /persona new clone-to-overlay and UI-only delete. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -669,9 +669,59 @@
|
||||
.sa-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.sa-persona-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.sa-persona-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.55rem 0.85rem;
|
||||
width: 100%;
|
||||
order: 5;
|
||||
padding: 0.15rem 0 0;
|
||||
}
|
||||
|
||||
.sa-control-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.78rem;
|
||||
min-width: 11rem;
|
||||
}
|
||||
|
||||
.sa-control-row label {
|
||||
opacity: 0.75;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sa-control-row input[type="range"] {
|
||||
width: 7.5rem;
|
||||
accent-color: color-mix(in srgb, #f0883e 70%, currentColor);
|
||||
}
|
||||
|
||||
.sa-control-val {
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 2.4rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
#sa_persona_delete:not([hidden]) {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
#sa_persona_delete:hover {
|
||||
opacity: 1;
|
||||
color: #e06c75;
|
||||
}
|
||||
|
||||
.sa-icon-btn {
|
||||
min-width: 2rem;
|
||||
padding-left: 0.45rem;
|
||||
|
||||
+238
-1
@@ -2150,6 +2150,8 @@
|
||||
}
|
||||
}
|
||||
fillEmptyParamsFromExact();
|
||||
renderPersonaControls(data?.controls || {}, data?.control_values || data?.exact?.controls || {});
|
||||
syncPersonaDeleteButton(data?.persona_source || data?.personas?.find((p) => p.id === id)?.source);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3212,6 +3214,34 @@
|
||||
setStatus('Prompt Images: drop the ref into the Swarm prompt box (no auto helper yet)');
|
||||
}
|
||||
}
|
||||
|
||||
// Persona Exact controls (model or user patch). Ignore persona_delete.
|
||||
if (patch.controls && typeof patch.controls === 'object' && !Array.isArray(patch.controls)) {
|
||||
const schema = state.config?.controls || {};
|
||||
const next = { ...(state.config?.control_values || state.exact?.controls || {}) };
|
||||
for (const [k, v] of Object.entries(patch.controls)) {
|
||||
if (schema[k]) {
|
||||
next[k] = v;
|
||||
}
|
||||
}
|
||||
savePersonaControls(next);
|
||||
renderPersonaControls(schema, next);
|
||||
}
|
||||
|
||||
const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : [];
|
||||
const wantSwitch = acts.includes('persona_switch')
|
||||
|| (patch.persona && typeof patch.persona === 'string')
|
||||
|| patch._persona_cloned
|
||||
|| patch._persona_written;
|
||||
if (wantSwitch) {
|
||||
const newId = String(patch.persona || patch._persona_cloned || patch._persona_written || '').trim();
|
||||
if (newId && AssistentConfigSafeIdClient(newId)) {
|
||||
await refreshPersonasAndSwitch(newId);
|
||||
} else if (acts.includes('persona_clone') || acts.includes('persona_write') || patch.persona_clone) {
|
||||
await refreshPersonasAndSwitch(null);
|
||||
}
|
||||
}
|
||||
|
||||
syncChipHighlight();
|
||||
syncLiveParamsBar();
|
||||
syncBuildGenButton();
|
||||
@@ -3224,10 +3254,46 @@
|
||||
}
|
||||
}
|
||||
if (!state.restoringChat) {
|
||||
setStatus('Applied patch');
|
||||
setStatus(patch._persona_error ? `Persona: ${patch._persona_error}` : 'Applied patch');
|
||||
}
|
||||
}
|
||||
|
||||
function AssistentConfigSafeIdClient(id) {
|
||||
return /^[A-Za-z0-9][A-Za-z0-9_\-]{0,63}$/.test(String(id || ''));
|
||||
}
|
||||
|
||||
async function refreshPersonasAndSwitch(preferId) {
|
||||
await new Promise((resolve) => {
|
||||
genericRequest(
|
||||
'AssistentListPersonas',
|
||||
{},
|
||||
async (data) => {
|
||||
if (Array.isArray(data?.personas)) {
|
||||
state.personas = data.personas.map((p) => ({
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
accent: p.accent,
|
||||
source: p.source,
|
||||
}));
|
||||
renderPersonaOptions(state.personas, preferId || $('sa_persona')?.value);
|
||||
}
|
||||
if (preferId && $('sa_persona')) {
|
||||
if ([...$('sa_persona').options].some((o) => o.value === preferId)) {
|
||||
$('sa_persona').value = preferId;
|
||||
await applyPersonaForChat(preferId, { quiet: true });
|
||||
}
|
||||
} else {
|
||||
loadConfig($('sa_persona')?.value, () => resolve());
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
0,
|
||||
() => resolve(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function triggerGenerate() {
|
||||
try {
|
||||
if (typeof mainGenHandler !== 'undefined' && mainGenHandler && typeof mainGenHandler.doGenerate === 'function') {
|
||||
@@ -4202,6 +4268,151 @@
|
||||
if (applyDefaults || data.exact) {
|
||||
fillEmptyParamsFromExact();
|
||||
}
|
||||
renderPersonaControls(data.controls || {}, data.control_values || data.exact?.controls || {});
|
||||
syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $('sa_persona')?.value))?.source);
|
||||
}
|
||||
|
||||
function syncPersonaDeleteButton(source) {
|
||||
const btn = $('sa_persona_delete');
|
||||
if (!btn) {
|
||||
return;
|
||||
}
|
||||
const src = String(source || '');
|
||||
const canDelete = src === 'overlay' || src === 'overlay+bundled';
|
||||
btn.hidden = !canDelete;
|
||||
btn.disabled = !canDelete;
|
||||
}
|
||||
|
||||
let controlSaveTimer = null;
|
||||
function renderPersonaControls(schema, values) {
|
||||
const box = $('sa_persona_controls');
|
||||
if (!box) {
|
||||
return;
|
||||
}
|
||||
box.innerHTML = '';
|
||||
const keys = schema && typeof schema === 'object' ? Object.keys(schema) : [];
|
||||
if (!keys.length) {
|
||||
box.hidden = true;
|
||||
return;
|
||||
}
|
||||
box.hidden = false;
|
||||
for (const id of keys) {
|
||||
const def = schema[id];
|
||||
if (!def || typeof def !== 'object') {
|
||||
continue;
|
||||
}
|
||||
if (String(def.type || 'slider').toLowerCase() !== 'slider') {
|
||||
continue;
|
||||
}
|
||||
const min = Number(def.min ?? -1);
|
||||
const max = Number(def.max ?? 1);
|
||||
const step = Number(def.step ?? 0.05);
|
||||
const defVal = Number(def.default ?? 0);
|
||||
let cur = values && values[id] != null ? Number(values[id]) : defVal;
|
||||
if (Number.isNaN(cur)) {
|
||||
cur = defVal;
|
||||
}
|
||||
const row = document.createElement('div');
|
||||
row.className = 'sa-control-row';
|
||||
row.title = def.hint || id;
|
||||
const lab = document.createElement('label');
|
||||
lab.textContent = def.label || id;
|
||||
const input = document.createElement('input');
|
||||
input.type = 'range';
|
||||
input.min = String(min);
|
||||
input.max = String(max);
|
||||
input.step = String(step);
|
||||
input.value = String(cur);
|
||||
input.dataset.controlId = id;
|
||||
const valEl = document.createElement('span');
|
||||
valEl.className = 'sa-control-val';
|
||||
valEl.textContent = cur.toFixed(2);
|
||||
const onInput = () => {
|
||||
const v = Number(input.value);
|
||||
valEl.textContent = v.toFixed(2);
|
||||
if (controlSaveTimer) {
|
||||
clearTimeout(controlSaveTimer);
|
||||
}
|
||||
controlSaveTimer = setTimeout(() => savePersonaControls({ [id]: v }), 350);
|
||||
};
|
||||
input.addEventListener('input', onInput);
|
||||
input.addEventListener('change', onInput);
|
||||
row.appendChild(lab);
|
||||
row.appendChild(input);
|
||||
row.appendChild(valEl);
|
||||
box.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
function savePersonaControls(partial) {
|
||||
const persona = $('sa_persona')?.value || 'neutral';
|
||||
if (typeof genericRequest !== 'function') {
|
||||
return;
|
||||
}
|
||||
genericRequest(
|
||||
'AssistentSaveControls',
|
||||
{ persona, controls: partial || {} },
|
||||
(data) => {
|
||||
if (data?.error) {
|
||||
setStatus(data.error);
|
||||
return;
|
||||
}
|
||||
if (data?.control_values && state.config) {
|
||||
state.config.control_values = data.control_values;
|
||||
if (state.exact) {
|
||||
state.exact.controls = data.control_values;
|
||||
}
|
||||
}
|
||||
if (data?.controls) {
|
||||
renderPersonaControls(data.controls, data.control_values || {});
|
||||
}
|
||||
},
|
||||
0,
|
||||
() => setStatus('controls save failed'),
|
||||
);
|
||||
}
|
||||
|
||||
async function deleteCurrentOverlayPersona() {
|
||||
const id = $('sa_persona')?.value;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const meta = (state.personas || []).find((p) => p.id === id);
|
||||
const title = meta?.title || id;
|
||||
const src = meta?.source || state.config?.persona_source || '';
|
||||
if (src !== 'overlay' && src !== 'overlay+bundled') {
|
||||
setStatus('Bundled personas cannot be deleted');
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`Удалить «${title}»?\nПоставка (bundled) не трогается.`)) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
genericRequest(
|
||||
'AssistentDeletePersona',
|
||||
{ persona: id },
|
||||
async (data) => {
|
||||
if (data?.error) {
|
||||
setStatus(data.error);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const next = data?.default_persona || 'neutral';
|
||||
if (Array.isArray(data?.personas)) {
|
||||
state.personas = data.personas;
|
||||
}
|
||||
renderPersonaOptions(state.personas || [], next);
|
||||
if ($('sa_persona')) {
|
||||
$('sa_persona').value = next;
|
||||
}
|
||||
await applyPersonaForChat(next, { quiet: false });
|
||||
setStatus(`Удалено: ${id}`);
|
||||
resolve();
|
||||
},
|
||||
0,
|
||||
() => { setStatus('delete failed'); resolve(); },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function renderPersonaOptions(personas, selected) {
|
||||
@@ -4223,6 +4434,8 @@
|
||||
if ([...sel.options].some((o) => o.value === cur)) {
|
||||
sel.value = cur;
|
||||
}
|
||||
const meta = (personas || []).find((p) => p.id === sel.value);
|
||||
syncPersonaDeleteButton(meta?.source || state.config?.persona_source);
|
||||
}
|
||||
|
||||
function renderPackOptions(packs, preferred) {
|
||||
@@ -5883,6 +6096,29 @@
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (cmd === 'persona') {
|
||||
const sub = (parts[1] || 'new').toLowerCase();
|
||||
const rest = parts.slice(2).join(' ').trim();
|
||||
setPackValue('author_persona', { flash: true, user: true });
|
||||
if (sub === 'save') {
|
||||
await sendChat({
|
||||
skipAutoPack: true,
|
||||
forcedUserText:
|
||||
'Сохрани согласованный черновик личности сейчас (persona_clone / persona_write). Не удаляй личности.',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
const fromId = sub === 'clone' && rest
|
||||
? rest.split(/\s+/)[0]
|
||||
: ($('sa_persona')?.value || 'neutral');
|
||||
await sendChat({
|
||||
skipAutoPack: true,
|
||||
forcedUserText:
|
||||
`Начни интервью author_persona: клон с источника «${fromId}». ` +
|
||||
'Спрашивай по полкам группами. Не пиши на диск, пока мало ответов. Не удаляй личности.',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
appendSystemNote(`Unknown command /${cmd}.\n\n${HELP_TEXT}`);
|
||||
setStatus(`Unknown /${cmd}`);
|
||||
@@ -6478,6 +6714,7 @@
|
||||
$('sa_board_tab_gen')?.addEventListener('click', () => setBoardTab('generate'));
|
||||
$('sa_board_tab_refs')?.addEventListener('click', () => setBoardTab('refs'));
|
||||
$('sa_persona')?.addEventListener('change', onPersonaChanged);
|
||||
$('sa_persona_delete')?.addEventListener('click', () => deleteCurrentOverlayPersona());
|
||||
$('sa_cards_kind')?.addEventListener('change', renderCardsList);
|
||||
$('sa_btn_cards_refresh')?.addEventListener('click', () => refreshInventory(() => renderCardsList(), { rescan: true }));
|
||||
$('sa_btn_card_meta')?.addEventListener('click', () => fetchCardMetaLive());
|
||||
|
||||
Reference in New Issue
Block a user