document.addEventListener('DOMContentLoaded', () => { const chatWindow = document.getElementById('chat-window'); const inputArea = document.querySelector('textarea'); const sendBtn = document.querySelector('#send-btn'); const stopBtn = document.querySelector('#stop-btn'); // Auto-focus input on load if(inputArea) inputArea.focus(); // Determine current model from body class const isCRSM = document.body.classList.contains('crsm-theme'); const modelName = isCRSM ? 'crsm' : 'aetheris'; let controller = null; // --- Auto-resize Textarea --- if (inputArea) { inputArea.addEventListener('input', function() { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 200) + 'px'; }); inputArea.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); } if (sendBtn) sendBtn.addEventListener('click', sendMessage); if (stopBtn) { stopBtn.addEventListener('click', () => { if (controller) { controller.abort(); controller = null; endGenerationState(); appendSystemNote('Generation stopped by user.'); } }); } async function sendMessage() { const text = inputArea.value.trim(); if (!text) return; // Reset UI inputArea.value = ''; inputArea.style.height = 'auto'; // 1. Add User Message appendMessage('user', text); // 2. Prepare UI for Bot startGenerationState(); // 3. Add Placeholder Bubble const { row, bubble } = createMessageRow('bot'); chatWindow.appendChild(row); // Add "Thinking" State const thinkingIndicator = document.createElement('div'); thinkingIndicator.className = 'thinking-indicator'; thinkingIndicator.innerHTML = ``; bubble.appendChild(thinkingIndicator); scrollToBottom(); try { controller = new AbortController(); const response = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: modelName, prompt: text }), signal: controller.signal }); // Remove thinking indicator immediately on first byte thinkingIndicator.remove(); const reader = response.body.getReader(); const decoder = new TextDecoder(); let rawText = ''; while (true) { const { value, done } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); rawText += chunk; // Use Marked.js for parsing if available, else fallback to text bubble.innerHTML = window.marked ? window.marked.parse(rawText) : rawText; scrollToBottom(); } } catch (error) { thinkingIndicator.remove(); if (error.name !== 'AbortError') { console.error(error); bubble.innerText = "Error: Connection to the neural interface failed."; bubble.classList.add('error'); } } finally { controller = null; endGenerationState(); } } // --- Helpers --- function createMessageRow(role) { const row = document.createElement('div'); row.className = `message-row ${role} animate-slide-in`; const avatar = document.createElement('div'); avatar.className = `avatar ${role}`; if(role === 'user') { avatar.innerHTML = ``; } const bubble = document.createElement('div'); bubble.className = 'bubble prose'; // 'prose' for markdown styling row.appendChild(avatar); row.appendChild(bubble); return { row, bubble }; } function appendMessage(role, text) { const { row, bubble } = createMessageRow(role); bubble.innerText = text; // User text is plain text chatWindow.appendChild(row); scrollToBottom(); } function appendSystemNote(text) { const note = document.createElement('div'); note.className = 'system-note'; note.innerText = text; chatWindow.appendChild(note); scrollToBottom(); } function scrollToBottom() { chatWindow.scrollTop = chatWindow.scrollHeight; } function startGenerationState() { if(sendBtn) sendBtn.classList.add('hidden'); if(stopBtn) stopBtn.classList.remove('hidden'); inputArea.disabled = true; } function endGenerationState() { if(sendBtn) sendBtn.classList.remove('hidden'); if(stopBtn) stopBtn.classList.add('hidden'); inputArea.disabled = false; setTimeout(() => inputArea.focus(), 100); } });