Spaces:
Running
Running
| /** | |
| * 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)); | |
| } | |