Changeset 0.22.7 (#149)
This commit is contained in:
@@ -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('');
|
||||
|
||||
Reference in New Issue
Block a user