File size: 17,485 Bytes
2dcfb8d
 
 
 
 
 
 
97cba6f
2dcfb8d
 
 
 
4567e95
2dcfb8d
 
 
 
 
 
 
 
 
 
97cba6f
2dcfb8d
 
 
 
97cba6f
2dcfb8d
 
 
 
97cba6f
2dcfb8d
 
 
 
 
 
 
97cba6f
 
2dcfb8d
 
 
97cba6f
 
 
 
 
 
 
 
 
 
 
 
 
 
2dcfb8d
 
97cba6f
 
2dcfb8d
 
97cba6f
2dcfb8d
 
97cba6f
2dcfb8d
97cba6f
 
2dcfb8d
97cba6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2dcfb8d
97cba6f
 
2dcfb8d
97cba6f
2dcfb8d
97cba6f
2dcfb8d
97cba6f
 
 
 
 
 
 
2dcfb8d
97cba6f
2dcfb8d
 
 
 
97cba6f
 
 
2dcfb8d
 
97cba6f
 
2dcfb8d
 
 
97cba6f
 
 
2dcfb8d
 
97cba6f
 
2dcfb8d
 
97cba6f
 
 
 
2dcfb8d
 
 
97cba6f
 
 
 
2dcfb8d
 
 
97cba6f
 
2dcfb8d
 
 
97cba6f
2dcfb8d
 
 
 
 
 
 
97cba6f
 
 
 
2dcfb8d
97cba6f
2dcfb8d
 
97cba6f
 
 
2dcfb8d
 
97cba6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2dcfb8d
97cba6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2dcfb8d
 
 
97cba6f
 
2dcfb8d
 
 
 
 
 
 
97cba6f
 
 
 
 
 
 
 
 
2dcfb8d
 
97cba6f
 
 
 
2dcfb8d
97cba6f
 
2dcfb8d
 
 
 
 
 
 
 
 
97cba6f
 
2dcfb8d
 
97cba6f
2dcfb8d
97cba6f
2dcfb8d
 
 
 
97cba6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2dcfb8d
97cba6f
2dcfb8d
97cba6f
 
2dcfb8d
97cba6f
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
import gradio as gr
import pandas as pd
from huggingface_hub import HfApi, hf_hub_download, upload_file
import os
from datetime import datetime
from dotenv import load_dotenv
import re
from gradio_htmlplus import HTMLPlus # Import the custom component

load_dotenv()

# CONFIGURATION
DATASET_REPO_ID = "MCP-1st-Birthday/hackathon-community-voting"
PROJECTS_FILE = "projects.csv"
VOTES_FILE = "votes.csv"
HF_TOKEN = os.getenv("HF_TOKEN")
api = HfApi(token=HF_TOKEN)

# Hackathon Structure
TRACKS = ["Track 1: Building MCP", "Track 2: MCP in Action"]
CATEGORIES = ["Enterprise", "Consumer", "Creative"]
LEADERBOARD_VIEWS = ["Track 1 (Overall)", "Track 2 (Enterprise)", "Track 2 (Consumer)", "Track 2 (Creative)"]

# --- DATA HANDLING FUNCTIONS ---
def load_data(filename, repo_id):
    """Loads a CSV from the dataset, or creates an empty DataFrame if it doesn't exist."""
    try:
        filepath = hf_hub_download(repo_id=repo_id, filename=filename, repo_type="dataset", token=HF_TOKEN)
        return pd.read_csv(filepath, dtype={'track': str, 'category': str})
    except Exception:
        if filename == PROJECTS_FILE:
            return pd.DataFrame(columns=["space_url", "video_url", "track", "category", "submitted_by", "timestamp"])
        elif filename == VOTES_FILE:
            return pd.DataFrame(columns=["space_url", "voted_by", "track", "category", "timestamp"])
        return pd.DataFrame()

def save_data(df, filename, repo_id, commit_message):
    """Saves a DataFrame as a CSV to the dataset."""
    temp_path = f"./{filename}"
    df.to_csv(temp_path, index=False)
    upload_file(
        path_or_fileobj=temp_path, path_in_repo=filename, repo_id=repo_id, repo_type="dataset",
        token=HF_TOKEN, commit_message=commit_message,
    )
    os.remove(temp_path)

# --- CORE LOGIC ---

def get_username(request: gr.Request):
    """Safely gets the username using the user's original, working method."""
    return request.request.session.get('oauth_info', {}).get('userinfo', {}).get('preferred_username')

def parse_view_to_context(filter_view: str):
    """Helper to get track and category from the leaderboard view string."""
    if "Track 1" in filter_view: return TRACKS[0], "Overall"
    if "Track 2" in filter_view:
        if "Enterprise" in filter_view: return TRACKS[1], "Enterprise"
        if "Consumer" in filter_view: return TRACKS[1], "Consumer"
        if "Creative" in filter_view: return TRACKS[1], "Creative"
    return None, None

def render_leaderboard(request: gr.Request, filter_view: str):
    """Renders the leaderboard HTML, including a conditional Edit button."""
    username = get_username(request)
    projects_df = load_data(PROJECTS_FILE, DATASET_REPO_ID)
    votes_df = load_data(VOTES_FILE, DATASET_REPO_ID)
    vote_context_str = f"ℹ️ Your vote will be cast in the **'{filter_view}'** context."

    if projects_df.empty:
        return "<h3>No projects submitted yet.</h3>", gr.update(choices=[], value=None), vote_context_str, gr.update(visible=False)

    track_filter, category_filter = parse_view_to_context(filter_view)
    display_df = pd.DataFrame()

    if track_filter == TRACKS[0]:
        track1_projects = projects_df[projects_df['track'].str.contains(TRACKS[0], na=False)].copy()
        if not track1_projects.empty:
            track1_projects['display_category'] = track1_projects['category'].str.replace(';', ' | ')
            track1_votes = votes_df[(votes_df['track'] == TRACKS[0]) & (votes_df['category'] == "Overall")]
            vote_counts = track1_votes.groupby('space_url').size().reset_index(name='votes')
            display_df = pd.merge(track1_projects, vote_counts, on='space_url', how='left').fillna(0)
    
    elif track_filter == TRACKS[1]:
        projects_df['track'] = projects_df['track'].str.split(';')
        exploded_tracks = projects_df.explode('track')
        exploded_tracks['category'] = exploded_tracks['category'].str.split(';')
        exploded_projects = exploded_tracks.explode('category')
        track2_filtered = exploded_projects[(exploded_projects['track'] == track_filter) & (exploded_projects['category'] == category_filter)]
        if not track2_filtered.empty:
            context_votes = votes_df[(votes_df['track'] == track_filter) & (votes_df['category'] == category_filter)]
            vote_counts = context_votes.groupby('space_url').size().reset_index(name='votes')
            display_df = pd.merge(track2_filtered, vote_counts, on='space_url', how='left').fillna(0)
            display_df['display_category'] = display_df['category']
            
    if display_df.empty:
        return f"<h3>No projects submitted for '{filter_view}' yet.</h3>", gr.update(choices=[], value=None), vote_context_str, gr.update(visible=False)

    display_df['votes'] = display_df['votes'].astype(int)
    display_df = display_df.sort_values(by="votes", ascending=False).reset_index(drop=True)

    html = "<div>"
    trophies = {0: "πŸ₯‡", 1: "πŸ₯ˆ", 2: "πŸ₯‰"}
    for index, row in display_df.iterrows():
        rank = trophies.get(index, f"<b>#{index + 1}</b>")
        space_name = re.sub(r'https://huggingface.co/spaces/', '', row["space_url"])
        category_text = row["display_category"]
        
        action_button_html = ""
        if username and row["submitted_by"] == username:
            action_button_html = f'<a href="#" class="edit-button" data-url="{row["space_url"]}" style="text-decoration: none; font-size: 20px; margin-left: 15px;" title="Edit this project">✏️</a>'

        html += f"""
        <div style="display: flex; align-items: center; padding: 12px; border-bottom: 1px solid #eee; gap: 15px;" data-space-url="{row['space_url']}">
            <div style="font-size: 24px; width: 50px;">{rank}</div>
            <div style="flex-grow: 1;">
                <div style="font-weight: bold; font-size: 16px;">{space_name}</div>
                <div style="font-size: 12px; color: #555;">
                    <a href="{row["space_url"]}" target="_blank">πŸš€ Space</a> | 
                    <a href="{row["video_url"]}" target="_blank">🎬 Video</a> |
                    <span style="color: #777;">Track: {row["track"].split(';')[0].split(':')[0]} | Categories: {category_text}</span>
                </div>
            </div>
            <div style="font-size: 20px; font-weight: bold; color: #3B82F6;">{row["votes"]} votes</div>
            {action_button_html}
        </div>
        """
    html += "</div>"
    
    project_urls = display_df['space_url'].unique().tolist()
    return html, gr.update(choices=project_urls, value=None), vote_context_str, gr.update(visible=False)


def submit_project(request: gr.Request, space_url, video_url, selected_tracks, selected_categories):
    username = get_username(request)
    if not username:
        gr.Info("Error: You must be logged in to submit a project.")
        return gr.skip(), gr.skip(), gr.skip(), gr.skip()
    if not all([space_url, video_url, selected_tracks, selected_categories]):
        gr.Info("Error: All fields are required, including at least one track and one category.")
        return gr.skip(), gr.skip(), gr.skip(), gr.skip()
    projects_df = load_data(PROJECTS_FILE, DATASET_REPO_ID)
    if space_url in projects_df['space_url'].values:
        gr.Info("Error: This project has already been submitted.")
        return gr.skip(), gr.skip(), gr.skip(), gr.skip()
    tracks_str = ";".join(selected_tracks)
    categories_str = ";".join(selected_categories)
    new_project = pd.DataFrame([{"space_url": space_url, "video_url": video_url, "track": tracks_str, "category": categories_str, "submitted_by": username, "timestamp": datetime.now().isoformat()}])
    updated_projects = pd.concat([projects_df, new_project], ignore_index=True)
    save_data(updated_projects, PROJECTS_FILE, DATASET_REPO_ID, f"Project submitted by {username}")
    gr.Info(f"βœ… Success! Project '{space_url.split('/')[-1]}' submitted.")
    html, dropdown, context, edit_box_visibility = render_leaderboard(request, LEADERBOARD_VIEWS[0])
    return f"Last action: Success.", html, dropdown, context


def cast_vote(request: gr.Request, project_to_vote, filter_view):
    username = get_username(request)
    if not username:
        gr.Info("Error: You must be logged in to vote.")
        return gr.skip(), gr.skip(), gr.skip()
    if not project_to_vote:
        gr.Info("Error: Please select a project to vote for.")
        return gr.skip(), gr.skip(), gr.skip()
    votes_df = load_data(VOTES_FILE, DATASET_REPO_ID)
    vote_track, vote_category = parse_view_to_context(filter_view)
    existing_vote = votes_df[(votes_df['space_url'] == project_to_vote) & (votes_df['voted_by'] == username) & (votes_df['track'] == vote_track) & (votes_df['category'] == vote_category)]
    if not existing_vote.empty:
        gr.Info(f"Notice: You have already voted for this project in the '{filter_view}' context.")
        return gr.skip(), gr.skip(), gr.skip()
    new_vote = pd.DataFrame([{"space_url": project_to_vote, "voted_by": username, "track": vote_track, "category": vote_category, "timestamp": datetime.now().isoformat()}])
    updated_votes = pd.concat([votes_df, new_vote], ignore_index=True)
    save_data(updated_votes, VOTES_FILE, DATASET_REPO_ID, f"Vote cast by {username}")
    gr.Info(f"βœ… Vote successfully cast for '{project_to_vote.split('/')[-1]}' in '{filter_view}'!")
    html, dropdown, context = render_leaderboard(request, filter_view)
    return f"Last action: Success.", html, dropdown


def edit_project(request: gr.Request, old_space_url: str, new_space_url: str, new_video_url: str, filter_view: str):
    username = get_username(request)
    if not username:
        gr.Info("Error: Authentication failed. Please log in again.")
        return gr.skip(), gr.skip(), gr.skip(), gr.update(visible=False)
    projects_df = load_data(PROJECTS_FILE, DATASET_REPO_ID)
    votes_df = load_data(VOTES_FILE, DATASET_REPO_ID)
    project_index = projects_df[(projects_df['space_url'] == old_space_url) & (projects_df['submitted_by'] == username)].index
    if project_index.empty:
        gr.Info("Error: You do not have permission to edit this project, or it no longer exists.")
        return gr.skip(), gr.skip(), gr.skip(), gr.update(visible=False)
    projects_df.loc[project_index, 'space_url'] = new_space_url
    projects_df.loc[project_index, 'video_url'] = new_video_url
    save_data(projects_df, PROJECTS_FILE, DATASET_REPO_ID, f"Project edited by {username}: {old_space_url} -> {new_space_url}")
    if old_space_url != new_space_url:
        votes_df.loc[votes_df['space_url'] == old_space_url, 'space_url'] = new_space_url
        save_data(votes_df, VOTES_FILE, DATASET_REPO_ID, f"Vote URLs updated for project edit by {username}")
    gr.Info("βœ… Project URLs updated successfully! Votes have been preserved.")
    html, dropdown, context, _ = render_leaderboard(request, filter_view)
    return "Project edited.", html, dropdown, context, gr.update(visible=False)

# --- THEME ---
theme = gr.themes.Default(primary_hue='blue', secondary_hue='yellow', neutral_hue='neutral').set(
    body_background_fill='*neutral_100', body_background_fill_dark='*neutral_800', body_text_color='*neutral_700', 
    body_text_color_dark='*neutral_200', body_text_size='1.1em', code_background_fill='*neutral_100', 
    code_background_fill_dark='*neutral_800', shadow_drop='2px 2px 4px *neutral_400', block_label_background_fill='*neutral_100', 
    block_label_background_fill_dark='*neutral_800', block_label_text_color='*neutral_700', block_label_text_color_dark='*neutral_200',
    block_title_text_color='*primary_700', block_title_text_color_dark='*primary_300', panel_background_fill='*neutral_50', 
    panel_background_fill_dark='*neutral_900', panel_border_color='*neutral_200', panel_border_color_dark='*neutral_700',
    checkbox_border_color='*neutral_300', checkbox_border_color_dark='*neutral_600', input_background_fill='white', 
    input_background_fill_dark='*neutral_800', input_border_color='*neutral_300', input_border_color_dark='*neutral_600',
    slider_color='*primary_500', slider_color_dark='*primary_400', button_primary_background_fill='*primary_600', 
    button_primary_background_fill_dark='*primary_500', button_primary_background_fill_hover='*primary_700', 
    button_primary_background_fill_hover_dark='*primary_600', button_primary_text_color='white', 
    button_primary_text_color_dark='white', button_secondary_background_fill='*secondary_400', 
    button_secondary_background_fill_dark='*secondary_500', button_secondary_background_fill_hover='*secondary_500', 
    button_secondary_background_fill_hover_dark='*secondary_600', button_secondary_text_color='*neutral_700', 
    button_secondary_text_color_dark='*neutral_200', button_cancel_background_fill='*neutral_200', 
    button_cancel_background_fill_dark='*neutral_700', button_cancel_background_fill_hover='*neutral_300', 
    button_cancel_background_fill_hover_dark='*neutral_600', button_cancel_text_color='*neutral_700', 
    button_cancel_text_color_dark='*neutral_200'
)

# --- GRADIO INTERFACE ---
with gr.Blocks(theme=theme, title="Hackathon Community Choice") as app:
    gr.Markdown("# πŸ† Hackathon Community Choice Award")
    gr.Markdown("Vote for your favorite hackathon projects! Please log in to participate.")
    gr.LoginButton()

    with gr.Tabs() as tabs:
        with gr.TabItem("πŸ† Leaderboard & Vote", id=0):
            leaderboard_filter = gr.Radio(LEADERBOARD_VIEWS, label="Select Leaderboard View", value=LEADERBOARD_VIEWS[0])
            with gr.Group(visible=False) as edit_group:
                gr.Markdown("### Edit Project URLs")
                edit_old_url = gr.Textbox(label="Original Space URL", interactive=False)
                edit_new_url = gr.Textbox(label="New Space URL")
                edit_new_video_url = gr.Textbox(label="New Video URL")
                with gr.Row():
                    save_edit_button = gr.Button("Save Changes", variant="primary")
                    cancel_edit_button = gr.Button("Cancel")
            
            with gr.Row():
                with gr.Column(scale=2):
                    leaderboard_html = HTMLPlus(
                        "Please log in to load...",
                        selectable_elements=[".edit-button"]
                    )
                with gr.Column(scale=1):
                    gr.Markdown("### Cast Your Vote")
                    vote_context_display = gr.Markdown()
                    vote_status = gr.Markdown()
                    project_dropdown = gr.Dropdown(label="Select a Project to Vote For", interactive=True)
                    vote_button = gr.Button("πŸ‘ Vote for Selected Project", variant="primary")
            
        with gr.TabItem("πŸš€ Submit Your Project", id=1):
            gr.Markdown("### Register Your Project")
            submission_status = gr.Markdown()
            space_url_input = gr.Textbox(label="Your Hugging Face Space URL", placeholder="https://huggingface.co/spaces/...")
            video_url_input = gr.Textbox(label="Your Demo Video URL (YouTube)", placeholder="https://www.youtube.com/watch?v=...")
            track_checkboxes = gr.CheckboxGroup(TRACKS, label="Select Your Track(s)")
            category_checkboxes = gr.CheckboxGroup(CATEGORIES, label="Select Your Category(s)")
            submit_button = gr.Button("Submit Project", variant="primary")

    # --- Event Handling ---
    def handle_page_load(request: gr.Request, filter_view: str):
        username = get_username(request)
        if username:
            gr.Info(f"Welcome, {username}!")
        return render_leaderboard(request, filter_view)

    def cancel_edit():
        return gr.update(visible=False), "", "", ""

    def handle_leaderboard_click(evt: gr.SelectData):
        if evt.index == ".edit-button":
            project_to_edit_url = evt.value.get("url")
            if project_to_edit_url:
                projects_df = load_data(PROJECTS_FILE, DATASET_REPO_ID)
                project_data = projects_df[projects_df['space_url'] == project_to_edit_url].iloc[0]
                return gr.update(visible=True), project_data['space_url'], project_data['space_url'], project_data['video_url']
        return gr.skip(), gr.skip(), gr.skip(), gr.skip()

    app.load(fn=handle_page_load, inputs=[leaderboard_filter], outputs=[leaderboard_html, project_dropdown, vote_context_display, edit_group])
    leaderboard_filter.change(fn=render_leaderboard, inputs=[leaderboard_filter], outputs=[leaderboard_html, project_dropdown, vote_context_display, edit_group])
    submit_button.click(fn=submit_project, inputs=[space_url_input, video_url_input, track_checkboxes, category_checkboxes], outputs=[submission_status, leaderboard_html, project_dropdown, vote_context_display])
    vote_button.click(fn=cast_vote, inputs=[project_dropdown, leaderboard_filter], outputs=[vote_status, leaderboard_html, project_dropdown])
    
    leaderboard_html.select(fn=handle_leaderboard_click, outputs=[edit_group, edit_old_url, edit_new_url, edit_new_video_url])
    
    save_edit_button.click(fn=edit_project, inputs=[edit_old_url, edit_new_url, edit_new_video_url, leaderboard_filter], outputs=[submission_status, leaderboard_html, project_dropdown, vote_context_display, edit_group])
    cancel_edit_button.click(fn=cancel_edit, outputs=[edit_group, edit_old_url, edit_new_url, edit_new_video_url])
    
app.launch()