PRO-onboarding / app.py
meganariley's picture
meganariley HF Staff
Fix: remove demo.load, use static default + username input
9de0855 verified
"""
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)