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);
}
});