script.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. document.addEventListener('DOMContentLoaded', () => {
  2. const chatForm = document.getElementById('chat-form');
  3. const userInput = document.getElementById('user-input');
  4. const chatContainer = document.getElementById('chat-container');
  5. const sendBtn = document.getElementById('send-btn');
  6. const clearChatBtn = document.getElementById('clear-chat');
  7. let chatHistory = []; // Store conversation history
  8. // Auto-resize textarea
  9. userInput.addEventListener('input', function() {
  10. this.style.height = 'auto';
  11. this.style.height = (this.scrollHeight > 150 ? 150 : this.scrollHeight) + 'px';
  12. if (this.value.trim() === '') {
  13. sendBtn.disabled = true;
  14. } else {
  15. sendBtn.disabled = false;
  16. }
  17. });
  18. // Handle Enter key (Shift+Enter for new line)
  19. userInput.addEventListener('keydown', function(e) {
  20. if (e.key === 'Enter' && !e.shiftKey) {
  21. e.preventDefault();
  22. if (!sendBtn.disabled) {
  23. chatForm.dispatchEvent(new Event('submit'));
  24. }
  25. }
  26. });
  27. clearChatBtn.addEventListener('click', () => {
  28. if (confirm('Are you sure you want to clear the chat history?')) {
  29. chatHistory = [];
  30. chatContainer.innerHTML = '';
  31. addMessage('system', 'Hello! I am LocalFoodAI, your completely local nutrition and menu assistant. How can I help you today?');
  32. }
  33. });
  34. chatForm.addEventListener('submit', async (e) => {
  35. e.preventDefault();
  36. const message = userInput.value.trim();
  37. if (!message) return;
  38. // Reset input
  39. userInput.value = '';
  40. userInput.style.height = 'auto';
  41. sendBtn.disabled = true;
  42. // Add user message to UI
  43. addMessage('user', message);
  44. chatHistory.push({ role: 'user', content: message });
  45. // Add loading indicator
  46. const loadingId = addTypingIndicator();
  47. try {
  48. // Fetch response from backend
  49. const response = await fetch('/chat', {
  50. method: 'POST',
  51. headers: { 'Content-Type': 'application/json' },
  52. body: JSON.stringify({ messages: chatHistory })
  53. });
  54. if (!response.ok) {
  55. throw new Error(`HTTP error! status: ${response.status}`);
  56. }
  57. // Remove loading indicator
  58. removeElement(loadingId);
  59. // Create new bot message container
  60. const botMessageId = 'msg-' + Date.now();
  61. const botContentEl = addMessage('system', '', botMessageId);
  62. let botFullResponse = '';
  63. // Handle Server-Sent Events (Streaming)
  64. const reader = response.body.getReader();
  65. const decoder = new TextDecoder('utf-8');
  66. let done = false;
  67. while (!done) {
  68. const { value, done: readerDone } = await reader.read();
  69. done = readerDone;
  70. if (value) {
  71. const chunk = decoder.decode(value, { stream: true });
  72. // Split the chunk by double newline to get individual SSE messages
  73. const lines = chunk.split('\n\n');
  74. for (const line of lines) {
  75. if (line.startsWith('data: ')) {
  76. const dataStr = line.substring(6);
  77. if (dataStr.trim() === '') continue;
  78. try {
  79. const data = JSON.parse(dataStr);
  80. if (data.error) {
  81. botContentEl.innerHTML += `<br><span style="color:#f85149">Error: ${data.error}</span>`;
  82. } else if (data.content !== undefined) {
  83. botFullResponse += data.content;
  84. // Basic text to HTML conversion
  85. botContentEl.innerHTML = formatText(botFullResponse);
  86. chatContainer.scrollTop = chatContainer.scrollHeight;
  87. }
  88. } catch (err) {
  89. console.error('Error parsing SSE data:', err, dataStr);
  90. }
  91. }
  92. }
  93. }
  94. }
  95. // Save bot response to history once complete
  96. chatHistory.push({ role: 'assistant', content: botFullResponse });
  97. } catch (error) {
  98. console.error('Chat error:', error);
  99. removeElement(loadingId);
  100. addMessage('system', 'Sorry, there was an error communicating with the local LLM. Make sure the server and Ollama are running.');
  101. } finally {
  102. sendBtn.disabled = false;
  103. userInput.focus();
  104. }
  105. });
  106. function addMessage(role, content, id = null) {
  107. const msgDiv = document.createElement('div');
  108. msgDiv.className = `message ${role}`;
  109. if (id) msgDiv.id = id;
  110. const avatarDiv = document.createElement('div');
  111. avatarDiv.className = 'avatar';
  112. avatarDiv.textContent = role === 'user' ? '👤' : '🤖';
  113. const contentDiv = document.createElement('div');
  114. contentDiv.className = 'message-content';
  115. contentDiv.innerHTML = formatText(content);
  116. msgDiv.appendChild(avatarDiv);
  117. msgDiv.appendChild(contentDiv);
  118. chatContainer.appendChild(msgDiv);
  119. chatContainer.scrollTop = chatContainer.scrollHeight;
  120. return contentDiv;
  121. }
  122. function addTypingIndicator() {
  123. const id = 'typing-' + Date.now();
  124. const msgDiv = document.createElement('div');
  125. msgDiv.className = 'message system';
  126. msgDiv.id = id;
  127. const avatarDiv = document.createElement('div');
  128. avatarDiv.className = 'avatar';
  129. avatarDiv.textContent = '🤖';
  130. const contentDiv = document.createElement('div');
  131. contentDiv.className = 'message-content typing-indicator';
  132. contentDiv.innerHTML = `
  133. <div class="typing-dot"></div>
  134. <div class="typing-dot"></div>
  135. <div class="typing-dot"></div>
  136. `;
  137. msgDiv.appendChild(avatarDiv);
  138. msgDiv.appendChild(contentDiv);
  139. chatContainer.appendChild(msgDiv);
  140. chatContainer.scrollTop = chatContainer.scrollHeight;
  141. return id;
  142. }
  143. function removeElement(id) {
  144. const el = document.getElementById(id);
  145. if (el) el.remove();
  146. }
  147. function formatText(text) {
  148. if (!text) return '';
  149. // Very basic markdown parsing for bold, italics, code, and newlines
  150. let formatted = text
  151. .replace(/&/g, "&amp;")
  152. .replace(/</g, "&lt;")
  153. .replace(/>/g, "&gt;")
  154. .replace(/\n/g, "<br>")
  155. .replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>") // bold
  156. .replace(/\*(.*?)\*/g, "<em>$1</em>") // italic
  157. .replace(/`(.*?)`/g, "<code style='background:rgba(255,255,255,0.1);padding:2px 4px;border-radius:4px'>$1</code>"); // inline code
  158. return formatted;
  159. }
  160. // Authentication & Session Logic
  161. const authScreen = document.getElementById('auth-screen');
  162. const chatApp = document.getElementById('chat-app');
  163. const loginForm = document.getElementById('login-form');
  164. const registerForm = document.getElementById('register-form');
  165. const showRegisterLink = document.getElementById('show-register');
  166. const showLoginLink = document.getElementById('show-login');
  167. const loginError = document.getElementById('login-error');
  168. const regError = document.getElementById('reg-error');
  169. const regSuccess = document.getElementById('reg-success');
  170. const logoutBtn = document.getElementById('nav-logout-btn');
  171. const userGreeting = document.getElementById('user-greeting');
  172. // Check session on load
  173. const savedUser = localStorage.getItem('localFoodUser');
  174. if (savedUser) {
  175. setLoggedInState(savedUser);
  176. }
  177. function setLoggedInState(username) {
  178. localStorage.setItem('localFoodUser', username);
  179. userGreeting.textContent = `Welcome, ${username}`;
  180. authScreen.classList.add('fade-out');
  181. setTimeout(() => {
  182. authScreen.style.display = 'none';
  183. chatApp.style.display = 'flex';
  184. chatApp.classList.add('fade-in');
  185. userInput.focus();
  186. }, 500);
  187. }
  188. function setLoggedOutState() {
  189. localStorage.removeItem('localFoodUser');
  190. chatApp.style.display = 'none';
  191. chatApp.classList.remove('fade-in');
  192. authScreen.style.display = 'block';
  193. setTimeout(() => {
  194. authScreen.classList.remove('fade-out');
  195. }, 50);
  196. loginForm.reset();
  197. registerForm.reset();
  198. loginError.textContent = '';
  199. regError.textContent = '';
  200. }
  201. logoutBtn.addEventListener('click', () => {
  202. setLoggedOutState();
  203. });
  204. // Toggles
  205. showRegisterLink.addEventListener('click', (e) => {
  206. e.preventDefault();
  207. loginForm.style.display = 'none';
  208. registerForm.style.display = 'block';
  209. });
  210. showLoginLink.addEventListener('click', (e) => {
  211. e.preventDefault();
  212. registerForm.style.display = 'none';
  213. loginForm.style.display = 'block';
  214. });
  215. // Login Submission
  216. loginForm.addEventListener('submit', async (e) => {
  217. e.preventDefault();
  218. loginError.textContent = '';
  219. const submitBtn = document.getElementById('login-submit-btn');
  220. submitBtn.disabled = true;
  221. const username = document.getElementById('login-username').value;
  222. const password = document.getElementById('login-password').value;
  223. try {
  224. const response = await fetch('/api/login', {
  225. method: 'POST',
  226. headers: { 'Content-Type': 'application/json' },
  227. body: JSON.stringify({ username, password })
  228. });
  229. const data = await response.json();
  230. if (!response.ok) {
  231. loginError.textContent = data.detail || 'Login failed.';
  232. } else {
  233. setLoggedInState(data.username);
  234. }
  235. } catch (err) {
  236. loginError.textContent = 'Server error. Is the backend running?';
  237. } finally {
  238. submitBtn.disabled = false;
  239. }
  240. });
  241. // Registration Submission
  242. registerForm.addEventListener('submit', async (e) => {
  243. e.preventDefault();
  244. regError.textContent = '';
  245. regSuccess.textContent = '';
  246. const submitBtn = document.getElementById('reg-submit-btn');
  247. submitBtn.disabled = true;
  248. const username = document.getElementById('reg-username').value;
  249. const password = document.getElementById('reg-password').value;
  250. const confirmInfo = document.getElementById('reg-confirm').value;
  251. if (password !== confirmInfo) {
  252. regError.textContent = "Passwords do not match.";
  253. submitBtn.disabled = false;
  254. return;
  255. }
  256. try {
  257. const response = await fetch('/api/register', {
  258. method: 'POST',
  259. headers: { 'Content-Type': 'application/json' },
  260. body: JSON.stringify({ username, password })
  261. });
  262. const data = await response.json();
  263. if (!response.ok) {
  264. regError.textContent = data.detail || 'Registration failed.';
  265. } else {
  266. regSuccess.textContent = 'Account created! Logging in...';
  267. setTimeout(() => {
  268. setLoggedInState(username);
  269. }, 1000);
  270. }
  271. } catch (err) {
  272. regError.textContent = 'Server error. Please try again later.';
  273. } finally {
  274. submitBtn.disabled = false;
  275. }
  276. });
  277. // Initialize state
  278. sendBtn.disabled = true;
  279. });