[object Object]

← back to Wallco Ai

subject-mismatch: add Sort (root-id/subject) + Density slider, both persist per standing rule; no color-sort/date-chip (API exposes no hex/created_at) [yolo tick4, local-only]

642ed28ec04ff35124a4635fbab051249f57c61a · 2026-06-02 12:30:30 -0700 · Steve Abrams

Files touched

Diff

commit 642ed28ec04ff35124a4635fbab051249f57c61a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 2 12:30:30 2026 -0700

    subject-mismatch: add Sort (root-id/subject) + Density slider, both persist per standing rule; no color-sort/date-chip (API exposes no hex/created_at) [yolo tick4, local-only]
---
 public/subject-mismatch.html | 94 ++++++++++++++++++++++++++++++++------------
 1 file changed, 68 insertions(+), 26 deletions(-)

diff --git a/public/subject-mismatch.html b/public/subject-mismatch.html
index 79e1b6d..641cbdd 100644
--- a/public/subject-mismatch.html
+++ b/public/subject-mismatch.html
@@ -5,7 +5,7 @@
 <meta name="viewport" content="width=device-width, initial-scale=1">
 <title>Subject-Mismatch Gallery — dropped-subject failures</title>
 <style>
-  :root { --bg:#fafaf8; --fg:#1a1a1a; --muted:#6a6a6a; --card:#fff; --border:#e8e6e1; --accent:#b3261e; --warn:#c98a00; }
+  :root { --bg:#fafaf8; --fg:#1a1a1a; --muted:#6a6a6a; --card:#fff; --border:#e8e6e1; --accent:#b3261e; --warn:#c98a00; --gold:#c9a14b; --card-min:360px; }
   * { box-sizing: border-box; }
   body { margin:0; background:var(--bg); color:var(--fg); font:14px/1.5 -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; }
   header { padding:22px 28px 16px; border-bottom:1px solid var(--border); background:var(--card); position:sticky; top:0; z-index:10; }
@@ -14,8 +14,12 @@
   header .stats { color:var(--muted); font-size:13px; }
   header .stats b { color:var(--fg); font-weight:600; }
   header .meta { color:var(--muted); font-size:12px; margin-top:6px; }
+  .filters { margin-top:12px; display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
+  .filters select, .filters input { padding:6px 10px; border:1px solid var(--border); border-radius:4px; background:var(--card); font:inherit; }
+  .filters label { font-size:12px; color:var(--muted); margin-right:4px; }
+  .filters input[type=range] { accent-color:var(--gold); vertical-align:middle; padding:0; }
   main { padding:22px 28px 64px; }
-  .grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(360px, 1fr)); gap:20px; }
+  .grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(var(--card-min), 1fr)); gap:20px; }
   .card { background:var(--card); border:1px solid var(--border); border-radius:8px; overflow:hidden; position:relative; }
   .card .img { aspect-ratio:1; background:#f0eee8; overflow:hidden; position:relative; }
   .card .img img { width:100%; height:100%; object-fit:cover; display:block; }
@@ -38,40 +42,78 @@
   <div class="stats">Roots flagged as <b>primary subject dropped</b> by the post-gen vision detector
     (<code>lib/subject-detector</code>). Image shows only background/foliage — the animal/object the prompt requested never landed.</div>
   <div class="meta">Scanned <b id="scanned">…</b> drunk-animals roots · use <code>node scripts/scan-subject-mismatch.js --limit=N --seed=S</code> to rescan.</div>
+  <div class="filters">
+    <label>Sort</label>
+    <select id="sortBy">
+      <option value="default">Default (scan order)</option>
+      <option value="id_desc">Root ID ↓</option>
+      <option value="id">Root ID ↑</option>
+      <option value="subject">Subject A→Z</option>
+    </select>
+    <label>Density</label>
+    <input type="range" id="density" min="280" max="600" step="20" title="card size">
+  </div>
 </header>
 <main>
   <div class="grid" id="grid"><div class="empty">Loading…</div></div>
 </main>
 <script>
+const $ = (id) => document.getElementById(id);
+let ALL = [];
+
+// sort + density persist (Steve standing rule: every card grid; both persist).
+// Note: this QA API exposes no created_at / dominant_hex, so no date chip + no color sort.
+$('sortBy').value = localStorage.getItem('subjMismatch_sort') || 'default';
+$('density').value = localStorage.getItem('subjMismatch_density') || '360';
+document.documentElement.style.setProperty('--card-min', $('density').value + 'px');
+$('sortBy').addEventListener('change', () => { localStorage.setItem('subjMismatch_sort', $('sortBy').value); render(); });
+$('density').addEventListener('input', () => {
+  localStorage.setItem('subjMismatch_density', $('density').value);
+  document.documentElement.style.setProperty('--card-min', $('density').value + 'px');
+});
+
+function sorted(rows) {
+  const r = rows.slice(), m = $('sortBy').value;
+  if (m === 'id_desc') r.sort((a, b) => b.root_id - a.root_id);
+  else if (m === 'id') r.sort((a, b) => a.root_id - b.root_id);
+  else if (m === 'subject') r.sort((a, b) => String(a.primary_subject || '').localeCompare(String(b.primary_subject || '')) || (b.root_id - a.root_id));
+  return r; // 'default' keeps scan order
+}
+
+function render() {
+  const grid = $('grid');
+  if (!ALL.length) {
+    grid.innerHTML = '<div class="empty">No flagged designs yet. Run <code>node scripts/scan-subject-mismatch.js</code>.</div>';
+    return;
+  }
+  grid.innerHTML = sorted(ALL).map(p => `
+    <div class="card">
+      <div class="img">
+        <span class="label">${p.primary_subject || '?'} MISSING</span>
+        <img loading="lazy" src="/designs/img/by-id/${p.root_id}?asis=1" alt="root ${p.root_id}">
+      </div>
+      <div class="meta">
+        <div class="row1">
+          <div class="id"><a href="/design/${p.root_id}?asis=1">root #${p.root_id}</a></div>
+          <div class="subject">prompt asked: <span class="missing">${(p.subjects || []).join(', ')}</span></div>
+        </div>
+        <div class="prompt">"${p.prompt_head || ''}"</div>
+        <div class="saw"><b>Vision saw:</b> ${p.what_you_see || '—'}</div>
+      </div>
+    </div>
+  `).join('');
+}
+
 async function load() {
   try {
     const r = await fetch('/api/subject-mismatch');
     const d = await r.json();
-    document.getElementById('count').textContent = d.count || 0;
-    document.getElementById('scanned').textContent = d.scanned || 0;
-    const grid = document.getElementById('grid');
-    if (!d.items || !d.items.length) {
-      grid.innerHTML = '<div class="empty">No flagged designs yet. Run <code>node scripts/scan-subject-mismatch.js</code>.</div>';
-      return;
-    }
-    grid.innerHTML = d.items.map(p => `
-      <div class="card">
-        <div class="img">
-          <span class="label">${p.primary_subject || '?'} MISSING</span>
-          <img loading="lazy" src="/designs/img/by-id/${p.root_id}?asis=1" alt="root ${p.root_id}">
-        </div>
-        <div class="meta">
-          <div class="row1">
-            <div class="id"><a href="/design/${p.root_id}?asis=1">root #${p.root_id}</a></div>
-            <div class="subject">prompt asked: <span class="missing">${(p.subjects || []).join(', ')}</span></div>
-          </div>
-          <div class="prompt">"${p.prompt_head || ''}"</div>
-          <div class="saw"><b>Vision saw:</b> ${p.what_you_see || '—'}</div>
-        </div>
-      </div>
-    `).join('');
+    $('count').textContent = d.count || 0;
+    $('scanned').textContent = d.scanned || 0;
+    ALL = d.items || [];
+    render();
   } catch (e) {
-    document.getElementById('grid').innerHTML = '<div class="empty">Failed: ' + e.message + '</div>';
+    $('grid').innerHTML = '<div class="empty">Failed: ' + e.message + '</div>';
   }
 }
 load();

← 1256df9 yolo loop tick 4: smoke-test health check (focus B)  ·  back to Wallco Ai  ·  yolo-backlog: T4 done — N4 subject-mismatch sort+density [lo 55e320e →