SignalMesh / editor /static /widget.js
KyklosIO's picture
Claude Opus 5
feat(editor): fold the live-site editor into the Studio as a mounted app
c05106c
Raw History Blame Contribute Delete
6.72 kB
/* Site helper widget — answers from the site's own published pages.
*
* Embed on the Joomla site with one line:
* <script src="https://YOUR-HOST/widget.js" defer></script>
*
* Read-only by construction: it calls one GET endpoint that can only return
* published, public content. It cannot change anything on the site.
*/
(function () {
'use strict';
if (window.__fixmeshWidget) return;
window.__fixmeshWidget = true;
var BASE = (document.currentScript && document.currentScript.src || '').replace(/\/widget\.js.*$/, '');
var OPEN = false;
var css = '\
.fxw-btn{position:fixed;right:20px;bottom:20px;z-index:2147483000;border:0;border-radius:999px;\
padding:14px 20px;font:600 15px/1 system-ui,sans-serif;background:#1f6feb;color:#fff;cursor:pointer;\
box-shadow:0 6px 22px rgba(0,0,0,.25)}\
.fxw-btn:hover{background:#1a5fd0}\
.fxw-panel{position:fixed;right:20px;bottom:84px;z-index:2147483000;width:min(380px,calc(100vw - 40px));\
max-height:min(560px,calc(100vh - 120px));background:#fff;color:#16181d;border-radius:14px;\
box-shadow:0 12px 44px rgba(0,0,0,.28);display:none;flex-direction:column;overflow:hidden;\
font:15px/1.5 system-ui,-apple-system,sans-serif}\
.fxw-panel.open{display:flex}\
.fxw-head{padding:12px 14px;background:#1f6feb;color:#fff;font-weight:700;display:flex;align-items:center}\
.fxw-head button{margin-left:auto;background:none;border:0;color:#fff;font-size:20px;cursor:pointer;line-height:1}\
.fxw-log{flex:1;overflow:auto;padding:12px;display:flex;flex-direction:column;gap:10px}\
.fxw-msg{padding:9px 12px;border-radius:12px;max-width:92%;white-space:pre-wrap}\
.fxw-me{align-self:flex-end;background:#1f6feb;color:#fff;border-bottom-right-radius:4px}\
.fxw-bot{align-self:flex-start;background:#f0f2f5;border-bottom-left-radius:4px}\
.fxw-src{margin-top:8px;display:flex;flex-direction:column;gap:6px}\
.fxw-src a{display:block;padding:8px 10px;background:#fff;border:1px solid #dfe3e8;border-radius:9px;\
text-decoration:none;color:#1f6feb;font-weight:600;font-size:14px}\
.fxw-src a:hover{border-color:#1f6feb}\
.fxw-src small{display:block;color:#5b6472;font-weight:400;margin-top:2px}\
.fxw-ask{display:flex;gap:6px;padding:10px;border-top:1px solid #e3e6ea;background:#fff}\
.fxw-ask input{flex:1;padding:10px;border:1px solid #dfe3e8;border-radius:9px;font:inherit;min-width:0}\
.fxw-ask button{padding:10px 14px;border:0;border-radius:9px;background:#1f6feb;color:#fff;font-weight:600;cursor:pointer}\
.fxw-chips{display:flex;flex-wrap:wrap;gap:6px;padding:0 12px 10px}\
.fxw-chips button{padding:6px 10px;border:1px solid #dfe3e8;background:#fff;border-radius:999px;\
font:inherit;font-size:13px;cursor:pointer;color:#16181d}\
@media(prefers-color-scheme:dark){\
.fxw-panel{background:#171a21;color:#e8eaed}.fxw-bot{background:#232833}\
.fxw-src a{background:#1d2129;border-color:#2d323c}.fxw-ask{background:#171a21;border-color:#272b34}\
.fxw-ask input{background:#1d2129;border-color:#2d323c;color:#e8eaed}\
.fxw-chips button{background:#1d2129;border-color:#2d323c;color:#e8eaed}}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var btn = document.createElement('button');
btn.className = 'fxw-btn';
btn.type = 'button';
btn.textContent = 'Ask a question';
btn.setAttribute('aria-expanded', 'false');
var panel = document.createElement('div');
panel.className = 'fxw-panel';
panel.setAttribute('role', 'dialog');
panel.setAttribute('aria-label', 'Site helper');
panel.innerHTML =
'<div class="fxw-head">Site helper<button type="button" aria-label="Close">&times;</button></div>' +
'<div class="fxw-log"></div>' +
'<div class="fxw-chips"></div>' +
'<form class="fxw-ask"><input placeholder="Ask about the school…" aria-label="Your question">' +
'<button type="submit">Ask</button></form>';
document.body.appendChild(btn);
document.body.appendChild(panel);
var log = panel.querySelector('.fxw-log');
var chips = panel.querySelector('.fxw-chips');
var form = panel.querySelector('.fxw-ask');
var input = form.querySelector('input');
function bubble(text, who) {
var d = document.createElement('div');
d.className = 'fxw-msg ' + (who === 'me' ? 'fxw-me' : 'fxw-bot');
d.textContent = text;
log.appendChild(d);
log.scrollTop = log.scrollHeight;
return d;
}
function sources(list, into) {
if (!list || !list.length) return;
var box = document.createElement('div');
box.className = 'fxw-src';
list.forEach(function (s) {
var a = document.createElement('a');
a.href = s.url;
a.textContent = 'Visit ' + s.title + ' now';
var sm = document.createElement('small');
sm.textContent = (s.quote || '').slice(0, 90);
a.appendChild(sm);
box.appendChild(a);
});
into.appendChild(box);
log.scrollTop = log.scrollHeight;
}
var SUGGESTIONS = ['Lunch menu', 'School calendar', 'Who is the superintendent',
'Contact the office', 'Enrollment'];
function drawChips() {
chips.innerHTML = '';
SUGGESTIONS.forEach(function (s) {
var b = document.createElement('button');
b.type = 'button';
b.textContent = s;
b.onclick = function () { input.value = s; send(); };
chips.appendChild(b);
});
}
function send() {
var q = input.value.trim();
if (!q) return;
bubble(q, 'me');
input.value = '';
var pending = bubble('Looking…', 'bot');
fetch(BASE + '/api/ask?q=' + encodeURIComponent(q))
.then(function (r) { return r.json(); })
.then(function (d) {
pending.textContent = (d.ok && d.lead ? d.lead + '\n\n' : '') + d.answer;
if (d.ok) sources(d.sources, pending);
})
.catch(function () {
pending.textContent = 'Sorry — I could not reach the site information just now.';
});
}
form.onsubmit = function (e) { e.preventDefault(); send(); };
function toggle(open) {
OPEN = open;
panel.classList.toggle('open', open);
btn.setAttribute('aria-expanded', String(open));
btn.textContent = open ? 'Close' : 'Ask a question';
if (open) {
if (!log.childElementCount) {
bubble('Hi! Ask me about lunch menus, calendars, staff or any school office. '
+ 'I answer using what is published on this site, and I can take you straight there.', 'bot');
drawChips();
}
input.focus();
}
}
btn.onclick = function () { toggle(!OPEN); };
panel.querySelector('.fxw-head button').onclick = function () { toggle(false); };
document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && OPEN) toggle(false); });
})();