[object Object]

← back to Wallco Ai

admin: side-by-side inspirations gallery (real seed × wallco AI original)

f4153d0d2178070ab6b588d964b88a3566d65955 · 2026-05-20 00:00:27 -0700 · Steve Abrams

public/admin/inspirations.html — sortable, density-adjustable admin grid that
pairs each AI-generated wallco design with the real archival vendor product
that seeded it.

LEFT pane (admin-only):
  - Real shopify_products image (Schumacher/Kravet/Thibaut/Sandberg/etc.)
  - Vendor name pill (visible because admin-only)
  - SKU, title, era, palette swatches from shopify_color_enrichment

RIGHT pane (mirrors what's on /design/:id):
  - The wallco AI-generated image (1024×1024 SDXL output)
  - Design # link, dominant hex, extracted palette
  - Open /design/N button, "Regenerate from same seed" button
  - Published / Unpublished pill

Standing rule: sort <select> (Newest / Vendor / SKU / Published-first /
Unpublished-first) + density <input range> (380-780px card min). Both
persist in localStorage under wallco.admin.inspirations.* keys.

Robots noindex,nofollow. Admin gate enforced server-side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit f4153d0d2178070ab6b588d964b88a3566d65955
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed May 20 00:00:27 2026 -0700

    admin: side-by-side inspirations gallery (real seed × wallco AI original)
    
    public/admin/inspirations.html — sortable, density-adjustable admin grid that
    pairs each AI-generated wallco design with the real archival vendor product
    that seeded it.
    
    LEFT pane (admin-only):
      - Real shopify_products image (Schumacher/Kravet/Thibaut/Sandberg/etc.)
      - Vendor name pill (visible because admin-only)
      - SKU, title, era, palette swatches from shopify_color_enrichment
    
    RIGHT pane (mirrors what's on /design/:id):
      - The wallco AI-generated image (1024×1024 SDXL output)
      - Design # link, dominant hex, extracted palette
      - Open /design/N button, "Regenerate from same seed" button
      - Published / Unpublished pill
    
    Standing rule: sort <select> (Newest / Vendor / SKU / Published-first /
    Unpublished-first) + density <input range> (380-780px card min). Both
    persist in localStorage under wallco.admin.inspirations.* keys.
    
    Robots noindex,nofollow. Admin gate enforced server-side.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 public/admin/inspirations.html | 323 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 323 insertions(+)

diff --git a/public/admin/inspirations.html b/public/admin/inspirations.html
new file mode 100644
index 0000000..3df49b0
--- /dev/null
+++ b/public/admin/inspirations.html
@@ -0,0 +1,323 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<meta name="robots" content="noindex,nofollow">
+<title>Inspirations — wallco.ai admin</title>
+<style>
+  :root {
+    --ink:#1f1808; --line:#d8d0c0; --gold:#c9a14b;
+    --bg:#fbf8f1; --card:#fff; --faint:#7a6e5a;
+    --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
+    --serif:'Playfair Display','Didot',Georgia,serif;
+  }
+  body { margin:0; background:var(--bg); color:var(--ink); font:14px var(--sans); }
+  header {
+    padding:18px 28px; border-bottom:1px solid var(--line); background:#fff;
+    display:flex; gap:16px; align-items:baseline;
+  }
+  header h1 { font:400 24px/1.2 var(--serif); margin:0; }
+  header .meta { color:var(--faint); font-size:12px; letter-spacing:.04em; }
+  header .right { margin-left:auto; display:flex; gap:14px; align-items:center; }
+  header a, header button {
+    color:var(--ink); text-decoration:none; font:600 11px var(--sans);
+    letter-spacing:.1em; text-transform:uppercase;
+    padding:7px 14px; border:1px solid var(--line); border-radius:18px;
+    background:#fff; cursor:pointer;
+  }
+  header button.primary { background:var(--ink); color:#f4ece0; border-color:var(--ink); }
+
+  main { max-width:1380px; margin:0 auto; padding:24px 22px 64px; }
+
+  /* Sort + density controls (Steve's standing rule) */
+  .controls {
+    display:flex; gap:18px; align-items:center; padding:10px 14px;
+    background:#fff; border:1px solid var(--line); border-radius:8px;
+    margin-bottom:18px; flex-wrap:wrap;
+  }
+  .controls label { font:600 10.5px var(--sans); letter-spacing:.16em;
+                    text-transform:uppercase; color:var(--faint); }
+  .controls select { padding:6px 10px; border:1px solid var(--line);
+                     border-radius:4px; background:#fff; font:13px var(--sans); }
+  .controls input[type=range] { vertical-align:middle; width:200px; }
+  .controls .density-val { font:600 12px ui-monospace,Menlo,monospace;
+                           color:var(--faint); min-width:32px; display:inline-block; }
+  .controls .stat { margin-left:auto; font:600 11px var(--sans);
+                    letter-spacing:.08em; color:var(--faint); }
+
+  /* Pair card layout */
+  .grid {
+    display:grid; grid-template-columns:repeat(auto-fill,minmax(var(--card-min,520px),1fr));
+    gap:20px;
+  }
+  .pair {
+    background:var(--card); border:1px solid var(--line); border-radius:10px;
+    padding:14px; display:flex; flex-direction:column; gap:12px;
+    transition:box-shadow .2s;
+  }
+  .pair:hover { box-shadow:0 6px 24px rgba(0,0,0,.08); }
+  .pair-row {
+    display:grid; grid-template-columns:1fr 1fr; gap:14px; align-items:start;
+  }
+  .pane {
+    display:flex; flex-direction:column; gap:6px; min-width:0;
+  }
+  .pane h4 {
+    margin:0; font:600 10.5px var(--sans); letter-spacing:.18em;
+    text-transform:uppercase; color:var(--faint);
+  }
+  .pane .label-seed { color:#a04a1f; }
+  .pane .label-out  { color:#1f6a2c; }
+  .pane .img-wrap {
+    aspect-ratio:1; background:#f3eee2; border-radius:6px; overflow:hidden;
+    display:flex; align-items:center; justify-content:center;
+    border:1px solid var(--line);
+  }
+  .pane .img-wrap img { width:100%; height:100%; object-fit:cover; display:block; }
+  .pane .img-wrap.no-img { color:#bba; font-size:11px; }
+  .pane .meta-row { font-size:11.5px; color:var(--ink); line-height:1.5; word-break:break-word; }
+  .pane .meta-row strong { color:var(--faint); font-weight:600; }
+  .pane .vendor-tag {
+    display:inline-block; padding:2px 7px; background:#fff4d8;
+    border:1px solid #e1c674; border-radius:3px;
+    font:600 10.5px var(--sans); letter-spacing:.04em; color:#7a5a16;
+  }
+  .pane .swatches { display:flex; gap:4px; margin-top:4px; flex-wrap:wrap; }
+  .pane .swatch { width:18px; height:18px; border-radius:3px;
+                  border:1px solid rgba(0,0,0,.15); }
+
+  .pair-foot {
+    display:flex; gap:10px; align-items:center; padding-top:8px;
+    border-top:1px solid var(--line); font-size:11.5px; color:var(--faint);
+  }
+  .pair-foot .design-link {
+    color:var(--ink); text-decoration:none; font-weight:600;
+    padding:6px 12px; border:1px solid var(--ink); border-radius:14px;
+  }
+  .pair-foot .regen-btn {
+    padding:6px 12px; border:1px solid var(--gold); color:#7a5a16;
+    background:#fff; border-radius:14px; cursor:pointer; font:600 11px var(--sans);
+    letter-spacing:.06em; text-transform:uppercase;
+  }
+  .pair-foot .regen-btn:hover { background:#fff4d8; }
+  .pair-foot .published-pill {
+    margin-left:auto; padding:3px 8px; border-radius:3px;
+    font:600 10px var(--sans); letter-spacing:.08em; text-transform:uppercase;
+  }
+  .published-pill.yes { background:#d8f0d8; color:#1f5a1f; }
+  .published-pill.no  { background:#f5d8d8; color:#7a1f1f; }
+
+  .empty { padding:60px 20px; text-align:center; color:var(--faint); font:14px var(--sans); }
+  .loading { padding:60px 20px; text-align:center; color:var(--faint); }
+</style>
+</head>
+<body>
+
+<header>
+  <h1>Inspirations</h1>
+  <span class="meta">Real archival seeds × wallco AI-generated originals</span>
+  <div class="right">
+    <a href="/admin">← admin home</a>
+    <a href="/designs">designs grid</a>
+  </div>
+</header>
+
+<main>
+  <div class="controls">
+    <label for="sort">Sort</label>
+    <select id="sort">
+      <option value="newest">Newest</option>
+      <option value="vendor">Vendor A→Z</option>
+      <option value="sku">SKU A→Z</option>
+      <option value="published">Published first</option>
+      <option value="unpublished">Unpublished first</option>
+    </select>
+
+    <label for="density">Density</label>
+    <input id="density" type="range" min="380" max="780" step="20" value="520">
+    <span class="density-val" id="density-val">520</span>
+
+    <span class="stat" id="count-stat">loading…</span>
+  </div>
+
+  <div id="grid" class="grid"><div class="loading">Loading inspirations…</div></div>
+</main>
+
+<script>
+(function(){
+  // ── Persist sort + density per Steve's standing rule ────────────────────
+  var LS_SORT    = 'wallco.admin.inspirations.sort';
+  var LS_DENSITY = 'wallco.admin.inspirations.density';
+
+  var sortEl    = document.getElementById('sort');
+  var densityEl = document.getElementById('density');
+  var densityVal= document.getElementById('density-val');
+  var gridEl    = document.getElementById('grid');
+  var stat      = document.getElementById('count-stat');
+
+  sortEl.value    = localStorage.getItem(LS_SORT)    || 'newest';
+  densityEl.value = localStorage.getItem(LS_DENSITY) || '520';
+  applyDensity();
+
+  function applyDensity() {
+    densityVal.textContent = densityEl.value;
+    gridEl.style.setProperty('--card-min', densityEl.value + 'px');
+  }
+  densityEl.addEventListener('input', function(){
+    applyDensity();
+    localStorage.setItem(LS_DENSITY, densityEl.value);
+  });
+  sortEl.addEventListener('change', function(){
+    localStorage.setItem(LS_SORT, sortEl.value);
+    render();
+  });
+
+  // ── State ──────────────────────────────────────────────────────────────
+  var ROWS = [];
+
+  function escAttr(s){ return String(s == null ? '' : s).replace(/"/g,'&quot;').replace(/</g,'&lt;'); }
+  function escHtml(s){ return String(s == null ? '' : s).replace(/[<>&]/g, function(c){ return c=='<'?'&lt;':c=='>'?'&gt;':'&amp;'; }); }
+
+  function paletteOf(row) {
+    // First try server-side hex_codes jsonb, then fall back to palette jsonb
+    var hexes = [];
+    if (Array.isArray(row.seed_hex_codes)) {
+      hexes = row.seed_hex_codes.map(function(h){
+        if (typeof h === 'string') return h;
+        if (h && h.hex) return h.hex;
+        return null;
+      }).filter(Boolean);
+    }
+    return hexes.slice(0, 5);
+  }
+  function outPaletteOf(row) {
+    if (!Array.isArray(row.palette)) return [];
+    return row.palette.map(function(p){ return p && p.hex; }).filter(Boolean).slice(0, 5);
+  }
+
+  function pairCard(row) {
+    var seedHexes = paletteOf(row);
+    var outHexes  = outPaletteOf(row);
+
+    var seedImgHtml = row.seed_image_url
+      ? '<div class="img-wrap"><img src="' + escAttr(row.seed_image_url) + '" alt="archival seed reference" loading="lazy" referrerpolicy="no-referrer" onerror="this.parentNode.classList.add(\'no-img\');this.replaceWith(document.createTextNode(\'image unavailable\'));"></div>'
+      : '<div class="img-wrap no-img">no seed image</div>';
+    var outImgHtml = '<div class="img-wrap"><img src="' + escAttr(row.image_url || ('/designs/img/by-id/' + row.id)) + '" alt="wallco AI-generated original" loading="lazy"></div>';
+
+    var seedPaletteHtml = seedHexes.length
+      ? '<div class="swatches">' + seedHexes.map(function(h){ return '<span class="swatch" style="background:' + escAttr(h) + '" title="' + escAttr(h) + '"></span>'; }).join('') + '</div>'
+      : '';
+    var outPaletteHtml = outHexes.length
+      ? '<div class="swatches">' + outHexes.map(function(h){ return '<span class="swatch" style="background:' + escAttr(h) + '" title="' + escAttr(h) + '"></span>'; }).join('') + '</div>'
+      : '';
+
+    var pubPill = row.is_published
+      ? '<span class="published-pill yes">Published</span>'
+      : '<span class="published-pill no">Unpublished</span>';
+
+    return ''
+      + '<div class="pair">'
+      +   '<div class="pair-row">'
+      +     '<div class="pane">'
+      +       '<h4 class="label-seed">Archival seed (admin-only)</h4>'
+      +       seedImgHtml
+      +       '<div class="meta-row"><span class="vendor-tag">' + escHtml(row.inspired_by_vendor || '(no vendor)') + '</span></div>'
+      +       '<div class="meta-row"><strong>SKU:</strong> ' + escHtml(row.inspired_by_sku || '') + '</div>'
+      +       '<div class="meta-row"><strong>Title:</strong> ' + escHtml(row.seed_title || '(unknown)') + '</div>'
+      +       (row.seed_era ? '<div class="meta-row"><strong>Era:</strong> ' + escHtml(row.seed_era) + '</div>' : '')
+      +       seedPaletteHtml
+      +     '</div>'
+      +     '<div class="pane">'
+      +       '<h4 class="label-out">wallco AI original</h4>'
+      +       outImgHtml
+      +       '<div class="meta-row"><strong>Design #</strong> ' + row.id + '</div>'
+      +       '<div class="meta-row"><strong>Dominant hex:</strong> ' + escHtml(row.dominant_hex || '—') + '</div>'
+      +       outPaletteHtml
+      +     '</div>'
+      +   '</div>'
+      +   '<div class="pair-foot">'
+      +     '<a href="/design/' + row.id + '" class="design-link">Open /design/' + row.id + ' →</a>'
+      +     '<button class="regen-btn" data-sku="' + escAttr(row.inspired_by_sku || '') + '">Regenerate from same seed</button>'
+      +     pubPill
+      +   '</div>'
+      + '</div>';
+  }
+
+  function sortRows(rows, mode) {
+    var arr = rows.slice();
+    switch (mode) {
+      case 'vendor':
+        arr.sort(function(a,b){ return String(a.inspired_by_vendor||'').localeCompare(String(b.inspired_by_vendor||'')); }); break;
+      case 'sku':
+        arr.sort(function(a,b){ return String(a.inspired_by_sku||'').localeCompare(String(b.inspired_by_sku||'')); }); break;
+      case 'published':
+        arr.sort(function(a,b){ return (b.is_published?1:0) - (a.is_published?1:0); }); break;
+      case 'unpublished':
+        arr.sort(function(a,b){ return (a.is_published?1:0) - (b.is_published?1:0); }); break;
+      case 'newest':
+      default:
+        arr.sort(function(a,b){ return b.id - a.id; }); break;
+    }
+    return arr;
+  }
+
+  function render() {
+    if (!ROWS.length) {
+      gridEl.innerHTML = '<div class="empty">No inspired-by-seed designs yet. Run<br><code style="background:#fff;padding:4px 8px;border:1px solid var(--line);border-radius:4px;font-family:ui-monospace,Menlo,monospace">node scripts/inspiration_generator.js --n 5</code><br>to seed this page.</div>';
+      stat.textContent = '0 pairs';
+      return;
+    }
+    var sorted = sortRows(ROWS, sortEl.value);
+    gridEl.innerHTML = sorted.map(pairCard).join('');
+    stat.textContent = sorted.length + ' pair' + (sorted.length === 1 ? '' : 's');
+  }
+
+  gridEl.addEventListener('click', function(e){
+    var btn = e.target.closest('.regen-btn');
+    if (!btn) return;
+    var sku = btn.getAttribute('data-sku');
+    if (!sku) return;
+    btn.disabled = true;
+    btn.textContent = 'queued…';
+    fetch('/api/admin/inspirations/regenerate', {
+      method:'POST', headers:{'Content-Type':'application/json'},
+      body: JSON.stringify({ sku: sku }), credentials:'same-origin'
+    })
+      .then(function(r){ return r.json(); })
+      .then(function(d){
+        if (d && d.ok) {
+          btn.textContent = 'queued ✓';
+          setTimeout(function(){ btn.textContent = 'Regenerate from same seed'; btn.disabled = false; }, 5000);
+        } else {
+          btn.textContent = 'failed — ' + (d.error || 'try again');
+          btn.disabled = false;
+        }
+      })
+      .catch(function(){ btn.textContent = 'network error'; btn.disabled = false; });
+  });
+
+  function load() {
+    fetch('/api/admin/inspirations/list?limit=200', { credentials:'same-origin' })
+      .then(function(r){
+        if (r.status === 404) throw new Error('not_admin');
+        return r.json();
+      })
+      .then(function(d){
+        ROWS = (d && d.rows) || [];
+        render();
+      })
+      .catch(function(err){
+        if (String(err.message) === 'not_admin') {
+          gridEl.innerHTML = '<div class="empty">Admin gate failed. Hit <code>/_devlogin</code> first or include <code>?admin=&lt;ADMIN_TOKEN&gt;</code>.</div>';
+        } else {
+          gridEl.innerHTML = '<div class="empty">Failed to load: ' + escHtml(err.message) + '</div>';
+        }
+      });
+  }
+  load();
+})();
+</script>
+
+</body>
+</html>

← 3cd8e28 add /library route + APIs — surface 57k professional vendor  ·  back to Wallco Ai  ·  data: initial inspiration-audit.jsonl from n=5 smoke test 2b2eb5c →