Spaces:
Running
Running
File size: 6,567 Bytes
0bc2c6b | 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 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | /**
* Shared page chrome for all MesmerTools HuggingFace Spaces.
*
* `createSpace()` builds the header, hero, a card with a <form> + output slot,
* a status area (errors + the rate-limit cross-sell banner), and the footer
* "more free tools" strip (the SEO backlinks). A tool space only has to fill
* the returned `form` and `output` elements and wire its submit handler.
*/
import { SITE, ENDPOINTS, TOOL_CATALOG, siteUrl } from "./config.js";
import { RateLimitError } from "./api-client.js";
/** Tiny hyperscript helper. `props` supports className, text, html, on*, attrs, dataset, style. */
export function el(tag, props = {}, ...children) {
const node = document.createElement(tag);
for (const [key, value] of Object.entries(props || {})) {
if (value == null) continue;
if (key === "className") node.className = value;
else if (key === "text") node.textContent = value;
else if (key === "html") node.innerHTML = value;
else if (key === "dataset") Object.assign(node.dataset, value);
else if (key === "style" && typeof value === "object") Object.assign(node.style, value);
else if (key.startsWith("on") && typeof value === "function") {
node.addEventListener(key.slice(2).toLowerCase(), value);
} else node.setAttribute(key, value);
}
for (const child of children.flat()) {
if (child == null || child === false) continue;
node.appendChild(typeof child === "string" ? document.createTextNode(child) : child);
}
return node;
}
function header() {
return el("header", { className: "ms-header" },
el("a", { className: "ms-brand", href: SITE.origin, target: "_blank", rel: "noopener" },
el("span", { className: "ms-brand-mark", text: "M" }),
el("span", { className: "ms-brand-name", text: SITE.name }),
),
el("a", { className: "ms-header-link", href: SITE.origin, target: "_blank", rel: "noopener",
html: "All tools <span aria-hidden=\"true\">↗</span>" }),
);
}
function crossSell(toolKey) {
const items = TOOL_CATALOG.filter((t) => t.key !== toolKey);
return el("footer", { className: "ms-footer" },
el("p", { className: "ms-footer-title", text: "More free tools on MesmerTools" }),
el("div", { className: "ms-grid" },
...items.map((t) =>
el("a", { className: "ms-tool-card", href: siteUrl(t.path), target: "_blank", rel: "noopener" },
el("span", { className: "ms-tool-emoji", text: t.emoji }),
el("span", { className: "ms-tool-text" },
el("span", { className: "ms-tool-name", text: t.name }),
el("span", { className: "ms-tool-desc", text: t.desc }),
),
),
),
),
el("p", { className: "ms-powered" },
"Powered by ",
el("a", { href: SITE.origin, target: "_blank", rel: "noopener", text: "mesmer.tools" }),
" — runs on the public MesmerTools API.",
),
);
}
/**
* @param {object} opts
* @param {string} opts.toolKey key in ENDPOINTS / TOOL_CATALOG (also excluded from the strip)
* @param {string} opts.emoji
* @param {string} opts.title
* @param {string} opts.subtitle
* @param {string} [opts.intro] optional longer paragraph under the subtitle
* @returns {{form: HTMLFormElement, output: HTMLElement, setLoading: Function,
* showError: Function, showRateLimit: Function, clearStatus: Function,
* clearOutput: Function, limitNote: string}}
*/
export function createSpace(opts) {
const { toolKey, emoji, title, subtitle, intro } = opts;
const endpoint = ENDPOINTS[toolKey] || {};
const limit = endpoint.freeLimitPerHour;
const limitNote = limit
? `${limit} free requests/hour per user · higher limits on mesmer.tools`
: `Free · backed by mesmer.tools`;
const root = document.getElementById("app") || document.body;
const status = el("div", { className: "ms-status", role: "status", "aria-live": "polite" });
const form = el("form", { className: "ms-form" });
const output = el("div", { className: "ms-output" });
const hero = el("section", { className: "ms-hero" },
el("div", { className: "ms-hero-emoji", text: emoji }),
el("h1", { className: "ms-title", text: title }),
el("p", { className: "ms-subtitle", text: subtitle }),
intro ? el("p", { className: "ms-intro", text: intro }) : null,
el("p", { className: "ms-limit-note", text: limitNote }),
);
const card = el("section", { className: "ms-card" }, form, status, output);
const main = el("main", { className: "ms-main" }, hero, card);
root.appendChild(header());
root.appendChild(main);
root.appendChild(crossSell(toolKey));
function clearStatus() {
status.replaceChildren();
}
function showRateLimit() {
const full = siteUrl(endpoint.fullToolPath || "/");
const msg = limit
? `You've used your ${limit} free requests this hour. Each visitor gets their own quota — `
: `You've hit a temporary limit — `;
clearStatus();
status.appendChild(
el("div", { className: "ms-banner ms-banner-limit" },
el("strong", { text: "Free limit reached. " }),
msg,
el("a", { className: "ms-banner-cta", href: full, target: "_blank", rel: "noopener",
text: "use the full tool on mesmer.tools →" }),
),
);
}
/** Renders a RateLimitError as the cross-sell banner; anything else as an error. */
function showError(err) {
if (err instanceof RateLimitError || (err && err.isRateLimit)) {
showRateLimit();
return;
}
const text = typeof err === "string" ? err : (err && err.message) || "Something went wrong.";
clearStatus();
status.appendChild(el("div", { className: "ms-banner ms-banner-error", text }));
}
function setLoading(isLoading, label) {
card.classList.toggle("is-loading", !!isLoading);
if (isLoading) {
clearStatus();
status.appendChild(
el("div", { className: "ms-banner ms-banner-loading" },
el("span", { className: "ms-spinner", "aria-hidden": "true" }),
el("span", { text: label || "Working…" }),
),
);
} else {
clearStatus();
}
}
function clearOutput() {
output.replaceChildren();
}
return { form, output, status, setLoading, showError, showRateLimit, clearStatus, clearOutput, limitNote };
}
/** Standalone footer for non-tool spaces (e.g. the benchmark) that build their own body. */
export function mountChrome(toolKey) {
const root = document.getElementById("app") || document.body;
root.prepend(header());
root.appendChild(crossSell(toolKey));
}
|