fix(ai): expand Type 0 fallback regex to all documented component triggers
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).
This commit is contained in:
parent
e9dcfb5d91
commit
6c80bd1b3e
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue