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 = `
$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;
});