/** * StackLogix Chatbot — Frontend Logic with Streaming SSE */ // --- Config --- const API_BASE = window.location.origin; let sessionId = generateSessionId(); let isStreaming = false; // --- DOM Elements --- const messagesContainer = document.getElementById('messagesContainer'); const messagesList = document.getElementById('messagesList'); const welcomeScreen = document.getElementById('welcomeScreen'); const userInput = document.getElementById('userInput'); const sendBtn = document.getElementById('sendBtn'); const newChatBtn = document.getElementById('newChatBtn'); const ingestBtn = document.getElementById('ingestBtn'); const statusDot = document.getElementById('statusDot'); const docsCount = document.getElementById('docsCount'); const sessionDisplay = document.getElementById('sessionDisplay'); // --- Init --- document.addEventListener('DOMContentLoaded', () => { sessionDisplay.textContent = sessionId.slice(0, 12) + '...'; checkHealth(); fetchCollectionInfo(); setupEventListeners(); }); // --- Event Listeners --- function setupEventListeners() { sendBtn.addEventListener('click', sendMessage); userInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); userInput.addEventListener('input', () => { userInput.style.height = 'auto'; userInput.style.height = Math.min(userInput.scrollHeight, 120) + 'px'; sendBtn.disabled = !userInput.value.trim() || isStreaming; }); newChatBtn.addEventListener('click', startNewChat); ingestBtn.addEventListener('click', triggerIngest); document.querySelectorAll('.prompt-chip, .suggestion-card').forEach(btn => { btn.addEventListener('click', () => { const prompt = btn.getAttribute('data-prompt'); if (prompt && !isStreaming) { userInput.value = prompt; sendBtn.disabled = false; sendMessage(); } }); }); } // --- Core: Streaming Chat --- async function sendMessage() { const message = userInput.value.trim(); if (!message || isStreaming) return; // Hide welcome screen welcomeScreen.classList.add('hidden'); // Clear input userInput.value = ''; userInput.style.height = 'auto'; sendBtn.disabled = true; isStreaming = true; // Add user message appendUserMessage(message); // Show typing indicator const typingEl = showTypingIndicator(); try { const response = await fetch(`${API_BASE}/chat/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId, user_message: message, }), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); // Remove typing indicator, create assistant message bubble typingEl.remove(); const { bubbleEl, contentEl, messageDiv } = createAssistantMessage(); const reader = response.body.getReader(); const decoder = new TextDecoder(); let fullText = ''; let sources = null; let followUp = null; let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // Process complete SSE lines const lines = buffer.split('\n'); buffer = lines.pop() || ''; // Keep incomplete line in buffer for (const line of lines) { if (!line.startsWith('data: ')) continue; try { const event = JSON.parse(line.slice(6)); if (event.type === 'token') { fullText += event.content; contentEl.innerHTML = formatContent(fullText); scrollToBottom(); } else if (event.type === 'sources') { sources = event.sources; } else if (event.type === 'follow_up') { followUp = event.content; } else if (event.type === 'error') { fullText = event.content; contentEl.innerHTML = formatContent(fullText); } else if (event.type === 'done') { // Append sources below the bubble if (sources && sources.length > 0) { const sourcesEl = createSourcesElement(sources); messageDiv.querySelector('.message-content').appendChild(sourcesEl); } // Append follow-up question if (followUp) { const followUpEl = createFollowUpElement(followUp); messageDiv.querySelector('.message-content').appendChild(followUpEl); } // Add timestamp const timeEl = document.createElement('div'); timeEl.classList.add('message-time'); timeEl.textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); messageDiv.querySelector('.message-content').appendChild(timeEl); scrollToBottom(); } } catch (e) { // Skip malformed JSON } } } } catch (error) { console.error('Chat error:', error); typingEl?.remove(); appendSimpleAssistantMessage('Sorry, something went wrong. Please try again.'); showToast('Failed to get response. Check if the server is running.', 'error'); } finally { isStreaming = false; sendBtn.disabled = !userInput.value.trim(); } } // --- Message Rendering --- function appendUserMessage(content) { const messageDiv = document.createElement('div'); messageDiv.classList.add('message', 'user'); const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); messageDiv.innerHTML = `
`; messagesList.appendChild(messageDiv); scrollToBottom(); } function createAssistantMessage() { const messageDiv = document.createElement('div'); messageDiv.classList.add('message', 'assistant'); const bubbleEl = document.createElement('div'); bubbleEl.classList.add('message-bubble'); const contentEl = document.createElement('div'); contentEl.classList.add('streaming-content'); contentEl.innerHTML = ''; bubbleEl.appendChild(contentEl); messageDiv.innerHTML = ` `; messageDiv.querySelector('.message-content').appendChild(bubbleEl); messagesList.appendChild(messageDiv); scrollToBottom(); return { bubbleEl, contentEl, messageDiv }; } function appendSimpleAssistantMessage(content) { const messageDiv = document.createElement('div'); messageDiv.classList.add('message', 'assistant'); const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); messageDiv.innerHTML = ` `; messagesList.appendChild(messageDiv); scrollToBottom(); } function createSourcesElement(sources) { const el = document.createElement('div'); el.classList.add('sources-container'); let html = '');
html = html.replace(/\n/g, '
');
if (!html.startsWith('<')) {
html = '
' + html + '
'; } return html; } function showTypingIndicator() { const typing = document.createElement('div'); typing.classList.add('typing-indicator'); typing.innerHTML = `