ha251 commited on
Commit
18d30a1
·
verified ·
1 Parent(s): c5c3ca2

Update miniapp_leaderboard.py

Browse files
Files changed (1) hide show
  1. miniapp_leaderboard.py +307 -476
miniapp_leaderboard.py CHANGED
@@ -7,17 +7,14 @@ import uuid
7
  from urllib.parse import urlparse
8
 
9
  import gradio as gr
 
10
  import pandas as pd
11
  import requests
12
  from huggingface_hub import HfApi, hf_hub_download
13
  from huggingface_hub.errors import RepositoryNotFoundError
14
 
15
-
16
  APP_NAME = "miniapp"
17
 
18
- # Space Secrets:
19
- # - HF_TOKEN: Hugging Face access token with WRITE access to the dataset repo
20
- # - LEADERBOARD_DATASET: dataset repo id like "ha251/miniapp-leaderboard"
21
  HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("TOKEN") or os.environ.get("HUGGINGFACE_TOKEN")
22
  LEADERBOARD_DATASET = (os.environ.get("LEADERBOARD_DATASET") or "").strip()
23
  MAX_ENTRIES = int(os.environ.get("MAX_ENTRIES", "500"))
@@ -53,6 +50,22 @@ NUMERIC_COLS = [
53
  "Lifestyle",
54
  ]
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  IN_SPACES = bool(
57
  os.environ.get("SPACE_ID")
58
  or os.environ.get("SPACE_REPO_NAME")
@@ -144,44 +157,15 @@ def _load_entries_df() -> pd.DataFrame:
144
  for c in LEADERBOARD_COLUMNS:
145
  if c not in df.columns:
146
  df[c] = ""
 
147
  df = df[LEADERBOARD_COLUMNS]
148
  for c in NUMERIC_COLS:
149
  df[c] = pd.to_numeric(df[c], errors="coerce")
150
- df = df.sort_values(by=["Submitted at"], ascending=False, kind="stable")
151
- return df
152
-
153
-
154
- def _apply_search_and_sort(
155
- df: pd.DataFrame,
156
- search_text: str,
157
- search_in: str,
158
- sort_by: str,
159
- sort_order: str,
160
- ) -> pd.DataFrame:
161
- search_text = (search_text or "").strip().lower()
162
- if search_text:
163
- if search_in == "Model name":
164
- df = df[df["Model name"].astype(str).str.lower().str.contains(search_text, na=False)]
165
- elif search_in == "Submitter":
166
- df = df[df["Submitter"].astype(str).str.lower().str.contains(search_text, na=False)]
167
- else:
168
- mask = (
169
- df["Model name"].astype(str).str.lower().str.contains(search_text, na=False)
170
- | df["Submitter"].astype(str).str.lower().str.contains(search_text, na=False)
171
- )
172
- df = df[mask]
173
 
174
- ascending = sort_order == "Ascending"
175
- if sort_by in df.columns:
176
- df = df.sort_values(by=[sort_by], ascending=ascending, kind="stable", na_position="last")
177
  return df
178
 
179
 
180
- def refresh(search_text: str, search_in: str, sort_by: str, sort_order: str):
181
- df = _load_entries_df()
182
- return _apply_search_and_sort(df, search_text, search_in, sort_by, sort_order)
183
-
184
-
185
  def _parse_hf_created_at(created_at: str) -> datetime.datetime | None:
186
  try:
187
  if created_at.endswith("Z"):
@@ -192,9 +176,6 @@ def _parse_hf_created_at(created_at: str) -> datetime.datetime | None:
192
 
193
 
194
  def _check_user_eligibility(username: str) -> tuple[bool, str]:
195
- """
196
- - Must be older than ~4 months (>= 120 days)
197
- """
198
  try:
199
  r = requests.get(f"https://huggingface.co/api/users/{username}/overview", timeout=10)
200
  r.raise_for_status()
@@ -207,8 +188,7 @@ def _check_user_eligibility(username: str) -> tuple[bool, str]:
207
  now = datetime.datetime.now(datetime.timezone.utc)
208
  if dt.tzinfo is None:
209
  dt = dt.replace(tzinfo=datetime.timezone.utc)
210
- age_days = (now - dt).days
211
- if age_days < 120:
212
  return False, "Account must be older than 4 months to submit."
213
  return True, ""
214
  except Exception:
@@ -226,76 +206,163 @@ def _submitted_today(username: str) -> bool:
226
  return any(str(v).startswith(today) for v in user_rows["Submitted at"].tolist())
227
 
228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  def submit(
230
- model_name: str,
231
  model_api: str,
232
  api_key: str,
233
- avg: float,
234
- easy: float,
235
- mid: float,
236
- hard: float,
237
- games: float,
238
- science: float,
239
- tools: float,
240
- humanities: float,
241
- viz: float,
242
- lifestyle: float,
243
  search_text: str,
244
- search_in: str,
245
- sort_by: str,
246
- sort_order: str,
247
  profile: gr.OAuthProfile | None,
248
  ):
249
- # Login required to submit (in Spaces)
250
  if IN_SPACES and (profile is None or not getattr(profile, "username", None)):
251
- return "You must log in to submit.", refresh(search_text, search_in, sort_by, sort_order)
252
 
253
  submitter = (getattr(profile, "username", None) if profile is not None else "local") or "anonymous"
254
 
255
- model_name = (model_name or "").strip()
256
  model_api = (model_api or "").strip()
257
  api_key = (api_key or "").strip()
258
 
259
- if not model_name:
260
- return "Model name is required.", refresh(search_text, search_in, sort_by, sort_order)
261
  if not model_api:
262
- return "Model API URL is required.", refresh(search_text, search_in, sort_by, sort_order)
263
  if not _is_valid_http_url(model_api):
264
- return "Model API must be a valid http(s) URL.", refresh(search_text, search_in, sort_by, sort_order)
265
  if not api_key:
266
- return "API key is required.", refresh(search_text, search_in, sort_by, sort_order)
267
 
268
  ok, msg = _ensure_dataset_readable()
269
  if not ok:
270
- return msg, refresh(search_text, search_in, sort_by, sort_order)
271
 
272
  if IN_SPACES:
273
  ok, msg = _check_user_eligibility(submitter)
274
  if not ok:
275
- return msg, refresh(search_text, search_in, sort_by, sort_order)
276
 
277
  if _submitted_today(submitter):
278
- return "You have already submitted today. Please try again tomorrow.", refresh(search_text, search_in, sort_by, sort_order)
279
 
280
  now = datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
281
  nonce = uuid.uuid4().hex[:8]
282
- safe_model = _slug(model_name)
283
  safe_user = _slug(submitter)
 
 
 
 
 
284
  path_in_repo = f"{ENTRIES_PREFIX}{now[:10]}/{now}-{safe_user}-{safe_model}-{nonce}.json"
285
 
286
- # NOTE: api_key is collected but NOT stored.
287
  payload = {
288
  "Model name": model_name,
289
- "Avg": avg,
290
- "Easy": easy,
291
- "Mid": mid,
292
- "Hard": hard,
293
- "Games": games,
294
- "Science": science,
295
- "Tools": tools,
296
- "Humanities": humanities,
297
- "Viz": viz,
298
- "Lifestyle": lifestyle,
299
  "Submitted at": now,
300
  "Submitter": submitter,
301
  "Model API": model_api,
@@ -312,417 +379,181 @@ def submit(
312
  token=HF_TOKEN,
313
  )
314
 
315
- return "Submitted successfully.", refresh(search_text, search_in, sort_by, sort_order)
316
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
  with gr.Blocks(title=f"{APP_NAME} leaderboard") as demo:
319
- # Layout: Overview -> Leaderboard -> Submission instructions
320
- gr.Markdown(
321
- f"## {APP_NAME} leaderboard\n\n"
322
- "### Overview\n"
323
- "_Placeholder overview text. Replace this with your benchmark description, rules, and links._\n\n"
324
- "### Leaderboard\n"
325
- "_The leaderboard below supports sorting and searching._\n\n"
326
- "### Submission\n"
327
- "_Placeholder submission instructions. Requires login. One submission per user per day. "
328
- "Account must be older than 4 months._\n"
329
- )
330
-
331
- with gr.Row():
332
- with gr.Column(scale=3):
333
  with gr.Row():
334
- search_text = gr.Textbox(label="Search", placeholder="Model name or submitter")
335
- search_in = gr.Dropdown(
336
- label="Search in",
337
- choices=["Both", "Model name", "Submitter"],
338
- value="Both",
 
339
  )
340
- with gr.Row():
341
- sort_by = gr.Dropdown(label="Sort by", choices=LEADERBOARD_COLUMNS, value="Avg")
342
- sort_order = gr.Radio(label="Order", choices=["Descending", "Ascending"], value="Descending")
343
- refresh_btn = gr.Button("Refresh")
344
-
345
- leaderboard = gr.Dataframe(
346
- label="Leaderboard",
347
- value=_load_entries_df(),
348
- interactive=False,
349
- wrap=True,
350
- )
351
-
352
- with gr.Column(scale=2):
353
- with gr.Accordion("Submit (login required)", open=True):
354
- model_name = gr.Textbox(label="Model name (required)")
355
- model_api = gr.Textbox(label="Model API (required)", placeholder="https://...")
356
- api_key = gr.Textbox(label="API key (required)", type="password", placeholder="Will not be stored")
357
-
358
- with gr.Row():
359
- avg = gr.Number(label="Avg", value=0)
360
- easy = gr.Number(label="Easy", value=0)
361
- mid = gr.Number(label="Mid", value=0)
362
- hard = gr.Number(label="Hard", value=0)
363
-
364
- with gr.Row():
365
- games = gr.Number(label="Games", value=0)
366
- science = gr.Number(label="Science", value=0)
367
- tools = gr.Number(label="Tools", value=0)
368
-
369
- with gr.Row():
370
- humanities = gr.Number(label="Humanities", value=0)
371
- viz = gr.Number(label="Viz", value=0)
372
- lifestyle = gr.Number(label="Lifestyle", value=0)
373
-
374
- with gr.Row():
375
- gr.LoginButton()
376
- submit_btn = gr.Button("Submit", variant="primary")
377
- status = gr.Markdown()
378
-
379
- refresh_btn.click(refresh, inputs=[search_text, search_in, sort_by, sort_order], outputs=[leaderboard])
380
- submit_btn.click(
381
- submit,
382
- inputs=[
383
- model_name,
384
- model_api,
385
- api_key,
386
- avg,
387
- easy,
388
- mid,
389
- hard,
390
- games,
391
- science,
392
- tools,
393
- humanities,
394
- viz,
395
- lifestyle,
396
- search_text,
397
- search_in,
398
- sort_by,
399
- sort_order,
400
- ],
401
- outputs=[status, leaderboard],
402
- )
403
-
404
- demo.launch()
405
-
406
- import datetime
407
- import io
408
- import json
409
- import os
410
- import re
411
- import uuid
412
- from urllib.parse import urlparse
413
-
414
- import gradio as gr
415
- import pandas as pd
416
- from huggingface_hub import HfApi, hf_hub_download
417
-
418
-
419
- APP_NAME = "miniapp"
420
-
421
- # 在 Space 里通过 Secrets 配置:
422
- # - HF_TOKEN: 具有写 dataset 权限的 token(Settings -> Variables and secrets -> Secrets)
423
- # - LEADERBOARD_DATASET: 形如 "your-username/miniapp-leaderboard"(repo_type=dataset)
424
- HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("TOKEN") or os.environ.get("HUGGINGFACE_TOKEN")
425
- LEADERBOARD_DATASET = os.environ.get("LEADERBOARD_DATASET", "").strip()
426
- # Owner 审核口令(放到 Space Secrets;不要放到公开 Variables)
427
- OWNER_REVIEW_TOKEN = os.environ.get("OWNER_REVIEW_TOKEN", "").strip()
428
-
429
- # 判断是否运行在 Hugging Face Spaces
430
- IN_SPACES = bool(
431
- os.environ.get("SPACE_ID")
432
- or os.environ.get("SPACE_REPO_NAME")
433
- or os.environ.get("SPACE_AUTHOR_NAME")
434
- or os.environ.get("system", "") == "spaces"
435
- )
436
-
437
- MAX_ENTRIES = int(os.environ.get("MAX_ENTRIES", "200"))
438
-
439
- PENDING_PREFIX = "pending/"
440
- APPROVED_PREFIX = "approved/"
441
-
442
-
443
- def _is_valid_http_url(url: str) -> bool:
444
- try:
445
- parsed = urlparse(url)
446
- return parsed.scheme in ("http", "https") and bool(parsed.netloc)
447
- except Exception:
448
- return False
449
-
450
-
451
- def _slug(s: str, max_len: int = 60) -> str:
452
- s = (s or "").strip().lower()
453
- s = re.sub(r"[^a-z0-9]+", "-", s)
454
- s = re.sub(r"-{2,}", "-", s).strip("-")
455
- return (s[:max_len] or "model")
456
-
457
-
458
- def _api() -> HfApi:
459
- return HfApi(token=HF_TOKEN)
460
-
461
-
462
- def _ensure_dataset_repo():
463
- if not HF_TOKEN:
464
- raise RuntimeError("未配置 HF_TOKEN(Space Secrets)。")
465
- if not LEADERBOARD_DATASET:
466
- raise RuntimeError("未配置 LEADERBOARD_DATASET(例如:your-username/miniapp-leaderboard)。")
467
- api = _api()
468
- try:
469
- api.repo_info(repo_id=LEADERBOARD_DATASET, repo_type="dataset")
470
- except Exception:
471
- # 不存在则创建(public dataset;你也可以手动创建并设为 private)
472
- api.create_repo(repo_id=LEADERBOARD_DATASET, repo_type="dataset", private=False, exist_ok=True)
473
-
474
-
475
- def _empty_df() -> pd.DataFrame:
476
- return pd.DataFrame(columns=["submitted_at", "username", "model_name", "model_api", "notes"])
477
-
478
-
479
- def _list_json_files(prefix: str) -> list[str]:
480
- if not HF_TOKEN or not LEADERBOARD_DATASET:
481
- return []
482
-
483
- api = _api()
484
- try:
485
- files = api.list_repo_files(repo_id=LEADERBOARD_DATASET, repo_type="dataset")
486
- except Exception:
487
- return []
488
-
489
- return sorted(
490
- [f for f in files if f.startswith(prefix) and f.endswith(".json")],
491
- reverse=True,
492
- )[:MAX_ENTRIES]
493
-
494
-
495
- def _load_entries_df(prefix: str, include_filename: bool) -> pd.DataFrame:
496
- files = _list_json_files(prefix)
497
- rows = []
498
- for filename in files:
499
- try:
500
- path = hf_hub_download(
501
- repo_id=LEADERBOARD_DATASET,
502
- repo_type="dataset",
503
- filename=filename,
504
- token=HF_TOKEN,
505
- )
506
- with open(path, "r", encoding="utf-8") as fp:
507
- row = json.load(fp)
508
- if include_filename:
509
- row["_filename"] = filename
510
- rows.append(row)
511
- except Exception:
512
- continue
513
-
514
- if not rows:
515
- df = _empty_df()
516
- if include_filename:
517
- df["_filename"] = []
518
- return df
519
-
520
- df = pd.DataFrame(rows)
521
- for col in ["submitted_at", "username", "model_name", "model_api", "notes"]:
522
- if col not in df.columns:
523
- df[col] = ""
524
- cols = ["submitted_at", "username", "model_name", "model_api", "notes"]
525
- if include_filename:
526
- if "_filename" not in df.columns:
527
- df["_filename"] = ""
528
- cols = cols + ["_filename"]
529
- df = df[cols]
530
- df = df.sort_values(by=["submitted_at"], ascending=False, kind="stable")
531
- return df
532
-
533
-
534
- def refresh():
535
- return _load_entries_df(APPROVED_PREFIX, include_filename=False)
536
-
537
-
538
- def refresh_pending():
539
- df = _load_entries_df(PENDING_PREFIX, include_filename=True)
540
- choices = list(df["_filename"]) if "_filename" in df.columns else []
541
- return df, gr.update(choices=choices, value=choices[0] if choices else None)
542
-
543
-
544
- def submit(model_name: str, model_api: str, notes: str, username: str | None):
545
- model_name = (model_name or "").strip()
546
- model_api = (model_api or "").strip()
547
- notes = (notes or "").strip()
548
- username = (username or "").strip() or ("local" if not IN_SPACES else "anonymous")
549
-
550
- if not model_name:
551
- return "请填写 **模型名称**。", refresh()
552
- if not model_api:
553
- return "请填写 **模型 API**。", refresh()
554
- if not _is_valid_http_url(model_api):
555
- return "**模型 API** 需要是合法的 `http(s)://...` URL。", refresh()
556
-
557
- if not HF_TOKEN:
558
- return "Space 未配置 **HF_TOKEN**(Secrets),无法写入排行榜。", refresh()
559
- if not LEADERBOARD_DATASET:
560
- return "Space 未配置 **LEADERBOARD_DATASET**(例如:`your-username/miniapp-leaderboard`)。", refresh()
561
-
562
- _ensure_dataset_repo()
563
- api = _api()
564
-
565
- now = datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
566
- safe_model = _slug(model_name)
567
- safe_user = _slug(username)
568
- nonce = uuid.uuid4().hex[:8]
569
- path_in_repo = f"{PENDING_PREFIX}{now[:10]}/{now}-{safe_user}-{safe_model}-{nonce}.json"
570
-
571
- payload = {
572
- "submitted_at": now,
573
- "username": username,
574
- "model_name": model_name,
575
- "model_api": model_api,
576
- "notes": notes,
577
- }
578
- data = (json.dumps(payload, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
579
- bio = io.BytesIO(data)
580
-
581
- api.upload_file(
582
- repo_id=LEADERBOARD_DATASET,
583
- repo_type="dataset",
584
- path_or_fileobj=bio,
585
- path_in_repo=path_in_repo,
586
- commit_message=f"miniapp: submit(pending) {username}/{model_name}",
587
- token=HF_TOKEN,
588
- )
589
-
590
- return "已提交,等待 owner 审核后才会上榜。", refresh()
591
-
592
-
593
- def approve(pending_filename: str | None, review_token: str | None):
594
- pending_filename = (pending_filename or "").strip()
595
- review_token = (review_token or "").strip()
596
-
597
- if not pending_filename:
598
- df, dd = refresh_pending()
599
- return "请选择一条待审核提交。", df, dd, refresh()
600
-
601
- if not OWNER_REVIEW_TOKEN:
602
- df, dd = refresh_pending()
603
- return "Space 未配置 **OWNER_REVIEW_TOKEN**(Secrets),无法启用审核。", df, dd, refresh()
604
 
605
- if review_token != OWNER_REVIEW_TOKEN:
606
- df, dd = refresh_pending()
607
- return "审核口令不正确。", df, dd, refresh()
608
 
609
- if not HF_TOKEN or not LEADERBOARD_DATASET:
610
- df, dd = refresh_pending()
611
- return "Space 未配置 HF 写入权限,无法审核。", df, dd, refresh()
612
 
613
- api = _api()
614
- try:
615
- local_path = hf_hub_download(
616
- repo_id=LEADERBOARD_DATASET,
617
- repo_type="dataset",
618
- filename=pending_filename,
619
- token=HF_TOKEN,
620
  )
621
- with open(local_path, "r", encoding="utf-8") as fp:
622
- payload = json.load(fp)
623
- except Exception as e:
624
- df, dd = refresh_pending()
625
- return f"读取待审核文件失败:{e}", df, dd, refresh()
626
-
627
- # 将文件复制到 approved 目录(保留原提交时间等字段)
628
- base = pending_filename[len(PENDING_PREFIX) :] if pending_filename.startswith(PENDING_PREFIX) else pending_filename
629
- approved_filename = f"{APPROVED_PREFIX}{base}"
630
- data = (json.dumps(payload, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
631
- bio = io.BytesIO(data)
632
 
633
- try:
634
- api.upload_file(
635
- repo_id=LEADERBOARD_DATASET,
636
- repo_type="dataset",
637
- path_or_fileobj=bio,
638
- path_in_repo=approved_filename,
639
- commit_message=f"miniapp: approve {payload.get('username','')}/{payload.get('model_name','')}",
640
- token=HF_TOKEN,
641
- )
642
- # 删除 pending 原文件(真正“上榜前审核”)
643
- api.delete_file(
644
- repo_id=LEADERBOARD_DATASET,
645
- repo_type="dataset",
646
- path_in_repo=pending_filename,
647
- commit_message=f"miniapp: remove pending {pending_filename}",
648
- token=HF_TOKEN,
649
  )
650
- except Exception as e:
651
- df, dd = refresh_pending()
652
- return f"审核写入失败:{e}", df, dd, refresh()
653
 
654
- pending_df, pending_dd = refresh_pending()
655
- approved_df = refresh()
656
- return "已通过审核并更新榜单。", pending_df, pending_dd, approved_df
657
-
658
-
659
- with gr.Blocks(title=f"{APP_NAME} leaderboard") as demo:
660
- gr.Markdown(
661
- f"## {APP_NAME} leaderboard\n\n"
662
- "用户提交信息后会进入 **pending(待审核)**;owner 审核通过后才会进入 **approved(上榜)**。\n\n"
663
- f"- 当前 `LEADERBOARD_DATASET`: `{LEADERBOARD_DATASET or '(未配置)'}`\n"
664
- )
665
-
666
- with gr.Row():
667
- with gr.Column(scale=2):
668
- model_name = gr.Textbox(label="模型名称(必填)", placeholder="例如:my-agent-v1")
669
- model_api = gr.Textbox(
670
- label="模型 API(必填)",
671
- placeholder="例如:https://api.example.com/v1/chat/completions",
672
- )
673
- notes = gr.Textbox(label="备注(可选)", lines=4)
674
- username = gr.Textbox(
675
- label="用户名(可选)",
676
- placeholder="建议填你的 HF 用户名(也可留空)",
677
- value="" if IN_SPACES else "local",
678
- )
679
- submit_btn = gr.Button("提交", variant="primary")
680
  status = gr.Markdown()
681
- with gr.Column(scale=3):
682
- leaderboard = gr.Dataframe(
683
- label="Leaderboard(按提交时间倒序)",
684
- value=_load_entries_df(APPROVED_PREFIX, include_filename=False),
685
- interactive=False,
686
- wrap=True,
687
- )
688
- refresh_btn = gr.Button("刷新")
689
-
690
- submit_btn.click(
691
- submit,
692
- inputs=[model_name, model_api, notes, username],
693
- outputs=[status, leaderboard],
694
- )
695
- refresh_btn.click(refresh, inputs=[], outputs=[leaderboard])
696
 
697
- with gr.Accordion("Owner 审核(测试 demo)", open=False):
698
- gr.Markdown(
699
- "需要在 Space Secrets 中配置 `OWNER_REVIEW_TOKEN`。只有输入正确口令,才允许把 pending 放入榜单。"
 
700
  )
701
- review_token = gr.Textbox(label="审核口令", type="password", placeholder="OWNER_REVIEW_TOKEN")
702
- pending_refresh_btn = gr.Button("刷新待审核列表")
703
- pending_df = gr.Dataframe(
704
- label="待审核(pending)",
705
- value=_load_entries_df(PENDING_PREFIX, include_filename=True),
706
- interactive=False,
707
- wrap=True,
708
- )
709
- pending_pick = gr.Dropdown(
710
- label="选择要通过的提交(文件)",
711
- choices=_list_json_files(PENDING_PREFIX),
712
- )
713
- approve_btn = gr.Button("通过审核并上榜", variant="primary")
714
- approve_status = gr.Markdown()
715
-
716
- pending_refresh_btn.click(
717
- refresh_pending,
718
- inputs=[],
719
- outputs=[pending_df, pending_pick],
720
- )
721
- approve_btn.click(
722
- approve,
723
- inputs=[pending_pick, review_token],
724
- outputs=[approve_status, pending_df, pending_pick, leaderboard],
725
- )
726
-
727
- demo.launch()
728
 
 
 
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"))
 
50
  "Lifestyle",
51
  ]
52
 
53
+ # ✅ 展示顺序:Model 最左,Avg 第二列
54
+ DISPLAY_ORDER = [
55
+ "Model name",
56
+ "Avg",
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")
 
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
+ df = df.sort_values(by=["Submitted at"], ascending=False, kind="stable")
 
 
166
  return df
167
 
168
 
 
 
 
 
 
169
  def _parse_hf_created_at(created_at: str) -> datetime.datetime | None:
170
  try:
171
  if created_at.endswith("Z"):
 
176
 
177
 
178
  def _check_user_eligibility(username: str) -> tuple[bool, str]:
 
 
 
179
  try:
180
  r = requests.get(f"https://huggingface.co/api/users/{username}/overview", timeout=10)
181
  r.raise_for_status()
 
188
  now = datetime.datetime.now(datetime.timezone.utc)
189
  if dt.tzinfo is None:
190
  dt = dt.replace(tzinfo=datetime.timezone.utc)
191
+ if (now - dt).days < 120:
 
192
  return False, "Account must be older than 4 months to submit."
193
  return True, ""
194
  except Exception:
 
206
  return any(str(v).startswith(today) for v in user_rows["Submitted at"].tolist())
207
 
208
 
209
+ # ---------- HTML Leaderboard ----------
210
+ def _fmt_cell(v):
211
+ if v is None or (isinstance(v, float) and pd.isna(v)):
212
+ return ""
213
+ if isinstance(v, (int, float, np.number)):
214
+ return f"{float(v):.2f}"
215
+ return str(v)
216
+
217
+
218
+ def _apply_search_and_sort(df: pd.DataFrame, search_text: str, sort_col: str, sort_dir: str) -> pd.DataFrame:
219
+ s = (search_text or "").strip().lower()
220
+ if s:
221
+ df = df[df["Model name"].astype(str).str.lower().str.contains(s, na=False)]
222
+
223
+ sort_col = sort_col if sort_col in df.columns else "Avg"
224
+ asc = sort_dir == "asc"
225
+ df = df.sort_values(by=[sort_col], ascending=asc, kind="stable", na_position="last")
226
+ return df
227
+
228
+
229
+ def _render_leaderboard_html(df: pd.DataFrame, sort_col: str, sort_dir: str) -> str:
230
+ import html as _html
231
+
232
+ def th(label, col=None, align_left=False, cls=""):
233
+ if col:
234
+ arrow = ""
235
+ if col == sort_col:
236
+ arrow = " ▲" if sort_dir == "asc" else " ▼"
237
+ al = " left" if align_left else ""
238
+ return f'<th class="th clickable{al} {cls}" data-col="{_html.escape(col)}">{_html.escape(label)}{arrow}</th>'
239
+ al = " left" if align_left else ""
240
+ return f'<th class="th{al} {cls}">{_html.escape(label)}</th>'
241
+
242
+ trs = []
243
+ for _, r in df.iterrows():
244
+ tds = []
245
+ for c in DISPLAY_ORDER:
246
+ val = _fmt_cell(r.get(c, ""))
247
+ if c == "Model name":
248
+ tds.append(f'<td class="td model">{_html.escape(val)}</td>')
249
+ else:
250
+ tds.append(f'<td class="td num">{_html.escape(val)}</td>')
251
+ trs.append("<tr class='tr'>" + "".join(tds) + "</tr>")
252
+
253
+ # ✅ 三层表头:Model 最左,Avg 第二列
254
+ return f"""
255
+ <div class="table-wrap">
256
+ <div class="table-scroll">
257
+ <table class="table" id="lb_table">
258
+ <thead>
259
+ <tr class="r1">
260
+ {th("Model", "Model name", align_left=True, cls="model")}
261
+ {th("Avg. (%)", "Avg", cls="avg")}
262
+ <th class="th group" colspan="9">Pass Rate (%)</th>
263
+ </tr>
264
+ <tr class="r2">
265
+ <th class="th"></th>
266
+ <th class="th"></th>
267
+ <th class="th group" colspan="3">Difficulty</th>
268
+ <th class="th group" colspan="6">Domain</th>
269
+ </tr>
270
+ <tr class="r3">
271
+ <th class="th"></th>
272
+ <th class="th"></th>
273
+ {th("Easy", "Easy")}
274
+ {th("Mid", "Mid")}
275
+ {th("Hard", "Hard")}
276
+ {th("Games", "Games")}
277
+ {th("Science", "Science")}
278
+ {th("Tools", "Tools")}
279
+ {th("Humanities", "Humanities")}
280
+ {th("Viz.", "Viz")}
281
+ {th("Lifestyle", "Lifestyle")}
282
+ </tr>
283
+ </thead>
284
+ <tbody>
285
+ {''.join(trs)}
286
+ </tbody>
287
+ </table>
288
+ </div>
289
+ </div>
290
+ """
291
+
292
+
293
+ def render_lb(search_text: str, sort_col: str, sort_dir: str) -> str:
294
+ df = _load_entries_df()
295
+ df = _apply_search_and_sort(df, search_text, sort_col, sort_dir)
296
+ return _render_leaderboard_html(df, sort_col, sort_dir)
297
+
298
+
299
+ def toggle_sort(clicked_col: str, current_col: str, current_dir: str):
300
+ clicked_col = (clicked_col or "").strip()
301
+ if clicked_col not in SORTABLE_COLS:
302
+ return current_col, current_dir
303
+ if clicked_col == current_col:
304
+ return current_col, ("asc" if current_dir == "desc" else "desc")
305
+ return clicked_col, "desc"
306
+
307
+
308
+ # ---------- Submit ----------
309
  def submit(
 
310
  model_api: str,
311
  api_key: str,
 
 
 
 
 
 
 
 
 
 
312
  search_text: str,
313
+ sort_col: str,
314
+ sort_dir: str,
 
315
  profile: gr.OAuthProfile | None,
316
  ):
 
317
  if IN_SPACES and (profile is None or not getattr(profile, "username", None)):
318
+ return "You must log in to submit.", render_lb(search_text, sort_col, sort_dir)
319
 
320
  submitter = (getattr(profile, "username", None) if profile is not None else "local") or "anonymous"
321
 
 
322
  model_api = (model_api or "").strip()
323
  api_key = (api_key or "").strip()
324
 
 
 
325
  if not model_api:
326
+ return "Model API URL is required.", render_lb(search_text, sort_col, sort_dir)
327
  if not _is_valid_http_url(model_api):
328
+ return "Model API must be a valid http(s) URL.", render_lb(search_text, sort_col, sort_dir)
329
  if not api_key:
330
+ return "API key is required.", render_lb(search_text, sort_col, sort_dir)
331
 
332
  ok, msg = _ensure_dataset_readable()
333
  if not ok:
334
+ return msg, render_lb(search_text, sort_col, sort_dir)
335
 
336
  if IN_SPACES:
337
  ok, msg = _check_user_eligibility(submitter)
338
  if not ok:
339
+ return msg, render_lb(search_text, sort_col, sort_dir)
340
 
341
  if _submitted_today(submitter):
342
+ return "You have already submitted today. Please try again tomorrow.", render_lb(search_text, sort_col, sort_dir)
343
 
344
  now = datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
345
  nonce = uuid.uuid4().hex[:8]
 
346
  safe_user = _slug(submitter)
347
+
348
+ host = urlparse(model_api).netloc or "unknown"
349
+ model_name = host
350
+
351
+ safe_model = _slug(model_name)
352
  path_in_repo = f"{ENTRIES_PREFIX}{now[:10]}/{now}-{safe_user}-{safe_model}-{nonce}.json"
353
 
 
354
  payload = {
355
  "Model name": model_name,
356
+ "Avg": None,
357
+ "Easy": None,
358
+ "Mid": None,
359
+ "Hard": None,
360
+ "Games": None,
361
+ "Science": None,
362
+ "Tools": None,
363
+ "Humanities": None,
364
+ "Viz": None,
365
+ "Lifestyle": None,
366
  "Submitted at": now,
367
  "Submitter": submitter,
368
  "Model API": model_api,
 
379
  token=HF_TOKEN,
380
  )
381
 
382
+ return "Submitted successfully.", render_lb(search_text, sort_col, sort_dir)
383
+
384
+
385
+ # ✅ 强制覆盖成浅灰线:用 !important,避免被主题样式覆盖成黑线
386
+ CSS = r"""
387
+ .gradio-container { max-width: 100% !important; }
388
+ #page { padding: 16px; }
389
+
390
+ #topbar { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom: 10px; }
391
+ #titleline { font-weight: 700; font-size: 18px; }
392
+ #searchbox { width: 280px; }
393
+ #searchbox label { display:none !important; }
394
+ #searchbox textarea, #searchbox input {
395
+ height: 34px !important;
396
+ border-radius: 8px !important;
397
+ border: 1px solid #e5e7eb !important;
398
+ background: #fff !important;
399
+ box-shadow: none !important;
400
+ }
401
+ #searchbox textarea::placeholder, #searchbox input::placeholder { color: #9ca3af; }
402
+
403
+ /* table container */
404
+ .table-wrap{
405
+ width: 100%;
406
+ border: 1px solid #e5e7eb !important;
407
+ border-radius: 8px;
408
+ background: #fff;
409
+ }
410
+ .table-scroll{ width: 100%; overflow-x: auto; }
411
+ table.table{
412
+ width: 100%;
413
+ border-collapse: separate;
414
+ border-spacing: 0;
415
+ min-width: 1100px;
416
+ }
417
+
418
+ /* header */
419
+ th.th{
420
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial !important;
421
+ font-weight: 600;
422
+ font-size: 13px;
423
+ color: #111827;
424
+ padding: 10px 12px;
425
+ text-align: center;
426
+ background: #f9fafb;
427
+ border-bottom: 1px solid #e5e7eb !important;
428
+ border-right: 1px solid #e5e7eb !important;
429
+ white-space: nowrap;
430
+ }
431
+ thead tr.r3 th.th { background: #ffffff; }
432
+ th.th.left{ text-align:left; }
433
+ th.group{ color:#374151; font-weight:600; }
434
+ th.th:last-child{ border-right: none !important; }
435
+
436
+ /* body */
437
+ td.td{
438
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial !important;
439
+ font-size: 13px;
440
+ color: #111827;
441
+ padding: 10px 12px;
442
+ border-bottom: 1px solid #f0f1f3 !important;
443
+ border-right: 1px solid #f0f1f3 !important;
444
+ background: #fff;
445
+ }
446
+ td.td:last-child{ border-right: none !important; }
447
+ td.num{ text-align:right; }
448
+ td.model{ text-align:left; min-width: 280px; }
449
+ tr.tr:hover td.td{ background: #fafafa; }
450
+
451
+ /* clickable sort */
452
+ th.clickable{ cursor:pointer; user-select:none; }
453
+ th.clickable:hover{ background:#f3f4f6; }
454
+
455
+ /* submit */
456
+ #submit_card{
457
+ width: 100%;
458
+ border: 1px solid #e5e7eb !important;
459
+ border-radius: 8px;
460
+ padding: 12px;
461
+ background: #fff;
462
+ margin-top: 14px;
463
+ }
464
+ #submit_card .hint{
465
+ margin: 0 0 10px 0;
466
+ color: #6b7280;
467
+ font-size: 13px;
468
+ }
469
+ """
470
 
471
  with gr.Blocks(title=f"{APP_NAME} leaderboard") as demo:
472
+ with gr.Column(elem_id="page"):
473
+ with gr.Row(elem_id="topbar"):
474
+ gr.Markdown(f"<div id='titleline'>{APP_NAME} leaderboard</div>")
 
 
 
 
 
 
 
 
 
 
 
475
  with gr.Row():
476
+ search_text = gr.Textbox(
477
+ elem_id="searchbox",
478
+ placeholder="Search model…",
479
+ show_label=False,
480
+ container=False,
481
+ scale=1,
482
  )
483
+ refresh_btn = gr.Button("Refresh", scale=0)
484
+
485
+ sort_col = gr.State("Avg")
486
+ sort_dir = gr.State("desc")
487
+
488
+ lb_html = gr.HTML(value=render_lb("", "Avg", "desc"))
489
+
490
+ clicked_col = gr.Textbox(visible=False, elem_id="clicked_col")
491
+
492
+ gr.HTML(
493
+ """
494
+ <script>
495
+ (function(){
496
+ function bindClicks(){
497
+ const table = document.getElementById("lb_table");
498
+ const hidden = document.getElementById("clicked_col");
499
+ if(!table || !hidden) return;
500
+
501
+ table.querySelectorAll("th.clickable").forEach(th=>{
502
+ th.onclick = () => {
503
+ const col = th.getAttribute("data-col") || "";
504
+ hidden.value = col;
505
+ hidden.dispatchEvent(new Event("input", {bubbles:true}));
506
+ hidden.dispatchEvent(new Event("change", {bubbles:true}));
507
+ };
508
+ });
509
+ }
510
+
511
+ const obs = new MutationObserver(()=>bindClicks());
512
+ obs.observe(document.body, {subtree:true, childList:true});
513
+ setTimeout(bindClicks, 250);
514
+ })();
515
+ </script>
516
+ """
517
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
 
519
+ search_text.change(render_lb, inputs=[search_text, sort_col, sort_dir], outputs=[lb_html])
520
+ refresh_btn.click(render_lb, inputs=[search_text, sort_col, sort_dir], outputs=[lb_html])
 
521
 
522
+ def _on_click(col, cur_col, cur_dir, s):
523
+ new_col, new_dir = toggle_sort(col, cur_col, cur_dir)
524
+ return new_col, new_dir, render_lb(s, new_col, new_dir)
525
 
526
+ clicked_col.change(
527
+ _on_click,
528
+ inputs=[clicked_col, sort_col, sort_dir, search_text],
529
+ outputs=[sort_col, sort_dir, lb_html],
 
 
 
530
  )
 
 
 
 
 
 
 
 
 
 
 
531
 
532
+ gr.HTML(
533
+ """
534
+ <div id="submit_card">
535
+ <div class="hint">
536
+ <b>Submission</b> — Submit <b>Model API URL</b> and <b>API key</b> only.
537
+ Requires login (Spaces). One submission per user per day. Account must be older than 4 months.
538
+ API key will <b>not</b> be stored.
539
+ </div>
540
+ </div>
541
+ """
 
 
 
 
 
 
542
  )
 
 
 
543
 
544
+ with gr.Column():
545
+ with gr.Row():
546
+ model_api = gr.Textbox(label="Model API URL", placeholder="https://...", scale=3)
547
+ api_key = gr.Textbox(label="API key", type="password", placeholder="Will not be stored", scale=2)
548
+ with gr.Row():
549
+ gr.LoginButton()
550
+ submit_btn = gr.Button("Submit", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  status = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
 
553
+ submit_btn.click(
554
+ submit,
555
+ inputs=[model_api, api_key, search_text, sort_col, sort_dir],
556
+ outputs=[status, lb_html],
557
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
 
559
+ demo.launch(css=CSS, ssr_mode=False)