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 { const token = localStorage.getItem('localFoodToken'); // Fetch response from backend const response = await fetch('/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ messages: chatHistory }) }); if (response.status === 401) { setLoggedOutState(); addMessage('system', 'Your session has expired. Please log in again.'); return; } 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; } // Authentication & Session Logic const authScreen = document.getElementById('auth-screen'); const chatApp = document.getElementById('chat-app'); const loginForm = document.getElementById('login-form'); const registerForm = document.getElementById('register-form'); const showRegisterLink = document.getElementById('show-register'); const showLoginLink = document.getElementById('show-login'); const loginError = document.getElementById('login-error'); const regError = document.getElementById('reg-error'); const regSuccess = document.getElementById('reg-success'); const logoutBtn = document.getElementById('nav-logout-btn'); const userGreeting = document.getElementById('user-greeting'); // Check session on load const savedUser = localStorage.getItem('localFoodUser'); const savedToken = localStorage.getItem('localFoodToken'); if (savedUser && savedToken) { setLoggedInState(savedUser, savedToken); } function setLoggedInState(username, token) { localStorage.setItem('localFoodUser', username); localStorage.setItem('localFoodToken', token); userGreeting.textContent = `Welcome, ${username}`; authScreen.classList.add('fade-out'); setTimeout(() => { authScreen.style.display = 'none'; chatApp.style.display = 'flex'; chatApp.classList.add('fade-in'); userInput.focus(); }, 500); } async function setLoggedOutState() { const token = localStorage.getItem('localFoodToken'); if (token) { try { await fetch('/api/logout', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` } }); } catch (err) { console.error("Error during logout:", err); } } localStorage.removeItem('localFoodUser'); localStorage.removeItem('localFoodToken'); chatApp.style.display = 'none'; chatApp.classList.remove('fade-in'); authScreen.style.display = 'block'; setTimeout(() => { authScreen.classList.remove('fade-out'); }, 50); loginForm.reset(); registerForm.reset(); loginError.textContent = ''; regError.textContent = ''; } logoutBtn.addEventListener('click', () => { setLoggedOutState(); }); // Toggles showRegisterLink.addEventListener('click', (e) => { e.preventDefault(); loginForm.style.display = 'none'; registerForm.style.display = 'block'; }); showLoginLink.addEventListener('click', (e) => { e.preventDefault(); registerForm.style.display = 'none'; loginForm.style.display = 'block'; }); // Login Submission loginForm.addEventListener('submit', async (e) => { e.preventDefault(); loginError.textContent = ''; const submitBtn = document.getElementById('login-submit-btn'); submitBtn.disabled = true; const username = document.getElementById('login-username').value; const password = document.getElementById('login-password').value; try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await response.json(); if (!response.ok) { loginError.textContent = data.detail || 'Login failed.'; } else { setLoggedInState(data.username, data.token); } } catch (err) { loginError.textContent = 'Server error. Is the backend running?'; } finally { submitBtn.disabled = false; } }); // Registration Submission registerForm.addEventListener('submit', async (e) => { e.preventDefault(); regError.textContent = ''; regSuccess.textContent = ''; const submitBtn = document.getElementById('reg-submit-btn'); submitBtn.disabled = true; const username = document.getElementById('reg-username').value; const password = document.getElementById('reg-password').value; const confirmInfo = document.getElementById('reg-confirm').value; if (password !== confirmInfo) { regError.textContent = "Passwords do not match."; submitBtn.disabled = false; return; } try { const response = await fetch('/api/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await response.json(); if (!response.ok) { regError.textContent = data.detail || 'Registration failed.'; } else { regSuccess.textContent = 'Account created! Logging in...'; setTimeout(() => { setLoggedInState(data.username, data.token); }, 1000); } } catch (err) { regError.textContent = 'Server error. Please try again later.'; } finally { submitBtn.disabled = false; } }); // Initialize state sendBtn.disabled = true; });