← 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
M data/chip-chats.jsonM data/moodboards.jsonM data/pairings.jsonM data/reviews.jsonM data/vision-cache.jsonA tests/keyboard-shortcuts.spec.js
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 →