// ========================================== // Chat Switchboard - Main Application // ========================================== async function init() { console.log('🔀 Chat Switchboard initializing...'); loadSettings(); loadModels(); Backend.init(); // Auto-detect backend at same origin let health = null; if (!Backend.baseUrl) { health = await Backend.checkHealth(''); if (health && health.database) { Backend.baseUrl = window.location.origin; Backend.save(); console.log('🔗 Backend detected at same origin'); } } // If we have saved auth, validate it if (Backend.accessToken) { health = health || await Backend.checkHealth(Backend.baseUrl); if (!health) { console.log('⚠️ Backend unreachable, falling back to unmanaged'); Backend.clear(); } } // Decide: splash gate or straight to app if (Backend.baseUrl && !Backend.accessToken) { // Online but not authenticated → show splash, hide app showSplashGate(health); initAuthListeners(); return; // Don't init app yet — authSuccess() will do it } // Already authed or no backend → go straight to app hideSplashGate(); await loadSettingsFromBackend(); await initApp(); } async function initApp() { await loadChats(); initializeEventListeners(); // In managed mode, fetch models from backend (ignore localStorage cache) if (Backend.isManaged) { fetchModels(false); // async, non-blocking } updateQuickModelSelector(); renderChatHistory(); updateConnectionStatus(); initAdmin(); console.log(`✅ Chat Switchboard ready (${Backend.isManaged ? 'managed' : 'unmanaged'} mode)`); } function showSplashGate(health) { const splash = document.getElementById('splashGate'); const app = document.getElementById('appContainer'); splash.classList.add('visible'); splash.style.display = 'flex'; // Fallback for cached CSS app.classList.add('app-hidden'); app.style.display = 'none'; // Fallback for cached CSS // Hide register tab if registration disabled const registerTab = document.getElementById('authTabRegister'); if (health && health.registration_enabled === false) { registerTab.style.display = 'none'; } else { registerTab.style.display = ''; } } function hideSplashGate() { const splash = document.getElementById('splashGate'); const app = document.getElementById('appContainer'); splash.classList.remove('visible'); splash.style.display = 'none'; // Hide splash app.classList.remove('app-hidden'); app.style.display = ''; // Show app } async function authSuccess() { hideSplashGate(); await loadSettingsFromBackend(); await initApp(); showToast(`✅ Welcome, ${Backend.user?.display_name || Backend.user?.username || 'user'}!`, 'success'); } function initializeEventListeners() { // Settings modal document.getElementById('settingsBtn').addEventListener('click', openSettings); document.getElementById('configureBtn').addEventListener('click', openSettings); document.getElementById('closeModalBtn').addEventListener('click', closeSettings); document.getElementById('cancelBtn').addEventListener('click', closeSettings); document.getElementById('saveBtn').addEventListener('click', handleSaveSettings); document.getElementById('fetchModelsBtn').addEventListener('click', () => fetchModels(true)); // Profile document.getElementById('profileChangePwBtn').addEventListener('click', () => { document.getElementById('profileChangePwForm').style.display = ''; }); document.getElementById('profileSavePwBtn').addEventListener('click', handleChangePassword); // Providers (managed mode) document.getElementById('providerShowAddBtn').addEventListener('click', showProviderForm); document.getElementById('providerCancelBtn').addEventListener('click', hideProviderForm); document.getElementById('providerCreateBtn').addEventListener('click', handleCreateProvider); document.getElementById('providerType').addEventListener('change', updateProviderEndpointHint); // Chat controls document.getElementById('newChatBtn').addEventListener('click', newChat); document.getElementById('sendBtn').addEventListener('click', sendMessage); document.getElementById('stopBtn').addEventListener('click', stopGeneration); document.getElementById('regenerateBtn').addEventListener('click', regenerateResponse); document.getElementById('toggleSidebarBtn').addEventListener('click', toggleSidebar); document.getElementById('quickFetchBtn').addEventListener('click', () => fetchModels(false)); // Quick model selector document.getElementById('quickModel').addEventListener('change', function() { if (this.value) { State.settings.model = this.value; saveSettings(); } }); // Export dropdown document.getElementById('exportBtn').addEventListener('click', function(e) { e.stopPropagation(); document.getElementById('exportDropdown').classList.toggle('show'); }); document.addEventListener('click', function() { document.getElementById('exportDropdown').classList.remove('show'); }); // Message input const messageInput = document.getElementById('messageInput'); messageInput.addEventListener('keydown', handleKeyDown); messageInput.addEventListener('input', function() { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 200) + 'px'; }); // Settings modal model sync document.getElementById('model').addEventListener('change', function() { if (this.value) document.getElementById('modelCustom').value = ''; }); document.getElementById('modelCustom').addEventListener('input', function() { if (this.value) document.getElementById('model').value = ''; }); // Close modals on overlay click document.getElementById('settingsModal').addEventListener('click', function(e) { if (e.target === this) closeSettings(); }); // Connection status click document.getElementById('connectionStatus').addEventListener('click', handleConnectionClick); // Admin panel document.getElementById('adminBtn').addEventListener('click', openAdmin); document.getElementById('adminCloseBtn').addEventListener('click', closeAdmin); document.querySelectorAll('.admin-tab').forEach(tab => { tab.addEventListener('click', () => switchAdminTab(tab.dataset.tab)); }); document.getElementById('adminSaveSettings').addEventListener('click', saveAdminSettings); document.getElementById('adminModal').addEventListener('click', function(e) { if (e.target === this) closeAdmin(); }); // Global keyboard shortcuts document.addEventListener('keydown', function(e) { if (e.ctrlKey && e.key === ',') { e.preventDefault(); openSettings(); } if (e.ctrlKey && e.key === 'b') { e.preventDefault(); toggleSidebar(); } if (e.key === 'Escape') { if (State.isGenerating) { stopGeneration(); } else if (document.getElementById('settingsModal').classList.contains('active')) { closeSettings(); } } }); } function handleKeyDown(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } } function handleSaveSettings() { State.settings.apiEndpoint = document.getElementById('apiEndpoint').value.trim(); State.settings.apiKey = document.getElementById('apiKey').value.trim(); const modelCustom = document.getElementById('modelCustom').value.trim(); const modelSelect = document.getElementById('model').value; State.settings.model = modelCustom || modelSelect || 'gpt-3.5-turbo'; State.settings.stream = document.getElementById('streamResponse').checked; State.settings.saveHistory = document.getElementById('saveHistory').checked; State.settings.showThinking = document.getElementById('showThinking').checked; State.settings.systemPrompt = document.getElementById('systemPrompt').value.trim(); State.settings.maxTokens = parseInt(document.getElementById('maxTokens').value) || 4096; State.settings.temperature = parseFloat(document.getElementById('temperature').value) || 0.7; State.settings.topP = parseFloat(document.getElementById('topP').value) || 1; State.settings.presencePenalty = parseFloat(document.getElementById('presencePenalty').value) || 0; saveSettings(); saveProfile(); // async, fire-and-forget updateQuickModelSelector(); closeSettings(); showToast('✅ Settings saved', 'success'); } function newChat() { State.currentChatId = null; document.getElementById('chatMessages').innerHTML = `

Start a Conversation

Type a message below to begin

`; document.getElementById('regenerateBtn').style.display = 'none'; renderChatHistory(); document.getElementById('messageInput').focus(); } async function loadChat(chatId) { const chat = getChat(chatId); if (!chat) return; State.currentChatId = chatId; // In managed mode, fetch messages if not cached if (Backend.isManaged && chat.messages.length === 0 && chat.messageCount > 0) { await loadMessages(chatId); } if (chat.model) { State.settings.model = chat.model; updateQuickModelSelector(); } renderMessages(chat.messages); renderChatHistory(); updateRegenerateButton(chat.messages); } async function handleDeleteChat(chatId, event) { event.stopPropagation(); if (confirm('Delete this chat?')) { await deleteChat(chatId); if (State.currentChatId === chatId) { newChat(); } renderChatHistory(); showToast('🗑️ Chat deleted', 'success'); } } async function sendMessage() { const input = document.getElementById('messageInput'); const message = input.value.trim(); if (!message || State.isGenerating) return; // In managed mode, backend has the keys. In unmanaged, user must configure. if (!Backend.isManaged) { if (!State.settings.apiEndpoint || !State.settings.apiKey) { showToast('⚠️ Configure API settings first', 'warning'); openSettings(); return; } } input.value = ''; input.style.height = 'auto'; let messages = []; if (State.settings.systemPrompt) { messages.push({ role: 'system', content: State.settings.systemPrompt }); } if (State.currentChatId) { const chat = getChat(State.currentChatId); if (chat) messages = [...chat.messages]; } messages.push({ role: 'user', content: message, timestamp: new Date().toISOString() }); if (!State.currentChatId) { const title = message.substring(0, 50) + (message.length > 50 ? '...' : ''); const chat = await createChat(title, messages); if (!chat) return; State.currentChatId = chat.id; } else { await updateChat(State.currentChatId, { messages }); // In unmanaged mode, persist user message to backend (if connected) // In managed mode, the completion proxy persists both messages if (!Backend.isManaged) { await persistMessage(State.currentChatId, 'user', message); } } renderMessages(messages); renderChatHistory(); await sendApiRequest(messages); } async function regenerateResponse() { if (State.isGenerating || !State.currentChatId) return; const chat = getChat(State.currentChatId); if (!chat || chat.messages.length === 0) return; // Remove last assistant message(s) while (chat.messages.length > 0 && chat.messages[chat.messages.length - 1].role === 'assistant') { chat.messages.pop(); } if (chat.messages.length === 0) return; await updateChat(State.currentChatId, { messages: chat.messages }); renderMessages(chat.messages); await sendApiRequest(chat.messages); } // ── Auth Flow ─────────────────────────────── let _authListenersInit = false; function initAuthListeners() { if (_authListenersInit) return; _authListenersInit = true; document.getElementById('authLoginBtn').addEventListener('click', handleLogin); document.getElementById('authRegisterBtn').addEventListener('click', handleRegister); document.getElementById('authTabLogin').addEventListener('click', () => switchAuthTab('login')); document.getElementById('authTabRegister').addEventListener('click', () => switchAuthTab('register')); document.getElementById('authSkipBtn').addEventListener('click', skipAuth); // Enter key in auth forms document.getElementById('authPassword').addEventListener('keydown', function(e) { if (e.key === 'Enter') handleLogin(); }); document.getElementById('authRegPassword').addEventListener('keydown', function(e) { if (e.key === 'Enter') handleRegister(); }); } function switchAuthTab(tab) { document.getElementById('authTabLogin').classList.toggle('active', tab === 'login'); document.getElementById('authTabRegister').classList.toggle('active', tab === 'register'); document.getElementById('authLoginForm').style.display = tab === 'login' ? 'block' : 'none'; document.getElementById('authRegisterForm').style.display = tab === 'register' ? 'block' : 'none'; document.getElementById('authLoginBtn').style.display = tab === 'login' ? 'block' : 'none'; document.getElementById('authRegisterBtn').style.display = tab === 'register' ? 'block' : 'none'; document.getElementById('authError').textContent = ''; } async function handleLogin() { const login = document.getElementById('authLogin').value.trim(); const password = document.getElementById('authPassword').value; if (!login || !password) { document.getElementById('authError').textContent = 'Please fill in all fields'; return; } setAuthLoading(true); try { await Backend.login(login, password); await authSuccess(); } catch (e) { document.getElementById('authError').textContent = e.message; } finally { setAuthLoading(false); } } async function handleRegister() { const username = document.getElementById('authUsername').value.trim(); const email = document.getElementById('authEmail').value.trim(); const password = document.getElementById('authRegPassword').value; if (!username || !email || !password) { document.getElementById('authError').textContent = 'Please fill in all fields'; return; } if (password.length < 8) { document.getElementById('authError').textContent = 'Password must be at least 8 characters'; return; } setAuthLoading(true); try { await Backend.register(username, email, password); await authSuccess(); } catch (e) { document.getElementById('authError').textContent = e.message; } finally { setAuthLoading(false); } } function setAuthLoading(loading) { const btns = document.querySelectorAll('#authLoginBtn, #authRegisterBtn'); btns.forEach(btn => { btn.disabled = loading; if (loading) btn.dataset.origText = btn.textContent; btn.textContent = loading ? '⏳ Please wait...' : (btn.dataset.origText || btn.textContent); }); } async function skipAuth() { Backend.clear(); hideSplashGate(); await initApp(); showToast('📴 Running in offline mode', 'success'); } // ── Connection Status ─────────────────────── function updateConnectionStatus() { const el = document.getElementById('connectionStatus'); if (Backend.isManaged) { el.className = 'connection-status managed'; el.innerHTML = ` ${Backend.user?.display_name || Backend.user?.username || 'Connected'}`; el.title = 'Managed mode – click to sign out'; } else if (Backend.baseUrl) { el.className = 'connection-status available'; el.innerHTML = ' Sign in'; el.title = 'Backend available – click to sign in'; } else { el.className = 'connection-status offline'; el.innerHTML = ' Offline'; el.title = 'Unmanaged mode – data stored locally'; } } async function handleConnectionClick() { if (Backend.isManaged) { if (confirm('Sign out? Your chats are saved on the server.')) { await Backend.logout(); State.chats = []; State.currentChatId = null; showSplashGate(null); initAuthListeners(); showToast('👋 Signed out', 'success'); } } else if (Backend.baseUrl) { showSplashGate(null); initAuthListeners(); } } // ── Post-LLM Persistence ──────────────────── // Called by api.js after assistant response is complete. async function onAssistantResponse(content, model) { if (Backend.isManaged && State.currentChatId) { await persistMessage(State.currentChatId, 'assistant', content, model); } } // Initialize on DOM ready document.addEventListener('DOMContentLoaded', init);