Deploy Hackathon Roulette
Browse files- README.md +32 -6
- app.py +712 -0
- requirements.txt +2 -0
README.md
CHANGED
|
@@ -1,13 +1,39 @@
|
|
| 1 |
---
|
| 2 |
title: Hackathon Roulette
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Hackathon Roulette
|
| 3 |
+
emoji: ๐ฐ
|
| 4 |
+
colorFrom: purple
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.16.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
short_description: Spin to discover hidden gems from the Build Small Hackathon
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# ๐ฐ Hackathon Roulette
|
| 14 |
+
|
| 15 |
+
Hundreds of projects ship during the **Build Small Hackathon** โ and most of
|
| 16 |
+
them never get seen because everyone scrolls the same first page. This is a
|
| 17 |
+
slot machine for the whole submission pool: pull the lever and get a random
|
| 18 |
+
project you haven't seen yet.
|
| 19 |
+
|
| 20 |
+
## What it does
|
| 21 |
+
|
| 22 |
+
- **Indexes every submission** in the hackathon org, live from the Hub.
|
| 23 |
+
- **Auto-categorizes** each project into 12 topic buckets (games, voice,
|
| 24 |
+
health, dev tools, creative, agents, and more) so you can browse by theme.
|
| 25 |
+
- **Roulette** โ one button serves up a random project you haven't viewed,
|
| 26 |
+
remembering what you've already seen for the session (no database needed).
|
| 27 |
+
- **Browse mode** โ open any category and scan the most-liked projects.
|
| 28 |
+
- Each result shows the name, description, category, official track badge,
|
| 29 |
+
likes, and a direct link to the Space.
|
| 30 |
+
|
| 31 |
+
## Run locally
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
pip install -r requirements.txt
|
| 35 |
+
python app.py
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
The app pulls submissions straight from the Hub at startup โ no API keys, no
|
| 39 |
+
configuration. Drop it on a Hugging Face Space and it just runs.
|
app.py
ADDED
|
@@ -0,0 +1,712 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hackathon Roulette โ spin through the Build Small Hackathon and discover
|
| 3 |
+
projects you'd never find by scrolling. Categorizes every submission and
|
| 4 |
+
lets you roulette your way to hidden gems (with in-session memory of what
|
| 5 |
+
you've already seen).
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import random
|
| 9 |
+
import re
|
| 10 |
+
import time
|
| 11 |
+
import html as _html
|
| 12 |
+
|
| 13 |
+
import gradio as gr
|
| 14 |
+
from huggingface_hub import HfApi
|
| 15 |
+
|
| 16 |
+
# โโ Config (the only hackathon this tool follows) โโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 17 |
+
|
| 18 |
+
HACKATHON_ORG = "build-small-hackathon"
|
| 19 |
+
HACKATHON_NAME = "Build Small Hackathon"
|
| 20 |
+
|
| 21 |
+
# Org spaces that aren't real submissions โ excluded from the roulette.
|
| 22 |
+
INFRA_SPACES = {"field-guide", "readme", "registration"}
|
| 23 |
+
|
| 24 |
+
# โโ Category taxonomy โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 25 |
+
# Ordered list of (name, emoji, keywords). Categorization is FIRST-MATCH-WINS,
|
| 26 |
+
# so order runs specific genres/domains first, modality next, and the broad
|
| 27 |
+
# "Assistants" bucket last as a catch-all. Keywords are matched with a LEFT
|
| 28 |
+
# word-boundary (\bkw) against a camelCase-split, lowercased blob of
|
| 29 |
+
# title+name+description+tags โ so "radio" never matches "gradio" and "voice"
|
| 30 |
+
# never matches "invoice", while stems like "transcrib" still catch
|
| 31 |
+
# "transcribe". Tuned against all 943 live submissions (~29% land in Other).
|
| 32 |
+
|
| 33 |
+
TAXONOMY = [
|
| 34 |
+
("Games, RPGs & Interactive Fiction", "๐ฎ", ["game","rpg","roguelike","roguelite","deckbuild","dungeon","escape room","escape-room","text-adventure","text adventure","adventure","interactive-fiction","interactive fiction","visual novel","visual-novel","ttrpg","tabletop","whodunit","detective","courtroom","tribunal","verdict","jury","puzzle","riddle","arcade","poker","chess","mafia","duel","arena","akinator","backrooms","parkour","phaser","playable","gameplay","roleplay","role-play","reverse-turing","reverse turing","trolley","board game","deathmatch","choose your","trivia","survival","maze","wordle","sudoku","betting","dating sim","platformer","sokoban","tower defense","idle game","mystery","heist","casino","card game"]),
|
| 35 |
+
("Worlds, Sims & Multi-Agent Societies", "๐", ["multi-agent","multi agent","simulation","simulator","persistent world","world-build","world building","world-building","world model","colony","civilization","civilisation","city-builder","city builder","village","smol town","ecosystem","terraform","self-play","agent-simulation","trading firm","rumor economy","gossip economy","tiny civilization","living forest","sandbox","life sim","tamagotchi","god game"]),
|
| 36 |
+
("Health, Medical & Wellness", "๐ฉบ", ["medical","clinical","clinic","healthcare","health companion","medgemma","patient","symptom","diagnos","radiology","x-ray","dental","pharmacy","pharma","prescription","medicine","blood test","nutrition","nutri","pregnan","caregiver","elderly","parkinson","stroke","vaccine","neoantigen","first aid","physiotherap","cardiac","heartbeat","mental-health","mental health","emotional support","anxiety","therapy","wellness","grief","adhd","neurodiverg","allergen"]),
|
| 37 |
+
("Tutors, Study & Language Learning", "๐", ["tutor","study buddy","study partner","studybuddy","study copilot","homework","exam","flashcard","quiz","socratic","lesson","teaching","classroom","kids","children","preschool","toddler","language-learning","language learning","language practice","lingo","learn korean","learn japanese","learn languages","duolingo","reading buddy","read-along","read along","dyslexi","iep","whiteboard","cbse","ncert","samacheer","scholar","literature-review","interview coach","interview prep","speech coach","running coach","gym coach","curriculum","educational","flashcard generator","spaced repetition","vocabulary","grammar","mnemonic"]),
|
| 38 |
+
("Finance, Documents & Business", "๐ผ", ["invoice","accounting","bookkeep","ledger","receipt","expense","budget","taxes","tax filing","tax season","finops","finance","financial","stocks","stock market","trading","alpha signal","sec filing","insurance","crm","inventory","udhaar","shopkeeper","kirana","dukaan","sales","pricing","price comparison","real-estate","real estate","ads advisor","resume","job description","reconcil","lease","legal","legislation","contract","bureaucra","immigration","compliance","ocr","document-ai","text-to-sql","spreadsheet","rfq","payroll","procurement","gig work","freelance"]),
|
| 39 |
+
("Scan, Vision & Physical-World Utilities", "๐ธ", ["plant","garden","crop","weed","herbicide","farm","beekeep","honeybee","apiar","soil","satellite","eurosat","land-use","solar","weather","watering","fridge","recipe","meal plan","cook","ingredient","food waste","upcycl","mushroom","forager","edible","stray animal","wardrobe","outfit","fashion","stylist","car diagnostic","appliance fault","repair","defect","building-inspection","road-defect","object-detection","yolo","vlm","vision-language","computer-vision","image classif","segment","label lens","posture","barcode","football","soccer","referee","sports"]),
|
| 40 |
+
("Safety, Security & Privacy", "๐ก๏ธ", ["scam","phishing","fraud","online-safety","online safety","cybersec","security","soc analyst","osint","threat","vuln","pii","privacy","redact","guardrail","firewall","dark pattern","deepfake","captcha","jailbreak","safety-eval","prompt-injection","airlock"]),
|
| 41 |
+
("Dev, Code, Robotics & Model Tooling", "โ๏ธ", ["code review","code-review","code-generation","commit","repo","github","mermaid","flowchart","diagram","debug","rubber duck","rubber-duck","pull request","refactor","decompile","gpu kernel","gpu-kernel","fine-tune","fine-tuning","distillation","quantiz","benchmark","structured json","data-engineering","excalidraw","skill router","skill-router","formal-verification","lean4","mcp-server","devops","reachy","robot","esp32","arduino","onnx","webgpu","transformers.js","transformers-js","rl-environment","openenv","grpo","interpretab","surprisal","next-token","abliterat","tokenizer","regex","evaluation pipeline","llm eval","sql query"]),
|
| 42 |
+
("Voice, Speech, Music & Translation", "๐๏ธ", ["voice","music","radio","voice-clon","voice clon","tts","text-to-speech","asr","speech-to-text","speech-to-speech","speech-translation","whisper","voxcpm","kokoro","f5-tts","fastconformer","dubbing","narration","narrator","read aloud","read-aloud","audiobook","podcast","music-generation","music generator","lofi","lullab","singalong","karaoke","lyric","sheet music","transcrib","pronunciation","recitation","sign language","sign-language","sign2voice","dictation","translation","translate","spoken word"]),
|
| 43 |
+
("Story, Image & Creative Generation", "๐จ", ["story","storybook","storytell","bedtime","fairytale","fairy tale","picture book","comic","manga","illustrat","poem","poetry","poetic","novel","fable","creative-writing","creative writing","narrative","text-to-image","image-generation","image generation","img2img","image-to-image","diffusion","flux","sd-turbo","sticker","pixel-art","pixel art","typograph","voxel","avatar","manim","animation","text-to-video","image-to-video","claymation","postcard","sketch","doodle","watercolour","minecraftify","generative-art","kinetic typography","icon generator","logo","wallpaper","emoji","meme","theater","theatre","puppet","myth","legend","fantasy","witch","wizard","dragon","coloring","collage","video","film"]),
|
| 44 |
+
("Companions, Personas & Reflective Toys", "๐", ["companion","waifu","persona","character chat","virtual character","desktop pet","floating desktop","virtual pet","tsundere","yandere","girlfriend","confidant","journaling","voice journal","mood journal","gratitude journal","daily journal","reflection","oneiro","oracle","divination","prophecy","fortune teller","magic 8","tarot","arcana","seance","sรฉance","occult","constellation","lives unlived","unlived","parallel life","museum of","elegy","afterlife","loneliness","mindful","stargazing","imaginary friend","pen pal","penpal","diary","gratitude","affirmation","horoscope","zodiac","astrolog"]),
|
| 45 |
+
("Assistants, Agents & Productivity", "๐ฌ", ["assistant","co-pilot","copilot","chatbot","chat assistant","chat with","agent","helper","concierge","butler","productivity","to-do","todo","task manager","note-taking","note taking","notetaking","meeting notes","summari","email","inbox","calendar","schedul","planner","reminder","advisor","recommend","knowledge base","knowledge hub","knowledgehub","second brain","retrieval-augmented","rag ","search engine","web search","personal ai","local ai","offline ai","home assistant","conversational","workflow","support","docu","research assistant","news","digest","faq","automation"]),
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
OTHER_NAME, OTHER_EMOJI = "Other", "๐"
|
| 49 |
+
|
| 50 |
+
CAT_EMOJI = {name: emoji for name, emoji, _ in TAXONOMY}
|
| 51 |
+
CAT_EMOJI[OTHER_NAME] = OTHER_EMOJI
|
| 52 |
+
CAT_EMOJI["All"] = "๐ฒ"
|
| 53 |
+
|
| 54 |
+
# Filter choices: All, every real category, then Other (the grab-bag).
|
| 55 |
+
ALL_CATEGORIES = ["All"] + [name for name, _, _ in TAXONOMY] + [OTHER_NAME]
|
| 56 |
+
|
| 57 |
+
# Pre-compile keyword patterns once (left word-boundary).
|
| 58 |
+
_COMPILED = [(name, [re.compile(r"\b" + re.escape(k)) for k in kws])
|
| 59 |
+
for name, _, kws in TAXONOMY]
|
| 60 |
+
_CAMEL = re.compile(r"(?<=[a-z0-9])(?=[A-Z])")
|
| 61 |
+
|
| 62 |
+
# Framework / sponsor / model tags that carry no topical signal.
|
| 63 |
+
_NOISE_TOK = {
|
| 64 |
+
"gradio", "docker", "static", "build-small-hackathon", "backyard-ai",
|
| 65 |
+
"backyard ai", "tiny-titan", "tiny titan", "off-the-grid", "offgrid",
|
| 66 |
+
"nemotron", "minicpm", "llama-cpp", "openbmb", "zerogpu", "transformers",
|
| 67 |
+
"nvidia", "modal", "openai", "region:us",
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
# Official hackathon tracks โ friendly label (shown as a card badge).
|
| 71 |
+
_TRACK_MAP = {
|
| 72 |
+
"track:backyard": "Backyard", "track:backyard-ai": "Backyard",
|
| 73 |
+
"track:wood": "Thousand Token Wood",
|
| 74 |
+
"track:thousand-token-wood": "Thousand Token Wood",
|
| 75 |
+
"track:thousand_token_wood": "Thousand Token Wood",
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
# โโ Categorization โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 79 |
+
|
| 80 |
+
def _categorize(title: str, raw: str, desc: str, tags: list) -> str:
|
| 81 |
+
keep = [t for t in tags if t.lower() not in _NOISE_TOK]
|
| 82 |
+
blob = f"{title} {raw.replace('-', ' ').replace('_', ' ')} {desc} {' '.join(keep)}"
|
| 83 |
+
blob = _CAMEL.sub(" ", blob).lower()
|
| 84 |
+
for name, pats in _COMPILED:
|
| 85 |
+
if any(p.search(blob) for p in pats):
|
| 86 |
+
return name
|
| 87 |
+
return OTHER_NAME
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _track_of(tags: list) -> str:
|
| 91 |
+
for t in tags:
|
| 92 |
+
lbl = _TRACK_MAP.get(t.lower())
|
| 93 |
+
if lbl:
|
| 94 |
+
return lbl
|
| 95 |
+
return ""
|
| 96 |
+
|
| 97 |
+
# โโ Data loading (cached once per process) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 98 |
+
|
| 99 |
+
_CACHE: list | None = None
|
| 100 |
+
_CACHE_TS: float = 0.0
|
| 101 |
+
CACHE_TTL = 600 # re-index at most every 10 min so live submissions join the pool
|
| 102 |
+
|
| 103 |
+
# Populated on every successful load so the UI can explain the pool size.
|
| 104 |
+
_STATS = {"listed": 0, "private": 0, "infra": 0, "pool": 0}
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _card_data(space) -> dict:
|
| 108 |
+
"""Robustly pull the card dict โ SpaceCardData is NOT a plain dict on
|
| 109 |
+
modern huggingface_hub, so isinstance(dict) would silently drop everything."""
|
| 110 |
+
cd = getattr(space, "cardData", None)
|
| 111 |
+
if cd is None:
|
| 112 |
+
return {}
|
| 113 |
+
if hasattr(cd, "to_dict"):
|
| 114 |
+
try:
|
| 115 |
+
return cd.to_dict()
|
| 116 |
+
except Exception:
|
| 117 |
+
pass
|
| 118 |
+
if isinstance(cd, dict):
|
| 119 |
+
return dict(cd)
|
| 120 |
+
return {}
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def load_spaces(force: bool = False) -> list:
|
| 124 |
+
"""Index every PUBLIC submission in the hackathon org.
|
| 125 |
+
|
| 126 |
+
Private/gated spaces are skipped on purpose: they aren't in the public
|
| 127 |
+
listing, they'd be dead links for anyone the roulette sends there, and
|
| 128 |
+
skipping them makes the pool identical whether or not an HF token is
|
| 129 |
+
present in the environment โ so the draw is fair and reproducible.
|
| 130 |
+
"""
|
| 131 |
+
global _CACHE, _CACHE_TS
|
| 132 |
+
now = time.time()
|
| 133 |
+
if not force and _CACHE is not None and (now - _CACHE_TS) < CACHE_TTL:
|
| 134 |
+
return _CACHE
|
| 135 |
+
|
| 136 |
+
api = HfApi()
|
| 137 |
+
spaces: list = []
|
| 138 |
+
listed = private = infra = 0
|
| 139 |
+
try:
|
| 140 |
+
for s in api.list_spaces(author=HACKATHON_ORG, full=True):
|
| 141 |
+
listed += 1
|
| 142 |
+
if getattr(s, "private", False):
|
| 143 |
+
private += 1
|
| 144 |
+
continue
|
| 145 |
+
raw = s.id.split("/")[-1] if "/" in s.id else s.id
|
| 146 |
+
if raw.lower() in INFRA_SPACES:
|
| 147 |
+
infra += 1
|
| 148 |
+
continue
|
| 149 |
+
|
| 150 |
+
cd = _card_data(s)
|
| 151 |
+
title = str(cd.get("title") or "").strip()
|
| 152 |
+
desc = str(cd.get("short_description") or cd.get("description") or "").strip()[:320]
|
| 153 |
+
sdk = str(cd.get("sdk") or "").strip()
|
| 154 |
+
tags = list(s.tags or [])
|
| 155 |
+
author = s.author or (s.id.split("/")[0] if "/" in s.id else "?")
|
| 156 |
+
|
| 157 |
+
spaces.append({
|
| 158 |
+
"id": s.id,
|
| 159 |
+
"name": title or raw.replace("-", " ").replace("_", " ").title(),
|
| 160 |
+
"raw_name": raw,
|
| 161 |
+
"author": author,
|
| 162 |
+
"tags": tags,
|
| 163 |
+
"likes": int(getattr(s, "likes", 0) or 0),
|
| 164 |
+
"url": f"https://huggingface.co/spaces/{s.id}",
|
| 165 |
+
"desc": desc,
|
| 166 |
+
"sdk": sdk,
|
| 167 |
+
"track": _track_of(tags),
|
| 168 |
+
"category": _categorize(title, raw, desc, tags),
|
| 169 |
+
})
|
| 170 |
+
except Exception as exc: # never let a network hiccup crash the Space
|
| 171 |
+
print(f"[hackathon-roulette] load error: {exc}")
|
| 172 |
+
|
| 173 |
+
# Only replace a good cache when we actually got data โ a transient fetch
|
| 174 |
+
# error shouldn't empty the pool mid-event.
|
| 175 |
+
if spaces or _CACHE is None:
|
| 176 |
+
_CACHE = spaces
|
| 177 |
+
_CACHE_TS = now
|
| 178 |
+
if spaces:
|
| 179 |
+
_STATS.update(listed=listed, private=private, infra=infra, pool=len(spaces))
|
| 180 |
+
return _CACHE
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def _bucket(spaces: list, cat: str) -> list:
|
| 184 |
+
return [s for s in spaces if cat == "All" or s["category"] == cat]
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def _display_tags(sp: dict) -> list:
|
| 188 |
+
out = []
|
| 189 |
+
for t in sp["tags"]:
|
| 190 |
+
tl = t.lower()
|
| 191 |
+
if tl in _NOISE_TOK:
|
| 192 |
+
continue
|
| 193 |
+
if t.startswith(("region:", "license:", "arxiv:", "doi:", "track:",
|
| 194 |
+
"sponsor:", "achievement:", "base_model:", "dataset:",
|
| 195 |
+
"pipeline_tag:", "badge-", "badge:")):
|
| 196 |
+
continue
|
| 197 |
+
out.append(t)
|
| 198 |
+
return out
|
| 199 |
+
|
| 200 |
+
# โโ HTML builders โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 201 |
+
|
| 202 |
+
def _stats(seen: list, spaces: list, cat: str) -> str:
|
| 203 |
+
seen_set = set(seen)
|
| 204 |
+
bucket = _bucket(spaces, cat)
|
| 205 |
+
total = len(bucket)
|
| 206 |
+
found = sum(1 for s in bucket if s["id"] in seen_set)
|
| 207 |
+
left = total - found
|
| 208 |
+
pct = round(found / total * 100) if total else 0
|
| 209 |
+
|
| 210 |
+
cats_html = ""
|
| 211 |
+
if cat == "All":
|
| 212 |
+
counts = {}
|
| 213 |
+
for s in spaces:
|
| 214 |
+
counts[s["category"]] = counts.get(s["category"], 0) + 1
|
| 215 |
+
ordered = [(n, e, counts.get(n, 0)) for n, e, _ in TAXONOMY]
|
| 216 |
+
ordered.append((OTHER_NAME, OTHER_EMOJI, counts.get(OTHER_NAME, 0)))
|
| 217 |
+
cats_html = "".join(
|
| 218 |
+
f'<span class="cat-count" title="{_html.escape(n)}">{e} {c}</span>'
|
| 219 |
+
for n, e, c in ordered if c
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
return f"""
|
| 223 |
+
<div class="stats-bar">
|
| 224 |
+
<div class="stat-row">
|
| 225 |
+
<div class="stat-item"><span class="snum">{total}</span><span class="slbl">spaces</span></div>
|
| 226 |
+
<span class="sdot">ยท</span>
|
| 227 |
+
<div class="stat-item"><span class="snum" style="color:#4ade80">{found}</span><span class="slbl">seen</span></div>
|
| 228 |
+
<span class="sdot">ยท</span>
|
| 229 |
+
<div class="stat-item"><span class="snum" style="color:#60a5fa">{left}</span><span class="slbl">to go</span></div>
|
| 230 |
+
<div class="pbar-wrap"><div class="pbar-fill" style="width:{pct}%"></div></div>
|
| 231 |
+
<span class="pbar-lbl">{pct}%</span>
|
| 232 |
+
</div>
|
| 233 |
+
{_why_note() if cat == "All" else ""}
|
| 234 |
+
{f'<div class="cat-counts">{cats_html}</div>' if cats_html else ""}
|
| 235 |
+
</div>"""
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
def _why_note() -> str:
|
| 239 |
+
pool = _STATS.get("pool", 0)
|
| 240 |
+
infra = _STATS.get("infra", 0)
|
| 241 |
+
priv = _STATS.get("private", 0)
|
| 242 |
+
# `listed` is the total the app actually enumerated โ i.e. exactly what the
|
| 243 |
+
# org's Spaces page shows the same viewer (946 with a token that can see the
|
| 244 |
+
# private spaces, 886 to an anonymous visitor). Compare against that so the
|
| 245 |
+
# number matches what the user sees on the Hub.
|
| 246 |
+
listed = _STATS.get("listed", 0) or (pool + infra + priv)
|
| 247 |
+
if not listed:
|
| 248 |
+
return ""
|
| 249 |
+
parts = []
|
| 250 |
+
if priv:
|
| 251 |
+
parts.append(f"<strong>{priv}</strong> are private or test spaces "
|
| 252 |
+
f"(hidden from the public listing, with dead links)")
|
| 253 |
+
if infra:
|
| 254 |
+
parts.append(f"<strong>{infra}</strong> are org info pages "
|
| 255 |
+
f"(<em>field-guide</em> & <em>README</em>), not competition entries")
|
| 256 |
+
breakdown = f" โ {'; '.join(parts)}," if parts else ""
|
| 257 |
+
return f"""
|
| 258 |
+
<div class="why-line">
|
| 259 |
+
<span class="why-chip" tabindex="0">โ why {pool}, not {listed}?
|
| 260 |
+
<span class="why-pop">The org page lists <strong>{listed}</strong> Spaces{breakdown}
|
| 261 |
+
leaving <strong>{pool}</strong> real, public submissions to spin through.</span>
|
| 262 |
+
</span>
|
| 263 |
+
</div>"""
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def _placeholder(msg: str = "") -> str:
|
| 267 |
+
inner = msg or "Pull the lever โ hit <strong>SPIN</strong> to discover a random project!"
|
| 268 |
+
return f"""
|
| 269 |
+
<div class="ph-wrap">
|
| 270 |
+
<div class="ph-dice">๐ฐ</div>
|
| 271 |
+
<p class="ph-txt">{inner}</p>
|
| 272 |
+
</div>"""
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def _card(sp: dict, spin_count: int, position: int, bucket_total: int) -> str:
|
| 276 |
+
e = _html.escape
|
| 277 |
+
cat = sp["category"]
|
| 278 |
+
em = CAT_EMOJI.get(cat, "๐")
|
| 279 |
+
|
| 280 |
+
chips = "".join(f'<span class="tag-chip">{e(t)}</span>' for t in _display_tags(sp)[:7])
|
| 281 |
+
sdk_b = f'<span class="sdk-pill">{e(sp["sdk"])}</span>' if sp.get("sdk") else ""
|
| 282 |
+
track_b = f'<span class="track-pill">๐ฒ {e(sp["track"])}</span>' if sp.get("track") else ""
|
| 283 |
+
|
| 284 |
+
desc = sp.get("desc", "").strip()
|
| 285 |
+
desc_html = e(desc) if desc else "<em class='nodesc'>No description provided โ open it to find out!</em>"
|
| 286 |
+
|
| 287 |
+
# data-spin carries the monotonic spin counter so the client can reliably
|
| 288 |
+
# detect "a fresh result arrived" by polling for the value to change โ robust
|
| 289 |
+
# whether Gradio mounts a new node or mutates the existing one. The card
|
| 290 |
+
# animates via pure CSS (no inline <script>, which wouldn't run via innerHTML).
|
| 291 |
+
return f"""
|
| 292 |
+
<div class="space-card" data-spin="{spin_count}">
|
| 293 |
+
<div class="card-shine"></div>
|
| 294 |
+
<div class="card-meta">
|
| 295 |
+
<span class="cat-pill">{em} {e(cat)}</span>
|
| 296 |
+
{track_b}{sdk_b}
|
| 297 |
+
<span class="likes-pill">โค๏ธ {sp['likes']}</span>
|
| 298 |
+
</div>
|
| 299 |
+
<h2 class="card-h">{e(sp['name'])}</h2>
|
| 300 |
+
<p class="card-by">by <strong>{e(sp['author'])}</strong></p>
|
| 301 |
+
<p class="card-desc">{desc_html}</p>
|
| 302 |
+
<div class="card-chips">{chips}</div>
|
| 303 |
+
<div class="card-foot">
|
| 304 |
+
<a href="{sp['url']}" target="_blank" rel="noopener" class="open-btn">๐ Open Space</a>
|
| 305 |
+
<span class="draw-note">draw #{position} of {bucket_total}</span>
|
| 306 |
+
</div>
|
| 307 |
+
</div>"""
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def _browse_list(spaces: list, cat: str, seen: list) -> str:
|
| 311 |
+
bucket = sorted(_bucket(spaces, cat), key=lambda s: -s["likes"])
|
| 312 |
+
if not bucket:
|
| 313 |
+
return '<p class="browse-empty">Nothing here yet.</p>'
|
| 314 |
+
|
| 315 |
+
seen_set = set(seen)
|
| 316 |
+
CAP = 60
|
| 317 |
+
rows = []
|
| 318 |
+
for s in bucket[:CAP]:
|
| 319 |
+
e = _html.escape
|
| 320 |
+
seen_mark = '<span class="seen-dot" title="already seen">โ</span>' if s["id"] in seen_set else ""
|
| 321 |
+
em = CAT_EMOJI.get(s["category"], "๐")
|
| 322 |
+
rows.append(f"""
|
| 323 |
+
<a class="browse-row" href="{s['url']}" target="_blank" rel="noopener">
|
| 324 |
+
<span class="br-em">{em}</span>
|
| 325 |
+
<span class="br-name">{e(s['name'])}{seen_mark}</span>
|
| 326 |
+
<span class="br-author">{e(s['author'])}</span>
|
| 327 |
+
<span class="br-likes">โค๏ธ {s['likes']}</span>
|
| 328 |
+
</a>""")
|
| 329 |
+
more = ""
|
| 330 |
+
if len(bucket) > CAP:
|
| 331 |
+
more = f'<p class="browse-more">+ {len(bucket) - CAP} more โ spin or pick a tighter category to see them</p>'
|
| 332 |
+
return f'<div class="browse-wrap">{"".join(rows)}{more}</div>'
|
| 333 |
+
|
| 334 |
+
# โโ Callbacks โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 335 |
+
|
| 336 |
+
def on_load():
|
| 337 |
+
spaces = load_spaces()
|
| 338 |
+
n = len(spaces)
|
| 339 |
+
msg = (f"Loaded <strong>{n}</strong> projects from the {HACKATHON_NAME} โ ready to roll! ๐"
|
| 340 |
+
if n else "Couldn't reach the Hub right now โ try refreshing in a moment.")
|
| 341 |
+
return [], "All", 0, _stats([], spaces, "All"), _placeholder(msg)
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def on_spin(seen: list, cat: str, spin_count: int):
|
| 345 |
+
spaces = load_spaces()
|
| 346 |
+
bucket = _bucket(spaces, cat)
|
| 347 |
+
if not bucket:
|
| 348 |
+
return _placeholder("No projects in this category."), seen, spin_count, _stats(seen, spaces, cat)
|
| 349 |
+
|
| 350 |
+
seen_set = set(seen)
|
| 351 |
+
unseen = [s for s in bucket if s["id"] not in seen_set]
|
| 352 |
+
banner = ""
|
| 353 |
+
if not unseen:
|
| 354 |
+
bucket_ids = {s["id"] for s in bucket}
|
| 355 |
+
seen = [sid for sid in seen if sid not in bucket_ids] # reset just this bucket
|
| 356 |
+
unseen = bucket
|
| 357 |
+
banner = '<div class="reset-note">๐ You\'ve seen every project here โ reshuffling the deck!</div>'
|
| 358 |
+
|
| 359 |
+
pick = random.choice(unseen)
|
| 360 |
+
new_seen = list(seen) + [pick["id"]]
|
| 361 |
+
new_count = spin_count + 1
|
| 362 |
+
position = sum(1 for s in bucket if s["id"] in set(new_seen))
|
| 363 |
+
|
| 364 |
+
card = banner + _card(pick, new_count, position, len(bucket))
|
| 365 |
+
return card, new_seen, new_count, _stats(new_seen, spaces, cat)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def on_category(cat: str, seen: list):
|
| 369 |
+
spaces = load_spaces()
|
| 370 |
+
em = CAT_EMOJI.get(cat, "๐ฒ")
|
| 371 |
+
n = len(_bucket(spaces, cat))
|
| 372 |
+
msg = f"{em} <strong>{_html.escape(cat)}</strong> โ {n} project{'s' if n != 1 else ''}. Hit SPIN!"
|
| 373 |
+
return cat, _stats(seen, spaces, cat), _placeholder(msg)
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
def on_browse(cat: str, seen: list):
|
| 377 |
+
spaces = load_spaces()
|
| 378 |
+
return _browse_list(spaces, cat, seen)
|
| 379 |
+
|
| 380 |
+
# โโ CSS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 381 |
+
|
| 382 |
+
CSS = r"""
|
| 383 |
+
/* โโ aurora backdrop โโ */
|
| 384 |
+
body, gradio-app { background:#070714 !important; }
|
| 385 |
+
.gradio-container { background: transparent !important; max-width: 760px !important; margin: 0 auto !important; position: relative; z-index: 1; }
|
| 386 |
+
body::before {
|
| 387 |
+
content:''; position:fixed; inset:-30%; z-index:0; pointer-events:none; filter: blur(22px);
|
| 388 |
+
background:
|
| 389 |
+
radial-gradient(38% 38% at 20% 22%, #7c3aed30, transparent 60%),
|
| 390 |
+
radial-gradient(36% 36% at 82% 18%, #2563eb2b, transparent 60%),
|
| 391 |
+
radial-gradient(44% 44% at 72% 82%, #f472b624, transparent 60%),
|
| 392 |
+
radial-gradient(40% 40% at 22% 86%, #22d3ee1f, transparent 60%);
|
| 393 |
+
animation: aurora 24s ease-in-out infinite alternate;
|
| 394 |
+
}
|
| 395 |
+
@keyframes aurora {
|
| 396 |
+
0% { transform: translate3d(-3%,-2%,0) rotate(0deg) scale(1.05); }
|
| 397 |
+
50% { transform: translate3d(3%, 2%,0) rotate(7deg) scale(1.16); }
|
| 398 |
+
100% { transform: translate3d(-2%,3%,0) rotate(-6deg) scale(1.08); }
|
| 399 |
+
}
|
| 400 |
+
footer { display: none !important; }
|
| 401 |
+
|
| 402 |
+
.app-header { text-align: center; padding: 2rem 1rem 0.4rem; position: relative; }
|
| 403 |
+
.hdr-icon { font-size: 3.1rem; display: block; margin-bottom: .25rem;
|
| 404 |
+
animation: pop 1s cubic-bezier(.175,.885,.32,1.275) both;
|
| 405 |
+
filter: drop-shadow(0 0 14px #7c3aed88); }
|
| 406 |
+
@keyframes pop { 0%{transform:scale(0) rotate(-200deg);opacity:0} 75%{transform:scale(1.18) rotate(10deg)} 100%{transform:scale(1) rotate(0);opacity:1} }
|
| 407 |
+
.hdr-title { font-size: 2rem; font-weight: 900; margin: .1rem 0 .35rem;
|
| 408 |
+
background: linear-gradient(120deg,#a78bfa,#60a5fa 50%,#f472b6);
|
| 409 |
+
background-size: 200% auto; -webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
| 410 |
+
background-clip: text; animation: shimmer 6s linear infinite; }
|
| 411 |
+
@keyframes shimmer { to { background-position: 200% center; } }
|
| 412 |
+
.hdr-sub { color: #50508a; font-size: .87rem; margin: 0; }
|
| 413 |
+
.hdr-sub strong { color: #8585bb; }
|
| 414 |
+
|
| 415 |
+
/* โโ mute toggle โโ */
|
| 416 |
+
.rl-mute { position:absolute; top:.4rem; right:.2rem; background:#0f0f28; border:1px solid #242456;
|
| 417 |
+
color:#c0c0e8; border-radius:10px; width:38px; height:38px; font-size:1.05rem; cursor:pointer;
|
| 418 |
+
transition:transform .12s, border-color .15s, box-shadow .2s; z-index:3; }
|
| 419 |
+
.rl-mute:hover { transform:scale(1.08); border-color:#7c3aed; box-shadow:0 0 14px #7c3aed55; }
|
| 420 |
+
.rl-mute.muted { opacity:.55; }
|
| 421 |
+
|
| 422 |
+
.stats-bar { background:#0f0f28cc; backdrop-filter:blur(6px); border:1px solid #1e1e48; border-radius:12px; padding:.6rem 1rem; margin:.5rem 0; }
|
| 423 |
+
.stat-row { display:flex; align-items:center; gap:.65rem; flex-wrap:wrap; }
|
| 424 |
+
.stat-item { display:flex; flex-direction:column; align-items:center; }
|
| 425 |
+
.snum { font-size:1.25rem; font-weight:800; color:#a78bfa; line-height:1.05; transition:color .3s; }
|
| 426 |
+
.slbl { font-size:.58rem; color:#404070; text-transform:uppercase; letter-spacing:.07em; }
|
| 427 |
+
.sdot { color:#1e1e48; }
|
| 428 |
+
.pbar-wrap { flex:1; min-width:60px; height:5px; background:#1e1e48; border-radius:3px; overflow:hidden; }
|
| 429 |
+
.pbar-fill { height:100%; background:linear-gradient(90deg,#7c3aed,#60a5fa,#f472b6); background-size:200% auto; transition:width .5s ease; animation:shimmer 3s linear infinite; }
|
| 430 |
+
.pbar-lbl { color:#50508a; font-size:.74rem; font-weight:600; min-width:2.4rem; text-align:right; }
|
| 431 |
+
.cat-counts { display:flex; gap:.32rem; flex-wrap:wrap; margin-top:.55rem; padding-top:.55rem; border-top:1px solid #16163a; }
|
| 432 |
+
.cat-count { background:#0a0a20; border:1px solid #1e1e48; border-radius:7px; padding:.1rem .42rem; font-size:.72rem; color:#6868a8; transition:transform .1s, border-color .15s; }
|
| 433 |
+
.cat-count:hover { transform:translateY(-1px); border-color:#34346e; }
|
| 434 |
+
|
| 435 |
+
/* โโ why-884 tooltip โโ */
|
| 436 |
+
.why-line { margin-top:.5rem; padding-top:.5rem; border-top:1px solid #16163a; }
|
| 437 |
+
.why-chip { position:relative; display:inline-block; font-size:.74rem; color:#6a6aa8;
|
| 438 |
+
background:#0a0a20; border:1px solid #20204a; border-radius:8px; padding:.12rem .5rem; cursor:help; outline:none; }
|
| 439 |
+
.why-chip:hover, .why-chip:focus { border-color:#7c3aed; color:#a78bfa; }
|
| 440 |
+
.why-pop { position:absolute; left:0; bottom:130%; width:300px; max-width:80vw; z-index:20;
|
| 441 |
+
background:#13132e; border:1px solid #34346e; border-radius:10px; padding:.6rem .75rem;
|
| 442 |
+
color:#a0a0d0; font-size:.78rem; line-height:1.5; text-align:left; box-shadow:0 8px 30px #000a;
|
| 443 |
+
opacity:0; transform:translateY(4px); pointer-events:none; transition:opacity .15s, transform .15s; }
|
| 444 |
+
.why-pop strong { color:#c8c8f0; } .why-pop em { color:#5fd58b; font-style:normal; }
|
| 445 |
+
.why-chip:hover .why-pop, .why-chip:focus .why-pop { opacity:1; transform:translateY(0); }
|
| 446 |
+
|
| 447 |
+
/* โโ SPIN button: glow + spin states โโ */
|
| 448 |
+
#spin-btn, #spin-btn button { background:linear-gradient(135deg,#7c3aed,#2563eb)!important; color:#fff!important; border:none!important;
|
| 449 |
+
border-radius:14px!important; font-size:1.3rem!important; font-weight:900!important; letter-spacing:.1em!important;
|
| 450 |
+
transition:transform .12s, box-shadow .2s!important; min-height:54px!important; animation: glow 2.6s ease-in-out infinite; }
|
| 451 |
+
@keyframes glow { 0%,100%{ box-shadow:0 4px 22px rgba(124,58,237,.45); } 50%{ box-shadow:0 4px 30px rgba(124,58,237,.7), 0 0 16px rgba(96,165,250,.45); } }
|
| 452 |
+
#spin-btn:hover, #spin-btn button:hover { transform:translateY(-2px)!important; box-shadow:0 7px 34px rgba(124,58,237,.75)!important; }
|
| 453 |
+
#spin-btn:active, #spin-btn button:active { transform:translateY(0) scale(.97)!important; }
|
| 454 |
+
#spin-btn.spinning, #spin-btn.spinning button { animation: glow-fast .45s ease-in-out infinite; }
|
| 455 |
+
@keyframes glow-fast { 0%,100%{ box-shadow:0 4px 26px rgba(124,58,237,.7); } 50%{ box-shadow:0 6px 44px rgba(244,114,182,.85), 0 0 28px rgba(96,165,250,.7); } }
|
| 456 |
+
|
| 457 |
+
#cat-dd label span { color:#50508a!important; font-size:.78rem!important; }
|
| 458 |
+
#cat-dd input, #cat-dd .wrap { background:#0f0f28!important; border-color:#1e1e48!important; color:#c0c0e8!important; }
|
| 459 |
+
|
| 460 |
+
/* โโ slot reel (shown while waiting for the draw) โโ */
|
| 461 |
+
#card-out { position: relative; }
|
| 462 |
+
.rl-reel-overlay { position:absolute; inset:0; min-height:172px; z-index:40; display:flex;
|
| 463 |
+
align-items:center; justify-content:center; background:#0a0a1c; border-radius:16px;
|
| 464 |
+
animation: ov-in .12s ease both; }
|
| 465 |
+
@keyframes ov-in { from { opacity:.35; } to { opacity:1; } }
|
| 466 |
+
.rl-reel-overlay .reel-wrap { padding:1rem; }
|
| 467 |
+
.reel-wrap { text-align:center; padding:2.2rem 1rem; }
|
| 468 |
+
.reel { width:100%; max-width:340px; height:78px; margin:0 auto; overflow:hidden; position:relative;
|
| 469 |
+
border:1px solid #2a2a5c; border-radius:14px; background:#0c0c22;
|
| 470 |
+
box-shadow:inset 0 0 30px #000, 0 0 26px #7c3aed66; }
|
| 471 |
+
.reel::before, .reel::after { content:''; position:absolute; top:0; bottom:0; width:46px; z-index:2; pointer-events:none; }
|
| 472 |
+
.reel::before { left:0; background:linear-gradient(90deg,#0c0c22,transparent); }
|
| 473 |
+
.reel::after { right:0; background:linear-gradient(270deg,#0c0c22,transparent); }
|
| 474 |
+
.reel-strip { display:inline-block; white-space:nowrap; font-size:2.7rem; line-height:78px;
|
| 475 |
+
animation: reel-roll .42s linear infinite; filter: blur(.3px); will-change: transform; }
|
| 476 |
+
@keyframes reel-roll { from { transform:translateX(0); } to { transform:translateX(-2em); } }
|
| 477 |
+
.reel-txt { color:#7c7cb8; margin-top:.85rem; font-size:.92rem; letter-spacing:.05em; animation: pulse-txt 1s ease-in-out infinite; }
|
| 478 |
+
@keyframes pulse-txt { 0%,100%{ opacity:.45; } 50%{ opacity:1; } }
|
| 479 |
+
|
| 480 |
+
/* โโ result card โโ */
|
| 481 |
+
.space-card { background:#0f0f28; border:1px solid #242456; border-radius:16px; padding:1.5rem; margin-top:.7rem;
|
| 482 |
+
position:relative; overflow:hidden; transform-origin:center top;
|
| 483 |
+
animation: card-drop .55s cubic-bezier(.175,.885,.32,1.275) both; }
|
| 484 |
+
.space-card::before { content:''; position:absolute; inset:0; z-index:0; border-radius:16px; background:linear-gradient(135deg,#7c3aed14,#2563eb14,#f472b614); pointer-events:none; }
|
| 485 |
+
@keyframes card-drop { 0%{opacity:0;transform:translateY(26px) scale(.94) rotateX(9deg);filter:blur(5px)} 60%{opacity:1;transform:translateY(-6px) scale(1.015)} 100%{opacity:1;transform:translateY(0) scale(1);filter:blur(0)} }
|
| 486 |
+
.card-shine { position:absolute; top:0; left:-60%; width:50%; height:100%; z-index:1; pointer-events:none;
|
| 487 |
+
background:linear-gradient(105deg, transparent, #ffffff22 45%, #ffffff33 50%, transparent 60%);
|
| 488 |
+
animation: shine 1.1s ease-out .25s both; }
|
| 489 |
+
@keyframes shine { from { left:-60%; } to { left:130%; } }
|
| 490 |
+
.space-card > *:not(.card-shine) { position:relative; z-index:2; }
|
| 491 |
+
|
| 492 |
+
.card-meta { display:flex; align-items:center; gap:.4rem; flex-wrap:wrap; margin-bottom:.85rem; position:relative; z-index:2; }
|
| 493 |
+
.cat-pill { background:#17173a; border:1px solid #34346e; border-radius:10px; padding:.2rem .6rem; font-size:.76rem; color:#a78bfa; }
|
| 494 |
+
.track-pill { background:#0c1f12; border:1px solid #1c4a2c; border-radius:10px; padding:.2rem .6rem; font-size:.72rem; color:#5fd58b; }
|
| 495 |
+
.sdk-pill { background:#0a1420; border:1px solid #1a3050; border-radius:10px; padding:.2rem .6rem; font-size:.7rem; color:#6aa8e0; text-transform:uppercase; letter-spacing:.04em; }
|
| 496 |
+
.likes-pill { margin-left:auto; color:#f472b6; font-weight:700; font-size:.86rem; }
|
| 497 |
+
.card-h { font-size:1.5rem; font-weight:900; color:#e6e6ff; margin:0 0 .15rem; line-height:1.18; position:relative; z-index:2; }
|
| 498 |
+
.card-by { color:#45457a; font-size:.86rem; margin:0 0 .8rem; position:relative; z-index:2; }
|
| 499 |
+
.card-by strong { color:#7070b0; }
|
| 500 |
+
.card-desc { color:#8888c0; font-size:.95rem; line-height:1.6; margin:0 0 1rem; position:relative; z-index:2; }
|
| 501 |
+
.card-desc .nodesc { color:#454575; }
|
| 502 |
+
.card-chips { display:flex; flex-wrap:wrap; gap:.3rem; margin-bottom:1.1rem; position:relative; z-index:2; }
|
| 503 |
+
.tag-chip { background:#080818; border:1px solid #1c1c3c; border-radius:6px; padding:.13rem .48rem; font-size:.68rem; color:#5a5a92; }
|
| 504 |
+
.card-foot { display:flex; align-items:center; gap:.8rem; }
|
| 505 |
+
.open-btn { display:inline-block; background:linear-gradient(135deg,#7c3aed,#2563eb); color:#fff!important; text-decoration:none!important;
|
| 506 |
+
border-radius:10px; padding:.55rem 1.3rem; font-weight:700; font-size:.92rem; box-shadow:0 2px 12px rgba(124,58,237,.35); transition:opacity .18s,transform .1s; }
|
| 507 |
+
.open-btn:hover { opacity:.9; transform:translateY(-1px); }
|
| 508 |
+
.draw-note { color:#3a3a68; font-size:.74rem; }
|
| 509 |
+
|
| 510 |
+
.ph-wrap { text-align:center; padding:2.6rem 1rem; color:#404070; }
|
| 511 |
+
.ph-dice { font-size:3.1rem; margin-bottom:.6rem; animation:float 3s ease-in-out infinite; }
|
| 512 |
+
@keyframes float { 0%,100%{transform:translateY(0) rotate(-4deg)} 50%{transform:translateY(-10px) rotate(4deg)} }
|
| 513 |
+
.ph-txt { font-size:1rem; } .ph-txt strong { color:#a78bfa; }
|
| 514 |
+
|
| 515 |
+
.reset-note { background:#15153a; border:1px solid #7c3aed44; border-radius:10px; padding:.55rem 1rem; color:#a78bfa; font-weight:600; text-align:center; margin-bottom:.6rem; font-size:.9rem; }
|
| 516 |
+
|
| 517 |
+
#browse-acc { margin-top:.4rem; }
|
| 518 |
+
.browse-wrap { display:flex; flex-direction:column; gap:.25rem; max-height:420px; overflow-y:auto; padding:.2rem; }
|
| 519 |
+
.browse-row { display:flex; align-items:center; gap:.6rem; padding:.45rem .6rem; border-radius:9px; background:#0c0c22; border:1px solid #17173a; text-decoration:none!important; transition:background .12s,border-color .12s; }
|
| 520 |
+
.browse-row:hover { background:#13132e; border-color:#34346e; }
|
| 521 |
+
.br-em { font-size:1rem; }
|
| 522 |
+
.br-name { color:#c8c8f0; font-weight:600; font-size:.9rem; flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
| 523 |
+
.seen-dot { color:#4ade80; font-size:.78rem; margin-left:.4rem; }
|
| 524 |
+
.br-author { color:#4a4a80; font-size:.78rem; max-width:120px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
| 525 |
+
.br-likes { color:#f472b6; font-size:.78rem; font-weight:600; }
|
| 526 |
+
.browse-more, .browse-empty { color:#45457a; font-size:.82rem; text-align:center; padding:.6rem; }
|
| 527 |
+
"""
|
| 528 |
+
|
| 529 |
+
# โโ Client-side JS (synthesized sound + confetti + slot reel) โโโโโโโโโโโโโโโโโ
|
| 530 |
+
# Wired through Gradio's supported client hooks: SETUP_JS runs once via
|
| 531 |
+
# demo.load(js=...), REVEAL_JS runs after each spin via .then(js=...). Audio is
|
| 532 |
+
# synthesized with the Web Audio API (no external files) and only starts after
|
| 533 |
+
# the user's first click, satisfying browser autoplay rules. A mute toggle and
|
| 534 |
+
# throttled/short envelopes keep it tasteful rather than grating.
|
| 535 |
+
|
| 536 |
+
HEAD_HTML = r"""<script>
|
| 537 |
+
(function () {
|
| 538 |
+
if (window.RL) return;
|
| 539 |
+
const RL = window.RL = { muted: false };
|
| 540 |
+
let ctx = null;
|
| 541 |
+
const ac = () => {
|
| 542 |
+
try { if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { return null; }
|
| 543 |
+
if (ctx && ctx.state === 'suspended') ctx.resume();
|
| 544 |
+
return ctx;
|
| 545 |
+
};
|
| 546 |
+
const blip = (freq, t0, dur, peak, type) => {
|
| 547 |
+
const c = ctx; if (!c) return;
|
| 548 |
+
const o = c.createOscillator(), g = c.createGain();
|
| 549 |
+
o.type = type || 'triangle'; o.frequency.setValueAtTime(freq, t0);
|
| 550 |
+
g.gain.setValueAtTime(0.0001, t0);
|
| 551 |
+
g.gain.exponentialRampToValueAtTime(peak, t0 + 0.01);
|
| 552 |
+
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
|
| 553 |
+
o.connect(g); g.connect(c.destination);
|
| 554 |
+
o.start(t0); o.stop(t0 + dur + 0.02);
|
| 555 |
+
};
|
| 556 |
+
RL.tick = (v) => { if (RL.muted || !ac()) return; blip(360 + Math.random() * 240, ctx.currentTime, 0.05, v || 0.045, 'square'); };
|
| 557 |
+
let hov = 0;
|
| 558 |
+
RL.hover = () => { if (RL.muted) return; const n = performance.now(); if (n - hov < 90) return; hov = n; if (!ac()) return; blip(880, ctx.currentTime, 0.05, 0.02, 'sine'); };
|
| 559 |
+
RL.chime = () => {
|
| 560 |
+
if (RL.muted || !ac()) return;
|
| 561 |
+
const base = 523.25, steps = [1, 1.25, 1.5, 2.0];
|
| 562 |
+
steps.forEach((r, i) => blip(base * r, ctx.currentTime + i * 0.075, 0.5, 0.085, 'triangle'));
|
| 563 |
+
const c = ctx, t = c.currentTime, o = c.createOscillator(), g = c.createGain();
|
| 564 |
+
o.type = 'sine'; o.frequency.setValueAtTime(170, t); o.frequency.exponentialRampToValueAtTime(70, t + 0.25);
|
| 565 |
+
g.gain.setValueAtTime(0.0001, t); g.gain.exponentialRampToValueAtTime(0.11, t + 0.012); g.gain.exponentialRampToValueAtTime(0.0001, t + 0.3);
|
| 566 |
+
o.connect(g); g.connect(c.destination); o.start(t); o.stop(t + 0.33);
|
| 567 |
+
};
|
| 568 |
+
let spinTimer = null;
|
| 569 |
+
RL.startSpinSound = () => {
|
| 570 |
+
if (RL.muted) return;
|
| 571 |
+
let delay = 38;
|
| 572 |
+
const step = () => { RL.tick(0.045); delay = Math.min(150, delay * 1.07); spinTimer = setTimeout(step, delay); };
|
| 573 |
+
step();
|
| 574 |
+
};
|
| 575 |
+
RL.stopSpinSound = () => { if (spinTimer) { clearTimeout(spinTimer); spinTimer = null; } };
|
| 576 |
+
|
| 577 |
+
// confetti burst on a fixed overlay canvas
|
| 578 |
+
let cv, cx, parts = [], raf = null;
|
| 579 |
+
const COLORS = ['#a78bfa', '#60a5fa', '#f472b6', '#4ade80', '#fbbf24', '#22d3ee'];
|
| 580 |
+
const mkCanvas = () => {
|
| 581 |
+
if (cv) return;
|
| 582 |
+
cv = document.createElement('canvas'); cv.id = 'rl-confetti';
|
| 583 |
+
cv.style.cssText = 'position:fixed;inset:0;width:100vw;height:100vh;pointer-events:none;z-index:99999';
|
| 584 |
+
document.body.appendChild(cv); cx = cv.getContext('2d');
|
| 585 |
+
const rs = () => { cv.width = innerWidth * devicePixelRatio; cv.height = innerHeight * devicePixelRatio; cx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); };
|
| 586 |
+
rs(); addEventListener('resize', rs);
|
| 587 |
+
};
|
| 588 |
+
const loop = () => {
|
| 589 |
+
cx.clearRect(0, 0, innerWidth, innerHeight);
|
| 590 |
+
for (const p of parts) { p.vy += p.g; p.x += p.vx; p.y += p.vy; p.vx *= 0.99; p.rot += p.vr; p.life -= 0.011; }
|
| 591 |
+
parts = parts.filter(p => p.life > 0 && p.y < innerHeight + 50);
|
| 592 |
+
for (const p of parts) { cx.save(); cx.globalAlpha = Math.max(0, p.life); cx.translate(p.x, p.y); cx.rotate(p.rot); cx.fillStyle = p.col; cx.fillRect(-p.s / 2, -p.s / 2, p.s, p.s * 0.55); cx.restore(); }
|
| 593 |
+
raf = parts.length ? requestAnimationFrame(loop) : null;
|
| 594 |
+
};
|
| 595 |
+
RL.burst = (x, y) => {
|
| 596 |
+
mkCanvas();
|
| 597 |
+
for (let i = 0; i < 54; i++) { const a = Math.random() * Math.PI * 2, sp = 3 + Math.random() * 8; parts.push({ x, y, vx: Math.cos(a) * sp, vy: Math.sin(a) * sp - 4.5, g: 0.16 + Math.random() * 0.12, s: 5 + Math.random() * 7, rot: Math.random() * 6.28, vr: -0.3 + Math.random() * 0.6, col: COLORS[i % COLORS.length], life: 1 }); }
|
| 598 |
+
if (!raf) loop();
|
| 599 |
+
};
|
| 600 |
+
|
| 601 |
+
const MIN_SPIN = 650; // keep the wheel visibly spinning even when the draw is instant
|
| 602 |
+
const REEL = '<div class="reel-wrap"><div class="reel"><div class="reel-strip">๐ฎ ๐จ ๐๏ธ ๐ฉบ ๐ผ ๐ โ๏ธ ๐ ๐ฌ ๐ก๏ธ ๐ธ ๐ ๐ฎ ๐จ ๐๏ธ ๐ฉบ<\/div><\/div><p class="reel-txt">spinning the wheelโฆ<\/p><\/div>';
|
| 603 |
+
// The reel is an OVERLAY on top of #card-out โ never the card's own DOM. Two
|
| 604 |
+
// bugs this fixes: (1) mutating Gradio's Svelte-managed card node broke every
|
| 605 |
+
// spin after the first; (2) when a cached draw returns instantly the reel/
|
| 606 |
+
// sound used to flash by invisibly โ now the overlay (and ticks) stay up for
|
| 607 |
+
// at least MIN_SPIN before the reveal.
|
| 608 |
+
RL.startSpin = () => {
|
| 609 |
+
ac();
|
| 610 |
+
if (RL._poll) { clearInterval(RL._poll); RL._poll = null; }
|
| 611 |
+
if (RL._revealTimer) { clearTimeout(RL._revealTimer); RL._revealTimer = null; }
|
| 612 |
+
RL.stopSpinSound();
|
| 613 |
+
RL._spinStart = performance.now();
|
| 614 |
+
const prev = document.querySelector('#card-out .space-card');
|
| 615 |
+
RL._prevSpin = prev ? prev.getAttribute('data-spin') : '-1';
|
| 616 |
+
const host = document.getElementById('card-out');
|
| 617 |
+
if (host) {
|
| 618 |
+
host.style.position = 'relative';
|
| 619 |
+
let ov = host.querySelector('.rl-reel-overlay');
|
| 620 |
+
if (!ov) { ov = document.createElement('div'); ov.className = 'rl-reel-overlay'; host.appendChild(ov); }
|
| 621 |
+
ov.innerHTML = REEL; ov.style.display = 'flex';
|
| 622 |
+
}
|
| 623 |
+
const b = document.getElementById('spin-btn'); if (b) b.classList.add('spinning');
|
| 624 |
+
RL.startSpinSound();
|
| 625 |
+
// Poll for the new card (data-spin changes every draw); reveal once it has
|
| 626 |
+
// arrived AND the wheel has spun for at least MIN_SPIN.
|
| 627 |
+
const t0 = performance.now();
|
| 628 |
+
RL._poll = setInterval(() => {
|
| 629 |
+
const cur = document.querySelector('#card-out .space-card');
|
| 630 |
+
const arrived = cur && cur.getAttribute('data-spin') !== RL._prevSpin;
|
| 631 |
+
if (arrived || performance.now() - t0 > 6000) {
|
| 632 |
+
clearInterval(RL._poll); RL._poll = null;
|
| 633 |
+
const wait = Math.max(0, MIN_SPIN - (performance.now() - RL._spinStart));
|
| 634 |
+
RL._revealTimer = setTimeout(() => { RL._revealTimer = null; RL.land(); }, wait);
|
| 635 |
+
}
|
| 636 |
+
}, 40);
|
| 637 |
+
};
|
| 638 |
+
RL.land = () => {
|
| 639 |
+
RL.stopSpinSound();
|
| 640 |
+
const b = document.getElementById('spin-btn'); if (b) b.classList.remove('spinning');
|
| 641 |
+
const host = document.getElementById('card-out');
|
| 642 |
+
const ov = host && host.querySelector('.rl-reel-overlay');
|
| 643 |
+
if (ov) ov.remove(); // reveal the card underneath
|
| 644 |
+
const card = document.querySelector('#card-out .space-card');
|
| 645 |
+
if (card) { card.style.animation = 'none'; void card.offsetWidth; card.style.animation = ''; } // replay entrance on reveal
|
| 646 |
+
RL.chime();
|
| 647 |
+
let x = innerWidth / 2, y = innerHeight * 0.4;
|
| 648 |
+
if (card) { const r = card.getBoundingClientRect(); x = r.left + r.width / 2; y = r.top + Math.min(120, r.height * 0.35); }
|
| 649 |
+
RL.burst(x, y);
|
| 650 |
+
};
|
| 651 |
+
|
| 652 |
+
// delegated listeners survive Gradio re-renders
|
| 653 |
+
document.addEventListener('click', (e) => {
|
| 654 |
+
if (e.target.closest && e.target.closest('#spin-btn')) RL.startSpin();
|
| 655 |
+
const m = e.target.closest && e.target.closest('#rl-mute');
|
| 656 |
+
if (m) { RL.muted = !RL.muted; m.textContent = RL.muted ? '๐' : '๐'; m.classList.toggle('muted', RL.muted); if (RL.muted) RL.stopSpinSound(); }
|
| 657 |
+
}, true);
|
| 658 |
+
document.addEventListener('pointerover', (e) => { if (e.target.closest && e.target.closest('#spin-btn')) RL.hover(); }, true);
|
| 659 |
+
})();
|
| 660 |
+
</script>
|
| 661 |
+
"""
|
| 662 |
+
|
| 663 |
+
# โโ UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 664 |
+
|
| 665 |
+
with gr.Blocks(css=CSS, title="๐ฐ Hackathon Roulette", theme=gr.themes.Base(),
|
| 666 |
+
head=HEAD_HTML) as demo:
|
| 667 |
+
seen_st = gr.State([])
|
| 668 |
+
cat_st = gr.State("All")
|
| 669 |
+
spin_ctr = gr.State(0)
|
| 670 |
+
|
| 671 |
+
gr.HTML(f"""
|
| 672 |
+
<div class="app-header">
|
| 673 |
+
<button id="rl-mute" class="rl-mute" title="Sound on / off" aria-label="Toggle sound">๐</button>
|
| 674 |
+
<span class="hdr-icon">๐ฐ</span>
|
| 675 |
+
<h1 class="hdr-title">Hackathon Roulette</h1>
|
| 676 |
+
<p class="hdr-sub">Spin to discover hidden gems from the <strong>{HACKATHON_NAME}</strong></p>
|
| 677 |
+
</div>""")
|
| 678 |
+
|
| 679 |
+
stats_out = gr.HTML()
|
| 680 |
+
|
| 681 |
+
with gr.Row():
|
| 682 |
+
cat_dd = gr.Dropdown(choices=ALL_CATEGORIES, value="All", label="Browse by category",
|
| 683 |
+
elem_id="cat-dd", scale=3, interactive=True, filterable=False)
|
| 684 |
+
spin_btn = gr.Button("๐ฐ SPIN", elem_id="spin-btn", scale=2, min_width=160)
|
| 685 |
+
|
| 686 |
+
card_out = gr.HTML(elem_id="card-out")
|
| 687 |
+
|
| 688 |
+
with gr.Accordion("๐ Browse all in this category", open=False, elem_id="browse-acc") as browse_acc:
|
| 689 |
+
browse_out = gr.HTML()
|
| 690 |
+
|
| 691 |
+
# events
|
| 692 |
+
demo.load(on_load, outputs=[seen_st, cat_st, spin_ctr, stats_out, card_out])
|
| 693 |
+
spin_btn.click(
|
| 694 |
+
on_spin,
|
| 695 |
+
inputs=[seen_st, cat_st, spin_ctr],
|
| 696 |
+
outputs=[card_out, seen_st, spin_ctr, stats_out],
|
| 697 |
+
) # the reveal (chime + confetti) is driven by the data-spin poller in HEAD_HTML
|
| 698 |
+
cat_dd.change(on_category, inputs=[cat_dd, seen_st], outputs=[cat_st, stats_out, card_out])
|
| 699 |
+
# keep the browse list in sync whenever the category changes or the panel opens
|
| 700 |
+
cat_dd.change(on_browse, inputs=[cat_dd, seen_st], outputs=[browse_out])
|
| 701 |
+
browse_acc.expand(on_browse, inputs=[cat_st, seen_st], outputs=[browse_out])
|
| 702 |
+
|
| 703 |
+
|
| 704 |
+
# Warm the cache at startup so the first visitor doesn't wait on the Hub.
|
| 705 |
+
try:
|
| 706 |
+
load_spaces()
|
| 707 |
+
except Exception as _exc: # pragma: no cover
|
| 708 |
+
print(f"[hackathon-roulette] prewarm skipped: {_exc}")
|
| 709 |
+
|
| 710 |
+
|
| 711 |
+
if __name__ == "__main__":
|
| 712 |
+
demo.launch()
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=6.0,<7
|
| 2 |
+
huggingface_hub>=0.25.0
|