← back to Model Arena
yolo idea C: model profile page — clickable leaderboard rows open a model's artifact gallery across all battles + aggregate stats
8264d56299c9f02ad1205040b872e81b1e70799b · 2026-07-23 00:26:20 -0700 · Steve
Files touched
M public/index.htmlM server.jsM yolo/IDEAS.md
Diff
commit 8264d56299c9f02ad1205040b872e81b1e70799b
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Jul 23 00:26:20 2026 -0700
yolo idea C: model profile page — clickable leaderboard rows open a model's artifact gallery across all battles + aggregate stats
---
public/index.html | 40 +++++++++++++++++++++++++++++++++++++++-
server.js | 16 ++++++++++++++++
yolo/IDEAS.md | 1 +
3 files changed, 56 insertions(+), 1 deletion(-)
diff --git a/public/index.html b/public/index.html
index 5c8521e..36e280a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -113,6 +113,14 @@ tr.first td{color:var(--gold)}
.srcview{height:460px;overflow:auto;margin:0;padding:12px;background:#05060d;color:#8fb8ff;font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-word}
.pane.winner .srcview{height:560px}
.view{display:none}.view.on{display:block}
+tr.clk{cursor:pointer}tr.clk:hover td{background:rgba(0,229,255,.06)}
+.pgal{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px}
+.gcard{border:1px solid var(--line);background:var(--panel);cursor:pointer}
+.gcard:hover{border-color:var(--neon)}.gcard.won{border-color:var(--gold)}
+.gcard img{width:100%;height:130px;object-fit:cover;object-position:top center;display:block;background:#000}
+.gcard .noimg{height:130px;display:flex;align-items:center;justify-content:center;color:var(--dim);font-size:11px;background:var(--panel2)}
+.gcard .gm{padding:7px 9px;font-size:11px}.gcard .gm .gt{color:#fff;display:block;margin-bottom:3px}
+.gcard .gm .gs{color:var(--dim)}
</style>
</head>
<body>
@@ -166,6 +174,13 @@ tr.first td{color:var(--gold)}
<div class="arena" id="d-arena"></div>
</section>
+ <section id="profile" class="view">
+ <span class="back" id="p-back">← back to leaderboard</span>
+ <h2 id="p-title"></h2>
+ <div class="note" id="p-stats"></div>
+ <div id="p-gallery" class="pgal"></div>
+ </section>
+
<section id="board" class="view">
<h2>Real-World Leaderboard</h2>
<div class="controls"><label>Category <select id="b-cat"><option value="">All</option></select></label>
@@ -410,7 +425,8 @@ async function loadBoard(){
const tb = $('#b-rows'); tb.innerHTML='';
d.models.forEach((s,i)=>{
const tr = document.createElement('tr');
- if (i===0 && s.wins>0) tr.className='first';
+ tr.className='clk'+(i===0 && s.wins>0?' first':'');
+ tr.onclick=()=>openProfile(s.id);
tr.innerHTML = `<td>${i+1}</td><td>${s.label}</td><td>${s.kind}</td><td><b>${s.elo}</b></td><td>${s.battles}</td><td>${s.wins}</td>
<td>${s.winRate==null?'—':s.winRate+'%'}</td><td>${s.avgAiScore==null?'—':s.avgAiScore}</td>
<td>${s.costPerWin==null?'—':(s.costPerWin?'$'+s.costPerWin.toFixed(3):'$0')}</td>
@@ -419,6 +435,28 @@ async function loadBoard(){
});
}
+// ---- model profile ----
+$('#p-back').onclick = ()=>{ show('board'); loadBoard(); };
+async function openProfile(mid){
+ show('profile');
+ const d = await (await fetch(API+'/api/models/'+mid+'/profile')).json();
+ const s = d.stats||{};
+ $('#p-title').textContent = d.model.label + ' — model profile';
+ $('#p-stats').textContent = `${d.model.kind} · ELO ${s.elo??'—'} · ${s.wins??0} wins / ${s.battles??0} judged (${s.winRate==null?'—':s.winRate+'%'}) · avg 🤖 ${s.avgAiScore??'—'} · ${s.errors??0} errors · avg ${s.avgSeconds==null?'—':s.avgSeconds+'s'} · spend $${(s.spend||0).toFixed(2)}`;
+ const g = $('#p-gallery'); g.innerHTML='';
+ if (!d.gallery.length){ g.innerHTML='<div class="empty">No artifacts yet.</div>'; return; }
+ for (const it of d.gallery){
+ const card=document.createElement('div'); card.className='gcard'+(it.won?' won':'');
+ const badge=[it.won?'👑':'',it.aiPick?'🤖':'',it.aiScore!=null?'🤖'+it.aiScore:''].filter(Boolean).join(' ');
+ const img = it.thumb ? `<img src="${API}/thumb/${it.id}/${mid}" alt="" loading="lazy">`
+ : `<div class="noimg">${it.status==='error'?'✗ '+esc((it.error||'failed').slice(0,40)):esc(it.status)}</div>`;
+ card.innerHTML = `${img}<div class="gm"><span class="gt">${esc(it.title)}</span>
+ <span class="gs">${it.category} · ${it.status==='done'?(it.seconds||0)+'s'+(it.cost?' · $'+it.cost.toFixed(3):''):it.status} ${badge}</span></div>`;
+ card.onclick=()=>openDetail(it.id);
+ g.appendChild(card);
+ }
+}
+
loadModels().then(loadList);
</script>
</body>
diff --git a/server.js b/server.js
index 14fd79a..e6a76e0 100644
--- a/server.js
+++ b/server.js
@@ -491,6 +491,22 @@ const server = http.createServer(async (req, res) => {
return send(res, 200, c);
}
+ if ((m = p.match(/^\/api\/models\/([\w-]+)\/profile$/))) {
+ const mid = m[1]; const mo = MODELS.find(x => x.id === mid);
+ if (!mo) return send(res, 404, { error: 'unknown model' });
+ const full = buildLedger(null).models.find(s => s.id === mid) || {};
+ const gallery = [];
+ for (const c of challenges) {
+ const r = c.runs.find(x => x.model === mid);
+ if (!r) continue;
+ gallery.push({ id: c.id, title: c.title, category: c.category || 'Custom', created_at: c.created_at,
+ status: r.status, seconds: r.seconds, cost: r.cost || 0, thumb: !!r.thumb,
+ aiScore: typeof r.aiScore === 'number' ? r.aiScore : null, won: c.winner === mid, aiPick: c.aiPick === mid, error: r.error || null });
+ }
+ gallery.reverse();
+ return send(res, 200, { model: { id: mid, label: mo.label, kind: mo.kind }, stats: full, gallery });
+ }
+
if (p === '/api/ledger') {
const cat = u.searchParams.get('category') || '';
const l = buildLedger(cat || null);
diff --git a/yolo/IDEAS.md b/yolo/IDEAS.md
index 2373b49..d95247f 100644
--- a/yolo/IDEAS.md
+++ b/yolo/IDEAS.md
@@ -34,3 +34,4 @@ Answering the /contrarian critique ("one builder vote = theater", "toy demos not
- [DONE] #9 Multi-vision CONSENSUS referee — a PANEL of local vision models (qwen2.5vl:7b + minicpm-v) scores each artifact; consensus = mean, with per-panelist breakdown (hover) + a ⚡ disagreement flag on high spread. On the Smoke Test the 2-model consensus pick (kimi) AGREES with the human crown where the single model didn't — more robust. VISION_MODELS env-configurable. $0 local.
- [DONE] #A Per-category leaderboards — challenges auto-tagged Games/Real Work/Custom (inferCategory + backfill of existing); ledger accepts ?category, leaderboard has a category filter. Answers 'which model is best at REAL WORK vs games'. Games: Grok #1, 100% AI/human agreement.
- [DONE] #B Export/share — GET /export/:id returns a self-contained standalone HTML of a battle (all artifacts inlined via sandboxed iframe srcdoc, scoreboard, winner/AI-pick), downloadable; verified it renders offline from file://. '⬇ export' button on each battle.
+- [DONE] #C Model profile page — click any leaderboard row → that model's full body of work (every artifact thumbnail across battles, won/AI-pick badges, per-battle stats) + aggregate stats. /api/models/:id/profile.
← 2a631a5 yolo idea B: export a battle as a self-contained standalone
·
back to Model Arena
·
yolo idea D: head-to-head record matrix — pairwise W-L (row 6fea94d →