ZENLLC commited on
Commit
5aedc50
·
verified ·
1 Parent(s): 3efdee6

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +613 -0
app.py ADDED
@@ -0,0 +1,613 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import random
4
+ import sqlite3
5
+ from dataclasses import dataclass
6
+ from datetime import datetime
7
+ from typing import Dict, List, Tuple, Optional
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ import gradio as gr
12
+ from filelock import FileLock
13
+
14
+ # ---------------------------
15
+ # Config
16
+ # ---------------------------
17
+ ARENAS = [
18
+ "Text",
19
+ "WebDev",
20
+ "Vision",
21
+ "Text-to-Image",
22
+ "Image Edit",
23
+ "Search",
24
+ "Text-to-Video",
25
+ "Image-to-Video",
26
+ ]
27
+
28
+ DB_PATH = "arena.db"
29
+ DB_LOCK = "arena.db.lock"
30
+ SEED_PATH = os.path.join("data", "seed_snapshot.json")
31
+ CSS_PATH = os.path.join("assets", "zen.css")
32
+
33
+ DEFAULT_RATING = 1200.0
34
+ K_FACTOR = 16.0
35
+
36
+ # ---------------------------
37
+ # Helpers: provider tagging (heuristic)
38
+ # ---------------------------
39
+ def guess_provider(model: str) -> str:
40
+ m = model.lower()
41
+ if "gpt" in m or "chatgpt" in m or "o3" in m:
42
+ return "OpenAI"
43
+ if "gemini" in m or "veo" in m:
44
+ return "Google"
45
+ if "claude" in m:
46
+ return "Anthropic"
47
+ if "grok" in m:
48
+ return "xAI"
49
+ if "sonar" in m or "ppl" in m:
50
+ return "Perplexity"
51
+ if "flux" in m:
52
+ return "Black Forest Labs"
53
+ if "kling" in m:
54
+ return "Kuaishou"
55
+ if "sora" in m:
56
+ return "OpenAI"
57
+ if "wan" in m:
58
+ return "WAN"
59
+ if "hunyuan" in m:
60
+ return "Tencent"
61
+ if "seedream" in m or "seedance" in m:
62
+ return "ByteDance"
63
+ return "Other"
64
+
65
+ # ---------------------------
66
+ # SQLite persistence
67
+ # ---------------------------
68
+ def db():
69
+ conn = sqlite3.connect(DB_PATH, check_same_thread=False)
70
+ conn.execute(
71
+ """
72
+ CREATE TABLE IF NOT EXISTS ratings (
73
+ arena TEXT NOT NULL,
74
+ model TEXT NOT NULL,
75
+ provider TEXT NOT NULL,
76
+ rating REAL NOT NULL,
77
+ votes INTEGER NOT NULL,
78
+ updated_at TEXT NOT NULL,
79
+ PRIMARY KEY (arena, model)
80
+ )
81
+ """
82
+ )
83
+ conn.execute(
84
+ """
85
+ CREATE TABLE IF NOT EXISTS votes_log (
86
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
87
+ arena TEXT NOT NULL,
88
+ winner TEXT NOT NULL,
89
+ loser TEXT NOT NULL,
90
+ created_at TEXT NOT NULL
91
+ )
92
+ """
93
+ )
94
+ conn.commit()
95
+ return conn
96
+
97
+ def now_iso() -> str:
98
+ return datetime.utcnow().isoformat(timespec="seconds") + "Z"
99
+
100
+ def ensure_model(arena: str, model: str, provider: Optional[str] = None, default_rating: float = DEFAULT_RATING):
101
+ provider = provider or guess_provider(model)
102
+ with FileLock(DB_LOCK):
103
+ conn = db()
104
+ conn.execute(
105
+ "INSERT OR IGNORE INTO ratings (arena, model, provider, rating, votes, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
106
+ (arena, model, provider, float(default_rating), 0, now_iso()),
107
+ )
108
+ conn.commit()
109
+ conn.close()
110
+
111
+ def get_rating(arena: str, model: str) -> Tuple[float, int, str]:
112
+ with FileLock(DB_LOCK):
113
+ conn = db()
114
+ cur = conn.cursor()
115
+ cur.execute("SELECT rating, votes, provider FROM ratings WHERE arena=? AND model=?", (arena, model))
116
+ row = cur.fetchone()
117
+ conn.close()
118
+ if row is None:
119
+ return (DEFAULT_RATING, 0, guess_provider(model))
120
+ return (float(row[0]), int(row[1]), str(row[2]))
121
+
122
+ def elo_update(r_a: float, r_b: float, a_wins: bool, k: float = K_FACTOR) -> Tuple[float, float]:
123
+ ea = 1.0 / (1.0 + 10 ** ((r_b - r_a) / 400.0))
124
+ sa = 1.0 if a_wins else 0.0
125
+ new_a = r_a + k * (sa - ea)
126
+ new_b = r_b + k * ((1.0 - sa) - (1.0 - ea))
127
+ return new_a, new_b
128
+
129
+ def vote(arena: str, winner: str, loser: str):
130
+ ensure_model(arena, winner)
131
+ ensure_model(arena, loser)
132
+
133
+ r_w, v_w, p_w = get_rating(arena, winner)
134
+ r_l, v_l, p_l = get_rating(arena, loser)
135
+ new_w, new_l = elo_update(r_w, r_l, True)
136
+
137
+ with FileLock(DB_LOCK):
138
+ conn = db()
139
+ conn.execute(
140
+ "UPDATE ratings SET rating=?, votes=?, updated_at=?, provider=? WHERE arena=? AND model=?",
141
+ (float(new_w), int(v_w + 1), now_iso(), p_w, arena, winner),
142
+ )
143
+ conn.execute(
144
+ "UPDATE ratings SET rating=?, votes=?, updated_at=?, provider=? WHERE arena=? AND model=?",
145
+ (float(new_l), int(v_l + 1), now_iso(), p_l, arena, loser),
146
+ )
147
+ conn.execute(
148
+ "INSERT INTO votes_log (arena, winner, loser, created_at) VALUES (?, ?, ?, ?)",
149
+ (arena, winner, loser, now_iso()),
150
+ )
151
+ conn.commit()
152
+ conn.close()
153
+
154
+ def seed_from_json(force: bool = False) -> Dict[str, int]:
155
+ if not os.path.exists(SEED_PATH):
156
+ return {"seeded_rows": 0, "note": "seed file missing"}
157
+
158
+ with open(SEED_PATH, "r", encoding="utf-8") as f:
159
+ seed = json.load(f)
160
+
161
+ seeded = 0
162
+ with FileLock(DB_LOCK):
163
+ conn = db()
164
+ cur = conn.cursor()
165
+
166
+ if force:
167
+ cur.execute("DELETE FROM ratings")
168
+ conn.commit()
169
+
170
+ for arena, rows in seed.items():
171
+ if arena not in ARENAS:
172
+ continue
173
+ for item in rows:
174
+ model = item["model"]
175
+ score = float(item.get("score", DEFAULT_RATING))
176
+ votes_n = int(item.get("votes", 0))
177
+ provider = guess_provider(model)
178
+
179
+ cur.execute(
180
+ """
181
+ INSERT OR REPLACE INTO ratings (arena, model, provider, rating, votes, updated_at)
182
+ VALUES (?, ?, ?, ?, ?, ?)
183
+ """,
184
+ (arena, model, provider, score, votes_n, now_iso()),
185
+ )
186
+ seeded += 1
187
+
188
+ conn.commit()
189
+ conn.close()
190
+
191
+ return {"seeded_rows": seeded, "note": "ok"}
192
+
193
+ def ensure_seed_once():
194
+ with FileLock(DB_LOCK):
195
+ conn = db()
196
+ cur = conn.cursor()
197
+ cur.execute("SELECT COUNT(*) FROM ratings")
198
+ n = cur.fetchone()[0]
199
+ conn.close()
200
+ if n == 0:
201
+ seed_from_json(force=False)
202
+
203
+ # ---------------------------
204
+ # Leaderboards / Matrices
205
+ # ---------------------------
206
+ def leaderboard_df(arena: str, search: str = "", provider: str = "All", min_votes: int = 0, limit: int = 100) -> pd.DataFrame:
207
+ ensure_seed_once()
208
+ search = (search or "").strip()
209
+ provider = provider or "All"
210
+
211
+ where = ["arena = ?"]
212
+ params: List[object] = [arena]
213
+
214
+ if search:
215
+ where.append("LOWER(model) LIKE ?")
216
+ params.append(f"%{search.lower()}%")
217
+ if provider != "All":
218
+ where.append("provider = ?")
219
+ params.append(provider)
220
+ if min_votes > 0:
221
+ where.append("votes >= ?")
222
+ params.append(int(min_votes))
223
+
224
+ where_sql = " AND ".join(where)
225
+ q = f"""
226
+ SELECT model AS Model, provider AS Provider, rating AS Rating, votes AS Votes, updated_at AS Updated
227
+ FROM ratings
228
+ WHERE {where_sql}
229
+ ORDER BY rating DESC
230
+ LIMIT ?
231
+ """
232
+ params.append(int(limit))
233
+
234
+ with FileLock(DB_LOCK):
235
+ conn = db()
236
+ df = pd.read_sql_query(q, conn, params=params)
237
+ conn.close()
238
+
239
+ if df.empty:
240
+ return pd.DataFrame(columns=["Rank", "Model", "Provider", "Score", "Votes", "Updated"])
241
+
242
+ df["Score"] = df["Rating"].round().astype(int)
243
+ df.drop(columns=["Rating"], inplace=True)
244
+ df.insert(0, "Rank", np.arange(1, len(df) + 1))
245
+ return df[["Rank", "Model", "Provider", "Score", "Votes", "Updated"]]
246
+
247
+ def providers_list() -> List[str]:
248
+ ensure_seed_once()
249
+ with FileLock(DB_LOCK):
250
+ conn = db()
251
+ cur = conn.cursor()
252
+ cur.execute("SELECT DISTINCT provider FROM ratings ORDER BY provider ASC")
253
+ rows = [r[0] for r in cur.fetchall()]
254
+ conn.close()
255
+ return ["All"] + rows
256
+
257
+ def all_models() -> List[str]:
258
+ ensure_seed_once()
259
+ with FileLock(DB_LOCK):
260
+ conn = db()
261
+ cur = conn.cursor()
262
+ cur.execute("SELECT DISTINCT model FROM ratings ORDER BY model ASC")
263
+ rows = [r[0] for r in cur.fetchall()]
264
+ conn.close()
265
+ return rows
266
+
267
+ def arena_overview_matrix(search: str = "", provider: str = "All", min_votes: int = 0, limit_models: int = 200) -> pd.DataFrame:
268
+ """
269
+ Wide matrix: each model gets Rank/Score/Votes per arena (compact).
270
+ """
271
+ ensure_seed_once()
272
+ search = (search or "").strip().lower()
273
+
274
+ with FileLock(DB_LOCK):
275
+ conn = db()
276
+ base = pd.read_sql_query(
277
+ "SELECT arena, model, provider, rating, votes FROM ratings",
278
+ conn
279
+ )
280
+ conn.close()
281
+
282
+ if base.empty:
283
+ return pd.DataFrame()
284
+
285
+ if provider != "All":
286
+ base = base[base["provider"] == provider]
287
+ if min_votes > 0:
288
+ base = base[base["votes"] >= int(min_votes)]
289
+ if search:
290
+ base = base[base["model"].str.lower().str.contains(search, na=False)]
291
+
292
+ # rank within arena
293
+ base["rank"] = base.groupby("arena")["rating"].rank(ascending=False, method="min").astype(int)
294
+ base["score"] = base["rating"].round().astype(int)
295
+
296
+ # choose top N models by "best average rank across arenas they appear in"
297
+ pivot_rank = base.pivot_table(index=["model", "provider"], columns="arena", values="rank", aggfunc="min")
298
+ avg_rank = pivot_rank.mean(axis=1, skipna=True).sort_values()
299
+ chosen = avg_rank.head(limit_models).index
300
+
301
+ base = base.set_index(["model", "provider"])
302
+ base = base.loc[base.index.isin(chosen)].reset_index()
303
+
304
+ # build matrix columns
305
+ out = pd.DataFrame({"Model": [m for (m, p) in chosen], "Provider": [p for (m, p) in chosen]})
306
+ out = out.reset_index(drop=True)
307
+
308
+ for a in ARENAS:
309
+ sub = base[base["arena"] == a][["model", "provider", "rank", "score", "votes"]]
310
+ sub = sub.rename(columns={
311
+ "rank": f"{a} Rank",
312
+ "score": f"{a} Score",
313
+ "votes": f"{a} Votes",
314
+ })
315
+ out = out.merge(sub, how="left", left_on=["Model", "Provider"], right_on=["model", "provider"])
316
+ out.drop(columns=[c for c in ["model", "provider"] if c in out.columns], inplace=True)
317
+
318
+ # sort by best avg rank
319
+ out["_avg_rank"] = out[[f"{a} Rank" for a in ARENAS]].mean(axis=1, skipna=True)
320
+ out = out.sort_values("_avg_rank", ascending=True).drop(columns=["_avg_rank"])
321
+
322
+ # nicer types
323
+ for a in ARENAS:
324
+ for col in [f"{a} Rank", f"{a} Score", f"{a} Votes"]:
325
+ if col in out.columns:
326
+ out[col] = out[col].astype("Int64")
327
+ return out
328
+
329
+ def kpis() -> Dict[str, str]:
330
+ ensure_seed_once()
331
+ with FileLock(DB_LOCK):
332
+ conn = db()
333
+ cur = conn.cursor()
334
+ cur.execute("SELECT COUNT(DISTINCT model) FROM ratings")
335
+ models = cur.fetchone()[0]
336
+ cur.execute("SELECT COUNT(*) FROM ratings")
337
+ rows = cur.fetchone()[0]
338
+ cur.execute("SELECT COUNT(*) FROM votes_log")
339
+ votes = cur.fetchone()[0]
340
+ cur.execute("SELECT MAX(created_at) FROM votes_log")
341
+ last_vote = cur.fetchone()[0]
342
+ conn.close()
343
+
344
+ return {
345
+ "models": str(models),
346
+ "entries": str(rows),
347
+ "votes": str(votes),
348
+ "last_vote": last_vote or "—",
349
+ }
350
+
351
+ # ---------------------------
352
+ # Voting mechanics UI
353
+ # ---------------------------
354
+ def pick_pair(arena: str, provider: str = "All") -> Tuple[str, str]:
355
+ df = leaderboard_df(arena, provider=provider, min_votes=0, limit=50)
356
+ models = df["Model"].tolist()
357
+ if len(models) < 2:
358
+ models = [m for m in all_models()]
359
+ if len(models) < 2:
360
+ return ("model-a", "model-b")
361
+ a, b = random.sample(models, 2)
362
+ return a, b
363
+
364
+ def model_card_md(model: str, arena: Optional[str] = None) -> str:
365
+ provider = guess_provider(model)
366
+ lines = [f"### {model}", f"<span class='zen-badge'>{provider}</span>"]
367
+ if arena:
368
+ r, v, p = get_rating(arena, model)
369
+ lines += [
370
+ "",
371
+ f"**Arena:** {arena}",
372
+ f"**Score:** {int(round(r))}",
373
+ f"**Votes:** {v}",
374
+ ]
375
+ return "\n".join(lines)
376
+
377
+ def model_profile(model: str) -> Tuple[pd.DataFrame, str]:
378
+ ensure_seed_once()
379
+ rows = []
380
+ for a in ARENAS:
381
+ r, v, p = get_rating(a, model)
382
+ rows.append({"Arena": a, "Score": int(round(r)), "Votes": v, "Provider": p})
383
+ df = pd.DataFrame(rows).sort_values("Score", ascending=False)
384
+ best = df.iloc[0]
385
+ worst = df.iloc[-1]
386
+ summary = (
387
+ f"<div class='zen-card'>"
388
+ f"<div class='zen-title'>Model Profile</div>"
389
+ f"<div class='zen-sub'><b>{model}</b> · Provider: <b>{guess_provider(model)}</b></div>"
390
+ f"<div class='zen-hr'></div>"
391
+ f"<div class='zen-note'>"
392
+ f"Best arena: <b>{best['Arena']}</b> (Score {best['Score']}, Votes {best['Votes']}). "
393
+ f"Worst arena: <b>{worst['Arena']}</b> (Score {worst['Score']}, Votes {worst['Votes']})."
394
+ f"</div>"
395
+ f"</div>"
396
+ )
397
+ return df, summary
398
+
399
+ # ---------------------------
400
+ # Build UI
401
+ # ---------------------------
402
+ ensure_seed_once()
403
+ css = ""
404
+ if os.path.exists(CSS_PATH):
405
+ with open(CSS_PATH, "r", encoding="utf-8") as f:
406
+ css = f.read()
407
+
408
+ with gr.Blocks(css=css, title="ZEN Model Arena Leaderboard") as demo:
409
+
410
+ k = kpis()
411
+ header = f"""
412
+ <div class="zen-card">
413
+ <div class="zen-title">ZEN Model Arena Leaderboard</div>
414
+ <p class="zen-sub">
415
+ A multi-arena leaderboard (Text · WebDev · Vision · Image · Video · Search) with an Arena Overview matrix and live Elo voting.
416
+ </p>
417
+ <div class="zen-kpi">
418
+ <div><div class="k">Models</div><div class="v">{k['models']}</div><div class="s">unique IDs tracked</div></div>
419
+ <div><div class="k">Arena Entries</div><div class="v">{k['entries']}</div><div class="s">model × arena rows</div></div>
420
+ <div><div class="k">Votes Logged</div><div class="v">{k['votes']}</div><div class="s">pairwise results</div></div>
421
+ <div><div class="k">Last Vote</div><div class="v" style="font-size:12px; font-weight:700;">{k['last_vote']}</div><div class="s">UTC</div></div>
422
+ </div>
423
+ <div class="zen-hr"></div>
424
+ <span class="zen-badge">Gradio 5.49.1</span>
425
+ <span class="zen-badge">SQLite + FileLock</span>
426
+ <span class="zen-badge">Multi-Arena Matrix</span>
427
+ <span class="zen-badge">Search + Filters</span>
428
+ </div>
429
+ """
430
+ gr.HTML(header)
431
+
432
+ provider_choices = providers_list()
433
+
434
+ with gr.Tabs():
435
+
436
+ # ---------------------------
437
+ # Overview
438
+ # ---------------------------
439
+ with gr.Tab("Leaderboard Overview"):
440
+ gr.Markdown("### Top 10 per Arena (seed snapshot) + live refresh\nThis view mirrors your snapshot format, but it’s wired to the live DB.")
441
+
442
+ with gr.Row():
443
+ arena_sel_ov = gr.Dropdown(choices=ARENAS, value="Text", label="Arena")
444
+ provider_sel_ov = gr.Dropdown(choices=provider_choices, value="All", label="Provider filter")
445
+ min_votes_ov = gr.Slider(0, 100000, value=0, step=50, label="Min votes")
446
+ search_ov = gr.Textbox(value="", label="Search models", placeholder="e.g., gpt, gemini, claude, flux...")
447
+
448
+ df_ov = gr.Dataframe(interactive=False, wrap=True, height=520)
449
+ refresh_ov = gr.Button("Refresh overview", variant="primary")
450
+
451
+ def refresh_overview(arena, provider, min_votes, search):
452
+ return leaderboard_df(arena, search=search, provider=provider, min_votes=int(min_votes), limit=10)
453
+
454
+ refresh_ov.click(refresh_overview, inputs=[arena_sel_ov, provider_sel_ov, min_votes_ov, search_ov], outputs=[df_ov])
455
+ demo.load(refresh_overview, inputs=[arena_sel_ov, provider_sel_ov, min_votes_ov, search_ov], outputs=[df_ov])
456
+
457
+ # ---------------------------
458
+ # Arena Overview Matrix (the cool part)
459
+ # ---------------------------
460
+ with gr.Tab("Arena Overview Matrix"):
461
+ gr.Markdown("### Cross-arena placements (rank/score/votes per arena)\nThis is the wide “overview table” that makes the leaderboard feel *real*.")
462
+
463
+ with gr.Row():
464
+ provider_sel_mx = gr.Dropdown(choices=provider_choices, value="All", label="Provider")
465
+ min_votes_mx = gr.Slider(0, 100000, value=0, step=50, label="Min votes")
466
+ search_mx = gr.Textbox(value="", label="Search", placeholder="Filter which models appear…")
467
+ limit_mx = gr.Slider(10, 400, value=200, step=10, label="Max models in matrix")
468
+
469
+ mx = gr.Dataframe(interactive=False, wrap=True, height=600)
470
+ refresh_mx = gr.Button("Build / Refresh Matrix", variant="primary")
471
+
472
+ def build_matrix(provider, min_votes, search, limit_models):
473
+ return arena_overview_matrix(search=search, provider=provider, min_votes=int(min_votes), limit_models=int(limit_models))
474
+
475
+ refresh_mx.click(build_matrix, inputs=[provider_sel_mx, min_votes_mx, search_mx, limit_mx], outputs=[mx])
476
+ demo.load(build_matrix, inputs=[provider_sel_mx, min_votes_mx, search_mx, limit_mx], outputs=[mx])
477
+
478
+ # ---------------------------
479
+ # Per-arena deep view
480
+ # ---------------------------
481
+ for arena in ARENAS:
482
+ with gr.Tab(arena):
483
+ gr.Markdown(f"### {arena} Leaderboard (live DB)\nFilter, search, and export-ready table.")
484
+
485
+ with gr.Row():
486
+ provider_sel = gr.Dropdown(choices=provider_choices, value="All", label="Provider")
487
+ min_votes = gr.Slider(0, 100000, value=0, step=50, label="Min votes")
488
+ search = gr.Textbox(value="", label="Search")
489
+
490
+ df = gr.Dataframe(interactive=False, wrap=True, height=560)
491
+ btn = gr.Button("Refresh", variant="primary")
492
+
493
+ btn.click(lambda p, mv, s, a=arena: leaderboard_df(a, search=s, provider=p, min_votes=int(mv), limit=150),
494
+ inputs=[provider_sel, min_votes, search], outputs=[df])
495
+ demo.load(lambda p="All", mv=0, s="", a=arena: leaderboard_df(a, search=s, provider=p, min_votes=int(mv), limit=150),
496
+ outputs=[df])
497
+
498
+ # ---------------------------
499
+ # Voting (pairwise)
500
+ # ---------------------------
501
+ with gr.Tab("Start Voting"):
502
+ gr.Markdown(
503
+ "### Pairwise Voting (Elo)\n"
504
+ "Pick which model wins for a chosen arena. Scores update instantly and feed the Matrix."
505
+ )
506
+
507
+ with gr.Row():
508
+ arena_vote = gr.Dropdown(choices=ARENAS, value="Text", label="Arena")
509
+ provider_vote = gr.Dropdown(choices=provider_choices, value="All", label="Provider pool")
510
+ new_match = gr.Button("New Matchup", variant="primary")
511
+
512
+ left_state = gr.State("")
513
+ right_state = gr.State("")
514
+
515
+ with gr.Row():
516
+ left_md = gr.Markdown()
517
+ right_md = gr.Markdown()
518
+
519
+ with gr.Row():
520
+ left_btn = gr.Button("⬅ Left Wins", variant="primary")
521
+ right_btn = gr.Button("Right Wins ➡", variant="primary")
522
+
523
+ vote_status = gr.HTML()
524
+
525
+ def new_matchup(arena, provider):
526
+ a, b = pick_pair(arena, provider=provider)
527
+ return model_card_md(a, arena), model_card_md(b, arena), a, b, "<div class='zen-note'>New matchup ready.</div>"
528
+
529
+ new_match.click(new_matchup, inputs=[arena_vote, provider_vote], outputs=[left_md, right_md, left_state, right_state, vote_status])
530
+ demo.load(new_matchup, inputs=[arena_vote, provider_vote], outputs=[left_md, right_md, left_state, right_state, vote_status])
531
+
532
+ def left_wins(arena, left, right, provider):
533
+ if not left or not right:
534
+ return new_matchup(arena, provider)
535
+ vote(arena, winner=left, loser=right)
536
+ return new_matchup(arena, provider)
537
+
538
+ def right_wins(arena, left, right, provider):
539
+ if not left or not right:
540
+ return new_matchup(arena, provider)
541
+ vote(arena, winner=right, loser=left)
542
+ return new_matchup(arena, provider)
543
+
544
+ left_btn.click(left_wins, inputs=[arena_vote, left_state, right_state, provider_vote],
545
+ outputs=[left_md, right_md, left_state, right_state, vote_status])
546
+ right_btn.click(right_wins, inputs=[arena_vote, left_state, right_state, provider_vote],
547
+ outputs=[left_md, right_md, left_state, right_state, vote_status])
548
+
549
+ # ---------------------------
550
+ # Model Profiles
551
+ # ---------------------------
552
+ with gr.Tab("Model Profiles"):
553
+ gr.Markdown("### Inspect a model across arenas\nThis is how you turn “a table” into a *product*.")
554
+
555
+ model_dd = gr.Dropdown(choices=all_models(), value=all_models()[0] if all_models() else None, label="Model")
556
+ prof_summary = gr.HTML()
557
+ prof_df = gr.Dataframe(interactive=False, wrap=True, height=360)
558
+
559
+ load_btn = gr.Button("Load Profile", variant="primary")
560
+ load_btn.click(model_profile, inputs=[model_dd], outputs=[prof_df, prof_summary])
561
+ demo.load(model_profile, inputs=[model_dd], outputs=[prof_df, prof_summary])
562
+
563
+ # ---------------------------
564
+ # Admin / Ops
565
+ # ---------------------------
566
+ with gr.Tab("Admin"):
567
+ gr.Markdown("### Admin Tools\nSeed DB, force reseed, add models, sanity checks.")
568
+
569
+ with gr.Row():
570
+ reseed_force = gr.Checkbox(value=False, label="Force reseed (wipes DB first)")
571
+ reseed_btn = gr.Button("Seed from data/seed_snapshot.json", variant="primary")
572
+
573
+ reseed_out = gr.JSON()
574
+
575
+ reseed_btn.click(lambda force: seed_from_json(force=bool(force)),
576
+ inputs=[reseed_force], outputs=[reseed_out])
577
+
578
+ gr.Markdown("#### Add a model to one or more arenas")
579
+ new_model = gr.Textbox(label="Model ID", placeholder="e.g., gpt-5.2, gemini-3-pro, claude-opus-4-5-…")
580
+ new_provider = gr.Textbox(label="Provider (optional)", placeholder="Leave blank for auto-detect")
581
+ arenas_add = gr.CheckboxGroup(choices=ARENAS, value=["Text"], label="Arenas")
582
+ add_btn = gr.Button("Add Model", variant="primary")
583
+ add_out = gr.HTML()
584
+
585
+ def add_model(model_id, provider, arenas):
586
+ model_id = (model_id or "").strip()
587
+ if not model_id:
588
+ return "<div class='zen-note'>Missing model id.</div>"
589
+ provider = (provider or "").strip() or guess_provider(model_id)
590
+ added = 0
591
+ for a in arenas or []:
592
+ ensure_model(a, model_id, provider=provider)
593
+ added += 1
594
+ return f"<div class='zen-note'>Added <b>{model_id}</b> ({provider}) to <b>{added}</b> arenas.</div>"
595
+
596
+ add_btn.click(add_model, inputs=[new_model, new_provider, arenas_add], outputs=[add_out])
597
+
598
+ gr.Markdown("#### Environment sanity")
599
+ sanity = gr.JSON()
600
+ def sanity_check():
601
+ return {
602
+ "time_utc": now_iso(),
603
+ "db_path_exists": os.path.exists(DB_PATH),
604
+ "seed_path_exists": os.path.exists(SEED_PATH),
605
+ "css_path_exists": os.path.exists(CSS_PATH),
606
+ "arenas": ARENAS,
607
+ "providers_detected": providers_list(),
608
+ "models_count": len(all_models()),
609
+ }
610
+ demo.load(sanity_check, outputs=[sanity])
611
+
612
+ gr.close_all()
613
+ demo.launch()