// ── Chat UI controller ──────────────────────────────────────────────────────── const $ = (sel) => document.querySelector(sel); export class Chat { constructor() { this._container = $('#messages'); this._emptyState = $('#empty-state'); } clear() { this._container.innerHTML = ''; this._emptyState?.classList.remove('hidden'); } renderHistory(messages) { this._container.innerHTML = ''; this._emptyState?.classList.add('hidden'); messages.forEach((m) => this._appendBubble(m.role, m.content)); this._scrollBottom(); } async send(sessionId, message, contextIds = []) { // Immediately render user bubble this._appendBubble('user', message); this._emptyState?.classList.add('hidden'); // Typing indicator const typingId = this._appendTyping(); try { const res = await fetch(`/api/chat/sessions/${sessionId}/ask`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, context_ids: contextIds }), }); this._removeTyping(typingId); let data; try { data = await res.json(); } catch { this._appendBubble('assistant', 'Server error — could not parse response.', true); this._scrollBottom(); return; } if (!res.ok) { this._appendBubble('assistant', `Error: ${data.error || 'Unknown error'}`, true); } else { this._appendBubble('assistant', data.reply); } } catch (err) { this._removeTyping(typingId); this._appendBubble('assistant', `Network error: ${err.message}`, true); } this._scrollBottom(); } _appendBubble(role, content, isError = false) { const wrap = document.createElement('div'); wrap.className = `flex ${role === 'user' ? 'justify-end' : 'justify-start'} gap-3`; if (role === 'assistant') { wrap.innerHTML = `
${escapeHtml(content)}
`; } else { wrap.innerHTML = `
${escapeHtml(content)}
U
`; } this._container.appendChild(wrap); return wrap; } _appendTyping() { const id = `typing-${Date.now()}`; const div = document.createElement('div'); div.id = id; div.className = 'flex justify-start gap-3'; div.innerHTML = `
`; this._container.appendChild(div); this._scrollBottom(); return id; } _removeTyping(id) { document.getElementById(id)?.remove(); } _scrollBottom() { this._container.scrollTop = this._container.scrollHeight; } } function escapeHtml(str = '') { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); }