← 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
A public/admin/inspirations.html
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,'"').replace(/</g,'<'); }
+ function escHtml(s){ return String(s == null ? '' : s).replace(/[<>&]/g, function(c){ return c=='<'?'<':c=='>'?'>':'&'; }); }
+
+ 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=<ADMIN_TOKEN></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 →