mesmertech's picture
Deploy static space build
0bc2c6b verified
Raw
History Blame Contribute Delete
6.57 kB
/**
* 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));
}