189 lines
5.7 KiB
JavaScript
189 lines
5.7 KiB
JavaScript
// 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 -> <br>
|
|
return $('<div>').text(s).html().replace(/\n/g, '<br>');
|
|
}
|
|
|
|
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 = `
|
|
<div class="msg user">
|
|
<div class="content">${escapeHtmlWithNewlines(text)}</div>
|
|
<div class="time">${nowTime()}</div>
|
|
</div>`;
|
|
$chat.append(html);
|
|
$chat.prop('scrollTop', $chat[0].scrollHeight);
|
|
}
|
|
|
|
function appendAssistantHtml(html) {
|
|
const clean = (window.DOMPurify ? DOMPurify.sanitize(html) : html);
|
|
const block = `
|
|
<div class="msg assistant">
|
|
<div class="content">${clean}</div>
|
|
<div class="time">${nowTime()}</div>
|
|
</div>`;
|
|
$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(`<p class="text-danger">Error: ${res.error || 'Unknown'}</p>`);
|
|
}
|
|
} catch (err) {
|
|
appendAssistantHtml(`<p class="text-danger">Network error</p>`);
|
|
}
|
|
});
|
|
|
|
// Reset chat
|
|
$reset.on('click', async function () {
|
|
try {
|
|
const res = await post('reset', {});
|
|
if (res.ok) {
|
|
$chat.html('<div class="text-secondary">Start the conversation below…</div>');
|
|
}
|
|
} 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');
|
|
}
|
|
})();
|