ha251 commited on
Commit
7be1132
·
verified ·
1 Parent(s): d83dbac

Update miniapp_leaderboard.py

Browse files
Files changed (1) hide show
  1. miniapp_leaderboard.py +0 -720
miniapp_leaderboard.py CHANGED
@@ -1,720 +0,0 @@
1
- import datetime
2
- import io
3
- import json
4
- import os
5
- import re
6
- import uuid
7
- from urllib.parse import urlparse
8
-
9
- import gradio as gr
10
- import numpy as np
11
- import pandas as pd
12
- import requests
13
- from huggingface_hub import HfApi, hf_hub_download
14
- from huggingface_hub.errors import RepositoryNotFoundError
15
- import logging
16
- logging.basicConfig(level=logging.INFO)
17
- logging.info("APP BOOT: miniapp_leaderboard.py NEW CODE LOADED")
18
-
19
-
20
- APP_NAME = "miniapp"
21
-
22
- HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("TOKEN") or os.environ.get("HUGGINGFACE_TOKEN")
23
- LEADERBOARD_DATASET = (os.environ.get("LEADERBOARD_DATASET") or "").strip()
24
- MAX_ENTRIES = int(os.environ.get("MAX_ENTRIES", "500"))
25
-
26
- PENDING_PREFIX = "pending/"
27
- APPROVED_PREFIX = "approved/"
28
-
29
- OWNER_USERNAMES = {"ha251"} # ✅ 只有这些用户名能看到审核区
30
-
31
- LEADERBOARD_COLUMNS = [
32
- "Model name",
33
- "Avg",
34
- "Easy",
35
- "Mid",
36
- "Hard",
37
- "Games",
38
- "Science",
39
- "Tools",
40
- "Humanities",
41
- "Viz",
42
- "Lifestyle",
43
- "Submitted at",
44
- "Submitter",
45
- # 额外字段不会显示在表格,但会写入 payload:
46
- # "Model API"
47
- ]
48
-
49
- NUMERIC_COLS = [
50
- "Avg",
51
- "Easy",
52
- "Mid",
53
- "Hard",
54
- "Games",
55
- "Science",
56
- "Tools",
57
- "Humanities",
58
- "Viz",
59
- "Lifestyle",
60
- ]
61
-
62
- # 显示顺序:Model 最左,Avg 第二列
63
- DISPLAY_ORDER = [
64
- "Model name",
65
- "Avg",
66
- "Easy",
67
- "Mid",
68
- "Hard",
69
- "Games",
70
- "Science",
71
- "Tools",
72
- "Humanities",
73
- "Viz",
74
- "Lifestyle",
75
- ]
76
- SORTABLE_COLS = DISPLAY_ORDER[:]
77
-
78
- IN_SPACES = bool(
79
- os.environ.get("SPACE_ID")
80
- or os.environ.get("SPACE_REPO_NAME")
81
- or os.environ.get("SPACE_AUTHOR_NAME")
82
- or os.environ.get("system", "") == "spaces"
83
- )
84
-
85
-
86
- # ----------------- basic utils -----------------
87
- def _api() -> HfApi:
88
- return HfApi(token=HF_TOKEN)
89
-
90
-
91
- def _is_valid_http_url(url: str) -> bool:
92
- try:
93
- parsed = urlparse(url)
94
- return parsed.scheme in ("http", "https") and bool(parsed.netloc)
95
- except Exception:
96
- return False
97
-
98
-
99
- def _slug(s: str, max_len: int = 60) -> str:
100
- s = (s or "").strip().lower()
101
- s = re.sub(r"[^a-z0-9]+", "-", s)
102
- s = re.sub(r"-{2,}", "-", s).strip("-")
103
- return (s[:max_len] or "x")
104
-
105
-
106
- def _empty_df() -> pd.DataFrame:
107
- return pd.DataFrame(columns=LEADERBOARD_COLUMNS)
108
-
109
-
110
- def _ensure_dataset_readable() -> tuple[bool, str]:
111
- if not HF_TOKEN:
112
- return False, "Space is missing HF_TOKEN (Secrets)."
113
- if not LEADERBOARD_DATASET:
114
- return False, "Space is missing LEADERBOARD_DATASET (Secrets)."
115
- api = _api()
116
- try:
117
- api.repo_info(repo_id=LEADERBOARD_DATASET, repo_type="dataset")
118
- return True, ""
119
- except RepositoryNotFoundError:
120
- return False, (
121
- f"Dataset repo not found: {LEADERBOARD_DATASET}. "
122
- "Create it first (as a dataset) or fix LEADERBOARD_DATASET."
123
- )
124
- except Exception:
125
- return False, "Cannot access the dataset repo. Check token permissions."
126
-
127
-
128
- def _list_json_files(prefix: str) -> list[str]:
129
- ok, _ = _ensure_dataset_readable()
130
- if not ok:
131
- return []
132
- api = _api()
133
- try:
134
- files = api.list_repo_files(repo_id=LEADERBOARD_DATASET, repo_type="dataset")
135
- except Exception:
136
- return []
137
- out = [f for f in files if f.startswith(prefix) and f.endswith(".json")]
138
- out.sort(reverse=True)
139
- return out[:MAX_ENTRIES]
140
-
141
-
142
- def _load_entries_df(prefix: str, include_filename: bool = False) -> pd.DataFrame:
143
- rows: list[dict] = []
144
- for filename in _list_json_files(prefix):
145
- try:
146
- path = hf_hub_download(
147
- repo_id=LEADERBOARD_DATASET,
148
- repo_type="dataset",
149
- filename=filename,
150
- token=HF_TOKEN,
151
- )
152
- with open(path, "r", encoding="utf-8") as fp:
153
- row = json.load(fp)
154
- if include_filename:
155
- row["_filename"] = filename
156
- rows.append(row)
157
- except Exception:
158
- continue
159
-
160
- if not rows:
161
- df = _empty_df()
162
- if include_filename:
163
- df["_filename"] = []
164
- return df
165
-
166
- df = pd.DataFrame(rows)
167
- for c in LEADERBOARD_COLUMNS:
168
- if c not in df.columns:
169
- df[c] = ""
170
- if include_filename and "_filename" not in df.columns:
171
- df["_filename"] = ""
172
-
173
- df = df[LEADERBOARD_COLUMNS + (["_filename"] if include_filename else [])]
174
- for c in NUMERIC_COLS:
175
- df[c] = pd.to_numeric(df[c], errors="coerce")
176
-
177
- df = df.sort_values(by=["Submitted at"], ascending=False, kind="stable")
178
- return df
179
-
180
-
181
- def _parse_hf_created_at(created_at: str) -> datetime.datetime | None:
182
- try:
183
- if created_at.endswith("Z"):
184
- created_at = created_at[:-1] + "+00:00"
185
- return datetime.datetime.fromisoformat(created_at)
186
- except Exception:
187
- return None
188
-
189
-
190
- def _check_user_eligibility(username: str) -> tuple[bool, str]:
191
- """
192
- - Must be older than ~30 days (>= 30 days)
193
- """
194
- try:
195
- r = requests.get(f"https://huggingface.co/api/users/{username}/overview", timeout=10)
196
- r.raise_for_status()
197
- created_at = r.json().get("createdAt")
198
- if not created_at:
199
- return False, "Cannot verify account creation date."
200
- dt = _parse_hf_created_at(created_at)
201
- if not dt:
202
- return False, "Cannot parse account creation date."
203
- now = datetime.datetime.now(datetime.timezone.utc)
204
- if dt.tzinfo is None:
205
- dt = dt.replace(tzinfo=datetime.timezone.utc)
206
- if (now - dt).days < 30:
207
- return False, "Account must be older than 30 days to submit."
208
- return True, ""
209
- except Exception:
210
- return False, "Cannot verify Hugging Face account. Please try again later."
211
-
212
-
213
- def _submitted_today_anywhere(username: str) -> bool:
214
- # today = datetime.datetime.utcnow().date().isoformat()
215
- # for prefix in (PENDING_PREFIX, APPROVED_PREFIX):
216
- # df = _load_entries_df(prefix, include_filename=False)
217
- # if df.empty:
218
- # continue
219
- # user_rows = df[df["Submitter"].astype(str) == username]
220
- # if user_rows.empty:
221
- # continue
222
- # if any(str(v).startswith(today) for v in user_rows["Submitted at"].tolist()):
223
- # return True
224
- return False
225
-
226
-
227
- # ----------------- leaderboard HTML -----------------
228
- def _fmt_cell(v):
229
- if v is None or (isinstance(v, float) and pd.isna(v)):
230
- return ""
231
- if isinstance(v, (int, float, np.number)):
232
- return f"{float(v):.2f}"
233
- return str(v)
234
-
235
-
236
- def _apply_search_and_sort(df: pd.DataFrame, search_text: str, sort_col: str, sort_dir: str) -> pd.DataFrame:
237
- s = (search_text or "").strip().lower()
238
- if s:
239
- df = df[df["Model name"].astype(str).str.lower().str.contains(s, na=False)]
240
-
241
- sort_col = sort_col if sort_col in df.columns else "Avg"
242
- asc = sort_dir == "asc"
243
- df = df.sort_values(by=[sort_col], ascending=asc, kind="stable", na_position="last")
244
- return df
245
-
246
-
247
- def _render_leaderboard_html(df: pd.DataFrame, sort_col: str, sort_dir: str) -> str:
248
- import html as _html
249
-
250
- def th(label, col=None, align_left=False, cls=""):
251
- if col:
252
- arrow = ""
253
- if col == sort_col:
254
- arrow = " ▲" if sort_dir == "asc" else " ▼"
255
- al = " left" if align_left else ""
256
- return f'<th class="th clickable{al} {cls}" data-col="{_html.escape(col)}">{_html.escape(label)}{arrow}</th>'
257
- al = " left" if align_left else ""
258
- return f'<th class="th{al} {cls}">{_html.escape(label)}</th>'
259
-
260
- trs = []
261
- for _, r in df.iterrows():
262
- tds = []
263
- for c in DISPLAY_ORDER:
264
- val = _fmt_cell(r.get(c, ""))
265
- if c == "Model name":
266
- tds.append(f'<td class="td model">{_html.escape(val)}</td>')
267
- else:
268
- tds.append(f'<td class="td num">{_html.escape(val)}</td>')
269
- trs.append("<tr class='tr'>" + "".join(tds) + "</tr>")
270
-
271
- return f"""
272
- <div class="table-wrap">
273
- <div class="table-scroll">
274
- <table class="table" id="lb_table">
275
- <thead>
276
- <tr class="r1">
277
- {th("Model", "Model name", align_left=True, cls="model")}
278
- {th("Avg. (%)", "Avg", cls="avg")}
279
- <th class="th group" colspan="9">Pass Rate (%)</th>
280
- </tr>
281
- <tr class="r2">
282
- <th class="th"></th>
283
- <th class="th"></th>
284
- <th class="th group" colspan="3">Difficulty</th>
285
- <th class="th group" colspan="6">Domain</th>
286
- </tr>
287
- <tr class="r3">
288
- <th class="th"></th>
289
- <th class="th"></th>
290
- {th("Easy", "Easy")}
291
- {th("Mid", "Mid")}
292
- {th("Hard", "Hard")}
293
- {th("Games", "Games")}
294
- {th("Science", "Science")}
295
- {th("Tools", "Tools")}
296
- {th("Humanities", "Humanities")}
297
- {th("Viz.", "Viz")}
298
- {th("Lifestyle", "Lifestyle")}
299
- </tr>
300
- </thead>
301
- <tbody>
302
- {''.join(trs)}
303
- </tbody>
304
- </table>
305
- </div>
306
- </div>
307
- """
308
-
309
-
310
- def render_lb(search_text: str, sort_col: str, sort_dir: str) -> str:
311
- df = _load_entries_df(APPROVED_PREFIX, include_filename=False)
312
- df = _apply_search_and_sort(df, search_text, sort_col, sort_dir)
313
- return _render_leaderboard_html(df, sort_col, sort_dir)
314
-
315
-
316
- def toggle_sort(clicked_col: str, current_col: str, current_dir: str):
317
- clicked_col = (clicked_col or "").strip()
318
- if clicked_col not in SORTABLE_COLS:
319
- return current_col, current_dir
320
- if clicked_col == current_col:
321
- return current_col, ("asc" if current_dir == "desc" else "desc")
322
- return clicked_col, "desc"
323
-
324
-
325
- # ----------------- submit & approve -----------------
326
- def submit(
327
- model_name: str,
328
- model_api: str,
329
- api_key: str,
330
- search_text: str,
331
- sort_col: str,
332
- sort_dir: str,
333
- profile: gr.OAuthProfile | None,
334
- ):
335
- logging.info("SUBMIT CALLED: no-daily-limit version")
336
-
337
-
338
- if IN_SPACES and (profile is None or not getattr(profile, "username", None)):
339
- return "You must log in to submit.", render_lb(search_text, sort_col, sort_dir)
340
-
341
- submitter = getattr(profile, "username", None) or "anonymous"
342
-
343
- model_name = (model_name or "").strip()
344
- model_api = (model_api or "").strip()
345
- api_key = (api_key or "").strip()
346
-
347
- if not model_name:
348
- return "Model name is required.", render_lb(search_text, sort_col, sort_dir)
349
- if not model_api:
350
- return "Model API URL is required.", render_lb(search_text, sort_col, sort_dir)
351
- if not _is_valid_http_url(model_api):
352
- return "Model API must be a valid http(s) URL.", render_lb(search_text, sort_col, sort_dir)
353
- if not api_key:
354
- return "API key is required.", render_lb(search_text, sort_col, sort_dir)
355
-
356
- ok, msg = _ensure_dataset_readable()
357
- if not ok:
358
- return msg, render_lb(search_text, sort_col, sort_dir)
359
-
360
- if IN_SPACES:
361
- ok, msg = _check_user_eligibility(submitter)
362
- if not ok:
363
- return msg, render_lb(search_text, sort_col, sort_dir)
364
-
365
- # if _submitted_today_anywhere(submitter):
366
- # return "You have already submitted today. Please try again tomorrow. kk", render_lb(search_text, sort_col, sort_dir)
367
-
368
- now = datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
369
- nonce = uuid.uuid4().hex[:8]
370
- safe_user = _slug(submitter)
371
- safe_model = _slug(model_name)
372
- path_in_repo = f"{PENDING_PREFIX}{now[:10]}/{now}-{safe_user}-{safe_model}-{nonce}.json"
373
-
374
- # api_key collected but NOT stored
375
- payload = {
376
- "Model name": model_name,
377
- "Avg": None,
378
- "Easy": None,
379
- "Mid": None,
380
- "Hard": None,
381
- "Games": None,
382
- "Science": None,
383
- "Tools": None,
384
- "Humanities": None,
385
- "Viz": None,
386
- "Lifestyle": None,
387
- "Submitted at": now,
388
- "Submitter": submitter,
389
- "Model API": model_api,
390
- }
391
-
392
- api = _api()
393
- data = (json.dumps(payload, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
394
- api.upload_file(
395
- repo_id=LEADERBOARD_DATASET,
396
- repo_type="dataset",
397
- path_or_fileobj=io.BytesIO(data),
398
- path_in_repo=path_in_repo,
399
- commit_message=f"miniapp: submit(pending) {submitter}/{model_name}",
400
- token=HF_TOKEN,
401
- )
402
-
403
- return "Submitted. Waiting for owner approval.", render_lb(search_text, sort_col, sort_dir)
404
-
405
-
406
- def refresh_pending(profile: gr.OAuthProfile | None):
407
- username = getattr(profile, "username", None) if profile else None
408
- if username not in OWNER_USERNAMES:
409
- # 非 owner:不返回任何 pending(且 UI 会隐藏)
410
- return pd.DataFrame(columns=["Submitted at", "Submitter", "Model name", "Model API", "_filename"]), gr.update(
411
- choices=[], value=None
412
- )
413
-
414
- df = _load_entries_df(PENDING_PREFIX, include_filename=True)
415
- # pending 表显示更实用的列
416
- view = df.copy()
417
- cols = ["Submitted at", "Submitter", "Model name"]
418
- if "Model API" in view.columns:
419
- cols.append("Model API")
420
- cols.append("_filename")
421
- view = view[cols]
422
-
423
- choices = view["_filename"].tolist() if not view.empty else []
424
- return view, gr.update(choices=choices, value=(choices[0] if choices else None))
425
-
426
-
427
- def approve(pending_filename: str | None, profile: gr.OAuthProfile | None, search_text: str, sort_col: str, sort_dir: str):
428
- username = getattr(profile, "username", None) if profile else None
429
- if username not in OWNER_USERNAMES:
430
- # 非 owner:不允许
431
- pending_view, dd = refresh_pending(profile)
432
- return "Not authorized.", pending_view, dd, render_lb(search_text, sort_col, sort_dir)
433
-
434
- pending_filename = (pending_filename or "").strip()
435
- if not pending_filename:
436
- pending_view, dd = refresh_pending(profile)
437
- return "Select a pending entry.", pending_view, dd, render_lb(search_text, sort_col, sort_dir)
438
-
439
- api = _api()
440
- try:
441
- local_path = hf_hub_download(
442
- repo_id=LEADERBOARD_DATASET,
443
- repo_type="dataset",
444
- filename=pending_filename,
445
- token=HF_TOKEN,
446
- )
447
- with open(local_path, "r", encoding="utf-8") as fp:
448
- payload = json.load(fp)
449
- except Exception as e:
450
- pending_view, dd = refresh_pending(profile)
451
- return f"Failed to read pending file: {e}", pending_view, dd, render_lb(search_text, sort_col, sort_dir)
452
-
453
- # move to approved/
454
- base = pending_filename[len(PENDING_PREFIX) :] if pending_filename.startswith(PENDING_PREFIX) else pending_filename
455
- approved_filename = f"{APPROVED_PREFIX}{base}"
456
- data = (json.dumps(payload, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
457
-
458
- try:
459
- api.upload_file(
460
- repo_id=LEADERBOARD_DATASET,
461
- repo_type="dataset",
462
- path_or_fileobj=io.BytesIO(data),
463
- path_in_repo=approved_filename,
464
- commit_message=f"miniapp: approve {payload.get('Submitter','')}/{payload.get('Model name','')}",
465
- token=HF_TOKEN,
466
- )
467
- api.delete_file(
468
- repo_id=LEADERBOARD_DATASET,
469
- repo_type="dataset",
470
- path_in_repo=pending_filename,
471
- commit_message=f"miniapp: remove pending {pending_filename}",
472
- token=HF_TOKEN,
473
- )
474
- except Exception as e:
475
- pending_view, dd = refresh_pending(profile)
476
- return f"Approve failed: {e}", pending_view, dd, render_lb(search_text, sort_col, sort_dir)
477
-
478
- pending_view, dd = refresh_pending(profile)
479
- return "Approved and published.", pending_view, dd, render_lb(search_text, sort_col, sort_dir)
480
-
481
-
482
- def show_owner_panel(profile: gr.OAuthProfile | None):
483
- username = getattr(profile, "username", None) if profile else None
484
- return gr.update(visible=(username in OWNER_USERNAMES))
485
-
486
-
487
- # ----------------- UI -----------------
488
- gr.Markdown("**APP VERSION:** 2026-02-28-no-daily-limit")
489
-
490
- CSS = r"""
491
- .gradio-container { max-width: 100% !important; }
492
- #page { padding: 16px; }
493
-
494
- #topbar { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom: 10px; }
495
- #titleline { font-weight: 700; font-size: 18px; }
496
- #searchbox { width: 280px; }
497
- #searchbox label { display:none !important; }
498
- #searchbox textarea, #searchbox input {
499
- height: 34px !important;
500
- border-radius: 8px !important;
501
- border: 1px solid #e5e7eb !important;
502
- background: #fff !important;
503
- box-shadow: none !important;
504
- }
505
- #searchbox textarea::placeholder, #searchbox input::placeholder { color: #9ca3af; }
506
-
507
- /* table container */
508
- .table-wrap{
509
- width: 100%;
510
- border: 1px solid #e5e7eb !important;
511
- border-radius: 8px;
512
- background: #fff;
513
- }
514
- .table-scroll{ width: 100%; overflow-x: auto; }
515
-
516
- /* HARD OVERRIDE borders to light gray */
517
- #lb_table {
518
- width: 100% !important;
519
- min-width: 1100px;
520
- border-collapse: collapse !important;
521
- border: 1px solid #e5e7eb !important;
522
- }
523
- #lb_table th, #lb_table td {
524
- border: 1px solid #e5e7eb !important;
525
- border-color: #e5e7eb !important;
526
- }
527
-
528
- th.th{
529
- font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial !important;
530
- font-weight: 600;
531
- font-size: 13px;
532
- color: #111827;
533
- padding: 10px 12px;
534
- text-align: center;
535
- background: #f9fafb !important;
536
- white-space: nowrap;
537
- }
538
- thead tr.r3 th.th { background: #ffffff !important; }
539
- th.th.left{ text-align:left !important; }
540
- th.group{ color:#374151 !important; font-weight:600 !important; }
541
-
542
- td.td{
543
- font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial !important;
544
- font-size: 13px;
545
- color: #111827;
546
- padding: 10px 12px;
547
- background: #fff !important;
548
- }
549
- td.num{ text-align:right !important; }
550
- td.model{ text-align:left !important; min-width: 280px; }
551
- tr.tr:hover td.td{ background: #fafafa !important; }
552
-
553
- th.clickable{ cursor:pointer; user-select:none; }
554
- th.clickable:hover{ background:#f3f4f6 !important; }
555
-
556
- /* cards */
557
- .card{
558
- width: 100%;
559
- border: 1px solid #e5e7eb !important;
560
- border-radius: 8px;
561
- padding: 12px;
562
- background: #fff;
563
- margin-top: 14px;
564
- }
565
- .muted{ color: #6b7280; font-size: 13px; margin: 0 0 10px 0; }
566
- """
567
-
568
- with gr.Blocks(title=f"{APP_NAME} leaderboard") as demo:
569
- with gr.Column(elem_id="page"):
570
- with gr.Row(elem_id="topbar"):
571
- gr.Markdown(f"<div id='titleline'>{APP_NAME} leaderboard</div>")
572
- with gr.Row():
573
- search_text = gr.Textbox(
574
- elem_id="searchbox",
575
- placeholder="Search model…",
576
- show_label=False,
577
- container=False,
578
- scale=1,
579
- )
580
- refresh_btn = gr.Button("Refresh", scale=0)
581
-
582
- # state for sorting
583
- sort_col = gr.State("Avg")
584
- sort_dir = gr.State("desc")
585
-
586
- # leaderboard (approved only)
587
- lb_html = gr.HTML(value=render_lb("", "Avg", "desc"))
588
-
589
- # click bridge
590
- clicked_col = gr.Textbox(visible=False, elem_id="clicked_col")
591
- gr.HTML(
592
- """
593
- <script>
594
- (function(){
595
- function bindClicks(){
596
- const table = document.getElementById("lb_table");
597
- const hidden = document.getElementById("clicked_col");
598
- if(!table || !hidden) return;
599
-
600
- table.querySelectorAll("th.clickable").forEach(th=>{
601
- th.onclick = () => {
602
- const col = th.getAttribute("data-col") || "";
603
- hidden.value = col;
604
- hidden.dispatchEvent(new Event("input", {bubbles:true}));
605
- hidden.dispatchEvent(new Event("change", {bubbles:true}));
606
- };
607
- });
608
- }
609
- const obs = new MutationObserver(()=>bindClicks());
610
- obs.observe(document.body, {subtree:true, childList:true});
611
- setTimeout(bindClicks, 250);
612
- })();
613
- </script>
614
- """
615
- )
616
-
617
- # search/refresh
618
- search_text.change(render_lb, inputs=[search_text, sort_col, sort_dir], outputs=[lb_html])
619
- refresh_btn.click(render_lb, inputs=[search_text, sort_col, sort_dir], outputs=[lb_html])
620
-
621
- def _on_click(col, cur_col, cur_dir, s):
622
- new_col, new_dir = toggle_sort(col, cur_col, cur_dir)
623
- return new_col, new_dir, render_lb(s, new_col, new_dir)
624
-
625
- clicked_col.change(
626
- _on_click,
627
- inputs=[clicked_col, sort_col, sort_dir, search_text],
628
- outputs=[sort_col, sort_dir, lb_html],
629
- )
630
-
631
- # submission card
632
- gr.HTML(
633
- """
634
- <div class="card">
635
- <div class="muted">
636
- <b>Submission</b> — Submissions go to <b>pending</b> and will appear on the leaderboard only after <b>owner approval</b>.
637
- Requires login (Spaces). One submission per user per day. Account must be older than 30 days.
638
- API key will <b>not</b> be stored.
639
- </div>
640
- </div>
641
- """
642
- )
643
-
644
- # submit full-width
645
- with gr.Column():
646
- with gr.Row():
647
- model_name = gr.Textbox(label="Model name", placeholder="e.g. my-model-v1", scale=2)
648
- model_api = gr.Textbox(label="Model API URL", placeholder="https://...", scale=3)
649
- with gr.Row():
650
- api_key = gr.Textbox(label="API key", type="password", placeholder="Will not be stored", scale=3)
651
- with gr.Column(scale=1):
652
- gr.LoginButton()
653
- submit_btn = gr.Button("Submit", variant="primary")
654
- submit_status = gr.Markdown()
655
-
656
- submit_btn.click(
657
- submit,
658
- inputs=[model_name, model_api, api_key, search_text, sort_col, sort_dir],
659
- outputs=[submit_status, lb_html],
660
- )
661
-
662
- # Owner panel (hidden unless owner)
663
- owner_panel = gr.Column(visible=False)
664
- with owner_panel:
665
- gr.HTML(
666
- """
667
- <div class="card">
668
- <div class="muted"><b>Owner review</b> — Visible only to owner accounts.</div>
669
- </div>
670
- """
671
- )
672
- pending_refresh_btn = gr.Button("Refresh pending")
673
- pending_df = gr.Dataframe(
674
- label="Pending submissions",
675
- interactive=False,
676
- wrap=True,
677
- value=pd.DataFrame(columns=["Submitted at", "Submitter", "Model name", "Model API", "_filename"]),
678
- )
679
- pending_pick = gr.Dropdown(label="Select a pending entry", choices=[], value=None)
680
- approve_btn = gr.Button("Approve and publish", variant="primary")
681
- approve_status = gr.Markdown()
682
-
683
- pending_refresh_btn.click(
684
- refresh_pending,
685
- inputs=[gr.State(None)], # placeholder, will be overridden by load/login binding below
686
- outputs=[pending_df, pending_pick],
687
- )
688
-
689
- # IMPORTANT: use load/profile to control visibility + pending refresh
690
- # In Gradio, we can wire OAuth profile by using a dummy fn that takes profile.
691
- def _on_load(profile: gr.OAuthProfile | None, s: str, sc: str, sd: str):
692
- # show owner panel?
693
- vis = show_owner_panel(profile)
694
- # also refresh pending if owner; else empty
695
- pdf, pdd = refresh_pending(profile)
696
- # keep leaderboard rendered (approved)
697
- lb = render_lb(s, sc, sd)
698
- return vis, pdf, pdd, lb
699
-
700
- demo.load(
701
- _on_load,
702
- inputs=[gr.OAuthProfile(), search_text, sort_col, sort_dir],
703
- outputs=[owner_panel, pending_df, pending_pick, lb_html],
704
- )
705
-
706
- # Approve wiring
707
- approve_btn.click(
708
- approve,
709
- inputs=[pending_pick, gr.OAuthProfile(), search_text, sort_col, sort_dir],
710
- outputs=[approve_status, pending_df, pending_pick, lb_html],
711
- )
712
-
713
- # Refresh pending button: needs profile too
714
- pending_refresh_btn.click(
715
- refresh_pending,
716
- inputs=[gr.OAuthProfile()],
717
- outputs=[pending_df, pending_pick],
718
- )
719
-
720
- demo.launch(css=CSS, ssr_mode=False)