| from __future__ import annotations |
|
|
| import html |
| from collections import Counter |
|
|
| from backend.models import SpaceItem |
| from backend.passport import PassportState, get_badges, get_next_track, get_rank |
| from backend.storage import get_recent_feedback |
| from backend.tracks import TRACK_NAMES, get_track_metadata |
|
|
|
|
| ABOUT_TEXT = """ |
| # About Build Small Quest Finder |
| |
| This app is a community discovery layer for the Build Small Hackathon Spaces. |
| It does not own or modify the original Spaces. |
| |
| ## How recommendations work |
| |
| - The app scores your query against each Space's title, summary, tags, track, difficulty, and README text when available. |
| - The LLM sharpens the query intent and helps explain why a Space is a confident match. |
| - Results are ordered by a lightweight relevance score. |
| |
| ## How quests work |
| |
| - Each Space gets a short, track-based mission. |
| - You can mark quests complete locally during your session. |
| - Shared comments are saved in the SQLite database so everyone can read them. |
| - Your progress lives in `gr.State`, not a database. |
| |
| ## How data is fetched |
| |
| - The app first tries live Hugging Face Hub fetching. |
| - If that is unavailable, it falls back to `data/spaces_cache.json`. |
| - If no cache exists, it uses `data/sample_spaces.json`. |
| |
| ## What builders get |
| |
| - A more fun way to discover hackathon demos |
| - Quest cards that explain why a Space matches |
| - Comment modals that let you read community notes |
| - A simple passport with badges and progress |
| - Shared reflections saved for the community |
| """ |
|
|
|
|
| def _escape(text: str) -> str: |
| return html.escape(text or "") |
|
|
|
|
| def render_passport(state: dict, spaces: list[SpaceItem]) -> str: |
| passport = PassportState.from_dict(state) |
| space_lookup = {space.repo_id: space for space in spaces} |
| completed_spaces = [space_lookup[repo_id] for repo_id in passport.completed_repo_ids if repo_id in space_lookup] |
| track_counts = Counter(space.track or "Unassigned" for space in completed_spaces) |
| badges = get_badges(state, spaces) |
| rank = get_rank(len(passport.completed_repo_ids)) |
| next_track = get_next_track(state, spaces) |
| explored = len(passport.tracks_explored) |
| total_tracks = len(TRACK_NAMES) |
|
|
| completed_list = "".join( |
| f"<li>{_escape(space.emoji)} {_escape(space.title)} <span class='passport-zone'>({_escape(space.track or space.zone)})</span></li>" |
| for space in completed_spaces[-8:] |
| ) or "<li>Complete a quest to start your passport.</li>" |
|
|
| badge_html = "".join(f"<span class='badge-pill'>{_escape(badge)}</span>" for badge in badges) |
| if not badge_html: |
| badge_html = "<span class='passport-muted'>No badges yet. Complete 3 quests in the same zone to earn one.</span>" |
|
|
| track_lines = [] |
| for track in TRACK_NAMES: |
| meta = get_track_metadata(track) |
| track_lines.append( |
| f"<div class='passport-zone-row'><span>{meta['emoji']} {_escape(track)}</span><strong>{track_counts.get(track, 0)}</strong></div>" |
| ) |
|
|
| recent_feedback = get_recent_feedback(limit=5) |
| feedback_list = "".join( |
| f"<li><strong>{_escape(row['space_title'])}</strong>: {_escape(row['answer'])}</li>" |
| for row in recent_feedback |
| ) or "<li>No reflections yet.</li>" |
|
|
| return f""" |
| <div class="passport-shell"> |
| <div class="passport-header"> |
| <div> |
| <div class="hero-badge">🎒 Passport</div> |
| <h2>Quest Progress</h2> |
| </div> |
| <div class="passport-rank">{_escape(rank)}</div> |
| </div> |
| <div class="passport-grid"> |
| <div class="passport-card"> |
| <h3>Progress</h3> |
| <div class="passport-stat"><span>Quests completed</span><strong>{len(passport.completed_repo_ids)}</strong></div> |
| <div class="passport-stat"><span>Tracks explored</span><strong>{explored} / {total_tracks}</strong></div> |
| <div class="passport-stat"><span>Suggested next track</span><strong>{_escape(next_track)}</strong></div> |
| </div> |
| <div class="passport-card"> |
| <h3>Badges</h3> |
| <div class="badge-row">{badge_html}</div> |
| </div> |
| </div> |
| <div class="passport-grid passport-grid-wide"> |
| <div class="passport-card"> |
| <h3>Track Progress</h3> |
| <div class="passport-zones"> |
| {''.join(track_lines)} |
| </div> |
| </div> |
| <div class="passport-card"> |
| <h3>Recently completed</h3> |
| <ul class="passport-list"> |
| {completed_list} |
| </ul> |
| </div> |
| </div> |
| <div class="passport-card"> |
| <h3>Community reflections</h3> |
| <ul class="passport-list"> |
| {feedback_list} |
| </ul> |
| </div> |
| </div> |
| """ |
|
|