This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/settings-handlers.js
2026-03-04 10:44:42 +00:00

690 lines
35 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==========================================
// Chat Switchboard Settings Handlers
// ==========================================
// Settings save, provider CRUD, avatar, command palette,
// user model roles.
// ── Settings ─────────────────────────────────
async function loadSettings() {
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;
}
} catch (e) { console.warn('Settings load failed:', e.message); }
}
async function saveSettings() {
try {
await API.updateSettings({
model: App.settings.model,
system_prompt: App.settings.systemPrompt,
max_tokens: App.settings.maxTokens,
temperature: App.settings.temperature,
});
} catch (e) { console.warn('Settings sync failed:', e.message); }
}
// ── Avatar Preview ──────────────────────────
function updateAvatarPreview(dataURI) {
const preview = document.getElementById('avatarPreview');
const letter = document.getElementById('avatarPreviewLetter');
const removeBtn = document.getElementById('avatarRemoveBtn');
const existingImg = preview.querySelector('img');
if (dataURI) {
letter.style.display = 'none';
if (existingImg) {
existingImg.src = dataURI;
} else {
const img = document.createElement('img');
img.src = dataURI;
img.alt = 'Avatar';
preview.insertBefore(img, letter);
}
removeBtn.style.display = '';
} else {
if (existingImg) existingImg.remove();
const name = API.user?.display_name || API.user?.username || '?';
letter.textContent = name[0].toUpperCase();
letter.style.display = '';
removeBtn.style.display = 'none';
}
}
// ── Settings Handlers ────────────────────────
async function handleSaveSettings() {
App.settings.model = document.getElementById('settingsModel').value || App.settings.model;
App.settings.systemPrompt = document.getElementById('settingsSystemPrompt').value.trim();
App.settings.maxTokens = parseInt(document.getElementById('settingsMaxTokens').value) || 0; // 0 = auto
App.settings.temperature = parseFloat(document.getElementById('settingsTemp').value) || 0.7;
App.settings.showThinking = document.getElementById('settingsThinking').checked;
saveSettings();
// ── Save profile fields (display name, email) ──
const displayName = document.getElementById('profileDisplayName').value.trim();
const email = document.getElementById('profileEmail').value.trim();
const profileUpdates = {};
if (displayName !== (API.user?.display_name || '')) profileUpdates.display_name = displayName;
if (email !== (API.user?.email || '')) profileUpdates.email = email;
if (Object.keys(profileUpdates).length > 0) {
try {
const updated = await API.updateProfile(profileUpdates);
// Sync local user object so sidebar/avatar reflect the change
if (updated) {
if (updated.display_name !== undefined) API.user.display_name = updated.display_name;
if (updated.email !== undefined) API.user.email = updated.email;
API.saveTokens();
UI.updateUser();
}
} catch (e) {
console.warn('Profile save failed:', e.message);
UI.toast('Profile update failed: ' + e.message, 'error');
}
}
UI.updateModelSelector();
UI.updateCapabilityBadges();
UI.closeSettings();
UI.toast('Settings saved', 'success');
}
async function handleChangePassword() {
const cur = document.getElementById('profileCurrentPw').value;
const pw = document.getElementById('profileNewPw').value;
if (!cur || !pw) return UI.toast('Fill in both fields', 'warning');
if (pw.length < 8) return UI.toast('Min 8 characters', 'warning');
try {
await API.changePassword(cur, pw);
UI.toast('Password updated', 'success');
document.getElementById('profileChangePwForm').style.display = 'none';
document.getElementById('profileCurrentPw').value = '';
document.getElementById('profileNewPw').value = '';
} catch (e) { UI.toast(e.message, 'error'); }
}
// ── User BYOK Provider — primitive instances (initialized in _initSettingsListeners) ──
var _userProvForm = null;
var _userProvList = null;
function _initUserProviderPrimitives() {
const formEl = document.getElementById('providerAddForm');
const listEl = document.getElementById('providerList');
if (!formEl || !listEl) return;
_userProvForm = renderProviderForm(formEl, {
prefix: 'userProv',
showPrivate: false,
showDefaultModel: true,
onSubmit: async (vals, isEdit) => {
try {
if (isEdit) {
if (!vals.name || !vals.endpoint) return UI.toast('Fill in required fields', 'warning');
const patch = { name: vals.name, provider: vals.provider, endpoint: vals.endpoint, model_default: vals.model_default };
if (vals.api_key) patch.api_key = vals.api_key;
await API.updateConfig(vals._editId, patch);
UI.toast('Provider updated', 'success');
} else {
if (!vals.name || !vals.endpoint || !vals.api_key) return UI.toast('Fill in required fields', 'warning');
const result = await API.createConfig(vals.name, vals.provider, vals.endpoint, vals.api_key, vals.model_default);
if (result.warning) UI.toast(`Provider added — ${result.warning}`, 'warning');
else {
const count = result.models_fetched || 0;
UI.toast(`Provider added — ${count} model${count !== 1 ? 's' : ''} synced`, 'success');
}
}
formEl.style.display = 'none';
_userProvForm.setCreateMode();
_userProvList.refresh();
fetchModels();
} catch (e) { UI.toast(e.message, 'error'); }
},
onCancel: () => {
formEl.style.display = 'none';
_userProvForm.setCreateMode();
},
});
_userProvList = renderProviderList(listEl, {
apiFetch: () => API.listConfigs(),
showRefresh: true,
emptyMsg: 'No providers configured.',
onEdit: async (prov) => {
try {
const cfg = await API.getConfig(prov.id);
_userProvForm.setEditMode(cfg.id, cfg);
formEl.style.display = '';
} catch (e) { UI.toast(e.message, 'error'); }
},
onDelete: async (prov) => {
if (!await showConfirm(`Remove provider "${prov.name}"?`)) return;
try {
await API.deleteConfig(prov.id);
UI.toast('Provider removed', 'success');
_userProvList.refresh();
fetchModels();
} catch (e) { UI.toast(e.message, 'error'); }
},
onRefresh: async (prov) => {
try {
UI.toast(`Fetching models for ${prov.name}...`, 'info');
const result = await API.fetchProviderModels(prov.id);
const total = result.total || 0;
UI.toast(`${prov.name}: ${total} model${total !== 1 ? 's' : ''} synced`, 'success');
fetchModels();
} catch (e) { UI.toast(`Failed to fetch models: ${e.message}`, 'error'); }
},
});
}
async function handleSaveAdminSettings() {
try {
// Admin system prompt → global_settings (JSON value)
const sysPromptContent = document.getElementById('adminSystemPrompt').value.trim();
await API.adminUpdateSetting('system_prompt', { value: { content: sysPromptContent } });
// Policies — send as string "true"/"false" so backend routes to platform_policies
const reg = document.getElementById('adminRegToggle').checked;
await API.adminUpdateSetting('allow_registration', { value: reg ? 'true' : 'false' });
// Registration default state → default_user_active policy
const regState = document.getElementById('adminRegDefaultState').value;
await API.adminUpdateSetting('default_user_active', { value: regState === 'active' ? 'true' : 'false' });
// User BYOK providers → allow_user_byok policy
const userProviders = document.getElementById('adminUserProvidersToggle').checked;
await API.adminUpdateSetting('allow_user_byok', { value: userProviders ? 'true' : 'false' });
// User personas → allow_user_personas policy
const userPersonas = document.getElementById('adminUserPersonasToggle').checked;
await API.adminUpdateSetting('allow_user_personas', { value: userPersonas ? 'true' : 'false' });
// Default model → default_model policy
const defaultModel = document.getElementById('adminDefaultModel').value;
await API.adminUpdateSetting('default_model', { value: defaultModel });
// KB direct access → kb_direct_access policy (v0.17.0)
const kbDirect = document.getElementById('adminKBDirectAccessToggle')?.checked;
if (kbDirect != null) {
await API.adminUpdateSetting('kb_direct_access', { value: kbDirect ? 'true' : 'false' });
}
// Banner → global_settings (JSON value)
const banner = {
enabled: document.getElementById('adminBannerEnabled').checked,
text: document.getElementById('adminBannerText').value,
position: document.getElementById('adminBannerPosition').value,
bg: document.getElementById('adminBannerBg').value,
fg: document.getElementById('adminBannerFg').value,
};
await API.adminUpdateSetting('banner', { value: banner });
// Web Search config → global_settings (JSON value)
const searchConfig = {
provider: document.getElementById('adminSearchProvider').value,
endpoint: document.getElementById('adminSearchEndpoint').value.trim(),
api_key: document.getElementById('adminSearchApiKey').value.trim(),
max_results: parseInt(document.getElementById('adminSearchMaxResults').value) || 5,
};
await API.adminUpdateSetting('search_config', { value: searchConfig });
// Auto-Compaction config → global_settings
const compactionEnabled = document.getElementById('adminCompactionEnabled')?.checked || false;
const compactionThreshold = parseInt(document.getElementById('adminCompactionThreshold')?.value) || 70;
const compactionCooldown = parseInt(document.getElementById('adminCompactionCooldown')?.value) || 30;
await API.adminUpdateSetting('auto_compaction', { value: {
enabled: compactionEnabled,
threshold: compactionThreshold,
cooldown: compactionCooldown,
}});
// Scanner reads these individual keys each tick
await API.adminUpdateSetting('auto_compaction_enabled', { value: compactionEnabled });
await API.adminUpdateSetting('auto_compaction_threshold', { value: compactionThreshold / 100 });
await API.adminUpdateSetting('auto_compaction_cooldown_minutes', { value: compactionCooldown });
// Memory extraction (v0.18.0)
const memExtractionEnabled = document.getElementById('adminMemoryExtractionEnabled')?.checked || false;
const memAutoApprove = document.getElementById('adminMemoryAutoApprove')?.checked || false;
await API.adminUpdateSetting('memory_extraction', { value: {
enabled: memExtractionEnabled,
auto_approve: memAutoApprove,
}});
await API.adminUpdateSetting('memory_extraction_enabled', { value: memExtractionEnabled });
// Email / SMTP config (v0.20.0 Phase 3)
if (typeof NotifPrefs !== 'undefined' && NotifPrefs._getAdminSmtp) {
const smtpConfig = NotifPrefs._getAdminSmtp();
await API.adminUpdateSetting('notifications', { value: smtpConfig });
}
UI.toast('Settings saved', 'success');
// Live-apply: refresh policies and dependent UI
await initBanners();
UI.checkUserProvidersAllowed();
UI.checkUserPersonasAllowed();
fetchModels();
} catch (e) { UI.toast(e.message, 'error'); }
}
// ── User Model Role — primitive instance (initialized in _initSettingsListeners) ──
var _userRoleConfig = null;
function _initUserRolePrimitive() {
const el = document.getElementById('userRolesConfig');
if (!el) return;
_userRoleConfig = renderRoleConfig(el, {
scope: 'personal',
showFallback: false,
modelIdField: 'baseModelId',
modelNameField: 'name',
providerIdField: 'configId',
noneLabel: '— use org default —',
fetchProviders: async () => {
const configs = await API.listConfigs();
const list = configs.configs || configs.data || [];
return list.filter(c => c.scope === 'personal');
},
fetchModels: async () => {
// Refresh App.models to ensure personal provider models are current.
// Without this, newly added providers may not appear in the dropdown.
await fetchModels();
return App.models || [];
},
fetchRoles: async () => {
const settings = await API.getSettings();
return settings.model_roles || {};
},
onSave: async (roleId, config) => {
if (!config.primary) throw new Error('Select both a provider and model');
const settings = await API.getSettings();
const roles = settings.model_roles || {};
roles[roleId] = config;
await API.updateSettings({ model_roles: roles });
UI.toast(`${roleId} role override saved`, 'success');
},
onClear: async (roleId) => {
const settings = await API.getSettings();
const roles = settings.model_roles || {};
delete roles[roleId];
await API.updateSettings({ model_roles: Object.keys(roles).length > 0 ? roles : null });
UI.toast(`${roleId} role override removed — using org default`, 'success');
},
});
}
// ── Command Palette ──────────────────────────
const _cmdCommands = [
// Navigation
{ id: 'new-chat', label: 'New Chat', group: 'Navigation', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>', action: () => newChat() },
{ id: 'search-chats', label: 'Search Chats', group: 'Navigation', hint: '⌘⇧S', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>', action: () => { const sb = document.getElementById('sidebar'); if (sb.classList.contains('collapsed')) UI.toggleSidebar(); document.getElementById('chatSearchInput')?.focus(); } },
{ id: 'toggle-sidebar', label: 'Toggle Sidebar', group: 'Navigation', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>', action: () => UI.toggleSidebar() },
// Tools
{ id: 'notes', label: 'Open Notes', group: 'Tools', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>', action: () => openNotes() },
{ id: 'export-md', label: 'Export Chat (Markdown)', group: 'Tools', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>', action: () => UI.exportChat('md') },
{ id: 'export-json', label: 'Export Chat (JSON)', group: 'Tools', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>', action: () => UI.exportChat('json') },
// Settings
{ id: 'settings', label: 'Settings', group: 'Settings', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>', action: () => UI.openSettings() },
{ id: 'admin', label: 'Admin Panel', group: 'Settings', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>', action: () => UI.openAdmin(), visible: () => API.user?.role === 'admin' },
{ id: 'debug', label: 'Debug Log', group: 'Settings', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 11V6a2 2 0 0 0-2-2h-1a2 2 0 0 0-2 2"/><path d="M9 6a2 2 0 0 0-2 2v3"/><circle cx="12" cy="14" r="6"/></svg>', action: () => openDebugModal() },
// Account
{ id: 'sign-out', label: 'Sign Out', group: 'Account', hint: '', icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>', action: () => handleLogout() },
];
let _cmdActiveIndex = 0;
function toggleCmdPalette() {
const el = document.getElementById('cmdPalette');
if (el.classList.contains('active')) { closeCmdPalette(); return; }
openCmdPalette();
}
function openCmdPalette() {
const el = document.getElementById('cmdPalette');
const input = document.getElementById('cmdInput');
el.classList.add('active');
input.value = '';
_cmdActiveIndex = 0;
_renderCmdResults('');
input.focus();
// Wire up input events (use named handler to avoid duplicates)
input.oninput = () => { _cmdActiveIndex = 0; _renderCmdResults(input.value); };
input.onkeydown = _handleCmdKey;
// Close on overlay click
el.onclick = (e) => { if (e.target === el) closeCmdPalette(); };
}
function closeCmdPalette() {
const el = document.getElementById('cmdPalette');
el.classList.remove('active');
document.getElementById('cmdInput').oninput = null;
document.getElementById('cmdInput').onkeydown = null;
}
function _handleCmdKey(e) {
const results = document.getElementById('cmdResults');
const items = results.querySelectorAll('.cmd-item');
if (!items.length) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
_cmdActiveIndex = (_cmdActiveIndex + 1) % items.length;
_highlightCmdItem(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
_cmdActiveIndex = (_cmdActiveIndex - 1 + items.length) % items.length;
_highlightCmdItem(items);
} else if (e.key === 'Enter') {
e.preventDefault();
items[_cmdActiveIndex]?.click();
}
}
function _highlightCmdItem(items) {
items.forEach((it, i) => it.classList.toggle('active', i === _cmdActiveIndex));
items[_cmdActiveIndex]?.scrollIntoView({ block: 'nearest' });
}
function _getVisibleCommands() {
return _cmdCommands.filter(c => !c.visible || c.visible());
}
function _renderCmdResults(query) {
const el = document.getElementById('cmdResults');
const q = query.trim().toLowerCase();
let commands = _getVisibleCommands();
// Add recent chats as commands when searching
if (q) {
const chatMatches = App.chats
.filter(c => (c.title || '').toLowerCase().includes(q))
.slice(0, 5)
.map(c => ({
id: 'chat-' + c.id,
label: c.title || 'Untitled',
group: 'Chats',
hint: _relativeTime(c.updatedAt),
icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
action: () => selectChat(c.id),
}));
commands = [...chatMatches, ...commands];
}
// Filter by query
if (q) {
commands = commands.filter(c => c.label.toLowerCase().includes(q));
}
if (commands.length === 0) {
el.innerHTML = '<div class="cmd-group-label" style="padding:16px 12px;text-transform:none;letter-spacing:0">No matching commands</div>';
return;
}
// Clamp active index
_cmdActiveIndex = Math.min(_cmdActiveIndex, commands.length - 1);
// Render grouped
let html = '';
let lastGroup = '';
commands.forEach((c, i) => {
if (c.group !== lastGroup) {
lastGroup = c.group;
html += `<div class="cmd-group-label">${c.group}</div>`;
}
html += `<div class="cmd-item${i === _cmdActiveIndex ? ' active' : ''}" data-cmd="${c.id}">
${c.icon}
<span class="cmd-item-label">${c.label}</span>
${c.hint ? `<span class="cmd-item-hint">${c.hint}</span>` : ''}
</div>`;
});
el.innerHTML = html;
// Attach click handlers
el.querySelectorAll('.cmd-item').forEach((item, i) => {
item.addEventListener('click', () => {
closeCmdPalette();
commands[i].action();
});
item.addEventListener('mouseenter', () => {
_cmdActiveIndex = i;
_highlightCmdItem(el.querySelectorAll('.cmd-item'));
});
});
}
// ── Settings Listeners (extracted from initListeners) ──
function _initSettingsListeners() {
// Settings modal
document.getElementById('settingsCloseBtn').addEventListener('click', UI.closeSettings);
document.getElementById('settingsSaveBtn').addEventListener('click', handleSaveSettings);
document.querySelectorAll('.settings-tab').forEach(tab => {
tab.addEventListener('click', () => UI.switchSettingsTab(tab.dataset.stab));
});
document.getElementById('settingsModel').addEventListener('change', function() {
const model = App.findModel(this.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)`
: '';
}
});
document.getElementById('profileChangePwBtn').addEventListener('click', () => {
document.getElementById('profileChangePwForm').style.display = '';
});
document.getElementById('profileSavePwBtn').addEventListener('click', handleChangePassword);
// Avatar upload
document.getElementById('avatarUploadBtn').addEventListener('click', () => {
document.getElementById('avatarFileInput').click();
});
document.getElementById('avatarFileInput').addEventListener('change', async function() {
const file = this.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async () => {
try {
const data = await API.uploadAvatar(reader.result);
if (data.avatar) {
API.user.avatar = data.avatar;
API.saveTokens();
updateAvatarPreview(data.avatar);
UI.updateUser();
UI.toast('Avatar updated');
}
} catch (e) { UI.toast(e.message || 'Upload failed', 'error'); }
};
reader.readAsDataURL(file);
this.value = '';
});
document.getElementById('avatarRemoveBtn').addEventListener('click', async () => {
try {
await API.deleteAvatar();
API.user.avatar = null;
API.saveTokens();
updateAvatarPreview(null);
UI.updateUser();
UI.toast('Avatar removed');
} catch (e) { UI.toast(e.message || 'Remove failed', 'error'); }
});
// User BYOK provider primitives
_initUserProviderPrimitives();
_initUserRolePrimitive();
document.getElementById('providerShowAddBtn').addEventListener('click', () => {
const formEl = document.getElementById('providerAddForm');
if (_userProvForm) _userProvForm.setCreateMode();
formEl.style.display = '';
});
// Settings — Team management (team admin self-service)
document.getElementById('settingsTeamAddMemberBtn')?.addEventListener('click', async () => {
document.getElementById('settingsTeamAddMember').style.display = '';
const sel = document.getElementById('settingsTeamMemberUser');
sel.innerHTML = '<option value="">Loading...</option>';
try {
const resp = await API.teamListMembers(UI._managingTeamId);
const existingIds = new Set((resp.data || []).map(m => m.user_id));
sel.innerHTML = '<option value="">Enter user ID or ask sys-admin</option>';
} catch (e) { sel.innerHTML = '<option value="">Error</option>'; }
});
document.getElementById('settingsTeamCancelMember')?.addEventListener('click', () => {
document.getElementById('settingsTeamAddMember').style.display = 'none';
});
var _teamPersonaForm = null;
document.getElementById('settingsTeamAddPersonaBtn')?.addEventListener('click', () => {
const container = document.getElementById('settingsTeamAddPersona');
container.style.display = '';
if (!_teamPersonaForm) {
_teamPersonaForm = renderPersonaForm(container, {
prefix: 'teamPersona',
showAvatar: true,
showProviderConfig: false,
showKBPicker: true,
kbScope: 'team',
kbTeamId: UI._managingTeamId,
onSubmit: async (vals) => {
const teamId = UI._managingTeamId;
if (!vals.name) return UI.toast('Name required', 'error');
if (!vals.base_model_id) return UI.toast('Select a base model', 'error');
try {
const result = await API.teamCreatePersona(teamId, vals);
const personaId = result?.id;
if (personaId && vals._pendingAvatar) {
try { await API.adminUploadPersonaAvatar(personaId, vals._pendingAvatar); }
catch (e) { console.warn('Team persona avatar upload failed:', e.message); }
}
if (personaId && vals._kbValues) {
try { await API.teamSetPresetKBs(teamId, personaId, vals._kbValues); }
catch (e) { console.warn('Team persona KB binding failed:', e.message); }
}
container.style.display = 'none';
_teamPersonaForm.clearForm();
UI.toast('Team persona created');
await UI.loadTeamManagePersonas(teamId);
await fetchModels();
} catch (e) { UI.toast(e.message, 'error'); }
},
onCancel: () => { container.style.display = 'none'; _teamPersonaForm.clearForm(); }
});
} else {
_teamPersonaForm.clearForm();
}
UI.loadTeamPersonaModelDropdown(UI._managingTeamId);
});
// Team — providers (primitive-based, initialized lazily via UI.loadTeamManageProviders)
document.getElementById('settingsTeamAddProviderBtn')?.addEventListener('click', () => {
const formEl = document.getElementById('settingsTeamProviderForm');
if (!formEl) return;
if (UI._teamProvForm) UI._teamProvForm.setCreateMode();
formEl.style.display = formEl.style.display === 'none' ? '' : 'none';
});
// User — personal presets
var _userPersonaForm = null;
document.getElementById('userAddPersonaBtn')?.addEventListener('click', () => {
const container = document.getElementById('userAddPersonaForm');
container.style.display = container.style.display === 'none' ? '' : 'none';
if (!_userPersonaForm) {
_userPersonaForm = renderPersonaForm(container, {
prefix: 'userPersona',
showAvatar: true,
showProviderConfig: false,
showKBPicker: true,
kbScope: 'personal',
onSubmit: async (vals) => {
if (!vals.name) return UI.toast('Name required', 'error');
if (!vals.base_model_id) return UI.toast('Select a base model', 'error');
try {
const result = await API.createUserPersona(vals);
const personaId = result?.id;
if (personaId && vals._pendingAvatar) {
try { await API.adminUploadPersonaAvatar(personaId, vals._pendingAvatar); }
catch (e) { console.warn('User persona avatar upload failed:', e.message); }
}
if (personaId && vals._kbValues) {
try { await API.setUserPresetKBs(personaId, vals._kbValues); }
catch (e) { console.warn('User persona KB binding failed:', e.message); }
}
container.style.display = 'none';
_userPersonaForm.clearForm();
UI.toast('Persona created');
await UI.loadUserPersonas();
await fetchModels();
} catch (e) { UI.toast(e.message, 'error'); }
},
onCancel: () => { container.style.display = 'none'; _userPersonaForm.clearForm(); }
});
}
const sel = _userPersonaForm.getModelSelect();
if (sel) {
sel.innerHTML = '<option value="">Select base model...</option>';
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);
});
}
});
// Admin — banner controls
document.getElementById('adminBannerEnabled')?.addEventListener('change', (e) => {
document.getElementById('bannerConfigFields').style.display = e.target.checked ? '' : 'none';
});
document.getElementById('adminBannerPreset')?.addEventListener('change', (e) => {
const preset = UI._bannerPresets[e.target.value];
if (persona) {
document.getElementById('adminBannerText').value = preset.text || '';
document.getElementById('adminBannerBg').value = preset.bg || '#007a33';
document.getElementById('adminBannerBgHex').value = preset.bg || '#007a33';
document.getElementById('adminBannerFg').value = preset.fg || '#ffffff';
document.getElementById('adminBannerFgHex').value = preset.fg || '#ffffff';
UI.updateBannerPreview();
}
});
['adminBannerText', 'adminBannerBg', 'adminBannerFg'].forEach(id => {
document.getElementById(id)?.addEventListener('input', UI.updateBannerPreview);
});
document.getElementById('adminBannerBg')?.addEventListener('input', (e) => { document.getElementById('adminBannerBgHex').value = e.target.value; });
document.getElementById('adminBannerFg')?.addEventListener('input', (e) => { document.getElementById('adminBannerFgHex').value = e.target.value; });
document.getElementById('adminBannerBgHex')?.addEventListener('input', (e) => { if (/^#[0-9a-f]{6}$/i.test(e.target.value)) { document.getElementById('adminBannerBg').value = e.target.value; UI.updateBannerPreview(); }});
document.getElementById('adminBannerFgHex')?.addEventListener('input', (e) => { if (/^#[0-9a-f]{6}$/i.test(e.target.value)) { document.getElementById('adminBannerFg').value = e.target.value; UI.updateBannerPreview(); }});
// Admin — auto-compaction controls
document.getElementById('adminCompactionEnabled')?.addEventListener('change', (e) => {
document.getElementById('compactionConfigFields').style.display = e.target.checked ? '' : 'none';
});
// Admin — search provider controls
document.getElementById('adminSearchProvider')?.addEventListener('change', (e) => {
document.getElementById('searxngConfigFields').style.display = e.target.value === 'searxng' ? '' : 'none';
});
// Admin — audit log
document.getElementById('auditRefreshBtn')?.addEventListener('click', () => UI.loadAuditLog(1));
document.getElementById('auditFilterAction')?.addEventListener('change', () => UI.loadAuditLog(1));
document.getElementById('auditFilterResource')?.addEventListener('change', () => UI.loadAuditLog(1));
document.getElementById('auditPrevBtn')?.addEventListener('click', () => { if (UI._auditPage > 1) UI.loadAuditLog(UI._auditPage - 1); });
document.getElementById('auditNextBtn')?.addEventListener('click', () => UI.loadAuditLog(UI._auditPage + 1));
// Admin — usage
document.getElementById('usageRefreshBtn')?.addEventListener('click', () => UI.loadAdminUsage());
document.getElementById('usagePeriod')?.addEventListener('change', () => UI.loadAdminUsage());
document.getElementById('usageGroupBy')?.addEventListener('change', () => UI.loadAdminUsage());
}