// ========================================== // Chat Switchboard – UI Settings & Teams // ========================================== // Extends UI with settings modal tabs, team management, // 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); // v0.23.2: Theme saved via Theme API (localStorage key: switchboard_theme) const activeThemeBtn = document.querySelector('#themeToggle .toggle-btn.active'); if (activeThemeBtn && typeof Theme !== 'undefined') { Theme.set(activeThemeBtn.dataset.theme); } 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 = '
You are not a member of any teams.
'; return; } el.innerHTML = teams.map(t => `

${esc(t.name)}

${t.my_role}
${t.description ? `

${esc(t.description)}

` : ''}
${t.member_count} member${t.member_count !== 1 ? 's' : ''}
`).join(''); } catch (e) { el.innerHTML = '
Failed to load teams.
'; } }, loadAppearanceSettings() { const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}'); const scale = prefs.scale || 100; const msgFont = prefs.msgFont || 14; // v0.23.2: Read theme from Theme API const theme = typeof Theme !== 'undefined' ? Theme.get() : 'system'; const keymap = prefs.editorKeymap || 'standard'; const scaleEl = document.getElementById('settingsScale'); const msgFontEl = document.getElementById('settingsMsgFont'); if (scaleEl) { scaleEl.value = scale; document.getElementById('scaleValue').textContent = scale + '%'; } if (msgFontEl) { msgFontEl.value = msgFont; document.getElementById('msgFontValue').textContent = msgFont + 'px'; } // Highlight active theme button and wire click document.querySelectorAll('#themeToggle .toggle-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.theme === theme); btn.onclick = () => { document.querySelectorAll('#themeToggle .toggle-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); if (typeof Theme !== 'undefined') Theme.set(btn.dataset.theme); }; }); // Highlight active keymap button document.querySelectorAll('#keymapToggle .toggle-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.keymap === keymap); }); }, initAppearance() { // Theme and appearance are already applied by the universal init // in base.html (Theme.init() + UI.restoreAppearance()). This method // only wires up the settings UI controls. // Scale slider: show value live, but apply zoom only on release to avoid // jarring layout jumps mid-drag (zoom changes the layout viewport). const scaleEl = document.getElementById('settingsScale'); const msgFontEl = document.getElementById('settingsMsgFont'); if (scaleEl) { scaleEl.addEventListener('input', () => { document.getElementById('scaleValue').textContent = parseInt(scaleEl.value) + '%'; }); scaleEl.addEventListener('change', () => { UI.applyAppearance(parseInt(scaleEl.value), parseInt(msgFontEl?.value || 14)); }); } if (msgFontEl) { msgFontEl.addEventListener('input', () => { document.getElementById('msgFontValue').textContent = parseInt(msgFontEl.value) + 'px'; }); msgFontEl.addEventListener('change', () => { UI.applyAppearance(parseInt(scaleEl?.value || 100), parseInt(msgFontEl.value)); }); } // Theme toggle buttons document.querySelectorAll('#themeToggle .toggle-btn').forEach(btn => { btn.addEventListener('click', () => { const theme = btn.dataset.theme; UI.applyTheme(theme); // delegates to Theme.set() — single source of truth document.querySelectorAll('#themeToggle .toggle-btn').forEach(b => b.classList.toggle('active', b === btn) ); }); }); // Editor keymap toggle buttons document.querySelectorAll('#keymapToggle .toggle-btn').forEach(btn => { btn.addEventListener('click', () => { const mode = btn.dataset.keymap; document.querySelectorAll('#keymapToggle .toggle-btn').forEach(b => b.classList.toggle('active', b === btn) ); // Persist const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}'); p.editorKeymap = mode; localStorage.setItem('cs-appearance', JSON.stringify(p)); // Notify open editors if (typeof Events !== 'undefined' && Events.emit) { Events.emit('keymap.changed', { mode }); } }); }); // System theme change listener (for "system" mode) // Note: Theme.set('system') registers its own listener, but this // additionally fires the theme.changed event for CM6 editors. UI._systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)'); UI._systemThemeQuery.addEventListener('change', () => { const mode = typeof Theme !== 'undefined' ? Theme.get() : 'system'; if (mode === 'system') UI.applyTheme('system'); }); }, /** * Apply theme to the document. * @param {'light'|'dark'|'system'} mode */ applyTheme(mode) { // Delegate to Theme API which handles system-mode resolution and // listens for OS preference changes. if (typeof Theme !== 'undefined') { Theme.set(mode); } else { // Fallback if Theme not loaded — resolve manually let resolved = mode; if (mode === 'system') { resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } document.documentElement.setAttribute('data-theme', resolved); } // Publish event for CM6 editors and extensions if (typeof Events !== 'undefined' && Events.emit) { const resolved = typeof Theme !== 'undefined' ? Theme.resolved() : mode; Events.emit('theme.changed', { mode, resolved }); } }, /** Get the currently resolved theme ('dark' or 'light') */ getResolvedTheme() { return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark'; }, // applyAppearance — moved to ui-core.js (v0.25.0-cs11.1) so all surfaces can use it. async checkUserProvidersAllowed() { const notice = document.getElementById('userProvidersDisabled'); const addBtn = document.getElementById('providerShowAddBtn'); const tabBtn = document.getElementById('settingsProvidersTabBtn'); const usageTabBtn = document.getElementById('settingsUsageTabBtn'); const rolesTabBtn = document.getElementById('settingsRolesTabBtn'); const kbTabBtn = document.getElementById('settingsKBTabBtn'); if (!notice) return; const allowed = App.policies?.allow_user_byok === 'true'; if (!kbTabBtn) console.warn('[Settings] settingsKBTabBtn not found in DOM'); notice.style.display = allowed ? 'none' : ''; if (addBtn) addBtn.style.display = allowed ? '' : 'none'; if (tabBtn) tabBtn.style.display = allowed ? '' : 'none'; if (usageTabBtn) usageTabBtn.style.display = allowed ? '' : 'none'; if (rolesTabBtn) rolesTabBtn.style.display = allowed ? '' : 'none'; if (kbTabBtn) kbTabBtn.style.display = allowed ? '' : 'none'; }, 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'; if (addForm && !allowed) addForm.style.display = 'none'; if (tabBtn) tabBtn.style.display = allowed ? '' : 'none'; }, async loadProfileIntoSettings() { try { const p = await API.getProfile(); document.getElementById('profileDisplayName').value = p.display_name || ''; document.getElementById('profileEmail').value = p.email || ''; updateAvatarPreview(p.avatar || null); } catch (e) { /* optional */ } }, async loadMyTeams() { const section = document.getElementById('settingsTeamsSection'); const el = document.getElementById('settingsTeamsList'); const menuBtn = document.getElementById('menuTeamAdmin'); if (!section || !el) return; try { const resp = await API.listMyTeams(); const teams = resp.data || []; UI._myTeams = teams; // Show/hide the Team Management flyout item for team admins const isTeamAdmin = teams.some(t => t.my_role === 'admin'); if (menuBtn) menuBtn.style.display = isTeamAdmin ? '' : 'none'; if (teams.length === 0) { section.style.display = 'none'; return; } section.style.display = ''; el.innerHTML = teams.map(t => `
${esc(t.name)} ${t.my_role} ${t.my_role === 'admin' ? 'Manage →' : ''}
${t.description ? `
${esc(t.description)}
` : ''}
${t.member_count} member${t.member_count !== 1 ? 's' : ''}
`).join(''); } catch (e) { section.style.display = 'none'; if (menuBtn) menuBtn.style.display = 'none'; } }, _myTeams: [], _managingTeamId: null, loadTeamsTab() { // No longer used — picker is in team admin modal }, async openTeamManage(teamId, teamName) { UI._managingTeamId = teamId; UI._teamAuditPage = 1; // If the modal isn't open yet (called from settings "Manage →"), open it if (!document.getElementById('teamAdminModal').classList.contains('active')) { openModal('teamAdminModal'); } // Show tabbed content, hide picker const adminTeams = (UI._myTeams || []).filter(t => t.my_role === 'admin'); const titleEl = document.getElementById('teamAdminTitle'); if (adminTeams.length > 1) { titleEl.innerHTML = ` ${esc(teamName)}`; } else { titleEl.textContent = teamName; } document.getElementById('teamAdminPicker').style.display = 'none'; document.getElementById('teamAdminContent').style.display = 'flex'; // Reset forms (null-safe — not all form containers may exist) ['settingsTeamAddMember', 'settingsTeamAddPersona', 'settingsTeamProviderForm'].forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; }); const af = document.getElementById('teamAuditFilterAction'); if (af) { af.innerHTML = ''; } // Show Members tab first, load its data UI.switchTeamTab('members'); }, showTeamPicker() { document.getElementById('teamAdminTitle').textContent = 'Team Management'; document.getElementById('teamAdminContent').style.display = 'none'; document.getElementById('teamAdminPicker').style.display = ''; }, async loadTeamManageMembers(teamId) { const el = document.getElementById('settingsTeamMembers'); el.innerHTML = '
Loading...
'; try { const resp = await API.teamListMembers(teamId); const members = resp.data || []; if (!members.length) { el.innerHTML = '
No members
'; return; } el.innerHTML = ` ${members.map(m => ``).join('')}
MemberRole
${esc(m.display_name || m.email)}
${m.user_role === 'admin' ? 'sys-admin' : ''}
`; } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, async loadTeamManageProviders(teamId) { const el = document.getElementById('settingsTeamProviders'); if (!el) return; const addBtn = document.getElementById('settingsTeamAddProviderBtn'); const formEl = document.getElementById('settingsTeamProviderForm'); // Initialize team provider form primitive (once) if (!UI._teamProvForm && formEl) { UI._teamProvForm = renderProviderForm(formEl, { prefix: 'teamProv', showPrivate: true, showDefaultModel: true, onSubmit: async (vals, isEdit) => { const tid = UI._managingTeamId; try { if (isEdit) { const updates = {}; if (vals.name) updates.name = vals.name; if (vals.endpoint) updates.endpoint = vals.endpoint; if (vals.api_key) updates.api_key = vals.api_key; if (vals.model_default) updates.model_default = vals.model_default; updates.is_private = vals.is_private || false; await API.teamUpdateProvider(tid, vals._editId, updates); UI.toast('Provider updated'); } else { if (!vals.name) return UI.toast('Name required', 'error'); if (!vals.endpoint) return UI.toast('Endpoint required', 'error'); await API.teamCreateProvider(tid, { name: vals.name, provider: vals.provider, endpoint: vals.endpoint, api_key: vals.api_key, is_private: vals.is_private || false, }); UI.toast('Provider added'); } formEl.style.display = 'none'; UI._teamProvForm.setCreateMode(); UI._teamProvList.refresh(); await fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } }, onCancel: () => { formEl.style.display = 'none'; UI._teamProvForm.setCreateMode(); }, }); } // Initialize team provider list primitive (once) if (!UI._teamProvList) { UI._teamProvList = renderProviderList(el, { apiFetch: async () => { const resp = await API.teamListProviders(UI._managingTeamId); // Handle policy check const allowed = resp.allow_team_providers !== false; if (addBtn) addBtn.style.display = allowed ? '' : 'none'; if (!allowed && !(resp.providers || []).length) { throw { message: 'Team provider management is disabled by your administrator', _empty: true }; } return resp; }, showPrivate: true, showActive: true, emptyMsg: 'No team providers — add one to give your team access to additional models', onEdit: (prov) => { if (UI._teamProvForm) { UI._teamProvForm.setEditMode(prov.id, prov); formEl.style.display = ''; } }, onDelete: async (prov) => { if (!await showConfirm(`Delete team provider "${prov.name}"? This will remove all models from this provider for your team.`)) return; try { await API.teamDeleteProvider(UI._managingTeamId, prov.id); UI.toast('Provider deleted'); UI._teamProvList.refresh(); await fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } }, onToggleActive: async (prov) => { try { await API.teamUpdateProvider(UI._managingTeamId, prov.id, { is_active: !prov.is_active }); UI.toast(prov.is_active ? 'Provider deactivated' : 'Provider activated'); UI._teamProvList.refresh(); await fetchModels(); } catch (e) { UI.toast(e.message, 'error'); } }, }); } await UI._teamProvList.refresh(); }, async loadTeamManagePersonas(teamId) { const el = document.getElementById('settingsTeamPersonas'); el.innerHTML = '
Loading...
'; try { const resp = await API.teamListPersonas(teamId); const personas = resp.personas || []; el.innerHTML = personas.map(p => { const icon = p.icon || ''; return `
${icon ? icon + ' ' : ''}${esc(p.name)}
${esc(p.base_model_id)}
`; }).join('') || '
No team personas — create one to give your team preconfigured models
'; } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, async loadTeamManageGroups(teamId) { const el = document.getElementById('settingsTeamGroups'); if (!el) return; el.innerHTML = '
Loading...
'; try { const resp = await API.teamListGroups(teamId); const groups = resp.data || []; if (!groups.length) { el.innerHTML = '
No groups assigned to this team yet
'; return; } el.innerHTML = ` ${groups.map(g => ``).join('')}
GroupMembers
${esc(g.name)}
${esc(g.description || 'No description')}
${g.member_count} member${g.member_count !== 1 ? 's' : ''}
`; } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, async loadTeamManageSettings(teamId) { if (!teamId) return; try { const team = await API.teamGetTeam ? API.teamGetTeam(teamId) : await API.adminGetTeam(teamId); const settings = typeof team.settings === 'string' ? JSON.parse(team.settings || '{}') : (team.settings || {}); const privEl = document.getElementById('teamSettingPrivatePolicy'); const allowEl = document.getElementById('teamSettingAllowProviders'); if (privEl) privEl.checked = !!settings.require_private_providers; if (allowEl) allowEl.checked = settings.allow_team_providers !== false; // Wire save button (once) const saveBtn = document.getElementById('teamSettingSaveBtn'); if (saveBtn && !saveBtn._wired) { saveBtn._wired = true; saveBtn.addEventListener('click', async () => { const tid = UI._managingTeamId; const priv = document.getElementById('teamSettingPrivatePolicy')?.checked || false; const allow = document.getElementById('teamSettingAllowProviders')?.checked !== false; try { await API.adminUpdateTeam(tid, { settings: JSON.stringify({ require_private_providers: priv, allow_team_providers: allow }) }); UI.toast('Team settings saved', 'success'); } catch (e) { UI.toast(e.message, 'error'); } }); } } catch (e) { /* proceed with defaults */ } }, async loadMyUsage() { const el = document.getElementById('myUsageTotals')?.parentElement; if (!el) return; if (!UI._myUsageDash) { UI._myUsageDash = renderUsageDashboard(el, { apiFetch: (p) => API.getMyUsage(p), periodElId: 'myUsagePeriod', groupByElId: 'myUsageGroupBy', compact: true, }); } await UI._myUsageDash.refresh(); }, async loadTeamUsage() { const teamId = UI._managingTeamId; if (!teamId) return; const el = document.getElementById('settingsTeamUsageTotals')?.parentElement; if (!el) return; if (!UI._teamUsageDash) { UI._teamUsageDash = renderUsageDashboard(el, { apiFetch: (p) => API.teamGetUsage(teamId, p), periodElId: 'teamUsagePeriod', groupByElId: 'teamUsageGroupBy', compact: true, showUserColumn: true, }); } await UI._teamUsageDash.refresh(); }, _teamAuditPage: 1, _teamAuditPerPage: 20, // ── User Model Roles (BYOK overrides) ── async loadUserRoles() { const el = document.getElementById('userRolesConfig'); const notice = document.getElementById('userRolesDisabled'); if (!el) return; // Only show if BYOK is enabled const byokAllowed = App.policies?.allow_user_byok === 'true'; if (!byokAllowed) { el.style.display = 'none'; if (notice) { notice.innerHTML = '

BYOK (Bring Your Own Key) must be enabled by your admin before you can configure model role overrides.

'; notice.style.display = ''; } return; } // Check if user has personal providers try { const configs = await API.listConfigs(); const configList = configs.configs || configs.data || []; const personalProviders = configList.filter(c => c.scope === 'personal'); if (personalProviders.length === 0) { el.style.display = 'none'; if (notice) { notice.innerHTML = '

Add a personal provider in My Providers first to configure model role overrides.

'; notice.style.display = ''; } return; } el.style.display = ''; if (notice) notice.style.display = 'none'; } catch (e) { el.innerHTML = `
${esc(e.message)}
`; return; } if (_userRoleConfig) await _userRoleConfig.refresh(); }, async loadTeamAuditLog(page) { const teamId = UI._managingTeamId; if (!teamId) return; if (page) UI._teamAuditPage = page; const el = document.getElementById('settingsTeamAudit'); if (!el) return; el.innerHTML = '
Loading...
'; // Populate action filter on first load const actionSel = document.getElementById('teamAuditFilterAction'); if (actionSel && actionSel.options.length <= 1) { try { const resp = await API.teamListAuditActions(teamId); (resp.actions || []).forEach(a => { const opt = document.createElement('option'); opt.value = a; opt.textContent = a; actionSel.appendChild(opt); }); } catch (e) { /* optional */ } } const params = { page: UI._teamAuditPage, per_page: UI._teamAuditPerPage, action: document.getElementById('teamAuditFilterAction')?.value || '', }; try { const resp = await API.teamListAudit(teamId, params); const entries = resp.data || []; const total = resp.total || 0; const totalPages = Math.ceil(total / UI._teamAuditPerPage); if (entries.length === 0) { el.innerHTML = '
No activity recorded for this team
'; } else { el.innerHTML = entries.map(e => { const ts = new Date(e.created_at); const timeStr = ts.toLocaleDateString() + ' ' + ts.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}); const actor = e.actor_name || 'system'; const meta = UI._formatAuditMeta(e.metadata); return `
${esc(e.action)} ${esc(actor)} ${esc(e.resource_type)}${e.resource_id ? ':' + esc(e.resource_id).slice(0,8) : ''}
${meta ? `${meta}` : ''} ${timeStr}
`; }).join(''); } // Pagination const pgEl = document.getElementById('teamAuditPagination'); if (totalPages > 1) { pgEl.style.display = 'flex'; document.getElementById('teamAuditPageInfo').textContent = `Page ${UI._teamAuditPage} of ${totalPages} (${total} entries)`; document.getElementById('teamAuditPrevBtn').disabled = UI._teamAuditPage <= 1; document.getElementById('teamAuditNextBtn').disabled = UI._teamAuditPage >= totalPages; } else { pgEl.style.display = 'none'; } } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, async loadTeamPersonaModelDropdown(teamId) { const sel = document.getElementById('teamPersona_model'); if (!sel) return; sel.innerHTML = ''; try { const resp = await API.teamListAvailableModels(teamId); const models = resp.models || []; sel.innerHTML = ''; // Group by source for clarity const globalModels = models.filter(m => m.source !== 'team'); const teamModels = models.filter(m => m.source === 'team'); const addGroup = (label, items) => { if (!items.length) return; const group = document.createElement('optgroup'); group.label = label; items.forEach(m => { const opt = document.createElement('option'); opt.value = m.model_id || m.id; const vis = m.visibility === 'team' ? ' [team-only]' : ''; opt.textContent = (m.model_id || m.id) + (m.provider_name ? ` (${m.provider_name})` : '') + vis; group.appendChild(opt); }); sel.appendChild(group); }; addGroup('Global Models', globalModels); addGroup('Team Provider Models', teamModels); } catch (e) { // Fallback to App.models if team endpoint fails sel.innerHTML = ''; 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})` : ''); sel.appendChild(opt); }); } }, // ── Providers ──────────────────────────── async loadProviderList() { if (_userProvList) { await _userProvList.refresh(); return; } // Fallback if primitives not yet initialized const el = document.getElementById('providerList'); el.innerHTML = '
Loading...
'; }, showProviderForm() { document.getElementById('providerAddForm').style.display = ''; }, hideProviderForm() { document.getElementById('providerAddForm').style.display = 'none'; if (_userProvForm) _userProvForm.setCreateMode(); }, // ── User Model List ───────────────────── async loadUserModels() { const el = document.getElementById('userModelList'); if (!el) return; el.innerHTML = '
Loading models...
'; try { // Load preferences if not already loaded if (!App.hiddenModels) { try { const prefData = await API.getModelPreferences(); App.hiddenModels = new Set( (prefData.preferences || []).filter(p => p.hidden).map(p => (p.provider_config_id || '') + ':' + p.model_id ) ); } catch (e) { if (!App.hiddenModels) App.hiddenModels = new Set(); } } const data = await API.listEnabledModels(); const models = (data.models || []).filter(m => !m.is_persona); if (!models.length) { el.innerHTML = '
No models available
'; return; } const hiddenCount = models.filter(m => { const cfgId = m.config_id || m.provider_config_id || ''; return App.hiddenModels.has((cfgId || '') + ':' + (m.model_id || m.id)); }).length; const visibleCount = models.length - hiddenCount; let html = `
${visibleCount} visible, ${hiddenCount} hidden of ${models.length} total
`; html += models.map(m => { const mid = m.model_id || m.id; const cfgId = m.config_id || m.provider_config_id || ''; const compositeKey = (cfgId || '') + ':' + mid; const caps = m.capabilities || {}; const badges = renderCapBadges(caps, { compact: true }); const src = m.source === 'personal' ? 'personal' : m.source === 'team' ? `👥 ${esc(m.team_name || 'team')}` : ''; const hidden = App.hiddenModels.has(compositeKey); const toggleCls = hidden ? '' : 'enabled'; const toggleLabel = hidden ? 'Hidden' : '✓ Visible'; return `
${esc(mid)} ${badges} ${esc(m.provider_name || m.provider || '')} ${src}
`; }).join(''); el.innerHTML = html; } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, async loadUserPersonas() { const el = document.getElementById('userPersonaList'); if (!el) return; try { const data = await API.listUserPersonas(); // Only show personal personas owned by user const personas = (data.personas || []).filter(p => p.scope === 'personal'); if (!personas.length) { el.innerHTML = '
No personal personas yet
'; return; } el.innerHTML = personas.map(p => { const avatarEl = p.avatar ? `` : ''; return `
${avatarEl}${esc(p.name)}
${esc(p.base_model_id)} · ${esc(p.provider_name || 'auto')}
`; }).join(''); } catch (e) { el.innerHTML = `
${esc(e.message)}
`; } }, });