Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Download web/js/explore.js from FineEnvs/MiMo-RL-Envs-Explorer: direct link, hf CLI and curl.
- Browser
- Download file 25.1 kB
-
https://huggingface.co/spaces/FineEnvs/MiMo-RL-Envs-Explorer/resolve/main/web/js/explore.js
- Command line
-
hf download hf://spaces/FineEnvs/MiMo-RL-Envs-Explorer/web/js/explore.js
-
curl -L -o explore.js https://huggingface.co/spaces/FineEnvs/MiMo-RL-Envs-Explorer/resolve/main/web/js/explore.js
25.1 kB
| // Explore: domain cards, the treemap, search, facets and the task list. | |
| import { $, esc, fmt, vals, getJSONgz, sk, progress, emptyState } from "./util.js"; | |
| import { icon, DOMAIN_ICON } from "./icons.js"; | |
| const CJK = /[γ-ιΏΏο€-ο«Ώ]/; | |
| const PAGE = 30; | |
| const AUTO_PAGES = 3; // pages that load by themselves as you scroll; after that a button, so the footer stays reachable | |
| const MAP_TOP = { code: 7, webdev: 9, cyber: 7, music: 9, general: 9 }; | |
| const LEFTOVER = /^(Other|Unknown|unknown|None named|Unspecified|Unrated)$/; | |
| const num = (v) => { const m = String(v).match(/\d+/); return m ? +m[0] : 99; }; | |
| const ORDINAL = { tier: num, voices: num, tempo: (v) => (/^Slow/.test(v) ? 0 : /^Moderate/.test(v) ? 1 : /^Fast/.test(v) ? 2 : 3), meter: (v) => ["2/4", "3/4", "4/4", "6/8", "7/8"].indexOf(v) }; | |
| let DATA, DOMS, ENVS, SEARCH; | |
| const state = { q: "", dom: null, sel: {}, seed: Math.random() }; | |
| let matches = [], shown = 0, searchIds = null, root = null, observer = null, autoLoaded = 0; | |
| const expanded = new Set(); | |
| export async function load() { | |
| if (DATA) return DATA; | |
| DATA = await getJSONgz("/data/index.json.gz"); | |
| DOMS = Object.fromEntries(DATA.domains.map((d) => [d.id, d])); | |
| ENVS = DATA.envs; | |
| ENVS.forEach((e) => { e._blob = (e.t + " " + e.s).toLowerCase(); e._rand = hash(e.id); }); | |
| SEARCH = new MiniSearch({ | |
| fields: ["t", "s", "x", "id"], storeFields: [], | |
| extractField: (doc, f) => (f === "x" ? Object.values(doc.f).flat().join(" ") + " " + DOMS[doc.d].name : doc[f]), | |
| searchOptions: { boost: { t: 3, x: 2 }, prefix: true, fuzzy: 0.15, combineWith: "AND" }, | |
| }); | |
| SEARCH.addAll(ENVS); | |
| return DATA; | |
| } | |
| export const record = (id) => ENVS && ENVS.find((e) => e.id === id); | |
| export const domains = () => DOMS; | |
| // Shuffle per page load, so people who arrive together don't all open (and run) the same first task. | |
| function hash(s) { let h = 2166136261; for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619); return h >>> 0; } | |
| const order = (e) => ((e._rand ^ Math.floor(state.seed * 4294967295)) >>> 0); | |
| function skeleton() { | |
| return `<div class="wrap"> | |
| <div class="ex-head"><div style="flex:1">${sk.line(28, 24)}<div style="height:10px"></div>${sk.line(52)}</div></div> | |
| <div class="domains">${Array.from({ length: 6 }, () => sk.card(sk.line(50) + sk.line(35, 20) + sk.line(70))).join("")}</div> | |
| <div class="panel" style="padding:16px;margin-bottom:18px">${sk.box(220)}</div> | |
| <div class="layout"><div>${sk.lines(60, 90, 80, 70, 85, 60)}</div> | |
| <div class="list">${Array.from({ length: 6 }, () => `<li>${sk.card(sk.line(30) + sk.line(90, 16) + sk.lines(100, 70))}</li>`).join("")}</div></div></div>`; | |
| } | |
| export async function mount(el, params) { | |
| root = el; | |
| if (!DATA) { | |
| el.innerHTML = skeleton(); | |
| try { await progress.wrap(load()); } | |
| catch (e) { el.innerHTML = `<div class="wrap page">${emptyState("alert", "Couldn't load the environments", esc(e.message), `<button class="btn" id="reload" type="button">${icon("refresh")}Try again</button>`)}</div>`; $("#reload", el)?.addEventListener("click", () => location.reload()); return; } | |
| if (root !== el) return; | |
| } | |
| if (params) { | |
| state.q = params.get("q") || ""; | |
| state.dom = DOMS[params.get("d")] ? params.get("d") : null; | |
| state.sel = {}; | |
| (params.get("f") || "").split(";").filter(Boolean).forEach((pair) => { | |
| const [k, v] = pair.split(":"); | |
| if (k && v) state.sel[k] = new Set(v.split("|").map(decodeURIComponent)); | |
| }); | |
| } | |
| el.innerHTML = ` | |
| <div class="wrap fade-in"> | |
| <section class="ex-head"> | |
| <div><h1>RL environments</h1> | |
| <p>Every task in Xiaomi's MiMo-V2.6 RL release. Open one to see exactly what the agent gets and how it is graded, then run a rollout with a model of your choice.</p></div> | |
| </section> | |
| <div class="domains" id="domains" role="tablist" aria-label="Domains"></div> | |
| <details class="panel map-card" id="map-card" ${matchMedia("(max-width: 760px)").matches ? "" : "open"}> | |
| <summary>${icon("chevronRight", 15, "chev")}<h2 id="map-title"></h2><span class="hint">Area is the number of environments. Click a block to filter.</span></summary> | |
| <div id="map" class="map"></div> | |
| </details> | |
| <div class="toolbar" id="toolbar"> | |
| <div class="search">${icon("search", 16)} | |
| <input id="q" type="search" placeholder="Search titles, briefs, categoriesβ¦" autocomplete="off" spellcheck="false" aria-label="Search environments"><kbd>/</kbd> | |
| </div> | |
| <button class="btn" id="surprise" type="button" title="Open a random environment from the current results">${icon("shuffle", 15)}<span class="long">Random</span></button> | |
| <button class="btn only-mobile" id="open-filters" type="button">${icon("filter", 15)}Filters<span id="nfilt"></span></button> | |
| </div> | |
| <div class="layout"> | |
| <div class="sheet-scrim" id="sheet-scrim" hidden></div> | |
| <aside class="filters" id="filters" aria-label="Filters"> | |
| <div class="sheet-head"><b>Filters</b><button class="icon-btn" id="close-filters" type="button" aria-label="Close filters">${icon("x", 18)}</button></div> | |
| <div class="filters-head"><span>Filters</span><button class="link" id="clear" type="button">Clear all</button></div> | |
| <div id="facets"></div> | |
| <div class="sheet-foot"><button class="btn" id="clear3" type="button">Clear</button><button class="btn primary grow" id="show-results" type="button">Show results</button></div> | |
| </aside> | |
| <section class="results" aria-live="polite"> | |
| <div class="results-head"><span class="count" id="count"></span><span class="order" id="order-note"></span><div class="active" id="active"></div></div> | |
| <ol class="list" id="list"></ol> | |
| <div class="sentinel" id="sentinel"></div> | |
| <div class="more-row" id="more-row" hidden><span class="muted sm" id="more-count"></span> | |
| <button class="btn" id="more" type="button">Load more</button></div> | |
| <div id="empty" hidden>${emptyState("search", "No environments match", "Try fewer filters or a different word.", `<button class="btn" id="clear2" type="button">Clear search and filters</button>`)}</div> | |
| </section> | |
| </div> | |
| </div>`; | |
| $("#q", el).value = state.q; | |
| wire(el); | |
| run(); | |
| } | |
| export function unmount() { if (observer) observer.disconnect(); observer = null; document.body.classList.remove("noscroll"); } | |
| function writeURL() { | |
| const p = new URLSearchParams(); | |
| if (state.q) p.set("q", state.q); | |
| if (state.dom) p.set("d", state.dom); | |
| const f = Object.entries(state.sel).filter(([, s]) => s.size).map(([k, s]) => k + ":" + [...s].map(encodeURIComponent).join("|")).join(";"); | |
| if (f) p.set("f", f); | |
| const qs = p.toString(); | |
| history.replaceState(null, "", "#/" + (qs ? "?" + qs : "")); | |
| } | |
| // ββ filtering ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const facetsFor = (dom) => (dom ? DOMS[dom].facets : [["d", "Domain"], ["language", "Brief language"]]); | |
| const valueOf = (e, key) => (key === "d" ? DOMS[e.d].name : e.f[key]); | |
| function passes(e, skipKey) { | |
| if (state.dom && e.d !== state.dom) return false; | |
| if (searchIds && !searchIds.has(e.id)) return false; | |
| for (const [k, set] of Object.entries(state.sel)) { | |
| if (k === skipKey || !set.size) continue; | |
| if (!vals(valueOf(e, k)).some((v) => set.has(v))) return false; | |
| } | |
| return true; | |
| } | |
| function computeSearch() { | |
| const q = state.q.trim(); | |
| if (!q) { searchIds = null; return null; } | |
| if (CJK.test(q)) { | |
| const needle = q.toLowerCase(); | |
| const hit = ENVS.filter((e) => e._blob.includes(needle)); | |
| searchIds = new Set(hit.map((e) => e.id)); | |
| return new Map(hit.map((e, i) => [e.id, -i])); | |
| } | |
| const res = SEARCH.search(q); | |
| searchIds = new Set(res.map((r) => r.id)); | |
| return new Map(res.map((r) => [r.id, r.score])); | |
| } | |
| function run() { | |
| const scores = computeSearch(); | |
| const allowed = new Set(facetsFor(state.dom).map(([k]) => k)); | |
| Object.keys(state.sel).forEach((k) => { if (!allowed.has(k)) delete state.sel[k]; }); | |
| matches = ENVS.filter((e) => passes(e)); | |
| if (scores) matches.sort((a, b) => (scores.get(b.id) ?? 0) - (scores.get(a.id) ?? 0)); | |
| else matches.sort((a, b) => order(a) - order(b)); | |
| $("#order-note", root).innerHTML = scores ? "best match first" : `${icon("shuffle", 13)}shuffled each visit`; | |
| const nf = Object.values(state.sel).reduce((n, s) => n + s.size, 0); | |
| $("#nfilt", root).textContent = nf ? ` (${nf})` : ""; | |
| renderDomains(); renderFacets(); renderActive(); renderMap(); renderList(true); writeURL(); | |
| } | |
| // ββ domain cards βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function renderDomains() { | |
| const inSearch = (d) => (searchIds ? ENVS.filter((e) => (!d || e.d === d) && searchIds.has(e.id)).length : null); | |
| const cards = [{ id: null, name: "All domains", verifier: "5 kinds of grader", count: DATA.total }, ...DATA.domains]; | |
| $("#domains", root).innerHTML = cards.map((d) => { | |
| const hit = inSearch(d.id); | |
| return `<button class="dom" role="tab" data-dom="${d.id ?? ""}" aria-selected="${state.dom === d.id}" style="--dc:${d.id ? `var(--c-${d.id})` : "var(--text)"}" title="${esc(d.task || "")}"> | |
| <span class="name">${icon(d.id ? DOMAIN_ICON[d.id] : "grid", 14)}${esc(d.name)}</span> | |
| <span class="n">${fmt.format(hit ?? d.count)}${hit != null ? `<span class="of"> / ${fmt.format(d.count)}</span>` : ""}</span> | |
| <span class="ver">${d.id ? `Graded by ${esc(d.verifier)}` : esc(d.verifier)}</span></button>`; | |
| }).join(""); | |
| } | |
| // ββ facets βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function renderFacets() { | |
| $("#facets", root).innerHTML = facetsFor(state.dom).map(([key, label]) => { | |
| const counts = new Map(); | |
| ENVS.forEach((e) => { if (passes(e, key)) vals(valueOf(e, key)).forEach((v) => counts.set(v, (counts.get(v) || 0) + 1)); }); | |
| const sel = state.sel[key] || new Set(); | |
| sel.forEach((v) => { if (!counts.has(v)) counts.set(v, 0); }); | |
| if (counts.size <= 1 && !sel.size && key !== "d") return ""; | |
| // ordinal facets keep their natural order (Tier 1 β 5, 1 voice β 6, slow β fast); the rest go by count | |
| const ordinal = ORDINAL[key]; | |
| let items = [...counts.entries()].sort(ordinal ? (a, b) => ordinal(a[0]) - ordinal(b[0]) | |
| : (a, b) => b[1] - a[1] || String(a[0]).localeCompare(String(b[0]))); | |
| const limit = expanded.has(key) ? Infinity : (phone() ? 5 : 8); | |
| const hidden = Math.max(0, items.length - limit); | |
| items = items.slice(0, limit); | |
| // sections fold: on a phone only the ones in use start open, so the sheet isn't one long scroll | |
| const open = facetOpen.has(key) ? facetOpen.get(key) : (!phone() || sel.size > 0); | |
| return `<details class="facet" data-facet="${esc(key)}" ${open ? "open" : ""}><summary><h3>${esc(label)}</h3>${sel.size ? `<span class="fsel">${sel.size} selected</span>` : ""}${icon("chevronDown", 14, "chev")}</summary> | |
| ${items.map(([v, c]) => `<button class="opt ${c ? "" : "zero"}" data-k="${esc(key)}" data-v="${esc(v)}" aria-pressed="${sel.has(v)}"> | |
| <span class="box">${sel.has(v) ? icon("check", 11) : ""}</span><span class="lab" title="${esc(v)}">${esc(v)}</span><span class="c">${fmt.format(c)}</span></button>`).join("")} | |
| ${hidden || expanded.has(key) ? `<button class="link more" data-more="${esc(key)}">${icon(expanded.has(key) ? "chevronDown" : "chevronRight", 13)}${expanded.has(key) ? "Show fewer" : `Show ${hidden} more`}</button>` : ""}</details>`; | |
| }).join(""); | |
| } | |
| const facetOpen = new Map(); | |
| const phone = () => matchMedia("(max-width: 760px)").matches; | |
| function toggle(key, v) { | |
| if (key === "d") { const id = DATA.domains.find((d) => d.name === v)?.id; if (id) { setDomain(id); return; } } | |
| const set = (state.sel[key] ||= new Set()); | |
| set.has(v) ? set.delete(v) : set.add(v); | |
| if (!set.size) delete state.sel[key]; | |
| run(); | |
| } | |
| function setDomain(id) { state.dom = id; state.sel = {}; run(); } | |
| function renderActive() { | |
| const labels = Object.fromEntries(facetsFor(state.dom)); | |
| const pills = []; | |
| Object.entries(state.sel).forEach(([k, set]) => set.forEach((v) => | |
| pills.push(`<button class="pill" data-k="${esc(k)}" data-v="${esc(v)}"><span>${esc(labels[k] || k)}</span>${esc(v)}${icon("x", 13)}</button>`))); | |
| $("#active", root).innerHTML = pills.join(""); | |
| $("#count", root).textContent = `${fmt.format(matches.length)} environment${matches.length === 1 ? "" : "s"}`; | |
| const sr = $("#show-results", root); | |
| if (sr) sr.textContent = `Show ${fmt.format(matches.length)} environment${matches.length === 1 ? "" : "s"}`; | |
| } | |
| // ββ map ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const cssVar = (n) => getComputedStyle(document.documentElement).getPropertyValue(n).trim(); | |
| function mapData() { | |
| if (!state.dom) { | |
| const children = DATA.domains.map((d) => { | |
| const counts = new Map(); | |
| ENVS.forEach((e) => { if (e.d === d.id && passes(e)) vals(e.f[d.main]).forEach((v) => counts.set(v, (counts.get(v) || 0) + 1)); }); | |
| const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]); | |
| const top = sorted.slice(0, MAP_TOP[d.id]); | |
| const rest = sorted.slice(MAP_TOP[d.id]).reduce((s, [, c]) => s + c, 0); | |
| const kids = top.map(([name, value], rank) => ({ name, value, rank, dom: d.id, other: LEFTOVER.test(name) })); | |
| if (rest) kids.push({ name: `${sorted.length - top.length} more`, value: rest, rank: top.length, dom: d.id, other: true }); | |
| return { name: d.name, dom: d.id, children: kids }; | |
| }).filter((d) => d.children.length); | |
| return { name: "all", children }; | |
| } | |
| const d = DOMS[state.dom]; | |
| const counts = new Map(); | |
| ENVS.forEach((e) => { if (passes(e, d.main)) vals(e.f[d.main]).forEach((v) => counts.set(v, (counts.get(v) || 0) + 1)); }); | |
| const sorted = [...counts.entries()].sort((a, b) => b[1] - a[1]); | |
| return { name: d.name, children: [{ name: d.name, dom: d.id, children: sorted.map(([name, value], rank) => ({ name, value, rank, dom: d.id, other: LEFTOVER.test(name) })) }] }; | |
| } | |
| export function renderMap() { | |
| const el = root && $("#map", root); | |
| if (!el) return; | |
| const W = el.clientWidth, H = el.clientHeight; | |
| $("#map-title", root).textContent = state.dom | |
| ? `${DOMS[state.dom].name}, by ${DOMS[state.dom].facets.find(([k]) => k === DOMS[state.dom].main)[1].replace(" (mentioned)", "").toLowerCase()}` | |
| : "Everything, by domain and category"; | |
| if (!W || !H) return; // the overview is collapsed | |
| const data = mapData(); | |
| el.innerHTML = ""; | |
| if (!data.children.length) { el.innerHTML = `<p class="empty">Nothing to map.</p>`; return; } | |
| const h = d3.hierarchy(data).sum((d) => d.value || 0).sort((a, b) => (!!a.data.other - !!b.data.other) || b.value - a.value); | |
| d3.treemap().size([W, H]).paddingInner(2).paddingTop((n) => (n.depth === 1 && !state.dom ? 20 : 0)).round(true).tile(d3.treemapSquarify.ratio(1.35))(h); | |
| const surface = cssVar("--surface"); | |
| const svg = d3.select(el).append("svg").attr("viewBox", `0 0 ${W} ${H}`).attr("role", "img").attr("aria-label", "Treemap of environments by category"); | |
| if (!state.dom) { | |
| svg.selectAll("g.dh").data(h.children).join("g").attr("class", "cell dh").attr("transform", (d) => `translate(${d.x0},${d.y0})`).call((g) => { | |
| g.append("rect").attr("width", (d) => d.x1 - d.x0).attr("height", 18).attr("fill", "transparent").on("click", (_, d) => setDomain(d.data.dom)); | |
| g.append("text").attr("class", "dname").attr("x", 1).attr("y", 13).attr("fill", (d) => cssVar(`--c-${d.data.dom}`)) | |
| .text((d) => (d.x1 - d.x0 > 90 ? `${d.data.name} Β· ${fmt.format(d.value)}` : d.x1 - d.x0 > 34 ? d.data.name : "")); | |
| }); | |
| } | |
| const leaves = h.leaves(); | |
| const maxRank = d3.max(leaves, (d) => d.data.rank) || 1; | |
| const sel = state.dom ? state.sel[DOMS[state.dom].main] : null; | |
| const g = svg.selectAll("g.leaf").data(leaves).join("g").attr("class", (d) => "cell leaf" + (sel && sel.size && !sel.has(d.data.name) ? " dim" : "")) | |
| .attr("transform", (d) => `translate(${d.x0},${d.y0})`); | |
| g.append("rect").attr("width", (d) => Math.max(0, d.x1 - d.x0)).attr("height", (d) => Math.max(0, d.y1 - d.y0)).attr("rx", 4) | |
| .attr("fill", (d) => d3.interpolateRgb(surface, cssVar(`--c-${d.data.dom}`))(d.data.other ? 0.42 : 0.95 - 0.38 * (d.data.rank / Math.max(maxRank, 6)))) | |
| .on("click", (_, d) => clickCell(d.data)) | |
| .on("mousemove", (ev, d) => tip(ev, `<b>${esc(d.data.name)}</b><br>${fmt.format(d.value)} environments Β· ${DOMS[d.data.dom].name}`)) | |
| .on("mouseleave", () => tip()); | |
| g.each(function (d) { | |
| const w = d.x1 - d.x0, hh = d.y1 - d.y0; | |
| if (w < 44 || hh < 26) return; | |
| const color = d3.lab(this.querySelector("rect").getAttribute("fill")).l > 64 ? "#17181c" : "#ffffff"; | |
| const max = Math.floor((w - 14) / 6.6); | |
| const label = d.data.name.length > max ? d.data.name.slice(0, Math.max(1, max - 1)) + "β¦" : d.data.name; | |
| const t = d3.select(this); | |
| t.append("text").attr("class", "lbl").attr("x", 8).attr("y", 18).attr("fill", color).text(label); | |
| if (hh > 42) t.append("text").attr("class", "cnt").attr("x", 8).attr("y", 34).attr("fill", color).text(fmt.format(d.value)); | |
| }); | |
| animateMap(svg, g, h, W, H); | |
| } | |
| // Drill-down motion: blocks that stay glide from where they were; new blocks grow out of the region they came | |
| // from (the domain you clicked), or out of their own centre. Layout and clicks are unchanged; this is only motion. | |
| let lastMap = null; | |
| const reduceMotion = matchMedia("(prefers-reduced-motion: reduce)"); | |
| const leafKey = (d) => d.data.dom + "|" + d.data.name; | |
| function animateMap(svg, g, h, W, H) { | |
| const now = { W, H, leaves: new Map(), doms: new Map() }; | |
| g.each((d) => now.leaves.set(leafKey(d), [d.x0, d.y0, d.x1 - d.x0, d.y1 - d.y0])); | |
| (h.children || []).forEach((c) => now.doms.set(c.data.dom, [c.x0, c.y0, c.x1 - c.x0, c.y1 - c.y0])); | |
| const prev = lastMap; | |
| lastMap = now; | |
| if (!prev || prev.W !== W || prev.H !== H || reduceMotion.matches) return; // first paint or a resize: no motion | |
| const T = d3.transition().duration(560).ease(d3.easeCubicInOut); | |
| g.each(function (d) { | |
| const to = [d.x0, d.y0, d.x1 - d.x0, d.y1 - d.y0]; | |
| const from = prev.leaves.get(leafKey(d)) || prev.doms.get(d.data.dom) || [to[0] + to[2] / 2, to[1] + to[3] / 2, 0, 0]; | |
| if (from.every((v, i) => Math.abs(v - to[i]) < 0.5)) return; | |
| const sel = d3.select(this); | |
| sel.attr("transform", `translate(${from[0]},${from[1]})`).transition(T).attr("transform", `translate(${to[0]},${to[1]})`); | |
| sel.select("rect").attr("width", from[2]).attr("height", from[3]).transition(T).attr("width", to[2]).attr("height", to[3]); | |
| sel.selectAll("text").attr("opacity", 0).transition(T).delay(260).duration(260).attr("opacity", 1); | |
| }); | |
| svg.selectAll("g.dh").attr("opacity", 0).transition(T).delay(200).attr("opacity", 1); | |
| } | |
| function clickCell(d) { | |
| tip(); | |
| const dom = DOMS[d.dom]; | |
| if (state.dom !== d.dom) { state.dom = d.dom; state.sel = {}; } | |
| if (!d.other) { | |
| const set = (state.sel[dom.main] ||= new Set()); | |
| set.has(d.name) ? set.delete(d.name) : set.add(d.name); | |
| if (!set.size) delete state.sel[dom.main]; | |
| } | |
| run(); | |
| // let the drill-down play, then bring the matching environments into view | |
| setTimeout(() => $("#toolbar", root)?.scrollIntoView({ behavior: "smooth", block: "start" }), reduceMotion.matches ? 0 : 620); | |
| } | |
| function tip(ev, html) { | |
| const el = $("#tip"); | |
| if (!ev) { el.hidden = true; return; } | |
| el.innerHTML = html; el.hidden = false; | |
| el.style.left = Math.min(ev.clientX + 14, innerWidth - el.offsetWidth - 8) + "px"; | |
| el.style.top = ev.clientY + 16 + "px"; | |
| } | |
| // ββ list βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function terms() { | |
| if (!state.q) return []; | |
| if (CJK.test(state.q)) return [state.q.trim()]; | |
| return state.q.toLowerCase().split(/[^\p{L}\p{N}_+#.-]+/u).filter((t) => t.length > 1); | |
| } | |
| function highlight(text) { | |
| const html = esc(text), ts = terms(); | |
| if (!ts.length) return html; | |
| const re = new RegExp("(" + ts.map((t) => esc(t).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|") + ")", "gi"); | |
| return html.replace(re, "<mark>$1</mark>"); | |
| } | |
| function card(e) { | |
| const d = DOMS[e.d]; | |
| const chips = d.facets.filter(([k]) => k !== d.main && k !== "source") | |
| .flatMap(([k]) => vals(e.f[k]).filter((v) => !LEFTOVER.test(v)).slice(0, 2)).slice(0, 4); | |
| const stats = e.n ? [[`${e.n.systems}`, "systems"], [`${e.n.files}`, "files"], [`${e.n.checks}`, "checks"]] : []; | |
| return `<li><a class="card" href="#/task/${encodeURIComponent(e.id)}" style="--dc:var(--c-${e.d})"> | |
| <div class="row1"><span class="dn">${icon(DOMAIN_ICON[e.d], 13)}${esc(d.name)}</span><span class="faint">/</span><span class="cat">${esc(vals(e.f[d.main]).join(", "))}</span></div> | |
| <p class="t">${highlight(e.t)}</p><p class="s">${highlight(e.s)}</p> | |
| ${chips.length || stats.length ? `<div class="chips">${chips.map((c) => `<span class="chip">${esc(c)}</span>`).join("")}${stats.map(([n, l]) => `<span class="chip outline"><b>${n}</b> ${l}</span>`).join("")}</div>` : ""} | |
| </a></li>`; | |
| } | |
| function renderList(reset) { | |
| const list = $("#list", root); | |
| if (reset) { list.innerHTML = ""; shown = 0; autoLoaded = 0; } | |
| const next = matches.slice(shown, shown + PAGE); | |
| list.insertAdjacentHTML("beforeend", next.map(card).join("")); | |
| shown += next.length; | |
| $("#empty", root).hidden = matches.length > 0; | |
| $("#list", root).hidden = !matches.length; | |
| const left = matches.length - shown; | |
| $("#more-row", root).hidden = left <= 0 || autoLoaded < AUTO_PAGES; | |
| $("#more-count", root).textContent = `Showing ${fmt.format(shown)} of ${fmt.format(matches.length)}`; | |
| $("#more", root).textContent = `Load ${fmt.format(Math.min(PAGE, left))} more`; | |
| } | |
| function wire(el) { | |
| let t; | |
| $("#q", el).addEventListener("input", (ev) => { clearTimeout(t); t = setTimeout(() => { state.q = ev.target.value; run(); }, 120); }); | |
| $("#domains", el).addEventListener("click", (ev) => { const b = ev.target.closest(".dom"); if (b) setDomain(b.dataset.dom || null); }); | |
| $("#facets", el).addEventListener("click", (ev) => { | |
| const o = ev.target.closest(".opt"); | |
| if (o) return toggle(o.dataset.k, o.dataset.v); | |
| const m = ev.target.closest("[data-more]"); | |
| if (m) { expanded.has(m.dataset.more) ? expanded.delete(m.dataset.more) : expanded.add(m.dataset.more); renderFacets(); } | |
| }); | |
| $("#active", el).addEventListener("click", (ev) => { const p = ev.target.closest(".pill"); if (p) toggle(p.dataset.k, p.dataset.v); }); | |
| const clearAll = () => { state.sel = {}; state.q = ""; $("#q", el).value = ""; state.dom = null; run(); }; | |
| $("#clear", el).addEventListener("click", clearAll); | |
| $("#clear2", el).addEventListener("click", clearAll); | |
| const sheet = (on) => { | |
| $("#filters", el).classList.toggle("open", on); | |
| $("#sheet-scrim", el).hidden = !on; | |
| document.body.classList.toggle("noscroll", on); | |
| }; | |
| $("#close-filters", el).addEventListener("click", () => sheet(false)); | |
| $("#show-results", el).addEventListener("click", () => { sheet(false); $("#toolbar", el).scrollIntoView({ block: "start" }); }); | |
| $("#sheet-scrim", el).addEventListener("click", () => sheet(false)); | |
| $("#clear3", el).addEventListener("click", () => { state.sel = {}; run(); }); | |
| el.addEventListener("keydown", (e) => { if (e.key === "Escape" && $("#filters", el).classList.contains("open")) sheet(false); }); | |
| $("#facets", el).addEventListener("toggle", (e) => { const d = e.target.closest?.("[data-facet]"); if (d) facetOpen.set(d.dataset.facet, d.open); }, true); | |
| if (phone()) $("#q", el).placeholder = "Search environmentsβ¦"; | |
| $("#map-card", el).addEventListener("toggle", () => renderMap()); | |
| $("#surprise", el).addEventListener("click", () => { | |
| const pool = matches.length ? matches : ENVS; | |
| location.hash = "#/task/" + encodeURIComponent(pool[Math.floor(Math.random() * pool.length)].id); | |
| }); | |
| $("#open-filters", el).addEventListener("click", () => sheet(true)); | |
| observer = new IntersectionObserver((es) => { | |
| if (es.some((x) => x.isIntersecting) && shown < matches.length && autoLoaded < AUTO_PAGES) { autoLoaded++; renderList(false); } | |
| }, { rootMargin: "600px" }); | |
| $("#more", el).addEventListener("click", () => renderList(false)); | |
| observer.observe($("#sentinel", el)); | |
| } | |
| export function focusSearch() { const q = root && $("#q", root); if (q) q.focus(); } | |