[object Object]

← back to Wallco Ai

designer-studio: wire Elements panel to the REAL visual motif library — /api/studio/elements now returns extracted silhouette cutouts (data/elements) as clickable thumbnails grouped by category; selection by motif id resolved to motifs/category text in compose

0b990f6d7608248ba5329a908017bc13a7382e93 · 2026-05-31 16:21:38 -0700 · Steve Abrams

Files touched

Diff

commit 0b990f6d7608248ba5329a908017bc13a7382e93
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun May 31 16:21:38 2026 -0700

    designer-studio: wire Elements panel to the REAL visual motif library — /api/studio/elements now returns extracted silhouette cutouts (data/elements) as clickable thumbnails grouped by category; selection by motif id resolved to motifs/category text in compose
---
 server.js | 69 +++++++++++++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 54 insertions(+), 15 deletions(-)

diff --git a/server.js b/server.js
index fab0b24..19538ac 100644
--- a/server.js
+++ b/server.js
@@ -21490,8 +21490,28 @@ app.get('/api/studio/elements', (req, res) => {
   // pasted from the admin generator and silently blanked the /designer-studio
   // Elements panel for every visitor (frontend did .forEach on the 404 object).
   try {
-    const raw = psqlQuery(`SELECT COALESCE(json_agg(t ORDER BY t.category, t.name), '[]'::json) FROM
-      (SELECT id, category, name, prompt_phrase FROM wallco_element_lexicon) t;`);
+    // REAL visual element library (2026-05-31): elements are the extracted
+    // silhouette/cutout MOTIFS of clean published designs (data/elements/
+    // <id>_motif.png — subject alpha-trimmed on transparent BG), grouped by
+    // category, reusable/combinable into new products. Replaces the old text
+    // vocabulary. Thumbnail = /elements/img/<id>_motif.png (serves the bake,
+    // falls back to by-id image). Capped per category so the panel stays
+    // browsable; all source data is already public (published designs).
+    const raw = psqlQuery(`SELECT COALESCE(json_agg(t),'[]'::json) FROM (
+      SELECT id, category, motif_url FROM (
+        SELECT id, category,
+               '/elements/img/' || id || '_motif.png' AS motif_url,
+               row_number() OVER (PARTITION BY category ORDER BY id DESC) AS rn
+        FROM all_designs
+        WHERE is_published = TRUE
+          AND (user_removed IS NULL OR user_removed = FALSE)
+          AND local_path IS NOT NULL
+          AND (notes IS NULL OR (
+            notes NOT LIKE '%EDGES_FAIL:%' AND notes NOT LIKE '%BLEED_GHOST_REGEN%'
+            AND notes NOT LIKE '%SETTLEMENT BLOCK%' AND notes NOT LIKE '%BLEED_GUARD:%'))
+      ) s WHERE rn <= 30
+      ORDER BY category, id DESC
+    ) t;`);
     res.json(JSON.parse(raw || '[]'));
   } catch (e) { res.status(500).json({ error: e.message }); }
 });
@@ -21707,7 +21727,18 @@ app.post('/api/studio/compose', (req, res) => {
     const artists = [...new Set(board.flatMap(b => [b.artist].filter(Boolean)))];
     const styles  = [...new Set(board.flatMap(b => (b.style_tags || []).concat(b.selected_elements || []).filter(Boolean)))];
     const colors  = [...new Set(board.flatMap(b => b.color_hex_seeds || []))].slice(0, 6);
-    const elems   = [...new Set((board.flatMap(b => b.selected_elements || [])).concat(extra_elements))];
+    // extra_elements are now VISUAL motif IDs (element library 2026-05-31) or
+    // legacy text. Resolve a numeric id → the design's motifs/category text so
+    // the prompt reads "featuring crane, botanical" not "featuring 54902".
+    const _resolveEl = (v) => {
+      if (!/^\d+$/.test(String(v))) return String(v);
+      try {
+        const r = psqlQuery(`SELECT COALESCE(NULLIF(array_to_string(motifs,', '),''), category, 'motif')
+          FROM all_designs WHERE id=${parseInt(v,10)} LIMIT 1;`).trim();
+        return r || String(v);
+      } catch { return String(v); }
+    };
+    const elems   = [...new Set((board.flatMap(b => b.selected_elements || [])).concat(extra_elements).map(_resolveEl))];
 
     const promptParts = [
       'A seamless wallpaper pattern',
@@ -22041,30 +22072,38 @@ async function loadRefs(){
 
 // Renders the elements-list pills with active/inactive styling based on EXTRA.
 // Pulled out so toggleExtra can re-render to show visual selected state.
+// Elements are now VISUAL motif cutouts (2026-05-31): each is the extracted
+// silhouette of a design, served at e.motif_url. Render as clickable thumbnails
+// grouped by category; selection holds the motif id. EL_BY_ID maps id->element
+// so the selected strip + compose can resolve the image.
+window.EL_BY_ID = {};
 function renderElementsList() {
   var byCat = window._ELEMENTS_BY_CAT || {};
+  window.EL_BY_ID = {};
   document.getElementById('elements-list').innerHTML = Object.keys(byCat).sort().map(function(c){
-    return '<div style="margin-bottom:6px"><div class="eyebrow" style="margin-bottom:3px">'+c+'</div><div style="display:flex;flex-wrap:wrap">'+byCat[c].map(function(e){
-      var on = EXTRA.has(e.name);
-      var bg = on ? '#1a1a1a' : '#fff';
-      var fg = on ? '#fff' : '#333';
-      var bd = on ? '#1a1a1a' : '#ccc';
-      return '<button data-extra="'+e.name+'" onclick="toggleExtra(\\''+e.name+'\\')" style="padding:2px 7px;font-size:10px;border-radius:9px;border:1px solid '+bd+';background:'+bg+';color:'+fg+';cursor:pointer;margin:1px;transition:all .12s">'+e.name+'</button>';
+    return '<div style="margin-bottom:8px"><div class="eyebrow" style="margin-bottom:3px">'+c+'</div><div style="display:flex;flex-wrap:wrap;gap:4px">'+byCat[c].map(function(e){
+      window.EL_BY_ID[String(e.id)] = e;
+      var on = EXTRA.has(String(e.id));
+      return '<button data-extra="'+e.id+'" title="'+c+' · motif #'+e.id+'" onclick="toggleExtra(\\''+e.id+'\\')" style="width:46px;height:46px;padding:0;border-radius:8px;border:2px solid '+(on?'#1a1a1a':'#e0e0e0')+';background:#faf8f2 url('+e.motif_url+') center/cover no-repeat;cursor:pointer;box-shadow:'+(on?'0 0 0 2px #1a1a1a inset':'none')+';transition:all .12s"></button>';
     }).join('')+'</div></div>';
   }).join('');
 }
 
 function renderExtra() {
-  var html = Array.from(EXTRA).map(function(e){
-    return pill(e + ' ✕', true, "toggleExtra('"+e+"')");
-  }).join('') || '<span style="color:#999;font-size:12px">No extra elements selected yet — click pills above.</span>';
+  var ids = Array.from(EXTRA);
+  var html = ids.map(function(id){
+    var e = (window.EL_BY_ID||{})[String(id)];
+    var url = e ? e.motif_url : '/elements/img/'+id+'_motif.png';
+    return '<button title="motif #'+id+' — click to remove" onclick="toggleExtra(\\''+id+'\\')" style="width:40px;height:40px;padding:0;border-radius:7px;border:1px solid #1a1a1a;background:#faf8f2 url('+url+') center/cover no-repeat;cursor:pointer;margin:2px;position:relative"></button>';
+  }).join('') || '<span style="color:#999;font-size:12px">No elements selected yet — click a motif cutout above to reuse it.</span>';
   document.getElementById('extra-elements-pills').innerHTML = html;
 }
 
-window.toggleExtra = function(name){
-  if (EXTRA.has(name)) EXTRA.delete(name); else EXTRA.add(name);
+window.toggleExtra = function(id){
+  id = String(id);
+  if (EXTRA.has(id)) EXTRA.delete(id); else EXTRA.add(id);
   renderExtra();
-  renderElementsList(); // re-render so the pill that was just clicked flips state
+  renderElementsList(); // re-render so the tile that was just clicked flips state
 };
 
 window.addArtist = function(a){

← 25e05d1 Publish drunk-animals batch 12 (39443-39447) — SQL-direct, s  ·  back to Wallco Ai  ·  tests: fix playwright integration specs against current UI ( 3964faf →