diff --git a/src/services/ai/ai-prompts.ts b/src/services/ai/ai-prompts.ts index 300cf7345..210c82440 100644 --- a/src/services/ai/ai-prompts.ts +++ b/src/services/ai/ai-prompts.ts @@ -23,7 +23,7 @@ TEXT RESIZING (textGrowth): - "auto" = Auto Width: text expands horizontally, no word wrapping. Best for short labels, buttons, single-line text. - "fixed-width" = Auto Height: width is fixed (or "fill_container"), height auto-sizes to wrapped content. Best for paragraphs, descriptions, multi-line text. - "fixed-width-height" = Fixed Size: both width and height are fixed. Content clips if too long. -- DEFAULT RULE: text inside layout frames should use textGrowth="fixed-width" + width="fill_container". This ensures text wraps within the parent and height auto-sizes. +- DEFAULT RULE: in vertical layout frames, body/description text should use textGrowth="fixed-width" + width="fill_container". In horizontal rows, short labels should use width="fit_content" (or omit width) + textGrowth="auto" to avoid squeezing siblings. - Short labels/buttons can omit textGrowth (defaults to "auto"). TEXT TYPOGRAPHY: @@ -41,12 +41,12 @@ RULES: - Use justifyContent="space_between" to spread items across full width (great for navbars, footers) OVERFLOW PREVENTION (CRITICAL — violations cause visual glitches): -- TEXT WIDTH: text nodes inside layout frames MUST use width="fill_container" + textGrowth="fixed-width". NEVER set a fixed pixel width on text inside a layout — it WILL overflow. The layout engine auto-constrains fill_container text to the parent's available content area. +- TEXT WIDTH: for text inside vertical layout frames, use width="fill_container" + textGrowth="fixed-width". For text inside horizontal rows (nav/footer/button rows), default to width="fit_content" (or omit width) + textGrowth="auto". NEVER set fixed pixel width on text inside a layout. BAD: {"type":"text","width":378,"textGrowth":"fixed-width"} inside a 195px card with 80px padding → overflows! GOOD: {"type":"text","width":"fill_container","textGrowth":"fixed-width"} → auto-fits to 115px available space. - CHILD SIZE: any child with a fixed pixel width must be ≤ parent's content area (parent width − total horizontal padding). If unsure, use "fill_container". - CJK TEXT (Chinese/Japanese/Korean): each character renders at ~1.0× fontSize width. For buttons/badges containing CJK text, ensure: container width ≥ (charCount × fontSize) + total horizontal padding. Example: "免费下载" (4 chars) at fontSize 15 → needs ~60px content + padding → button width ≥ 104px with padding [8,22]. -- BADGES: when badge text is long or CJK, use width="fit_content" on the badge frame so it auto-sizes to its text content. +- BADGES: use badge/chip style only for short labels (CJK <=8 chars / Latin <=16 chars). If text is longer, do NOT use badge style; use a normal text row or small card. ` const DESIGN_EXAMPLES = ` @@ -63,7 +63,7 @@ ICONS & IMAGES: IMPORTANT: Give icon nodes a descriptive name matching standard icon names (e.g. "SearchIcon", "MenuIcon", "ArrowRightIcon", "CheckIcon", "StarIcon", "DownloadIcon", "PlayIcon", "ShieldIcon", "ZapIcon", "HeartIcon", "UserIcon", "HomeIcon", "MailIcon", "BellIcon", "SettingsIcon", "PlusIcon", "EyeIcon", "LockIcon", "PhoneIcon", "ChevronRightIcon", "ChevronDownIcon", "XIcon"). The system will auto-resolve icon names to verified SVG paths, so the name is more important than the d data. - Never use emoji characters as icons (e.g. 🧠✨📱✅). Always use path nodes for icons. -- For app screenshot/mockup areas, use a phone placeholder frame with solid fill matching the page theme + 1px subtle stroke. cornerRadius ~32. No text inside — just a clean phone shape. +- For app screenshot/mockup areas, use a phone placeholder frame with solid fill matching the page theme + 1px subtle stroke. cornerRadius ~32. Prefer no inner content; if a placeholder copy is needed (e.g. "APP截图占位"), keep exactly one centered text node INSIDE the phone frame (never as a sibling below it). - Do NOT use random real-world app screenshots or dense mini-app simulations for showcase sections. ` @@ -156,7 +156,7 @@ LAYOUT ENGINE (flexbox-based): - WIDTH CONSISTENCY: siblings in a vertical layout must use the SAME width strategy. If one input/button uses "fill_container", ALL sibling inputs/buttons must also use "fill_container". Mixing fixed-px and fill_container causes misalignment. - NEVER use "fill_container" on children of a "fit_content" parent — circular dependency breaks layout. - For two-column layouts: root (vertical) → content row (horizontal) → left column + right column. Each column uses "fill_container" width. -- TEXT IN LAYOUTS (MOST COMMON BUG — read carefully): text inside layout frames MUST use textGrowth="fixed-width" + width="fill_container". NEVER use a fixed pixel width (e.g. width:378) on text inside a layout — the text WILL overflow its parent. "fill_container" auto-constrains to available space. +- TEXT IN LAYOUTS (MOST COMMON BUG — read carefully): in vertical layouts, body text should use textGrowth="fixed-width" + width="fill_container". In horizontal rows, labels should use textGrowth="auto" + width="fit_content" (or omit width). NEVER use fixed pixel width (e.g. width:378) on text inside a layout. - SHORT TEXT: buttons, labels, single-line text can use textGrowth="auto" (or omit it) — text expands horizontally to fit content. - TEXT HEIGHT: NEVER set explicit pixel height on text nodes (e.g. height:22, height:44). OMIT the height property entirely — the layout engine auto-calculates height from textGrowth + content. Setting a small height causes text clipping and overlap with siblings below. - CJK BUTTONS/BADGES: Chinese/Japanese/Korean characters are wider. For a button with CJK text, ensure container width ≥ (charCount × fontSize) + horizontal padding. Example: "免费下载" (4 chars) at 15px → min content width ~60px → button width ≥ 60 + left padding + right padding. @@ -185,7 +185,7 @@ DESIGN GUIDELINES: - Text: titles 22-28px bold, body 14-16px, captions 12px - Buttons: height 44-52px, cornerRadius 8-12, padding [12, 24] (vertical, horizontal). With icon+text: layout="horizontal", gap=8, alignItems="center". Width: "fill_container" (stretch), "fit_content" (hug), or fixed px — choose per context. - Icon-only buttons (heart, bookmark, share, etc.): square frame 44x44px, justifyContent="center", alignItems="center", path icon 20-24px inside. -- Badges/tags ("NEW", "SALE", "PRO"): frame with padding [4, 12], cornerRadius 4-6, height="fit_content". Small text (11-13px), no textGrowth. Never clip badge text. +- Badges/tags ("NEW", "SALE", "PRO"): only for short labels (CJK <=8 chars / Latin <=16 chars). For longer copy, use a normal text row/card instead of badge/chip style. - Button + icon-button row: horizontal, gap=8-12. Primary button width="fill_container"; icon-only button fixed square 44-48px. - Inputs: height 44px, light bg, subtle border. Use width="fill_container" in form contexts. - Fixed-width children must NOT exceed their parent's content area (parent width minus padding). @@ -196,7 +196,8 @@ DESIGN GUIDELINES: - Icons: use "path" nodes with descriptive names matching standard icon names (e.g. "SearchIcon", "MenuIcon", "ArrowRightIcon"). The system auto-resolves names to verified SVG paths. Size 16-24px. - Never use emoji glyphs as icon substitutes. If an icon is needed, use a path node with a descriptive icon name. - Use image nodes for generic photos/illustrations only; for app preview areas prefer phone mockup placeholders -- Phone mockup/screenshot placeholder: exactly ONE "frame" node, width 260-300, height 520-580, cornerRadius 32, solid fill matching theme + 1px subtle stroke. NEVER use ellipse or circle for mockups. NEVER add any children inside (no text, no frames, no images). All mockups must look identical. +- Phone mockup/screenshot placeholder: exactly ONE "frame" node, width 260-300, height 520-580, cornerRadius 32, solid fill matching theme + 1px subtle stroke. NEVER use ellipse or circle for mockups. If a placeholder label is used, keep exactly ONE centered text child inside the phone frame; otherwise no children. Never put the label as a sibling below the phone. +- Hero with phone mockup (desktop): prefer a two-column horizontal layout (left text/cta, right phone). Do NOT stack the phone below headline unless mobile. - NEVER use ellipse nodes for decorative/placeholder shapes. Use frame or rectangle with cornerRadius instead. - Avoid adding an extra full-width CTA strip directly under navigation unless the prompt explicitly asks for that section. - Buttons, nav items, and list items should include icons when appropriate for better UX @@ -262,11 +263,11 @@ CRITICAL RULES: - Use width/height (or "fill_container") on all children. Unique descriptive IDs. All colors as fill arrays. - Start with tags, then immediately the json block. NO preamble text. - After the json block, add a 1-sentence summary. -- Phone mockup: exactly ONE "frame" node, width 260-300, height 520-580, cornerRadius 32, solid fill + 1px stroke. NEVER use ellipse. NEVER add any children inside (no text, no frames, no images). All mockups identical. +- Phone mockup: exactly ONE "frame" node, width 260-300, height 520-580, cornerRadius 32, solid fill + 1px stroke. NEVER use ellipse. If a placeholder label is needed, allow exactly ONE centered text child inside the phone; otherwise no children. Never put placeholder text below the phone as a sibling. - NEVER use ellipse for decorative/placeholder shapes — use frame or rectangle with cornerRadius. - Navigation bars (when applicable): justifyContent="space_between", 3 groups (logo | links | CTA), padding=[0,80], alignItems="center". - Never use emoji as icons; use path nodes with descriptive icon names (system auto-resolves to verified SVG paths). -- TEXT IN LAYOUTS: text inside layout frames MUST use textGrowth="fixed-width" + width="fill_container". NEVER use fixed pixel widths on text. Short labels/buttons can omit textGrowth. +- TEXT IN LAYOUTS: vertical layout body text should use textGrowth="fixed-width" + width="fill_container". Horizontal layout labels/buttons should use textGrowth="auto" + width="fit_content" (or omit width). NEVER use fixed pixel widths on text. - TEXT HEIGHT: NEVER set explicit pixel height on text nodes (e.g. height:22). OMIT the height property — the engine auto-calculates from textGrowth + content. A small explicit height causes text clipping and overlap. - Cards with images: ALWAYS set clipContent: true + cornerRadius. Use "fill_container" width on image/body/text children inside the card. - CARD ROW ALIGNMENT: cards in a horizontal row MUST ALL use width="fill_container" + height="fill_container" for even distribution and equal height. Never use different fixed heights on sibling cards — it creates an ugly uneven row. @@ -274,10 +275,10 @@ CRITICAL RULES: - Keep section rhythm consistent (80-120px vertical padding) and preserve alignment between sections. OVERFLOW PREVENTION (CRITICAL — #1 source of visual bugs): -- ALL text nodes inside layout frames → width="fill_container" + textGrowth="fixed-width". No exceptions. NEVER width:378 or width:224 on text inside a layout frame. +- Do NOT force fill width for all text. Use width="fill_container" + textGrowth="fixed-width" for vertical body text; use width="fit_content"/auto for short text in horizontal rows. NEVER width:378 or width:224 on text inside a layout frame. - Fixed-width children must be ≤ parent content area (parent width − horizontal padding). Example: a card width=195 with padding=[24,40,24,40] has 115px available — a child with width=378 causes severe overflow. - CJK (Chinese/Japanese/Korean) text in buttons: each CJK char ≈ fontSize wide. "免费下载" (4 chars) at fontSize 15 = ~60px minimum content width. Button must be ≥ 60 + horizontal padding. -- Badges with dynamic text: use width="fit_content" so the badge auto-expands to fit its text. +- Badges must stay short-label only (CJK <=8 chars / Latin <=16 chars). For longer text, avoid badge/chip style. COPYWRITING (keep all text content concise — verbose copy breaks layout and hurts aesthetics): - Headlines: 2-6 words, punchy. Subtitles: 1 sentence ≤15 words. diff --git a/src/services/ai/orchestrator-prompt-optimizer.ts b/src/services/ai/orchestrator-prompt-optimizer.ts index b6616de0a..e2becc81f 100644 --- a/src/services/ai/orchestrator-prompt-optimizer.ts +++ b/src/services/ai/orchestrator-prompt-optimizer.ts @@ -68,6 +68,9 @@ export function prepareDesignPrompt(prompt: string): PreparedDesignPrompt { const sectionLines = extractWebsiteSectionLines(sections).slice(0, PROMPT_OPTIMIZER_LIMITS.maxSectionLines) const highlightLines = extractCoreHighlightLines(sections).slice(0, 6) const needsScreenshotPlaceholder = hasScreenshotRequirements(normalized) + const needsDenseCardCompaction = hasDenseCardRequirements(normalized) + const needsTableStructure = hasTableStructureRequirements(normalized) + const needsHeroPhoneTwoColumn = hasHeroPhoneTwoColumnRequirements(normalized) const sloganLines = extractSloganCandidates(sections).slice(0, 4) const personalityHint = extractProductPersonality(normalized) @@ -99,6 +102,21 @@ export function prepareDesignPrompt(prompt: string): PreparedDesignPrompt { 'Screenshot rule: for sections like "App截图"/"XX截图"/"screenshot", use a minimal phone placeholder (outline + short label), avoid random real screenshots, and do not recreate detailed mini-app internals.', ) } + if (needsDenseCardCompaction) { + conciseParts.push( + 'Dense-card rule: if a horizontal row has 5+ cards (or very narrow cards), cards must be natively compact at generation time: keep max 2 text blocks per card (title + metric), rewrite copy into concise keyword phrases, remove non-essential decorative elements, and never output truncation marks ("..." / "…").', + ) + } + if (needsTableStructure) { + conciseParts.push( + 'Table rule: render table as explicit header/body grid rows with aligned cells. Never collapse multiple columns into a single long text line.', + ) + } + if (needsHeroPhoneTwoColumn) { + conciseParts.push( + 'Hero phone layout rule (desktop): use a horizontal two-column hero (left text/cta, right phone mockup). Do not stack the phone below headline unless mobile.', + ) + } conciseParts.push( 'Icon rule: never use emoji glyphs as icons; always use path nodes with descriptive icon names (e.g. "SearchIcon", "MenuIcon"). System auto-resolves to verified SVG paths.', ) @@ -280,6 +298,26 @@ function hasScreenshotRequirements(text: string): boolean { return /(截图|screenshot|mockup)/i.test(text) } +function hasDenseCardRequirements(text: string): boolean { + if (/(dense|密集|多卡片|卡片过多|超过\s*4\s*个|5\+\s*cards?|cards?\s*row|一行.*卡片|横排.*卡片)/i.test(text)) { + return true + } + if (/(cefr|a1[\s-]*c2|a1|a2|b1|b2|c1|c2|词库分级|分级词库|学习阶段|等级)/i.test(text)) { + return true + } + return false +} + +function hasTableStructureRequirements(text: string): boolean { + return /(table|grid|tabular|表格|表头|表体|列|行|字段|等级|级别|词汇量|适用人群|对应考试)/i.test(text) +} + +function hasHeroPhoneTwoColumnRequirements(text: string): boolean { + const heroLike = /(hero|首页首屏|首屏|横幅|banner)/i.test(text) + const phoneLike = /(phone|mockup|screenshot|截图|手机|app\s*screen|应用截图)/i.test(text) + return heroLike && phoneLike +} + function extractFallbackSectionLabels(prompt: string): string[] { const lines = prompt.replace(/\r/g, '').split('\n') const labels: string[] = [] diff --git a/src/services/ai/orchestrator-prompts.ts b/src/services/ai/orchestrator-prompts.ts index 64d0372f2..0c4d541f6 100644 --- a/src/services/ai/orchestrator-prompts.ts +++ b/src/services/ai/orchestrator-prompts.ts @@ -44,7 +44,7 @@ const BLOCK = "```" export const SUB_AGENT_PROMPT = `PenNode flat JSONL engine. Output a ${BLOCK}json block with ONE node per line. TYPES: frame (width,height,layout,gap,padding,justifyContent,alignItems,clipContent,cornerRadius,fill,stroke,effects,children), rectangle, ellipse, text (content,fontFamily,fontSize,fontWeight,fontStyle,fill,width,height,textAlign,textGrowth,lineHeight,letterSpacing,textAlignVertical), path (d,width,height,fill,stroke), image (src,width,height) -textGrowth: "auto" (expand horizontally, no wrap), "fixed-width" (fixed width, height auto-sizes to wrapped content), "fixed-width-height" (both fixed). Default for text in layouts: "fixed-width" + width="fill_container". +textGrowth: "auto" (expand horizontally, no wrap), "fixed-width" (fixed width, height auto-sizes to wrapped content), "fixed-width-height" (both fixed). Default: vertical body text uses "fixed-width" + width="fill_container"; horizontal labels use "auto" + width="fit_content" (or omit width). lineHeight: multiplier (1.1-1.2 for headings, 1.4-1.6 for body). letterSpacing: px (-0.5 for headlines, 0.5-2 for uppercase labels). textAlignVertical: "top"|"middle"|"bottom". SHARED: id, type, name, x, y, opacity width/height: number (px), "fill_container" (stretch), "fit_content" (shrink-wrap) @@ -78,7 +78,7 @@ CRITICAL LAYOUT RULES (violations cause rendering bugs): ⚠️ TEXT RULES (the #1 most common bug source — MUST follow): TEXT WIDTH: -- ALL text nodes inside a layout frame → width="fill_container" + textGrowth="fixed-width". NO EXCEPTIONS. +- Vertical layout body text → width="fill_container" + textGrowth="fixed-width". In horizontal rows, short labels should use width="fit_content" (or omit width) + textGrowth="auto" to avoid squeezing siblings. - NEVER output a text node with a fixed pixel width (width:224, width:378, width:784 etc.) inside a layout frame. This causes the text to overflow horizontally and break the design. - The ONLY time text can have a fixed pixel width is when it is NOT inside a layout frame (layout="none" parent). - BAD example (causes overflow): parent card width=195, padding=[24,40,24,40] (available=115px), child text width=378 → text overflows by 263px! @@ -128,15 +128,17 @@ DESIGN RULES: - CJK FONTS: When content is in Chinese/Japanese/Korean, use CJK-compatible fonts — "Noto Sans SC" for headings, "Inter" or "Noto Sans SC" for body. NEVER use "Space Grotesk" or "Manrope" for CJK text (they have no CJK glyphs). CJK lineHeight: 1.3-1.4 for headings, 1.6-1.8 for body. CJK letterSpacing: 0 for body, never negative. - Cards in horizontal rows: ALL cards MUST use width="fill_container" + height="fill_container" for even distribution and equal height. Never use fixed pixel width/height on cards in a row. A card with icon+title+description needs at least 160-200px content height — the row will auto-size. ALWAYS set clipContent: true on cards with cornerRadius + image children. - Dense rows (5+ cards): compact card internals aggressively. Use very short titles (CJK ≤6 chars / Latin ≤12 chars), keep max 2 text blocks per card (title + short metric), and remove non-essential decorative elements. Refine text into short keyword phrases; never use "..." or "…" truncation. +- Dense rows must be natively compact in the generated JSON. Do NOT emit long card descriptions expecting post-processing cleanup. - Icons: "path" nodes with descriptive names (e.g. "SearchIcon", "MenuIcon", "ArrowRightIcon", "StarIcon", "ShieldIcon", "ZapIcon"). System auto-resolves to verified SVG paths. Size 16-24px. NEVER use emoji. -- PHONE MOCKUP: exactly ONE "frame" node, width 260-300, height 520-580, cornerRadius 32, solid fill + 1px stroke. NEVER use ellipse or circle for mockups. NEVER add any children inside (no text, no frames, no images). Every phone mockup must look identical. +- PHONE MOCKUP: exactly ONE "frame" node, width 260-300, height 520-580, cornerRadius 32, solid fill + 1px stroke. NEVER use ellipse or circle for mockups. If placeholder copy is needed, keep exactly ONE centered text child inside the phone; otherwise no children. Never place that label as a sibling below the phone. - NEVER use ellipse nodes for decorative shapes. Use frame or rectangle with cornerRadius instead. - Use STYLE GUIDE colors/fonts from user prompt consistently. Do not introduce random colors. -- TEXT: text inside layout frames MUST use textGrowth="fixed-width" + width="fill_container". NEVER set fixed pixel width on text. NEVER set height on text — omit it entirely, the engine auto-sizes. Short labels/buttons can omit textGrowth. +- TEXT: for vertical layouts, body text should use textGrowth="fixed-width" + width="fill_container". For horizontal rows (nav/footer/button rows), labels should use textGrowth="auto" + width="fit_content" (or omit width). NEVER set fixed pixel width on text. NEVER set height on text — omit it entirely; the engine auto-sizes. - BUTTONS: height 44-52px, padding [12, 24] minimum. With icon+text: layout="horizontal", gap=8, alignItems="center". Sizing: "fill_container" (stretch), "fit_content" (hug content), or fixed px — choose per context. - CJK BUTTONS (Chinese/Japanese/Korean text): each CJK character renders ~1.0× fontSize wide. For "免费下载" (4 chars) at fontSize 15: content needs ~60px width → button needs 60 + horizontal padding (e.g. padding [8,22] = 44px → total 104px minimum). ALWAYS calculate: button width ≥ charCount × fontSize + totalHorizontalPadding. - ICON-ONLY BUTTONS (heart, bookmark, share, etc.): square frame, minimum 44x44px, justifyContent="center", alignItems="center". Path icon inside 20-24px. -- BADGES/TAGS (e.g. "NEW", "SALE", "PRO"): frame with padding [4, 12] minimum, cornerRadius 4-6, height="fit_content". For badges with CJK or long text, use width="fit_content" so badge auto-sizes. Text inside must NOT be clipped — use short fontSize (11-13px), no textGrowth needed. +- BADGES/TAGS (e.g. "NEW", "SALE", "PRO"): only for SHORT labels (CJK <=8 chars / Latin <=16 chars). If text is longer, do NOT use badge/chip style — use a normal text row or small card instead. +- HERO + PHONE LAYOUT (desktop): when hero contains a phone mockup, prefer a two-column horizontal layout (left text/cta, right phone). Do NOT stack phone below headline unless mobile. - BUTTON + ICON-BUTTON ROW: horizontal frame, gap=8-12. Primary button width="fill_container" to take remaining space; icon-only button fixed square (44-48px). - LANDING PAGE SECTIONS (only when designing landing pages/websites): - Hero: large headline (40-56px), gradient or bold backgrounds, clear CTA, generous whitespace diff --git a/src/services/ai/orchestrator.ts b/src/services/ai/orchestrator.ts index 2becffac3..a60c02e13 100644 --- a/src/services/ai/orchestrator.ts +++ b/src/services/ai/orchestrator.ts @@ -445,6 +445,7 @@ async function executeSubAgent( subtask, plan, promptOverride ?? preparedPrompt.subAgentPrompt, + request.prompt, request.context?.variables, request.context?.themes, ) @@ -627,6 +628,7 @@ function buildSubAgentUserPrompt( subtask: SubTask, plan: OrchestratorPlan, compactPrompt: string, + fullPrompt: string, variables?: Record, themes?: Record, ): string { @@ -647,10 +649,33 @@ CRITICAL LAYOUT CONSTRAINTS: - Use "fill_container" for children that stretch, "fit_content" for shrink-wrap sizing. - Use justifyContent="space_between" to distribute items (e.g. navbar: logo | links | CTA). Use padding=[0,80] for horizontal page margins. - For side-by-side layouts, nest a horizontal frame with child frames using "fill_container" width. -- Phone mockup = ONE frame node, cornerRadius 32, NO children inside. NEVER use ellipse. +- Phone mockup = ONE frame node, cornerRadius 32. If a placeholder label is needed, allow exactly ONE centered text child inside the phone; otherwise no children. Never place placeholder text below the phone as a sibling. NEVER use ellipse. - Text height must fit content: estimate lines × fontSize × 1.4. - IDs prefix="${subtask.idPrefix}-". No tags. Output \`\`\`json immediately.` + if (needsNativeDenseCardInstruction(subtask.label, compactPrompt, fullPrompt)) { + prompt += `\n\nNATIVE DENSE-CARD MODE (must be solved during generation): +- If you create a horizontal row with 5+ cards (or cards become narrow), compact each card natively BEFORE output. +- Each card: max 2 text blocks only (title + one short metric). Remove long descriptions. +- Rewrite long copy into concise keyword phrases. Never use truncation marks ("..." or "…"). +- Prefer removing non-essential decorative elements before shrinking readability. +- Do NOT rely on post-processing to prune card content.` + } + if (needsTableStructureInstruction(subtask.label, compactPrompt, fullPrompt)) { + prompt += `\n\nTABLE MODE (must be structured natively): +- Build table as explicit grid frames, NOT a single long text line. +- Header must be its own horizontal row with separate cell frames for each column. +- Body rows must align to the same column structure as header. +- Keep level badge/chip inside the level cell; do not merge multiple columns into one text node. +- In table rows, avoid badge/button auto-style patterns unless the node is explicitly a chip.` + } + if (needsHeroPhoneTwoColumnInstruction(subtask.label, compactPrompt, fullPrompt)) { + prompt += `\n\nHERO PHONE LAYOUT MODE (desktop): +- Use a horizontal two-column hero layout: left = headline/subtitle/CTA, right = phone mockup. +- Keep phone as a sibling in the same horizontal row, NOT stacked below the headline. +- Only use stacked layout for mobile/narrow viewport sections.` + } + // Inject style guide so sub-agent uses consistent colors/fonts if (plan.styleGuide) { const sg = plan.styleGuide @@ -671,6 +696,50 @@ CRITICAL LAYOUT CONSTRAINTS: return prompt } +function needsNativeDenseCardInstruction( + subtaskLabel: string, + compactPrompt: string, + fullPrompt: string, +): boolean { + const text = `${subtaskLabel}\n${compactPrompt}\n${fullPrompt}`.toLowerCase() + if (/(dense|密集|多卡片|卡片过多|超过\s*4\s*个|5\+\s*cards?|cards?\s*row|一行.*卡片|横排.*卡片)/.test(text)) { + return true + } + if (/(cefr|a1[\s-]*c2|a1|a2|b1|b2|c1|c2|词库分级|分级词库|学习阶段|等级)/.test(text)) { + return true + } + if (/(feature\s*cards?|cards?\s*section|词库|词汇|card)/.test(text) && /(a1|b1|c1|c2|cefr|等级|阶段)/.test(text)) { + return true + } + return false +} + +function needsTableStructureInstruction( + subtaskLabel: string, + compactPrompt: string, + fullPrompt: string, +): boolean { + const text = `${subtaskLabel}\n${compactPrompt}\n${fullPrompt}`.toLowerCase() + if (/(table|grid|tabular|表格|表头|表体|列|行|字段|等级|级别|词汇量|适用人群|对应考试)/.test(text)) { + return true + } + if (/(cefr|a1[\s-]*c2|a1|a2|b1|b2|c1|c2)/.test(text) && /(level|table|表格|等级)/.test(text)) { + return true + } + return false +} + +function needsHeroPhoneTwoColumnInstruction( + subtaskLabel: string, + compactPrompt: string, + fullPrompt: string, +): boolean { + const text = `${subtaskLabel}\n${compactPrompt}\n${fullPrompt}`.toLowerCase() + const heroLike = /(hero|首页首屏|首屏|横幅|banner)/.test(text) + const phoneLike = /(phone|mockup|screenshot|截图|手机|app\s*screen|应用截图)/.test(text) + return heroLike && phoneLike +} + // --------------------------------------------------------------------------- // ID namespace isolation // ---------------------------------------------------------------------------