// 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 = `
`;
$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');
}
})();