registration / app.py
ysharma's picture
ysharma HF Staff
Update app.py
7d74abb verified
import gradio as gr
from datetime import datetime
from datasets import Dataset, load_dataset
from huggingface_hub import login, HfApi
import pandas as pd
import os
import time
import tempfile
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
HF_TOKEN = os.environ.get("HF_TOKEN")
if HF_TOKEN:
login(token=HF_TOKEN)
logger.info("Authenticated with Hugging Face")
else:
logger.warning("HF_TOKEN not found - running without authentication")
DATASET_NAME = "build-small-hackathon/build-small-hackathon-registrations"
# Auto-backups land under the user namespace (not the org) so the org's dataset
# list isn't polluted with thousands of timestamped backup repos. The HF_TOKEN
# secret on the Space must have write access to BOTH the org dataset (above)
# and this user namespace.
BACKUP_DATASET_PREFIX = "ysharma/build-small-hackathon-registrations-auto-backup"
DISCORD_INVITE = "https://discord.gg/YHECTft87Z"
DISCORD_CHANNEL = "build-small-hackathon-official"
HEADER_HTML = """
<div class="bsh-banner">
<h1>πŸͺ΅ Build Small Hackathon</h1>
<div class="tagline">Making AI Fun Again</div>
<div class="meta">May 29 – June 8, 2026 Β· Online &amp; Global</div>
<div class="placeholder">πŸ—ΊοΈ event artwork being illustrated Β· placeholder for now</div>
</div>
"""
INFO_ROW_HTML = """
<div class="bsh-info-row">
<div class="bsh-card">
<div class="bsh-title">❋ Two Tracks</div>
<div class="bsh-content">
<div class="bsh-track">
<span class="bsh-emoji">🏑</span>
<span class="bsh-track-name">Backyard AI</span>
</div>
<div class="bsh-track-desc">Solve a real problem for someone you know</div>
<div class="bsh-track">
<span class="bsh-emoji">πŸ„</span>
<span class="bsh-track-name">Thousand Token Wood</span>
</div>
<div class="bsh-track-desc">Build something delightful and whimsical</div>
</div>
</div>
<div class="bsh-card">
<div class="bsh-title">✦ Three Rules</div>
<ol class="bsh-rules">
<li>Models ≀ 32B parameters</li>
<li>Built on Gradio + Spaces</li>
<li>Demo video + social post</li>
</ol>
</div>
<div class="bsh-card">
<div class="bsh-title">🎁 Prizes &amp; Credits</div>
<div class="bsh-content">
<div class="bsh-soon"><em>Sponsor lineup, prize pool, and free API/compute credits announcing soon β€” sit tight, friend.</em></div>
<div class="bsh-link">β†’ <a href="https://huggingface.co/build-small-hackathon" target="_blank">Read the chapter book</a></div>
</div>
</div>
</div>
"""
DATES_ROW_HTML = """
<div class="bsh-dates-row">
<div class="bsh-dates">
<div class="bsh-title">✦ Trail Map · Key Dates</div>
<div class="bsh-dates-grid">
<div>πŸ“… <b>Reg closes:</b> Wed, May 27 Β· 23:59 UTC</div>
<div>🎟️ <b>Credits:</b> Thu, May 28</div>
<div>πŸš€ <b>Hack opens:</b> Fri, May 29</div>
<div>🏁 <b>Submit by:</b> Mon, June 8</div>
</div>
<div class="bsh-warn">β›” No registrations accepted once the event starts. Lock it in before May 27!</div>
</div>
<div class="bsh-counter">
<div class="bsh-counter-title">πŸ•οΈ Hack opens in</div>
<div class="bsh-counter-target">May 29, 2026 Β· 00:00 UTC</div>
<div class="bsh-nums">
<div class="bsh-unit"><span class="bsh-num" id="bsh-days">--</span><span class="bsh-lbl">Days</span></div>
<div class="bsh-unit"><span class="bsh-num" id="bsh-hours">--</span><span class="bsh-lbl">Hrs</span></div>
<div class="bsh-unit"><span class="bsh-num" id="bsh-mins">--</span><span class="bsh-lbl">Min</span></div>
<div class="bsh-unit"><span class="bsh-num" id="bsh-secs">--</span><span class="bsh-lbl">Sec</span></div>
</div>
</div>
</div>
"""
DISCORD_RIBBON_HTML = f"""
<div class="bsh-discord">
πŸ’¬ Join the <a href="{DISCORD_INVITE}" target="_blank">Gradio Discord</a> Β· channel <code>{DISCORD_CHANNEL}</code> Β· office hours, AMAs, and support.
</div>
"""
COUNTDOWN_JS = """
() => {
const targetDate = new Date(Date.UTC(2026, 4, 29, 0, 0, 0));
function update() {
const now = new Date();
const diff = targetDate - now;
let d, h, m, s;
if (diff <= 0) {
d = h = m = s = 0;
} else {
d = Math.floor(diff / 86400000);
h = Math.floor((diff % 86400000) / 3600000);
m = Math.floor((diff % 3600000) / 60000);
s = Math.floor((diff % 60000) / 1000);
}
const set = (id, v) => {
const el = document.getElementById(id);
if (el) el.textContent = String(v).padStart(2, '0');
};
set('bsh-days', d);
set('bsh-hours', h);
set('bsh-mins', m);
set('bsh-secs', s);
}
update();
setInterval(update, 1000);
}
"""
def safe_add_to_dataset(registration_data, max_retries=5, retry_delay=3):
"""Append a new registration; refuse to write if existing rows can't be loaded (data-loss guard)."""
try:
logger.info("Starting new registration process")
new_row = {
"timestamp": registration_data["timestamp"],
"full_name": registration_data["personal_info"]["full_name"],
"email": registration_data["personal_info"]["email"],
"hf_username": registration_data["personal_info"]["hf_username"],
"gradio_usage": registration_data["personal_info"]["gradio_usage"],
"track_interest": registration_data["participation"]["track_interest"],
"previous_participation": str(registration_data["participation"]["previous_participation"]),
"experience_level": registration_data["participation"]["experience_level"],
"how_heard": registration_data["participation"]["how_heard"],
"planned_small_model": registration_data["build_small"]["planned_small_model"] or "",
"bonus_quests": str(registration_data["build_small"]["bonus_quests"]),
"project_description": registration_data["additional"]["project_description"] or "",
}
existing_df = None
load_successful = False
for attempt in range(max_retries):
logger.info(f"Loading attempt {attempt + 1}/{max_retries}")
try:
api = HfApi()
files = api.list_repo_files(DATASET_NAME, repo_type="dataset")
parquet_files = [f for f in files if f.endswith('.parquet') and 'train' in f]
if parquet_files:
logger.info(f"Found parquet file: {parquet_files[0]}")
with tempfile.TemporaryDirectory() as temp_dir:
parquet_file = api.hf_hub_download(
repo_id=DATASET_NAME,
filename=parquet_files[0],
repo_type="dataset",
cache_dir=temp_dir,
force_download=True,
)
existing_df = pd.read_parquet(parquet_file)
logger.info(f"Successfully loaded {len(existing_df)} existing rows")
load_successful = True
break
else:
logger.warning("No parquet files found")
except Exception as load_error:
logger.warning(f"Attempt {attempt + 1} failed: {str(load_error)[:100]}")
if attempt < max_retries - 1:
logger.info(f"Waiting {retry_delay} seconds before retry...")
time.sleep(retry_delay)
continue
if not load_successful or existing_df is None:
logger.error("CRITICAL SAFETY ERROR: Could not load existing dataset after multiple attempts.")
logger.error("REFUSING to proceed to prevent data loss!")
return False, (
"❌ Registration temporarily unavailable due to technical issues. "
"Please try again in a few minutes. If the problem persists, contact support."
)
if len(existing_df) > 0:
duplicate_check = existing_df[
(existing_df['email'].str.lower() == new_row['email'].lower())
| (existing_df['hf_username'].str.lower() == new_row['hf_username'].lower())
]
if len(duplicate_check) > 0:
logger.warning("Duplicate registration attempt detected")
return False, "❌ Error: This email or Hugging Face username is already registered."
combined_df = pd.concat([existing_df, pd.DataFrame([new_row])], ignore_index=True)
logger.info(f"Combined data now has {len(combined_df)} rows (was {len(existing_df)})")
backup_timestamp = int(time.time())
try:
updated_dataset = Dataset.from_pandas(combined_df)
backup_name = f"{BACKUP_DATASET_PREFIX}-{backup_timestamp}"
logger.info(f"Creating backup: {backup_name}")
updated_dataset.push_to_hub(backup_name, private=True)
logger.info("Pushing to main dataset...")
updated_dataset.push_to_hub(DATASET_NAME, private=True)
logger.info(f"Successfully saved registration. Total rows: {len(combined_df)}")
time.sleep(2)
try:
api.list_repo_files(DATASET_NAME, repo_type="dataset")
logger.info("Upload verification: Files updated successfully")
except Exception:
logger.warning("Could not verify upload (this may be normal)")
return True, "Registration successful!"
except Exception as upload_error:
error_msg = str(upload_error).lower()
if any(indicator in error_msg for indicator in ['rate limit', '429', 'too many requests']):
logger.warning("Rate limit hit - registration system temporarily busy")
return False, "⏳ Registration temporarily unavailable due to high server load. Please try again in 10–15 minutes."
logger.error(f"Upload failed: {upload_error}")
return False, f"❌ Registration failed during upload: {str(upload_error)}"
except Exception as e:
logger.error(f"Unexpected error in registration: {e}")
import traceback
traceback.print_exc()
return False, f"❌ Registration failed: {str(e)}"
def verify_registration(email, hf_username):
"""Verify a registration by exact email + HF username match (case-insensitive)."""
try:
if not email or not email.strip():
return "❌ Please enter your email address"
if not hf_username or not hf_username.strip():
return "❌ Please enter your Hugging Face username"
try:
dataset = load_dataset(DATASET_NAME, split="train")
df = dataset.to_pandas()
logger.info(f"Loaded dataset with {len(df)} registrations")
except Exception as load_error:
logger.error(f"Failed to load dataset: {load_error}")
return "❌ Unable to verify registration at this time. Please try again later."
if len(df) == 0:
return "❌ No registration found with this email and Hugging Face username combination."
email_lower = email.strip().lower()
username_lower = hf_username.strip().lower()
match = df[
(df['email'].str.lower() == email_lower)
& (df['hf_username'].str.lower() == username_lower)
]
if len(match) == 0:
email_exists = df[df['email'].str.lower() == email_lower]
username_exists = df[df['hf_username'].str.lower() == username_lower]
if len(email_exists) > 0 and len(username_exists) == 0:
return "❌ Email found but Hugging Face username doesn't match. Please check your username."
if len(username_exists) > 0 and len(email_exists) == 0:
return "❌ Hugging Face username found but email doesn't match. Please check your email."
return "❌ No registration found with this email and Hugging Face username combination."
registration = match.iloc[0]
try:
timestamp = pd.to_datetime(registration['timestamp'])
reg_date = timestamp.strftime("%B %d, %Y at %I:%M %p UTC")
except Exception:
reg_date = registration['timestamp']
prev = registration['previous_participation']
if isinstance(prev, str):
prev = prev.strip("[]'\"").replace("'", "")
quests = registration['bonus_quests'] if 'bonus_quests' in registration.index else ''
if isinstance(quests, str):
quests = quests.strip("[]'\"").replace("'", "")
quests_str = quests if quests else "_None selected_"
planned_model = registration['planned_small_model'] if 'planned_small_model' in registration.index else ''
planned_model_str = planned_model if planned_model else "_Not specified_"
result = f"""
## βœ… Registration Confirmed!
**Participant Details:**
- **Full Name:** {registration['full_name']}
- **Email:** {registration['email']}
- **Hugging Face Username:** {registration['hf_username']}
- **Registered On:** {reg_date}
**Hackathon Participation:**
- **Track Interest:** {registration['track_interest']}
- **Hackathon Experience:** {prev}
- **Experience Level:** {registration['experience_level']}
- **Gradio Usage:** {registration['gradio_usage']}
- **How You Heard:** {registration['how_heard']}
**Build Small Plans:**
- **Small Model You Plan to Use:** {planned_model_str}
- **Bonus Quests of Interest:** {quests_str}
**Project Idea:**
{registration['project_description'] if registration['project_description'] else '_No project description provided_'}
---
**Next Steps:**
- 🎟️ API & compute credits will be assigned on **Thu, May 28, 2026**
- πŸš€ Hack window opens **Fri, May 29, 2026**
- 🏁 Submissions due **Mon, June 8, 2026**
- πŸ’¬ Join the Gradio Discord channel `{DISCORD_CHANNEL}`: {DISCORD_INVITE}
- πŸ“§ Watch your email for important updates
"""
logger.info(f"Verification successful for {email}")
return result
except Exception as e:
logger.error(f"Error during verification: {e}")
import traceback
traceback.print_exc()
return f"❌ An error occurred during verification: {str(e)}"
def submit_registration(full_name, email, hf_username, gradio_usage,
track_interest, previous_participation, experience_level, how_heard,
planned_small_model, bonus_quests,
acknowledgment, project_description):
"""Validate and submit a registration."""
if not full_name or not full_name.strip():
return "❌ Error: Please enter your full name"
if not email or not email.strip():
return "❌ Error: Please enter your email address"
if not hf_username or not hf_username.strip():
return "❌ Error: Please enter your Hugging Face username"
if not gradio_usage:
return "❌ Error: Please select how you're currently using Gradio"
if not track_interest:
return "❌ Error: Please select your preferred track"
if not previous_participation:
return "❌ Error: Please select at least one option for hackathon experience"
if not experience_level:
return "❌ Error: Please select your experience level"
if not how_heard:
return "❌ Error: Please select how you heard about this hackathon"
if not acknowledgment:
return "❌ Error: Please confirm your acknowledgment to participate"
import re
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(email_pattern, email.strip()):
return "❌ Error: Please enter a valid email address"
registration_data = {
"timestamp": datetime.now().isoformat(),
"personal_info": {
"full_name": full_name.strip(),
"email": email.strip().lower(),
"hf_username": hf_username.strip(),
"gradio_usage": gradio_usage,
},
"participation": {
"track_interest": track_interest,
"previous_participation": previous_participation,
"experience_level": experience_level,
"how_heard": how_heard,
},
"build_small": {
"planned_small_model": planned_small_model.strip() if planned_small_model else None,
"bonus_quests": bonus_quests or [],
},
"additional": {
"project_description": project_description.strip() if project_description else None,
},
}
success, message = safe_add_to_dataset(registration_data)
if not success:
return f"❌ Registration failed: {message}"
return f"""βœ… Registration Successful!
Thank you, **{full_name}**! Your spot is locked in.
🎟️ Credits assigned **Thu, May 28** Β· πŸš€ Hack opens **Fri, May 29** Β· 🏁 Submissions due **Mon, Jun 8**.<br>
πŸ’¬ Join the Gradio Discord channel `{DISCORD_CHANNEL}`: {DISCORD_INVITE}<br>
πŸ“§ Watch your email for credits and updates.
**See you in the woods! πŸ„πŸͺ΅**"""
def check_dataset_health():
try:
api = HfApi()
files = api.list_repo_files(DATASET_NAME, repo_type="dataset")
parquet_files = [f for f in files if f.endswith('.parquet')]
if parquet_files:
logger.info(f"Dataset health check passed - found {len(parquet_files)} parquet files")
return True
logger.warning("Dataset health check: No parquet files found (did you run seed_dataset.py?)")
return False
except Exception as e:
logger.error(f"Dataset health check failed: {e}")
return False
logger.info("Starting Build Small Hackathon Registration System")
logger.info(f"Dataset: {DATASET_NAME}")
if check_dataset_health():
logger.info("System ready - dataset is healthy")
else:
logger.warning("System starting with dataset health warnings")
custom_css = """
.gradio-container {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
/* Brand color tokens β€” light mode defaults */
--bsh-card-bg: #fbf6e8;
--bsh-card-border: rgba(139, 111, 71, 0.30);
--bsh-text: #2a1d0a;
--bsh-text-soft: #6b4423;
--bsh-accent: #4a7c2e;
--bsh-accent-deep: #2d5016;
--bsh-banner-text: #f5ecd9;
--bsh-warn: #8b2e25;
--bsh-warn-bg: rgba(178, 60, 50, 0.10);
--bsh-warn-border: rgba(178, 60, 50, 0.40);
--bsh-counter-bg: linear-gradient(135deg, #2d5016 0%, #1f3a0f 100%);
--bsh-counter-num: #f5ecd9;
--bsh-counter-label: #c9b072;
--bsh-banner-bg: linear-gradient(135deg, #2d5016 0%, #4a7c2e 50%, #6b9039 100%);
--bsh-discord-bg: rgba(74, 124, 46, 0.08);
--bsh-link: #4a7c2e;
}
.dark .gradio-container,
body.dark .gradio-container,
.gradio-container.dark {
/* Brand color tokens β€” dark mode overrides */
--bsh-card-bg: #1f1a12;
--bsh-card-border: rgba(201, 176, 114, 0.35);
--bsh-text: #ede1c3;
--bsh-text-soft: #c9b072;
--bsh-accent: #9bc466;
--bsh-accent-deep: #6b9039;
--bsh-banner-text: #f5ecd9;
--bsh-warn: #ff9b8e;
--bsh-warn-bg: rgba(255, 122, 110, 0.12);
--bsh-warn-border: rgba(255, 122, 110, 0.40);
--bsh-counter-bg: linear-gradient(135deg, #1a2e0d 0%, #0c1606 100%);
--bsh-counter-num: #f5ecd9;
--bsh-counter-label: #c9b072;
--bsh-banner-bg: linear-gradient(135deg, #1a2e0d 0%, #2d5016 60%, #4a7c2e 100%);
--bsh-discord-bg: rgba(155, 196, 102, 0.08);
--bsh-link: #9bc466;
}
/* Banner */
.bsh-banner {
background: var(--bsh-banner-bg);
color: var(--bsh-banner-text);
border-radius: 14px;
padding: 20px 22px;
margin: 0 0 12px 0;
text-align: center;
border: 1px solid var(--bsh-card-border);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
}
.bsh-banner h1 {
margin: 0 0 4px 0 !important;
font-size: 28px !important;
font-weight: 800 !important;
letter-spacing: -0.5px !important;
color: var(--bsh-banner-text) !important;
line-height: 1.15 !important;
}
.bsh-banner .tagline {
font-size: 16px;
font-style: italic;
opacity: 0.92;
margin-bottom: 4px;
}
.bsh-banner .meta {
font-size: 14px;
opacity: 0.85;
}
.bsh-banner .placeholder {
margin-top: 12px;
display: inline-block;
background: rgba(245, 236, 217, 0.12);
border: 1px dashed rgba(245, 236, 217, 0.40);
border-radius: 6px;
padding: 4px 12px;
font-size: 11px;
opacity: 0.80;
letter-spacing: 0.3px;
}
/* Three-card info row */
.bsh-info-row {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
margin-bottom: 12px;
}
.bsh-card {
background: var(--bsh-card-bg);
border: 1px solid var(--bsh-card-border);
border-radius: 10px;
padding: 12px 14px;
color: var(--bsh-text);
}
.bsh-title {
color: var(--bsh-text-soft);
font-size: 11px;
font-weight: 800;
letter-spacing: 1.5px;
text-transform: uppercase;
margin-bottom: 8px;
}
.bsh-content {
font-size: 13.5px;
line-height: 1.55;
color: var(--bsh-text);
}
.bsh-track {
display: flex;
align-items: baseline;
gap: 8px;
margin-top: 4px;
}
.bsh-emoji { font-size: 16px; line-height: 1; }
.bsh-track-name {
font-weight: 700;
color: var(--bsh-accent);
font-size: 13.5px;
}
.bsh-track-desc {
font-size: 12px;
opacity: 0.82;
color: var(--bsh-text);
padding-left: 24px;
margin-bottom: 4px;
line-height: 1.4;
}
.bsh-rules {
margin: 0 !important;
padding-left: 18px !important;
color: var(--bsh-text);
}
.bsh-rules li {
font-size: 13.5px;
line-height: 1.7;
color: var(--bsh-text);
}
.bsh-soon {
font-size: 13px;
line-height: 1.55;
color: var(--bsh-text);
opacity: 0.92;
}
.bsh-link {
margin-top: 6px;
font-size: 12px;
}
.bsh-link a {
color: var(--bsh-link);
font-weight: 600;
text-decoration: underline;
}
/* Dates + countdown row */
.bsh-dates-row {
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 10px;
margin-bottom: 14px;
}
.bsh-dates {
background: var(--bsh-card-bg);
border: 1px solid var(--bsh-card-border);
border-radius: 10px;
padding: 12px 14px;
color: var(--bsh-text);
}
.bsh-dates-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px 14px;
font-size: 13px;
color: var(--bsh-text);
}
.bsh-dates-grid b { color: var(--bsh-accent); font-weight: 700; }
.bsh-warn {
margin-top: 10px;
background: var(--bsh-warn-bg);
border: 1px solid var(--bsh-warn-border);
border-radius: 6px;
padding: 6px 10px;
color: var(--bsh-warn);
font-size: 12px;
font-weight: 600;
text-align: center;
}
.bsh-counter {
background: var(--bsh-counter-bg);
border: 1px solid var(--bsh-card-border);
border-radius: 10px;
padding: 12px 14px;
color: var(--bsh-counter-num);
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
}
.bsh-counter-title {
color: var(--bsh-counter-num);
font-size: 11px;
letter-spacing: 1.5px;
font-weight: 800;
text-transform: uppercase;
margin-bottom: 2px;
}
.bsh-counter-target {
color: var(--bsh-counter-label);
font-size: 11px;
font-style: italic;
margin-bottom: 8px;
opacity: 0.85;
}
.bsh-nums {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 4px;
}
.bsh-unit {
display: flex;
flex-direction: column;
align-items: center;
}
.bsh-num {
font-family: 'Courier New', monospace;
font-size: 22px;
font-weight: 700;
color: var(--bsh-counter-num);
line-height: 1;
}
.bsh-lbl {
font-size: 9px;
letter-spacing: 1px;
color: var(--bsh-counter-label);
margin-top: 4px;
text-transform: uppercase;
}
/* Discord ribbon */
.bsh-discord {
margin-top: 14px;
background: var(--bsh-discord-bg);
border: 1px solid var(--bsh-card-border);
border-radius: 8px;
padding: 10px 14px;
text-align: center;
color: var(--bsh-text);
font-size: 13px;
}
.bsh-discord a {
color: var(--bsh-link);
font-weight: 700;
text-decoration: underline;
}
.bsh-discord code {
background: var(--bsh-discord-bg);
border: 1px solid var(--bsh-card-border);
padding: 1px 6px;
border-radius: 4px;
font-size: 11.5px;
color: var(--bsh-text);
}
/* Buttons */
#submit-btn {
background: linear-gradient(135deg, #4a7c2e 0%, #2d5016 100%) !important;
border: 1px solid #6b4423 !important;
color: #f5ecd9 !important;
font-weight: 700 !important;
font-size: 16px !important;
padding: 12px 24px !important;
border-radius: 10px !important;
box-shadow: 0 3px 10px rgba(74, 49, 16, 0.18) !important;
transition: all 0.25s ease !important;
}
#submit-btn:hover {
transform: translateY(-1px) !important;
box-shadow: 0 5px 14px rgba(74, 49, 16, 0.28) !important;
background: linear-gradient(135deg, #5a8c3e 0%, #3d6020 100%) !important;
}
#verify-btn {
background: #6b4423 !important;
border: 1px solid #c9b072 !important;
color: #f5ecd9 !important;
font-weight: 700 !important;
font-size: 14px !important;
padding: 10px 20px !important;
border-radius: 8px !important;
transition: all 0.25s ease !important;
}
#verify-btn:hover {
transform: translateY(-1px) !important;
background: #7d5430 !important;
}
/* Responsive */
@media (max-width: 760px) {
.bsh-info-row { grid-template-columns: 1fr; }
.bsh-dates-row { grid-template-columns: 1fr; }
.bsh-banner h1 { font-size: 24px !important; }
}
"""
with gr.Blocks(
title="Build Small Hackathon β€” Registration",
css=custom_css,
theme=gr.themes.Soft(primary_hue="green", secondary_hue="amber", neutral_hue="stone"),
) as demo:
gr.HTML(HEADER_HTML)
gr.HTML(INFO_ROW_HTML)
gr.HTML(DATES_ROW_HTML)
with gr.Tabs():
with gr.Tab("πŸ“ Register"):
with gr.Row():
with gr.Column():
gr.Markdown("### ❋ 1. Personal Information")
full_name = gr.Textbox(
label="Full Name *",
placeholder="Your full name as you'd like it on certificates",
max_lines=1,
)
email = gr.Textbox(
label="Email Address *",
placeholder="Primary contact email β€” we'll send updates and credits info here",
max_lines=1,
)
hf_username = gr.Textbox(
label="Hugging Face Username *",
placeholder="Required for org access and submissions",
max_lines=1,
)
gradio_usage = gr.Radio(
label="How are you currently using Gradio? *",
choices=[
"Professional work β€” my company uses Gradio",
"Personal projects β€” building side projects",
"Academic/Research β€” university or research work",
"Learning β€” new to Gradio, want to learn",
"Not using yet β€” interested to start",
],
info="Helps us understand the community better",
)
with gr.Column():
gr.Markdown("### ❋ 2. Hackathon Participation")
track_interest = gr.Radio(
label="Which track interests you most? *",
choices=[
"🏑 Backyard AI",
"πŸ„ An Adventure in Thousand Token Wood",
"Both",
"Undecided / Not sure yet",
],
)
previous_participation = gr.CheckboxGroup(
label="Hackathon experience (select all that apply) *",
choices=[
"MCP 1st Birthday",
"Agents & MCP Hackathon",
"Other AI hackathons",
"First timer",
],
)
experience_level = gr.Radio(
label="Your experience with AI/Agents development *",
choices=[
"Beginner β€” new to AI development",
"Intermediate β€” some AI projects",
"Advanced β€” regular AI developer",
"Expert β€” professional AI engineer",
],
)
how_heard = gr.Dropdown(
label="How did you hear about this hackathon? *",
choices=[
"Hugging Face email/newsletter",
"Build Small org page",
"Twitter/X",
"LinkedIn",
"Gradio Discord",
"From a colleague/friend",
"YouTube",
"Reddit",
"Sponsor announcement",
"Previous Gradio hackathon",
"Other",
],
)
with gr.Row():
with gr.Column():
gr.Markdown("### ❋ 3. Build Small Specifics")
planned_small_model = gr.Textbox(
label="Which small model are you planning to use? (optional)",
placeholder="e.g. Llama-3.2-3B, Qwen2.5-7B, gemma-2-2b-it, SmolLM-1.7B...",
info="Reminder: ≀ 32B parameters. Leave blank if you haven't decided.",
max_lines=1,
)
bonus_quests = gr.CheckboxGroup(
label="Which bonus quests / merit badges interest you? (optional)",
choices=[
"πŸ”Œ Off the Grid β€” no cloud APIs",
"🎯 Well-Tuned β€” use a fine-tuned published model",
"🎨 Off-Brand β€” custom Gradio frontend",
"πŸ¦™ Llama Champion β€” use llama.cpp runtime",
"πŸ“‘ Sharing is Caring β€” share an agent trace",
"πŸ““ Field Notes β€” write a blog post / report",
],
info="Pick as many as you'd like. Just signal for us β€” you can change your mind during the event.",
)
with gr.Column():
gr.Markdown("### ❋ 4. Project Idea (optional)")
project_description = gr.Textbox(
label="What are you most excited to build?",
placeholder="A neighbor's recipe-translator? A whimsical mushroom-identifier? Tell us in a sentence or two.",
lines=4,
)
gr.Markdown("### ❋ 5. Acknowledgment")
acknowledgment = gr.Checkbox(
label="I'm in. *",
info=(
"I commit to actively participate, build with a model ≀ 32B parameters, "
"host on a Gradio Hugging Face Space, and submit a project (with demo video and social post) "
"by Mon, June 8, 2026. I understand that any API/compute credits provided are intended for use on my "
"hackathon project during the event period."
),
)
submit_btn = gr.Button(
"πŸͺ΅ Register for the Build Small Hackathon",
variant="primary",
size="lg",
elem_id="submit-btn",
)
output = gr.Markdown()
def handle_registration_with_state(*args):
try:
result = submit_registration(*args)
return result, gr.Button(
"πŸͺ΅ Register for the Build Small Hackathon",
interactive=True,
variant="primary",
elem_id="submit-btn",
)
except Exception as e:
logger.error(f"Registration handling error: {e}")
return f"❌ An unexpected error occurred: {str(e)}", gr.Button(
"πŸͺ΅ Register for the Build Small Hackathon",
interactive=True,
variant="primary",
elem_id="submit-btn",
)
submit_btn.click(
fn=lambda *args: (gr.Button("⏳ Processing registration...", interactive=False, variant="secondary"), ""),
inputs=[
full_name, email, hf_username, gradio_usage,
track_interest, previous_participation, experience_level, how_heard,
planned_small_model, bonus_quests,
acknowledgment, project_description,
],
outputs=[submit_btn, output],
queue=False,
).then(
fn=handle_registration_with_state,
inputs=[
full_name, email, hf_username, gradio_usage,
track_interest, previous_participation, experience_level, how_heard,
planned_small_model, bonus_quests,
acknowledgment, project_description,
],
outputs=[output, submit_btn],
queue=True,
)
with gr.Tab("πŸ” Verify Registration"):
gr.Markdown("""
### Check Your Registration Status
Enter your email address and Hugging Face username to verify your registration and view your details. Both must match exactly (case-insensitive) for security purposes.
""")
with gr.Row():
with gr.Column():
verify_email = gr.Textbox(
label="Email Address",
placeholder="Enter your registered email",
max_lines=1,
)
verify_hf_username = gr.Textbox(
label="Hugging Face Username",
placeholder="Enter your registered HF username",
max_lines=1,
)
verify_btn = gr.Button(
"πŸ” Check Registration Status",
variant="primary",
size="lg",
elem_id="verify-btn",
)
with gr.Column():
gr.Markdown(f"""
**Need Help?**
- Use the **exact** email and username you registered with
- Both fields are case-insensitive but must match
- Registration is open until **Wed, May 27, 2026 Β· 23:59 UTC** β€” once it closes, no new entries can be added
**Support:**
- Discord: [{DISCORD_INVITE}]({DISCORD_INVITE}) (channel `{DISCORD_CHANNEL}`)
- Email: gradio-team@huggingface.co
""")
verify_output = gr.Markdown()
verify_btn.click(
fn=verify_registration,
inputs=[verify_email, verify_hf_username],
outputs=verify_output,
)
gr.HTML(DISCORD_RIBBON_HTML)
gr.Markdown(f"""
<div style="text-align: center; font-size: 12px; opacity: 0.75; margin-top: 8px;">
✦ Questions? Email gradio-team@huggingface.co ✦
</div>
""")
# Hook up the countdown JS β€” runs once on page load, then ticks every second
demo.load(fn=lambda: None, inputs=None, outputs=None, js=COUNTDOWN_JS)
if __name__ == "__main__":
demo.launch(allowed_paths=["."])