[object Object]

← back to Wallco Ai

drunk-curator: two-pane hot-or-not — flat tile left + instant CSS room-setting (tiled wall + floor + furniture silhouette) right, so designs are judged as real wallpaper

dabde5152c5cc99514d2b5eb1c5e8babe9c55420 · 2026-05-27 11:02:00 -0700 · Steve Abrams

Files touched

Diff

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

    drunk-curator: two-pane hot-or-not — flat tile left + instant CSS room-setting (tiled wall + floor + furniture silhouette) right, so designs are judged as real wallpaper
---
 public/admin/drunk-curator.html | 104 ++++++++++++++++++++++------------------
 1 file changed, 58 insertions(+), 46 deletions(-)

diff --git a/public/admin/drunk-curator.html b/public/admin/drunk-curator.html
index ebeceda..b7a6a70 100644
--- a/public/admin/drunk-curator.html
+++ b/public/admin/drunk-curator.html
@@ -8,83 +8,95 @@
   :root{--bg:#0d0e10;--panel:#17191b;--line:#2a2e31;--ink:#eef1f3;--mut:#9aa3ab;--hot:#ff4d6d;--not:#5a6470;}
   *{box-sizing:border-box;}
   body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;overflow:hidden;}
-  header{display:flex;align-items:center;gap:16px;padding:12px 20px;border-bottom:1px solid var(--line);}
+  header{display:flex;align-items:center;gap:16px;padding:11px 20px;border-bottom:1px solid var(--line);}
   h1{font-size:16px;margin:0;font-weight:650;} h1 small{color:var(--mut);font-weight:400;}
   .stats{margin-left:auto;display:flex;gap:18px;font-size:13px;color:var(--mut);font-variant-numeric:tabular-nums;}
-  .stats b{color:var(--ink);}
-  .streak{color:var(--hot);}
-  .stage{display:flex;flex-direction:column;align-items:center;justify-content:center;height:calc(100vh - 58px);gap:14px;padding:14px;}
-  .card{position:relative;width:min(62vh,560px);max-width:92vw;}
-  .card img{width:100%;aspect-ratio:1/1;object-fit:cover;object-position:center 12%;border-radius:14px;background:#000;border:1px solid var(--line);box-shadow:0 12px 40px rgba(0,0,0,.5);}
-  .meta{display:flex;justify-content:space-between;margin-top:8px;font-size:11.5px;color:var(--mut);font-variant-numeric:tabular-nums;}
-  .prompt{font-size:11px;color:var(--mut);max-width:min(62vh,560px);max-height:34px;overflow:hidden;text-align:center;cursor:pointer;}
+  .stats b{color:var(--ink);} .streak{color:var(--hot);}
+  .stage{display:flex;flex-direction:column;align-items:center;height:calc(100vh - 56px);padding:14px;gap:10px;}
+  .panes{display:grid;grid-template-columns:1fr 1fr;gap:16px;width:min(96vw,1100px);height:min(60vh,540px);}
+  .pane{position:relative;border-radius:14px;overflow:hidden;border:1px solid var(--line);background:#000;}
+  .pane .tag{position:absolute;top:8px;left:10px;z-index:3;font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:#fff;background:rgba(0,0,0,.45);padding:2px 8px;border-radius:20px;backdrop-filter:blur(4px);}
+  /* left: flat tile */
+  .tile{width:100%;height:100%;object-fit:cover;object-position:center 12%;display:block;}
+  /* right: CSS room setting */
+  .room{position:relative;width:100%;height:100%;background:#cfc7ba;}
+  .wall{position:absolute;inset:0 0 24% 0;background-repeat:repeat;background-position:center top;background-size:24% auto;filter:brightness(.98);}
+  .wall::after{content:"";position:absolute;inset:0;background:radial-gradient(120% 90% at 50% 35%,transparent 55%,rgba(0,0,0,.28));} /* gentle room light */
+  .floor{position:absolute;left:0;right:0;bottom:0;height:24%;background:linear-gradient(#b9a684,#8d7a5b);}
+  .baseboard{position:absolute;left:0;right:0;bottom:24%;height:3.2%;background:#efe9df;border-top:1px solid rgba(0,0,0,.12);box-shadow:0 2px 5px rgba(0,0,0,.18);}
+  .furn{position:absolute;left:50%;bottom:24%;transform:translateX(-50%);width:64%;z-index:2;filter:drop-shadow(0 8px 14px rgba(0,0,0,.32));}
+  .meta{display:flex;justify-content:space-between;width:min(96vw,1100px);font-size:11.5px;color:var(--mut);font-variant-numeric:tabular-nums;}
+  .prompt{font-size:11px;color:var(--mut);width:min(96vw,1100px);max-height:30px;overflow:hidden;text-align:center;cursor:pointer;}
   .prompt.open{max-height:none;}
   .btns{display:flex;gap:18px;}
-  button{border:0;border-radius:12px;padding:14px 34px;font-size:17px;font-weight:700;cursor:pointer;transition:transform .08s;}
+  button{border:0;border-radius:12px;padding:13px 34px;font-size:17px;font-weight:700;cursor:pointer;transition:transform .08s;}
   button:active{transform:scale(.94);}
   .not{background:var(--not);color:#fff;} .hot{background:var(--hot);color:#fff;}
   .hint{font-size:11px;color:var(--mut);}
-  .done{text-align:center;color:var(--mut);}
-  .done h2{color:var(--ink);}
-  .flash{position:fixed;inset:0;pointer-events:none;opacity:0;transition:opacity .25s;font-size:120px;display:flex;align-items:center;justify-content:center;}
+  .done{text-align:center;color:var(--mut);margin-top:18vh;} .done h2{color:var(--ink);}
+  .flash{position:fixed;inset:0;pointer-events:none;opacity:0;transition:opacity .25s;font-size:140px;display:flex;align-items:center;justify-content:center;z-index:9;}
   .flash.show{opacity:1;}
 </style>
 </head>
 <body>
 <header>
-  <h1>🔥 Drunk-Animals <small>· Hot or Not</small></h1>
+  <h1>🔥 Drunk-Animals <small>· Hot or Not · tile + room</small></h1>
   <div class="stats">
-    <span>seen <b id="seen">0</b></span>
-    <span>🔥 hot <b id="hot">0</b></span>
-    <span>👎 not <b id="not">0</b></span>
-    <span class="streak" id="streak"></span>
+    <span>seen <b id="seen">0</b></span><span>🔥 hot <b id="hot">0</b></span>
+    <span>👎 not <b id="not">0</b></span><span class="streak" id="streak"></span>
     <span>left <b id="left">…</b></span>
   </div>
 </header>
 <div class="stage" id="stage"><div class="done">loading…</div></div>
 <div class="flash" id="flash"></div>
 
+<!-- furniture silhouette (console + lamp + vase), muted so it never fights the wallpaper -->
+<svg style="display:none"><symbol id="console" viewBox="0 0 320 150">
+  <g fill="#2c2620"><rect x="40" y="60" width="240" height="14" rx="3"/>
+  <rect x="52" y="74" width="9" height="62"/><rect x="259" y="74" width="9" height="62"/>
+  <!-- lamp -->
+  <rect x="92" y="22" width="6" height="38"/><path d="M73 22 h44 l-9 -20 h-26 z"/>
+  <!-- vase + stems -->
+  <path d="M212 36 q-10 24 0 24 h22 q10 0 0 -24 z"/>
+  <path d="M220 36 q-6 -22 -16 -30 M226 36 q2 -24 14 -30 M223 36 q0 -26 0 -34" stroke="#2c2620" stroke-width="2.4" fill="none" stroke-linecap="round"/>
+  </g></symbol></svg>
+
 <script>
 const $=s=>document.querySelector(s);
 let QUEUE=[], i=0, hot=0, not=0, streak=0;
-
 function fmtDate(iso){ if(!iso)return''; try{return new Date(iso).toLocaleString(undefined,{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}catch{return iso;} }
-function flash(emoji){ const f=$('#flash'); f.textContent=emoji; f.classList.add('show'); setTimeout(()=>f.classList.remove('show'),250); }
-
+function flash(e){ const f=$('#flash'); f.textContent=e; f.classList.add('show'); setTimeout(()=>f.classList.remove('show'),250); }
 function render(){
   $('#seen').textContent=hot+not; $('#hot').textContent=hot; $('#not').textContent=not;
   $('#left').textContent=Math.max(0,QUEUE.length-i);
-  $('#streak').textContent = streak>=3 ? `🔥×${streak} streak` : '';
-  if(i>=QUEUE.length){
-    $('#stage').innerHTML=`<div class="done"><h2>Pile cleared 🎉</h2><div>${hot} published · ${not} sent to defect registry</div><div class="hint" style="margin-top:10px">Generate more, then refresh.</div></div>`;
-    return;
-  }
-  const d=QUEUE[i];
+  $('#streak').textContent= streak>=3?`🔥×${streak} streak`:'';
+  if(i>=QUEUE.length){ $('#stage').innerHTML=`<div class="done"><h2>Pile cleared 🎉</h2><div>${hot} published · ${not} → defect registry</div><div class="hint" style="margin-top:10px">Generate more, then refresh.</div></div>`; return; }
+  const d=QUEUE[i], src=`/designs/img/by-id/${d.id}`;
   $('#stage').innerHTML=`
-    <div class="card"><img src="/designs/img/by-id/${d.id}" alt="design ${d.id}" onerror="this.style.opacity=.25"></div>
-    <div class="meta" style="width:min(62vh,560px)"><span>#${d.id} · ${d.category||''}</span><span>🕓 ${fmtDate(d.created_at)}</span></div>
-    <div class="prompt" onclick="this.classList.toggle('open')">${(d.prompt||'').replace(/</g,'&lt;').slice(0,240)}</div>
-    <div class="btns">
-      <button class="not" onclick="decide('not')">👎 NOT</button>
-      <button class="hot" onclick="decide('hot')">🔥 HOT</button>
+    <div class="panes">
+      <div class="pane"><span class="tag">flat tile</span><img class="tile" src="${src}" onerror="this.style.opacity=.25"></div>
+      <div class="pane"><span class="tag">room setting</span>
+        <div class="room">
+          <div class="wall" style="background-image:url('${src}')"></div>
+          <div class="baseboard"></div><div class="floor"></div>
+          <svg class="furn"><use href="#console"></use></svg>
+        </div>
+      </div>
     </div>
-    <div class="hint">← / N = not · → / H = hot · click image prompt to expand</div>`;
+    <div class="meta"><span>#${d.id} · ${d.category||''}</span><span>🕓 ${fmtDate(d.created_at)}</span></div>
+    <div class="prompt" onclick="this.classList.toggle('open')">${(d.prompt||'').replace(/</g,'&lt;').slice(0,240)}</div>
+    <div class="btns"><button class="not" onclick="decide('not')">👎 NOT</button><button class="hot" onclick="decide('hot')">🔥 HOT</button></div>
+    <div class="hint">← / N = not · → / H = hot · left = flat repeat, right = on a wall</div>`;
 }
-async function decide(verdict){
-  if(i>=QUEUE.length) return;
-  const d=QUEUE[i];
-  if(verdict==='hot'){ hot++; streak++; flash('🔥'); } else { not++; streak=0; flash('👎'); }
+async function decide(v){
+  if(i>=QUEUE.length) return; const d=QUEUE[i];
+  if(v==='hot'){hot++;streak++;flash('🔥');}else{not++;streak=0;flash('👎');}
   i++; render();
-  try{ await fetch('/api/admin/drunk/decide',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:d.id,verdict})}); }catch{}
+  try{ await fetch('/api/admin/drunk/decide',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:d.id,verdict:v})}); }catch{}
 }
-document.addEventListener('keydown',e=>{
-  if(e.key==='ArrowRight'||e.key.toLowerCase()==='h') decide('hot');
-  else if(e.key==='ArrowLeft'||e.key.toLowerCase()==='n') decide('not');
-});
-(async function(){
-  const r=await fetch('/api/admin/drunk/list'); const j=await r.json();
-  QUEUE=j.items||[]; render();
-})();
+document.addEventListener('keydown',e=>{ const k=e.key.toLowerCase();
+  if(e.key==='ArrowRight'||k==='h')decide('hot'); else if(e.key==='ArrowLeft'||k==='n')decide('not'); });
+(async function(){ const r=await fetch('/api/admin/drunk/list'); QUEUE=(await r.json()).items||[]; render(); })();
 </script>
 </body>
 </html>

← c8123d2 add /admin/drunk-curator hot-or-not picker: HOT publishes, N  ·  back to Wallco Ai  ·  wallco curator: add Collection picker (distinct base lines) dad5b6d →