From 6c80bd1b3e8bbaad25a8322e3b2d7d42e11eaa74 Mon Sep 17 00:00:00 2001 From: Fini Date: Sat, 9 May 2026 20:59:27 +0800 Subject: [PATCH] fix(ai): expand Type 0 fallback regex to all documented component triggers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: Codex stop-time review #4 — the previous regex covered ~17 nouns but design-type.md documents 25 (button / label / row / item / selector / panel / chart were missing) and the CJK 卡片 alias was also listed. JS `\b` is ASCII-only and never fires between two CJK chars, so `\b卡片\b` matched nothing in "design a 卡片". What: split into COMPONENT_TRIGGER_LATIN_RE (full noun list with `\b` boundaries) + COMPONENT_TRIGGER_CJK_RE (kana-free subset of the most common Chinese aliases — 卡片 / 徽章 / 标签 / 按钮 / 开关 / 对话框 / 提示 / 气泡 / 图表). Either match is enough to classify Type 0. Disqualifier regex also gains 屏幕 (screen in zh-Hans). Tests: 23 it.each cases pin one Latin trigger each plus the CJK 卡片; 6 negative cases prove the disqualifier still wins for "X screen / page / app / onboarding / flow" prompts. 1055 / 1055 AI tests pass (was 1027; +28 new). --- .../orchestrator-prompt-optimizer.test.ts | 46 +++++++++++++++++-- .../src/services/ai/design-type-presets.ts | 31 +++++++++---- 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/apps/web/src/services/ai/__tests__/orchestrator-prompt-optimizer.test.ts b/apps/web/src/services/ai/__tests__/orchestrator-prompt-optimizer.test.ts index ad8fca3d4..e710464fc 100644 --- a/apps/web/src/services/ai/__tests__/orchestrator-prompt-optimizer.test.ts +++ b/apps/web/src/services/ai/__tests__/orchestrator-prompt-optimizer.test.ts @@ -151,9 +151,49 @@ describe('buildFallbackPlanFromPrompt', () => { expect(plan.subtasks[0]?.region.height).toBe(200); }); - it('does NOT misclassify "X card screen" as a component (must stay landing-page or mobile)', () => { - const plan = buildFallbackPlanFromPrompt('design a card screen page'); - // "screen" / "page" disqualifier prevents the component shortcut. + // Codex review #4: cover every documented Type 0 trigger from + // pen-ai-skills/skills/phases/planning/design-type.md so the fallback + // doesn't silently route "design a primary button" to a 1200px landing + // page when AI parsing fails. + it.each([ + ['design a profile card', 'card'], + ['design a 卡片', '卡片'], + ['design a primary button', 'button'], + ['design a status badge', 'badge'], + ['design a category chip', 'chip'], + ['design a price tag', 'tag'], + ['design a setting toggle', 'toggle'], + ['design a confirm dialog', 'dialog'], + ['design a tooltip with arrow', 'tooltip'], + ['design a popover with menu', 'popover'], + ['design a bottom sheet', 'sheet'], + ['design a stat tile', 'tile'], + ['design a notification row', 'row'], + ['design an inbox item', 'item'], + ['design a status label', 'label'], + ['design a segmented selector', 'selector'], + ['design a side panel', 'panel'], + ['design a metric widget', 'widget'], + ['design an avatar with initial', 'avatar'], + ['design a step stepper', 'stepper'], + ['design a revenue stat', 'stat'], + ['design a metric for sales', 'metric'], + ['design a pie chart', 'chart'], + ])('classifies "%s" as Type 0 component (matches "%s")', (prompt) => { + const plan = buildFallbackPlanFromPrompt(prompt); + expect(plan.rootFrame.width).toBe(400); + expect(plan.rootFrame.height).toBe(0); + }); + + it.each([ + ['design a card screen page'], + ['design a profile page'], + ['design a settings screen'], + ['design a mobile login screen'], + ['design a dashboard home page'], + ['design an onboarding flow'], + ])('does NOT misclassify "%s" as a component', (prompt) => { + const plan = buildFallbackPlanFromPrompt(prompt); expect(plan.rootFrame.width).not.toBe(400); }); diff --git a/apps/web/src/services/ai/design-type-presets.ts b/apps/web/src/services/ai/design-type-presets.ts index 9e211a4fe..0c9ae1c70 100644 --- a/apps/web/src/services/ai/design-type-presets.ts +++ b/apps/web/src/services/ai/design-type-presets.ts @@ -12,15 +12,28 @@ export interface DesignTypePreset { /** * Component triggers — when the prompt names one atomic UI piece without a - * surrounding screen ("X card", "X badge", "X chip", etc.). Mirrors the - * Type 0 list in `pen-ai-skills/skills/phases/planning/design-type.md`. + * surrounding screen. Mirrors the Type 0 list in + * `pen-ai-skills/skills/phases/planning/design-type.md`: + * - "X card" / "X 卡片" — profile / pricing / stat / event / user card + * - "X badge" / "X chip" / "X tag" / "X tile" / "X label" / "X row" / "X item" + * - "X button" / "X toggle" / "X switch" / "X selector" + * - "X modal" / "X dialog" / "X tooltip" / "X popover" / "X sheet" + * - "X widget" / "X panel" (when no surrounding screen) + * - Single visualisations: "a chart" / "a pie chart" / "a stat" / "a metric" * - * Match policy: word boundary + the noun must lead OR follow the qualifier - * to keep noisy mid-sentence hits out (e.g. "shopping cart screen" should - * not match "cart" as a component). + * The disqualifier regex below blocks the trigger when the prompt also + * names a screen / page / app, so "shopping cart screen" or "navbar with + * cart button" stays a non-component. */ -const COMPONENT_TRIGGER_RE = - /\b(?:[a-z一-鿿]+\s+)?(card|badge|chip|tile|tag|pill|toggle|switch|modal|dialog|tooltip|popover|sheet|widget|avatar|stepper|stat|metric)(?:\s+(?:design|component|widget))?\b/i; +// Split into Latin and CJK matchers because JS `\b` is ASCII-only and never +// fires between two CJK characters (so `\b卡片\b` would never match a prompt +// like "design a 卡片"). Each matcher runs independently and either is enough +// to classify as Type 0. +const COMPONENT_TRIGGER_LATIN_RE = + /\b(card|badge|chip|tag|tile|pill|label|row|item|button|toggle|switch|selector|modal|dialog|tooltip|popover|sheet|widget|panel|avatar|stepper|stat|metric|chart)\b/i; +const COMPONENT_TRIGGER_CJK_RE = /(卡片|徽章|标签|按钮|开关|对话框|提示|气泡|图表)/; + +const COMPONENT_DISQUALIFIER_RE = /\b(screen|page|app|home|onboarding|flow)\b|网页|页面|屏幕/i; /** * Minimal fallback design type detection. @@ -36,7 +49,9 @@ export function detectDesignType(prompt: string): DesignTypePreset { // mobile/dashboard so a "profile card" prompt doesn't fall through to // landing-page (1200px) when AI parsing fails — the user gets a // sensibly-sized 400px component instead. - if (COMPONENT_TRIGGER_RE.test(prompt) && !/screen|page|app|网页|页面/i.test(prompt)) { + const matchesComponent = + COMPONENT_TRIGGER_LATIN_RE.test(prompt) || COMPONENT_TRIGGER_CJK_RE.test(prompt); + if (matchesComponent && !COMPONENT_DISQUALIFIER_RE.test(prompt)) { return { type: 'component', width: 400,