feat: on a computer the assistant's ball and chat can be dragged, the chat opening beside the ball; on a phone a smaller ball further from the edges; the send button the field's height
This commit is contained in:
1 parent
ed77c8e0c1
commit
3044e650d3
2 files changed
+82
-3
No files matched your search
@@ -43,6 +43,7 @@
|
||||
const phone = () => matchMedia('(max-width: 640px)').matches;
|
||||
const isOpen = () => box.classList.contains('on');
|
||||
function show() {
|
||||
placeBox();
|
||||
box.classList.add('on');
|
||||
if (phone() && !(history.state && history.state.aiChat)) history.pushState({aiChat: 1}, '', location.href);
|
||||
where();
|
||||
@@ -57,7 +58,73 @@
|
||||
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 = () => (isOpen() ? hide() : show());
|
||||
fab.onclick = () => { if (moved) { moved = false; return; } isOpen() ? hide() : show(); };
|
||||
|
||||
// ---------- on a computer: the ball and the chat move ----------
|
||||
// The ball goes where it is dragged and stays there (kept in this
|
||||
// browser, as distances from the right and bottom edges); the chat opens
|
||||
// beside it, toward the room, and moves by its title.
|
||||
const desk = !matchMedia('(pointer: coarse)').matches;
|
||||
let moved = false;
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
function placeFab(right, bottom) {
|
||||
right = clamp(right, 8, innerWidth - fab.offsetWidth - 8);
|
||||
bottom = clamp(bottom, 8, innerHeight - fab.offsetHeight - 8);
|
||||
fab.style.right = right + 'px'; fab.style.bottom = bottom + 'px';
|
||||
return {right, bottom};
|
||||
}
|
||||
function placeBox() {
|
||||
if (!desk || phone()) { box.style.left = box.style.top = box.style.right = box.style.bottom = ''; return; }
|
||||
const f = fab.getBoundingClientRect(), w = box.offsetWidth, h = box.offsetHeight, gap = 12;
|
||||
const toLeft = f.left + f.width / 2 > innerWidth / 2, up = f.top + f.height / 2 > innerHeight / 2;
|
||||
let x = toLeft ? f.right - w : f.left;
|
||||
let y = up ? f.top - gap - h : f.bottom + gap;
|
||||
// No room above or below: beside the ball.
|
||||
if (y < 8 || y + h > innerHeight - 8) { y = clamp(f.top + f.height - h, 8, innerHeight - h - 8); x = toLeft ? f.left - gap - w : f.right + gap; }
|
||||
box.style.right = box.style.bottom = 'auto';
|
||||
box.style.left = clamp(x, 8, innerWidth - w - 8) + 'px';
|
||||
box.style.top = clamp(y, 8, innerHeight - h - 8) + 'px';
|
||||
box.style.transformOrigin = (up ? 'bottom ' : 'top ') + (toLeft ? 'right' : 'left');
|
||||
}
|
||||
if (desk) {
|
||||
try { const p = JSON.parse(localStorage.getItem('lpra.ai.pos') || 'null'); if (p) requestAnimationFrame(() => placeFab(p.right, p.bottom)); } catch (e) { /* default */ }
|
||||
const drag = (el, ev, onMove, onEnd) => {
|
||||
if (ev.button !== 0) return;
|
||||
const x0 = ev.clientX, y0 = ev.clientY;
|
||||
let on = false;
|
||||
const mv = e => {
|
||||
if (!on && Math.hypot(e.clientX - x0, e.clientY - y0) < 5) return;
|
||||
on = true;
|
||||
onMove(e.clientX - x0, e.clientY - y0);
|
||||
};
|
||||
const up = () => {
|
||||
removeEventListener('pointermove', mv); removeEventListener('pointerup', up);
|
||||
document.body.classList.remove('ai-dragging');
|
||||
if (on) onEnd();
|
||||
};
|
||||
addEventListener('pointermove', mv); addEventListener('pointerup', up);
|
||||
document.body.classList.add('ai-dragging');
|
||||
};
|
||||
fab.addEventListener('pointerdown', ev => {
|
||||
const r0 = innerWidth - fab.getBoundingClientRect().right, b0 = innerHeight - fab.getBoundingClientRect().bottom;
|
||||
let pos = {right: r0, bottom: b0};
|
||||
drag(fab, ev, (dx, dy) => { moved = true; pos = placeFab(r0 - dx, b0 - dy); if (isOpen()) placeBox(); }, () => {
|
||||
try { localStorage.setItem('lpra.ai.pos', JSON.stringify(pos)); } catch (e) { /* not kept */ }
|
||||
});
|
||||
});
|
||||
q('.ai-head').addEventListener('pointerdown', ev => {
|
||||
if (ev.target.closest('button')) return;
|
||||
const r = box.getBoundingClientRect();
|
||||
drag(box, ev, (dx, dy) => {
|
||||
box.style.left = clamp(r.left + dx, 8, innerWidth - r.width - 8) + 'px';
|
||||
box.style.top = clamp(r.top + dy, 8, innerHeight - r.height - 8) + 'px';
|
||||
}, () => {});
|
||||
});
|
||||
addEventListener('resize', () => {
|
||||
try { const p = JSON.parse(localStorage.getItem('lpra.ai.pos') || 'null'); if (p) placeFab(p.right, p.bottom); } catch (e) { /* default */ }
|
||||
if (isOpen()) placeBox();
|
||||
});
|
||||
}
|
||||
q('.ai-close').onclick = hide;
|
||||
q('.ai-clear').onclick = () => {
|
||||
if (ac) ac.abort();
|
||||
|
||||
Reference in new issue
Block a user