document.addEventListener('DOMContentLoaded', () => { const chatForm = document.getElementById('chat-form'); const userInput = document.getElementById('user-input'); const chatContainer = document.getElementById('chat-container'); const sendBtn = document.getElementById('send-btn'); const clearChatBtn = document.getElementById('clear-chat'); let chatHistory = []; // Store conversation history // Auto-resize textarea userInput.addEventListener('input', function() { this.style.height = 'auto'; this.style.height = (this.scrollHeight > 150 ? 150 : this.scrollHeight) + 'px'; if (this.value.trim() === '') { sendBtn.disabled = true; } else { sendBtn.disabled = false; } }); // Handle Enter key (Shift+Enter for new line) userInput.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (!sendBtn.disabled) { chatForm.dispatchEvent(new Event('submit')); } } }); clearChatBtn.addEventListener('click', () => { if (confirm('Are you sure you want to clear the chat history?')) { chatHistory = []; chatContainer.innerHTML = ''; addMessage('system', 'Hello! I am LocalFoodAI, your completely local nutrition and menu assistant. How can I help you today?'); } }); chatForm.addEventListener('submit', async (e) => { e.preventDefault(); const message = userInput.value.trim(); if (!message) return; // Reset input userInput.value = ''; userInput.style.height = 'auto'; sendBtn.disabled = true; // Add user message to UI addMessage('user', message); chatHistory.push({ role: 'user', content: message }); // Add loading indicator const loadingId = addTypingIndicator(); try { // Fetch response from backend const response = await fetch('/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: chatHistory }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } // Remove loading indicator removeElement(loadingId); // Create new bot message container const botMessageId = 'msg-' + Date.now(); const botContentEl = addMessage('system', '', botMessageId); let botFullResponse = ''; // Handle Server-Sent Events (Streaming) const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let done = false; while (!done) { const { value, done: readerDone } = await reader.read(); done = readerDone; if (value) { const chunk = decoder.decode(value, { stream: true }); // Split the chunk by double newline to get individual SSE messages const lines = chunk.split('\n\n'); for (const line of lines) { if (line.startsWith('data: ')) { const dataStr = line.substring(6); if (dataStr.trim() === '') continue; try { const data = JSON.parse(dataStr); if (data.error) { botContentEl.innerHTML += `
Error: ${data.error}`; } else if (data.content !== undefined) { botFullResponse += data.content; // Basic text to HTML conversion botContentEl.innerHTML = formatText(botFullResponse); chatContainer.scrollTop = chatContainer.scrollHeight; } } catch (err) { console.error('Error parsing SSE data:', err, dataStr); } } } } } // Save bot response to history once complete chatHistory.push({ role: 'assistant', content: botFullResponse }); } catch (error) { console.error('Chat error:', error); removeElement(loadingId); addMessage('system', 'Sorry, there was an error communicating with the local LLM. Make sure the server and Ollama are running.'); } finally { sendBtn.disabled = false; userInput.focus(); } }); function addMessage(role, content, id = null) { const msgDiv = document.createElement('div'); msgDiv.className = `message ${role}`; if (id) msgDiv.id = id; const avatarDiv = document.createElement('div'); avatarDiv.className = 'avatar'; avatarDiv.textContent = role === 'user' ? '👤' : '🤖'; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content'; contentDiv.innerHTML = formatText(content); msgDiv.appendChild(avatarDiv); msgDiv.appendChild(contentDiv); chatContainer.appendChild(msgDiv); chatContainer.scrollTop = chatContainer.scrollHeight; return contentDiv; } function addTypingIndicator() { const id = 'typing-' + Date.now(); const msgDiv = document.createElement('div'); msgDiv.className = 'message system'; msgDiv.id = id; const avatarDiv = document.createElement('div'); avatarDiv.className = 'avatar'; avatarDiv.textContent = '🤖'; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content typing-indicator'; contentDiv.innerHTML = `
`; msgDiv.appendChild(avatarDiv); msgDiv.appendChild(contentDiv); chatContainer.appendChild(msgDiv); chatContainer.scrollTop = chatContainer.scrollHeight; return id; } function removeElement(id) { const el = document.getElementById(id); if (el) el.remove(); } function formatText(text) { if (!text) return ''; // Very basic markdown parsing for bold, italics, code, and newlines let formatted = text .replace(/&/g, "&") .replace(//g, ">") .replace(/\n/g, "
") .replace(/\*\*(.*?)\*\*/g, "$1") // bold .replace(/\*(.*?)\*/g, "$1") // italic .replace(/`(.*?)`/g, "$1"); // inline code return formatted; } // Initialize state sendBtn.disabled = true; });