Balla & Snella – Liquid Glass
Assenza o Recupero? Scrivi qui
Come usarlo:
1) Comunicami la tua assenza o la tua volontà di recupero, grazie all' Ask AI qui sopra.
2) Scrivi Nome, Cognome e Sede, poi dimmi quando non puoi venire o vuoi recuperare la lezione.
3) Analizzo la tua richiesta e ti rispondo immediatamente.
`;
body.appendChild(typingEl);
requestAnimationFrame(()=>{ body.scrollTop = body.scrollHeight; });
}
function hideTyping(){
if (typingEl && typingEl.parentNode){
typingEl.parentNode.removeChild(typingEl);
}
typingEl = null;
}
function scheduleStatusMessage(){
if (statusTimer) clearTimeout(statusTimer);
statusTimer = setTimeout(() => {
if (!awaitingReply) return;
// rimuovo eventuale bubble di typing vecchia
hideTyping();
const msgText = WAIT_MESSAGES[Math.floor(Math.random()*WAIT_MESSAGES.length)];
pushHistory("bot", msgText);
renderHistory(true);
// dopo il messaggio intermedio ricreo la bubble coi puntini
showTyping();
}, 7000); // 7 secondi
}
function cancelStatusMessage(){
if (statusTimer){
clearTimeout(statusTimer);
statusTimer = null;
}
}
/* Apertura input in pagina */
function openBar(){
if (!wrap.classList.contains("is-open")){
wrap.classList.add("is-open");
requestAnimationFrame(()=> { input.focus(); autoGrow(input); updateSendState(); });
}
}
inner.addEventListener("click", openBar);
placeholder.addEventListener("click", openBar);
function showToast(s, ok=true){
msg.textContent = s;
msg.style.color = ok ? "#c9f7d1" : "#ffd4d4";
msg.classList.add("show");
setTimeout(()=> msg.classList.remove("show"), 2300);
}
/* Modal */
let untrap = null;
function trapFocus(container){
const focusables = container.querySelectorAll('button, [href], input, textarea, [tabindex]:not([tabindex="-1"])');
if (!focusables.length) return ()=>{};
const first = focusables[0], last = focusables[focusables.length - 1];
function handle(e){
if(e.key !== 'Tab') return;
if(e.shiftKey && document.activeElement === first){ e.preventDefault(); last.focus(); }
else if(!e.shiftKey && document.activeElement === last){ e.preventDefault(); first.focus(); }
}
container.addEventListener('keydown', handle);
return () => container.removeEventListener('keydown', handle);
}
function openModal(){
try{ document.activeElement && document.activeElement.blur && document.activeElement.blur(); }catch(e){}
modal.classList.add("show");
modal.setAttribute("aria-hidden","false");
document.body.style.overflow = "hidden";
renderHistory(true);
untrap = trapFocus(modal.querySelector(".ask-panel"));
}
function closeModal(){
modal.classList.remove("show");
modal.setAttribute("aria-hidden","true");
document.body.style.overflow = "";
hideTyping();
cancelStatusMessage();
awaitingReply = false;
if (untrap) untrap();
}
closeB.addEventListener("click", closeModal);
modal.addEventListener("click", (e)=>{ if(e.target===modal) closeModal(); });
chatOpen.addEventListener("click", openModal);
/* Cestino */
clearB.addEventListener("click", ()=>{
if (confirm("Svuotare tutte le chat salvate su questo dispositivo?")){
clearHistory();
renderHistory(true);
setPendingIntent(null);
setPendingStep(null);
hideTyping();
cancelStatusMessage();
awaitingReply = false;
}
});
/* Storico */
function pushHistory(role, text){
const items = loadHistory();
items.push({ role, text, ts: Date.now() });
saveHistory(items);
return items;
}
function fmtTime(ts){
return new Date(ts).toLocaleString(undefined,{dateStyle:"short",timeStyle:"short"});
}
function renderHistory(scrollBottom = false){
const items = loadHistory();
body.innerHTML = "";
if (items.length === 0) {
const welcome = document.createElement("div");
welcome.className = "msg bot";
welcome.innerHTML = `
Ciao, comunicaci qui le Assenze o i Recuperi. Clicca uno dei due pulsanti qui sotto e segui ciò che ti viene chiesto.
${fmtTime(Date.now())}
`;
body.appendChild(welcome);
if (scrollBottom){
requestAnimationFrame(()=>{ body.scrollTop = body.scrollHeight; });
}
return;
}
items.forEach(it => {
const el = document.createElement("div");
el.className = "msg " + (it.role === "you" ? "you" : "bot");
el.innerHTML = `
${escapeHTML(it.text)}
${fmtTime(it.ts)}
`;
body.appendChild(el);
});
if (scrollBottom){
requestAnimationFrame(()=>{ body.scrollTop = body.scrollHeight; });
}
}
function escapeHTML(s){
return String(s).replace(/[&"']/g, m => ({'&':'&','':'>','"':'"',"'":'''}[m]));
}
/* Input helpers */
function autoGrow(el){ el.style.height = "auto"; el.style.height = el.scrollHeight + "px"; }
input.addEventListener("input", ()=> { autoGrow(input); updateSendState(); });
inputModal.addEventListener("input", ()=> { autoGrow(inputModal); updateSendState(); });
inputModal.addEventListener("keydown", (ev)=>{ if (ev.key === "Escape") closeModal(); });
/* Stato invio */
let sending = false;
function updateSendState(){
const hasTextMain = (input.value || "").trim().length > 0;
const hasTextModal = (inputModal.value || "").trim().length > 0;
send.disabled = !hasTextMain || sending;
sendModal.disabled = !hasTextModal || sending;
}
/* Payload per n8n */
function buildPayload(text, meta={}){
const sessionId = getSessionId();
return {
text,
query: text,
sessionId,
chatId: sessionId,
messageId: nextMessageId(),
ts: Date.now(),
source: "askai-widget",
origin: meta.origin || "user_reply",
intent: meta.intent || getPendingIntent() || null,
step: meta.step || (getPendingIntent() ? getPendingStep() || "details" : null)
};
}
/* Messaggi guida */
const GUIDE_RECUPERO = `Perfetto! Per organizzare il recupero mi lasci queste info?
1) Nome e Cognome
2) Sede (es. Pietralata)
3) Quando vorresti recuperare la lezione
(es. Ven 21 Novembre alle 19:00)`;
const GUIDE_ASSENZA = `Per segnarti assente mi servono queste informazioni:
1) Nome e Cognome
2) Sede (es. Pietralata)
3) Quando non puoi venire a lezione
(es. Gio 20 Novembre alle 18:00)`;
/* Intent + messaggi iniziali (HOME) */
btnRecupero.addEventListener("click", ()=>{
setPendingIntent("recupero");
setPendingStep("details");
pushHistory("you", "Recuperare una Lezione");
pushHistory("bot", GUIDE_RECUPERO);
openModal();
renderHistory(true);
});
btnAssenza.addEventListener("click", ()=>{
setPendingIntent("assenza");
setPendingStep("details");
pushHistory("you", "Assenza per Lezione");
pushHistory("bot", GUIDE_ASSENZA);
openModal();
renderHistory(true);
});
/* Intent + messaggi iniziali (POPUP) */
btnRecuperoModal.addEventListener("click", ()=>{
setPendingIntent("recupero");
setPendingStep("details");
pushHistory("you", "Recuperare una Lezione");
pushHistory("bot", GUIDE_RECUPERO);
renderHistory(true);
});
btnAssenzaModal.addEventListener("click", ()=>{
setPendingIntent("assenza");
setPendingStep("details");
pushHistory("you", "Assenza per Lezione");
pushHistory("bot", GUIDE_ASSENZA);
renderHistory(true);
});
function ensureOnline(){
if (!navigator.onLine){ showToast(ASKAI_CONFIG.messages.offline, false); return false; }
return true;
}
/* Invio (solo bottone) */
async function handleSendFrom(whichInput){
const val = (whichInput.value || "").trim();
if (!val){ showToast(ASKAI_CONFIG.messages.empty, false); return; }
if (!ensureOnline()) return;
if (sending) return;
whichInput.value = "";
whichInput.style.height = "auto";
autoGrow(whichInput);
updateSendState();
sending = true;
awaitingReply = true;
hideTyping();
cancelStatusMessage();
updateSendState();
pushHistory("you", val);
if (!modal.classList.contains("show")) openModal();
renderHistory(true);
// mostra puntini + programma messaggio intermedio
showTyping();
scheduleStatusMessage();
const payload = buildPayload(val, { origin: "user_reply" });
const wasDetails = payload.step === "details" && payload.intent;
const ac = new AbortController();
const t = setTimeout(()=> ac.abort(), 60000);
try{
const res = await fetch(ASKAI_CONFIG.http.endpoint, {
method: ASKAI_CONFIG.http.method || "POST",
headers: ASKAI_CONFIG.http.headers || {},
body: JSON.stringify(payload),
signal: ac.signal
});
if (!res.ok) throw new Error("bad_response");
const replyText = await ASKAI_CONFIG.http.parseReply(res);
awaitingReply = false;
cancelStatusMessage();
hideTyping();
pushHistory("bot", String(replyText || "(nessuna risposta)"));
renderHistory(true);
showToast(ASKAI_CONFIG.messages.sent, true);
}catch(e){
console.error(e);
awaitingReply = false;
cancelStatusMessage();
hideTyping();
pushHistory("bot", "⚠️ Errore di rete o webhook");
renderHistory(true);
showToast(ASKAI_CONFIG.messages.error, false);
}finally{
clearTimeout(t);
sending = false;
updateSendState();
if (wasDetails){
setPendingIntent(null);
setPendingStep(null);
}
}
}
send.addEventListener("click", ()=> handleSendFrom(input));
sendModal.addEventListener("click", ()=> handleSendFrom(inputModal));
})();