Upload 7 modified files from chat-switchboard-v0.5.2.zip

This commit is contained in:
2026-02-18 22:14:35 +00:00
parent a6b55a0352
commit 92f4f87b6a
5 changed files with 750 additions and 788 deletions

View File

@@ -1,5 +1,5 @@
// ==========================================
// Chat Switchboard Application (v0.5.0)
// Chat Switchboard Application (v0.5.2)
// ==========================================
const App = {
@@ -25,7 +25,6 @@ async function init() {
console.log('🔀 Chat Switchboard initializing...');
API.loadTokens();
// Always check backend health first
let health = null;
try {
health = await API.health();
@@ -37,20 +36,13 @@ async function init() {
return;
}
// If we have tokens, validate them
if (API.isAuthed) {
try {
await API.getProfile();
console.log('✅ Session valid for', API.user?.username);
} catch (e) {
if (e.status === 401) {
console.warn('⚠️ Session expired, clearing');
API.clearTokens();
} else {
// 404 = profile not created yet, other errors = transient
// Token is still valid, proceed
console.log(' Profile fetch returned', e.status || e.message, '— session OK');
}
console.warn('⚠️ Session expired, clearing');
API.clearTokens();
}
}
@@ -63,12 +55,13 @@ async function init() {
async function startApp() {
hideSplash();
UI.restoreSidebar();
await loadSettings();
await loadChats();
await fetchModels();
UI.renderChatList();
UI.updateModelSelector();
UI.updateConnectionStatus();
UI.updateUser();
UI.showAdminButton(API.isAdmin);
initListeners();
console.log('✅ Chat Switchboard ready');
@@ -85,9 +78,7 @@ async function loadSettings() {
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);
}
} catch (e) { console.warn('Settings load failed:', e.message); }
}
async function saveSettings() {
@@ -98,9 +89,7 @@ async function saveSettings() {
max_tokens: App.settings.maxTokens,
temperature: App.settings.temperature,
});
} catch (e) {
console.warn('Settings sync failed:', e.message);
}
} catch (e) { console.warn('Settings sync failed:', e.message); }
}
// ── Models ───────────────────────────────────
@@ -114,7 +103,6 @@ async function fetchModels() {
configId: m.config_id || null
}));
// Fallback to raw aggregation if no curated models
if (App.models.length === 0) {
const raw = await API.listAllModels();
App.models = (raw.models || []).map(m => ({
@@ -126,9 +114,7 @@ async function fetchModels() {
App.models.sort((a, b) => a.id.localeCompare(b.id));
console.log(`📋 Loaded ${App.models.length} models`);
} catch (e) {
console.warn('Model fetch failed:', e.message);
}
} catch (e) { console.warn('Model fetch failed:', e.message); }
UI.updateModelSelector();
}
@@ -142,7 +128,7 @@ async function loadChats() {
title: c.title,
model: c.model || '',
messageCount: c.message_count || 0,
messages: [], // lazy loaded
messages: [],
updatedAt: c.updated_at
}));
} catch (e) {
@@ -158,15 +144,11 @@ async function selectChat(chatId) {
const chat = App.chats.find(c => c.id === chatId);
if (!chat) return;
// Lazy-load messages
if (chat.messages.length === 0 && chat.messageCount > 0) {
try {
const resp = await API.listMessages(chatId);
chat.messages = (resp.data || []).map(m => ({
role: m.role,
content: m.content,
model: m.model || '',
timestamp: m.created_at
role: m.role, content: m.content, model: m.model || '', timestamp: m.created_at
}));
} catch (e) {
console.error('Failed to load messages:', e.message);
@@ -182,6 +164,7 @@ async function newChat() {
App.currentChatId = null;
UI.renderChatList();
UI.showEmptyState();
UI.showRegenerate(false);
document.getElementById('messageInput').focus();
}
@@ -190,13 +173,9 @@ async function deleteChat(chatId) {
try {
await API.deleteChat(chatId);
App.chats = App.chats.filter(c => c.id !== chatId);
if (App.currentChatId === chatId) {
newChat();
}
if (App.currentChatId === chatId) newChat();
UI.renderChatList();
} catch (e) {
UI.toast('Failed to delete: ' + e.message, 'error');
}
} catch (e) { UI.toast('Failed to delete: ' + e.message, 'error'); }
}
// ── Send Message ─────────────────────────────
@@ -211,69 +190,45 @@ async function sendMessage() {
const model = document.getElementById('modelSelect').value || App.settings.model;
// Create chat on first message
if (!App.currentChatId) {
try {
const title = text.slice(0, 50) + (text.length > 50 ? '...' : '');
const resp = await API.createChat(title, model, App.settings.systemPrompt);
const chat = {
id: resp.id,
title: resp.title,
model: model,
messages: [],
messageCount: 0,
updatedAt: resp.updated_at
};
const chat = { id: resp.id, title: resp.title, model, messages: [], messageCount: 0, updatedAt: resp.updated_at };
App.chats.unshift(chat);
App.currentChatId = chat.id;
UI.renderChatList();
} catch (e) {
UI.toast('Failed to create chat: ' + e.message, 'error');
return;
}
} catch (e) { UI.toast('Failed to create chat: ' + e.message, 'error'); return; }
}
// Add user message to local state + render
const chat = App.chats.find(c => c.id === App.currentChatId);
if (!chat) return;
chat.messages.push({ role: 'user', content: text, timestamp: new Date().toISOString() });
UI.renderMessages(chat.messages);
// Stream the completion
App.isGenerating = true;
App.abortController = new AbortController();
UI.setGenerating(true);
try {
const resp = await API.streamCompletion(
App.currentChatId, text, model, App.abortController.signal
);
const resp = await API.streamCompletion(App.currentChatId, text, model, App.abortController.signal);
const assistantContent = await UI.streamResponse(resp, chat.messages);
chat.messages.push({
role: 'assistant',
content: assistantContent,
model: model,
timestamp: new Date().toISOString()
});
chat.messages.push({ role: 'assistant', content: assistantContent, model, timestamp: new Date().toISOString() });
chat.messageCount = chat.messages.length;
UI.showRegenerate(true);
} catch (e) {
if (e.name === 'AbortError') {
UI.toast('Generation stopped', 'warning');
} else {
console.error('Completion error:', e);
// Distinguish provider-side auth errors from session issues
const msg = e.message || '';
if (msg.includes('provider error') && msg.includes('401')) {
UI.toast('Provider API key rejected — check your key in Settings → Providers', 'error');
UI.toast('Provider API key rejected — check Settings → Providers', 'error');
} else if (msg.includes('provider error') && msg.includes('429')) {
UI.toast('Provider rate limit hit — wait a moment and retry', 'warning');
UI.toast('Provider rate limit — wait and retry', 'warning');
} else if (msg.includes('no API config') || msg.includes('no model')) {
UI.toast('No provider configured — add one in Settings → Providers', 'error');
UI.toast('No provider configured — add one in Settings', 'error');
} else {
UI.toast(msg, 'error');
}
@@ -285,39 +240,27 @@ async function sendMessage() {
}
}
function stopGeneration() {
if (App.abortController) {
App.abortController.abort();
}
}
function stopGeneration() { if (App.abortController) App.abortController.abort(); }
async function regenerate() {
if (App.isGenerating || !App.currentChatId) return;
const chat = App.chats.find(c => c.id === App.currentChatId);
if (!chat || chat.messages.length === 0) return;
// Pop trailing assistant messages
while (chat.messages.length && chat.messages[chat.messages.length - 1].role === 'assistant') {
chat.messages.pop();
}
while (chat.messages.length && chat.messages[chat.messages.length - 1].role === 'assistant') chat.messages.pop();
if (chat.messages.length === 0) return;
UI.renderMessages(chat.messages);
// Re-send the last user message
const lastUser = chat.messages[chat.messages.length - 1];
if (lastUser.role !== 'user') return;
const model = document.getElementById('modelSelect').value || App.settings.model;
App.isGenerating = true;
App.abortController = new AbortController();
UI.setGenerating(true);
try {
const resp = await API.streamCompletion(
App.currentChatId, lastUser.content, model, App.abortController.signal
);
const resp = await API.streamCompletion(App.currentChatId, lastUser.content, model, App.abortController.signal);
const content = await UI.streamResponse(resp, chat.messages);
chat.messages.push({ role: 'assistant', content, model, timestamp: new Date().toISOString() });
UI.showRegenerate(true);
@@ -325,10 +268,8 @@ async function regenerate() {
if (e.name !== 'AbortError') {
const msg = e.message || '';
if (msg.includes('provider error') && msg.includes('401')) {
UI.toast('Provider API key rejected — check Settings → Providers', 'error');
} else {
UI.toast(msg, 'error');
}
UI.toast('Provider API key rejected — check Settings', 'error');
} else { UI.toast(msg, 'error'); }
}
} finally {
App.isGenerating = false;
@@ -342,7 +283,6 @@ async function regenerate() {
function showSplash(health) {
document.getElementById('splashGate').style.display = 'flex';
document.getElementById('appContainer').style.display = 'none';
if (health && health.registration_enabled === false) {
document.getElementById('authTabRegister').style.display = 'none';
}
@@ -357,16 +297,10 @@ async function handleLogin() {
const login = document.getElementById('authLogin').value.trim();
const password = document.getElementById('authPassword').value;
if (!login || !password) return setAuthError('Fill in all fields');
setAuthLoading(true);
try {
await API.login(login, password);
await startApp();
} catch (e) {
setAuthError(e.message);
} finally {
setAuthLoading(false);
}
try { await API.login(login, password); await startApp(); }
catch (e) { setAuthError(e.message); }
finally { setAuthLoading(false); }
}
async function handleRegister() {
@@ -375,16 +309,10 @@ async function handleRegister() {
const password = document.getElementById('authRegPassword').value;
if (!username || !email || !password) return setAuthError('Fill in all fields');
if (password.length < 8) return setAuthError('Password must be at least 8 characters');
setAuthLoading(true);
try {
await API.register(username, email, password);
await startApp();
} catch (e) {
setAuthError(e.message);
} finally {
setAuthLoading(false);
}
try { await API.register(username, email, password); await startApp(); }
catch (e) { setAuthError(e.message); }
finally { setAuthLoading(false); }
}
async function handleLogout() {
@@ -409,7 +337,7 @@ function setAuthError(msg) { document.getElementById('authError').textContent =
function setAuthLoading(on) {
document.querySelectorAll('#authLoginBtn, #authRegisterBtn').forEach(btn => {
btn.disabled = on;
btn.textContent = on ? 'Please wait...' : btn.dataset.label;
btn.textContent = on ? 'Please wait...' : btn.dataset.label;
});
}
@@ -420,43 +348,56 @@ function initListeners() {
if (_listenersInit) return;
_listenersInit = true;
// Sidebar
document.getElementById('sidebarToggle').addEventListener('click', UI.toggleSidebar);
document.getElementById('newChatBtn').addEventListener('click', newChat);
// User flyout
document.getElementById('userMenuBtn').addEventListener('click', (e) => {
e.stopPropagation();
UI.toggleUserMenu();
});
document.addEventListener('click', (e) => {
if (!e.target.closest('.sidebar-bottom')) UI.closeUserMenu();
});
// Flyout items
document.getElementById('menuSettings').addEventListener('click', () => { UI.closeUserMenu(); UI.openSettings(); });
document.getElementById('menuAdmin').addEventListener('click', () => { UI.closeUserMenu(); UI.openAdmin(); });
document.getElementById('menuDebug').addEventListener('click', () => { UI.closeUserMenu(); openDebugModal(); });
document.getElementById('menuSignout').addEventListener('click', () => { UI.closeUserMenu(); handleLogout(); });
// Chat actions
document.getElementById('sendBtn').addEventListener('click', sendMessage);
document.getElementById('stopBtn').addEventListener('click', stopGeneration);
document.getElementById('regenerateBtn').addEventListener('click', regenerate);
// Settings
document.getElementById('settingsBtn').addEventListener('click', UI.openSettings);
document.getElementById('settingsCloseBtn').addEventListener('click', UI.closeSettings);
document.getElementById('settingsSaveBtn').addEventListener('click', handleSaveSettings);
document.getElementById('fetchModelsBtn').addEventListener('click', async () => {
await fetchModels();
UI.toast(`Loaded ${App.models.length} models`, 'success');
});
// Profile
document.getElementById('profileChangePwBtn').addEventListener('click', () => {
document.getElementById('profileChangePwForm').style.display = '';
});
document.getElementById('profileSavePwBtn').addEventListener('click', handleChangePassword);
// Providers
document.getElementById('providerShowAddBtn').addEventListener('click', UI.showProviderForm);
document.getElementById('providerCancelBtn').addEventListener('click', UI.hideProviderForm);
document.getElementById('providerCreateBtn').addEventListener('click', handleCreateProvider);
// Admin
document.getElementById('adminBtn').addEventListener('click', UI.openAdmin);
document.getElementById('adminCloseBtn').addEventListener('click', UI.closeAdmin);
document.querySelectorAll('.admin-tab').forEach(tab => {
tab.addEventListener('click', () => UI.switchAdminTab(tab.dataset.tab));
});
document.getElementById('adminSaveSettings').addEventListener('click', handleSaveAdminSettings);
// Model selector
document.getElementById('modelSelect').addEventListener('change', function() {
if (this.value) { App.settings.model = this.value; saveSettings(); }
});
document.getElementById('fetchModelsBtn').addEventListener('click', async () => {
await fetchModels();
UI.toast(`Loaded ${App.models.length} models`, 'success');
});
// Settings modal
document.getElementById('settingsCloseBtn').addEventListener('click', UI.closeSettings);
document.getElementById('settingsSaveBtn').addEventListener('click', handleSaveSettings);
document.getElementById('profileChangePwBtn').addEventListener('click', () => {
document.getElementById('profileChangePwForm').style.display = '';
});
document.getElementById('profileSavePwBtn').addEventListener('click', handleChangePassword);
document.getElementById('providerShowAddBtn').addEventListener('click', UI.showProviderForm);
document.getElementById('providerCancelBtn').addEventListener('click', UI.hideProviderForm);
document.getElementById('providerCreateBtn').addEventListener('click', handleCreateProvider);
// Admin modal
document.getElementById('adminCloseBtn').addEventListener('click', UI.closeAdmin);
document.querySelectorAll('.admin-tab').forEach(tab => {
tab.addEventListener('click', () => UI.switchAdminTab(tab.dataset.tab));
});
document.getElementById('adminSaveSettings').addEventListener('click', handleSaveAdminSettings);
// Input
const input = document.getElementById('messageInput');
@@ -468,19 +409,7 @@ function initListeners() {
this.style.height = Math.min(this.scrollHeight, 200) + 'px';
});
// Connection status
document.getElementById('connectionStatus').addEventListener('click', handleLogout);
// Export dropdown
document.getElementById('exportBtn').addEventListener('click', (e) => {
e.stopPropagation();
document.getElementById('exportDropdown').classList.toggle('show');
});
document.addEventListener('click', () => {
document.getElementById('exportDropdown').classList.remove('show');
});
// Close modals on overlay
// Close modals on overlay click
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', (e) => {
if (e.target === overlay) overlay.classList.remove('active');
@@ -501,7 +430,6 @@ function handleSaveSettings() {
App.settings.maxTokens = parseInt(document.getElementById('settingsMaxTokens').value) || 4096;
App.settings.temperature = parseFloat(document.getElementById('settingsTemp').value) || 0.7;
App.settings.showThinking = document.getElementById('settingsThinking').checked;
saveSettings();
UI.updateModelSelector();
UI.closeSettings();
@@ -513,16 +441,13 @@ async function handleChangePassword() {
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');
}
} catch (e) { UI.toast(e.message, 'error'); }
}
async function handleCreateProvider() {
@@ -531,18 +456,14 @@ async function handleCreateProvider() {
const endpoint = document.getElementById('providerEndpoint').value.trim();
const apiKey = document.getElementById('providerApiKey').value.trim();
const model = document.getElementById('providerDefaultModel').value.trim();
if (!name || !endpoint || !apiKey) return UI.toast('Fill in required fields', 'warning');
try {
await API.createConfig(name, provider, endpoint, apiKey, model);
UI.toast('Provider added', 'success');
UI.hideProviderForm();
UI.loadProviderList();
fetchModels();
} catch (e) {
UI.toast(e.message, 'error');
}
} catch (e) { UI.toast(e.message, 'error'); }
}
async function deleteProvider(id, name) {
@@ -552,23 +473,16 @@ async function deleteProvider(id, name) {
UI.toast('Provider removed', 'success');
UI.loadProviderList();
fetchModels();
} catch (e) {
UI.toast(e.message, 'error');
}
} catch (e) { UI.toast(e.message, 'error'); }
}
// ── Admin Handlers ───────────────────────────
async function handleSaveAdminSettings() {
try {
const reg = document.getElementById('adminRegToggle').checked;
await API.adminUpdateSetting('registration_enabled', { value: reg });
UI.toast('Admin settings saved', 'success');
} catch (e) {
UI.toast(e.message, 'error');
}
} catch (e) { UI.toast(e.message, 'error'); }
}
// ── Boot ─────────────────────────────────────
document.addEventListener('DOMContentLoaded', init);