import { CONFIG } from "./config.js"; import { fetchDetail } from "./api.js"; const ALLOW = "accelerometer; autoplay; camera; microphone; clipboard-read; clipboard-write; encrypted-media; geolocation; gyroscope; midi"; const SANDBOX = "allow-scripts allow-same-origin allow-forms allow-popups allow-downloads allow-modals"; function el(tag, cls, html) { const e = document.createElement(tag); if (cls) e.className = cls; if (html != null) e.innerHTML = html; return e; } export function buildIframe(vm) { const f = document.createElement("iframe"); f.src = vm.embedUrl; f.title = vm.title; f.loading = "eager"; f.setAttribute("allow", ALLOW); f.setAttribute("sandbox", SANDBOX); f.referrerPolicy = "no-referrer-when-downgrade"; return f; } function fallbackCard(vm, message, { onSkip, countdownMs }) { const box = el("div", "fallback"); box.append(el("div", "fallback-emoji", vm.emoji)); box.append(el("strong", null, vm.title)); box.append(el("p", null, message)); const open = el("a", "btn"); open.textContent = "↗ Open on HF"; open.href = vm.hfUrl; open.target = "_blank"; open.rel = "noopener"; const skip = el("button", "btn btn-ghost"); skip.textContent = "Skip →"; skip.onclick = onSkip; const actions = el("div", "fallback-actions"); actions.append(open, skip); box.append(actions); let timer = null; if (countdownMs && onSkip) timer = setTimeout(onSkip, countdownMs); box._cleanup = () => timer && clearTimeout(timer); return box; } /** * Mounts a card into `stage`. Returns a controller with destroy(). * onState(state) receives "loading" | "waking" | "ready" | "broken". */ export function mountCard(stage, vm, { onState, onSkip } = {}) { stage.innerHTML = ""; const loader = el("div", "loader", "Loading the demo…"); stage.append(loader); onState?.("loading"); let destroyed = false; let iframe = null; let timeout = null; let fb = null; const showBroken = (msg) => { if (destroyed) return; iframe?.remove(); loader.remove(); fb?._cleanup?.(); fb = fallbackCard(vm, msg, { onSkip, countdownMs: CONFIG.autoAdvanceMs }); stage.append(fb); onState?.("broken"); }; const embed = (waking) => { if (destroyed) return; if (waking) { loader.textContent = "Waking this Space up…"; onState?.("waking"); } iframe = buildIframe(vm); iframe.addEventListener("load", () => { if (destroyed) return; clearTimeout(timeout); iframe.classList.add("ready"); // cross-fade in loader.remove(); onState?.("ready"); }); stage.append(iframe); timeout = setTimeout( () => showBroken("This one didn't wake in time. Open it on Hugging Face, or skip ahead."), CONFIG.loadTimeoutMs ); }; if (CONFIG.detailFetch) { fetchDetail(vm.id).then((d) => { if (destroyed) return; if (d.gated || d.disabled || CONFIG.skipUnhealthyStages.includes(d.stage)) { showBroken("This one isn't running right now — catch it on Hugging Face."); } else { embed(d.stage === "SLEEPING"); } }).catch(() => embed(false)); // detail failed — just try embedding } else { embed(false); } return { el: () => iframe, destroy() { destroyed = true; clearTimeout(timeout); fb?._cleanup?.(); iframe?.remove(); }, }; }