← back to Model Arena
yolo idea 4: blind judging mode — anonymize models to Model A/B/C during voting (Chatbot-Arena style), reveal on crown; hides AI pick + names/alts/titles
7ec7ada7564ace62f7b568037f994d7888fec771 · 2026-07-22 23:37:39 -0700 · Steve
Files touched
A data/artifacts/aed52f240396/gemma3-12b.htmlA data/artifacts/aed52f240396/gemma3-12b.pngA data/artifacts/aed52f240396/hermes3-8b.htmlA data/artifacts/aed52f240396/hermes3-8b.pngM data/challenges.jsonM public/index.htmlM yolo/IDEAS.md
Diff
commit 7ec7ada7564ace62f7b568037f994d7888fec771
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 22 23:37:39 2026 -0700
yolo idea 4: blind judging mode — anonymize models to Model A/B/C during voting (Chatbot-Arena style), reveal on crown; hides AI pick + names/alts/titles
---
data/artifacts/aed52f240396/gemma3-12b.html | 72 ++++++++++++++++++++++++++++
data/artifacts/aed52f240396/gemma3-12b.png | Bin 0 -> 3661 bytes
data/artifacts/aed52f240396/hermes3-8b.html | 70 +++++++++++++++++++++++++++
data/artifacts/aed52f240396/hermes3-8b.png | Bin 0 -> 3114 bytes
data/challenges.json | 38 +++++++++++++++
public/index.html | 30 ++++++++----
yolo/IDEAS.md | 1 +
7 files changed, 202 insertions(+), 9 deletions(-)
diff --git a/data/artifacts/aed52f240396/gemma3-12b.html b/data/artifacts/aed52f240396/gemma3-12b.html
new file mode 100644
index 0000000..d9bfe31
--- /dev/null
+++ b/data/artifacts/aed52f240396/gemma3-12b.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Bouncing Ball</title>
+<style>
+body { margin: 0; overflow: hidden; }
+canvas { background-color: #eee; display: block; }
+</style>
+</head>
+<body>
+
+<canvas id="myCanvas"></canvas>
+
+<script>
+const canvas = document.getElementById("myCanvas");
+const ctx = canvas.getContext("2d");
+
+canvas.width = window.innerWidth;
+canvas.height = window.innerHeight;
+
+let x = canvas.width / 2;
+let y = canvas.height / 2;
+let radius = 20;
+let dx = 2;
+let dy = 2;
+const gravity = 0.1;
+const friction = 0.95;
+
+function drawBall() {
+ ctx.beginPath();
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
+ ctx.fillStyle = "blue";
+ ctx.fill();
+ ctx.closePath();
+}
+
+function update() {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ drawBall();
+
+ x += dx;
+ y += dy;
+
+ if (x + radius > canvas.width || x - radius < 0) {
+ dx = -dx * friction;
+ }
+
+ if (y + radius > canvas.height || y - radius < 0) {
+ dy = -dy * friction;
+ // Add gravity on the ground, but reduce its effect slightly
+ if(y + radius > canvas.height){
+ dy *= 0.8;
+ }
+ }
+
+ dy += gravity; // Apply Gravity
+
+ requestAnimationFrame(update);
+}
+
+update();
+
+window.addEventListener('resize', function() {
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+ x = canvas.width / 2;
+ y = canvas.height / 2;
+});
+</script>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/aed52f240396/gemma3-12b.png b/data/artifacts/aed52f240396/gemma3-12b.png
new file mode 100644
index 0000000..e0a6cd8
Binary files /dev/null and b/data/artifacts/aed52f240396/gemma3-12b.png differ
diff --git a/data/artifacts/aed52f240396/hermes3-8b.html b/data/artifacts/aed52f240396/hermes3-8b.html
new file mode 100644
index 0000000..4babc1d
--- /dev/null
+++ b/data/artifacts/aed52f240396/hermes3-8b.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Ball Bouncing in a Box</title>
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ background-color: #f0f0f0;
+ }
+ canvas {
+ border: 1px solid #000;
+ }
+ </style>
+</head>
+<body>
+ <canvas id="box" width="400" height="300"></canvas>
+
+ <script>
+ let box, ball, gravity, speed;
+
+ function init() {
+ box = document.getElementById('box');
+ ball = { x: 0, y: 0, radius: 10, speedX: 2, speedY: 3 };
+ gravity = 0.5;
+ speed = 1;
+
+ draw();
+ move();
+ }
+
+ function draw() {
+ let ctx = box.getContext('2d');
+
+ // Clear canvas
+ ctx.clearRect(0, 0, box.width, box.height);
+
+ // Draw ball
+ ctx.beginPath();
+ ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
+ ctx.fillStyle = '#000';
+ ctx.fill();
+ }
+
+ function move() {
+ ball.speedY += gravity;
+ ball.y += ball.speedY;
+ ball.x += ball.speedX;
+
+ // Check if ball collides with top or bottom
+ if (ball.y + ball.radius > box.height || ball.y - ball.radius < 0) {
+ ball.speedY = -ball.speedY;
+ }
+
+ // Check if ball collides with left or right
+ if (ball.x + ball.radius > box.width || ball.x - ball.radius < 0) {
+ ball.speedX = -ball.speedX;
+ }
+
+ draw();
+
+ setTimeout(move, speed);
+ }
+
+ init();
+ </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/aed52f240396/hermes3-8b.png b/data/artifacts/aed52f240396/hermes3-8b.png
new file mode 100644
index 0000000..93c0626
Binary files /dev/null and b/data/artifacts/aed52f240396/hermes3-8b.png differ
diff --git a/data/challenges.json b/data/challenges.json
index 4aec40d..0ca2915 100644
--- a/data/challenges.json
+++ b/data/challenges.json
@@ -735,5 +735,43 @@
"thumb": true
}
]
+ },
+ {
+ "id": "aed52f240396",
+ "title": "Blind Test: Bouncing Ball",
+ "prompt": "Single HTML file: a ball bouncing inside a box with gravity, canvas, ~60 lines.",
+ "created_at": "2026-07-23T06:36:47.337Z",
+ "winner": null,
+ "runs": [
+ {
+ "model": "gemma3-12b",
+ "status": "done",
+ "error": null,
+ "seconds": 14,
+ "cost": 0,
+ "started_at": "2026-07-23T06:36:47.339Z",
+ "finished_at": "2026-07-23T06:37:01.039Z",
+ "bytes": 1354,
+ "thumb": true,
+ "aiScore": 5,
+ "aiReason": "The ball is visible and bouncing, but the canvas lacks visual appeal or additional elements."
+ },
+ {
+ "model": "hermes3-8b",
+ "status": "done",
+ "error": null,
+ "seconds": 7,
+ "cost": 0,
+ "started_at": "2026-07-23T06:37:01.039Z",
+ "finished_at": "2026-07-23T06:37:08.082Z",
+ "bytes": 1802,
+ "thumb": true,
+ "aiScore": 3,
+ "aiReason": "The image is blank and does not display the expected ball bouncing inside a box."
+ }
+ ],
+ "judging": false,
+ "aiPick": "gemma3-12b",
+ "judged_at": "2026-07-23T06:37:16.754Z"
}
]
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 174a96a..5a47b53 100644
--- a/public/index.html
+++ b/public/index.html
@@ -106,6 +106,9 @@ tr.first td{color:var(--gold)}
.pane .aipick{border-color:#a06bff;color:#c8a6ff}
.pane .bar .robo{color:#c8a6ff;font-size:11px}
.aireason{padding:6px 12px;border-top:1px solid var(--line);color:var(--dim);font-size:11px;font-style:italic}
+.blindtog{display:block;color:var(--dim);font-size:12px;margin:4px 0 10px;cursor:pointer;user-select:none}
+.blindtog input{accent-color:#a06bff;vertical-align:middle}
+.pane.blind .name{color:#a06bff;letter-spacing:2px}
.view{display:none}.view.on{display:block}
</style>
</head>
@@ -152,6 +155,7 @@ tr.first td{color:var(--gold)}
<section id="detail" class="view">
<span class="back" id="back">← back to challenges</span>
+ <label class="blindtog"><input type="checkbox" id="blind"> 🕶 Blind judging — hide model names until a winner is crowned (kills brand bias)</label>
<h2 id="d-title"></h2>
<div class="meta" id="d-meta"></div>
<div id="d-ai" class="aibar"></div>
@@ -182,6 +186,9 @@ const PRESETS = [
function fmtWhen(iso){ return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }
function mLabel(id){ const m = models.find(x=>x.id===id); return m ? m.label : id; }
+let blind = localStorage.getItem('ma-blind')==='1';
+// during blind judging (blind ON and no winner yet) show "Model A/B/C…" instead of names
+function dLabel(c, idx, id){ return (blind && !c.winner) ? ('Model '+String.fromCharCode(65+idx)) : mLabel(id); }
// ---- views ----
function show(view){
@@ -195,6 +202,8 @@ $('#nav-challenges').onclick = ()=>{ show('challenges'); loadList(); };
$('#nav-new').onclick = ()=>show('new');
$('#nav-board').onclick = ()=>{ show('board'); loadBoard(); };
$('#back').onclick = ()=>{ show('challenges'); loadList(); };
+$('#blind').checked = blind;
+$('#blind').onchange = ()=>{ blind = $('#blind').checked; localStorage.setItem('ma-blind', blind?'1':'0'); if (current) renderDetail(current.id); };
// ---- controls persistence (house rule) ----
$('#sort').value = localStorage.getItem('ma-sort') || 'newest';
@@ -291,7 +300,9 @@ async function renderDetail(id, statusOnly){
const anyArt = c.runs.some(r=>r.status==='done'&&r.thumb);
const ai = $('#d-ai');
if (c.judging) ai.innerHTML = `🤖 <b>AI referee</b> is looking at each artifact… (local vision model, $0)`;
- else if (c.aiPick) ai.innerHTML = `🤖 <b>AI referee pick:</b> <span class="aiscore">${mLabel(c.aiPick)}</span> · a local vision model scored each rendered result. Your 👑 crown is the human vote — agree or overrule. <button class="btn" id="rejudge">re-judge</button>`;
+ else if (c.aiPick) ai.innerHTML = (blind&&!c.winner)
+ ? `🤖 <b>AI referee</b> has scored every artifact — pick hidden until you crown a winner (blind mode). <button class="btn" id="rejudge">re-judge</button>`
+ : `🤖 <b>AI referee pick:</b> <span class="aiscore">${mLabel(c.aiPick)}</span> · a local vision model scored each rendered result. Your 👑 crown is the human vote — agree or overrule. <button class="btn" id="rejudge">re-judge</button>`;
else if (anyArt) ai.innerHTML = `🤖 <b>AI referee</b> — have a local vision model score every artifact 0–10 ($0). <button class="btn" id="judge">Run AI referee</button>`;
else ai.innerHTML = '';
const jb = $('#judge')||$('#rejudge');
@@ -299,11 +310,12 @@ async function renderDetail(id, statusOnly){
const arena = $('#d-arena');
const sig = c.runs.map(r=>r.model+':'+r.status+':'+(r.aiScore??'')+':'+(r.thumb?'t':'')).join('|') + '|' + c.winner + '|' + c.aiPick + '|' + c.judging;
if (statusOnly && arena.dataset.sig === sig) return c;
- arena.dataset.sig = sig;
+ arena.dataset.sig = sig + '|blind' + (blind?1:0);
arena.innerHTML = '';
- for (const run of c.runs){
+ const anon = blind && !c.winner;
+ c.runs.forEach((run, idx)=>{
const pane = document.createElement('div');
- pane.className = 'pane' + (c.winner===run.model?' winner':'');
+ pane.className = 'pane' + (c.winner===run.model?' winner':'') + (anon?' blind':'');
const stats = [];
if (run.seconds!=null) stats.push(run.seconds+'s');
if (run.bytes) stats.push(Math.round(run.bytes/1024)+' KB');
@@ -315,25 +327,25 @@ async function renderDetail(id, statusOnly){
// eagerly booting N live iframes at once, and gives the grid real imagery
bodyHtml = run.thumb
? `<div class="poster" data-run="${c.id}/${run.model}" title="Click to run this artifact live">
- <img src="${API}/thumb/${c.id}/${run.model}" alt="${mLabel(run.model)} artifact" loading="lazy">
+ <img src="${API}/thumb/${c.id}/${run.model}" alt="${dLabel(c,idx,run.model)} artifact" loading="lazy">
<span class="play">▶ run live</span></div>`
- : `<iframe sandbox="allow-scripts" src="${API}/artifact/${c.id}/${run.model}" title="${mLabel(run.model)}"></iframe>`;
+ : `<iframe sandbox="allow-scripts" src="${API}/artifact/${c.id}/${run.model}" title="${dLabel(c,idx,run.model)}"></iframe>`;
else if (run.status==='error')
bodyHtml = `<div class="msg">✗ ${esc(run.error||'failed')}</div>`;
else
bodyHtml = `<div class="msg">⏳ ${run.status}… ${run.status==='running'?'generating':'waiting for a free host'}</div>`;
pane.innerHTML = `<div class="bar">
- <span class="name">${mLabel(run.model)}</span>
+ <span class="name">${dLabel(c,idx,run.model)}</span>
<span class="stat">${run.status==='done'?stats.join(' · '):run.status}</span>
<span class="right">
${run.status==='done'&&c.winner!==run.model?`<button class="btn" data-win="${run.model}" title="Crown this model the winner of this battle — records a vote into the real-world win-rate ledger. One winner per battle; you can re-crown.">👑 Crown</button>`:''}
- ${c.aiPick===run.model?'<span class="chip aipick">🤖 AI PICK</span>':''}
+ ${!anon&&c.aiPick===run.model?'<span class="chip aipick">🤖 AI PICK</span>':''}
${c.winner===run.model?'<span class="chip winner">WINNER</span>':''}
${(run.status==='error'||run.status==='done')?`<button class="btn pink" data-retry="${run.model}">↻</button>`:''}
</span>
</div>${bodyHtml}${run.aiReason&&run.status==='done'?`<div class="aireason">🤖 ${esc(run.aiReason)}</div>`:''}`;
arena.appendChild(pane);
- }
+ });
arena.querySelectorAll('[data-win]').forEach(b=>b.onclick=async e=>{
await fetch(API+'/api/challenges/'+c.id+'/vote',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({winner:e.target.dataset.win})});
renderDetail(c.id);
diff --git a/yolo/IDEAS.md b/yolo/IDEAS.md
index ed2d76f..9d24cf2 100644
--- a/yolo/IDEAS.md
+++ b/yolo/IDEAS.md
@@ -16,3 +16,4 @@ Answering the /contrarian critique ("one builder vote = theater", "toy demos not
- [DONE] #1 Artifact auto-screenshot — thumbnails on cards (.shots strip) + battle panes (poster→click-to-run-live), startup backfill, /thumb route. Commit next.
- [DONE] #2 AI auto-referee — local qwen2.5vl:7b scores each rendered artifact 0-10 (JSON), auto-runs on battle settle, picks a winner. Verified on Smoke Test: AI pick (gemma) AGREES with human crown = two-signal validation, directly answers 'one vote = theater'. $0 local vision.
- [DONE] #3 ELO + cost-efficiency + AI-agreement ledger — ELO (K=24, crowned beats all rendered) as headline rank, $/win, avg 🤖 score, and 'AI referee agrees with human X% of N battles' meta-metric. Grok leads @1118.
+- [DONE] #4 Blind judging — toggle hides model names as 'Model A/B/C' until a winner is crowned (also hides AI pick), reveal on crown. Strips brand bias from the vote. localStorage-persisted.
← f109f27 yolo idea 3: ELO ranking (K=24) + cost-per-win + avg AI-refe
·
back to Model Arena
·
yolo idea 5: real-workload preset pack (Strategist's fix) — 3854099 →