/* CheddahBot Frontend JS */ // ── Session Management ── const SESSION_KEY = 'cheddahbot_session'; function getSession() { try { return JSON.parse(localStorage.getItem(SESSION_KEY) || '{}'); } catch { return {}; } } function saveSession(data) { const s = getSession(); Object.assign(s, data); localStorage.setItem(SESSION_KEY, JSON.stringify(s)); } function getActiveAgent() { return getSession().agent_name || document.getElementById('input-agent-name')?.value || 'default'; } // ── Agent Switching ── function switchAgent(name) { // Update UI document.querySelectorAll('.agent-btn').forEach(b => { b.classList.toggle('active', b.dataset.agent === name); }); document.getElementById('input-agent-name').value = name; document.getElementById('input-conv-id').value = ''; saveSession({ agent_name: name, conv_id: null }); // Clear chat and load new sidebar document.getElementById('chat-messages').innerHTML = ''; refreshSidebar(); } function setActiveAgent(name) { document.querySelectorAll('.agent-btn').forEach(b => { b.classList.toggle('active', b.dataset.agent === name); }); const agentInput = document.getElementById('input-agent-name'); if (agentInput) agentInput.value = name; } // ── Sidebar ── function refreshSidebar() { const agent = getActiveAgent(); htmx.ajax('GET', '/chat/conversations?agent_name=' + agent, { target: '#sidebar-conversations', swap: 'innerHTML' }); } // ── Conversation Loading ── function loadConversation(convId) { const agent = getActiveAgent(); document.getElementById('input-conv-id').value = convId; saveSession({ conv_id: convId }); htmx.ajax('GET', '/chat/load/' + convId + '?agent_name=' + agent, { target: '#chat-messages', swap: 'innerHTML' }).then(() => { scrollChat(); renderAllMarkdown(); }); } // ── Chat Input ── function handleKeydown(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); document.getElementById('chat-form').requestSubmit(); } } function autoResize(el) { el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 200) + 'px'; } function afterSend(event) { const input = document.getElementById('chat-input'); input.value = ''; input.style.height = 'auto'; // Clear file input and preview const fileInput = document.querySelector('input[type="file"]'); if (fileInput) fileInput.value = ''; const preview = document.getElementById('file-preview'); if (preview) { preview.style.display = 'none'; preview.innerHTML = ''; } scrollChat(); } function scrollChat() { const el = document.getElementById('chat-messages'); if (el) { requestAnimationFrame(() => { el.scrollTop = el.scrollHeight; }); } } // ── File Upload Preview ── function showFileNames(input) { const preview = document.getElementById('file-preview'); if (!input.files.length) { preview.style.display = 'none'; return; } let html = ''; for (const f of input.files) { html += '' + f.name + ''; } preview.innerHTML = html; preview.style.display = 'block'; } // Drag and drop document.addEventListener('DOMContentLoaded', () => { const chatMain = document.querySelector('.chat-main'); if (!chatMain) return; chatMain.addEventListener('dragover', e => { e.preventDefault(); chatMain.style.outline = '2px dashed var(--accent)'; }); chatMain.addEventListener('dragleave', () => { chatMain.style.outline = ''; }); chatMain.addEventListener('drop', e => { e.preventDefault(); chatMain.style.outline = ''; const fileInput = document.querySelector('input[type="file"]'); if (fileInput && e.dataTransfer.files.length) { fileInput.files = e.dataTransfer.files; showFileNames(fileInput); } }); }); // ── SSE Streaming ── // Handle SSE chunks for chat streaming let streamBuffer = ''; let activeSSE = null; document.addEventListener('htmx:sseBeforeMessage', function(e) { // This fires for each SSE event received by htmx }); // Watch for SSE trigger divs being added to the DOM const observer = new MutationObserver(mutations => { for (const m of mutations) { for (const node of m.addedNodes) { if (node.id === 'sse-trigger') { setupStream(node); } } } }); document.addEventListener('DOMContentLoaded', () => { const chatMessages = document.getElementById('chat-messages'); if (chatMessages) { observer.observe(chatMessages, { childList: true, subtree: true }); } }); function setupStream(triggerDiv) { const sseUrl = triggerDiv.getAttribute('sse-connect'); if (!sseUrl) return; // Remove the htmx SSE to manage manually triggerDiv.remove(); const responseDiv = document.getElementById('assistant-response'); if (!responseDiv) return; streamBuffer = ''; // Show typing indicator responseDiv.innerHTML = '
'; const source = new EventSource(sseUrl); activeSSE = source; source.addEventListener('chunk', function(e) { if (streamBuffer === '') { // Remove typing indicator on first chunk responseDiv.innerHTML = ''; } streamBuffer += e.data; // Render markdown try { responseDiv.innerHTML = marked.parse(streamBuffer); } catch { responseDiv.textContent = streamBuffer; } scrollChat(); }); source.addEventListener('done', function(e) { source.close(); activeSSE = null; // Final markdown render if (streamBuffer) { try { responseDiv.innerHTML = marked.parse(streamBuffer); } catch { responseDiv.textContent = streamBuffer; } } streamBuffer = ''; // Update conv_id from done event data const convId = e.data; if (convId) { document.getElementById('input-conv-id').value = convId; saveSession({ conv_id: convId }); } // Refresh sidebar refreshSidebar(); scrollChat(); }); source.onerror = function() { source.close(); activeSSE = null; if (!streamBuffer) { responseDiv.innerHTML = 'Connection lost'; } }; } // ── Markdown Rendering ── function renderAllMarkdown() { document.querySelectorAll('.message-content').forEach(el => { const raw = el.textContent; if (raw && typeof marked !== 'undefined') { try { el.innerHTML = marked.parse(raw); } catch { /* keep raw text */ } } }); } // ── Mobile Sidebar ── function toggleSidebar() { const sidebar = document.getElementById('chat-sidebar'); const overlay = document.getElementById('sidebar-overlay'); if (sidebar) { sidebar.classList.toggle('open'); } if (overlay) { overlay.classList.toggle('visible'); } } // ── Notification Banner (chat page) ── function setupChatNotifications() { const banner = document.getElementById('notification-banner'); if (!banner) return; const source = new EventSource('/sse/notifications'); source.addEventListener('notification', function(e) { const notif = JSON.parse(e.data); banner.textContent = notif.message; banner.style.display = 'block'; // Auto-hide after 15s setTimeout(() => { banner.style.display = 'none'; }, 15000); }); } document.addEventListener('DOMContentLoaded', setupChatNotifications); // ── HTMX Events ── document.addEventListener('scrollChat', scrollChat); document.addEventListener('htmx:afterSwap', function(e) { if (e.target.id === 'chat-messages') { renderAllMarkdown(); scrollChat(); } });