Spaces:
Build error
Build error
| 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" | |
| # Hero layout: banner on the left, stacked info cards on the right. | |
| HERO_HTML = """ | |
| <div class="bsh-hero-grid"> | |
| <div class="bsh-banner-img"> | |
| <img src="https://cdn-uploads.huggingface.co/production/uploads/60d2dc1007da9c17c72708f8/VhVvEN0e8oZKxjIzT9Qi0.png" | |
| alt="Build Small Hackathon β Small Models, Big Adventures" /> | |
| </div> | |
| <div class="bsh-hero-side"> | |
| <div class="bsh-hero-pair"> | |
| <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> | |
| <div class="bsh-dates"> | |
| <div class="bsh-title">β¦ Trail Map Β· Key Dates</div> | |
| <div class="bsh-dates-grid"> | |
| <div>π <b>Reg closes:</b> Wed, June 3 Β· 23:59 UTC</div> | |
| <div>ποΈ <b>Credits:</b> Thu, June 4</div> | |
| <div>π <b>Hack opens:</b> Fri, June 5 Β· 00:01 UTC</div> | |
| <div>π <b>Submit by:</b> Mon, June 15 Β· 23:59 UTC</div> | |
| </div> | |
| <div class="bsh-warn">β No registrations accepted once the event starts. Lock it in before June 3!</div> | |
| </div> | |
| <div class="bsh-counter"> | |
| <div class="bsh-counter-title">β³ Registration closes in</div> | |
| <div class="bsh-counter-target">June 3, 2026 Β· 23:59 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> | |
| </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, 5, 4, 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, June 04, 2026** | |
| - π Hack window opens **Fri, June 05, 2026** | |
| - π Submissions due **Mon, June 15, 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, June 04** Β· π Hack opens **Fri, June 05** Β· π Submissions due **Mon, Jun 15**.<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; | |
| } | |
| /* ---------- Hero grid: banner left, info cards right ---------- | |
| Right column is a 3-row flex stack: | |
| Row 1: Two Tracks + Three Rules (side-by-side 2-col subgrid) | |
| Row 2: Trail Map Β· Key Dates (full width) | |
| Row 3: Registration countdown (full width) | |
| align-items: stretch makes the right column take the banner's natural | |
| height; flex: 1 on each row distributes that height evenly. */ | |
| .bsh-hero-grid { | |
| display: grid; | |
| grid-template-columns: 1.55fr 1fr; | |
| gap: 12px; | |
| margin-bottom: 14px; | |
| align-items: stretch; | |
| } | |
| .bsh-banner-img { | |
| margin: 0; | |
| border-radius: 14px; | |
| overflow: hidden; | |
| border: 1px solid var(--bsh-card-border); | |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12); | |
| line-height: 0; /* kills the phantom gap under the img */ | |
| } | |
| .bsh-banner-img img { | |
| display: block; | |
| width: 100%; | |
| height: auto; | |
| } | |
| .bsh-hero-side { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| min-width: 0; /* lets the column shrink properly inside the grid */ | |
| height: 100%; /* fill the grid cell so the rows have room to stretch */ | |
| } | |
| /* Each row claims an equal share of the available column height. */ | |
| .bsh-hero-side > * { | |
| flex: 1 1 0; | |
| min-height: 0; | |
| } | |
| /* Row 1: pair of short cards (Two Tracks | Three Rules) side-by-side. */ | |
| .bsh-hero-pair { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 6px; | |
| } | |
| .bsh-hero-pair > .bsh-card { | |
| /* Grid stretches the cards to fill the row height by default. */ | |
| min-width: 0; | |
| } | |
| /* Safety net: clip anything that would visually leak past the card edge. | |
| With 3 rows the content now fits comfortably, but this guards against | |
| future copy edits making a card taller than its slot. */ | |
| .bsh-hero-side .bsh-card, | |
| .bsh-hero-side .bsh-dates, | |
| .bsh-hero-side .bsh-counter { | |
| overflow: hidden; | |
| } | |
| /* Compact card/text sizing for the right column. With 3 rows instead of | |
| 4 each row gets ~35% more height, so we can be less aggressive than | |
| the previous pass. */ | |
| .bsh-hero-side .bsh-card, | |
| .bsh-hero-side .bsh-dates { | |
| padding: 8px 11px; | |
| } | |
| .bsh-hero-side .bsh-counter { | |
| padding: 7px 11px; | |
| } | |
| .bsh-hero-side .bsh-title { | |
| margin-bottom: 4px; | |
| font-size: 10px; | |
| letter-spacing: 1.2px; | |
| } | |
| .bsh-hero-side .bsh-content, | |
| .bsh-hero-side .bsh-rules li { | |
| font-size: 11.5px; | |
| line-height: 1.4; | |
| } | |
| .bsh-hero-side .bsh-track { | |
| margin-top: 2px; | |
| gap: 6px; | |
| } | |
| .bsh-hero-side .bsh-emoji { | |
| font-size: 13px; | |
| } | |
| .bsh-hero-side .bsh-track-name { | |
| font-size: 12px; | |
| } | |
| .bsh-hero-side .bsh-track-desc { | |
| font-size: 10.5px; | |
| padding-left: 19px; | |
| margin-bottom: 1px; | |
| line-height: 1.3; | |
| } | |
| .bsh-hero-side .bsh-rules { | |
| padding-left: 15px !important; | |
| } | |
| .bsh-hero-side .bsh-rules li { | |
| line-height: 1.45; | |
| } | |
| .bsh-hero-side .bsh-dates-grid { | |
| font-size: 11px; | |
| gap: 3px 10px; | |
| } | |
| .bsh-hero-side .bsh-warn { | |
| margin-top: 4px; | |
| padding: 3px 7px; | |
| font-size: 10px; | |
| } | |
| .bsh-hero-side .bsh-counter-title { | |
| font-size: 10px; | |
| margin-bottom: 1px; | |
| } | |
| .bsh-hero-side .bsh-counter-target { | |
| font-size: 9px; | |
| margin-bottom: 3px; | |
| } | |
| .bsh-hero-side .bsh-num { | |
| font-size: 19px; | |
| } | |
| .bsh-hero-side .bsh-lbl { | |
| font-size: 8px; | |
| margin-top: 1px; | |
| } | |
| /* ---------- Card base styles (shared by old + new layouts) ---------- */ | |
| .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); | |
| } | |
| /* ---------- Dates card ---------- */ | |
| .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) !important; 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; | |
| } | |
| /* ---------- Countdown card ---------- */ | |
| .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) !important; | |
| text-align: center; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| } | |
| .bsh-counter-title { | |
| color: var(--bsh-counter-num) !important; | |
| font-size: 11px; | |
| letter-spacing: 1.5px; | |
| font-weight: 800; | |
| text-transform: uppercase; | |
| margin-bottom: 2px; | |
| } | |
| .bsh-counter-target { | |
| color: var(--bsh-counter-label) !important; | |
| 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) !important; | |
| line-height: 1; | |
| } | |
| .bsh-lbl { | |
| font-size: 9px; | |
| letter-spacing: 1px; | |
| color: var(--bsh-counter-label) !important; | |
| 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: collapse the hero grid on narrow screens ---------- */ | |
| @media (max-width: 760px) { | |
| .bsh-hero-grid { grid-template-columns: 1fr; } | |
| } | |
| """ | |
| 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(HERO_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 15, 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, June 03, 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=["."]) |