// Shared helpers: DOM, formatting, API, markdown, modal, loading states.
import { icon, FILE_ICON } from "./icons.js";
import { SITE, DESCRIPTION, canonicalPath, imagePath } from "./seo-meta.js";
export const $ = (s, el = document) => el.querySelector(s);
// the page's title, description and canonical address as you move around (the server writes them for a first load)
export function setMeta({ title, description, indexable } = {}) {
const url = location.origin + canonicalPath(location.href);
const rendered = document.querySelector('meta[name="rlx-page-url"]');
const changed = rendered?.content !== url;
if (changed) {
document.querySelectorAll('script[type="application/ld+json"]').forEach((s) => s.remove());
if (rendered) rendered.content = url;
}
if (title || changed) document.title = !title ? `${SITE}: RL environments on the Hugging Face Hub` : title.includes(SITE) ? title : `${title} · ${SITE}`;
const desc = (description || (changed ? DESCRIPTION : document.querySelector('meta[name="description"]')?.content) || DESCRIPTION).slice(0, 300);
const set = (selector, value) => document.querySelector(selector)?.setAttribute("content", value);
set('meta[name="description"]', desc);
for (const key of ["og:title", "twitter:title"]) set(`meta[${key.startsWith("og:") ? "property" : "name"}="${key}"]`, document.title);
set('meta[property="og:description"]', desc);
set('meta[name="twitter:description"]', desc);
set('meta[property="og:url"]', url);
if (changed) {
const image = location.origin + imagePath(location.href);
set('meta[property="og:image"]', image);
set('meta[name="twitter:image"]', image);
set('meta[property="og:type"]', /^\/t\//.test(location.pathname) || location.search.includes("task=") ? "article" : "website");
}
if (changed || indexable !== undefined) set('meta[name="robots"]', indexable === false || /^\/(runs|run\/|compare\/)/.test(location.pathname)
? "noindex, follow" : "index, follow, max-image-preview:large, max-snippet:-1");
const canon = document.querySelector('link[rel="canonical"]');
if (canon) canon.href = url;
}
// go to another page of the app: a real path (/d/org/name, /t/...), so every page has its own address for search
// engines and links; main.js routes on popstate
export function nav(to, { replace = false } = {}) {
history[replace ? "replaceState" : "pushState"](null, "", to);
dispatchEvent(new PopStateEvent("popstate"));
}
export const $$ = (s, el = document) => [...el.querySelectorAll(s)];
export const fmt = new Intl.NumberFormat("en-US");
export const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
export const vals = (v) => (Array.isArray(v) ? v : v == null ? [] : [v]);
// Rollouts graded before the runner translated it carry the General verifier's own label ("llm 3票" = 3 votes).
export const checkMsg = (m) => String(m ?? "").replace(/^llm (\d+)票$/, (_, n) => `judged by the model (${n} vote${n === "1" ? "" : "s"})`);
export const DOMAIN_NAME = { code: "Code", webdev: "Webdev", cyber: "Cyber", music: "Music", general: "General" };
export const storage = {
get(k) { try { return localStorage.getItem(k); } catch { return null; } },
set(k, v) { try { localStorage.setItem(k, v); } catch { /* private mode */ } },
};
// `retry`: a GET that failed upstream (502/503/504: the dataset viewer or the Hub, briefly) is asked again that many
// times, a moment apart, before the page shows an error
export async function api(path, opts = {}) {
const { retry = 0, ...o } = opts;
for (let attempt = 0; ; attempt++) {
try {
return await apiOnce(path, o);
} catch (e) {
const again = [502, 503, 504].includes(e.status) && (!o.method || o.method === "GET") && !o.signal?.aborted;
if (!again || attempt >= retry) throw e;
await new Promise((r) => setTimeout(r, 1500 * (attempt + 1)));
}
}
}
async function apiOnce(path, opts) {
const res = await fetch(path, {
credentials: "same-origin",
...opts,
headers: { ...(opts.body ? { "Content-Type": "application/json" } : {}), ...(opts.headers || {}) },
body: opts.body && typeof opts.body !== "string" ? JSON.stringify(opts.body) : opts.body,
});
if (!res.ok) {
let msg = `${res.status}`;
try {
const detail = (await res.json()).detail;
if (Array.isArray(detail)) msg = detail.map((x) => `${(x.loc || []).filter((k) => k !== "body").join(".")}: ${x.msg || "invalid value"}`).join("; ");
else if (typeof detail === "string") msg = detail;
} catch { /* not json */ }
const err = new Error(msg);
err.status = res.status;
throw err;
}
return res.json();
}
// Data ships gzipped (Spaces serve static files uncompressed); inflate in the browser.
export async function getJSONgz(url) {
const res = await fetch(url, { cache: "no-cache" }); // revalidate (a cheap 304), so a redeploy never serves stale data
if (!res.ok) throw new Error(`${url}: HTTP ${res.status}`);
const buf = new Uint8Array(await res.arrayBuffer());
if (buf[0] !== 0x1f || buf[1] !== 0x8b) return JSON.parse(new TextDecoder().decode(buf));
return new Response(new Blob([buf]).stream().pipeThrough(new DecompressionStream("gzip"))).json();
}
export function money(x) {
if (x == null || isNaN(x)) return "–";
if (x === 0) return "$0";
if (x < 0.01) return "<$0.01";
return "$" + (x < 10 ? x.toFixed(2) : x.toFixed(0));
}
export function ago(ts) {
if (!ts) return "";
const s = Date.now() / 1000 - ts;
if (s < 60) return "just now";
if (s < 3600) return `${Math.floor(s / 60)} min ago`;
if (s < 86400) return `${Math.floor(s / 3600)} h ago`;
return new Date(ts * 1000).toLocaleDateString();
}
export function dur(s) {
if (s == null) return "";
s = Math.max(0, Math.round(s));
return s < 60 ? `${s}s` : s < 3600 ? `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, "0")}s` : `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
}
export function bytes(n) {
if (n == null) return "";
return n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(0)} KB` : `${(n / 1048576).toFixed(1)} MB`;
}
export function tokensShort(n) {
if (!n) return "0";
return n < 1000 ? String(n) : n < 1e6 ? `${(n / 1000).toFixed(n < 1e4 ? 1 : 0)}k` : `${(n / 1e6).toFixed(2)}M`;
}
// Escape first, then headings, lists, fences, inline code, bold, links.
export function md(src) {
const out = [];
const lines = String(src ?? "").replace(/\r/g, "").split("\n");
let i = 0, para = [], list = null;
const inline = (s) => esc(s)
.replace(/`([^`]+)`/g, "$1")
.replace(/\*\*([^*]+)\*\*/g, "$1")
.replace(/(^|[\s(])\*([^*\s][^*]*?)\*(?=[\s.,;:!?)]|$)/g, "$1$2")
.replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g, '$1');
const flush = () => {
if (para.length) { out.push(`
${inline(para.join(" "))}
`); para = []; } if (list) { out.push(`<${list.tag}>${list.items.map((x) => `${esc(buf.join("\n"))}`);
i++;
continue;
}
const h = line.match(/^\s*(#{1,4})\s+(.*)$/);
const ul = line.match(/^\s*[-*•]\s+(.*)$/);
const ol = line.match(/^\s*\d+[.)]\s+(.*)$/);
const tr = /^\s*\|.*\|\s*$/.test(line);
if (h) { flush(); out.push(`| ${inline(c)} | ` : `${inline(c)} | `)).join("")}
|---|
${inline(para.join(" "))}
`); para = []; } const tag = ul ? "ul" : "ol"; if (!list || list.tag !== tag) { if (list) flush(); list = { tag, items: [] }; } list.items.push((ul || ol)[1]); } else if (!line.trim()) flush(); else { if (list) flush(); para.push(line.trim()); } i++; } flush(); return out.join(""); } export function toast(text, ms = 2200) { const t = $("#toast"); t.textContent = text; t.hidden = false; clearTimeout(toast._t); toast._t = setTimeout(() => (t.hidden = true), ms); } // A thin bar across the top while something loads. Nested calls are counted, so it finishes once. let busy = 0, crawl = null; export const progress = { start() { const bar = $("#progress i"); if (!bar) return; if (busy++ === 0) { clearInterval(crawl); bar.style.transition = "none"; bar.style.opacity = "1"; bar.style.width = "0"; requestAnimationFrame(() => { bar.style.transition = ""; bar.style.width = "30%"; }); crawl = setInterval(() => { const w = parseFloat(bar.style.width) || 0; if (w < 90) bar.style.width = w + (90 - w) * 0.08 + "%"; }, 250); } }, done() { const bar = $("#progress i"); if (!bar || busy === 0 || --busy > 0) return; clearInterval(crawl); bar.style.width = "100%"; setTimeout(() => { if (!busy) { bar.style.opacity = "0"; setTimeout(() => { if (!busy) bar.style.width = "0"; }, 300); } }, 180); }, async wrap(p) { this.start(); try { return await p; } finally { this.done(); } }, }; // Skeleton building blocks: widths are percentages, so they read as text rather than as boxes. export const sk = { line: (w = 100, h = 12) => ``, lines: (...ws) => ws.map((w) => sk.line(w)).join(""), box: (h, extra = "") => ``, card: (inner) => `${text}
` : ""}${action}Empty.
`; const [head, ...body] = header ? rows : [null, ...rows]; return `| ${esc(c)} | `).join("")}
|---|
| ${esc(c)} | `).join("")}
Empty sheet.
`; const width = Math.max(...rows.map((r) => r.length)); let used = 0; rows.forEach((r) => r.forEach((c, i) => { if (c !== "" && c != null) used = Math.max(used, i + 1); })); const cols = Math.max(1, Math.min(width, used)); const out = []; let gap = 0; rows.forEach((r, n) => { const empty = !r.slice(0, cols).some((c) => c !== "" && c != null); if (empty) { gap++; return; } if (gap) { out.push(`| ${Array.from({ length: cols }, (_, i) => ` | ${colName(i)} | `).join("")}
|---|