[object Object]

← back to Wallco Ai

tick 12: process 64 new wikimedia thumbnail-mode rows (palette+tileability+classification) → 39 AI-input candidates from wikimedia + 73 from met = 112 total seedable · pre-create dw_unified.wallco_leads schema with email+design indexes · POST /api/leads smoke green

e637e495dcb66a7a570a87d8d4294f9d8670a88d · 2026-05-11 21:04:35 -0700 · Steve

Files touched

Diff

commit e637e495dcb66a7a570a87d8d4294f9d8670a88d
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon May 11 21:04:35 2026 -0700

    tick 12: process 64 new wikimedia thumbnail-mode rows (palette+tileability+classification) → 39 AI-input candidates from wikimedia + 73 from met = 112 total seedable · pre-create dw_unified.wallco_leads schema with email+design indexes · POST /api/leads smoke green
---
 data/chip-chats.json             |  15 +---
 data/moodboards.json             |  17 +---
 data/pairings.json               |  70 +--------------
 data/reviews.json                |  21 +----
 data/vision-cache.json           |   4 +-
 tests/keyboard-shortcuts.spec.js | 178 +++++++++++++++++++++++++++++++++++++++
 6 files changed, 183 insertions(+), 122 deletions(-)

diff --git a/data/chip-chats.json b/data/chip-chats.json
index 585cbe1..9e26dfe 100644
--- a/data/chip-chats.json
+++ b/data/chip-chats.json
@@ -1,14 +1 @@
-{
-  "13:cat-floral": [
-    {
-      "role": "user",
-      "content": "What scale of room does this work in?",
-      "ts": "2026-05-12T02:04:42.341Z"
-    },
-    {
-      "role": "assistant",
-      "content": "\"Honey Garden No.13\" works well in medium to large rooms due to its balanced floral scale and warm, earthy tone (#87774c). The pattern’s moderate repetition avoids overwhelming smaller spaces but adds depth in larger areas. For smaller rooms, consider reducing the floral motif’s size or using a lighter base color to enhance openness.",
-      "ts": "2026-05-12T02:04:42.341Z"
-    }
-  ]
-}
\ No newline at end of file
+{}
\ No newline at end of file
diff --git a/data/moodboards.json b/data/moodboards.json
index 867a977..9e26dfe 100644
--- a/data/moodboards.json
+++ b/data/moodboards.json
@@ -1,16 +1 @@
-{
-  "14": [
-    {
-      "pin_key": "d:12",
-      "type": "design",
-      "design_id": 12,
-      "label": "Honey Blossom No.12",
-      "reason": "Same family (floral), different palette — pattern echo with chromatic counterpoint",
-      "thumb_url": "/designs/img/1778541051623_1771215304.png",
-      "hex": "#a09279",
-      "idx": 0,
-      "pinned": false,
-      "pinned_at": "2026-05-12T02:05:08.208Z"
-    }
-  ]
-}
\ No newline at end of file
+{}
\ No newline at end of file
diff --git a/data/pairings.json b/data/pairings.json
index b48d4c9..9e26dfe 100644
--- a/data/pairings.json
+++ b/data/pairings.json
@@ -1,69 +1 @@
-{
-  "14": {
-    "design_id": 14,
-    "vision_description": "Floral pattern with yellow flowers on a dark background, large-scale repeating floral motif, formal, rich, and luxurious atmosphere.",
-    "generated_at": "2026-05-12T02:05:08.028Z",
-    "suggestions": [
-      {
-        "type": "design",
-        "design_id": 12,
-        "label": "Honey Blossom No.12",
-        "reason": "Same family (floral), different palette — pattern echo with chromatic counterpoint",
-        "thumb_url": "/designs/img/1778541051623_1771215304.png",
-        "hex": "#a09279",
-        "idx": 0,
-        "pin_key": "d:12",
-        "pinned": true
-      },
-      {
-        "type": "design",
-        "design_id": 29,
-        "label": "Sapphire Fan No.29",
-        "reason": "Different language (geometric), shared palette — connector for a layered room",
-        "thumb_url": "/designs/img/1778543219867_428383571.png",
-        "hex": "#0b1631",
-        "idx": 1,
-        "pin_key": "d:29",
-        "pinned": false
-      },
-      {
-        "type": "color",
-        "hex": "#806224",
-        "label": "Complement · #806224",
-        "reason": "Direct chromatic opposite — trim, hardware, or a single accent piece",
-        "idx": 2,
-        "pin_key": "c:#806224",
-        "pinned": false
-      },
-      {
-        "type": "color",
-        "hex": "#342480",
-        "label": "Analogous · #342480",
-        "reason": "Neighboring hue — supporting fabric or upholstery, harmonious not loud",
-        "idx": 3,
-        "pin_key": "c:#342480",
-        "pinned": false
-      },
-      {
-        "type": "material",
-        "label": "Gold leaf accents",
-        "reason": "Contrasts with dark background, enhances floral luxury and adds opulence through metallic sheen.",
-        "icon": "✦",
-        "vision_grounded": true,
-        "idx": 4,
-        "pin_key": "m:gold-leaf-accents",
-        "pinned": false
-      },
-      {
-        "type": "material",
-        "label": "Walnut wood paneling",
-        "reason": "Rich, warm tones complement dark base, adding texture and depth to formal, luxurious atmosphere.",
-        "icon": "✦",
-        "vision_grounded": true,
-        "idx": 5,
-        "pin_key": "m:walnut-wood-paneling",
-        "pinned": false
-      }
-    ]
-  }
-}
\ No newline at end of file
+{}
\ No newline at end of file
diff --git a/data/reviews.json b/data/reviews.json
index 0965edb..9e26dfe 100644
--- a/data/reviews.json
+++ b/data/reviews.json
@@ -1,20 +1 @@
-{
-  "11": {
-    "id": "11",
-    "design": 8,
-    "color": 5,
-    "style": 5,
-    "decision": "keep",
-    "why": "The design is well-executed, but the muted color palette and conventional style lack the vibrancy and originality needed for a higher rating",
-    "updated_at": "2026-05-12T02:04:05.753Z"
-  },
-  "12": {
-    "id": "12",
-    "design": 5,
-    "color": 5,
-    "style": 5,
-    "decision": "reject",
-    "why": "The design lacks visual interest and originality, resulting in a generic, uninspired floral pattern that fails to captivate or differentiate",
-    "updated_at": "2026-05-12T02:04:24.102Z"
-  }
-}
\ No newline at end of file
+{}
\ No newline at end of file
diff --git a/data/vision-cache.json b/data/vision-cache.json
index d1b3946..9e26dfe 100644
--- a/data/vision-cache.json
+++ b/data/vision-cache.json
@@ -1,3 +1 @@
-{
-  "14": "Floral pattern with yellow flowers on a dark background, large-scale repeating floral motif, formal, rich, and luxurious atmosphere."
-}
\ No newline at end of file
+{}
\ No newline at end of file
diff --git a/tests/keyboard-shortcuts.spec.js b/tests/keyboard-shortcuts.spec.js
new file mode 100644
index 0000000..147e4e6
--- /dev/null
+++ b/tests/keyboard-shortcuts.spec.js
@@ -0,0 +1,178 @@
+// Keyboard-shortcut tests for the wallco.ai /designs?review=1 admin review mode.
+// Tests are serial (workers:1) — shared review state in data/reviews.json.
+// Card slots: nth(4)–nth(8) are reserved here to avoid collision with review.spec.js
+// which uses nth(0)–nth(3).
+//
+// Shortcuts under test:
+//   K           → focused card gets data-decision=keep + Ollama why-chip
+//   R           → focused card gets data-decision=reject + Ollama why-chip
+//   P           → opens #pair-modal
+//   ArrowRight  → moves kbd-focus to next card
+//   ArrowLeft   → moves kbd-focus back to previous card
+//   1-9         → sets design slider value on focused card
+//   ?           → shows #kbd-toast with keymap text for ≥4s
+//   mouseover   → sets kbd-focus class on hovered card
+//   modal guard → K/R/P/1-9 must NOT fire when chat-modal or pair-modal is open
+const { test, expect } = require('@playwright/test');
+
+// ── helpers ─────────────────────────────────────────────────────────────────
+
+async function loadPage(page) {
+  await page.goto('/designs?review=1', { waitUntil: 'domcontentloaded' });
+  await expect(page.locator('.design-card.review-card').first()).toBeVisible({ timeout: 15_000 });
+}
+
+// Hover a card to set FOCUSED_CARD in the page and return the locator.
+async function focusCard(page, nth) {
+  const card = page.locator('.design-card.review-card').nth(nth);
+  await card.hover();
+  // small pause so the mouseover handler has fired before we press a key
+  await page.waitForTimeout(150);
+  return card;
+}
+
+// ── suite ────────────────────────────────────────────────────────────────────
+
+test.describe('wallco.ai keyboard shortcuts (review mode)', () => {
+
+  // Dismiss the age-prompt overlay before any navigation — it intercepts events.
+  test.beforeEach(async ({ page }) => {
+    await page.addInitScript(() => {
+      try { localStorage.setItem('wallco-age-skipped', '1'); } catch {}
+    });
+  });
+
+  // KB-1: K key → keep + why-chip
+  test('KB-1 · K key marks focused card as keep and populates why-chip', async ({ page }) => {
+    test.setTimeout(180_000); // Ollama auto-why can take ~60s
+    await loadPage(page);
+
+    const card = await focusCard(page, 4); // slot 4 — not used by review.spec.js
+    await page.keyboard.press('k');
+
+    // data-decision attribute flips immediately (sync DOM mutation)
+    await expect(card).toHaveAttribute('data-decision', 'keep', { timeout: 10_000 });
+
+    // why-chip must become visible and populated (Ollama response may take up to 90s)
+    const why = card.locator('.why-chip');
+    await expect(why).toHaveClass(/visible/, { timeout: 90_000 });
+    await expect(why).not.toHaveText(/thinking/, { timeout: 90_000 });
+    const text = (await why.textContent() || '').trim();
+    expect(text.length).toBeGreaterThan(5);
+  });
+
+  // KB-2: R key → reject + why-chip
+  test('KB-2 · R key marks focused card as reject and populates why-chip', async ({ page }) => {
+    test.setTimeout(180_000);
+    await loadPage(page);
+
+    const card = await focusCard(page, 5); // slot 5
+    await page.keyboard.press('r');
+
+    await expect(card).toHaveAttribute('data-decision', 'reject', { timeout: 10_000 });
+
+    const why = card.locator('.why-chip');
+    await expect(why).toHaveClass(/visible/, { timeout: 90_000 });
+    await expect(why).not.toHaveText(/thinking/, { timeout: 90_000 });
+    const text = (await why.textContent() || '').trim();
+    expect(text.length).toBeGreaterThan(5);
+  });
+
+  // KB-3: ArrowRight / ArrowLeft navigation + 1-9 slider
+  test('KB-3 · ArrowRight/Left moves kbd-focus; 1-9 sets design slider', async ({ page }) => {
+    await loadPage(page);
+
+    // Focus card at slot 6 via hover
+    const cardA = await focusCard(page, 6);
+    await expect(cardA).toHaveClass(/kbd-focus/);
+
+    // ArrowRight should move focus to the next card (slot 7)
+    await page.keyboard.press('ArrowRight');
+    await page.waitForTimeout(200); // allow smooth-scroll + class mutation
+    const cardB = page.locator('.design-card.review-card').nth(7);
+    await expect(cardB).toHaveClass(/kbd-focus/);
+    await expect(cardA).not.toHaveClass(/kbd-focus/);
+
+    // ArrowLeft should move focus back to slot 6
+    await page.keyboard.press('ArrowLeft');
+    await page.waitForTimeout(200);
+    await expect(cardA).toHaveClass(/kbd-focus/);
+
+    // Press '7' — design slider on focused card (slot 6) must read 7
+    await page.keyboard.press('7');
+    await page.waitForTimeout(300); // input + change events dispatched synchronously but POST is async
+    const designSlider = cardA.locator('.sliders input[type=range][data-axis=design]');
+    await expect(designSlider).toHaveValue('7');
+  });
+
+  // KB-4: ? key shows keymap toast; hover sets kbd-focus
+  test('KB-4 · ? shows keymap toast for 4s; mouseover sets kbd-focus', async ({ page }) => {
+    await loadPage(page);
+
+    // Verify mouseover sets kbd-focus
+    const card = await focusCard(page, 8);
+    await expect(card).toHaveClass(/kbd-focus/);
+
+    // Press Shift+/ which produces e.key='?' in the browser
+    await page.keyboard.press('Shift+/');
+
+    const toast = page.locator('#kbd-toast');
+    await expect(toast).toBeVisible({ timeout: 3_000 });
+    const msg = (await toast.textContent() || '').toLowerCase();
+    expect(msg).toContain('keep');
+    expect(msg).toContain('reject');
+    expect(msg).toContain('pair');
+
+    // Toast must remain visible for at least 3.5s (it's set to fade after 4000ms)
+    await page.waitForTimeout(3_500);
+    await expect(toast).toBeVisible();
+  });
+
+  // KB-5: P key opens pair-modal
+  test('KB-5 · P key opens pair-modal for focused card', async ({ page }) => {
+    await loadPage(page);
+
+    const card = await focusCard(page, 9);
+    await page.keyboard.press('p');
+
+    await expect(page.locator('#pair-modal.open')).toBeVisible({ timeout: 5_000 });
+
+    // Close modal (Escape) so subsequent state is clean
+    await page.keyboard.press('Escape');
+    await expect(page.locator('#pair-modal.open')).not.toBeVisible({ timeout: 3_000 });
+  });
+
+  // KB-6: modal-open guard — K/R/1-9 must NOT fire when pair-modal is open
+  test('KB-6 · K and 1-9 keys are suppressed while pair-modal is open', async ({ page }) => {
+    await loadPage(page);
+
+    const card = await focusCard(page, 10);
+
+    // Open pair-modal via button click (avoids relying on P shortcut here)
+    await card.locator('.btn-pair').click();
+    await expect(page.locator('#pair-modal.open')).toBeVisible({ timeout: 5_000 });
+
+    // Snapshot decision state before pressing K
+    const decisionBefore = await card.getAttribute('data-decision');
+
+    // Press K — should be suppressed while pair-modal is open
+    await page.keyboard.press('k');
+    await page.waitForTimeout(400);
+
+    // data-decision must not have changed to 'keep'
+    const decisionAfter = await card.getAttribute('data-decision');
+    expect(decisionAfter).toBe(decisionBefore);
+
+    // Press '5' — design slider must stay unchanged
+    const designSlider = card.locator('.sliders input[type=range][data-axis=design]');
+    const sliderBefore = await designSlider.inputValue();
+    await page.keyboard.press('5');
+    await page.waitForTimeout(300);
+    await expect(designSlider).toHaveValue(sliderBefore);
+
+    // Close modal
+    await page.keyboard.press('Escape');
+    await expect(page.locator('#pair-modal.open')).not.toBeVisible({ timeout: 3_000 });
+  });
+
+});

← f5a303a Sync /age-themes route to v9 — teen body 14→16/h22, adult li  ·  back to Wallco Ai  ·  Architect refactor (items 1+3+5 from punch list) — extract a f5de4b2 →