Mode switching (#10) (#26)

This commit is contained in:
2026-02-16 11:30:46 +00:00
parent 4e5b873087
commit b7731bd83f
8 changed files with 771 additions and 83 deletions

View File

@@ -2,17 +2,44 @@
// Chat Switchboard - Main Application
// ==========================================
function init() {
async function init() {
console.log('🔀 Chat Switchboard initializing...');
loadSettings();
loadChats();
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();
console.log('✅ Chat Switchboard ready');
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() {
@@ -23,7 +50,7 @@ function initializeEventListeners() {
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);
@@ -31,7 +58,7 @@ function initializeEventListeners() {
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) {
@@ -39,17 +66,17 @@ function initializeEventListeners() {
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);
@@ -57,7 +84,7 @@ function initializeEventListeners() {
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 = '';
@@ -65,12 +92,31 @@ function initializeEventListeners() {
document.getElementById('modelCustom').addEventListener('input', function() {
if (this.value) document.getElementById('model').value = '';
});
// Close modal on overlay click
// 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 === ',') {
@@ -86,6 +132,20 @@ function initializeEventListeners() {
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();
}
}
});
@@ -101,11 +161,11 @@ function handleKeyDown(e) {
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;
@@ -114,7 +174,7 @@ function handleSaveSettings() {
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();
@@ -139,24 +199,30 @@ function newChat() {
document.getElementById('messageInput').focus();
}
function loadChat(chatId) {
async function loadChat(chatId) {
const chat = getChat(chatId);
if (chat) {
State.currentChatId = chatId;
if (chat.model) {
State.settings.model = chat.model;
updateQuickModelSelector();
}
renderMessages(chat.messages);
renderChatHistory();
updateRegenerateButton(chat.messages);
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);
}
function handleDeleteChat(chatId, event) {
async function handleDeleteChat(chatId, event) {
event.stopPropagation();
if (confirm('Delete this chat?')) {
deleteChat(chatId);
await deleteChat(chatId);
if (State.currentChatId === chatId) {
newChat();
}
@@ -168,9 +234,9 @@ function handleDeleteChat(chatId, event) {
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();
@@ -179,56 +245,199 @@ async function sendMessage() {
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',
messages.push({
role: 'user',
content: message,
timestamp: new Date().toISOString()
});
if (!State.currentChatId) {
const title = message.substring(0, 50) + (message.length > 50 ? '...' : '');
const chat = createChat(title, messages);
const chat = await createChat(title, messages);
if (!chat) return;
State.currentChatId = chat.id;
} else {
updateChat(State.currentChatId, { messages });
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;
updateChat(State.currentChatId, { messages: chat.messages });
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 = `<span class="status-dot"></span> ${Backend.user?.username || 'Connected'}`;
el.title = 'Managed mode click to sign out';
} else if (Backend.baseUrl) {
el.className = 'connection-status available';
el.innerHTML = '<span class="status-dot"></span> Sign in';
el.title = 'Backend available click to sign in';
} else {
el.className = 'connection-status offline';
el.innerHTML = '<span class="status-dot"></span> 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);