|
|
@@ -0,0 +1,192 @@
|
|
|
+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 {
|
|
|
+ // Fetch response from backend
|
|
|
+ const response = await fetch('/chat', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ messages: chatHistory })
|
|
|
+ });
|
|
|
+
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Initialize state
|
|
|
+ sendBtn.disabled = true;
|
|
|
+});
|