[object Object]

← back to Model Arena

arena: move Build-in-iTerm from whole-challenge to per-MODEL-CHIP — pick which model's attempt to build; brief embeds that model's actual artifact so Claude builds it forward. Per-pane ⚒ in detail view too.

1e4746aa45d838e56b55b62ad226249328e271b1 · 2026-07-23 06:50:56 -0700 · Steve

Files touched

Diff

commit 1e4746aa45d838e56b55b62ad226249328e271b1
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 23 06:50:56 2026 -0700

    arena: move Build-in-iTerm from whole-challenge to per-MODEL-CHIP — pick which model's attempt to build; brief embeds that model's actual artifact so Claude builds it forward. Per-pane ⚒ in detail view too.
---
 data/artifacts/ca355985ca95/qwen3-14b.html | 100 +++++++++++++++++++++++++++++
 data/artifacts/ca355985ca95/qwen3-14b.png  | Bin 0 -> 2727 bytes
 data/challenges.json                       |  35 +++++-----
 data/votes.jsonl                           |   1 +
 public/index.html                          |  32 ++++-----
 5 files changed, 136 insertions(+), 32 deletions(-)

diff --git a/data/artifacts/ca355985ca95/qwen3-14b.html b/data/artifacts/ca355985ca95/qwen3-14b.html
new file mode 100644
index 0000000..90eea87
--- /dev/null
+++ b/data/artifacts/ca355985ca95/qwen3-14b.html
@@ -0,0 +1,100 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <style>
+    html, body { margin:0; overflow:hidden; }
+    canvas { display: block; }
+  </style>
+</head>
+<body>
+<canvas></canvas>
+<script>
+const canvas = document.querySelector('canvas');
+const ctx = canvas.getContext('2d');
+canvas.width = window.innerWidth;
+canvas.height = window.innerHeight;
+
+let particles = [];
+const numParticles = 2000;
+const letters = 'ARENA'.split('');
+const springConstant = 0.005;
+const damping = 0.8;
+const formationDuration = 2000; // ms
+const holdingDuration = 1000; // ms
+const explosionDuration = 1000; // ms
+const reformDuration = 2000; // ms
+
+function createParticles() {
+  const letterCount = letters.length;
+  const particlesPerLetter = Math.floor(numParticles / letterCount);
+  const fontSize = 100;
+  const textWidth = 5 * fontSize * 0.6;
+  const textX = (canvas.width - textWidth) / 2;
+  const textY = canvas.height / 2;
+  const letterWidth = fontSize * 0.6;
+  const letterHeight = fontSize;
+
+  particles = [];
+  for (let i = 0; i < numParticles; i++) {
+    const letterIndex = Math.floor(i / particlesPerLetter) % letterCount;
+    const startX = textX + letterIndex * letterWidth;
+    const startY = textY - letterHeight / 2;
+    const targetX = startX + Math.random() * letterWidth;
+    const targetY = startY + Math.random() * letterHeight;
+    const x = Math.random() * canvas.width;
+    const y = Math.random() * canvas.height;
+    particles.push({
+      x, y,
+      vx: 0,
+      vy: 0,
+      ax: 0,
+      ay: 0,
+      targetX, targetY,
+      group: letterIndex
+    });
+  }
+}
+
+createParticles();
+
+let state = 'forming';
+let stateStartTime = performance.now();
+
+function updateParticles() {
+  const now = performance.now();
+  const deltaTime = (now - stateStartTime) / 1000;
+
+  switch (state) {
+    case 'forming':
+      if (deltaTime >= formationDuration / 1000) {
+        state = 'holding';
+        stateStartTime = now;
+      }
+      break;
+    case 'holding':
+      if (deltaTime >= holdingDuration / 1000) {
+        state = 'exploding';
+        stateStartTime = now;
+      }
+      break;
+    case 'exploding':
+      if (deltaTime >= explosionDuration / 1000) {
+        state = 'reforming';
+        stateStartTime = now;
+      }
+      break;
+    case 'reforming':
+      if (deltaTime >= reformDuration / 1000) {
+        state = 'forming';
+        stateStartTime = now;
+      }
+      break;
+  }
+
+  for (let p of particles) {
+    let ax = 0, ay = 0;
+    if (state === 'forming' || state === 'reforming') {
+      const dx = p.targetX - p.x;
+      const dy = p.targetY - p.y;
+      ax = dx * springConstant;
+      ay = dy *
\ No newline at end of file
diff --git a/data/artifacts/ca355985ca95/qwen3-14b.png b/data/artifacts/ca355985ca95/qwen3-14b.png
new file mode 100644
index 0000000..16a103c
Binary files /dev/null and b/data/artifacts/ca355985ca95/qwen3-14b.png differ
diff --git a/data/challenges.json b/data/challenges.json
index b2734a4..51d33f3 100644
--- a/data/challenges.json
+++ b/data/challenges.json
@@ -1569,31 +1569,33 @@
     "prompt": "Build a single self-contained HTML file: a canvas particle system where ~2000 particles assemble into the word \"ARENA\", hold, then explode outward and reform on click. Smooth 60fps, glowing neon particles, physics-based motion. Output ONLY the HTML.",
     "category": "Games",
     "created_at": "2026-07-23T13:46:18.891Z",
-    "winner": null,
+    "winner": "qwen25-7b",
     "runs": [
       {
         "model": "qwen3-14b",
-        "status": "running",
+        "status": "done",
         "error": null,
-        "seconds": null,
-        "cost": null,
+        "seconds": 137,
+        "cost": 0,
         "started_at": "2026-07-23T13:47:44.578Z",
-        "finished_at": null
+        "finished_at": "2026-07-23T13:50:01.322Z",
+        "bytes": 2597,
+        "thumb": true
       },
       {
         "model": "gemma3-12b",
-        "status": "queued",
-        "error": null,
-        "seconds": null,
+        "status": "error",
+        "error": "server restarted mid-run",
+        "seconds": 0,
         "cost": null,
-        "started_at": null,
+        "started_at": "2026-07-23T13:50:01.324Z",
         "finished_at": null
       },
       {
         "model": "hermes3-8b",
-        "status": "queued",
-        "error": null,
-        "seconds": null,
+        "status": "error",
+        "error": "server restarted mid-run",
+        "seconds": 0,
         "cost": null,
         "started_at": null,
         "finished_at": null
@@ -1611,9 +1613,9 @@
       },
       {
         "model": "kimi",
-        "status": "running",
-        "error": null,
-        "seconds": null,
+        "status": "error",
+        "error": "server restarted mid-run",
+        "seconds": 0,
         "cost": null,
         "started_at": "2026-07-23T13:48:22.281Z",
         "finished_at": null
@@ -1639,6 +1641,7 @@
         "bytes": 5051,
         "thumb": true
       }
-    ]
+    ],
+    "voted_at": "2026-07-23T13:50:02.948Z"
   }
 ]
\ No newline at end of file
diff --git a/data/votes.jsonl b/data/votes.jsonl
index c4ee3f1..145a347 100644
--- a/data/votes.jsonl
+++ b/data/votes.jsonl
@@ -5,3 +5,4 @@
 {"ts":"2026-07-23T06:13:14.106Z","challenge":"02bc35a3d0b2","title":"Propagation all-7","winner":"grok","field":["qwen3-14b","gemma3-12b","hermes3-8b","qwen25-7b","kimi","gpt","grok"]}
 {"ts":"2026-07-23T13:10:47.373Z","challenge":"08d1c975f6db","title":"Daily: Bouncing Balls","winner":"gemma3-12b","field":["gemma3-12b","hermes3-8b","qwen25-7b"]}
 {"ts":"2026-07-23T13:35:47.844Z","challenge":"f2af82652fcd","title":"california real estate brokers","winner":"grok","field":["qwen3-14b","gemma3-12b","hermes3-8b","qwen25-7b","kimi","gpt","grok"]}
+{"ts":"2026-07-23T13:50:02.948Z","challenge":"ca355985ca95","title":"Particle Text Reactor","winner":"qwen25-7b","field":["qwen3-14b","qwen25-7b","gpt","grok"]}
diff --git a/public/index.html b/public/index.html
index 193dfae..a239ffc 100644
--- a/public/index.html
+++ b/public/index.html
@@ -47,11 +47,11 @@ input[type=range]{accent-color:var(--neon);width:130px}
 .chip.queued{border-color:var(--dim)}
 .chip.error{border-color:var(--err);color:var(--err)}
 .chip.winner{border-color:var(--gold);color:var(--gold);font-weight:700}
-.mini.build{margin-top:10px;background:none;border:1px solid var(--neon2);color:var(--neon2);padding:5px 12px;font:inherit;font-size:11px;
-  letter-spacing:1px;text-transform:uppercase;cursor:pointer;border-radius:3px;width:100%}
-.mini.build:hover{background:rgba(255,46,166,.12);box-shadow:0 0 10px rgba(255,46,166,.25)}
-.mini.build:disabled{opacity:.6;cursor:default}
-#d-build{color:var(--neon2)}
+.chip[data-build-model]{cursor:pointer}
+.chip[data-build-model] .bx{color:var(--neon2);opacity:.55;font-weight:400}
+.chip[data-build-model]:hover{border-color:var(--neon2);box-shadow:0 0 8px rgba(255,46,166,.3)}
+.chip[data-build-model]:hover .bx{opacity:1}
+.chip.launched{border-color:var(--ok);color:var(--ok)}
 @keyframes pulse{50%{opacity:.35}}
 .empty{color:var(--dim);padding:40px;text-align:center;border:1px dashed var(--line)}
 /* new challenge */
@@ -186,7 +186,6 @@ tr.clk{cursor:pointer}tr.clk:hover td{background:rgba(0,229,255,.06)}
   <section id="detail" class="view">
     <span class="back" id="back">← back to challenges</span>
     <a class="back" id="export" style="margin-left:16px" download>⬇ export battle as standalone HTML</a>
-    <span class="back" id="d-build" style="margin-left:16px">⚒ Build in iTerm</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>
@@ -291,21 +290,21 @@ async function loadList(){
     div.innerHTML = `<h3>${esc(c.title)}</h3>
       <div class="when" title="${c.created_at}">🕓 ${fmtWhen(c.created_at)}</div>
       ${shotsHtml}
-      <div class="chips">${chips}</div>
-      <button class="mini build" data-build="${c.id}" title="Open a real Claude session in iTerm2, pre-loaded with this challenge, to actually build it">⚒ Build in iTerm</button>`;
-    div.onclick = (e)=>{ if(e.target.dataset.build) return; openDetail(c.id); };
-    div.querySelector('[data-build]').onclick = (e)=>{ e.stopPropagation(); buildInIterm(c.id, e.target); };
+      <div class="chips">${chips}</div>`;
+    div.onclick = (e)=>{ const chip=e.target.closest('[data-build-model]'); if(chip){ e.stopPropagation(); buildInIterm(c.id, chip, chip.dataset.buildModel); return; } openDetail(c.id); };
     el.appendChild(div);
   }
 }
 function esc(s){ return String(s).replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
-async function buildInIterm(id, btn){
-  const t = btn && btn.textContent; if(btn){ btn.textContent='⚒ launching…'; btn.disabled=true; }
+async function buildInIterm(id, el, model){
+  const t = el && el.innerHTML; if(el){ el.style.opacity=.6; }
   try{
-    const r = await fetch(API+'/api/challenges/'+id+'/build',{method:'POST'});
+    const url = API+'/api/challenges/'+id+'/build'+(model?'/'+model:'');
+    const r = await fetch(url,{method:'POST'});
     if(!r.ok){ alert('Build launch failed: '+(await r.text())); }
-    else if(btn){ btn.textContent='⚒ launched ↗'; setTimeout(()=>{btn.textContent=t;btn.disabled=false;},2500); }
-  }catch(e){ alert('Build launch failed: '+e.message); if(btn){btn.textContent=t;btn.disabled=false;} }
+    else if(el){ el.classList.add('launched'); setTimeout(()=>{el.style.opacity=1;el.classList.remove('launched');},2000); }
+  }catch(e){ alert('Build launch failed: '+e.message); }
+  finally{ if(el) el.style.opacity=1; }
 }
 
 // ---- new battle ----
@@ -372,7 +371,6 @@ async function renderDetail(id, statusOnly){
   const remixNote = c.remixOf ? '🧬 remixed from ' + mLabel(c.remixSource) + "'s artifact\n" : '';
   $('#d-meta').textContent = '🕓 ' + fmtWhen(c.created_at) + (c.category?'  ·  '+c.category:'') + '\n' + remixNote + c.prompt;
   $('#export').href = API+'/export/'+c.id;
-  $('#d-build').onclick = (e)=>buildInIterm(c.id, e.target);
   // AI referee status bar
   const anyArt = c.runs.some(r=>r.status==='done'&&r.thumb);
   const ai = $('#d-ai');
@@ -436,6 +434,7 @@ async function renderDetail(id, statusOnly){
           ${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>`:''}
           ${!anon&&c.aiPick===run.model?'<span class="chip aipick">🤖 AI PICK</span>':''}
           ${c.winner===run.model?'<span class="chip winner">WINNER</span>':''}
+          ${run.status==='done'?`<button class="btn pink" data-build="${run.model}" title="Build ${mLabel(run.model)}'s artifact into a real project in an iTerm Claude session">⚒</button>`:''}
           ${run.status==='done'?`<button class="btn" data-src="${c.id}/${run.model}" title="View HTML source">&lt;/&gt;</button>`:''}
           ${(run.status==='error'||run.status==='done')?`<button class="btn pink" data-retry="${run.model}">↻</button>`:''}
         </span>
@@ -454,6 +453,7 @@ async function renderDetail(id, statusOnly){
     const [cid,mid]=el.dataset.run.split('/');
     el.outerHTML = `<iframe sandbox="allow-scripts" src="${API}/artifact/${cid}/${mid}"></iframe>`;
   });
+  arena.querySelectorAll('[data-build]').forEach(b=>b.onclick=e=>buildInIterm(c.id, e.target, e.target.dataset.build));
   arena.querySelectorAll('[data-src]').forEach(b=>b.onclick=async e=>{
     const [cid,mid]=e.target.dataset.src.split('/');
     const body = e.target.closest('.pane').querySelector('iframe,.poster,.msg,.srcview');

← 2897af6 auto-save: 2026-07-23T06:49:07 (7 files) — data/challenges.j  ·  back to Model Arena  ·  resilience: global unhandledRejection/uncaughtException hand b25a81b →