// ========================================== // Chat Switchboard – Pages (v0.22.5) // ========================================== // Client-side handlers for Go template-rendered pages. // Works with server-rendered HTML — no DOM construction, // just event handling and API calls. // // The server pre-populates all dropdowns. This JS handles: // - Cascading model select (provider change → filter models) // - Admin save operations (roles, routing, providers, teams, users, settings) // - Table filtering // // Exports: window.Pages, window._val (used by pages-splash.js) // ========================================== (function() { 'use strict'; const Pages = { // ── Model Select Cascading ─────────────── roleProviderChanged(selectEl) { const providerID = selectEl.value; const filterType = selectEl.dataset.filterType || ''; const row = selectEl.closest('.form-row') || selectEl.parentElement; const modelSelect = row.querySelector('.model-model-select'); if (!modelSelect) return; const models = _pageData('Models') || []; const filtered = models.filter(m => m.provider_config_id === providerID && (!filterType || m.model_type === filterType) ); modelSelect.innerHTML = '' + filtered.map(m => `` ).join(''); }, // ── Role Save ──────────────────────────── async saveRole(roleName) { const container = document.querySelector(`.role-config[data-role="${roleName}"]`); if (!container) return; const getValue = (slot, cls) => { const sel = container.querySelector(`[data-slot="${slot}"].${cls}`); return sel ? sel.value : ''; }; const body = { role: roleName, primary: getValue('primary','model-provider-select') && getValue('primary','model-model-select') ? { provider_config_id: getValue('primary','model-provider-select'), model_id: getValue('primary','model-model-select') } : null, fallback: getValue('fallback','model-provider-select') && getValue('fallback','model-model-select') ? { provider_config_id: getValue('fallback','model-provider-select'), model_id: getValue('fallback','model-model-select') } : null, }; const resp = await _api('PUT', '/api/v1/admin/roles/' + roleName, body); resp ? _toast('Role saved', 'success') : null; }, // ── Routing Policy ─────────────────────── showRoutingForm() { _show('routingPolicyForm'); }, hideRoutingForm() { _hide('routingPolicyForm'); }, routingScopeChanged(sel) { const row = document.getElementById('routingTeamRow'); if (row) row.style.display = sel.value === 'team' ? '' : 'none'; }, async saveRoutingPolicy() { const id = _val('routingPolicyId'); const body = { name: _val('routingPolicyName'), priority: parseInt(_val('routingPolicyPriority') || '100'), type: _val('routingPolicyType'), scope: _val('routingPolicyScope'), team_id: _val('routingPolicyScope') === 'team' ? _val('routingPolicyTeamId') : '', config: _parseJSON(_val('routingPolicyConfig')), active: document.getElementById('routingPolicyActive')?.checked ?? true, }; if (body.config === null) return; // parse error const ok = await _api(id ? 'PUT' : 'POST', id ? '/api/v1/admin/routing/policies/' + id : '/api/v1/admin/routing/policies', body); if (ok) { this.hideRoutingForm(); window.location.reload(); } }, // ── Providers ──────────────────────────── showProviderForm() { _show('providerFormWrap'); _val('providerFormId', ''); _val('providerFormTitle', 'Add Provider'); }, hideProviderForm() { _hide('providerFormWrap'); }, editProvider(id) { const details = (_pageData('ProviderDetails') || []).find(p => p.id === id); if (!details) return; _val('providerFormId', id); _val('providerFormName', details.name); _val('providerFormEndpoint', details.endpoint); document.getElementById('providerFormTitle').textContent = 'Edit Provider'; _show('providerFormWrap'); }, async saveProvider() { const id = _val('providerFormId'); const body = { name: _val('providerFormName'), provider: _val('providerFormType'), endpoint: _val('providerFormEndpoint'), scope: _val('providerFormScope'), }; const key = _val('providerFormKey'); if (key) body.api_key = key; const teamId = _val('providerFormTeamId'); if (body.scope === 'team' && teamId) body.team_id = teamId; const ok = await _api(id ? 'PUT' : 'POST', id ? '/api/v1/admin/providers/' + id : '/api/v1/admin/providers', body); if (ok) { this.hideProviderForm(); window.location.reload(); } }, async syncProvider(id) { _toast('Syncing…', 'info'); const ok = await _api('POST', '/api/v1/admin/providers/' + id + '/sync', {}); if (ok) { _toast('Sync complete', 'success'); window.location.reload(); } }, // ── Model Filtering ────────────────────── filterModels(query) { const q = (query || _val('modelSearch') || '').toLowerCase(); const typeF = _val('modelTypeFilter') || ''; const provF = _val('modelProviderFilter') || ''; document.querySelectorAll('#modelTable tbody tr').forEach(tr => { const name = (tr.dataset.name || '').toLowerCase(); const type = tr.dataset.type || ''; const prov = tr.dataset.provider || ''; const show = (!q || name.includes(q)) && (!typeF || type === typeF) && (!provF || prov === provF); tr.style.display = show ? '' : 'none'; }); }, // ── Teams ──────────────────────────────── showTeamForm() { _show('teamFormWrap'); _val('teamFormId', ''); _val('teamFormName', ''); _val('teamFormDescription', ''); }, hideTeamForm() { _hide('teamFormWrap'); }, editTeam(id, name, desc) { _val('teamFormId', id); _val('teamFormName', name); _val('teamFormDescription', desc); document.getElementById('teamFormTitle').textContent = 'Edit Team'; _show('teamFormWrap'); }, async saveTeam() { const id = _val('teamFormId'); const body = { name: _val('teamFormName'), description: _val('teamFormDescription') }; const ok = await _api(id ? 'PUT' : 'POST', id ? '/api/v1/admin/teams/' + id : '/api/v1/admin/teams', body); if (ok) { this.hideTeamForm(); window.location.reload(); } }, // ── Users ──────────────────────────────── filterUsers(query) { const q = (query || '').toLowerCase(); document.querySelectorAll('#userTable tbody tr').forEach(tr => { tr.style.display = (!q || (tr.dataset.name || '').toLowerCase().includes(q)) ? '' : 'none'; }); }, async editUserRole(id, username, currentRole) { const role = await showPrompt({ title: `Set role for ${username}`, value: currentRole, ok: 'Save' }); if (!role || role === currentRole) return; const ok = await _api('PUT', '/api/v1/admin/users/' + id, { role }); if (ok) window.location.reload(); }, async toggleUser(id, active) { const ok = await _api('PUT', '/api/v1/admin/users/' + id, { is_active: active }); if (ok) window.location.reload(); }, // ── Settings ───────────────────────────── async saveSettings() { // Policies const policies = { allow_registration: document.getElementById('settRegEnabled')?.checked ? 'true' : 'false', default_user_active: _val('settRegDefaultState') === 'active' ? 'true' : 'false', allow_user_byok: document.getElementById('settUserBYOK')?.checked ? 'true' : 'false', allow_user_personas: document.getElementById('settUserPersonas')?.checked ? 'true' : 'false', kb_direct_access: document.getElementById('settKBDirect')?.checked ? 'true' : 'false', }; // Banner const banner = { enabled: document.getElementById('settBannerEnabled')?.checked || false, text: _val('settBannerText'), bg: _val('settBannerBg'), fg: _val('settBannerFg'), }; // System prompt const system_prompt = { content: _val('settSystemPrompt') }; const ok = await _api('PUT', '/api/v1/admin/settings', { policies, settings: { banner, system_prompt } }); if (ok) _toast('Settings saved', 'success'); }, // ── Login ───────────────────────────────── async doLogin() { const username = _val('loginUsername'); const password = _val('loginPassword'); const errEl = document.getElementById('loginError'); const btn = document.getElementById('loginBtn'); if (!username || !password) { if (errEl) { errEl.textContent = 'Enter username and password'; errEl.style.display = ''; } return; } if (errEl) errEl.style.display = 'none'; if (btn) { btn.disabled = true; btn.textContent = 'Logging in…'; } const base = window.__BASE__ || ''; try { const resp = await fetch(base + '/api/v1/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ login: username, password }), }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.error || `Login failed (${resp.status})`); } const data = await resp.json(); // Save tokens in same format as API._setAuth / API.saveTokens const storageKey = base ? `sb_auth_${base.replace(/\//g, '')}` : 'sb_auth'; localStorage.setItem(storageKey, JSON.stringify({ accessToken: data.access_token, refreshToken: data.refresh_token, user: data.user, })); // Set cookie for server-rendered page auth document.cookie = `sb_token=${data.access_token}; path=/; max-age=900; SameSite=Strict`; // Redirect to chat surface window.location.href = base + '/'; } catch (e) { if (errEl) { errEl.textContent = e.message; errEl.style.display = ''; } if (btn) { btn.disabled = false; btn.textContent = 'Log In'; } } }, // ── User Settings (settings surface) ───── async saveProfile() { const name = _val('profileDisplayName'); if (!name) { _toast('Display name is required', 'error'); return; } const ok = await _api('PUT', '/api/v1/profile', { display_name: name }); if (ok) _toast('Profile saved', 'success'); }, async changePassword() { const current = _val('settingsCurrentPw'); const newPw = _val('settingsNewPw'); const confirm = _val('settingsConfirmPw'); if (!current || !newPw) { _toast('All password fields are required', 'error'); return; } if (newPw !== confirm) { _toast('Passwords do not match', 'error'); return; } if (newPw.length < 8) { _toast('Password must be at least 8 characters', 'error'); return; } const ok = await _api('POST', '/api/v1/profile/password', { current_password: current, new_password: newPw }); if (ok) { _toast('Password changed', 'success'); _val('settingsCurrentPw', ''); _val('settingsNewPw', ''); _val('settingsConfirmPw', ''); } }, }; // ── Init: banner toggle ────────────────────── document.addEventListener('DOMContentLoaded', () => { const cb = document.getElementById('settBannerEnabled'); const fields = document.getElementById('bannerFields'); if (cb && fields) { cb.addEventListener('change', () => { fields.style.display = cb.checked ? '' : 'none'; }); fields.style.display = cb.checked ? '' : 'none'; } }); // ── Helpers ────────────────────────────────── function _pageData(key) { const d = window.__PAGE_DATA__; return d ? (d[key] || d[key.toLowerCase()]) : null; } function _val(id, setVal) { const el = document.getElementById(id); if (!el) return ''; if (setVal !== undefined) { el.value = setVal; return; } return el.value; } function _show(id) { const el = document.getElementById(id); if (el) el.style.display = ''; } function _hide(id) { const el = document.getElementById(id); if (el) el.style.display = 'none'; } function _toast(msg, type) { if (typeof UI !== 'undefined' && UI.toast) { UI.toast(msg, type); } else { console[type === 'error' ? 'error' : 'log']('[Pages]', msg); } } function _parseJSON(text) { if (!text || !text.trim()) return {}; try { return JSON.parse(text); } catch (e) { _toast('Invalid JSON', 'error'); return null; } } async function _api(method, path, body) { const base = window.__BASE__ || ''; const storageKey = base ? `sb_auth_${base.replace(/\//g, '')}` : 'sb_auth'; let token = ''; try { const saved = JSON.parse(localStorage.getItem(storageKey) || '{}'); token = saved.accessToken || ''; } catch (e) { /* ignore */ } const headers = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = 'Bearer ' + token; try { const resp = await fetch(base + path, { method, headers, body: body ? JSON.stringify(body) : undefined }); if (resp.ok) return true; const err = await resp.json().catch(() => ({})); _toast(err.error || `Error ${resp.status}`, 'error'); return false; } catch (e) { _toast('Connection error', 'error'); return false; } } // ── Exports ───────────────────────────────── sb.ns('Pages', Pages); sb.register('_val', _val); // used by pages-splash.js })();