Changeset 0.10.3 (#59)
This commit is contained in:
640
src/js/ui-settings.js
Normal file
640
src/js/ui-settings.js
Normal file
@@ -0,0 +1,640 @@
|
||||
// ==========================================
|
||||
// Chat Switchboard – UI Settings & Teams
|
||||
// ==========================================
|
||||
// Extends UI with settings modal tabs, team management,
|
||||
// providers, usage, model roles, and user preferences.
|
||||
|
||||
Object.assign(UI, {
|
||||
// ── Appearance Settings ─────────────────
|
||||
|
||||
loadAppearanceSettings() {
|
||||
const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
||||
const scale = prefs.scale || 100;
|
||||
const msgFont = prefs.msgFont || 14;
|
||||
|
||||
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'; }
|
||||
},
|
||||
|
||||
initAppearance() {
|
||||
// Load saved prefs on startup
|
||||
const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
|
||||
UI.applyAppearance(prefs.scale || 100, prefs.msgFont || 14);
|
||||
|
||||
// Live preview on slider change
|
||||
const scaleEl = document.getElementById('settingsScale');
|
||||
const msgFontEl = document.getElementById('settingsMsgFont');
|
||||
if (scaleEl) scaleEl.addEventListener('input', () => {
|
||||
const v = parseInt(scaleEl.value);
|
||||
document.getElementById('scaleValue').textContent = v + '%';
|
||||
UI.applyAppearance(v, parseInt(msgFontEl?.value || 14));
|
||||
});
|
||||
if (msgFontEl) msgFontEl.addEventListener('input', () => {
|
||||
const v = parseInt(msgFontEl.value);
|
||||
document.getElementById('msgFontValue').textContent = v + 'px';
|
||||
UI.applyAppearance(parseInt(scaleEl?.value || 100), v);
|
||||
});
|
||||
},
|
||||
|
||||
applyAppearance(scale, msgFont) {
|
||||
const z = scale === 100 ? '' : scale / 100;
|
||||
// Zoom content areas + modals (but NOT .app or banners)
|
||||
document.querySelectorAll('.sidebar, .chat-area, .modal-overlay').forEach(el => el.style.zoom = z);
|
||||
const splash = document.getElementById('splashGate');
|
||||
if (splash) splash.style.zoom = z;
|
||||
document.documentElement.style.setProperty('--msg-font', msgFont + 'px');
|
||||
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);
|
||||
});
|
||||
},
|
||||
|
||||
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');
|
||||
if (!notice) return;
|
||||
const allowed = App.policies?.allow_user_byok === 'true';
|
||||
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';
|
||||
},
|
||||
|
||||
checkUserPresetsAllowed() {
|
||||
const addBtn = document.getElementById('userAddPresetBtn');
|
||||
const addForm = document.getElementById('userAddPresetForm');
|
||||
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 => `
|
||||
<div class="team-card"${t.my_role === 'admin' ? ` style="cursor:pointer" onclick="UI.openTeamManage('${t.id}', '${esc(t.name)}')"` : ''}>
|
||||
<div class="team-card-info">
|
||||
<strong>${esc(t.name)}</strong>
|
||||
<span class="badge-${t.my_role === 'admin' ? 'admin' : 'user'}">${t.my_role}</span>
|
||||
${t.my_role === 'admin' ? '<span style="margin-left:auto;color:var(--text-3);font-size:12px">Manage →</span>' : ''}
|
||||
</div>
|
||||
${t.description ? `<div class="text-muted" style="font-size:12px">${esc(t.description)}</div>` : ''}
|
||||
<div class="text-muted" style="font-size:11px">${t.member_count} member${t.member_count !== 1 ? 's' : ''}</div>
|
||||
</div>
|
||||
`).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 = `<span class="team-admin-back" onclick="UI.showTeamPicker()" title="Back to teams">←</span> ${esc(teamName)}`;
|
||||
} else {
|
||||
titleEl.textContent = teamName;
|
||||
}
|
||||
document.getElementById('teamAdminPicker').style.display = 'none';
|
||||
document.getElementById('teamAdminContent').style.display = '';
|
||||
|
||||
// Reset forms
|
||||
document.getElementById('settingsTeamAddMember').style.display = 'none';
|
||||
document.getElementById('settingsTeamAddPreset').style.display = 'none';
|
||||
document.getElementById('settingsTeamAddProvider').style.display = 'none';
|
||||
document.getElementById('settingsTeamEditProvider').style.display = 'none';
|
||||
const af = document.getElementById('teamAuditFilterAction');
|
||||
if (af) { af.innerHTML = '<option value="">All actions</option>'; }
|
||||
|
||||
// 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 = '<div class="loading">Loading...</div>';
|
||||
try {
|
||||
const resp = await API.teamListMembers(teamId);
|
||||
const members = resp.data || [];
|
||||
el.innerHTML = members.map(m => `
|
||||
<div class="admin-user-row" style="padding:6px 0">
|
||||
<div class="admin-user-info">
|
||||
<strong style="font-size:13px">${esc(m.display_name || m.email)}</strong>
|
||||
<span class="badge-${m.role === 'admin' ? 'admin' : 'user'}" style="font-size:10px">${m.role}</span>
|
||||
</div>
|
||||
<div class="admin-user-actions">
|
||||
<select onchange="settingsUpdateTeamMember('${teamId}','${m.id}',this.value)" class="inline-select">
|
||||
<option value="member" ${m.role === 'member' ? 'selected' : ''}>Member</option>
|
||||
<option value="admin" ${m.role === 'admin' ? 'selected' : ''}>Admin</option>
|
||||
</select>
|
||||
<button class="btn-delete" onclick="settingsRemoveTeamMember('${teamId}','${m.id}','${esc(m.email)}')" title="Remove">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('') || '<div class="empty-hint">No members</div>';
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
async loadTeamManageProviders(teamId) {
|
||||
const el = document.getElementById('settingsTeamProviders');
|
||||
if (!el) return;
|
||||
el.innerHTML = '<div class="loading">Loading...</div>';
|
||||
const addBtn = document.getElementById('settingsTeamAddProviderBtn');
|
||||
try {
|
||||
const resp = await API.teamListProviders(teamId);
|
||||
const provs = resp.providers || [];
|
||||
const allowed = resp.allow_team_providers !== false;
|
||||
|
||||
// Show/hide add button and entire section based on policy
|
||||
if (addBtn) addBtn.style.display = allowed ? '' : 'none';
|
||||
|
||||
if (!allowed && provs.length === 0) {
|
||||
el.innerHTML = '<div class="empty-hint">Team provider management is disabled by your administrator</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = provs.map(p => {
|
||||
const statusCls = p.is_active ? 'badge-admin' : 'badge-user';
|
||||
const statusLabel = p.is_active ? 'active' : 'inactive';
|
||||
const privateBadge = p.is_private ? ' <span class="badge-user" style="font-size:9px">🔒 private</span>' : '';
|
||||
return `<div class="admin-preset-row" style="padding:6px 0">
|
||||
<div class="preset-info">
|
||||
<strong>${esc(p.name)}</strong>
|
||||
<div class="preset-meta" style="font-size:11px">${esc(p.provider)} · ${p.has_key ? '🔑' : 'no key'} <span class="${statusCls}" style="font-size:10px">${statusLabel}</span>${privateBadge}</div>
|
||||
</div>
|
||||
<div class="preset-actions">
|
||||
<button class="btn-small" onclick="settingsEditTeamProvider('${teamId}','${p.id}','${esc(p.name)}','${esc(p.endpoint || '')}','${esc(p.model_default || '')}',${!!p.is_private})" title="Edit">✎</button>
|
||||
<button class="admin-model-toggle ${p.is_active ? 'enabled' : ''}" onclick="settingsToggleTeamProvider('${teamId}','${p.id}',${p.is_active})">${p.is_active ? '✓ Active' : 'Inactive'}</button>
|
||||
<button class="btn-delete" onclick="settingsDeleteTeamProvider('${teamId}','${p.id}','${esc(p.name)}')" title="Delete">✕</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('') || '<div class="empty-hint">No team providers — add one to give your team access to additional models</div>';
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
async loadTeamManagePresets(teamId) {
|
||||
const el = document.getElementById('settingsTeamPresets');
|
||||
el.innerHTML = '<div class="loading">Loading...</div>';
|
||||
try {
|
||||
const resp = await API.teamListPresets(teamId);
|
||||
const presets = resp.presets || [];
|
||||
el.innerHTML = presets.map(p => {
|
||||
const icon = p.icon || '';
|
||||
return `<div class="admin-preset-row" style="padding:6px 0">
|
||||
<div class="preset-info">
|
||||
<strong>${icon ? icon + ' ' : ''}${esc(p.name)}</strong>
|
||||
<div class="preset-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>
|
||||
</div>`;
|
||||
}).join('') || '<div class="empty-hint">No team presets — create one to give your team preconfigured models</div>';
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
async loadMyUsage() {
|
||||
const totalsEl = document.getElementById('myUsageTotals');
|
||||
const resultsEl = document.getElementById('myUsageResults');
|
||||
if (!totalsEl) return;
|
||||
|
||||
const period = document.getElementById('myUsagePeriod')?.value || '30d';
|
||||
const groupBy = document.getElementById('myUsageGroupBy')?.value || 'model';
|
||||
|
||||
totalsEl.innerHTML = '<div class="loading" style="font-size:12px">Loading...</div>';
|
||||
resultsEl.innerHTML = '';
|
||||
|
||||
try {
|
||||
const data = await API.getMyUsage({ period, group_by: groupBy });
|
||||
const t = data.totals || {};
|
||||
|
||||
if (!t.requests) {
|
||||
totalsEl.innerHTML = '<div class="empty-hint">No usage recorded in this period</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
totalsEl.innerHTML = `
|
||||
<div class="stats-grid" style="grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:8px">
|
||||
<div class="stat-card" style="padding:8px"><div class="stat-value" style="font-size:16px">${(t.requests || 0).toLocaleString()}</div><div class="stat-label" style="font-size:10px">Requests</div></div>
|
||||
<div class="stat-card" style="padding:8px"><div class="stat-value" style="font-size:16px">${(t.input_tokens || 0).toLocaleString()}</div><div class="stat-label" style="font-size:10px">Input</div></div>
|
||||
<div class="stat-card" style="padding:8px"><div class="stat-value" style="font-size:16px">${(t.output_tokens || 0).toLocaleString()}</div><div class="stat-label" style="font-size:10px">Output</div></div>
|
||||
<div class="stat-card" style="padding:8px"><div class="stat-value" style="font-size:16px">$${(t.total_cost || 0).toFixed(4)}</div><div class="stat-label" style="font-size:10px">Est. Cost</div></div>
|
||||
</div>`;
|
||||
|
||||
const results = data.results || [];
|
||||
if (results.length > 0) {
|
||||
resultsEl.innerHTML = `
|
||||
<table class="admin-table" style="font-size:12px">
|
||||
<thead><tr>
|
||||
<th>${groupBy === 'day' ? 'Date' : 'Model'}</th>
|
||||
<th style="text-align:right">Reqs</th>
|
||||
<th style="text-align:right">Input</th>
|
||||
<th style="text-align:right">Output</th>
|
||||
<th style="text-align:right">Cost</th>
|
||||
</tr></thead>
|
||||
<tbody>${results.map(r => `<tr>
|
||||
<td>${esc(r.label || r.group_key)}</td>
|
||||
<td style="text-align:right">${(r.requests || 0).toLocaleString()}</td>
|
||||
<td style="text-align:right">${(r.input_tokens || 0).toLocaleString()}</td>
|
||||
<td style="text-align:right">${(r.output_tokens || 0).toLocaleString()}</td>
|
||||
<td style="text-align:right">$${(r.total_cost || 0).toFixed(4)}</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
} catch (e) { totalsEl.innerHTML = `<div class="error-hint" style="font-size:12px">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
async loadTeamUsage() {
|
||||
const teamId = UI._managingTeamId;
|
||||
if (!teamId) return;
|
||||
|
||||
const totalsEl = document.getElementById('settingsTeamUsageTotals');
|
||||
const resultsEl = document.getElementById('settingsTeamUsageResults');
|
||||
if (!totalsEl) return;
|
||||
|
||||
const period = document.getElementById('teamUsagePeriod')?.value || '30d';
|
||||
const groupBy = document.getElementById('teamUsageGroupBy')?.value || 'model';
|
||||
|
||||
totalsEl.innerHTML = '<div class="loading" style="font-size:12px">Loading...</div>';
|
||||
resultsEl.innerHTML = '';
|
||||
|
||||
try {
|
||||
const data = await API.teamGetUsage(teamId, { period, group_by: groupBy });
|
||||
const t = data.totals || {};
|
||||
|
||||
if (!t.requests) {
|
||||
totalsEl.innerHTML = '<div class="empty-hint">No usage recorded for team providers in this period</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
totalsEl.innerHTML = `
|
||||
<div class="stats-grid" style="grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:8px">
|
||||
<div class="stat-card" style="padding:8px"><div class="stat-value" style="font-size:16px">${(t.requests || 0).toLocaleString()}</div><div class="stat-label" style="font-size:10px">Requests</div></div>
|
||||
<div class="stat-card" style="padding:8px"><div class="stat-value" style="font-size:16px">${(t.input_tokens || 0).toLocaleString()}</div><div class="stat-label" style="font-size:10px">Input</div></div>
|
||||
<div class="stat-card" style="padding:8px"><div class="stat-value" style="font-size:16px">${(t.output_tokens || 0).toLocaleString()}</div><div class="stat-label" style="font-size:10px">Output</div></div>
|
||||
<div class="stat-card" style="padding:8px"><div class="stat-value" style="font-size:16px">$${(t.total_cost || 0).toFixed(4)}</div><div class="stat-label" style="font-size:10px">Est. Cost</div></div>
|
||||
</div>`;
|
||||
|
||||
const results = data.results || [];
|
||||
if (results.length > 0) {
|
||||
resultsEl.innerHTML = `
|
||||
<table class="admin-table" style="font-size:12px">
|
||||
<thead><tr>
|
||||
<th>${groupBy === 'day' ? 'Date' : groupBy === 'user' ? 'User' : 'Model'}</th>
|
||||
<th style="text-align:right">Reqs</th>
|
||||
<th style="text-align:right">Input</th>
|
||||
<th style="text-align:right">Output</th>
|
||||
<th style="text-align:right">Cost</th>
|
||||
</tr></thead>
|
||||
<tbody>${results.map(r => `<tr>
|
||||
<td>${esc(r.label || r.group_key)}</td>
|
||||
<td style="text-align:right">${(r.requests || 0).toLocaleString()}</td>
|
||||
<td style="text-align:right">${(r.input_tokens || 0).toLocaleString()}</td>
|
||||
<td style="text-align:right">${(r.output_tokens || 0).toLocaleString()}</td>
|
||||
<td style="text-align:right">$${(r.total_cost || 0).toFixed(4)}</td>
|
||||
</tr>`).join('')}</tbody>
|
||||
</table>`;
|
||||
}
|
||||
} catch (e) { totalsEl.innerHTML = `<div class="error-hint" style="font-size:12px">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
_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;
|
||||
|
||||
try {
|
||||
// Get user's personal providers and their models
|
||||
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';
|
||||
|
||||
// Load all models for personal providers
|
||||
const allModels = App.models || [];
|
||||
const personalModels = allModels.filter(m =>
|
||||
personalProviders.some(p => p.id === m.configId)
|
||||
);
|
||||
|
||||
// Load current user settings to get existing role overrides
|
||||
const settings = await API.getSettings();
|
||||
const userRoles = settings.model_roles || {};
|
||||
|
||||
const roleNames = ['utility', 'embedding'];
|
||||
el.innerHTML = roleNames.map(role => {
|
||||
const cfg = userRoles[role] || { primary: null };
|
||||
return `
|
||||
<div class="settings-section" style="margin-bottom:12px">
|
||||
<h4 style="font-size:13px;margin:0 0 6px;text-transform:capitalize">${role}
|
||||
<span class="form-hint">${role === 'utility' ? '(summarization, title gen)' : '(future: KB search, note search)'}</span>
|
||||
</h4>
|
||||
<div class="form-row" style="gap:8px;align-items:center">
|
||||
<label style="min-width:60px;font-size:12px">Provider</label>
|
||||
<select id="userRole-${role}-provider" onchange="userRoleProviderChanged('${role}')" style="min-width:140px;font-size:12px">
|
||||
<option value="">— use org default —</option>
|
||||
${personalProviders.map(p => `<option value="${p.id}" ${cfg.primary?.provider_config_id === p.id ? 'selected' : ''}>${esc(p.name)}</option>`).join('')}
|
||||
</select>
|
||||
<select id="userRole-${role}-model" style="min-width:200px;font-size:12px">
|
||||
<option value="">— select model —</option>
|
||||
${cfg.primary ? personalModels.filter(m => m.configId === cfg.primary.provider_config_id).map(m => `<option value="${m.baseModelId}" ${m.baseModelId === cfg.primary.model_id ? 'selected' : ''}>${esc(m.name || m.baseModelId)}</option>`).join('') : ''}
|
||||
</select>
|
||||
<button class="btn-small" onclick="saveUserRole('${role}')" style="font-size:11px">Save</button>
|
||||
${cfg.primary ? `<button class="btn-small btn-danger" onclick="clearUserRole('${role}')" style="font-size:11px" title="Remove override, use org default">Clear</button>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
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 = '<div class="loading">Loading...</div>';
|
||||
|
||||
// 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 = '<div class="empty-hint">No activity recorded for this team</div>';
|
||||
} 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 `<div class="audit-row">
|
||||
<div class="audit-main">
|
||||
<span class="audit-action">${esc(e.action)}</span>
|
||||
<span class="audit-actor">${esc(actor)}</span>
|
||||
<span class="audit-resource">${esc(e.resource_type)}${e.resource_id ? ':' + esc(e.resource_id).slice(0,8) : ''}</span>
|
||||
</div>
|
||||
<div class="audit-meta">
|
||||
${meta ? `<span class="audit-details">${meta}</span>` : ''}
|
||||
<span class="audit-time">${timeStr}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}).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 = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
async loadTeamPresetModelDropdown(teamId) {
|
||||
const sel = document.getElementById('teamPreset_model');
|
||||
if (!sel) return;
|
||||
sel.innerHTML = '<option value="">Loading models...</option>';
|
||||
try {
|
||||
const resp = await API.teamListAvailableModels(teamId);
|
||||
const models = resp.models || [];
|
||||
sel.innerHTML = '<option value="">Select base model...</option>';
|
||||
|
||||
// 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 = '<option value="">Select base model...</option>';
|
||||
App.models.filter(m => !m.isPreset).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() {
|
||||
const el = document.getElementById('providerList');
|
||||
el.innerHTML = '<div class="loading">Loading...</div>';
|
||||
try {
|
||||
const data = await API.listConfigs();
|
||||
const list = Array.isArray(data) ? data : (data.configs || data.data || []);
|
||||
if (list.length === 0) {
|
||||
el.innerHTML = '<div class="empty-hint">No providers configured.</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = list.map(c => `
|
||||
<div class="provider-row">
|
||||
<div>
|
||||
<span class="provider-name">${esc(c.name)}</span>
|
||||
<span class="provider-meta">${esc(c.provider)} · ${esc(c.model_default || 'no default')}</span>
|
||||
</div>
|
||||
<div class="provider-actions">
|
||||
${c.has_key ? '🔑' : '⚠️'}
|
||||
<button class="btn-small" onclick="refreshProviderModels('${c.id}','${esc(c.name)}')">Refresh Models</button>
|
||||
<button class="btn-small" onclick="editProvider('${c.id}')">Edit</button>
|
||||
<button class="btn-small btn-danger" onclick="deleteProvider('${c.id}','${esc(c.name)}')">Remove</button>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
} catch (e) {
|
||||
el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
showProviderForm() { document.getElementById('providerAddForm').style.display = ''; },
|
||||
hideProviderForm() {
|
||||
document.getElementById('providerAddForm').style.display = 'none';
|
||||
UI._editingProviderId = null;
|
||||
document.getElementById('providerApiKey').placeholder = 'API key';
|
||||
},
|
||||
|
||||
// ── User Model List ─────────────────────
|
||||
|
||||
async loadUserModels() {
|
||||
const el = document.getElementById('userModelList');
|
||||
if (!el) return;
|
||||
el.innerHTML = '<div class="loading">Loading models...</div>';
|
||||
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.model_id)
|
||||
);
|
||||
} catch (e) { App.hiddenModels = new Set(); }
|
||||
}
|
||||
|
||||
const data = await API.listEnabledModels();
|
||||
const models = (data.models || []).filter(m => !m.is_preset);
|
||||
if (!models.length) {
|
||||
el.innerHTML = '<div class="empty-hint">No models available</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = models.map(m => {
|
||||
const mid = m.model_id || m.id;
|
||||
const caps = m.capabilities || {};
|
||||
const badges = [];
|
||||
if (caps.max_output_tokens > 0) badges.push(`<span class="cap-badge cap-context">${(caps.max_output_tokens/1000).toFixed(0)}K</span>`);
|
||||
if (caps.tool_calling) badges.push('<span class="cap-badge cap-accent">🔧</span>');
|
||||
if (caps.vision) badges.push('<span class="cap-badge cap-accent">👁</span>');
|
||||
if (caps.thinking) badges.push('<span class="cap-badge cap-accent">💭</span>');
|
||||
const src = m.source === 'personal' ? '<span class="badge-user" style="font-size:10px">personal</span>'
|
||||
: m.source === 'team' ? `<span class="badge-team" style="font-size:10px">👥 ${esc(m.team_name || 'team')}</span>` : '';
|
||||
const hidden = App.hiddenModels.has(mid);
|
||||
const toggleCls = hidden ? '' : 'enabled';
|
||||
const toggleLabel = hidden ? 'Hidden' : '✓ Visible';
|
||||
return `<div class="model-list-item ${hidden ? 'model-hidden' : ''}">
|
||||
<span class="model-name">${esc(mid)}</span>
|
||||
<span class="model-caps-inline">${badges.join('')}</span>
|
||||
<span class="model-provider">${esc(m.provider_name || m.provider || '')}</span>
|
||||
${src}
|
||||
<button class="admin-model-toggle ${toggleCls}" onclick="toggleUserModelVisibility('${esc(mid)}', ${hidden})" title="${hidden ? 'Show in selector' : 'Hide from selector'}">${toggleLabel}</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
async loadUserPresets() {
|
||||
const el = document.getElementById('userPresetList');
|
||||
if (!el) return;
|
||||
try {
|
||||
const data = await API.listUserPresets();
|
||||
// Only show personal presets owned by user
|
||||
const presets = (data.presets || []).filter(p => p.scope === 'personal');
|
||||
if (!presets.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">
|
||||
<strong>${avatarEl}${esc(p.name)}</strong>
|
||||
<div class="preset-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>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user