[object Object]

← back to Model Arena

yolo idea 6: daily auto-challenge generator (accrues real judged data, $0 local) + AI-judged; launchd install drafted to pending-approval (gated, not installed)

b15a98bc0aa490d6cc2d45efcd5b50a68bd43f7f · 2026-07-22 23:45:20 -0700 · Steve

Files touched

Diff

commit b15a98bc0aa490d6cc2d45efcd5b50a68bd43f7f
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 22 23:45:20 2026 -0700

    yolo idea 6: daily auto-challenge generator (accrues real judged data, $0 local) + AI-judged; launchd install drafted to pending-approval (gated, not installed)
---
 data/artifacts/08d1c975f6db/gemma3-12b.html | 110 ++++++++++++++++++++++++++++
 data/artifacts/08d1c975f6db/gemma3-12b.png  | Bin 0 -> 11354 bytes
 data/artifacts/08d1c975f6db/hermes3-8b.html |  85 +++++++++++++++++++++
 data/artifacts/08d1c975f6db/hermes3-8b.png  | Bin 0 -> 6321 bytes
 data/artifacts/08d1c975f6db/qwen25-7b.html  |  80 ++++++++++++++++++++
 data/artifacts/08d1c975f6db/qwen25-7b.png   | Bin 0 -> 3828 bytes
 data/challenges.json                        |  60 +++++++++++++++
 scripts/com.steve.model-arena-daily.plist   |  14 ++++
 scripts/daily-challenge.js                  |  59 +++++++++++++++
 yolo/IDEAS.md                               |   1 +
 yolo/daily-log.jsonl                        |   1 +
 11 files changed, 410 insertions(+)

diff --git a/data/artifacts/08d1c975f6db/gemma3-12b.html b/data/artifacts/08d1c975f6db/gemma3-12b.html
new file mode 100644
index 0000000..e6c3a47
--- /dev/null
+++ b/data/artifacts/08d1c975f6db/gemma3-12b.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Bouncing Balls</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");
+
+    let width = window.innerWidth;
+    let height = window.innerHeight;
+    canvas.width = width;
+    canvas.height = height;
+
+    let balls = [];
+    let gravity = 0.5;
+
+    function init() {
+      for (let i = 0; i < 20; i++) {
+        balls.push({
+          x: Math.random() * width,
+          y: Math.random() * height,
+          radius: Math.random() * 10 + 5,
+          vx: (Math.random() - 0.5) * 4,
+          vy: (Math.random() - 0.5) * 4,
+          color: `rgba(${Math.random()*255}, ${Math.random()*255}, ${Math.random()*255}, 0.8)`
+        });
+      }
+
+      const gravitySlider = document.createElement('input');
+      gravitySlider.type = 'range';
+      gravitySlider.min = 0;
+      gravitySlider.max = 10;
+      gravitySlider.value = 0.5;
+      gravitySlider.onchange = function() {
+          gravity = parseFloat(this.value);
+      };
+
+      document.body.appendChild(gravitySlider);
+    }
+
+
+    function draw() {
+      ctx.clearRect(0, 0, width, height);
+
+      for (let i = 0; i < balls.length; i++) {
+        const ball = balls[i];
+
+        // Update position
+        ball.y += ball.vy;
+        ball.x += ball.vx;
+
+        // Bounce off walls
+        if (ball.x + ball.radius > width || ball.x - ball.radius < 0) {
+          ball.vx = -ball.vx;
+        }
+        if (ball.y + ball.radius > height || ball.y - ball.radius < 0) {
+          ball.vy = -ball.vy;
+        }
+
+        // Apply gravity
+        ball.vy += gravity;
+
+        // Draw trail
+        ctx.beginPath();
+        ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
+        ctx.fillStyle = ball.color;
+        ctx.fill();
+
+
+        //Draw ball
+        ctx.beginPath();
+        ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
+        ctx.fillStyle = ball.color;
+        ctx.fill();
+
+      }
+    }
+
+    function resize() {
+      width = window.innerWidth;
+      height = window.innerHeight;
+      canvas.width = width;
+      canvas.height = height;
+    }
+
+    init();
+
+    let animationFrameId;
+
+    function animate() {
+        draw();
+        animationFrameId = requestAnimationFrame(animate);
+    }
+
+
+    resize();
+    window.addEventListener('resize', resize);
+    animate();
+
+  </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/08d1c975f6db/gemma3-12b.png b/data/artifacts/08d1c975f6db/gemma3-12b.png
new file mode 100644
index 0000000..7c98c1b
Binary files /dev/null and b/data/artifacts/08d1c975f6db/gemma3-12b.png differ
diff --git a/data/artifacts/08d1c975f6db/hermes3-8b.html b/data/artifacts/08d1c975f6db/hermes3-8b.html
new file mode 100644
index 0000000..755a1af
--- /dev/null
+++ b/data/artifacts/08d1c975f6db/hermes3-8b.html
@@ -0,0 +1,85 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>20 Bouncing Balls</title>
+  <style>
+    body {
+      margin: 0;
+      overflow: hidden;
+    }
+    canvas {
+      display: block;
+      background-color: #f5gsg;
+    }
+    slider {
+      position: fixed;
+      bottom: 10px;
+      left: 10px;
+    }
+  </style>
+</head>
+<body>
+  <canvas id="gameCanvas"></canvas>
+  <input type="range" min="0.1" max="1" step="0.01" value="0.5" class="slider" id="gravitySlider">
+  
+  <script>
+    const canvas = document.getElementById('gameCanvas');
+    const ctx = canvas.getContext('2d');
+
+    let gravity = 0.5;
+    const balls = [];
+    for (let i = 0; i < 20; i++) {
+      balls.push({
+        x: Math.random() * canvas.width,
+        y: Math.random() * canvas.height,
+        radius: Math.random() * 10 + 2,
+        speedX: (Math.random() - 0.5) * 4,
+        speedY: (Math.random() - 0.5) * 4,
+        color: `hsl(${(i*360)/20}, 100%, 50%)`,
+        trailLength: Math.random() * 10 + 2
+      });
+    }
+
+    function drawBalls() {
+      ctx.clearRect(0, 0, canvas.width, canvas.height);
+      
+      for (let i = 0; i < balls.length; i++) {
+        const ball = balls[i];
+        
+        ctx.beginPath();
+        ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
+        ctx.fillStyle = ball.color;
+        ctx.fill();
+        
+        ctx.strokeStyle = `rgba(${ball.color}, ${ball.trailLength})`;
+        ctx.lineWidth = ball.radius / 5;
+        ctx.moveTo(ball.x - ball.radius, ball.y + ball.radius);
+        for (let j = 0; j < ball.trailLength; j++) {
+          const x = ball.x - Math.sin(j * Math.PI / ball.trailLength) * ball.radius;
+          const y = ball.y - Math.cos(j * Math.PI / ball.trailLength) * ball.radius;
+          ctx.lineTo(x, y);
+        }
+        ctx.stroke();
+        
+        balls[i].x += ball.speedX;
+        balls[i].y += ball.speedY + gravity;
+
+        if (balls[i].x < 0 || balls[i].x > canvas.width - ball.radius)
+          balls[i].speedX *= -1;
+        if (balls[i].y < 0 || balls[i].y > canvas.height - ball.radius)
+          balls[i].speedY *= -1;
+      }
+    }
+
+    function updateGravity(newGravity) {
+      gravity = newGravity;
+    }
+
+    setInterval(drawBalls, 20);
+    
+    document.getElementById('gravitySlider').oninput = function() {
+      updateGravity(this.value);
+    };
+  </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/08d1c975f6db/hermes3-8b.png b/data/artifacts/08d1c975f6db/hermes3-8b.png
new file mode 100644
index 0000000..a71a32c
Binary files /dev/null and b/data/artifacts/08d1c975f6db/hermes3-8b.png differ
diff --git a/data/artifacts/08d1c975f6db/qwen25-7b.html b/data/artifacts/08d1c975f6db/qwen25-7b.html
new file mode 100644
index 0000000..af303f7
--- /dev/null
+++ b/data/artifacts/08d1c975f6db/qwen25-7b.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Bouncing Balls</title>
+    <style>
+        body { margin: 0; overflow: hidden; background-color: #2b2b2b; }
+        canvas { display: block; }
+    </style>
+</head>
+<body>
+<canvas id="canvas" width="800" height="600"></canvas>
+<script>
+const canvas = document.getElementById('canvas');
+const ctx = canvas.getContext('2d');
+
+let balls = [];
+for (let i = 0; i < 20; i++) {
+    balls.push({
+        x: Math.random() * canvas.width,
+        y: 0,
+        dx: (Math.random() - 0.5) * 3,
+        dy: 0,
+        radius: Math.random() * 10 + 10
+    });
+}
+
+function drawBall(ball, trail = false) {
+    ctx.beginPath();
+    ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
+    if (trail) ctx.strokeStyle = 'rgba(255,255,255,' + (1 - ball.y / canvas.height) + ')';
+    else ctx.fillStyle = 'white';
+    if (trail) ctx.stroke();
+    else ctx.fill();
+}
+
+function draw() {
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+    const gravity = 0.5;
+    for (let i = balls.length - 1; i >= 0; i--) {
+        let ball = balls[i];
+        if (ball.y + ball.radius > canvas.height) {
+            ball.dy = -(ball.dy * 0.7);
+            ball.y = canvas.height - ball.radius;
+        } else {
+            ball.dy += gravity;
+        }
+        if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
+            ball.dx = -ball.dx;
+        }
+        for (let j = i; j < balls.length; j++) {
+            let otherBall = balls[j];
+            if ((i !== j) && (Math.hypot(ball.x - otherBall.x, ball.y - otherBall.y) < (ball.radius + otherBall.radius))) {
+                let angle = Math.atan2(otherBall.y - ball.y, otherBall.x - ball.x);
+                let speed = 0.75 * ((ball.dx * ball.dx + ball.dy * ball.dy) ** 0.5 + otherBall.dx * otherBall.dx + otherBall.dy * otherBall.dy) ** 0.5;
+                balls[i] = {
+                    x: canvas.width / 2,
+                    y: canvas.height / 2,
+                    dx: speed * Math.cos(angle),
+                    dy: speed * -Math.sin(angle)
+                };
+                balls[j] = {
+                    x: canvas.width / 2,
+                    y: canvas.height / 2,
+                    dx: -speed * Math.cos(angle),
+                    dy: -speed * Math.sin(angle)
+                };
+            }
+        }
+        ball.x += ball.dx;
+        ball.y += ball.dy;
+        drawBall(ball, true);
+    }
+}
+
+setInterval(draw, 1000/60);
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/08d1c975f6db/qwen25-7b.png b/data/artifacts/08d1c975f6db/qwen25-7b.png
new file mode 100644
index 0000000..1d5cbe1
Binary files /dev/null and b/data/artifacts/08d1c975f6db/qwen25-7b.png differ
diff --git a/data/challenges.json b/data/challenges.json
index a458866..ea213c2 100644
--- a/data/challenges.json
+++ b/data/challenges.json
@@ -824,5 +824,65 @@
     "judging": false,
     "aiPick": "gemma3-12b",
     "judged_at": "2026-07-23T06:40:30.864Z"
+  },
+  {
+    "id": "08d1c975f6db",
+    "title": "Daily: Bouncing Balls",
+    "prompt": "Single-file HTML canvas with 20 balls bouncing under gravity with collisions and trails; a slider controls gravity.",
+    "created_at": "2026-07-23T06:41:58.764Z",
+    "winner": null,
+    "runs": [
+      {
+        "model": "qwen3-14b",
+        "status": "error",
+        "error": "no HTML document in model output (0 chars)",
+        "seconds": 132,
+        "cost": null,
+        "started_at": "2026-07-23T06:41:58.771Z",
+        "finished_at": "2026-07-23T06:44:10.361Z"
+      },
+      {
+        "model": "gemma3-12b",
+        "status": "done",
+        "error": null,
+        "seconds": 21,
+        "cost": 0,
+        "started_at": "2026-07-23T06:44:10.364Z",
+        "finished_at": "2026-07-23T06:44:31.681Z",
+        "bytes": 2535,
+        "thumb": true,
+        "aiScore": 7,
+        "aiReason": "The model demonstrates the basic functionality of balls bouncing under gravity and colliding, but lacks visual polish or additional features."
+      },
+      {
+        "model": "hermes3-8b",
+        "status": "done",
+        "error": null,
+        "seconds": 10,
+        "cost": 0,
+        "started_at": "2026-07-23T06:44:31.683Z",
+        "finished_at": "2026-07-23T06:44:41.667Z",
+        "bytes": 2327,
+        "thumb": true,
+        "aiScore": 7,
+        "aiReason": "The model demonstrates basic functionality but lacks visual appeal and detail."
+      },
+      {
+        "model": "qwen25-7b",
+        "status": "done",
+        "error": null,
+        "seconds": 18,
+        "cost": 0,
+        "started_at": "2026-07-23T06:41:58.773Z",
+        "finished_at": "2026-07-23T06:42:16.559Z",
+        "bytes": 2572,
+        "thumb": true,
+        "aiScore": 4,
+        "aiReason": "The canvas is empty and no balls or gravity are visible."
+      }
+    ],
+    "judging": false,
+    "aiPick": "gemma3-12b",
+    "judged_at": "2026-07-23T06:44:54.324Z"
   }
 ]
\ No newline at end of file
diff --git a/scripts/com.steve.model-arena-daily.plist b/scripts/com.steve.model-arena-daily.plist
new file mode 100644
index 0000000..04adc24
--- /dev/null
+++ b/scripts/com.steve.model-arena-daily.plist
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0"><dict>
+  <key>Label</key><string>com.steve.model-arena-daily</string>
+  <key>ProgramArguments</key>
+  <array>
+    <string>/opt/homebrew/bin/node</string>
+    <string>/Users/macstudio3/Projects/model-arena/scripts/daily-challenge.js</string>
+  </array>
+  <key>StartCalendarInterval</key><dict><key>Hour</key><integer>7</integer><key>Minute</key><integer>15</integer></dict>
+  <key>StandardOutPath</key><string>/tmp/model-arena-daily.log</string>
+  <key>StandardErrorPath</key><string>/tmp/model-arena-daily.err</string>
+  <key>RunAtLoad</key><false/>
+</dict></plist>
diff --git a/scripts/daily-challenge.js b/scripts/daily-challenge.js
new file mode 100644
index 0000000..df355c5
--- /dev/null
+++ b/scripts/daily-challenge.js
@@ -0,0 +1,59 @@
+#!/usr/bin/env node
+// Daily auto-challenge — creates one $0 all-local battle on a rotating prompt,
+// waits for it to render, triggers the AI vision referee, and logs it. This
+// accrues REAL judged data over time so the leaderboard stops being "one vote".
+// It deliberately does NOT auto-crown (the human 👑 vote stays pure); it just
+// builds a backlog of AI-scored battles for Steve to crown at his leisure.
+//
+// Usage:  node scripts/daily-challenge.js            (uses day-of-year rotation)
+//         PICK=3 node scripts/daily-challenge.js     (force a specific prompt)
+// Env:    BASE (default http://127.0.0.1:9758), AUTH (default admin:DW2024!)
+const http = require('http'), fs = require('fs'), path = require('path');
+const BASE = process.env.BASE || 'http://127.0.0.1:9758';
+const AUTH = process.env.AUTH || 'admin:DW2024!';
+const LOG = path.join(__dirname, '..', 'yolo', 'daily-log.jsonl');
+const LOCAL = ['qwen3-14b', 'gemma3-12b', 'hermes3-8b', 'qwen25-7b'];
+
+const POOL = [
+  { t: 'Daily: Particle Fireworks', p: 'Interactive fireworks in one HTML file: click to launch rockets that burst into physics particles with trails, gravity, glow; include an auto-show mode.' },
+  { t: 'Daily: Sortable Catalog', p: 'Single-file HTML catalog of 8 invented wallcovering SKUs (sku, pattern, colorway, price, stock) as a responsive card grid with a live search box and click-to-sort columns.' },
+  { t: 'Daily: Bouncing Balls', p: 'Single-file HTML canvas with 20 balls bouncing under gravity with collisions and trails; a slider controls gravity.' },
+  { t: 'Daily: Luxury Product Page', p: 'Single-file HTML luxury wallpaper product page: hero (CSS gradient), serif display type, price, Add to Cart, spec table. High-end interior-design aesthetic.' },
+  { t: 'Daily: Starfield Warp', p: 'Single-file HTML animated starfield warp effect on canvas; mouse position steers the warp direction; a slider controls speed.' },
+  { t: 'Daily: Sample-Sale Email', p: 'Single-file email-safe HTML marketing email for a wallpaper sample sale: header, 3 featured patterns in a responsive grid (gradient placeholders), CTA button, compliant footer. Inline CSS only.' },
+  { t: 'Daily: Conway Life', p: "Single-file HTML Conway's Game of Life on canvas with play/pause, randomize, and a speed slider." },
+];
+
+function req(method, p, body) {
+  return new Promise((res, rej) => {
+    const u = new URL(BASE + p);
+    const r = http.request(u, { method, headers: { 'Content-Type': 'application/json', Authorization: 'Basic ' + Buffer.from(AUTH).toString('base64') } }, resp => {
+      let b = ''; resp.on('data', d => b += d); resp.on('end', () => { try { res(JSON.parse(b)); } catch { res({}); } });
+    });
+    r.on('error', rej); if (body) r.write(JSON.stringify(body)); r.end();
+  });
+}
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+
+(async () => {
+  const idx = process.env.PICK != null ? (+process.env.PICK % POOL.length)
+    : Math.floor((Date.now() / 86400000)) % POOL.length; // day-of-epoch rotation
+  const pick = POOL[idx];
+  const c = await req('POST', '/api/challenges', { title: pick.t, prompt: pick.p, models: LOCAL });
+  if (!c.id) { console.error('create failed', c); process.exit(1); }
+  console.log('created', c.id, '—', pick.t);
+  // poll until settled + judged (cap ~20 min)
+  for (let i = 0; i < 150; i++) {
+    await sleep(8000);
+    const cur = await req('GET', '/api/challenges/' + c.id);
+    const settled = cur.runs.every(r => r.status === 'done' || r.status === 'error');
+    if (settled && !cur.judging && cur.judged_at) {
+      const done = cur.runs.filter(r => r.status === 'done').length;
+      const entry = { ts: new Date().toISOString(), id: c.id, title: pick.t, done, aiPick: cur.aiPick };
+      try { fs.appendFileSync(LOG, JSON.stringify(entry) + '\n'); } catch {}
+      console.log('DONE', JSON.stringify(entry));
+      return;
+    }
+  }
+  console.error('timed out waiting for battle to settle'); process.exit(2);
+})().catch(e => { console.error(e); process.exit(1); });
diff --git a/yolo/IDEAS.md b/yolo/IDEAS.md
index c40b86b..60310c9 100644
--- a/yolo/IDEAS.md
+++ b/yolo/IDEAS.md
@@ -18,3 +18,4 @@ Answering the /contrarian critique ("one builder vote = theater", "toy demos not
 - [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.
 - [DONE] #5 Real-workload presets (Strategist answer) — added a '🏭 Real Work' preset category (luxury product page, sortable catalog, sample-sale email, room visualizer, JSON→grid) alongside '🎮 Games'. Verified full stack (gen→render→AI-judge) on the product-page task; AI reasons were task-aware. Moves the arena toward Steve's real decisions, not just toy demos.
+- [DONE] #6 Daily auto-challenge generator — scripts/daily-challenge.js creates a $0 all-local battle on a day-rotating prompt, renders + AI-judges, logs to yolo/daily-log.jsonl (no auto-crown, keeps human vote pure). Test-run OK. launchd install DRAFTED to pending-approval (GATED, not installed).
diff --git a/yolo/daily-log.jsonl b/yolo/daily-log.jsonl
new file mode 100644
index 0000000..f4e7d92
--- /dev/null
+++ b/yolo/daily-log.jsonl
@@ -0,0 +1 @@
+{"ts":"2026-07-23T06:44:54.850Z","id":"08d1c975f6db","title":"Daily: Bouncing Balls","done":3,"aiPick":"gemma3-12b"}

← 3854099 yolo idea 5: real-workload preset pack (Strategist's fix) —  ·  back to Model Arena  ·  auto-save: 2026-07-22T23:47:26 (3 files) — data/challenges.j 956ad95 →