[object Object]

← back to Wallco Ai

Fix design concierge returning 0 results

126ee4fa740e8f90c2c953592e7bb7aacf3135e5 · 2026-06-23 09:42:19 -0700 · Steve Abrams

toolSearchDesigns had three compounding bugs that all returned empty-but-200:
- generator='replicate' scope filter excluded ~84% of the published catalog
  (incl. every comfy/gemini/wallpapersback row and ALL damask rows)
- exact category= match against a vocabulary (floral/damask/...) the catalog's
  real taxonomy (drunk-animals/chinoiserie/...) never uses -> always 0
- image_url built from local_path basename 404'd; switched to /designs/img/by-id/:id

Now scopes to brand='wallco.ai' AND is_published, treats category as a soft
keyword (category OR prompt ILIKE), enlarges the candidate pool when a color
filter is active, and fixes hueNameToDeg returning 0(=rose) for unknown colors.

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

Files touched

Diff

commit 126ee4fa740e8f90c2c953592e7bb7aacf3135e5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 23 09:42:19 2026 -0700

    Fix design concierge returning 0 results
    
    toolSearchDesigns had three compounding bugs that all returned empty-but-200:
    - generator='replicate' scope filter excluded ~84% of the published catalog
      (incl. every comfy/gemini/wallpapersback row and ALL damask rows)
    - exact category= match against a vocabulary (floral/damask/...) the catalog's
      real taxonomy (drunk-animals/chinoiserie/...) never uses -> always 0
    - image_url built from local_path basename 404'd; switched to /designs/img/by-id/:id
    
    Now scopes to brand='wallco.ai' AND is_published, treats category as a soft
    keyword (category OR prompt ILIKE), enlarges the candidate pool when a color
    filter is active, and fixes hueNameToDeg returning 0(=rose) for unknown colors.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 src/chat.js | 50 ++++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 40 insertions(+), 10 deletions(-)

diff --git a/src/chat.js b/src/chat.js
index 4ff2de2..2aecb34 100644
--- a/src/chat.js
+++ b/src/chat.js
@@ -103,13 +103,26 @@ function extractToolCall(text) {
 // -------- Tool implementations --------
 
 function toolSearchDesigns({ category, hue, hex_near, motif_kw, limit = 12 } = {}) {
-  const where = [`generator='replicate'`];
-  if (category) where.push(`category=${esc(category)}`);
+  // Scope to the LIVE wallco.ai catalog only — the same brand + publish gate the
+  // public /api/designs grid uses. The previous `generator='replicate'` filter
+  // was the primary bug: it excluded ~84% of the published catalog (incl. every
+  // 'comfy' / 'gemini-*' / 'wallpapersback.com' row and EVERY damask row), so the
+  // concierge returned 0 results for every query while still answering HTTP 200.
+  const where = [`brand='wallco.ai'`, `is_published = TRUE`];
+  // The concierge's category vocabulary (floral|geometric|damask|mixed) does NOT
+  // match the catalog's real taxonomy (drunk-animals, chinoiserie, stripe, …), so
+  // an exact `category=` match also always yielded 0. Treat category as a SOFT
+  // keyword: match the category column OR the prompt text.
+  if (category) where.push(`(category ILIKE ${esc('%' + category + '%')} OR prompt ILIKE ${esc('%' + category + '%')})`);
   if (motif_kw) where.push(`prompt ILIKE ${esc('%' + motif_kw + '%')}`);
   const lim = Math.min(parseInt(limit, 10) || 12, 30);
+  // A hue / hex_near filter is applied in JS AFTER the query, so pull a larger
+  // candidate pool first — otherwise the color filter starves on the newest N
+  // rows and silently returns nothing.
+  const poolLim = (hue || hex_near) ? 400 : lim;
   const sql = `SELECT COALESCE(json_agg(t),'[]'::json) FROM (
-    SELECT id, kind, category, dominant_hex, prompt, local_path
-    FROM spoon_all_designs WHERE ${where.join(' AND ')} ORDER BY created_at DESC LIMIT ${lim}) t;`;
+    SELECT id, kind, category, dominant_hex, prompt
+    FROM spoon_all_designs WHERE ${where.join(' AND ')} ORDER BY created_at DESC LIMIT ${poolLim}) t;`;
   const rows = JSON.parse(psql(sql) || '[]');
   // Hex-near filter (post-query): Lab distance in JS is overkill; use Euclidean in RGB
   let filtered = rows;
@@ -121,14 +134,23 @@ function toolSearchDesigns({ category, hue, hex_near, motif_kw, limit = 12 } = {
       .sort((a, b) => a._dist - b._dist);
   } else if (hue) {
     const huedeg = hueNameToDeg(hue);
-    filtered = rows.filter(r => withinHue(r.dominant_hex, huedeg, 30));
+    // huedeg === null → unknown color name; skip the hue filter rather than
+    // defaulting to 0° (which silently filtered everything down to red/rose).
+    if (huedeg != null) {
+      const hueMatch = rows.filter(r => withinHue(r.dominant_hex, huedeg, 30));
+      // Hue is fuzzy — if nothing falls inside the cone, fall back to the
+      // keyword/category matches so the user still sees relevant designs.
+      filtered = hueMatch.length ? hueMatch : rows;
+    }
   }
   return filtered.slice(0, lim).map(r => ({
     id: r.id,
     title: titleFromPrompt(r.prompt),
     category: r.category,
     hex: r.dominant_hex,
-    image_url: r.local_path ? '/designs/img/' + path.basename(r.local_path) : null
+    // Resolve via the by-id route. local_path basenames are NOT reliably served
+    // on every host (they 404'd here), but /designs/img/by-id/:id always resolves.
+    image_url: '/designs/img/by-id/' + r.id
   }));
 }
 
@@ -144,8 +166,16 @@ function hexToRgb(h) {
 }
 function rgbDist(a, b) { return Math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2 + (a[2]-b[2])**2); }
 function hueNameToDeg(name) {
-  const map = { rose:0, amber:25, honey:45, olive:70, sage:120, marine:180, sapphire:220, blue:220, mauve:270, plum:310, gold:45 };
-  return map[(name || '').toLowerCase()] ?? 0;
+  const map = {
+    rose:0, red:0, copper:20, amber:25, bronze:35, honey:45, gold:45, brass:45,
+    olive:70, sage:120, green:120, emerald:150, teal:180, marine:180,
+    sapphire:220, blue:220, navy:220, indigo:230, mauve:270, purple:280,
+    plum:310, magenta:320
+  };
+  // Unknown color name → null (caller skips the hue filter). Returning 0 here
+  // silently collapsed every unknown hue down to red/rose.
+  const deg = map[(name || '').toLowerCase()];
+  return deg === undefined ? null : deg;
 }
 function withinHue(hex, deg, tolerance) {
   if (!hex) return false;
@@ -278,8 +308,8 @@ ALWAYS:
 - Write a 1-2 sentence preamble before the JSON block to explain what you're about to do.
 - Keep the user's stated colors and motifs in the generate prompt.
 
-Categories: floral, geometric, damask, mixed.
-Color hue names you can use: rose, amber, honey, olive, sage, marine, sapphire, mauve, plum.`;
+Categories (soft keywords — also matched against the design's description): floral, geometric, damask, mixed, chinoiserie, stripe, scenic, mural.
+Color hue names you can use: rose, copper, amber, bronze, honey, gold, brass, olive, sage, emerald, teal, marine, sapphire, navy, indigo, mauve, purple, plum, magenta.`;
 
 const SYS_DESIGN = (d) => `You are the wallco.ai design concierge, in dialogue about ONE specific design.
 

← 10c7ad7 auto-save: 2026-06-23T02:49:11 (1 files) — data/logs/gate-re  ·  back to Wallco Ai  ·  Concierge search reads in-memory designs.json cache, not Pos 3a7458b →