From efb4b84e7d7a0ddf427699b40bb3f74d4c3e6bab Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Sun, 21 Jun 2026 18:24:12 +0800 Subject: [PATCH] fix(orchestrator): refine chat-generated mobile layouts --- crates/op-orchestrator/src/plan_normalize.rs | 19 ++++++++++++++++--- crates/op-orchestrator/src/prompt.rs | 7 +++++++ crates/op-orchestrator/src/prompt_tests.rs | 6 ++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/crates/op-orchestrator/src/plan_normalize.rs b/crates/op-orchestrator/src/plan_normalize.rs index 2cf2529d0..484f743ad 100644 --- a/crates/op-orchestrator/src/plan_normalize.rs +++ b/crates/op-orchestrator/src/plan_normalize.rs @@ -20,6 +20,7 @@ pub struct NormInfo { /// 移动端宽度上限(含)—— ≤ 此值视为移动端单屏。 const MOBILE_MAX_WIDTH: f64 = 480.0; const MOBILE_DEFAULT_HEIGHT: f64 = 812.0; +const MOBILE_DEFAULT_ROOT_GAP: f64 = 20.0; /// subtask 的 id / label 命中即视为"状态栏"区块 —— 移动端由 /// scaffold 注入固定状态栏,plan 里若带状态栏 subtask 则剔除。 @@ -124,7 +125,9 @@ pub fn normalize(plan: &mut OrchestratorPlan, req: &DesignRequest) -> NormInfo { if is_mobile { plan.root_frame.layout = Some("vertical".into()); - plan.root_frame.gap = Some(0.0); + if plan.root_frame.gap.unwrap_or(0.0) <= 0.0 { + plan.root_frame.gap = Some(MOBILE_DEFAULT_ROOT_GAP); + } plan.root_frame.padding = Some(0.0); if plan.root_frame.height <= 0.0 { plan.root_frame.height = MOBILE_DEFAULT_HEIGHT; @@ -249,7 +252,7 @@ mod tests { } #[test] - fn normalize_mobile_forces_vertical_root_layout() { + fn normalize_mobile_forces_vertical_root_layout_and_keeps_positive_gap() { let mut p = plan(390.0, vec![subtask("hero", "Hero")]); p.root_frame.layout = Some("none".into()); p.root_frame.gap = Some(12.0); @@ -258,10 +261,20 @@ mod tests { normalize(&mut p, &req()); assert_eq!(p.root_frame.layout.as_deref(), Some("vertical")); - assert_eq!(p.root_frame.gap, Some(0.0)); + assert_eq!(p.root_frame.gap, Some(12.0)); assert_eq!(p.root_frame.padding, Some(0.0)); } + #[test] + fn normalize_mobile_zero_gap_uses_section_spacing() { + let mut p = plan(390.0, vec![subtask("hero", "Hero")]); + p.root_frame.gap = Some(0.0); + + normalize(&mut p, &req()); + + assert_eq!(p.root_frame.gap, Some(20.0)); + } + #[test] fn normalize_mobile_zero_height_uses_default_viewport() { let mut p = plan(390.0, vec![subtask("hero", "Hero")]); diff --git a/crates/op-orchestrator/src/prompt.rs b/crates/op-orchestrator/src/prompt.rs index 8b7da08ce..74e0f2ea5 100644 --- a/crates/op-orchestrator/src/prompt.rs +++ b/crates/op-orchestrator/src/prompt.rs @@ -535,6 +535,13 @@ Overall design: {}\n\n\ CRITICAL LAYOUT CONSTRAINTS:\n\ - {}\n\ - Target content amount: ~{:.0}px tall. Generate enough elements to fill this area.\n\ +- DENSITY: Do NOT pack the area edge-to-edge. Prefer fewer, stronger modules with visible negative space; most sections should have 3-5 primary rows/cards at most.\n\ +- VISUAL HIERARCHY: Each section must have one clear focal element, secondary supporting text, and quieter metadata. Avoid equal-weight blocks competing for attention.\n\ +- SPACING CONSISTENCY: Use a single outer content gutter and consistent internal gaps. Do not create nested wrappers with conflicting padding or content touching edges.\n\ +- CRAFT POLISH: Add refinement through restrained 1px low-contrast borders, tonal surfaces, small state badges, and subtle shadows. Avoid template-like thick outlines, giant pills, or flat blocks with no micro-detail.\n\ +- MEDIA CONSISTENCY: Use photographic images sparingly and keep them visually consistent in subject, crop, tone, and radius. For food/category UI, prefer cohesive icon or illustration tiles over random unrelated photos.\n\ +- ICON SCALE: Icons support content; keep most icons 16-22px inside 36-48px controls. Avoid oversized circular icon bubbles or repeated identical icon treatments unless the design brief calls for them.\n\ +- ACCENT DISCIPLINE: Reserve saturated accent color for one primary CTA or promo plus small highlights. Do not apply it to every icon, label, border, and large surface at once.\n\ - {}\n\ - NEVER set x or y on children inside layout frames.\n\ - Use \"fill_container\" for children that stretch, \"fit_content\" for shrink-wrap sizing.\n\ diff --git a/crates/op-orchestrator/src/prompt_tests.rs b/crates/op-orchestrator/src/prompt_tests.rs index 744080ce8..9a267a680 100644 --- a/crates/op-orchestrator/src/prompt_tests.rs +++ b/crates/op-orchestrator/src/prompt_tests.rs @@ -160,6 +160,12 @@ fn subagent_prompt_carries_ts_layout_contract() { assert!(cr.user_prompt.contains("NO BLANK PLACEHOLDERS")); assert!(cr.user_prompt.contains("MOBILE NAV SURFACE")); assert!(cr.user_prompt.contains("TYPOGRAPHY HIERARCHY")); + assert!(cr.user_prompt.contains("DENSITY")); + assert!(cr.user_prompt.contains("VISUAL HIERARCHY")); + assert!(cr.user_prompt.contains("SPACING CONSISTENCY")); + assert!(cr.user_prompt.contains("CRAFT POLISH")); + assert!(cr.user_prompt.contains("MEDIA CONSISTENCY")); + assert!(cr.user_prompt.contains("ICON SCALE")); } #[test]