File size: 3,343 Bytes
653b61d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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(); },
  };
}