// ==========================================
// 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 = '';
// 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 = 'All actions '; }
// 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 = `
Member Role
${members.map(m => `
${esc(m.display_name || m.email)}
${m.user_role === 'admin' ? 'sys-admin ' : ''}
Member
Admin
🗑
`).join('')}
`;
} 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 = `
Group Members
${groups.map(g => `
${esc(g.name)}
${esc(g.description || 'No description')}
${g.member_count} member${g.member_count !== 1 ? 's' : ''}
`).join('')}
`;
} 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');
const tabBtn = document.getElementById('settingsRolesTabBtn');
if (!el) return;
// Only show if BYOK is enabled
const byokAllowed = App.policies?.allow_user_byok === 'true';
if (tabBtn) tabBtn.style.display = byokAllowed ? '' : 'none';
if (!byokAllowed) 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.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 = 'Loading models... ';
try {
const resp = await API.teamListAvailableModels(teamId);
const models = resp.models || [];
sel.innerHTML = 'Select base model... ';
// 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 = 'Select base model... ';
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;
}
el.innerHTML = 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}
${toggleLabel}
`;
}).join('');
} 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)}
`; }
},
});