fix(agent): stop landing pages from scaffolding as sidebar dashboards
`plan_is_sidebar_dashboard` accepted a sidebar signal from ANY subtask, so a landing page whose plan happens to carry a nav/menu section was built on the two-column dashboard scaffold — a sidebar rail down the left of a page that should be a full-width hero stack. Require the signal to come from the FIRST subtask (a real sidebar is the leading section, not an incidental one), and let a plan's landing-page anatomy veto an ambiguous signal via `plan_has_landing_anatomy`. An explicit landing-page request now vetoes every dashboard signal, while an explicit dashboard or admin-console request still wins ahead of the structural check, so the unambiguous cases are decided by what the user asked for rather than by section keywords. Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
This commit is contained in:
parent
72fd294514
commit
d709f2c572
|
|
@ -20,8 +20,19 @@ use crate::plan::{OrchestratorPlan, Subtask};
|
|||
///
|
||||
/// Port of TS `isSidebarSubtask` (`orchestrator.ts:175-180`).
|
||||
pub(crate) fn is_sidebar_subtask(st: &Subtask) -> bool {
|
||||
let text = subtask_text(st);
|
||||
has_sidebar_keyword(&text) && !has_topbar_keyword(&text)
|
||||
let identity = subtask_identity_text(st);
|
||||
if has_strong_sidebar_keyword(&identity) {
|
||||
return true;
|
||||
}
|
||||
if has_topbar_keyword(&identity) {
|
||||
return false;
|
||||
}
|
||||
if has_sidebar_keyword(&identity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let elements = st.elements.as_deref().unwrap_or_default().to_lowercase();
|
||||
(identity.contains("left") || identity.contains("side")) && has_sidebar_keyword(&elements)
|
||||
}
|
||||
|
||||
/// A STRONG sidebar signal — "sidebar" / "side bar" / "side nav" / "rail" — is
|
||||
|
|
@ -30,15 +41,7 @@ pub(crate) fn is_sidebar_subtask(st: &Subtask) -> bool {
|
|||
/// app-shell scaffold without requiring a dashboard-content gate.
|
||||
pub(crate) fn is_strong_sidebar_subtask(st: &Subtask) -> bool {
|
||||
let t = subtask_text(st);
|
||||
(t.contains("sidebar")
|
||||
|| t.contains("side bar")
|
||||
|| t.contains("side nav")
|
||||
|| t.contains("side-nav")
|
||||
|| t.contains("left rail")
|
||||
|| t.contains("left nav")
|
||||
|| t.contains("nav rail")
|
||||
|| t.contains("navigation rail"))
|
||||
&& !has_topbar_keyword(&t)
|
||||
has_strong_sidebar_keyword(&t)
|
||||
}
|
||||
|
||||
/// Returns `true` when the prompt + plan subtasks suggest a dashboard-like
|
||||
|
|
@ -47,18 +50,53 @@ pub(crate) fn is_strong_sidebar_subtask(st: &Subtask) -> bool {
|
|||
/// Matches `/(dashboard|admin|analytics|fintech|workspace|data)/` over
|
||||
/// `prompt` concatenated with every subtask's `id + label + elements`.
|
||||
///
|
||||
/// Explicit landing-page intent vetoes every dashboard signal. Otherwise an
|
||||
/// explicit dashboard or admin-console request wins before the plan's
|
||||
/// structural landing-page anatomy is considered.
|
||||
///
|
||||
/// Port of TS `isDashboardLikePrompt` (`orchestrator.ts:192-197`).
|
||||
pub(crate) fn is_dashboard_like_prompt(prompt: &str, plan: &OrchestratorPlan) -> bool {
|
||||
let prompt = prompt.to_lowercase();
|
||||
if has_explicit_landing_page_intent(&prompt) {
|
||||
return false;
|
||||
}
|
||||
if has_explicit_dashboard_intent(&prompt) {
|
||||
return true;
|
||||
}
|
||||
if plan_has_landing_anatomy(plan) {
|
||||
return false;
|
||||
}
|
||||
let subtask_text: String = plan
|
||||
.subtasks
|
||||
.iter()
|
||||
.map(subtask_text)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let text = format!("{}\n{}", prompt, subtask_text).to_lowercase();
|
||||
let text = format!("{prompt}\n{subtask_text}");
|
||||
has_dashboard_keyword(&text)
|
||||
}
|
||||
|
||||
pub(crate) fn plan_has_landing_anatomy(plan: &OrchestratorPlan) -> bool {
|
||||
let identities: Vec<String> = plan.subtasks.iter().map(subtask_identity_text).collect();
|
||||
let has_hero = identities.iter().any(|text| text.contains("hero"));
|
||||
let has_supporting_story = identities.iter().any(|text| {
|
||||
text.contains("feature")
|
||||
|| text.contains("capabilit")
|
||||
|| text.contains("workflow")
|
||||
|| text.contains("testimonial")
|
||||
|| text.contains("customer proof")
|
||||
|| text.contains("logo")
|
||||
});
|
||||
let has_conversion_end = identities.iter().any(|text| {
|
||||
text.contains("pricing")
|
||||
|| text.contains("faq")
|
||||
|| text.contains("footer")
|
||||
|| text.contains("final cta")
|
||||
|| text.contains("call to action")
|
||||
});
|
||||
has_hero && has_supporting_story && has_conversion_end
|
||||
}
|
||||
|
||||
/// Returns `true` when the subtask is a dashboard-grade CONTENT section — a
|
||||
/// table, metric/KPI/stat block, or chart. Used (with a sidebar subtask) as the
|
||||
/// structural gate for pre-building the two-column scaffold, so a landing page
|
||||
|
|
@ -91,6 +129,21 @@ fn subtask_text(st: &Subtask) -> String {
|
|||
.to_lowercase()
|
||||
}
|
||||
|
||||
fn subtask_identity_text(st: &Subtask) -> String {
|
||||
format!("{} {}", st.id, st.label).to_lowercase()
|
||||
}
|
||||
|
||||
fn has_strong_sidebar_keyword(text: &str) -> bool {
|
||||
text.contains("sidebar")
|
||||
|| text.contains("side bar")
|
||||
|| text.contains("side nav")
|
||||
|| text.contains("side-nav")
|
||||
|| text.contains("left rail")
|
||||
|| text.contains("left nav")
|
||||
|| text.contains("nav rail")
|
||||
|| text.contains("navigation rail")
|
||||
}
|
||||
|
||||
/// `/(sidebar|side bar|navigation|nav|menu)/`
|
||||
fn has_sidebar_keyword(text: &str) -> bool {
|
||||
text.contains("sidebar")
|
||||
|
|
@ -102,7 +155,20 @@ fn has_sidebar_keyword(text: &str) -> bool {
|
|||
|
||||
/// `/(top bar|header)/`
|
||||
fn has_topbar_keyword(text: &str) -> bool {
|
||||
text.contains("top bar") || text.contains("header")
|
||||
text.contains("top bar")
|
||||
|| text.contains("header")
|
||||
|| text.contains("top navigation")
|
||||
|| text.contains("navigation bar")
|
||||
|| text.contains("nav bar")
|
||||
|| text.contains("navbar")
|
||||
}
|
||||
|
||||
fn has_explicit_landing_page_intent(text: &str) -> bool {
|
||||
text.contains("landing page") || text.contains("landing-page")
|
||||
}
|
||||
|
||||
fn has_explicit_dashboard_intent(text: &str) -> bool {
|
||||
text.contains("dashboard") || text.contains("admin console") || text.contains("admin-console")
|
||||
}
|
||||
|
||||
/// `/(dashboard|admin|analytics|fintech|workspace|data)/`
|
||||
|
|
|
|||
|
|
@ -63,10 +63,24 @@ fn sidebar_subtask_true_for_nav_in_elements() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_subtask_false_for_top_bar() {
|
||||
// "Top Navigation Bar" does NOT contain "top bar" or "header",
|
||||
// so it IS a sidebar — use a proper "Top Bar" example for the false case.
|
||||
let st = subtask("top-bar", "Top Bar", None);
|
||||
fn sidebar_subtask_false_for_navigation_bar() {
|
||||
let st = subtask("nav", "Navigation Bar", None);
|
||||
assert!(!is_sidebar_subtask(&st));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_subtask_true_for_sidebar_navigation_bar() {
|
||||
let st = subtask("sidebar", "Sidebar Navigation Bar", None);
|
||||
assert!(is_sidebar_subtask(&st));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_subtask_false_for_footer_navigation_links() {
|
||||
let st = subtask(
|
||||
"cta",
|
||||
"Final CTA & Footer",
|
||||
Some("conversion CTA, footer navigation links, legal links"),
|
||||
);
|
||||
assert!(!is_sidebar_subtask(&st));
|
||||
}
|
||||
|
||||
|
|
@ -119,6 +133,78 @@ fn dashboard_like_false_for_landing_page() {
|
|||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_landing_page_intent_vetoes_dashboard_intent() {
|
||||
let plan = plan_with(
|
||||
1440.0,
|
||||
vec![subtask(
|
||||
"analytics",
|
||||
"Analytics Dashboard",
|
||||
Some("KPI cards and revenue chart"),
|
||||
)],
|
||||
);
|
||||
assert!(!is_dashboard_like_prompt(
|
||||
"Design a landing-page for an analytics dashboard",
|
||||
&plan
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_dashboard_intent_wins_over_landing_anatomy() {
|
||||
let plan = plan_with(
|
||||
1440.0,
|
||||
vec![
|
||||
subtask("hero", "Hero Summary", Some("analytics KPIs")),
|
||||
subtask("workflow", "Workflow", Some("operations activity")),
|
||||
subtask("footer", "Footer", Some("workspace links")),
|
||||
],
|
||||
);
|
||||
assert!(plan_has_landing_anatomy(&plan));
|
||||
assert!(is_dashboard_like_prompt(
|
||||
"Design an analytics dashboard for a growth team",
|
||||
&plan
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_admin_console_intent_wins_over_landing_anatomy() {
|
||||
let plan = plan_with(
|
||||
1440.0,
|
||||
vec![
|
||||
subtask("hero", "Hero Summary", Some("account status")),
|
||||
subtask("workflow", "Workflow", Some("support queue")),
|
||||
subtask("footer", "Footer", Some("admin links")),
|
||||
],
|
||||
);
|
||||
assert!(plan_has_landing_anatomy(&plan));
|
||||
assert!(is_dashboard_like_prompt(
|
||||
"Design an admin-console for support operations",
|
||||
&plan
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dashboard_like_false_for_landing_page_with_data_sections() {
|
||||
let plan = plan_with(
|
||||
1440.0,
|
||||
vec![
|
||||
subtask("nav", "Navigation Bar", Some("main navigation links")),
|
||||
subtask("hero", "Hero Section", Some("headline and product visual")),
|
||||
subtask(
|
||||
"capabilities",
|
||||
"Capability Stories",
|
||||
Some("live node graph"),
|
||||
),
|
||||
subtask("proof", "Customer Proof", Some("three key metrics cards")),
|
||||
subtask("faq", "FAQ", Some("data privacy guarantees")),
|
||||
],
|
||||
);
|
||||
assert!(!is_dashboard_like_prompt(
|
||||
"Design a responsive website for an AI workbench",
|
||||
&plan
|
||||
));
|
||||
}
|
||||
|
||||
// ---- infer_dashboard_section_height ----------------------------------------
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -557,10 +557,20 @@ pub(crate) const CONTENT_COLUMN_NAME: &str = "Main Content";
|
|||
pub(crate) fn plan_is_sidebar_dashboard(plan: &OrchestratorPlan, is_mobile: bool) -> bool {
|
||||
use crate::dashboard_columns::{
|
||||
is_dashboard_content_subtask, is_sidebar_subtask, is_strong_sidebar_subtask,
|
||||
plan_has_landing_anatomy,
|
||||
};
|
||||
if is_mobile || plan.root_frame.width < 900.0 {
|
||||
return false;
|
||||
}
|
||||
let Some(first) = plan.subtasks.first() else {
|
||||
return false;
|
||||
};
|
||||
if !is_sidebar_subtask(first) {
|
||||
return false;
|
||||
}
|
||||
if !is_strong_sidebar_subtask(first) && plan_has_landing_anatomy(plan) {
|
||||
return false;
|
||||
}
|
||||
let sidebars = plan
|
||||
.subtasks
|
||||
.iter()
|
||||
|
|
@ -581,7 +591,7 @@ pub(crate) fn plan_is_sidebar_dashboard(plan: &OrchestratorPlan, is_mobile: bool
|
|||
// a sidebar dashboard whose sections lack table/metric/chart keywords used
|
||||
// to fall through to the single-root path and fill the sidebar full-width
|
||||
// during streaming.
|
||||
if plan.subtasks.iter().any(is_strong_sidebar_subtask) {
|
||||
if is_strong_sidebar_subtask(first) {
|
||||
return true;
|
||||
}
|
||||
// Only an AMBIGUOUS nav/menu signal → require >=2 data-content sections so a
|
||||
|
|
|
|||
|
|
@ -253,6 +253,21 @@ fn plan_is_sidebar_dashboard_false_for_landing_nav_without_data() {
|
|||
assert!(!plan_is_sidebar_dashboard(&p, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_is_sidebar_dashboard_false_for_landing_nav_with_graph_and_metrics() {
|
||||
let mut p = plan();
|
||||
p.root_frame.width = 1440.0;
|
||||
p.subtasks = vec![
|
||||
st("nav", "Navigation"),
|
||||
st("hero", "Hero Section"),
|
||||
st("capabilities", "Capability Graph"),
|
||||
st("proof", "Customer Metrics"),
|
||||
st("pricing", "Pricing"),
|
||||
st("cta", "Final CTA & Footer Navigation"),
|
||||
];
|
||||
assert!(!plan_is_sidebar_dashboard(&p, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_is_sidebar_dashboard_false_when_narrow() {
|
||||
let mut p = sidebar_dashboard_plan();
|
||||
|
|
|
|||
Loading…
Reference in a new issue