roulette / js /embed.js
ratandeep's picture
Deploy Build Small Roulette — static space serendipity engine for the org
653b61d
Raw
History Blame Contribute Delete
3.34 kB
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(); },
};
}