// Minimal client that sends CSRF, renders assistant HTML, and toggles theme. (jQuery version) $(function () { // Element refs const $chat = $('#chat'); const $form = $('#chatForm'); const $msg = $('#messageInput'); const $sys = $('#systemInput'); const $reset = $('#resetBtn'); const $theme = $('#themeToggle'); // Helpers function escapeHtmlWithNewlines(s) { // jQuery-safe escape via .text(), then convert \n ->
return $('
').text(s).html().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}`; } function appendUserMessage(text) { const html = `
${escapeHtmlWithNewlines(text)}
${nowTime()}
`; $chat.append(html); $chat.prop('scrollTop', $chat[0].scrollHeight); } function appendAssistantHtml(html) { const clean = (window.DOMPurify ? DOMPurify.sanitize(html) : html); const block = `
${clean}
${nowTime()}
`; $chat.append(block); $chat.prop('scrollTop', $chat[0].scrollHeight); } // AJAX POST helper (x-www-form-urlencoded) function post(action, data = {}) { const csrf = (window.__APP__ && window.__APP__.csrf) || ''; const payload = new URLSearchParams({ action, csrf, ...data }).toString(); return $.ajax({ url: location.pathname, method: 'POST', data: payload, contentType: 'application/x-www-form-urlencoded; charset=UTF-8', dataType: 'json' }); } // Submit chat $form.on('submit', async function (e) { e.preventDefault(); const message = ($msg.val() || '').toString().trim(); const system = $sys.length ? ($sys.val() || '').toString().trim() : ''; if (!message) return; appendUserMessage(message); $msg.val('').focus(); try { const res = await post('chat', { message, system }); if (res.ok) { appendAssistantHtml(res.reply_html || escapeHtmlWithNewlines(res.reply || '')); } else { appendAssistantHtml(`

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

`); } } catch (err) { appendAssistantHtml(`

Network error

`); } }); // Reset chat $reset.on('click', async function () { try { const res = await post('reset', {}); if (res.ok) { $chat.html('
Start the conversation below…
'); } } catch (_) { /* ignore */ } }); // Theme toggle $theme.on('click', async function () { const current = (window.__APP__ && window.__APP__.theme) || 'dark'; const next = current === 'dark' ? 'light' : 'dark'; try { const res = await post('set_theme', { theme: next }); if (res.ok) { document.documentElement.setAttribute('data-theme', next); if (window.__APP__) window.__APP__.theme = next; } } catch (_) { /* ignore */ } }); // With "enter" key press, send message (Shift+Enter => newline) $msg.on('keydown', function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); $form.trigger('submit'); } }); }); // ========= Vanilla JS Page Switching (fixed) ========= (function () { const navLinks = document.querySelectorAll('.nav-tab .nav-link[data-page]'); const pages = document.querySelectorAll('#pageWrapper .page'); const DURATION = 200; // keep in sync with CSS function getActivePage() { return document.querySelector('#pageWrapper .page.active'); } function setActiveNav(targetId) { navLinks.forEach(a => { const isActive = a.getAttribute('data-page') === targetId; a.classList.toggle('active', isActive); a.setAttribute('aria-current', isActive ? 'page' : 'false'); }); } function showPage(targetId) { const current = getActivePage(); const next = document.getElementById(targetId); if (!next || current === next) return; // Prepare next: visible container, but still transparent next.classList.add('active'); // display:block; opacity:0 // Fade-in next on the next frame requestAnimationFrame(() => { next.classList.add('show'); // opacity -> 1 (transition) }); // Fade-out current (if any) if (current) { current.classList.remove('show'); // opacity -> 0 (transition) let cleaned = false; const cleanup = () => { if (cleaned) return; cleaned = true; current.classList.remove('active'); current.removeEventListener('transitionend', onEnd); }; const onEnd = (e) => { if (e.target === current && e.propertyName === 'opacity') { cleanup(); } }; current.addEventListener('transitionend', onEnd); // Fallback in case transitionend doesn't fire (reduced motion, etc.) setTimeout(cleanup, DURATION + 50); } setActiveNav(targetId); history.replaceState(null, '', '#' + targetId); } // Click handlers navLinks.forEach(link => { link.addEventListener('click', (ev) => { ev.preventDefault(); const targetId = link.getAttribute('data-page'); showPage(targetId); }); }); // Initial page from hash const initial = (location.hash || '#chat').slice(1); if (initial !== 'chat' && document.getElementById(initial)) { showPage(initial); } else { setActiveNav('chat'); } })();