// ========================================== // Chat Switchboard - Main Application // ========================================== async function init() { console.log('🔀 Chat Switchboard initializing...'); loadSettings(); loadModels(); Backend.init(); // Auto-detect backend at same origin if (!Backend.baseUrl) { const health = await Backend.checkHealth(''); if (health && health.database) { Backend.baseUrl = window.location.origin; // same origin Backend.save(); console.log('🔗 Backend detected at same origin'); } } // If we have saved auth, validate it if (Backend.accessToken) { const health = await Backend.checkHealth(Backend.baseUrl); if (!health) { console.log('⚠️ Backend unreachable, falling back to unmanaged'); Backend.clear(); } } await loadChats(); initializeEventListeners(); updateQuickModelSelector(); renderChatHistory(); updateConnectionStatus(); // If backend available but not logged in, show auth if (Backend.baseUrl && !Backend.accessToken) { showAuthModal(); } console.log(`✅ Chat Switchboard ready (${Backend.isManaged ? 'managed' : 'unmanaged'} mode)`); } 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)); // 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(); }); document.getElementById('authModal').addEventListener('click', function(e) { if (e.target === this) { // Only closeable if user has option to skip (unmanaged available) if (!Backend.baseUrl || Backend.accessToken) { closeAuthModal(); } } }); // Auth modal 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('authCloseBtn').addEventListener('click', closeAuthModal); document.getElementById('authSkipBtn').addEventListener('click', skipAuth); // Connection status click document.getElementById('connectionStatus').addEventListener('click', handleConnectionClick); // 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(); } else if (document.getElementById('authModal').classList.contains('active')) { closeAuthModal(); } } }); // Enter key in auth form document.getElementById('authPassword').addEventListener('keydown', function(e) { if (e.key === 'Enter') { const activeTab = document.querySelector('.auth-tab.active'); if (activeTab && activeTab.id === 'authTabRegister') { // Focus email field first if registering } else { handleLogin(); } } }); } 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(); 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; 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 }); // Persist user message to backend 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 ─────────────────────────────── function showAuthModal() { document.getElementById('authModal').classList.add('active'); document.getElementById('authLogin').value = ''; document.getElementById('authPassword').value = ''; document.getElementById('authUsername').value = ''; document.getElementById('authEmail').value = ''; document.getElementById('authError').textContent = ''; switchAuthTab('login'); } function closeAuthModal() { document.getElementById('authModal').classList.remove('active'); } 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); closeAuthModal(); await loadChats(); renderChatHistory(); updateConnectionStatus(); showToast(`✅ Welcome back, ${Backend.user.username}`, 'success'); } 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); closeAuthModal(); await loadChats(); renderChatHistory(); updateConnectionStatus(); showToast(`✅ Welcome, ${Backend.user.username}!`, 'success'); } 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); }); } function skipAuth() { closeAuthModal(); Backend.clear(); updateConnectionStatus(); 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?.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; loadChats(); newChat(); renderChatHistory(); updateConnectionStatus(); showToast('👋 Signed out', 'success'); } } else if (Backend.baseUrl) { showAuthModal(); } } // ── 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);