[object Object]

← 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

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> &nbsp;·&nbsp; 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> &nbsp;·&nbsp; 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 →