Changeset 0.22.7 (#149)

This commit is contained in:
2026-03-04 10:44:42 +00:00
parent d8e0664fa3
commit 389e47b0f9
62 changed files with 6820 additions and 1476 deletions

View File

@@ -5,8 +5,138 @@
// providers, usage, model roles, and user preferences.
Object.assign(UI, {
// ── General Settings (Settings Surface) ──
async loadGeneralSettings() {
// Fetch settings from API into App.settings
try {
const remote = await API.getSettings();
if (remote && typeof remote === 'object') {
if (remote.model) App.settings.model = remote.model;
if (remote.system_prompt !== undefined) App.settings.systemPrompt = remote.system_prompt;
if (remote.max_tokens) App.settings.maxTokens = remote.max_tokens;
if (remote.temperature !== undefined) App.settings.temperature = remote.temperature;
if (remote.show_thinking !== undefined) App.settings.showThinking = remote.show_thinking;
}
} catch (e) { console.warn('Settings load failed:', e.message); }
// Fetch models for the model dropdown
await fetchModels();
// Populate form elements
const modelSel = document.getElementById('settingsModel');
if (modelSel) {
modelSel.innerHTML = '';
App.models.filter(m => !m.hidden).forEach(m => {
const opt = document.createElement('option');
opt.value = m.id;
opt.textContent = (m.name || m.id) + (m.provider ? ` (${m.provider})` : '');
modelSel.appendChild(opt);
});
if (App.settings.model) modelSel.value = App.settings.model;
}
const sysEl = document.getElementById('settingsSystemPrompt');
if (sysEl) sysEl.value = App.settings.systemPrompt || '';
const maxEl = document.getElementById('settingsMaxTokens');
if (maxEl) maxEl.value = App.settings.maxTokens || '';
const tempEl = document.getElementById('settingsTemp');
const tempVal = document.getElementById('settingsTempValue');
if (tempEl) {
tempEl.value = App.settings.temperature;
if (tempVal) tempVal.textContent = App.settings.temperature;
tempEl.addEventListener('input', () => {
if (tempVal) tempVal.textContent = tempEl.value;
});
}
const thinkEl = document.getElementById('settingsThinking');
if (thinkEl) thinkEl.checked = App.settings.showThinking;
// Model change → update max tokens hint
if (modelSel) {
modelSel.addEventListener('change', () => {
const model = App.findModel(modelSel.value);
const caps = model?.capabilities || {};
const hint = document.getElementById('settingsMaxHint');
if (hint) {
hint.textContent = caps.max_output_tokens > 0
? `(model max: ${(caps.max_output_tokens / 1000).toFixed(0)}K)` : '';
}
});
// Trigger once to show current model's hint
modelSel.dispatchEvent(new Event('change'));
}
// Save button (reuse existing pattern — form auto-saves are not a thing yet)
const section = document.querySelector('.settings-section');
if (section && !section.querySelector('.settings-save-btn')) {
const btn = document.createElement('button');
btn.className = 'btn-md btn-primary settings-save-btn';
btn.textContent = 'Save';
btn.style.marginTop = '12px';
btn.addEventListener('click', async () => {
App.settings.model = modelSel?.value || App.settings.model;
App.settings.systemPrompt = sysEl?.value?.trim() || '';
App.settings.maxTokens = parseInt(maxEl?.value) || 0;
App.settings.temperature = parseFloat(tempEl?.value) || 0.7;
App.settings.showThinking = thinkEl?.checked || false;
try {
await API.updateSettings({
model: App.settings.model,
system_prompt: App.settings.systemPrompt,
max_tokens: App.settings.maxTokens,
temperature: App.settings.temperature,
show_thinking: App.settings.showThinking,
});
UI.toast('Settings saved', 'success');
} catch (e) { UI.toast(e.message, 'error'); }
});
section.appendChild(btn);
}
},
// ── Appearance Settings ─────────────────
saveAppearance() {
const scaleEl = document.getElementById('settingsScale');
const msgFontEl = document.getElementById('settingsMsgFont');
const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
if (scaleEl) prefs.scale = parseInt(scaleEl.value);
if (msgFontEl) prefs.msgFont = parseInt(msgFontEl.value);
localStorage.setItem('cs-appearance', JSON.stringify(prefs));
UI.toast('Appearance saved', 'success');
},
// ── Teams Settings (Settings Surface) ────
async loadTeamsSettings() {
const el = document.getElementById('settingsDynamic');
if (!el) return;
try {
const resp = await API.listMyTeams();
const teams = resp.data || [];
if (!teams.length) {
el.innerHTML = '<div class="empty-hint">You are not a member of any teams.</div>';
return;
}
el.innerHTML = teams.map(t => `
<div class="settings-section" style="margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<h3 style="margin:0">${esc(t.name)}</h3>
<span class="badge-${t.my_role === 'admin' ? 'success' : 'muted'}" style="font-size:11px;">${t.my_role}</span>
</div>
${t.description ? `<p style="color:var(--text-2);font-size:13px;margin:0 0 8px;">${esc(t.description)}</p>` : ''}
<div style="font-size:12px;color:var(--text-3);">${t.member_count} member${t.member_count !== 1 ? 's' : ''}</div>
</div>
`).join('');
} catch (e) {
el.innerHTML = '<div class="empty-hint">Failed to load teams.</div>';
}
},
loadAppearanceSettings() {
const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
const scale = prefs.scale || 100;
@@ -125,7 +255,7 @@ Object.assign(UI, {
localStorage.setItem('cs-appearance', JSON.stringify({ scale, msgFont }));
// Recheck tab overflow after layout settles with new zoom
requestAnimationFrame(() => {
document.querySelectorAll('.modal-overlay.active .modal-tabs').forEach(checkTabsOverflow);
document.querySelectorAll('.modal-overlay.active .modal-tabs').forEach(t => { if (typeof checkTabsOverflow === 'function') checkTabsOverflow(t); });
});
},
@@ -147,9 +277,9 @@ Object.assign(UI, {
if (kbTabBtn) kbTabBtn.style.display = allowed ? '' : 'none';
},
checkUserPresetsAllowed() {
const addBtn = document.getElementById('userAddPresetBtn');
const addForm = document.getElementById('userAddPresetForm');
checkUserPersonasAllowed() {
const addBtn = document.getElementById('userAddPersonaBtn');
const addForm = document.getElementById('userAddPersonaForm');
const tabBtn = document.getElementById('settingsPersonasTabBtn');
const allowed = App.policies?.allow_user_personas === 'true';
if (addBtn) addBtn.style.display = allowed ? '' : 'none';
@@ -227,7 +357,7 @@ Object.assign(UI, {
document.getElementById('teamAdminContent').style.display = '';
// Reset forms (null-safe — not all form containers may exist)
['settingsTeamAddMember', 'settingsTeamAddPreset', 'settingsTeamProviderForm'].forEach(id => {
['settingsTeamAddMember', 'settingsTeamAddPersona', 'settingsTeamProviderForm'].forEach(id => {
const el = document.getElementById(id);
if (el) el.style.display = 'none';
});
@@ -359,22 +489,22 @@ Object.assign(UI, {
await UI._teamProvList.refresh();
},
async loadTeamManagePresets(teamId) {
const el = document.getElementById('settingsTeamPresets');
async loadTeamManagePersonas(teamId) {
const el = document.getElementById('settingsTeamPersonas');
el.innerHTML = '<div class="loading">Loading...</div>';
try {
const resp = await API.teamListPresets(teamId);
const presets = resp.presets || [];
el.innerHTML = presets.map(p => {
const resp = await API.teamListPersonas(teamId);
const personas = resp.personas || [];
el.innerHTML = personas.map(p => {
const icon = p.icon || '';
return `<div class="admin-preset-row" style="padding:6px 0">
<div class="preset-info">
return `<div class="admin-persona-row" style="padding:6px 0">
<div class="persona-info">
<strong>${icon ? icon + ' ' : ''}${esc(p.name)}</strong>
<div class="preset-meta" style="font-size:11px">${esc(p.base_model_id)}</div>
<div class="persona-meta" style="font-size:11px">${esc(p.base_model_id)}</div>
</div>
<button class="btn-delete" onclick="settingsDeleteTeamPreset('${teamId}','${p.id}','${esc(p.name)}')" title="Delete">✕</button>
<button class="btn-delete" onclick="settingsDeleteTeamPersona('${teamId}','${p.id}','${esc(p.name)}')" title="Delete">✕</button>
</div>`;
}).join('') || '<div class="empty-hint">No team presets — create one to give your team preconfigured models</div>';
}).join('') || '<div class="empty-hint">No team personas — create one to give your team preconfigured models</div>';
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
},
@@ -534,8 +664,8 @@ Object.assign(UI, {
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
},
async loadTeamPresetModelDropdown(teamId) {
const sel = document.getElementById('teamPreset_model');
async loadTeamPersonaModelDropdown(teamId) {
const sel = document.getElementById('teamPersona_model');
if (!sel) return;
sel.innerHTML = '<option value="">Loading models...</option>';
try {
@@ -566,7 +696,7 @@ Object.assign(UI, {
} catch (e) {
// Fallback to App.models if team endpoint fails
sel.innerHTML = '<option value="">Select base model...</option>';
App.models.filter(m => !m.isPreset).forEach(m => {
App.models.filter(m => !m.isPersona).forEach(m => {
const opt = document.createElement('option');
opt.value = m.baseModelId || m.id;
opt.textContent = (m.name || m.id) + (m.provider ? ` (${m.provider})` : '');
@@ -610,7 +740,7 @@ Object.assign(UI, {
}
const data = await API.listEnabledModels();
const models = (data.models || []).filter(m => !m.is_preset);
const models = (data.models || []).filter(m => !m.is_persona);
if (!models.length) {
el.innerHTML = '<div class="empty-hint">No models available</div>';
return;
@@ -635,26 +765,26 @@ Object.assign(UI, {
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
},
async loadUserPresets() {
const el = document.getElementById('userPresetList');
async loadUserPersonas() {
const el = document.getElementById('userPersonaList');
if (!el) return;
try {
const data = await API.listUserPresets();
const data = await API.listUserPersonas();
// Only show personal presets owned by user
const presets = (data.presets || []).filter(p => p.scope === 'personal');
if (!presets.length) {
const personas = (data.personas || []).filter(p => p.scope === 'personal');
if (!personas.length) {
el.innerHTML = '<div class="empty-hint">No personal presets yet</div>';
return;
}
el.innerHTML = presets.map(p => {
const avatarEl = p.avatar ? `<img src="${p.avatar}" class="preset-row-avatar" alt="">` : '';
return `<div class="admin-preset-row" style="padding:6px 0">
<div class="preset-info">
el.innerHTML = personas.map(p => {
const avatarEl = p.avatar ? `<img src="${p.avatar}" class="persona-row-avatar" alt="">` : '';
return `<div class="admin-persona-row" style="padding:6px 0">
<div class="persona-info">
<strong>${avatarEl}${esc(p.name)}</strong>
<div class="preset-meta">${esc(p.base_model_id)} · ${esc(p.provider_name || 'auto')}</div>
<div class="persona-meta">${esc(p.base_model_id)} · ${esc(p.provider_name || 'auto')}</div>
</div>
<div class="preset-actions">
<button class="btn-delete" onclick="deleteUserPreset('${p.id}', '${esc(p.name)}')" title="Delete">✕</button>
<div class="persona-actions">
<button class="btn-delete" onclick="deleteUserPersona('${p.id}', '${esc(p.name)}')" title="Delete">✕</button>
</div>
</div>`;
}).join('');