// ======================================== // State // ======================================== const conversationHistory = []; let isStreaming = false; // ======================================== // DOM Elements // ======================================== const chatForm = document.getElementById('chatForm'); const userInput = document.getElementById('userInput'); const sendBtn = document.getElementById('sendBtn'); const chatArea = document.getElementById('chatArea'); const messagesDiv = document.getElementById('messages'); const welcome = document.getElementById('welcome'); const clearBtn = document.getElementById('clearBtn'); const suggestions = document.querySelectorAll('.suggestion'); // ======================================== // Auto-resize textarea // ======================================== userInput.addEventListener('input', () => { userInput.style.height = 'auto'; userInput.style.height = Math.min(userInput.scrollHeight, 150) + 'px'; sendBtn.disabled = !userInput.value.trim() || isStreaming; }); // Enter to send (Shift+Enter for newline) userInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (userInput.value.trim() && !isStreaming) { chatForm.dispatchEvent(new Event('submit')); } } }); // ======================================== // Suggestion Buttons // ======================================== suggestions.forEach(btn => { btn.addEventListener('click', () => { userInput.value = btn.dataset.prompt; userInput.dispatchEvent(new Event('input')); chatForm.dispatchEvent(new Event('submit')); }); }); // ======================================== // Clear Chat // ======================================== clearBtn.addEventListener('click', () => { conversationHistory.length = 0; messagesDiv.innerHTML = ''; welcome.classList.remove('hidden'); userInput.value = ''; userInput.style.height = 'auto'; sendBtn.disabled = true; }); // ======================================== // Create message element // ======================================== function createMessageEl(role, content = '') { const msg = document.createElement('div'); msg.className = `message ${role}`; const avatar = document.createElement('div'); avatar.className = 'message-avatar'; avatar.textContent = role === 'user' ? 'Y' : 'AI'; const body = document.createElement('div'); body.className = 'message-body'; const roleLabel = document.createElement('div'); roleLabel.className = 'message-role'; roleLabel.textContent = role === 'user' ? 'You' : 'Assistant'; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content'; contentDiv.textContent = content; body.appendChild(roleLabel); body.appendChild(contentDiv); msg.appendChild(avatar); msg.appendChild(body); return { msg, contentDiv }; } // ======================================== // Scroll to bottom // ======================================== function scrollToBottom() { chatArea.scrollTo({ top: chatArea.scrollHeight, behavior: 'smooth' }); } // ======================================== // Format text with basic markdown // ======================================== function formatContent(text) { // Escape HTML let html = text .replace(/&/g, '&') .replace(//g, '>'); // Code blocks html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '
$2
'); // Inline code html = html.replace(/`([^`]+)`/g, '$1'); // Bold html = html.replace(/\*\*(.+?)\*\*/g, '$1'); // Italic html = html.replace(/\*(.+?)\*/g, '$1'); // Line breaks → paragraphs html = html .split(/\n\n+/) .map(p => `

${p.replace(/\n/g, '
')}

`) .join(''); return html; } // ======================================== // Send Message // ======================================== chatForm.addEventListener('submit', async (e) => { e.preventDefault(); const text = userInput.value.trim(); if (!text || isStreaming) return; // Hide welcome welcome.classList.add('hidden'); // Add user message const { msg: userMsg } = createMessageEl('user', text); messagesDiv.appendChild(userMsg); // Track in history conversationHistory.push({ role: 'user', content: text }); // Clear input userInput.value = ''; userInput.style.height = 'auto'; sendBtn.disabled = true; isStreaming = true; scrollToBottom(); // Create assistant message placeholder const { msg: assistantMsg, contentDiv } = createMessageEl('assistant'); contentDiv.innerHTML = `
`; messagesDiv.appendChild(assistantMsg); assistantMsg.classList.add('streaming'); scrollToBottom(); // Stream the response let fullResponse = ''; try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: conversationHistory }), }); if (!response.ok) { throw new Error(`Server error: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); fullResponse += chunk; contentDiv.innerHTML = formatContent(fullResponse); scrollToBottom(); } } catch (err) { fullResponse = `⚠️ Error: ${err.message}. Please check the server and try again.`; contentDiv.innerHTML = `

${fullResponse}

`; } // Finalize assistantMsg.classList.remove('streaming'); conversationHistory.push({ role: 'assistant', content: fullResponse }); isStreaming = false; sendBtn.disabled = !userInput.value.trim(); scrollToBottom(); });