// chat.js — EMM·AI (client-only, works on any static page) (() => { // --------- helpers ---------- const $ = (q, r=document) => r.querySelector(q); const $$ = (q, r=document) => [...r.querySelectorAll(q)]; // Build a tiny KB from whatever exists on the page function buildKB(){ // presenters: pills inside the Presenters card if present const presenters = $$("#live + div .pill").map(x=>x.textContent.trim()); // schedule: rows in the schedule section if present const scheduleCards = $$("#schedule .grid > div"); const schedule = scheduleCards.map(c => ({ title: $(".text-yellow-200", c)?.textContent.trim() || "", time: $(".text-yellow-100\\/70", c)?.textContent.trim() || "" })).filter(x=>x.title); // categories: links on any page const categories = $$('a[href$=".html"]').map(a => ({ name: a.textContent.trim().replace(/\s+arrow_right.*$/i,''), href: a.getAttribute('href') })).filter(c => /category/i.test(c.href)); return { presenters, schedule, categories }; } const KB = buildKB(); // Inject styles const css = ` .chat-fab{position:fixed;right:18px;bottom:18px;z-index:50} .chat-panel{position:fixed;right:18px;bottom:86px;width:340px;max-width:92vw;z-index:50} .glass2{backdrop-filter: blur(10px); background: linear-gradient(135deg, rgba(18,16,12,.92), rgba(18,16,12,.55)); border:1px solid rgba(234,179,8,.25)} .msg{border-radius:14px;padding:.6rem .8rem;font-size:.9rem;line-height:1.35} .msg-ai{background:rgba(234,179,8,.07);border:1px solid rgba(234,179,8,.25);color:#fde68a} .msg-me{background:#0b0b0b;border:1px solid #222;color:#fefce8} .blink{animation:blink 1.1s linear infinite}@keyframes blink{50%{opacity:.35}} `; const st = document.createElement("style"); st.textContent = css; document.head.appendChild(st); // Inject widget HTML const wrap = document.createElement("div"); wrap.innerHTML = `
`; document.body.appendChild(wrap); if (window.lucide) window.lucide.createIcons?.(); // UI helpers const log = $("#chatLog"), panel = $("#chatPanel"), input = $("#chatInput"); function msg(role, text){ const el = document.createElement("div"); el.className = "msg " + (role==="ai" ? "msg-ai" : "msg-me"); el.textContent = text; log.appendChild(el); log.scrollTop = log.scrollHeight; if (role==="ai" && speakOn) speak(text); } function typing(on){ const old = $("#typing"); if (old) old.remove(); if (!on) return; const d = document.createElement("div"); d.id="typing"; d.className="msg msg-ai blink"; d.textContent="EMM·AI is typing…"; log.appendChild(d); log.scrollTop = log.scrollHeight; } // Countdown helpers reused for ICS function nextShowtime(){ const now = new Date(); const show = new Date(); show.setHours(20,0,0,0); if (now > show) show.setDate(show.getDate()+1); return show; } function addICS(){ const target = nextShowtime(); const fix = d => d.toISOString().replace(/[-:]/g,'').split('.')[0] + 'Z'; const start = fix(target), end = fix(new Date(target.getTime()+2*60*60*1000)); const ics = `BEGIN:VCALENDAR VERSION:2.0 PRODID:-//AI WEEK//EMMYS//EN BEGIN:VEVENT UID:${Date.now()}@emmys DTSTAMP:${start} DTSTART:${start} DTEND:${end} SUMMARY:Emmys Live Ceremony DESCRIPTION:AI WEEK Emmys live broadcast. END:VEVENT END:VCALENDAR`.replace(/\\n/g,'\n'); const blob = new Blob([ics], {type:'text/calendar'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href=url; a.download='emmys.ics'; a.click(); URL.revokeObjectURL(url); } // Simple router/intents (local) function localAnswer(q){ const t = q.toLowerCase(); // open category by name or index const cat = KB.categories.find(c => t.includes(c.name.toLowerCase()) || t.includes(c.href.replace('.html',''))); if (cat){ location.href = cat.href; return `Opening “${cat.name}”…`; } if (/(time|when|schedule|start|live)/.test(t) && KB.schedule.length){ const s = KB.schedule.map(i=>`• ${i.title}: ${i.time}`).join('\n'); return `Tonight’s schedule:\n${s}`; } if ((t.includes("present") || t.includes("host")) && KB.presenters.length){ return `Presenters: ${KB.presenters.join(', ')}.`; } if (t.includes("calendar") || t.includes("ics")){ addICS(); return "Added to calendar (.ics downloaded)."; } if (t.includes("access denied") || t.includes("verify") || t.includes("security")){ return "“Access Denied” = score below threshold. Accepted unlocks tools; denied keeps the system locked and logs an audit event."; } return null; } // Optional backend (if you later add one). POST /chat -> {reply} async function callBackend(message){ try{ const r = await fetch('/chat',{method:'POST',headers:{'Content-Type':'application/json'}, body: JSON.stringify({ message, context: { page: location.pathname, KB } })}); if (r.ok){ const j = await r.json(); return (j.reply||"").trim() || null; } }catch(e){} return null; } async function replyTo(text){ typing(true); let ans = localAnswer(text); if (!ans) ans = await callBackend(text); if (!ans) ans = "I can help with schedule, presenters, winners ticker, calendar, and opening categories. Connect a backend at /chat for full AI chat."; typing(false); msg("ai", ans); } function hello(){ msg("ai","Welcome to the Emmys ✨ Ask about the schedule, presenters, or say “Open Outstanding Drama Series”. Say “Add to calendar”."); } // Voice toggle (browser TTS) let speakOn = false; function speak(t){ try{ const u = new SpeechSynthesisUtterance(t); u.rate=1.02; speechSynthesis.speak(u);}catch{} } // Wire events $("#chatToggle").onclick = ()=> panel.classList.toggle("hidden"); $("#chatClose").onclick = ()=> panel.classList.add("hidden"); $("#clearBtn").onclick = ()=> { log.innerHTML=""; hello(); }; $$("#chatPanel [data-sample]").forEach(a=> a.onclick = ()=> { const i=$("#chatInput"); i.value=a.dataset.sample; i.form.requestSubmit(); }); $("#chatForm").addEventListener("submit", e=>{ e.preventDefault(); const t = $("#chatInput").value.trim(); if (!t) return; msg("me", t); $("#chatInput").value=""; replyTo(t); }); $("#voiceBtn").onclick = (e)=>{ speakOn = !speakOn; e.target.classList.toggle("bg-yellow-400"); e.target.classList.toggle("text-black"); }; hello(); })();