| """ |
| vROM Hub Space β Browse, submit, and manage vROM packages. |
| |
| Tabs: |
| - Browse: View all published vROMs in the registry |
| - Submit: Request a new vROM to be built from documentation URLs |
| - Admin: Approve/reject submissions, trigger builds (philipp-zettl only) |
| |
| Mobile-optimized: single demo.load, lazy admin loading, cached hub downloads, |
| no blocking spinners, pre-rendered tab children. |
| """ |
|
|
| import json |
| import logging |
| import os |
| import re |
| import time |
| import uuid |
| from datetime import datetime, timezone |
| from typing import Optional |
|
|
| import gradio as gr |
| from huggingface_hub import HfApi |
|
|
| |
|
|
| ADMIN_USERNAMES = {"philipp-zettl"} |
| REGISTRY_REPO = "philipp-zettl/vrom-registry" |
| SUBMISSIONS_REPO = "philipp-zettl/vrom-submissions" |
| SUBMISSIONS_FILE = "submissions.json" |
| CACHE_TTL = 30 |
|
|
| logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(name)s | %(message)s") |
| logger = logging.getLogger("vrom-hub-space") |
|
|
| hf_api = HfApi(token=os.environ.get("HF_TOKEN")) |
|
|
| |
|
|
| MOBILE_CSS = """ |
| /* Fix: gradio-container clips horizontal overflow */ |
| .gradio-container { |
| overflow-x: auto !important; |
| } |
| |
| /* Fix: ensure any table can scroll horizontally on mobile */ |
| .table-wrap { |
| overflow-x: auto !important; |
| -webkit-overflow-scrolling: touch; |
| } |
| |
| /* Fix: minimum tap target for all buttons */ |
| button { |
| min-height: 44px !important; |
| } |
| |
| /* Fix: tab overflow β ensure tabs scroll instead of clipping */ |
| .tab-container { |
| overflow-x: auto !important; |
| overflow-y: hidden !important; |
| } |
| |
| /* vROM card styles */ |
| .vrom-card { |
| border: 1px solid var(--border-color-primary, #e0e0e0); |
| border-radius: 12px; |
| padding: 16px; |
| margin-bottom: 12px; |
| background: var(--background-fill-primary, #fff); |
| } |
| .vrom-card-title { |
| font-weight: 700; |
| font-size: 17px; |
| margin-bottom: 4px; |
| word-break: break-word; |
| } |
| .vrom-card-meta { |
| color: var(--body-text-color-subdued, #666); |
| font-size: 13px; |
| margin-bottom: 8px; |
| } |
| .vrom-card-desc { |
| font-size: 14px; |
| margin-bottom: 8px; |
| word-break: break-word; |
| } |
| .vrom-tag { |
| display: inline-block; |
| background: var(--color-accent-soft, #e0e7ff); |
| border-radius: 6px; |
| padding: 2px 8px; |
| font-size: 12px; |
| margin: 2px 2px 2px 0; |
| } |
| .vrom-files { |
| margin-top: 8px; |
| font-size: 13px; |
| } |
| .vrom-files a { |
| color: var(--link-text-color, #2563eb); |
| text-decoration: none; |
| margin-right: 12px; |
| } |
| .vrom-files a:hover { |
| text-decoration: underline; |
| } |
| .vrom-empty { |
| text-align: center; |
| padding: 40px 20px; |
| color: var(--body-text-color-subdued, #666); |
| font-size: 16px; |
| } |
| |
| /* Submission card styles (admin) */ |
| .sub-card { |
| border: 1px solid var(--border-color-primary, #e0e0e0); |
| border-radius: 10px; |
| padding: 14px; |
| margin-bottom: 10px; |
| background: var(--background-fill-primary, #fff); |
| } |
| .sub-card-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: flex-start; |
| flex-wrap: wrap; |
| gap: 4px; |
| } |
| .sub-card-name { |
| font-weight: 600; |
| font-size: 15px; |
| word-break: break-word; |
| } |
| .sub-card-id { |
| font-family: monospace; |
| font-size: 12px; |
| color: var(--body-text-color-subdued, #888); |
| background: var(--background-fill-secondary, #f3f4f6); |
| padding: 2px 6px; |
| border-radius: 4px; |
| } |
| .sub-card-meta { |
| font-size: 13px; |
| color: var(--body-text-color-subdued, #666); |
| margin-top: 4px; |
| } |
| .status-badge { |
| display: inline-block; |
| padding: 2px 8px; |
| border-radius: 10px; |
| font-size: 12px; |
| font-weight: 600; |
| } |
| .status-pending { background: #fef3c7; color: #92400e; } |
| .status-building { background: #dbeafe; color: #1e40af; } |
| .status-published { background: #d1fae5; color: #065f46; } |
| .status-rejected { background: #fee2e2; color: #991b1b; } |
| .status-failed { background: #fce7f3; color: #9d174d; } |
| |
| /* Only apply hover effects on devices that support hover (not mobile) */ |
| @media (hover: hover) { |
| .vrom-card:hover { |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); |
| } |
| .sub-card:hover { |
| box-shadow: 0 1px 6px rgba(0,0,0,0.08); |
| border-color: var(--color-accent, #6366f1); |
| } |
| } |
| |
| /* Mobile-specific overrides */ |
| @media (max-width: 640px) { |
| .gradio-container { |
| padding: 8px !important; |
| } |
| .vrom-card { |
| padding: 12px; |
| } |
| } |
| """ |
|
|
|
|
| |
| |
| |
|
|
| _registry_cache = {"data": [], "ts": 0} |
| _submissions_cache = {"data": {"version": "1.0.0", "submissions": []}, "ts": 0} |
|
|
|
|
| def load_registry(force: bool = False) -> list[dict]: |
| now = time.time() |
| if not force and now - _registry_cache["ts"] < CACHE_TTL: |
| return _registry_cache["data"] |
| try: |
| path = hf_api.hf_hub_download( |
| repo_id=REGISTRY_REPO, filename="registry.json", |
| repo_type="dataset", force_download=True, |
| ) |
| with open(path, "r") as f: |
| data = json.load(f) |
| result = data.get("vroms", []) |
| _registry_cache.update({"data": result, "ts": now}) |
| return result |
| except Exception as e: |
| logger.error(f"Failed to load registry: {e}") |
| return _registry_cache["data"] or [] |
|
|
|
|
| def load_submissions(force: bool = False) -> dict: |
| now = time.time() |
| if not force and now - _submissions_cache["ts"] < CACHE_TTL: |
| return _submissions_cache["data"] |
| try: |
| path = hf_api.hf_hub_download( |
| repo_id=SUBMISSIONS_REPO, filename=SUBMISSIONS_FILE, |
| repo_type="dataset", force_download=True, |
| ) |
| with open(path, "r") as f: |
| result = json.load(f) |
| _submissions_cache.update({"data": result, "ts": now}) |
| return result |
| except Exception as e: |
| logger.error(f"Failed to load submissions: {e}") |
| return _submissions_cache["data"] |
|
|
|
|
| |
|
|
| def is_admin(profile: Optional[gr.OAuthProfile]) -> bool: |
| return profile is not None and profile.username in ADMIN_USERNAMES |
|
|
|
|
| def save_submissions(data: dict): |
| content = json.dumps(data, indent=2, ensure_ascii=False) |
| hf_api.upload_file( |
| path_or_fileobj=content.encode("utf-8"), |
| path_in_repo=SUBMISSIONS_FILE, |
| repo_id=SUBMISSIONS_REPO, |
| repo_type="dataset", |
| commit_message=f"Update submissions ({datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M')})", |
| ) |
| |
| _submissions_cache["ts"] = 0 |
|
|
|
|
| def slugify(text: str) -> str: |
| text = text.lower().strip() |
| text = re.sub(r'[^\w\s-]', '', text) |
| text = re.sub(r'[\s_]+', '-', text) |
| text = re.sub(r'-+', '-', text) |
| return text[:50].strip('-') |
|
|
|
|
| def _escape_html(text: str) -> str: |
| """Escape HTML special characters.""" |
| return (text or "").replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """) |
|
|
|
|
| |
|
|
| def get_registry_cards_html(force: bool = False): |
| vroms = load_registry(force=force) |
| if not vroms: |
| return '<div class="vrom-empty">π No vROMs published yet.<br>Submit a request to get started!</div>' |
|
|
| cards = [] |
| for v in vroms: |
| name = _escape_html(v.get("name", v.get("id", "?"))) |
| vrom_id = _escape_html(v.get("id", "?")) |
| version = _escape_html(str(v.get("version", "?"))) |
| vectors = v.get("vectors", "?") |
| size_mb = v.get("size_mb", "?") |
| dims = v.get("dimensions", "?") |
| desc = _escape_html(v.get("description", "No description.")) |
| official = ' <span title="Official">β
</span>' if v.get("official") else "" |
|
|
| tags_html = "".join( |
| f'<span class="vrom-tag">{_escape_html(t)}</span>' |
| for t in v.get("tags", []) |
| ) |
|
|
| files = v.get("files", {}) |
| files_html = "" |
| if files: |
| links = [] |
| for label, key in [("manifest", "manifest"), ("index", "index"), ("chunks", "chunks")]: |
| url = files.get(key, "") |
| if url: |
| links.append(f'<a href="{_escape_html(url)}" target="_blank">π {label}</a>') |
| if links: |
| files_html = f'<div class="vrom-files">{" Β· ".join(links)}</div>' |
|
|
| cards.append(f"""<div class="vrom-card"> |
| <div class="vrom-card-title">π¦ {name}{official}</div> |
| <div class="vrom-card-meta"> |
| <code>{vrom_id}</code> Β· v{version} Β· {vectors} vectors Β· {size_mb} MB Β· {dims}d |
| </div> |
| <div>{tags_html}</div> |
| <div class="vrom-card-desc">{desc}</div> |
| {files_html} |
| </div>""") |
|
|
| return "\n".join(cards) |
|
|
|
|
| |
|
|
| def get_pending_cards_html(profile: Optional[gr.OAuthProfile], force: bool = False): |
| if not is_admin(profile): |
| return "" |
| data = load_submissions(force=force) |
| pending = [s for s in data["submissions"] if s["status"] == "pending"] |
| if not pending: |
| return '<div class="vrom-empty" style="padding:20px">No pending submissions.</div>' |
|
|
| cards = [] |
| for s in pending: |
| name = _escape_html(s.get("name", "?")) |
| sub_id = _escape_html(s["id"]) |
| vrom_id = _escape_html(s.get("vrom_id", "?")) |
| submitter = _escape_html(s.get("submitter", "?")) |
| n_urls = len(s.get("urls", [])) |
| date = s.get("submitted_at", "?")[:10] |
| urls_preview = ", ".join(s["urls"][:2]) |
| if len(s["urls"]) > 2: |
| urls_preview += f" (+{len(s['urls'])-2} more)" |
| urls_preview = _escape_html(urls_preview) |
|
|
| cards.append(f"""<div class="sub-card"> |
| <div class="sub-card-header"> |
| <span class="sub-card-name">{name}</span> |
| <span class="sub-card-id">{sub_id}</span> |
| </div> |
| <div class="sub-card-meta"> |
| <strong>{vrom_id}</strong> Β· by {submitter} Β· {n_urls} URL(s) Β· {date} |
| </div> |
| <div style="font-size:12px;color:var(--body-text-color-subdued,#888);margin-top:4px;word-break:break-all">{urls_preview}</div> |
| </div>""") |
|
|
| return "\n".join(cards) |
|
|
|
|
| def get_all_submissions_html(profile: Optional[gr.OAuthProfile], force: bool = False): |
| if not is_admin(profile): |
| return "" |
| data = load_submissions(force=force) |
| if not data["submissions"]: |
| return '<div class="vrom-empty" style="padding:20px">No submissions yet.</div>' |
|
|
| status_class = {"pending": "status-pending", "building": "status-building", |
| "published": "status-published", "rejected": "status-rejected", |
| "failed": "status-failed"} |
| status_emoji = {"pending": "β³", "building": "π¨", "published": "π¦", |
| "rejected": "β", "failed": "π₯"} |
|
|
| cards = [] |
| for s in data["submissions"]: |
| name = _escape_html(s.get("name", "?")) |
| sub_id = _escape_html(s["id"]) |
| submitter = _escape_html(s.get("submitter", "?")) |
| status = s.get("status", "?") |
| cls = status_class.get(status, "") |
| emoji = status_emoji.get(status, "β") |
| date = s.get("submitted_at", "?")[:10] |
|
|
| cards.append(f"""<div class="sub-card" style="cursor:default"> |
| <div class="sub-card-header"> |
| <span class="sub-card-name">{name}</span> |
| <span class="status-badge {cls}">{emoji} {_escape_html(status)}</span> |
| </div> |
| <div class="sub-card-meta"> |
| <span class="sub-card-id">{sub_id}</span> Β· by {submitter} Β· {date} |
| </div> |
| </div>""") |
|
|
| return "\n".join(cards) |
|
|
|
|
| |
|
|
| def handle_submit(name, urls, description, tags, version, profile: Optional[gr.OAuthProfile]): |
| if not name or not name.strip(): |
| raise gr.Error("Name is required.") |
| if not urls or not urls.strip(): |
| raise gr.Error("At least one documentation URL is required.") |
|
|
| url_list = [u.strip() for u in urls.strip().split("\n") if u.strip()] |
| if not url_list: |
| raise gr.Error("Please enter at least one valid URL.") |
| for u in url_list: |
| if not u.startswith("http"): |
| raise gr.Error(f"Invalid URL: {u}") |
|
|
| tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else [] |
| vrom_id = slugify(name) |
| submitter = profile.username if profile else "anonymous" |
|
|
| data = load_submissions(force=True) |
| registry_ids = {v["id"] for v in load_registry()} |
| if vrom_id in registry_ids: |
| raise gr.Error(f"A vROM with ID `{vrom_id}` already exists in the registry.") |
| pending_ids = {s["vrom_id"] for s in data["submissions"] if s["status"] == "pending"} |
| if vrom_id in pending_ids: |
| raise gr.Error(f"A submission for `{vrom_id}` is already pending review.") |
|
|
| submission = { |
| "id": str(uuid.uuid4())[:8], "vrom_id": vrom_id, "name": name.strip(), |
| "description": description.strip() if description else "", |
| "urls": url_list, "tags": tag_list, |
| "version": version.strip() if version else "1.0.0", |
| "submitter": submitter, "status": "pending", |
| "submitted_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), |
| "reviewed_at": None, "reviewed_by": None, "review_note": None, "build_error": None, |
| } |
| data["submissions"].append(submission) |
| save_submissions(data) |
|
|
| gr.Info(f"Submitted! Your request for '{name}' is pending admin review.") |
| logger.info(f"New submission: {vrom_id} by {submitter} ({len(url_list)} URLs)") |
|
|
| return ( |
| f"### β
Submission received!\n\n" |
| f"**vROM ID:** `{vrom_id}` \n" |
| f"**Submitted by:** {submitter} \n" |
| f"**URLs:** {len(url_list)} documentation page(s) \n" |
| f"**Status:** β³ Pending admin review\n\n" |
| f"You'll see it appear in the Browse tab once approved and built." |
| ) |
|
|
|
|
| |
|
|
| def get_submission_detail(sub_id: str, profile: Optional[gr.OAuthProfile]): |
| if not is_admin(profile): |
| return "π Not authorized." |
| if not sub_id or not sub_id.strip(): |
| return "Enter a submission ID above." |
| data = load_submissions() |
| for s in data["submissions"]: |
| if s["id"] == sub_id.strip(): |
| urls_md = "\n".join(f"- {u}" for u in s["urls"]) |
| tags_md = ", ".join(f"`{t}`" for t in s.get("tags", [])) or "none" |
| return ( |
| f"### π Submission `{s['id']}`\n\n" |
| f"**Name:** {s['name']} \n**vROM ID:** `{s['vrom_id']}` \n" |
| f"**Version:** {s.get('version', '1.0.0')} \n**Submitter:** {s['submitter']} \n" |
| f"**Status:** {s['status']} \n**Tags:** {tags_md} \n" |
| f"**Submitted:** {s['submitted_at']} \n\n" |
| f"**Description:** \n{s.get('description', 'No description.')}\n\n" |
| f"**URLs:**\n{urls_md}\n\n" |
| f"{'**Build Error:** ' + s['build_error'] if s.get('build_error') else ''}" |
| ) |
| return f"Submission `{sub_id}` not found." |
|
|
|
|
| def approve_submission(sub_id: str, profile: Optional[gr.OAuthProfile]): |
| if not is_admin(profile): |
| raise gr.Error("Unauthorized β admins only.") |
| if not sub_id or not sub_id.strip(): |
| raise gr.Error("Enter a submission ID.") |
|
|
| data = load_submissions(force=True) |
| submission = None |
| for s in data["submissions"]: |
| if s["id"] == sub_id.strip(): |
| submission = s |
| break |
| if submission is None: |
| raise gr.Error(f"Submission `{sub_id}` not found.") |
| if submission["status"] != "pending": |
| raise gr.Error(f"Submission is already '{submission['status']}', not pending.") |
|
|
| submission["status"] = "building" |
| submission["reviewed_at"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") |
| submission["reviewed_by"] = profile.username |
| save_submissions(data) |
|
|
| gr.Info(f"Building vROM '{submission['name']}'... This may take a minute.") |
| logger.info(f"Admin {profile.username} approved {sub_id}, starting build") |
|
|
| try: |
| from vrom_hub import VromHubBackend |
| hub = VromHubBackend(registry_repo=REGISTRY_REPO) |
| result = hub.submit_project( |
| vrom_id=submission["vrom_id"], name=submission["name"], |
| description=submission.get("description", ""), |
| version=submission.get("version", "1.0.0"), |
| urls=submission["urls"], tags=submission.get("tags", []), |
| official=False, upload=True, |
| ) |
| submission["status"] = "published" |
| submission["build_error"] = None |
| save_submissions(data) |
|
|
| |
| _registry_cache["ts"] = 0 |
|
|
| stats = result["stats"] |
| logger.info(f"vROM '{submission['vrom_id']}' published: {stats['vectors']} vectors") |
| gr.Info(f"Published! {stats['vectors']} vectors, {stats['total_tokens']} tokens") |
|
|
| return ( |
| f"### β
Published `{submission['vrom_id']}`\n\n" |
| f"**Vectors:** {stats['vectors']} \n**Tokens:** {stats['total_tokens']} \n" |
| f"**Index size:** {stats['index_size_mb']} MB \n" |
| f"**Hub:** [{submission['vrom_id']}]({result.get('hub_url', '#')})" |
| ), get_pending_cards_html(profile), get_all_submissions_html(profile) |
|
|
| except Exception as e: |
| submission["status"] = "failed" |
| submission["build_error"] = str(e) |
| save_submissions(data) |
| logger.error(f"Build failed for {sub_id}: {e}") |
| return ( |
| f"### π₯ Build failed for `{submission['vrom_id']}`\n\n" |
| f"**Error:** {str(e)}\n\n" |
| f"The submission has been marked as failed. You can retry or reject it." |
| ), get_pending_cards_html(profile), get_all_submissions_html(profile) |
|
|
|
|
| def reject_submission(sub_id: str, note: str, profile: Optional[gr.OAuthProfile]): |
| if not is_admin(profile): |
| raise gr.Error("Unauthorized β admins only.") |
| if not sub_id or not sub_id.strip(): |
| raise gr.Error("Enter a submission ID.") |
| data = load_submissions(force=True) |
| for s in data["submissions"]: |
| if s["id"] == sub_id.strip(): |
| if s["status"] not in ("pending", "failed"): |
| raise gr.Error(f"Can only reject pending/failed submissions, not '{s['status']}'.") |
| s["status"] = "rejected" |
| s["reviewed_at"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") |
| s["reviewed_by"] = profile.username |
| s["review_note"] = note.strip() if note else None |
| save_submissions(data) |
| logger.info(f"Admin {profile.username} rejected {sub_id}") |
| gr.Info(f"Rejected submission '{s['name']}'.") |
| return ( |
| f"### β Rejected `{s['vrom_id']}`\n\n**Note:** {note or 'No reason given.'}" |
| ), get_pending_cards_html(profile), get_all_submissions_html(profile) |
| raise gr.Error(f"Submission `{sub_id}` not found.") |
|
|
|
|
| def retry_submission(sub_id: str, profile: Optional[gr.OAuthProfile]): |
| if not is_admin(profile): |
| raise gr.Error("Unauthorized β admins only.") |
| if not sub_id or not sub_id.strip(): |
| raise gr.Error("Enter a submission ID.") |
| data = load_submissions(force=True) |
| for s in data["submissions"]: |
| if s["id"] == sub_id.strip(): |
| if s["status"] != "failed": |
| raise gr.Error(f"Can only retry failed submissions, not '{s['status']}'.") |
| s["status"] = "pending" |
| s["build_error"] = None |
| s["reviewed_at"] = None |
| s["reviewed_by"] = None |
| save_submissions(data) |
| logger.info(f"Admin {profile.username} reset {sub_id} to pending") |
| gr.Info(f"Reset '{s['name']}' to pending.") |
| return ( |
| f"### π Reset `{s['vrom_id']}` to pending" |
| ), get_pending_cards_html(profile), get_all_submissions_html(profile) |
| raise gr.Error(f"Submission `{sub_id}` not found.") |
|
|
|
|
| |
|
|
| with gr.Blocks(title="π§© vROM Hub") as demo: |
| gr.Markdown( |
| "# π§© vROM Hub\n" |
| "> Browse, submit, and manage [vROM](https://github.com/philsupertramp/vecdb-wasm) " |
| "packages β pre-computed vector knowledge bases for in-browser semantic search." |
| ) |
|
|
| with gr.Row(): |
| login_btn = gr.LoginButton(size="sm") |
|
|
| with gr.Tabs(): |
| |
| with gr.Tab("Browse", id="browse"): |
| gr.Markdown("### π Published vROMs\nAvailable for instant use via `AgentMemory.mount(id)`.") |
| refresh_browse_btn = gr.Button("π Refresh", size="sm", min_width=120) |
| browse_cards = gr.HTML("") |
|
|
| def refresh_browse_clicked(): |
| return get_registry_cards_html(force=True) |
| refresh_browse_btn.click( |
| refresh_browse_clicked, |
| outputs=[browse_cards], |
| show_progress="minimal", |
| ) |
|
|
| |
| with gr.Tab("Submit", id="submit"): |
| gr.Markdown( |
| "### β Request a new vROM\n" |
| "Submit documentation URLs and we'll build a vector knowledge base from them.\n" |
| "An admin will review your request before building." |
| ) |
| sub_name = gr.Textbox( |
| label="Project Name", |
| placeholder="e.g. My Project Documentation", |
| info="Human-readable name for the vROM", |
| ) |
| sub_urls = gr.Textbox( |
| label="Documentation URLs (one per line)", |
| placeholder="https://example.com/docs/getting-started\nhttps://example.com/docs/api-reference", |
| lines=5, |
| info="URLs to markdown or HTML documentation pages", |
| ) |
| sub_desc = gr.Textbox( |
| label="Description", |
| placeholder="What does this documentation cover?", |
| lines=2, |
| ) |
| sub_tags = gr.Textbox( |
| label="Tags (comma-separated)", |
| placeholder="python, api, docs", |
| ) |
| sub_version = gr.Textbox(label="Version", value="1.0.0") |
|
|
| with gr.Accordion("π‘ Tips & Specs", open=False): |
| gr.Markdown( |
| "- Each URL should point to a single documentation page\n" |
| "- Markdown pages work best\n" |
| "- HTML pages are auto-converted to markdown\n" |
| "- Include 5β50 pages for best results\n" |
| "- The vROM ID is auto-generated from the name\n\n" |
| "**Specs:** all-MiniLM-L6-v2 (384d) Β· ~256 token chunks Β· HNSW (m=16, cosine)" |
| ) |
|
|
| submit_btn = gr.Button("π Submit Request", variant="primary", size="lg") |
| submit_status = gr.Markdown("") |
| submit_btn.click( |
| handle_submit, |
| inputs=[sub_name, sub_urls, sub_desc, sub_tags, sub_version], |
| outputs=submit_status, |
| show_progress="minimal", |
| ) |
|
|
| |
| with gr.Tab("Admin", id="admin") as admin_tab: |
| admin_panel = gr.Column(visible=False) |
| not_admin_msg = gr.Markdown( |
| "### π Admin Access Required\n\nSign in with the admin account to manage submissions.", |
| visible=True, |
| ) |
| with admin_panel: |
| gr.Markdown("### β³ Pending Submissions") |
| refresh_admin_btn = gr.Button("π Refresh", size="sm", min_width=120) |
| pending_html = gr.HTML("") |
|
|
| gr.Markdown("---\n### Review Submission") |
| admin_sub_id = gr.Textbox( |
| label="Submission ID", |
| placeholder="Enter the ID from the cards above", |
| ) |
| detail_btn = gr.Button("π View Details", min_width=140) |
| detail_md = gr.Markdown("") |
|
|
| approve_btn = gr.Button("β
Approve & Build", variant="primary", size="lg") |
| reject_note = gr.Textbox( |
| label="Rejection note (optional)", |
| placeholder="Reason for rejection", |
| ) |
| with gr.Row(): |
| reject_btn = gr.Button("β Reject", variant="stop", min_width=140) |
| retry_btn = gr.Button("π Retry Failed", min_width=140) |
| admin_status = gr.Markdown("") |
|
|
| gr.Markdown("---\n### π All Submissions") |
| all_html = gr.HTML("") |
|
|
| detail_btn.click( |
| get_submission_detail, inputs=[admin_sub_id], outputs=[detail_md], |
| show_progress="minimal", |
| ) |
| approve_btn.click( |
| approve_submission, inputs=[admin_sub_id], |
| outputs=[admin_status, pending_html, all_html], |
| show_progress="minimal", |
| ) |
| reject_btn.click( |
| reject_submission, inputs=[admin_sub_id, reject_note], |
| outputs=[admin_status, pending_html, all_html], |
| show_progress="minimal", |
| ) |
| retry_btn.click( |
| retry_submission, inputs=[admin_sub_id], |
| outputs=[admin_status, pending_html, all_html], |
| show_progress="minimal", |
| ) |
|
|
| def refresh_admin_clicked(profile: Optional[gr.OAuthProfile]): |
| return get_pending_cards_html(profile, force=True), get_all_submissions_html(profile, force=True) |
| refresh_admin_btn.click( |
| refresh_admin_clicked, outputs=[pending_html, all_html], |
| show_progress="minimal", |
| ) |
|
|
| |
| |
| |
| def on_page_load(profile: Optional[gr.OAuthProfile]): |
| |
| cards_html = get_registry_cards_html(force=False) |
|
|
| |
| admin_visible = is_admin(profile) |
| if admin_visible: |
| pending = get_pending_cards_html(profile, force=False) |
| all_subs = get_all_submissions_html(profile, force=False) |
| else: |
| pending = "" |
| all_subs = "" |
|
|
| return ( |
| cards_html, |
| gr.update(visible=admin_visible), |
| gr.update(visible=not admin_visible), |
| pending, |
| all_subs, |
| ) |
|
|
| demo.load( |
| on_page_load, |
| outputs=[browse_cards, admin_panel, not_admin_msg, pending_html, all_html], |
| show_progress="hidden", |
| concurrency_limit=None, |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch(ssr_mode=False, css=MOBILE_CSS) |
|
|