""" 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 = """ """ 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 = 'PRO' if is_pro else "" header = f"""
{"" if avatar else ""}
@{username} {pro_tag}

Welcome, {fullname.split()[0] if fullname else '@'+username} 🎉

Your PRO perks are ready to go. Here's everything unlocked for you.

""" else: header = """
Hugging Face PRO

Unlock the full
power of the Hub

PRO gives you real compute, real credits, and real storage — everything you need to stop reading and start building.

""" return f""" {SHARED_CSS}
{header}
ZeroGPU
25 min/day
🔌
API Credits
$2/month
🗄️
Storage
50GB+
🤗
Models
500k+
🚀
Spaces
400k+
🌐
Providers
20+
""" 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'{text}' for text, url in card["links"] ) items_html += f"""
{check_inner}
{card['icon']} {card['label']}
{card['desc']}
""" credits_card = """
💳
Your inference credits
Check your live $2 monthly credit balance on the HF billing page.
View balance
""" return f""" {SHARED_CSS}
{persona['title']}

{persona['intro']}

{credits_card}
Getting started checklist
{done_count} of {total} complete
{items_html}
""" # ── 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}

What brings you
to PRO?

Pick the one that fits — we'll build your personalized checklist.

""") 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("
Toggle items to track your progress
") 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)