Spaces:
Sleeping
Sleeping
| """ | |
| HF PRO Onboarding Space | |
| ----------------------- | |
| A personalized, interactive onboarding experience for new Hugging Face PRO users. | |
| Features: | |
| - Username lookup + PRO status check via HF Hub API | |
| - Goal-based personalized resource guide (4 personas) | |
| - Persistent per-user checklist stored in a HF Dataset | |
| - Persona analytics logged to a HF Dataset | |
| - Link to HF billing page for credit balance | |
| """ | |
| import os | |
| import json | |
| import gradio as gr | |
| import requests | |
| import pandas as pd | |
| from datetime import datetime, timezone | |
| from huggingface_hub import HfApi, hf_hub_download | |
| from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError | |
| # ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| HF_TOKEN = os.environ.get("HF_TOKEN", "") | |
| ANALYTICS_REPO = "meganariley/pro-onboarding-analytics" | |
| CHECKLIST_FILE = "checklist_progress.json" | |
| ANALYTICS_FILE = "persona_analytics.json" | |
| api = HfApi(token=HF_TOKEN) | |
| # ββ Persona & Guide Data ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PERSONAS = { | |
| "runner": { | |
| "label": "π§ͺ Run & explore models", | |
| "hint": "Try models, run inference, experiment hands-on", | |
| "title": "Your inference starter kit", | |
| "intro": "You have $2 in monthly credits and ZeroGPU time ready to go. Work through these in order.", | |
| "cards": [ | |
| { | |
| "id": "r1", | |
| "icon": "π", | |
| "label": "Send your first API call", | |
| "desc": "Use Inference Providers to call any model with a few lines of code. Your $2 credit resets monthly.", | |
| "links": [ | |
| ("First API call guide", "https://huggingface.co/docs/inference-providers/en/guides/first-api-call#your-first-inference-provider-call"), | |
| ("Browse trending models", "https://huggingface.co/models?inference_provider=all&sort=trending"), | |
| ], | |
| }, | |
| { | |
| "id": "r2", | |
| "icon": "β‘", | |
| "label": "Run a ZeroGPU Space", | |
| "desc": "GPU-accelerated demos without billing β try diffusion models, LLMs, and more.", | |
| "links": [ | |
| ("Browse ZeroGPU Spaces", "https://huggingface.co/spaces/enzostvs/zero-gpu-spaces"), | |
| ], | |
| }, | |
| { | |
| "id": "r3", | |
| "icon": "ποΈ", | |
| "label": "Create your first repo", | |
| "desc": "Star or fork a model, then create your own repo to save experiments.", | |
| "links": [ | |
| ("Create a repo", "https://huggingface.co/docs/huggingface_hub/main/en/guides/repository#repo-creation-and-deletion"), | |
| ], | |
| }, | |
| { | |
| "id": "r4", | |
| "icon": "π¬", | |
| "label": "Watch the Hub tour", | |
| "desc": "10-minute video covering models, datasets, and Spaces end to end.", | |
| "links": [ | |
| ("Watch on YouTube", "https://www.youtube.com/watch?v=qP9mbY3wuWk"), | |
| ], | |
| }, | |
| ], | |
| }, | |
| "builder": { | |
| "label": "π οΈ Build a Space or app", | |
| "hint": "Deploy a demo, tool, or full application", | |
| "title": "Your builder starter kit", | |
| "intro": "ZeroGPU is your best friend β real GPU compute for free. Here's the fast path to your first deployed app.", | |
| "cards": [ | |
| { | |
| "id": "b1", | |
| "icon": "β‘", | |
| "label": "Enable ZeroGPU on your Space", | |
| "desc": "Add @spaces.GPU to any Gradio function for free shared GPU access.", | |
| "links": [ | |
| ("ZeroGPU docs", "https://huggingface.co/docs/hub/spaces-zerogpu#getting-started-with-zerogpu"), | |
| ("Browse examples", "https://huggingface.co/spaces/enzostvs/zero-gpu-spaces"), | |
| ], | |
| }, | |
| { | |
| "id": "b2", | |
| "icon": "ποΈ", | |
| "label": "Create a Space repo", | |
| "desc": "PRO unlocks expanded storage limits β great for weights, datasets, and Space files.", | |
| "links": [ | |
| ("Create a repo", "https://huggingface.co/docs/huggingface_hub/main/en/guides/repository#repo-creation-and-deletion"), | |
| ("Storage limits", "https://huggingface.co/docs/hub/storage-limits"), | |
| ], | |
| }, | |
| { | |
| "id": "b3", | |
| "icon": "π", | |
| "label": "Prototype with API credits", | |
| "desc": "Call hosted models during development β no GPU setup needed while you iterate.", | |
| "links": [ | |
| ("First API call guide", "https://huggingface.co/docs/inference-providers/en/guides/first-api-call#your-first-inference-provider-call"), | |
| ], | |
| }, | |
| { | |
| "id": "b4", | |
| "icon": "π", | |
| "label": "Deploy & share your Space", | |
| "desc": "Publish your Space publicly and share the link with the world.", | |
| "links": [ | |
| ("Watch the Hub tour", "https://www.youtube.com/watch?v=qP9mbY3wuWk"), | |
| ], | |
| }, | |
| ], | |
| }, | |
| "trainer": { | |
| "label": "π Train or fine-tune", | |
| "hint": "Fine-tune a model on my own data", | |
| "title": "Your training starter kit", | |
| "intro": "PRO storage and compute make fine-tuning on the Hub much more practical. Start here.", | |
| "cards": [ | |
| { | |
| "id": "t1", | |
| "icon": "ποΈ", | |
| "label": "Create a model repo", | |
| "desc": "Store checkpoints and push fine-tuned weights. PRO gives you the headroom to do this properly.", | |
| "links": [ | |
| ("Create a repo", "https://huggingface.co/docs/huggingface_hub/main/en/guides/repository#repo-creation-and-deletion"), | |
| ("Storage limits", "https://huggingface.co/docs/hub/storage-limits"), | |
| ], | |
| }, | |
| { | |
| "id": "t2", | |
| "icon": "π", | |
| "label": "Pick a base model via API", | |
| "desc": "Use Inference Providers to evaluate base models before choosing which to fine-tune.", | |
| "links": [ | |
| ("Browse trending models", "https://huggingface.co/models?inference_provider=all&sort=trending"), | |
| ], | |
| }, | |
| { | |
| "id": "t3", | |
| "icon": "β‘", | |
| "label": "Build a demo Space post-training", | |
| "desc": "Once trained, a ZeroGPU Space gives you a live demo without extra billing.", | |
| "links": [ | |
| ("ZeroGPU getting started", "https://huggingface.co/docs/hub/spaces-zerogpu#getting-started-with-zerogpu"), | |
| ], | |
| }, | |
| { | |
| "id": "t4", | |
| "icon": "π¬", | |
| "label": "Watch the Hub tour", | |
| "desc": "Useful context on how models, datasets, and Spaces connect.", | |
| "links": [ | |
| ("Watch on YouTube", "https://www.youtube.com/watch?v=qP9mbY3wuWk"), | |
| ], | |
| }, | |
| ], | |
| }, | |
| "explorer": { | |
| "label": "πΊοΈ Just exploring", | |
| "hint": "I'm new here β show me the best place to start", | |
| "title": "Your grand tour starter kit", | |
| "intro": "No rush β the Hub is huge. Here's the friendliest path through it, one step at a time.", | |
| "cards": [ | |
| { | |
| "id": "e1", | |
| "icon": "π¬", | |
| "label": "Watch the Hub tour video", | |
| "desc": "The best 10-minute intro to Models, Datasets, and Spaces and how they fit together.", | |
| "links": [ | |
| ("Watch on YouTube", "https://www.youtube.com/watch?v=qP9mbY3wuWk"), | |
| ], | |
| }, | |
| { | |
| "id": "e2", | |
| "icon": "β‘", | |
| "label": "Try a ZeroGPU Space", | |
| "desc": "Click Run on any ZeroGPU Space β fastest way to see the Hub in action.", | |
| "links": [ | |
| ("Browse ZeroGPU Spaces", "https://huggingface.co/spaces/enzostvs/zero-gpu-spaces"), | |
| ], | |
| }, | |
| { | |
| "id": "e3", | |
| "icon": "π", | |
| "label": "Make your first API call", | |
| "desc": "A 5-minute walkthrough using your free monthly credit β no setup required.", | |
| "links": [ | |
| ("First API call guide", "https://huggingface.co/docs/inference-providers/en/guides/first-api-call#your-first-inference-provider-call"), | |
| ], | |
| }, | |
| { | |
| "id": "e4", | |
| "icon": "ποΈ", | |
| "label": "Create your first repo", | |
| "desc": "When you're ready to store or share something, this is the natural next step.", | |
| "links": [ | |
| ("Create a repo", "https://huggingface.co/docs/huggingface_hub/main/en/guides/repository#repo-creation-and-deletion"), | |
| ], | |
| }, | |
| ], | |
| }, | |
| } | |
| # ββ HF API Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def lookup_user(username: str) -> dict: | |
| """Fetch basic user info from HF Hub API.""" | |
| if not username: | |
| return {"found": False, "is_pro": False, "avatar": "", "fullname": ""} | |
| try: | |
| url = f"https://huggingface.co/api/users/{username}" | |
| headers = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {} | |
| r = requests.get(url, headers=headers, timeout=5) | |
| if r.status_code == 200: | |
| data = r.json() | |
| is_pro = data.get("isPro", False) or data.get("isEnterprise", False) | |
| return { | |
| "found": True, | |
| "is_pro": is_pro, | |
| "avatar": data.get("avatarUrl", ""), | |
| "fullname": data.get("fullname", "") or username, | |
| } | |
| except Exception: | |
| pass | |
| return {"found": False, "is_pro": False, "avatar": "", "fullname": ""} | |
| # ββ Dataset Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _ensure_dataset_repo(): | |
| """Create the analytics dataset repo if it doesn't exist.""" | |
| try: | |
| api.repo_info(ANALYTICS_REPO, repo_type="dataset") | |
| except RepositoryNotFoundError: | |
| api.create_repo(ANALYTICS_REPO, repo_type="dataset", private=False) | |
| def _read_json_from_dataset(filename: str) -> dict: | |
| """Read a JSON file from the analytics dataset, return {} if missing.""" | |
| try: | |
| path = hf_hub_download( | |
| repo_id=ANALYTICS_REPO, | |
| filename=filename, | |
| repo_type="dataset", | |
| token=HF_TOKEN or None, | |
| ) | |
| with open(path, "r") as f: | |
| return json.load(f) | |
| except (EntryNotFoundError, Exception): | |
| return {} | |
| def _write_json_to_dataset(filename: str, data: dict): | |
| """Write a JSON file to the analytics dataset.""" | |
| try: | |
| _ensure_dataset_repo() | |
| content = json.dumps(data, indent=2) | |
| api.upload_file( | |
| path_or_fileobj=content.encode(), | |
| path_in_repo=filename, | |
| repo_id=ANALYTICS_REPO, | |
| repo_type="dataset", | |
| commit_message=f"Update {filename}", | |
| ) | |
| except Exception as e: | |
| print(f"[warn] Could not write {filename} to dataset: {e}") | |
| def load_checklist(username: str) -> dict: | |
| """Load checklist state for a user. Returns {item_id: bool}.""" | |
| if not username: | |
| return {} | |
| all_progress = _read_json_from_dataset(CHECKLIST_FILE) | |
| return all_progress.get(username, {}) | |
| def save_checklist(username: str, state: dict): | |
| """Persist checklist state for a user.""" | |
| if not username: | |
| return | |
| all_progress = _read_json_from_dataset(CHECKLIST_FILE) | |
| all_progress[username] = state | |
| _write_json_to_dataset(CHECKLIST_FILE, all_progress) | |
| def log_persona_pick(username: str, persona_key: str): | |
| """Log a persona selection to the analytics dataset.""" | |
| analytics = _read_json_from_dataset(ANALYTICS_FILE) | |
| if "picks" not in analytics: | |
| analytics["picks"] = [] | |
| analytics["picks"].append({ | |
| "username": username or "anonymous", | |
| "persona": persona_key, | |
| "timestamp": datetime.now(timezone.utc).isoformat(), | |
| }) | |
| # Also maintain summary counts | |
| counts = analytics.get("counts", {}) | |
| counts[persona_key] = counts.get(persona_key, 0) + 1 | |
| analytics["counts"] = counts | |
| _write_json_to_dataset(ANALYTICS_FILE, analytics) | |
| def get_analytics_summary() -> str: | |
| """Return a markdown summary of persona picks for the team.""" | |
| analytics = _read_json_from_dataset(ANALYTICS_FILE) | |
| counts = analytics.get("counts", {}) | |
| if not counts: | |
| return "No data yet." | |
| total = sum(counts.values()) | |
| lines = [f"**Total picks: {total}**\n"] | |
| for key, persona in PERSONAS.items(): | |
| count = counts.get(key, 0) | |
| pct = int((count / total) * 100) if total else 0 | |
| bar = "β" * (pct // 5) + "β" * (20 - pct // 5) | |
| lines.append(f"{persona['label']}\n`{bar}` {count} ({pct}%)\n") | |
| return "\n".join(lines) | |
| # ββ HTML Builders βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| SHARED_CSS = """ | |
| <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:wght@400;500&display=swap" rel="stylesheet"> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| .wrap { font-family: 'DM Sans', sans-serif; color: #e6edf3; max-width: 560px; margin: 0 auto; } | |
| .eyebrow { display: flex; align-items: center; gap: 8px; justify-content: center; margin-bottom: 1.25rem; } | |
| .dot { width: 8px; height: 8px; border-radius: 50%; background: #FF9D00; animation: pulse 2s infinite; } | |
| @keyframes pulse { 0%,100%{box-shadow:0 0 0 0 rgba(255,157,0,0.4)} 50%{box-shadow:0 0 0 6px rgba(255,157,0,0)} } | |
| .badge-label { font-size: 11px; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: #FF9D00; } | |
| h1.title { font-family: 'Syne', sans-serif; font-size: 28px; font-weight: 800; text-align: center; line-height: 1.2; color: #fff; margin-bottom: 0.5rem; } | |
| h1.title em { font-style: normal; color: #FF9D00; } | |
| .subtitle { text-align: center; font-size: 13.5px; color: #8b949e; margin-bottom: 1.75rem; line-height: 1.65; } | |
| .perk-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 10px; margin-bottom: 1.5rem; } | |
| .perk { background: #111827; border: 1px solid rgba(255,157,0,0.12); border-radius: 12px; padding: 1rem 0.75rem; text-align: center; transition: border-color 0.2s, transform 0.2s; } | |
| .perk:hover { border-color: rgba(255,157,0,0.4); transform: translateY(-2px); } | |
| .perk-emoji { font-size: 20px; margin-bottom: 0.4rem; display: block; } | |
| .perk-name { font-family: 'Syne', sans-serif; font-size: 11.5px; font-weight: 700; color: #fff; margin-bottom: 0.2rem; } | |
| .perk-val { font-size: 13px; color: #FF9D00; font-weight: 700; font-family: 'Syne', sans-serif; } | |
| .divider { text-align: center; font-size: 11.5px; color: #3d4752; margin: 1.25rem 0; position: relative; } | |
| .divider::before,.divider::after { content:''; position:absolute; top:50%; width:28%; height:1px; background:rgba(255,255,255,0.05); } | |
| .divider::before { left:0; } .divider::after { right:0; } | |
| .persona-grid { display: grid; grid-template-columns: repeat(2,1fr); gap: 10px; } | |
| .persona { background: #111827; border: 2px solid rgba(255,157,0,0.08); border-radius: 12px; padding: 1.1rem 1rem; } | |
| .persona-icon { font-size: 22px; margin-bottom: 0.45rem; display: block; } | |
| .persona-title { font-family: 'Syne', sans-serif; font-size: 13px; font-weight: 700; color: #fff; margin-bottom: 0.2rem; } | |
| .persona-hint { font-size: 11.5px; color: #8b949e; line-height: 1.4; } | |
| .section-label { font-family: 'Syne', sans-serif; font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: #555f6d; margin-bottom: 0.75rem; } | |
| .prog-label { font-size: 11px; color: #555f6d; display: flex; justify-content: space-between; margin-bottom: 6px; } | |
| .prog-label span { color: #FF9D00; font-weight: 500; } | |
| .prog-wrap { background: #111827; border-radius: 999px; height: 4px; margin-bottom: 1.5rem; overflow: hidden; } | |
| .prog-bar { height: 4px; background: linear-gradient(90deg,#FF9D00,#ffba40); border-radius: 999px; transition: width 0.4s ease; } | |
| .check-item { display: flex; align-items: flex-start; gap: 10px; padding: 0.85rem 0.9rem; background: #111827; border: 1px solid rgba(255,157,0,0.08); border-radius: 10px; margin-bottom: 8px; } | |
| .check-item.done { background: #0f1a14; border-color: rgba(74,222,128,0.2); } | |
| .check-box { width: 18px; height: 18px; min-width: 18px; border-radius: 5px; border: 2px solid rgba(255,157,0,0.3); display: flex; align-items: center; justify-content: center; margin-top: 2px; } | |
| .check-item.done .check-box { background: #4ade80; border-color: #4ade80; color: #0a0e17; font-size: 11px; } | |
| .check-label { font-family: 'Syne', sans-serif; font-size: 12.5px; font-weight: 700; color: #e6edf3; margin-bottom: 2px; } | |
| .check-item.done .check-label { color: #8b949e; text-decoration: line-through; } | |
| .check-desc { font-size: 11.5px; color: #8b949e; line-height: 1.45; margin-bottom: 0.45rem; } | |
| .check-links { display: flex; flex-wrap: wrap; gap: 5px; } | |
| .cl { font-size: 11px; font-weight: 500; color: #FF9D00; text-decoration: none; background: rgba(255,157,0,0.07); border: 1px solid rgba(255,157,0,0.15); border-radius: 5px; padding: 2px 8px; } | |
| .cl::before { content: "β "; opacity: 0.6; } | |
| .credits-card { background: #111827; border: 1px solid rgba(255,157,0,0.15); border-radius: 12px; padding: 1rem 1.1rem; margin-bottom: 1.25rem; display: flex; align-items: center; gap: 12px; } | |
| .credits-icon { font-size: 22px; } | |
| .credits-body { flex: 1; } | |
| .credits-title { font-family: 'Syne', sans-serif; font-size: 12.5px; font-weight: 700; color: #fff; margin-bottom: 2px; } | |
| .credits-desc { font-size: 11.5px; color: #8b949e; } | |
| .credits-link { font-size: 11.5px; font-weight: 500; color: #FF9D00; text-decoration: none; background: rgba(255,157,0,0.07); border: 1px solid rgba(255,157,0,0.15); border-radius: 6px; padding: 4px 10px; white-space: nowrap; } | |
| .credits-link::before { content: "β "; opacity: 0.6; } | |
| .avatar { width: 36px; height: 36px; border-radius: 50%; border: 2px solid rgba(255,157,0,0.3); } | |
| .user-greeting { display: flex; align-items: center; gap: 10px; justify-content: center; margin-bottom: 0.5rem; } | |
| .pro-badge { font-size: 10px; font-weight: 700; background: rgba(255,157,0,0.12); color: #FF9D00; border: 1px solid rgba(255,157,0,0.3); border-radius: 20px; padding: 2px 8px; letter-spacing: 0.05em; } | |
| .footer { text-align: center; font-size: 11px; color: #3d4752; border-top: 1px solid rgba(255,255,255,0.04); padding-top: 1rem; margin-top: 1.25rem; } | |
| .footer span { color: #FF9D00; } | |
| </style> | |
| """ | |
| def build_showcase_html(username: str, user_info: dict) -> str: | |
| """Build the PRO showcase / landing screen HTML.""" | |
| if username and user_info.get("found"): | |
| avatar = user_info.get("avatar", "") | |
| fullname = user_info.get("fullname", username) | |
| is_pro = user_info.get("is_pro", False) | |
| pro_tag = '<span class="pro-badge">PRO</span>' if is_pro else "" | |
| header = f""" | |
| <div class="user-greeting"> | |
| {"<img class='avatar' src='" + avatar + "' />" if avatar else ""} | |
| <div> | |
| <div style="font-family:'Syne',sans-serif;font-size:13px;font-weight:700;color:#fff;"> | |
| @{username} {pro_tag} | |
| </div> | |
| </div> | |
| </div> | |
| <h1 class="title">Welcome, <em>{fullname.split()[0] if fullname else '@'+username}</em> π</h1> | |
| <p class="subtitle">Your PRO perks are ready to go. Here's everything unlocked for you.</p> | |
| """ | |
| else: | |
| header = """ | |
| <div class="eyebrow"><span class="dot"></span><span class="badge-label">Hugging Face PRO</span></div> | |
| <h1 class="title">Unlock the full<br>power of <em>the Hub</em></h1> | |
| <p class="subtitle">PRO gives you real compute, real credits, and real storage β everything you need to stop reading and start building.</p> | |
| """ | |
| return f""" | |
| {SHARED_CSS} | |
| <div class="wrap"> | |
| {header} | |
| <div class="perk-grid"> | |
| <div class="perk"><span class="perk-emoji">β‘</span><div class="perk-name">ZeroGPU</div><div class="perk-val">25 min/day</div></div> | |
| <div class="perk"><span class="perk-emoji">π</span><div class="perk-name">API Credits</div><div class="perk-val">$2/month</div></div> | |
| <div class="perk"><span class="perk-emoji">ποΈ</span><div class="perk-name">Storage</div><div class="perk-val">50GB+</div></div> | |
| <div class="perk"><span class="perk-emoji">π€</span><div class="perk-name">Models</div><div class="perk-val">500k+</div></div> | |
| <div class="perk"><span class="perk-emoji">π</span><div class="perk-name">Spaces</div><div class="perk-val">400k+</div></div> | |
| <div class="perk"><span class="perk-emoji">π</span><div class="perk-name">Providers</div><div class="perk-val">20+</div></div> | |
| </div> | |
| </div> | |
| """ | |
| def build_checklist_html(persona_key: str, checklist_state: dict) -> str: | |
| """Build the checklist HTML for a given persona and state.""" | |
| persona = PERSONAS[persona_key] | |
| cards = persona["cards"] | |
| done_count = sum(1 for c in cards if checklist_state.get(c["id"], False)) | |
| total = len(cards) | |
| pct = int((done_count / total) * 100) if total else 0 | |
| items_html = "" | |
| for card in cards: | |
| is_done = checklist_state.get(card["id"], False) | |
| done_class = " done" if is_done else "" | |
| check_inner = "β" if is_done else "" | |
| links_html = "".join( | |
| f'<a class="cl" href="{url}" target="_blank">{text}</a>' | |
| for text, url in card["links"] | |
| ) | |
| items_html += f""" | |
| <div class="check-item{done_class}" id="item-{card['id']}"> | |
| <div class="check-box">{check_inner}</div> | |
| <div style="flex:1"> | |
| <div class="check-label">{card['icon']} {card['label']}</div> | |
| <div class="check-desc">{card['desc']}</div> | |
| <div class="check-links">{links_html}</div> | |
| </div> | |
| </div> | |
| """ | |
| credits_card = """ | |
| <div class="credits-card"> | |
| <span class="credits-icon">π³</span> | |
| <div class="credits-body"> | |
| <div class="credits-title">Your inference credits</div> | |
| <div class="credits-desc">Check your live $2 monthly credit balance on the HF billing page.</div> | |
| </div> | |
| <a class="credits-link" href="https://huggingface.co/settings/billing" target="_blank">View balance</a> | |
| </div> | |
| """ | |
| return f""" | |
| {SHARED_CSS} | |
| <div class="wrap"> | |
| <div style="margin-bottom:1.25rem"> | |
| <div style="font-family:'Syne',sans-serif;font-size:20px;font-weight:800;color:#fff;margin-bottom:0.3rem"> | |
| {persona['title']} | |
| </div> | |
| <p style="font-size:12.5px;color:#8b949e;line-height:1.55">{persona['intro']}</p> | |
| </div> | |
| {credits_card} | |
| <div class="section-label">Getting started checklist</div> | |
| <div class="prog-label"><span>{done_count} of {total} complete</span></div> | |
| <div class="prog-wrap"><div class="prog-bar" style="width:{pct}%"></div></div> | |
| {items_html} | |
| <div class="footer">Made with <span>β₯</span> by the HF Support Team</div> | |
| </div> | |
| """ | |
| # ββ Gradio App ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def on_greet(username_input: str) -> tuple: | |
| """Called when user submits their username.""" | |
| username = username_input.strip().lstrip("@") | |
| user_info = lookup_user(username) if username else {} | |
| showcase_html = build_showcase_html(username, user_info) | |
| checklist_state = load_checklist(username) if username else {} | |
| return ( | |
| showcase_html, | |
| username, | |
| user_info, | |
| checklist_state, | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| gr.update(visible=False), # hide username input area | |
| ) | |
| def default_showcase() -> str: | |
| """Return default showcase HTML with no username.""" | |
| return build_showcase_html("", {}) | |
| def on_persona_select( | |
| persona_key: str, | |
| username: str, | |
| checklist_state: dict, | |
| ) -> tuple: | |
| """Handle persona button click β log analytics, render checklist.""" | |
| # Load fresh checklist state (may have been saved from previous visit) | |
| saved_state = load_checklist(username) if username else {} | |
| merged_state = {**checklist_state, **saved_state} | |
| # Log the pick | |
| log_persona_pick(username, persona_key) | |
| checklist_html = build_checklist_html(persona_key, merged_state) | |
| return ( | |
| checklist_html, | |
| persona_key, | |
| merged_state, | |
| gr.update(visible=False), # showcase | |
| gr.update(visible=False), # persona | |
| gr.update(visible=True), # guide | |
| ) | |
| def on_checklist_toggle( | |
| item_id: str, | |
| persona_key: str, | |
| username: str, | |
| checklist_state: dict, | |
| ) -> tuple: | |
| """Toggle a checklist item and persist.""" | |
| new_state = dict(checklist_state) | |
| new_state[item_id] = not new_state.get(item_id, False) | |
| # Persist async-style (fire and don't block UI) | |
| if username: | |
| save_checklist(username, new_state) | |
| checklist_html = build_checklist_html(persona_key, new_state) | |
| return checklist_html, new_state | |
| def show_personas(): | |
| """Switch to persona picker screen.""" | |
| return ( | |
| gr.update(visible=False), | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| ) | |
| def show_showcase(): | |
| """Go back to showcase screen.""" | |
| return ( | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| gr.update(visible=False), | |
| ) | |
| # ββ Build UI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| THEME = gr.themes.Base( | |
| primary_hue="orange", | |
| neutral_hue="slate", | |
| font=gr.themes.GoogleFont("DM Sans"), | |
| ).set( | |
| body_background_fill="#0a0e17", | |
| block_background_fill="#111827", | |
| block_border_color="rgba(255,157,0,0.12)", | |
| button_primary_background_fill="#FF9D00", | |
| button_primary_text_color="#0a0e17", | |
| button_secondary_background_fill="#111827", | |
| button_secondary_text_color="#e6edf3", | |
| ) | |
| APP_CSS = """ | |
| .gradio-container { background: #0a0e17 !important; } | |
| .gr-panel, .gr-box { background: transparent !important; border: none !important; } | |
| footer { display: none !important; } | |
| button.gr-button { font-family: 'Syne', sans-serif !important; } | |
| .contain { max-width: 600px; margin: 0 auto; } | |
| """ | |
| with gr.Blocks(title="HF PRO Onboarding") as demo: | |
| # ββ State ββ | |
| s_username = gr.State("") | |
| s_user_info = gr.State({}) | |
| s_checklist = gr.State({}) | |
| s_persona_key = gr.State("") | |
| with gr.Column(elem_classes="contain"): | |
| # ββ Screen 1: Showcase ββ | |
| with gr.Column(visible=True) as screen_showcase: | |
| showcase_html = gr.HTML(value=build_showcase_html("", {})) | |
| with gr.Column(visible=True) as username_area: | |
| username_input = gr.Textbox( | |
| placeholder="Enter your HF username (optional)", | |
| label="", | |
| scale=4, | |
| container=False, | |
| ) | |
| btn_greet = gr.Button("Personalize β", variant="secondary", size="sm") | |
| btn_go_personas = gr.Button( | |
| "I'm new to PRO β show me around β", | |
| variant="primary", | |
| size="lg", | |
| ) | |
| btn_already_pro = gr.Button( | |
| "Already PRO? Jump to your personalized guide", | |
| variant="secondary", | |
| size="sm", | |
| ) | |
| # ββ Screen 2: Persona Picker ββ | |
| with gr.Column(visible=False) as screen_personas: | |
| gr.HTML(f""" | |
| {SHARED_CSS} | |
| <div class="wrap" style="text-align:center;margin-bottom:1.5rem"> | |
| <h1 class="title" style="font-size:24px">What brings you<br>to <em>PRO?</em></h1> | |
| <p class="subtitle" style="margin-bottom:0">Pick the one that fits β we'll build your personalized checklist.</p> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| btn_runner = gr.Button("π§ͺ Run & explore models\nTry models, run inference, experiment hands-on", variant="secondary") | |
| btn_builder = gr.Button("π οΈ Build a Space or app\nDeploy a demo, tool, or full application", variant="secondary") | |
| with gr.Row(): | |
| btn_trainer = gr.Button("π Train or fine-tune\nFine-tune a model on my own data", variant="secondary") | |
| btn_explorer = gr.Button("πΊοΈ Just exploring\nI'm new here β show me the best place to start", variant="secondary") | |
| btn_back_to_showcase = gr.Button("β Back", variant="secondary", size="sm") | |
| # ββ Screen 3: Checklist Guide ββ | |
| with gr.Column(visible=False) as screen_guide: | |
| guide_html = gr.HTML() | |
| gr.HTML("<div style='font-family:Syne,sans-serif;font-size:11px;font-weight:700;letter-spacing:0.08em;text-transform:uppercase;color:#555f6d;margin:1rem 0 0.5rem'>Toggle items to track your progress</div>") | |
| with gr.Row(): | |
| toggle_1 = gr.Button("Toggle item 1", variant="secondary", size="sm") | |
| toggle_2 = gr.Button("Toggle item 2", variant="secondary", size="sm") | |
| with gr.Row(): | |
| toggle_3 = gr.Button("Toggle item 3", variant="secondary", size="sm") | |
| toggle_4 = gr.Button("Toggle item 4", variant="secondary", size="sm") | |
| btn_change_goal = gr.Button("β Change my goal", variant="secondary", size="sm") | |
| # ββ Analytics (team-only section) ββ | |
| with gr.Accordion("π Persona Analytics (Support Team)", open=False): | |
| analytics_md = gr.Markdown("Click refresh to load.") | |
| btn_refresh_analytics = gr.Button("Refresh Analytics", variant="secondary", size="sm") | |
| # ββ Wire up toggle labels dynamically ββ | |
| def update_toggle_labels(persona_key): | |
| if not persona_key or persona_key not in PERSONAS: | |
| return [gr.update()] * 4 | |
| cards = PERSONAS[persona_key]["cards"] | |
| updates = [] | |
| for i in range(4): | |
| if i < len(cards): | |
| updates.append(gr.update(value=f"{cards[i]['icon']} {cards[i]['label']}")) | |
| else: | |
| updates.append(gr.update(value=f"Item {i+1}", visible=False)) | |
| return updates | |
| # ββ Event handlers ββ | |
| # Username personalization | |
| greet_outputs = [ | |
| showcase_html, s_username, s_user_info, s_checklist, | |
| screen_showcase, screen_personas, screen_guide, username_area, | |
| ] | |
| btn_greet.click(on_greet, inputs=[username_input], outputs=greet_outputs) | |
| username_input.submit(on_greet, inputs=[username_input], outputs=greet_outputs) | |
| btn_go_personas.click( | |
| show_personas, | |
| inputs=[], | |
| outputs=[screen_showcase, screen_personas, screen_guide], | |
| ) | |
| btn_already_pro.click( | |
| show_personas, | |
| inputs=[], | |
| outputs=[screen_showcase, screen_personas, screen_guide], | |
| ) | |
| btn_back_to_showcase.click( | |
| show_showcase, | |
| inputs=[], | |
| outputs=[screen_showcase, screen_personas, screen_guide], | |
| ) | |
| btn_change_goal.click( | |
| show_personas, | |
| inputs=[], | |
| outputs=[screen_showcase, screen_personas, screen_guide], | |
| ) | |
| # Persona buttons | |
| for btn, key in [ | |
| (btn_runner, "runner"), | |
| (btn_builder, "builder"), | |
| (btn_trainer, "trainer"), | |
| (btn_explorer, "explorer"), | |
| ]: | |
| def make_persona_fn(k): | |
| def fn(username, checklist_state): | |
| return on_persona_select(k, username, checklist_state) | |
| return fn | |
| def make_label_fn(k): | |
| def fn(): | |
| cards = PERSONAS[k]["cards"] | |
| return tuple( | |
| gr.update(value=f"{c['icon']} {c['label']}") if i < len(cards) | |
| else gr.update(visible=False) | |
| for i, c in enumerate((cards + [{'icon':'','label':''}]*4)[:4]) | |
| ) | |
| return fn | |
| btn.click( | |
| make_persona_fn(key), | |
| inputs=[s_username, s_checklist], | |
| outputs=[ | |
| guide_html, s_persona_key, s_checklist, | |
| screen_showcase, screen_personas, screen_guide, | |
| ], | |
| ).then( | |
| make_label_fn(key), | |
| inputs=[], | |
| outputs=[toggle_1, toggle_2, toggle_3, toggle_4], | |
| ) | |
| # Checklist toggles | |
| for toggle_btn, idx in [(toggle_1, 0), (toggle_2, 1), (toggle_3, 2), (toggle_4, 3)]: | |
| def make_toggle_fn(i): | |
| def fn(persona_key, username, checklist_state): | |
| if not persona_key or persona_key not in PERSONAS: | |
| return guide_html, checklist_state | |
| cards = PERSONAS[persona_key]["cards"] | |
| if i >= len(cards): | |
| return guide_html, checklist_state | |
| item_id = cards[i]["id"] | |
| return on_checklist_toggle(item_id, persona_key, username, checklist_state) | |
| return fn | |
| toggle_btn.click( | |
| make_toggle_fn(idx), | |
| inputs=[s_persona_key, s_username, s_checklist], | |
| outputs=[guide_html, s_checklist], | |
| ) | |
| # Analytics | |
| btn_refresh_analytics.click( | |
| lambda: get_analytics_summary(), | |
| outputs=analytics_md, | |
| ) | |
| demo.launch(css=APP_CSS, theme=THEME) |