| 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" |
| |
| |
| |
| |
| 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 & 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 & 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> |
| """) |
|
|
| |
| demo.load(fn=lambda: None, inputs=None, outputs=None, js=COUNTDOWN_JS) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch(allowed_paths=["."]) |
|
|