ha251 commited on
Commit
80ad682
·
verified ·
1 Parent(s): 6b639e3

Update miniapp_leaderboard.py

Browse files
Files changed (1) hide show
  1. miniapp_leaderboard.py +426 -110
miniapp_leaderboard.py CHANGED
@@ -1,49 +1,263 @@
1
- import html
2
- import pandas as pd
 
 
 
 
 
 
3
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- # 你已有的:LEADERBOARD_COLUMNS / NUMERIC_COLS / _load_entries_df / _apply_search 或 refresh 等
6
- # 这里假设 df 列名还是:
7
- # Model name, Avg, Easy, Mid, Hard, Games, Science, Tools, Humanities, Viz, Lifestyle, Submitted at, Submitter
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- DISPLAY_COLS = [
 
10
  "Avg",
11
  "Model name",
12
- "Easy", "Mid", "Hard",
13
- "Games", "Science", "Tools", "Humanities", "Viz", "Lifestyle",
 
 
 
 
 
 
 
14
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- def _fmt(v):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  if v is None or (isinstance(v, float) and pd.isna(v)):
18
  return ""
19
- if isinstance(v, (int, float)):
20
- return f"{v:.2f}"
21
  return str(v)
22
 
23
- def _render_table(df: pd.DataFrame, sort_col: str, sort_dir: str) -> str:
24
- # 三段:Avg(最左) / Model / Pass Rate(%): Difficulty + Domain
25
- # 注意:这里“Avg.(%)”放最左,满足你要求
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  def th(label, col=None, cls=""):
27
- # 可点击排序的列
28
  if col:
29
  arrow = ""
30
  if col == sort_col:
31
  arrow = " ▲" if sort_dir == "asc" else " ▼"
32
- return f'<th class="{cls} clickable" data-col="{html.escape(col)}">{html.escape(label)}{arrow}</th>'
33
- return f'<th class="{cls}">{html.escape(label)}</th>'
34
 
35
- rows_html = []
36
  for _, r in df.iterrows():
37
  tds = []
38
- for c in DISPLAY_COLS:
39
- val = _fmt(r.get(c, ""))
40
- tds.append(f'<td class="num" data-col="{html.escape(c)}">{html.escape(val)}</td>' if c != "Model name"
41
- else f'<td class="model" data-col="{html.escape(c)}">{html.escape(val)}</td>')
42
- rows_html.append("<tr>" + "".join(tds) + "</tr>")
 
 
43
 
44
- table = f"""
45
  <div class="lb-wrap">
46
- <table class="lb">
47
  <thead>
48
  <tr class="top">
49
  {th("Avg. (%)", "Avg", "avg")}
@@ -71,130 +285,232 @@ def _render_table(df: pd.DataFrame, sort_col: str, sort_dir: str) -> str:
71
  </tr>
72
  </thead>
73
  <tbody>
74
- {"".join(rows_html)}
75
  </tbody>
76
  </table>
77
  </div>
78
  """
79
- return table
80
 
81
- def render_leaderboard(search_text: str, sort_col: str, sort_dir: str):
82
- df = _load_entries_df()
83
- # 搜索仅 Model name
84
- s = (search_text or "").strip().lower()
85
- if s:
86
- df = df[df["Model name"].astype(str).str.lower().str.contains(s, na=False)]
87
 
88
- # 排序
89
- if sort_col in df.columns:
90
- asc = (sort_dir == "asc")
91
- df = df.sort_values(by=[sort_col], ascending=asc, kind="stable", na_position="last")
92
 
93
- return _render_table(df, sort_col, sort_dir)
94
 
95
  def toggle_sort(clicked_col: str, current_col: str, current_dir: str):
96
- # 点击同一列:切换 asc/desc;点击新列:默认 desc
97
  clicked_col = (clicked_col or "").strip()
98
- if not clicked_col:
99
- return gr.update(), current_col, current_dir
100
-
101
  if clicked_col == current_col:
102
- new_dir = "asc" if current_dir == "desc" else "desc"
103
- return clicked_col, clicked_col, new_dir
104
- else:
105
- return clicked_col, clicked_col, "desc"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
 
 
 
 
107
 
108
- CSS = """
109
- /* 搜索框:不明显,一行 */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  #topbar { display:flex; align-items:center; gap:10px; }
111
- #search input { border:1px solid rgba(0,0,0,.12) !important; background:rgba(0,0,0,.02) !important; }
112
- #search label { display:none !important; }
113
- #search input::placeholder { color: rgba(0,0,0,.35); }
 
 
 
 
 
114
 
115
- /* 表格整体风格接近截图 */
116
  .lb-wrap { width: 100%; overflow-x: auto; }
117
  table.lb { width: 100%; border-collapse: collapse; font-family: serif; }
118
- table.lb thead th { text-align:center; font-weight:700; padding:10px 8px; border-bottom:1px solid #222; }
119
- table.lb thead tr.top th { border-top:2px solid #222; }
120
- table.lb thead tr.mid th { border-bottom:1px solid #777; font-weight:700; padding-top:6px; padding-bottom:6px;}
121
- table.lb thead tr.bot th { border-bottom:2px solid #222; padding-top:8px; padding-bottom:8px; }
122
 
123
- table.lb tbody td { padding:10px 10px; border-bottom:1px solid rgba(0,0,0,.08); }
124
- table.lb tbody td.num { text-align:right; white-space:nowrap; }
125
  table.lb tbody td.model { text-align:left; min-width:260px; }
126
 
127
- th.avg { min-width:90px; }
 
128
  th.model { text-align:left; min-width:260px; }
129
- th.group { font-weight:800; letter-spacing:.3px; }
130
 
 
131
  th.clickable { user-select:none; }
132
  th.clickable:hover { background: rgba(0,0,0,.04); cursor:pointer; }
 
 
 
133
  """
134
 
 
135
  with gr.Blocks(title=f"{APP_NAME} leaderboard") as demo:
136
- # 顶部一行:不明显搜索 + refresh
 
137
  with gr.Row(elem_id="topbar"):
138
  search_text = gr.Textbox(
139
- elem_id="search",
140
- placeholder="Search model...",
141
  show_label=False,
142
  container=False,
143
  scale=1,
144
  )
145
  refresh_btn = gr.Button("Refresh", scale=0)
146
 
147
- # 隐藏状态:当前排序列/方向
148
  sort_col = gr.State("Avg")
149
  sort_dir = gr.State("desc")
150
 
151
- lb_html = gr.HTML(value=render_leaderboard("", "Avg", "desc"))
152
-
153
- # 用一个隐藏 textbox 接收“点击的列名”(由 JS 写入)
154
- clicked = gr.Textbox(visible=False)
155
-
156
- # JS:给表头绑定点击事件,把 data-col 写入隐藏组件并触发 change
157
- demo.load(
158
- None,
159
- js="""
160
- () => {
161
- const root = document;
162
- function bind() {
163
- const ths = root.querySelectorAll('th.clickable');
164
- ths.forEach(th => {
165
- th.onclick = () => {
166
- const col = th.getAttribute('data-col');
167
- const hidden = root.querySelector('textarea, input')?.closest('.gradio-container')
168
- return col;
169
- };
170
- });
171
- }
172
- setTimeout(bind, 300);
173
- return null;
174
- }
175
- """,
 
 
 
 
 
 
 
 
176
  )
177
 
178
- # 更稳妥的方式用自定义 JS 直接找到 hidden textbox 并写值触发 input 事件
179
- gr.HTML("""
180
- <script>
181
- function bindLeaderboardClicks(){
182
- const host = document.querySelector('.gradio-container');
183
- if(!host) return;
184
- const hidden = host.querySelector('input[type="text"][data-testid="textbox"]');
185
- // 上面这行在不同版本可能不稳定;因此我们下面用更可靠的:找 visible=false textbox 的 input
186
- }
187
- </script>
188
- """)
189
-
190
- # 简化:不用上面那段不稳定的定位,改用 gradio 的内置“前端事件传参”:
191
- # ——Gradio 目前没有官方 th click -> python 的直接通道,所以建议你用下面“纯后端”方案:
192
- # 1) 先不做 th 点击;改用一行内很轻的 Dropdown 做排序列(你说要点列名,所以这里先给你 HTML 版本)
193
- #
194
- # 如果你愿意接受“点列名不做,仍然一行内轻量排序”,我可以给你完全无 JS 的版本。
195
-
196
- # 先把搜索/刷新做好(无 JS)
197
- search_text.change(render_leaderboard, inputs=[search_text, sort_col, sort_dir], outputs=[lb_html])
198
- refresh_btn.click(render_leaderboard, inputs=[search_text, sort_col, sort_dir], outputs=[lb_html])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
  demo.launch(css=CSS, ssr_mode=False)
 
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
+
16
+ APP_NAME = "miniapp"
17
+
18
+ HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("TOKEN") or os.environ.get("HUGGINGFACE_TOKEN")
19
+ LEADERBOARD_DATASET = (os.environ.get("LEADERBOARD_DATASET") or "").strip()
20
+ MAX_ENTRIES = int(os.environ.get("MAX_ENTRIES", "500"))
21
+
22
+ ENTRIES_PREFIX = "entries/"
23
 
24
+ LEADERBOARD_COLUMNS = [
25
+ "Model name",
26
+ "Avg",
27
+ "Easy",
28
+ "Mid",
29
+ "Hard",
30
+ "Games",
31
+ "Science",
32
+ "Tools",
33
+ "Humanities",
34
+ "Viz",
35
+ "Lifestyle",
36
+ "Submitted at",
37
+ "Submitter",
38
+ ]
39
+
40
+ NUMERIC_COLS = [
41
+ "Avg",
42
+ "Easy",
43
+ "Mid",
44
+ "Hard",
45
+ "Games",
46
+ "Science",
47
+ "Tools",
48
+ "Humanities",
49
+ "Viz",
50
+ "Lifestyle",
51
+ ]
52
 
53
+ # Avg 最左 + 多级表头展示用
54
+ DISPLAY_ORDER = [
55
  "Avg",
56
  "Model name",
57
+ "Easy",
58
+ "Mid",
59
+ "Hard",
60
+ "Games",
61
+ "Science",
62
+ "Tools",
63
+ "Humanities",
64
+ "Viz",
65
+ "Lifestyle",
66
  ]
67
+ SORTABLE_COLS = DISPLAY_ORDER[:]
68
+
69
+ IN_SPACES = bool(
70
+ os.environ.get("SPACE_ID")
71
+ or os.environ.get("SPACE_REPO_NAME")
72
+ or os.environ.get("SPACE_AUTHOR_NAME")
73
+ or os.environ.get("system", "") == "spaces"
74
+ )
75
+
76
+
77
+ def _api() -> HfApi:
78
+ return HfApi(token=HF_TOKEN)
79
+
80
+
81
+ def _is_valid_http_url(url: str) -> bool:
82
+ try:
83
+ parsed = urlparse(url)
84
+ return parsed.scheme in ("http", "https") and bool(parsed.netloc)
85
+ except Exception:
86
+ return False
87
+
88
+
89
+ def _slug(s: str, max_len: int = 60) -> str:
90
+ s = (s or "").strip().lower()
91
+ s = re.sub(r"[^a-z0-9]+", "-", s)
92
+ s = re.sub(r"-{2,}", "-", s).strip("-")
93
+ return (s[:max_len] or "x")
94
+
95
+
96
+ def _empty_df() -> pd.DataFrame:
97
+ return pd.DataFrame(columns=LEADERBOARD_COLUMNS)
98
+
99
+
100
+ def _ensure_dataset_readable() -> tuple[bool, str]:
101
+ if not HF_TOKEN:
102
+ return False, "Space is missing HF_TOKEN (Secrets)."
103
+ if not LEADERBOARD_DATASET:
104
+ return False, "Space is missing LEADERBOARD_DATASET (Secrets)."
105
+ api = _api()
106
+ try:
107
+ api.repo_info(repo_id=LEADERBOARD_DATASET, repo_type="dataset")
108
+ return True, ""
109
+ except RepositoryNotFoundError:
110
+ return False, (
111
+ f"Dataset repo not found: {LEADERBOARD_DATASET}. "
112
+ "Create it first (as a dataset) or fix LEADERBOARD_DATASET."
113
+ )
114
+ except Exception:
115
+ return False, "Cannot access the dataset repo. Check token permissions."
116
 
117
+
118
+ def _list_entry_files() -> list[str]:
119
+ ok, _ = _ensure_dataset_readable()
120
+ if not ok:
121
+ return []
122
+ api = _api()
123
+ try:
124
+ files = api.list_repo_files(repo_id=LEADERBOARD_DATASET, repo_type="dataset")
125
+ except Exception:
126
+ return []
127
+
128
+ entry_files = [f for f in files if f.startswith(ENTRIES_PREFIX) and f.endswith(".json")]
129
+ entry_files.sort(reverse=True)
130
+ return entry_files[:MAX_ENTRIES]
131
+
132
+
133
+ def _load_entries_df() -> pd.DataFrame:
134
+ ok, _ = _ensure_dataset_readable()
135
+ if not ok:
136
+ return _empty_df()
137
+
138
+ rows: list[dict] = []
139
+ for filename in _list_entry_files():
140
+ try:
141
+ path = hf_hub_download(
142
+ repo_id=LEADERBOARD_DATASET,
143
+ repo_type="dataset",
144
+ filename=filename,
145
+ token=HF_TOKEN,
146
+ )
147
+ with open(path, "r", encoding="utf-8") as fp:
148
+ row = json.load(fp)
149
+ rows.append(row)
150
+ except Exception:
151
+ continue
152
+
153
+ if not rows:
154
+ return _empty_df()
155
+
156
+ df = pd.DataFrame(rows)
157
+ for c in LEADERBOARD_COLUMNS:
158
+ if c not in df.columns:
159
+ df[c] = ""
160
+
161
+ df = df[LEADERBOARD_COLUMNS]
162
+ for c in NUMERIC_COLS:
163
+ df[c] = pd.to_numeric(df[c], errors="coerce")
164
+
165
+ # 默认按提交时间倒序
166
+ df = df.sort_values(by=["Submitted at"], ascending=False, kind="stable")
167
+ return df
168
+
169
+
170
+ def _parse_hf_created_at(created_at: str) -> datetime.datetime | None:
171
+ try:
172
+ if created_at.endswith("Z"):
173
+ created_at = created_at[:-1] + "+00:00"
174
+ return datetime.datetime.fromisoformat(created_at)
175
+ except Exception:
176
+ return None
177
+
178
+
179
+ def _check_user_eligibility(username: str) -> tuple[bool, str]:
180
+ """
181
+ - Must be older than ~4 months (>= 120 days)
182
+ """
183
+ try:
184
+ r = requests.get(f"https://huggingface.co/api/users/{username}/overview", timeout=10)
185
+ r.raise_for_status()
186
+ created_at = r.json().get("createdAt")
187
+ if not created_at:
188
+ return False, "Cannot verify account creation date."
189
+ dt = _parse_hf_created_at(created_at)
190
+ if not dt:
191
+ return False, "Cannot parse account creation date."
192
+ now = datetime.datetime.now(datetime.timezone.utc)
193
+ if dt.tzinfo is None:
194
+ dt = dt.replace(tzinfo=datetime.timezone.utc)
195
+ age_days = (now - dt).days
196
+ if age_days < 120:
197
+ return False, "Account must be older than 4 months to submit."
198
+ return True, ""
199
+ except Exception:
200
+ return False, "Cannot verify Hugging Face account. Please try again later."
201
+
202
+
203
+ def _submitted_today(username: str) -> bool:
204
+ df = _load_entries_df()
205
+ if df.empty:
206
+ return False
207
+ today = datetime.datetime.utcnow().date().isoformat()
208
+ user_rows = df[df["Submitter"].astype(str) == username]
209
+ if user_rows.empty:
210
+ return False
211
+ return any(str(v).startswith(today) for v in user_rows["Submitted at"].tolist())
212
+
213
+
214
+ # ---------- HTML Leaderboard helpers ----------
215
+ def _fmt_cell(v):
216
  if v is None or (isinstance(v, float) and pd.isna(v)):
217
  return ""
218
+ if isinstance(v, (int, float, np.number)):
219
+ return f"{float(v):.2f}"
220
  return str(v)
221
 
222
+
223
+ def _apply_search_and_sort(df: pd.DataFrame, search_text: str, sort_col: str, sort_dir: str) -> pd.DataFrame:
224
+ # 搜索仅 Model name
225
+ s = (search_text or "").strip().lower()
226
+ if s:
227
+ df = df[df["Model name"].astype(str).str.lower().str.contains(s, na=False)]
228
+
229
+ # 排序
230
+ sort_col = sort_col if sort_col in df.columns else "Avg"
231
+ asc = sort_dir == "asc"
232
+ df = df.sort_values(by=[sort_col], ascending=asc, kind="stable", na_position="last")
233
+ return df
234
+
235
+
236
+ def _render_leaderboard_html(df: pd.DataFrame, sort_col: str, sort_dir: str) -> str:
237
+ import html as _html
238
+
239
  def th(label, col=None, cls=""):
 
240
  if col:
241
  arrow = ""
242
  if col == sort_col:
243
  arrow = " ▲" if sort_dir == "asc" else " ▼"
244
+ return f'<th class="clickable {cls}" data-col="{_html.escape(col)}">{_html.escape(label)}{arrow}</th>'
245
+ return f'<th class="{cls}">{_html.escape(label)}</th>'
246
 
247
+ trs = []
248
  for _, r in df.iterrows():
249
  tds = []
250
+ for c in DISPLAY_ORDER:
251
+ val = _fmt_cell(r.get(c, ""))
252
+ if c == "Model name":
253
+ tds.append(f'<td class="model">{_html.escape(val)}</td>')
254
+ else:
255
+ tds.append(f'<td class="num">{_html.escape(val)}</td>')
256
+ trs.append("<tr>" + "".join(tds) + "</tr>")
257
 
258
+ return f"""
259
  <div class="lb-wrap">
260
+ <table class="lb" id="lb_table">
261
  <thead>
262
  <tr class="top">
263
  {th("Avg. (%)", "Avg", "avg")}
 
285
  </tr>
286
  </thead>
287
  <tbody>
288
+ {''.join(trs)}
289
  </tbody>
290
  </table>
291
  </div>
292
  """
 
293
 
 
 
 
 
 
 
294
 
295
+ def render_lb(search_text: str, sort_col: str, sort_dir: str) -> str:
296
+ df = _load_entries_df()
297
+ df = _apply_search_and_sort(df, search_text, sort_col, sort_dir)
298
+ return _render_leaderboard_html(df, sort_col, sort_dir)
299
 
 
300
 
301
  def toggle_sort(clicked_col: str, current_col: str, current_dir: str):
 
302
  clicked_col = (clicked_col or "").strip()
303
+ if clicked_col not in SORTABLE_COLS:
304
+ return current_col, current_dir
 
305
  if clicked_col == current_col:
306
+ return current_col, ("asc" if current_dir == "desc" else "desc")
307
+ return clicked_col, "desc"
308
+
309
+
310
+ # ---------- Submit ----------
311
+ def submit(
312
+ model_api: str,
313
+ api_key: str,
314
+ search_text: str,
315
+ sort_col: str,
316
+ sort_dir: str,
317
+ profile: gr.OAuthProfile | None,
318
+ ):
319
+ # Login required to submit (in Spaces)
320
+ if IN_SPACES and (profile is None or not getattr(profile, "username", None)):
321
+ return "You must log in to submit.", render_lb(search_text, sort_col, sort_dir)
322
+
323
+ submitter = (getattr(profile, "username", None) if profile is not None else "local") or "anonymous"
324
+
325
+ model_api = (model_api or "").strip()
326
+ api_key = (api_key or "").strip()
327
+
328
+ if not model_api:
329
+ return "Model API URL is required.", render_lb(search_text, sort_col, sort_dir)
330
+ if not _is_valid_http_url(model_api):
331
+ return "Model API must be a valid http(s) URL.", render_lb(search_text, sort_col, sort_dir)
332
+ if not api_key:
333
+ return "API key is required.", render_lb(search_text, sort_col, sort_dir)
334
+
335
+ ok, msg = _ensure_dataset_readable()
336
+ if not ok:
337
+ return msg, render_lb(search_text, sort_col, sort_dir)
338
+
339
+ if IN_SPACES:
340
+ ok, msg = _check_user_eligibility(submitter)
341
+ if not ok:
342
+ return msg, render_lb(search_text, sort_col, sort_dir)
343
+
344
+ if _submitted_today(submitter):
345
+ return "You have already submitted today. Please try again tomorrow.", render_lb(search_text, sort_col, sort_dir)
346
 
347
+ # api_key collected but NOT stored
348
+ now = datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
349
+ nonce = uuid.uuid4().hex[:8]
350
+ safe_user = _slug(submitter)
351
 
352
+ host = urlparse(model_api).netloc or "unknown"
353
+ model_name = host
354
+
355
+ safe_model = _slug(model_name)
356
+ path_in_repo = f"{ENTRIES_PREFIX}{now[:10]}/{now}-{safe_user}-{safe_model}-{nonce}.json"
357
+
358
+ payload = {
359
+ "Model name": model_name,
360
+ "Avg": None,
361
+ "Easy": None,
362
+ "Mid": None,
363
+ "Hard": None,
364
+ "Games": None,
365
+ "Science": None,
366
+ "Tools": None,
367
+ "Humanities": None,
368
+ "Viz": None,
369
+ "Lifestyle": None,
370
+ "Submitted at": now,
371
+ "Submitter": submitter,
372
+ "Model API": model_api,
373
+ }
374
+
375
+ api = _api()
376
+ data = (json.dumps(payload, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
377
+ api.upload_file(
378
+ repo_id=LEADERBOARD_DATASET,
379
+ repo_type="dataset",
380
+ path_or_fileobj=io.BytesIO(data),
381
+ path_in_repo=path_in_repo,
382
+ commit_message=f"miniapp: submit {submitter}/{model_name}",
383
+ token=HF_TOKEN,
384
+ )
385
+
386
+ return "Submitted successfully.", render_lb(search_text, sort_col, sort_dir)
387
+
388
+
389
+ CSS = r"""
390
+ /* 顶部一行:搜索不明显 */
391
  #topbar { display:flex; align-items:center; gap:10px; }
392
+ #searchbox { max-width: 320px; }
393
+ #searchbox label { display:none !important; }
394
+ #searchbox textarea, #searchbox input {
395
+ border: 1px solid rgba(0,0,0,.10) !important;
396
+ background: rgba(0,0,0,.02) !important;
397
+ box-shadow: none !important;
398
+ }
399
+ #searchbox textarea::placeholder, #searchbox input::placeholder { color: rgba(0,0,0,.35); }
400
 
401
+ /* 表格风格接近截图 */
402
  .lb-wrap { width: 100%; overflow-x: auto; }
403
  table.lb { width: 100%; border-collapse: collapse; font-family: serif; }
404
+ table.lb thead th { text-align:center; font-weight:700; padding:10px 8px; border-bottom:1px solid #222; white-space:nowrap; }
405
+ table.lb thead tr.top th { border-top:2px solid #222; font-size: 22px; }
406
+ table.lb thead tr.mid th { border-bottom:1px solid #777; font-weight:700; padding-top:6px; padding-bottom:6px; font-size: 20px; }
407
+ table.lb thead tr.bot th { border-bottom:2px solid #222; padding-top:8px; padding-bottom:8px; font-size: 20px; }
408
 
409
+ table.lb tbody td { padding:10px 10px; border-bottom:1px solid rgba(0,0,0,.08); font-family: sans-serif; font-size: 14px; }
410
+ table.lb tbody td.num { text-align:right; }
411
  table.lb tbody td.model { text-align:left; min-width:260px; }
412
 
413
+ th.group { font-weight:800; letter-spacing:.2px; }
414
+ th.avg { min-width:100px; }
415
  th.model { text-align:left; min-width:260px; }
 
416
 
417
+ /* 点击排序 */
418
  th.clickable { user-select:none; }
419
  th.clickable:hover { background: rgba(0,0,0,.04); cursor:pointer; }
420
+
421
+ /* 提交区紧凑 */
422
+ #submitbox { max-width: 720px; }
423
  """
424
 
425
+
426
  with gr.Blocks(title=f"{APP_NAME} leaderboard") as demo:
427
+ gr.Markdown(f"## {APP_NAME} leaderboard")
428
+
429
  with gr.Row(elem_id="topbar"):
430
  search_text = gr.Textbox(
431
+ elem_id="searchbox",
432
+ placeholder="Search model",
433
  show_label=False,
434
  container=False,
435
  scale=1,
436
  )
437
  refresh_btn = gr.Button("Refresh", scale=0)
438
 
439
+ # 排序状态
440
  sort_col = gr.State("Avg")
441
  sort_dir = gr.State("desc")
442
 
443
+ # HTML leaderboard
444
+ lb_html = gr.HTML(value=render_lb("", "Avg", "desc"))
445
+
446
+ # elem_id 精确定位的隐藏输入:JS 写入点击列名
447
+ clicked_col = gr.Textbox(visible=False, elem_id="clicked_col")
448
+
449
+ # JS:绑定表头点击,把 data-col 写入隐藏 textbox
450
+ gr.HTML(
451
+ """
452
+ <script>
453
+ (function(){
454
+ function bindClicks(){
455
+ const table = document.getElementById("lb_table");
456
+ const hidden = document.getElementById("clicked_col");
457
+ if(!table || !hidden) return;
458
+
459
+ table.querySelectorAll("th.clickable").forEach(th=>{
460
+ th.onclick = () => {
461
+ const col = th.getAttribute("data-col") || "";
462
+ hidden.value = col;
463
+ hidden.dispatchEvent(new Event("input", {bubbles:true}));
464
+ hidden.dispatchEvent(new Event("change", {bubbles:true}));
465
+ };
466
+ });
467
+ }
468
+
469
+ // 首次 + 每次页面更新后都尝试重新绑定
470
+ const obs = new MutationObserver(()=>bindClicks());
471
+ obs.observe(document.body, {subtree:true, childList:true});
472
+ setTimeout(bindClicks, 300);
473
+ })();
474
+ </script>
475
+ """
476
  )
477
 
478
+ # 搜索/刷新重新渲染 HTML
479
+ search_text.change(render_lb, inputs=[search_text, sort_col, sort_dir], outputs=[lb_html])
480
+ refresh_btn.click(render_lb, inputs=[search_text, sort_col, sort_dir], outputs=[lb_html])
481
+
482
+ # 点击列名:更新排序状态 + 重新渲染
483
+ def _on_click(col, cur_col, cur_dir, s):
484
+ new_col, new_dir = toggle_sort(col, cur_col, cur_dir)
485
+ return new_col, new_dir, render_lb(s, new_col, new_dir)
486
+
487
+ clicked_col.change(
488
+ _on_click,
489
+ inputs=[clicked_col, sort_col, sort_dir, search_text],
490
+ outputs=[sort_col, sort_dir, lb_html],
491
+ )
492
+
493
+ gr.Markdown(
494
+ "### Submission\n"
495
+ "- Submit **Model API URL** and **API key** only\n"
496
+ "- Requires login (in Spaces)\n"
497
+ "- One submission per user per day\n"
498
+ "- HF account must be older than 4 months\n"
499
+ "- API key **will not be stored**"
500
+ )
501
+
502
+ with gr.Column(elem_id="submitbox"):
503
+ model_api = gr.Textbox(label="Model API URL (required)", placeholder="https://...")
504
+ api_key = gr.Textbox(label="API key (required)", type="password", placeholder="Will not be stored")
505
+ with gr.Row():
506
+ gr.LoginButton()
507
+ submit_btn = gr.Button("Submit", variant="primary")
508
+ status = gr.Markdown()
509
+
510
+ submit_btn.click(
511
+ submit,
512
+ inputs=[model_api, api_key, search_text, sort_col, sort_dir],
513
+ outputs=[status, lb_html],
514
+ )
515
 
516
  demo.launch(css=CSS, ssr_mode=False)