File size: 4,869 Bytes
1e34d32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4784470
1e34d32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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>
    """