script.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  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. // Initialize state
  161. sendBtn.disabled = true;
  162. });