feat: the assistant's chat slides open and shut, smaller on a computer, a screen of its own on a phone that «назад» closes; a sparkle button

This commit is contained in:
redtea committed 2026-10-15 22:19:23 +03:00
1 parent 94bf292494
commit d9c7c2dd42
3 files changed
+35 -11

No files matched your search

+20 -5
View File
@@ -15,10 +15,9 @@
fab.hidden = true;
fab.title = 'ИИ-ассистент';
fab.setAttribute('aria-label', 'ИИ-ассистент');
fab.innerHTML = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12z"/><path d="M8.5 11h.01M12 11h.01M15.5 11h.01"/></svg>';
fab.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M10 2.5c.3 0 .5.2.6.5l1 3.7a5 5 0 0 0 3.6 3.6l3.7 1c.6.2.6 1 0 1.2l-3.7 1a5 5 0 0 0-3.6 3.6l-1 3.7c-.2.6-1 .6-1.2 0l-1-3.7a5 5 0 0 0-3.6-3.6l-3.7-1c-.6-.2-.6-1 0-1.2l3.7-1a5 5 0 0 0 3.6-3.6l1-3.7c.1-.3.3-.5.6-.5z"/><path d="M18.5 1.5c.2 0 .3.1.4.3l.4 1.4c.2.6.7 1.1 1.3 1.3l1.4.4c.4.1.4.6 0 .8l-1.4.4c-.6.2-1.1.7-1.3 1.3l-.4 1.4c-.1.4-.7.4-.8 0l-.4-1.4a2 2 0 0 0-1.3-1.3l-1.4-.4c-.4-.2-.4-.7 0-.8l1.4-.4c.6-.2 1.1-.7 1.3-1.3l.4-1.4c.1-.2.2-.3.4-.3z" opacity=".75"/></svg>';
const box = document.createElement('div');
box.className = 'ai-box';
box.hidden = true;
box.innerHTML =
'<div class="ai-head"><b>Ассистент</b><span class="muted ai-where"></span><span class="spacer"></span>' +
'<button type="button" class="btn ghost small ai-clear" title="Новый разговор">Очистить</button>' +
@@ -32,11 +31,27 @@
const talk = []; // {role, content}
let ac = null;
function setOn(on) { fab.hidden = !on; if (!on) box.hidden = true; }
// Open and shut with a short slide. On a phone the chat takes the screen
// and is one step of history: «назад» shuts it.
const phone = () => matchMedia('(max-width: 640px)').matches;
const isOpen = () => box.classList.contains('on');
function show() {
box.classList.add('on');
if (phone() && !(history.state && history.state.aiChat)) history.pushState({aiChat: 1}, '', location.href);
where();
if (!phone()) input.focus();
}
function hide() {
if (!isOpen()) return;
if (history.state && history.state.aiChat) { history.back(); return; }
box.classList.remove('on');
}
window.addEventListener('popstate', () => { if (isOpen() && !(history.state && history.state.aiChat)) box.classList.remove('on'); });
function setOn(on) { fab.hidden = !on; if (!on) hide(); }
fetch('/api/panel/ai', {cache: 'no-store', credentials: 'same-origin'}).then(r => r.json()).then(d => setOn(!!d.on)).catch(() => {});
document.addEventListener('lpra:ai', ev => setOn(ev.detail));
fab.onclick = () => { box.hidden = !box.hidden; if (!box.hidden) { where(); input.focus(); } };
q('.ai-close').onclick = () => { box.hidden = true; };
fab.onclick = () => (isOpen() ? hide() : show());
q('.ai-close').onclick = hide;
q('.ai-clear').onclick = () => {
if (ac) ac.abort();
talk.length = 0;