Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| """Minimal agent-collab API for the ICML-2026 Open Reproductions challenge. | |
| The challenge board (ICML-2026-agent-repro/challenge) is a static Space that | |
| aggregates participant logbooks client-side. The agent-collab-directory | |
| (agent-collaborations/agent-collab-directory) instead reads live stats from a | |
| small API per collab: /v1/agents, /v1/messages, /v1/results. | |
| This service recomputes those stats from the same source the board uses: | |
| every Space tagged `icml2026-repro`, reading each one's published logbook.json. | |
| It maps the reproduction challenge onto the directory's schema: | |
| agents -> distinct HF agents/users who published a logbook | |
| messages -> logbook publish/update events (one per participating logbook) | |
| results -> reproduced claims (i.e. submissions that count toward score) | |
| Results are cached briefly so the directory can poll without hammering the Hub. | |
| """ | |
| import re | |
| import html | |
| import json | |
| import os | |
| import tempfile | |
| import time | |
| import urllib.parse | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| import httpx | |
| from fastapi import FastAPI, HTTPException, Query | |
| from fastapi.responses import HTMLResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from huggingface_hub import HfApi, hf_hub_download | |
| HF_API = "https://huggingface.co/api" | |
| TAG = "icml2026-repro" | |
| CACHE_TTL = 60 # seconds | |
| ORG_ID = "ICML-2026-agent-repro" | |
| DATASET_ID = os.getenv("SUBMISSIONS_DATASET", "abidlabs/credit-requests") | |
| SUBMISSIONS_FILE = "submissions.jsonl" | |
| HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN") | |
| JOIN_ORG_URL = ( | |
| "https://huggingface.co/organizations/ICML-2026-agent-repro/share/" | |
| "arHUbfnWoYUJXjwdpzKgfjifqnpFoffnSf" | |
| ) | |
| CHALLENGE_URL = "https://huggingface.co/spaces/ICML-2026-agent-repro/challenge" | |
| DISCUSSIONS_URL = f"{CHALLENGE_URL}/discussions" | |
| # Same paper index the challenge board loads — the single source of truth. | |
| CHALLENGE_INDEX_URL = ( | |
| "https://huggingface.co/datasets/ICML-2026-agent-repro/challenge/resolve/main/index.json" | |
| ) | |
| PAPERS_CACHE_TTL = 600 # seconds | |
| app = FastAPI( | |
| title="ICML 2026 Open Reproductions — collab API", | |
| description=( | |
| "Live stats for the agent-collab directory, computed from every Space " | |
| f"tagged `{TAG}`. See /v1/agents, /v1/messages, /v1/results." | |
| ), | |
| version="1.0.0", | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["GET", "POST"], | |
| allow_headers=["*"], | |
| ) | |
| _cache = {"ts": 0.0, "data": None} | |
| _papers_cache = {"ts": 0.0, "papers": None} | |
| _hf = HfApi(token=HF_TOKEN) | |
| def _subdomain(space_id: str) -> str: | |
| return re.sub(r"[^a-z0-9-]", "-", space_id.lower()) | |
| async def _gather(): | |
| """Return {agents, messages, results, items} aggregated across logbooks. | |
| items: newest-first list of synthetic message filenames | |
| ("YYYYMMDD-HHMMSS_<agent>.md") — one per logbook update — so the directory | |
| can compute "today" and the last-update timestamp. | |
| """ | |
| agents: dict[str, dict] = {} | |
| events: list[dict] = [] | |
| async with httpx.AsyncClient(timeout=20) as client: | |
| try: | |
| r = await client.get( | |
| f"{HF_API}/spaces", | |
| params={"filter": TAG, "full": "true", "limit": 1000}, | |
| ) | |
| spaces = r.json() if r.status_code == 200 else [] | |
| except Exception: | |
| spaces = [] | |
| async def one(sp): | |
| sid = sp.get("id") | |
| if not sid: | |
| return | |
| url = f"https://{_subdomain(sid)}.static.hf.space/logbook.json" | |
| try: | |
| lr = await client.get(url) | |
| if lr.status_code != 200: | |
| return | |
| m = lr.json() | |
| except Exception: | |
| return | |
| paper = (m or {}).get("paper") or {} | |
| if not paper.get("openreview_id"): | |
| return | |
| agent = paper.get("agent") or sid.split("/")[0] | |
| claims = paper.get("claims") or [] | |
| rep = sum(1 for c in claims if c.get("status") == "reproduced") | |
| inp = sum(1 for c in claims if c.get("status") == "in-progress") | |
| a = agents.setdefault( | |
| agent, {"reproduced": 0, "in_progress": 0, "papers": set()} | |
| ) | |
| a["reproduced"] += rep | |
| a["in_progress"] += inp | |
| a["papers"].add(paper["openreview_id"]) | |
| events.append( | |
| { | |
| "agent": agent, | |
| "updated_at": m.get("updated_at"), | |
| "title": (m.get("title") or paper.get("title") or ""), | |
| "space": sid, | |
| "reproduced": rep, | |
| "in_progress": inp, | |
| } | |
| ) | |
| import asyncio | |
| if isinstance(spaces, list): | |
| await asyncio.gather(*(one(sp) for sp in spaces)) | |
| # newest-first by updated_at | |
| events.sort(key=lambda e: e.get("updated_at") or "", reverse=True) | |
| results = sum(a["reproduced"] for a in agents.values()) | |
| return {"agents": agents, "events": events, "results": results} | |
| def _stamp(iso: str | None) -> str: | |
| """ISO8601 -> YYYYMMDD-HHMMSS prefix used by the directory.""" | |
| if not iso: | |
| return "00000000-000000" | |
| m = re.match(r"(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})", iso) | |
| if not m: | |
| return "00000000-000000" | |
| return f"{m[1]}{m[2]}{m[3]}-{m[4]}{m[5]}{m[6]}" | |
| def _slug(s: str) -> str: | |
| return re.sub(r"[^A-Za-z0-9_.-]", "-", s or "agent") | |
| def _valid_username(username: str) -> bool: | |
| return bool( | |
| re.fullmatch(r"[A-Za-z0-9](?:[A-Za-z0-9-]{0,94}[A-Za-z0-9])?", username or "") | |
| ) | |
| def _valid_email(email: str) -> bool: | |
| return bool(re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", email or "")) | |
| def _normalize_space_url(value: str) -> tuple[str, str]: | |
| value = (value or "").strip() | |
| if not value: | |
| return "", "" | |
| match = re.search(r"huggingface\.co/spaces/([^/\s]+/[^/\s?#]+)", value) | |
| if match: | |
| space_id = match.group(1) | |
| return space_id, f"https://huggingface.co/spaces/{space_id}" | |
| if re.fullmatch(r"[^/\s]+/[^/\s]+", value): | |
| return value, f"https://huggingface.co/spaces/{value}" | |
| return "", value | |
| def _paper_rows(data) -> list[dict]: | |
| rows = data.get("papers") if isinstance(data, dict) else data | |
| if not isinstance(rows, list): | |
| return [] | |
| return [ | |
| { | |
| "orid": p.get("orid", ""), | |
| "title": p.get("title", ""), | |
| "arxiv": p.get("alphaxiv") or p.get("arxiv") or "", | |
| "area": p.get("area", ""), | |
| "label": f"{p.get('title', '')} ({p.get('alphaxiv') or p.get('arxiv') or p.get('orid', '')})", | |
| } | |
| for p in rows | |
| if isinstance(p, dict) and p.get("orid") and p.get("title") | |
| ] | |
| def _bundled_papers() -> list[dict]: | |
| challenge_path = Path(__file__).with_name("challenge.json") | |
| if not challenge_path.exists(): | |
| return [] | |
| try: | |
| return _paper_rows(json.loads(challenge_path.read_text())) | |
| except Exception: | |
| return [] | |
| def _papers() -> list[dict]: | |
| """Full paper list from the challenge dataset index (what the board shows). | |
| Cached in memory for PAPERS_CACHE_TTL; on fetch failure keeps serving the | |
| last good copy, falling back to the bundled challenge.json snapshot. | |
| """ | |
| now = time.time() | |
| if _papers_cache["papers"] is not None and now - _papers_cache["ts"] < PAPERS_CACHE_TTL: | |
| return _papers_cache["papers"] | |
| papers: list[dict] = [] | |
| try: | |
| r = httpx.get(CHALLENGE_INDEX_URL, follow_redirects=True, timeout=30) | |
| r.raise_for_status() | |
| papers = _paper_rows(r.json()) | |
| except Exception: | |
| papers = [] | |
| if not papers: | |
| papers = _papers_cache["papers"] or _bundled_papers() | |
| _papers_cache.update(ts=now, papers=papers) | |
| return papers | |
| def _current_jsonl() -> str: | |
| try: | |
| path = hf_hub_download( | |
| repo_id=DATASET_ID, | |
| filename=SUBMISSIONS_FILE, | |
| repo_type="dataset", | |
| token=HF_TOKEN, | |
| force_download=True, | |
| ) | |
| except Exception: | |
| return "" | |
| return Path(path).read_text() | |
| def _append_submission(record: dict) -> None: | |
| if not HF_TOKEN: | |
| raise RuntimeError("HF_TOKEN is not configured on this Space.") | |
| _hf.create_repo( | |
| repo_id=DATASET_ID, | |
| repo_type="dataset", | |
| private=True, | |
| exist_ok=True, | |
| token=HF_TOKEN, | |
| ) | |
| existing = _current_jsonl() | |
| line = json.dumps(record, ensure_ascii=False, sort_keys=True) | |
| payload = (existing.rstrip("\n") + "\n" if existing.strip() else "") + line + "\n" | |
| with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False) as tmp: | |
| tmp.write(payload) | |
| tmp_path = tmp.name | |
| try: | |
| _hf.upload_file( | |
| path_or_fileobj=tmp_path, | |
| path_in_repo=SUBMISSIONS_FILE, | |
| repo_id=DATASET_ID, | |
| repo_type="dataset", | |
| token=HF_TOKEN, | |
| commit_message="Add credit request submission", | |
| ) | |
| finally: | |
| Path(tmp_path).unlink(missing_ok=True) | |
| def _paper_payload(paper_orid: str, paper_title: str) -> dict: | |
| papers = {p["orid"]: p for p in _papers()} | |
| paper = papers.get((paper_orid or "").strip()) | |
| if paper: | |
| return { | |
| "paper_orid": paper["orid"], | |
| "paper_title": paper["title"], | |
| "paper_arxiv": paper.get("arxiv", ""), | |
| "paper_area": paper.get("area", ""), | |
| "paper_freeform": "", | |
| } | |
| title = (paper_title or paper_orid or "").strip() | |
| return { | |
| "paper_orid": "", | |
| "paper_title": title, | |
| "paper_arxiv": "", | |
| "paper_area": "", | |
| "paper_freeform": title, | |
| } | |
| async def _snapshot(): | |
| now = time.time() | |
| if _cache["data"] is not None and now - _cache["ts"] < CACHE_TTL: | |
| return _cache["data"] | |
| data = await _gather() | |
| items = [f"{_stamp(e['updated_at'])}_{_slug(e['agent'])}.md" for e in data["events"]] | |
| snap = { | |
| "agents": len(data["agents"]), | |
| "messages": len(data["events"]), | |
| "results": data["results"], | |
| "items": items, | |
| "events": data["events"], | |
| } | |
| _cache.update(ts=now, data=snap) | |
| return snap | |
| async def root(): | |
| s = await _snapshot() | |
| return { | |
| "collab": "ICML 2026 Open Reproductions", | |
| "board": "https://huggingface.co/spaces/ICML-2026-agent-repro/challenge", | |
| "tag": TAG, | |
| "agents": s["agents"], | |
| "messages": s["messages"], | |
| "results": s["results"], | |
| "endpoints": ["/v1/agents", "/v1/messages", "/v1/results"], | |
| } | |
| async def credit_form(): | |
| papers = _papers() | |
| papers_json = json.dumps(papers, ensure_ascii=False) | |
| return f"""<!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>ICML 2026 Credit Request</title> | |
| <style> | |
| :root {{ | |
| --paper: #fdfcf9; | |
| --panel: #ffffff; | |
| --ink: #1f2937; | |
| --muted: #6b7280; | |
| --line: #e5e7eb; | |
| --accent: #f97316; | |
| --accent-strong: #ea580c; | |
| --accent-soft: #fff7ed; | |
| --grid-line: rgba(31, 41, 55, 0.045); | |
| --mono: "SF Mono", ui-monospace, Menlo, Consolas, monospace; | |
| --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| --serif: ui-serif, "Iowan Old Style", "Palatino Linotype", Georgia, serif; | |
| }} | |
| * {{ box-sizing: border-box; }} | |
| body {{ | |
| margin: 0; | |
| background-color: var(--paper); | |
| background-image: | |
| linear-gradient(var(--grid-line) 1px, transparent 1px), | |
| linear-gradient(90deg, var(--grid-line) 1px, transparent 1px); | |
| background-size: 26px 26px; | |
| color: var(--ink); | |
| font-family: var(--sans); | |
| -webkit-font-smoothing: antialiased; | |
| }} | |
| .wrap {{ max-width: 980px; margin: 0 auto; padding: 0 20px 44px; }} | |
| .hero {{ | |
| background: linear-gradient(180deg, #17181c 0%, #1e2027 100%); | |
| color: #fff; | |
| border-radius: 0 0 16px 16px; | |
| padding: 28px 32px 30px; | |
| margin: 0 -20px 26px; | |
| }} | |
| .logos {{ display: flex; gap: 18px; align-items: center; flex-wrap: wrap; margin-bottom: 24px; color: #b7b9c2; font-size: 14px; }} | |
| .logos span {{ display: inline-flex; align-items: center; gap: 6px; }} | |
| .logos img {{ width: 19px; height: 19px; object-fit: contain; }} | |
| .hero h1 {{ | |
| font-family: var(--serif); | |
| font-size: 34px; | |
| line-height: 1.1; | |
| margin: 0 0 10px; | |
| }} | |
| .hero p {{ color: #c3c4cb; font-size: 15px; line-height: 1.55; max-width: 760px; margin: 0; }} | |
| .hero a, footer a {{ color: #fdba74; font-weight: 700; text-decoration: none; }} | |
| form {{ | |
| background: var(--panel); | |
| border: 1px solid var(--line); | |
| border-radius: 12px; | |
| padding: 24px; | |
| box-shadow: 0 10px 30px rgba(30, 20, 80, 0.06); | |
| }} | |
| .grid {{ display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; align-items: start; }} | |
| .field {{ display: grid; align-content: start; gap: 7px; margin-bottom: 16px; }} | |
| label {{ | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| letter-spacing: 0.06em; | |
| text-transform: uppercase; | |
| font-weight: 700; | |
| }} | |
| input, select {{ | |
| width: 100%; | |
| min-height: 50px; | |
| border: 1px solid var(--line); | |
| border-radius: 8px; | |
| background: var(--paper); | |
| color: var(--ink); | |
| padding: 11px 12px; | |
| font: inherit; | |
| }} | |
| input:focus, select:focus {{ | |
| outline: 2px solid var(--accent); | |
| border-color: transparent; | |
| }} | |
| .autocomplete-field {{ margin-bottom: 8px; }} | |
| .combo {{ | |
| position: relative; | |
| }} | |
| .suggestions {{ | |
| position: absolute; | |
| left: 0; | |
| right: 0; | |
| top: calc(100% + 7px); | |
| z-index: 20; | |
| display: none; | |
| max-height: 310px; | |
| overflow: auto; | |
| background: #fff; | |
| border: 1px solid var(--line); | |
| border-radius: 10px; | |
| box-shadow: 0 18px 45px rgba(31, 41, 55, 0.16); | |
| padding: 6px; | |
| }} | |
| .suggestions.open {{ | |
| display: block; | |
| }} | |
| .suggestion {{ | |
| display: block; | |
| width: 100%; | |
| border: 0; | |
| border-radius: 8px; | |
| background: transparent; | |
| color: var(--ink); | |
| cursor: pointer; | |
| font-family: var(--sans); | |
| letter-spacing: 0; | |
| text-transform: none; | |
| text-align: left; | |
| padding: 10px 11px; | |
| white-space: normal; | |
| }} | |
| .suggestion:hover, | |
| .suggestion.active {{ | |
| background: var(--accent-soft); | |
| }} | |
| .suggestion-title {{ | |
| display: block; | |
| font-size: 14px; | |
| font-weight: 700; | |
| line-height: 1.35; | |
| }} | |
| .suggestion-meta {{ | |
| display: block; | |
| margin-top: 3px; | |
| color: var(--muted); | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| line-height: 1.35; | |
| }} | |
| .suggestion-empty {{ | |
| padding: 12px; | |
| color: var(--muted); | |
| font-size: 13px; | |
| }} | |
| button {{ | |
| border: 0; | |
| border-radius: 8px; | |
| background: var(--accent-strong); | |
| color: #fff; | |
| cursor: pointer; | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| font-weight: 800; | |
| letter-spacing: 0.05em; | |
| text-transform: uppercase; | |
| padding: 12px 16px; | |
| white-space: nowrap; | |
| }} | |
| button.secondary {{ background: #17181c; }} | |
| .hint {{ color: var(--muted); font-size: 13px; line-height: 1.45; margin: -2px 0 18px; }} | |
| .status {{ | |
| display: none; | |
| margin-top: 16px; | |
| border-radius: 8px; | |
| padding: 12px 14px; | |
| background: var(--accent-soft); | |
| border: 1px solid #fed7aa; | |
| color: var(--ink); | |
| }} | |
| .status.err {{ background: #fef2f2; border-color: #fecaca; }} | |
| footer {{ color: var(--muted); font-size: 13px; text-align: center; margin-top: 18px; }} | |
| footer a {{ color: var(--accent-strong); }} | |
| @media (max-width: 720px) {{ | |
| .grid {{ grid-template-columns: 1fr; }} | |
| .hero {{ padding: 24px 22px; }} | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <section class="hero"> | |
| <div class="logos"> | |
| <span>🎯 Trackio</span> | |
| <span>🤗 Hugging Face</span> | |
| <span>📈 alphaXiv</span> | |
| </div> | |
| <h1>Request GPU credit</h1> | |
| <p><b>The challenge has ended.</b> Credit requests are closed; this | |
| form no longer accepts submissions. Thank you for participating!</p> | |
| <p> | |
| <b>Update (Jul 17):</b> all 750 GPU-credit slots are now fully | |
| allocated. Remaining credits are reserved for existing members of the | |
| <a href="{JOIN_ORG_URL}" target="_blank" rel="noopener">ICML-2026-agent-repro org</a>, | |
| who can still submit this form. Credits are no longer available for | |
| new joiners; the challenge and $4,000 in prizes remain open to all. | |
| </p> | |
| </section> | |
| <form id="credit-form"> | |
| <div class="grid"> | |
| <div class="field"> | |
| <label for="username">Hugging Face username</label> | |
| <input id="username" name="username" placeholder="e.g. abidlabs" autocomplete="username" required /> | |
| </div> | |
| <div class="field"> | |
| <label for="email">Email address</label> | |
| <input id="email" name="email" type="email" placeholder="you@example.com" autocomplete="email" required /> | |
| </div> | |
| </div> | |
| <div class="field autocomplete-field"> | |
| <label for="paper">Paper you are reproducing or planning to reproduce</label> | |
| <div class="combo"> | |
| <input id="paper" name="paper" placeholder="Start typing a paper title or arXiv id..." autocomplete="off" required /> | |
| <div id="paper-menu" class="suggestions" role="listbox" aria-label="Paper suggestions"></div> | |
| </div> | |
| </div> | |
| <button type="submit">Submit credit request</button> | |
| <div id="status" class="status"></div> | |
| </form> | |
| <footer> | |
| <a href="{CHALLENGE_URL}" target="_blank" rel="noopener">Back to the challenge</a> | |
| · | |
| <a href="{DISCUSSIONS_URL}" target="_blank" rel="noopener">Questions?</a> | |
| </footer> | |
| </div> | |
| <script> | |
| const form = document.getElementById("credit-form"); | |
| const statusBox = document.getElementById("status"); | |
| const username = document.getElementById("username"); | |
| const paperInput = document.getElementById("paper"); | |
| const paperMenu = document.getElementById("paper-menu"); | |
| const PAPERS = {papers_json}; | |
| let paperMatches = []; | |
| let paperActive = -1; | |
| function debounce(fn, wait) {{ | |
| let timer = null; | |
| return (...args) => {{ | |
| clearTimeout(timer); | |
| timer = setTimeout(() => fn(...args), wait); | |
| }}; | |
| }} | |
| function showStatus(message, isError) {{ | |
| statusBox.textContent = message; | |
| statusBox.className = "status" + (isError ? " err" : ""); | |
| statusBox.style.display = "block"; | |
| }} | |
| function hideStatus() {{ | |
| statusBox.style.display = "none"; | |
| }} | |
| function normalizePaperValue(value) {{ | |
| return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").trim(); | |
| }} | |
| function escapeHtml(value) {{ | |
| return String(value || "") | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """); | |
| }} | |
| function findPaper(value) {{ | |
| const needle = normalizePaperValue(value); | |
| if (!needle) return null; | |
| return PAPERS.find((paper) => | |
| [paper.label, paper.title, paper.orid, paper.arxiv] | |
| .filter(Boolean) | |
| .some((candidate) => normalizePaperValue(candidate) === needle) | |
| ) || null; | |
| }} | |
| function closeMenus() {{ | |
| paperMenu.classList.remove("open"); | |
| }} | |
| function paperSearch(value) {{ | |
| const query = normalizePaperValue(value); | |
| const terms = query ? query.split(" ").filter(Boolean) : []; | |
| return PAPERS.filter((paper) => {{ | |
| if (!terms.length) return true; | |
| const hay = normalizePaperValue([paper.title, paper.arxiv, paper.orid, paper.area].join(" ")); | |
| return terms.every((term) => hay.includes(term)); | |
| }}).slice(0, 8); | |
| }} | |
| function renderPaperSuggestions() {{ | |
| paperMatches = paperSearch(paperInput.value); | |
| paperActive = paperMatches.length ? 0 : -1; | |
| if (!paperMatches.length) {{ | |
| paperMenu.innerHTML = '<div class="suggestion-empty">No matching paper. Keep typing or paste a title.</div>'; | |
| paperMenu.classList.add("open"); | |
| return; | |
| }} | |
| paperMenu.innerHTML = paperMatches.map((paper, index) => ( | |
| '<button class="suggestion' + (index === paperActive ? ' active' : '') + '" type="button" data-index="' + index + '">' + | |
| '<span class="suggestion-title">' + escapeHtml(paper.title) + '</span>' + | |
| '<span class="suggestion-meta">' + escapeHtml([paper.arxiv ? "arXiv " + paper.arxiv : "", paper.area || ""].filter(Boolean).join(" · ")) + '</span>' + | |
| '</button>' | |
| )).join(""); | |
| paperMenu.classList.add("open"); | |
| }} | |
| function selectPaper(index) {{ | |
| const paper = paperMatches[index]; | |
| if (!paper) return; | |
| paperInput.value = paper.label; | |
| paperMenu.classList.remove("open"); | |
| }} | |
| function setActive(menu, matches, nextIndex) {{ | |
| if (!matches.length) return -1; | |
| const index = (nextIndex + matches.length) % matches.length; | |
| Array.from(menu.querySelectorAll(".suggestion")).forEach((el, i) => {{ | |
| el.classList.toggle("active", i === index); | |
| if (i === index) el.scrollIntoView({{ block: "nearest" }}); | |
| }}); | |
| return index; | |
| }} | |
| paperInput.addEventListener("input", () => {{ | |
| hideStatus(); | |
| renderPaperSuggestions(); | |
| }}); | |
| paperInput.addEventListener("focus", renderPaperSuggestions); | |
| paperMenu.addEventListener("mousedown", (event) => {{ | |
| const item = event.target.closest(".suggestion"); | |
| if (!item) return; | |
| event.preventDefault(); | |
| selectPaper(Number(item.dataset.index)); | |
| }}); | |
| paperInput.addEventListener("keydown", (event) => {{ | |
| if (!paperMenu.classList.contains("open")) return; | |
| if (event.key === "ArrowDown") {{ | |
| event.preventDefault(); | |
| paperActive = setActive(paperMenu, paperMatches, paperActive + 1); | |
| }} else if (event.key === "ArrowUp") {{ | |
| event.preventDefault(); | |
| paperActive = setActive(paperMenu, paperMatches, paperActive - 1); | |
| }} else if (event.key === "Enter" && paperActive >= 0) {{ | |
| event.preventDefault(); | |
| selectPaper(paperActive); | |
| }} else if (event.key === "Escape") {{ | |
| paperMenu.classList.remove("open"); | |
| }} | |
| }}); | |
| document.addEventListener("mousedown", (event) => {{ | |
| if (!event.target.closest(".combo")) closeMenus(); | |
| }}); | |
| form.addEventListener("submit", async (event) => {{ | |
| event.preventDefault(); | |
| const paper = findPaper(paperInput.value); | |
| const payload = {{ | |
| hf_username: username.value.trim(), | |
| email: document.getElementById("email").value.trim(), | |
| paper_orid: paper ? paper.orid : "", | |
| paper_title: paper ? paper.title : paperInput.value.trim(), | |
| space_url: "", | |
| }}; | |
| showStatus("Saving...", false); | |
| const res = await fetch("/credit/request", {{ | |
| method: "POST", | |
| headers: {{ "content-type": "application/json" }}, | |
| body: JSON.stringify(payload), | |
| }}); | |
| const data = await res.json().catch(() => ({{ ok: false, error: "Unexpected response" }})); | |
| showStatus(data.ok ? data.message : data.error, !data.ok); | |
| }}); | |
| </script> | |
| </body> | |
| </html>""" | |
| async def credit_spaces( | |
| username: str = Query("", max_length=96), | |
| q: str = Query("", max_length=120), | |
| ): | |
| username = username.strip() | |
| q = q.strip() | |
| if username and not _valid_username(username): | |
| raise HTTPException(status_code=400, detail="Invalid Hugging Face username.") | |
| if not username and len(q) < 2: | |
| return {"spaces": []} | |
| spaces = [] | |
| try: | |
| for space in _hf.list_spaces( | |
| author=username or None, | |
| search=q or None, | |
| limit=30 if q else 75, | |
| ): | |
| space_id = getattr(space, "id", "") | |
| if space_id: | |
| spaces.append( | |
| {"id": space_id, "url": f"https://huggingface.co/spaces/{space_id}"} | |
| ) | |
| except Exception as exc: | |
| raise HTTPException(status_code=502, detail=f"Could not load Spaces: {exc}") | |
| return {"spaces": spaces} | |
| async def credit_request(payload: dict): | |
| return JSONResponse({"error": "The challenge has ended; credit requests are closed."}, status_code=410) | |
| username = (payload.get("hf_username") or "").strip() | |
| email = (payload.get("email") or "").strip() | |
| if not _valid_username(username): | |
| return {"ok": False, "error": "Enter a valid Hugging Face username."} | |
| if not _valid_email(email): | |
| return {"ok": False, "error": "Enter a valid email address."} | |
| space_id, space_url = _normalize_space_url(payload.get("space_url") or "") | |
| paper = _paper_payload(payload.get("paper_orid") or "", payload.get("paper_title") or "") | |
| if not paper["paper_title"]: | |
| return {"ok": False, "error": "Pick the paper you are reproducing."} | |
| record = { | |
| "submitted_at": datetime.now(timezone.utc).isoformat(), | |
| "hf_username": username, | |
| "email": email, | |
| "space_id": space_id, | |
| "space_url": space_url, | |
| **paper, | |
| } | |
| try: | |
| _append_submission(record) | |
| except Exception as exc: | |
| return {"ok": False, "error": f"Could not save the request yet: {exc}"} | |
| return { | |
| "ok": True, | |
| "message": "Request saved. Credits are typically applied within 24-48 hours after you join the org.", | |
| } | |
| async def agents(limit: int = 100): | |
| s = await _snapshot() | |
| return {"count": s["agents"], "matched": s["agents"], "limit": limit} | |
| async def results(limit: int = 1): | |
| s = await _snapshot() | |
| return {"count": s["results"], "matched": s["results"], "limit": limit} | |
| async def messages(limit: int = 2000): | |
| s = await _snapshot() | |
| items = s["items"][: max(0, limit)] | |
| return {"count": s["messages"], "matched": s["messages"], "items": items} | |
| async def message_detail(filename: str): | |
| s = await _snapshot() | |
| filename = urllib.parse.unquote(filename) | |
| for e, item in zip(s["events"], s["items"]): | |
| if item == filename: | |
| body = e["title"] | |
| if e["reproduced"] or e["in_progress"]: | |
| body += ( | |
| f" — {e['reproduced']} reproduced, " | |
| f"{e['in_progress']} in progress" | |
| ) | |
| return { | |
| "frontmatter": {"agent": e["agent"], "timestamp": e["updated_at"]}, | |
| "body": body, | |
| "space": e["space"], | |
| } | |
| return {"frontmatter": {}, "body": ""} | |