// Minimal client that sends CSRF, renders assistant HTML, and toggles theme. const qs = (s, r = document) => r.querySelector(s); const chatEl = qs('#chat'); const formEl = qs('#chatForm'); const msgInput = qs('#messageInput'); const sysInput = qs('#systemInput'); const resetBtn = qs('#resetBtn'); const themeBtn = qs('#themeToggle'); function post(action, data = {}) { const body = new URLSearchParams({ action, csrf: (window.__APP__ && window.__APP__.csrf) || '', ...data }); return fetch(location.pathname, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, body }).then(r => r.json()); } // Simple helpers to append messages to the chat function appendUserMessage(text) { const wrap = document.createElement('div'); wrap.className = 'msg user'; wrap.innerHTML = `
${escapeHtmlWithNewlines(text)}
${nowTime()}
`; chatEl.appendChild(wrap); chatEl.scrollTop = chatEl.scrollHeight; } function appendAssistantHtml(html) { const clean = (window.DOMPurify ? DOMPurify.sanitize(html) : html); const wrap = document.createElement('div'); wrap.className = 'msg assistant'; wrap.innerHTML = `
${clean}
${nowTime()}
`; chatEl.appendChild(wrap); chatEl.scrollTop = chatEl.scrollHeight; } function escapeHtmlWithNewlines(s) { const t = document.createElement('div'); t.innerText = s; return t.innerHTML.replace(/\n/g, '
'); } function nowTime() { const d = new Date(); const hh = String(d.getHours()).padStart(2,'0'); const mm = String(d.getMinutes()).padStart(2,'0'); return `${hh}:${mm}`; } // Submit chat formEl?.addEventListener('submit', async (e) => { e.preventDefault(); const message = msgInput.value.trim(); const system = sysInput ? sysInput.value.trim() : ''; if (!message) return; appendUserMessage(message); msgInput.value = ''; msgInput.focus(); const res = await post('chat', { message, system }); if (res.ok) { // Prefer server-rendered sanitized HTML for assistant appendAssistantHtml(res.reply_html || escapeHtmlWithNewlines(res.reply || '')); } else { appendAssistantHtml(`

Error: ${res.error || 'Unknown'}

`); } }); // Reset chat resetBtn?.addEventListener('click', async () => { const res = await post('reset', {}); if (res.ok) { chatEl.innerHTML = `
Start the conversation below…
`; } }); // Theme toggle themeBtn?.addEventListener('click', async () => { const current = (window.__APP__ && window.__APP__.theme) || 'dark'; const next = current === 'dark' ? 'light' : 'dark'; const res = await post('set_theme', { theme: next }); if (res.ok) { document.documentElement.setAttribute('data-theme', next); window.__APP__.theme = next; } }); // With "enter" key press, send message msgInput.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); // stop newline formEl.dispatchEvent(new Event('submit', {cancelable: true})); } });