// Explore: FineEnvs at the top with the numbers that matter, the trending environments in a row you slide
// through, then everything: Harbor task datasets and OpenEnv Spaces, by collection, searchable and filterable.
// The server filters, counts and sorts (/api/search, over the catalog snapshot: app/search_api.py); the page asks
// for one page of cards at a time, and the first request also brings the header numbers and the trending row.
import { $, api, esc, fmt, ago, sk, progress, emptyState, toast, openDialog, closeModal, storage, nav, setMeta, ownerLink } from "./util.js";
import { icon } from "./icons.js";
import { myDatasets } from "./data.js";
import { countUp } from "./count-up.js";
const PAGE = 40, TRENDING = 24;
const compact = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 });
const enc = (s) => s.split("/").map(encodeURIComponent).join("/");
const href = (d) => `/${d.kind === "space" ? "s" : "d"}/${enc(d.id)}`;
const SORTS = [["trending", "Hub interest, then likes and downloads"], ["downloads", "most downloaded first"], ["likes", "most liked first"],
["tasks", "most tasks first"], ["rollouts", "most rollouts first"], ["updated", "recently updated first"], ["new", "newest first"]];
const time = (iso) => (iso ? Date.parse(iso) : 0);
// what kind of environment each is: Harbor task folders, an OpenEnv (or other) Space, or rows a processor reads
// Spaces: API-checked OpenEnv servers, unverified discovery candidates, other environment servers; datasets: by the Hub's
// framework tags, Harbor whenever its index found task folders (the server works it out: `fw` on each card)
const KINDS = [["all", "Everything", "All"], ["harbor", "Harbor datasets", "Harbor"], ["openenv", "OpenEnv · API checked", "OpenEnv"], ["unverified-space", "Unverified Space candidates", "Unverified Spaces"],
["other-space", "Other environment Spaces", "Other Spaces"], ["verifiers", "Verifiers", "Verifiers"],
["nemo-gym", "NeMo Gym", "NeMo Gym"], ["openenv-data", "OpenEnv datasets", "OpenEnv data"], ["verl", "verl and SkyRL", "verl"],
["rows", "Other RL datasets", "Other"]];
// ORS servers are few (one, so far): they sit with the other environment Spaces, their cards still say ORS
const fw = (d) => d.fw || (d.kind === "space" ? (d.framework === "openenv" || d.openenv ? "openenv" : "other-space") : d.framework || "harbor");
const FW = { harbor: ["database", "Harbor", "dataset", "A dataset of tasks in Harbor's format"], verifiers: ["check", "Verifiers", "dataset", "Data for a Verifiers environment"],
"nemo-gym": ["cpu", "NeMo Gym", "dataset", "A NeMo Gym environment's data"], "openenv-data": ["table", "OpenEnv", "dataset", "Data for an OpenEnv environment"],
verl: ["table", "verl", "dataset", "RL rows in verl's format"], rows: ["table", "RL", "dataset", "A dataset of RL tasks, read row by row"] };
const OTHER = "Not in a collection";
const SIZE_ORDER = ["Under 100", "100 to 1,000", "1,000 or more", "Not indexed yet"];
const MINE = { id: "mine", group: "Your datasets", icon: "user", color: null, about: "yours and your organizations', private ones too" };
const FACETS = [["health", "OpenEnv API availability"], ["mode", "OpenEnv interface"], ["oe", "OpenEnv version (declared)"],
["oe_source", "Version evidence"], ["tools", "MCP tool discovery"], ["type", "Labelled on the Hub as"], ["size", "Tasks"],
["stage", "Space status at snapshot"], ["mcp", "MCP metadata"], ["evidence", "OpenEnv evidence"], ["tags", "Tags"]];
const FACET_HELP = {
health: "API checked means running with healthy OpenEnv endpoints checked within the last hour. Episodes and rewards are not tested.",
mode: "Simulation exposes reset, step and state. MCP service exposes tools without episode control routes.",
oe: "Repository dependency or Hub tag. This does not confirm the installed runtime version. Ranges and Git references are kept as declared.",
tools: "Tools returned by the server within the last hour. Discovery does not test tool execution.",
};
const versionGroup = (v) => /^\d/.test(v) ? 0 : /^[<>=!~]/.test(v) ? 1 : v.startsWith("git:") ? 2 : v === "Unknown" ? 4 : 3;
let root = null, COLLECTIONS = [], COLL = {}, STATS = null, TREND = null, FEATURED = [], PROMOTED = [], SNAPSHOT = null, RANKING = null, MINE_N = 0;
const state = { q: "", owner: null, coll: null, kind: "all", sel: {}, sort: "trending", mine: false, candidates: false };
let res = null, rows = [], page = 1, seq = 0, ctrl = null, typing = null;
let refreshTimer = null, refreshing = false, lastAnswer = 0;
const known = new Map(); // every card seen, by key: what the quick look opens
const expanded = new Set(), facetOpen = new Map();
// ── the API ──────────────────────────────────────────────────────────────────
const cache = new Map(); // recent answers by URL: back to Explore renders at once
async function get(url, signal) {
const hit = cache.get(url);
if (hit && Date.now() - hit.at < 30000) return hit.d;
const d = await api(url, { signal });
cache.set(url, { at: Date.now(), d });
if (cache.size > 40) cache.delete(cache.keys().next().value);
return d;
}
function fString() {
return Object.entries(state.sel).filter(([, s]) => s.size).map(([k, s]) => k + ":" + [...s].map(encodeURIComponent).join("|")).join(";");
}
function query({ page: pg = 1, facets = true, trending = 0 } = {}) {
const p = new URLSearchParams();
p.set("scope", state.candidates || state.kind === "unverified-space" ? "all" : "ready");
if (state.q) p.set("q", state.q);
if (state.owner) p.set("owner", state.owner);
if (state.kind !== "all") p.set("kind", state.kind);
if (state.coll) p.set("collection", state.coll);
if (state.sort !== "trending") p.set("sort", state.sort);
const f = fString();
if (f) p.set("f", f);
if (pg > 1) p.set("page", pg);
if (!facets) p.set("facets", "0");
if (trending) p.set("trending", trending);
if (state.mine) p.set("mine", "1");
return `/api/search?${p}`;
}
function remember(list) { (list || []).forEach((d) => known.set(d.key, d)); }
function apply(d) {
if (d.collections) {
COLLECTIONS = d.collections;
COLL = Object.fromEntries(COLLECTIONS.map((c) => [c.id, c]));
if (MINE_N) COLL.mine = { ...MINE, ids: [] };
}
if (d.stats) STATS = d.stats;
if (d.snapshot) SNAPSHOT = d.snapshot;
if (d.ranking) RANKING = d.ranking;
if (d.featured) FEATURED = d.featured;
if (d.promoted) { PROMOTED = d.promoted; remember(PROMOTED); }
if (d.trending) { TREND = d.trending; remember(TREND); }
remember(d.rows);
// what the server applied: an unknown collection is no filter (but "Yours" waits for the visitor's datasets)
if (d.query && !(state.coll === "mine" && !MINE_N)) state.coll = d.query.collection;
if (d.query) state.owner = d.query.owner || null;
if (d.query?.f) state.sel = Object.fromEntries(Object.entries(d.query.f).map(([k, v]) => [k, new Set(v)]));
}
const color = (id) => (COLL[id]?.color ? `var(--c-${COLL[id].color})` : "var(--text)");
// the page's own containers with placeholders in them, so nothing moves when the data arrives
export function skeleton() {
const trCard = () => `
All four counts follow your current search, organization and filters.
By default, datasets and running Spaces with a recently checked environment API are shown. Offline and unverified candidates are available in Filters.
Checks run automatically; OpenEnv labels require passing OpenEnv API checks within the last hour. An API check does not certify episode or reward quality.
${fmt.format(numbers.tasks)} tasks indexed are dataset records loaded into this explorer's index.
${fmt.format(numbers.reported)} Space task entries are reported by ${fmt.format(coverage.spaces || 0)} running Spaces within the last hour; individual task records are fetched when you open them.
These are entries across repositories and splits, not unique problems: forks, conversions and shared datasets can overlap. The two task counts can also overlap and are not added together.
${coverage.partial_spaces ? `${fmt.format(coverage.partial_spaces)} Space catalogs are partial; only known split counts are included.` : ""}
Spaces without a recent task count are uncounted, not assumed empty. Space totals describe their Task API catalogs; their task bodies are loaded on demand.
${coverage.oldest_check ? `Oldest included Space count: ${esc(new Date(coverage.oldest_check * 1000).toLocaleString())}.` : "No recent Space counts yet."}
Repository names, tags and an openenv.yaml filename are discovery hints only. Versions describe repository dependencies, not a confirmed installed runtime.
Automatic indexing refreshes public repositories and supported dataset indexes in batches; full task records from row datasets and Spaces are loaded on demand.
${esc(RANKING?.description || "Hub trending score, then total likes and monthly dataset downloads.")} ${esc(RANKING?.quality || "")}
Counts cover the public, tagged environment catalog and curated repositories, not a census of all Hub Spaces.
${at ? `Catalog snapshot: . Runtime status and metrics can change.` : "Snapshot time unavailable."}
`;
}
// ── trending: a grid of cards, a page at a time ──────────────────────────────
let trPage = 0;
function trendCard(d, i = null) {
const [org, name] = d.id.split("/");
const c = COLL[d.collection];
const space = d.kind === "space";
const nums = space
? [[icon("heart", 12), compact.format(d.likes), "likes"], ...(d.mcp ? [[icon("plug", 12), "MCP", "tagged for MCP"]] : [])]
: [[icon("download", 12), compact.format(d.downloads), "downloads last month"], [icon("heart", 12), compact.format(d.likes), "likes"],
...(d.indexed?.tasks ? [[icon("list", 12), compact.format(d.indexed.tasks), "tasks"]] : [])];
nums.unshift([icon("flame", 12), compact.format(d.trending || 0), "Hub trending score; 0 means ordering falls back to likes and dataset downloads"]);
const kind = kindTag(d, true);
return `