emmys / chat.js
hudaakram's picture
Update chat.js
d367b31 verified
// 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 = `
<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?.();
// 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();
})();