| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358 |
- 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 += `<br><span style="color:#f85149">Error: ${data.error}</span>`;
- } 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 = `
- <div class="typing-dot"></div>
- <div class="typing-dot"></div>
- <div class="typing-dot"></div>
- `;
- 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(/>/g, ">")
- .replace(/\n/g, "<br>")
- .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>") // bold
- .replace(/\*(.*?)\*/g, "<em>$1</em>") // italic
- .replace(/`(.*?)`/g, "<code style='background:rgba(255,255,255,0.1);padding:2px 4px;border-radius:4px'>$1</code>"); // 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;
- });
|