/** * Shared page chrome for all MesmerTools HuggingFace Spaces. * * `createSpace()` builds the header, hero, a card with a
+ 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 " }), ); } 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)); }