|
|
|
|
|
(() => { |
|
|
|
|
|
const $ = (q, r=document) => r.querySelector(q); |
|
|
const $$ = (q, r=document) => [...r.querySelectorAll(q)]; |
|
|
|
|
|
|
|
|
function buildKB(){ |
|
|
|
|
|
const presenters = $$("#live + div .pill").map(x=>x.textContent.trim()); |
|
|
|
|
|
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); |
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
const wrap = document.createElement("div"); |
|
|
wrap.innerHTML = ` |
|
|
<div class="chat-fab"> |
|
|
<button id="chatToggle" class="rounded-full w-14 h-14 bg-yellow-400 text-black shadow-xl flex items-center justify-center"> |
|
|
<i data-lucide="message-circle" class="w-6 h-6"></i> |
|
|
</button> |
|
|
</div> |
|
|
<div id="chatPanel" class="chat-panel hidden"> |
|
|
<div class="glass2 rounded-2xl overflow-hidden shadow-2xl"> |
|
|
<div class="px-4 py-3 flex items-center justify-between border-b border-yellow-400/20"> |
|
|
<div class="flex items-center gap-2"> |
|
|
<div class="w-7 h-7 rounded-full bg-yellow-500/20 ring-1 ring-yellow-400/50 flex items-center justify-center"> |
|
|
<i data-lucide="sparkles" class="w-4 h-4 text-yellow-300"></i> |
|
|
</div> |
|
|
<div class="text-yellow-100 text-sm font-semibold">EMM·AI — Golden Concierge</div> |
|
|
</div> |
|
|
<div class="flex items-center gap-2"> |
|
|
<button id="voiceBtn" class="text-[11px] px-2 py-1 rounded bg-yellow-50/10 border border-yellow-400/20 text-yellow-200">Voice</button> |
|
|
<button id="clearBtn" class="text-[11px] px-2 py-1 rounded bg-yellow-50/10 border border-yellow-400/20 text-yellow-200">Clear</button> |
|
|
<button id="chatClose" class="text-yellow-200"><i data-lucide="x" class="w-5 h-5"></i></button> |
|
|
</div> |
|
|
</div> |
|
|
<div id="chatLog" class="h-[360px] overflow-y-auto p-3 space-y-2 text-[14px]"></div> |
|
|
<div class="p-3 border-t border-yellow-400/20"> |
|
|
<form id="chatForm" class="flex gap-2"> |
|
|
<input id="chatInput" class="flex-1 px-3 py-2 rounded-xl bg-black/60 border border-yellow-400/20 text-yellow-100 placeholder:text-yellow-100/50" |
|
|
placeholder="Ask about schedule, presenters, winners…"> |
|
|
<button class="px-3 py-2 rounded-xl bg-yellow-400 text-black font-semibold">Send</button> |
|
|
</form> |
|
|
<div class="mt-2 text-[11px] text-yellow-100/60"> |
|
|
Try: <span class="underline cursor-pointer" data-sample="When does the live start?">When does the live start?</span> · |
|
|
<span class="underline cursor-pointer" data-sample="Add the show to my calendar">Add the show to my calendar</span> · |
|
|
<span class="underline cursor-pointer" data-sample="Open Outstanding Drama Series">Open Outstanding Drama Series</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div>`; |
|
|
document.body.appendChild(wrap); |
|
|
if (window.lucide) window.lucide.createIcons?.(); |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
function localAnswer(q){ |
|
|
const t = q.toLowerCase(); |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
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”."); |
|
|
} |
|
|
|
|
|
|
|
|
let speakOn = false; |
|
|
function speak(t){ try{ const u = new SpeechSynthesisUtterance(t); u.rate=1.02; speechSynthesis.speak(u);}catch{} } |
|
|
|
|
|
|
|
|
$("#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(); |
|
|
})(); |
|
|
|