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/pages.js
2026-03-14 14:43:55 +00:00

353 lines
15 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 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 = '<option value="">— select model —</option>' +
filtered.map(m =>
`<option value="${m.model_id}">${esc(m.display_name || m.model_id)}</option>`
).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
})();