Просмотр исходного кода

web: Agent Landing Page A/B testing toolkit (#9018)

Co-authored-by: Roo Code <[email protected]>
Bruno Bergher 1 месяц назад
Родитель
Сommit
39448f4010

+ 94 - 0
apps/web-roo-code/src/app/pr-fixer/content-a.tsx

@@ -0,0 +1,94 @@
+import { type AgentPageContent } from "@/app/shared/agent-page-content"
+import Link from "next/link"
+
+// Workaround for next/image choking on these for some reason
+import hero from "/public/heroes/agent-pr-fixer.png"
+
+// Re-export for convenience
+export type { AgentPageContent }
+
+export const content: AgentPageContent = {
+	agentName: "PR Fixer",
+	hero: {
+		icon: "Wrench",
+		heading: "State-of-the-art fixes for the comments on your PRs.",
+		paragraphs: [
+			"Roo Code's PR Fixer applies high-quality changes to your PRs, right from GitHub. Invoke via a PR comment and it will read the entire comment history to understand context, agreements, and tradeoffs — then implement the right fix.",
+			"As always, you bring the model key; we orchestrate smart, efficient workflows.",
+		],
+		image: {
+			url: hero.src,
+			width: 800,
+			height: 711,
+			alt: "Example of a PR Fixer applying changes from review comments",
+		},
+		crossAgentLink: {
+			text: "Works great with",
+			links: [
+				{
+					text: "PR Reviewer Agent",
+					href: "/reviewer",
+					icon: "GitPullRequest",
+				},
+			],
+		},
+		cta: {
+			buttonText: "Start 14-day Free Trial",
+			disclaimer: "(cancel anytime)",
+		},
+	},
+	howItWorks: {
+		heading: "How It Works",
+		steps: [
+			{
+				title: "1. Connect your GitHub repositories",
+				description: "Pick which repos the PR Fixer can work on by pushing to ongoing branches.",
+				icon: "GitPullRequest",
+			},
+			{
+				title: "2. Invoke from a comment",
+				description:
+					'Ask the agent to fix issues directly from GitHub PR comments (e.g. "@roomote: fix these review comments"). It\'s fully aware of the entire comment history and latest diffs and focuses on fixing them – not random changes to your code.',
+				icon: "MessageSquareCode",
+			},
+			{
+				title: "3. Get clean scoped commits",
+				description: (
+					<>
+						The agent proposes targeted changes and pushes concise commits or patch suggestions you (or{" "}
+						<Link href="/reviewer">PR Reviewer</Link>) can review and merge quickly.
+					</>
+				),
+				icon: "Wrench",
+			},
+		],
+	},
+	whyBetter: {
+		heading: "Why Roo Code's PR Fixer is different",
+		features: [
+			{
+				title: "Comment-history aware",
+				description:
+					"Understands the entire conversation on the PR – previous reviews, your replies, follow-ups – and uses that context to produce accurate fixes.",
+				icon: "History",
+			},
+			{
+				title: "Bring your own key",
+				description:
+					"Use your preferred models at full strength. We optimize prompts and execution without capping your model to protect our margins.",
+				icon: "Key",
+			},
+			{
+				title: "Repository- and diff-aware",
+				description:
+					"Analyzes the full repo context and the latest diff to ensure fixes align with project conventions and pass checks.",
+				icon: "GitPullRequest",
+			},
+		],
+	},
+	cta: {
+		heading: "Ship fixes, not follow-ups.",
+		description: "Let Roo Code's PR Fixer turn your review feedback into clean, ready-to-merge commits.",
+		buttonText: "Start 14-day Free Trial",
+	},
+}

+ 10 - 3
apps/web-roo-code/src/app/pr-fixer/page.tsx

@@ -2,7 +2,9 @@ import type { Metadata } from "next"
 
 import { SEO } from "@/lib/seo"
 import { ogImageUrl } from "@/lib/og"
-import { PrFixerContent } from "./PrFixerContent"
+import { AgentLandingContent } from "@/app/shared/AgentLandingContent"
+import { getContentVariant } from "@/app/shared/getContentVariant"
+import { content as contentA } from "./content-a"
 
 const TITLE = "PR Fixer"
 const DESCRIPTION =
@@ -55,6 +57,11 @@ export const metadata: Metadata = {
 	],
 }
 
-export default function AgentPrFixerPage() {
-	return <PrFixerContent />
+export default async function AgentPrFixerPage({ searchParams }: { searchParams: Promise<{ v?: string }> }) {
+	const params = await searchParams
+	const content = getContentVariant(params, {
+		A: contentA,
+	})
+
+	return <AgentLandingContent content={content} />
 }

+ 0 - 296
apps/web-roo-code/src/app/reviewer/ReviewerContent.tsx

@@ -1,296 +0,0 @@
-"use client"
-
-import {
-	ArrowRight,
-	Blocks,
-	BookMarked,
-	ListChecks,
-	LucideIcon,
-	GitPullRequest,
-	Key,
-	MessageSquareCode,
-	Wrench,
-} from "lucide-react"
-import Image from "next/image"
-import Link from "next/link"
-
-import { Button } from "@/components/ui"
-import { AnimatedBackground } from "@/components/homepage"
-import { AgentCarousel } from "@/components/reviewer/agent-carousel"
-import { EXTERNAL_LINKS } from "@/lib/constants"
-import { trackGoogleAdsConversion } from "@/lib/analytics/google-ads"
-
-interface Feature {
-	icon: LucideIcon
-	title: string
-	description: string | React.ReactNode
-	logos?: string[]
-}
-
-const workflowSteps: Feature[] = [
-	{
-		icon: GitPullRequest,
-		title: "1. Connect Your Repository",
-		description: "Link your GitHub repository and configure which branches and pull requests should be reviewed.",
-	},
-	{
-		icon: Key,
-		title: "2. Add Your API Key",
-		description:
-			"Provide your AI provider API key and set your review preferences, custom rules, and quality standards.",
-	},
-	{
-		icon: MessageSquareCode,
-		title: "3. Get Review Comments",
-		description:
-			"Every pull request gets detailed GitHub comments in minutes from a Roo Code agent highlighting issues and suggesting improvements.",
-	},
-]
-
-const howItWorks: Feature[] = [
-	{
-		icon: Blocks,
-		title: "Our agents, your provider keys",
-		description: (
-			<>
-				<p>
-					We orchestrate the review, optimize the hell out of the prompts, integrate with GitHub, keep you
-					properly posted.
-				</p>
-				<p>We&apos;re thoughtful about token usage, but not incentivized to skimp to grow our margins.</p>
-			</>
-		),
-	},
-	{
-		icon: ListChecks,
-		title: "Advanced reasoning and workflows",
-		description:
-			"We optimize for state-of-the-art reasoning models and leverage powerful workflows (Diff analysis → Context Gathering → Impact Mapping → Contract checks) to produce crisp, actionable comments at the right level.",
-	},
-	{
-		icon: BookMarked,
-		title: "Fully repository-aware",
-		description:
-			"Reviews traverse code ownership, dependency graphs, and historical patterns to surface risk and deviations, not noise.",
-	},
-]
-
-// Workaround for next/image choking on these for some reason
-import hero from "/public/heroes/agent-reviewer.png"
-
-export function ReviewerContent() {
-	return (
-		<>
-			<section className="relative flex md:h-[calc(70vh-theme(spacing.12))] items-center overflow-hidden">
-				<AnimatedBackground />
-				<div className="container relative flex items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
-					<div className="grid h-full relative gap-4 md:gap-20 lg:grid-cols-2">
-						<div className="flex flex-col px-4 justify-center space-y-6 sm:space-y-8">
-							<div>
-								<h1 className="text-3xl font-bold tracking-tight mt-8  md:text-left md:text-4xl lg:text-5xl lg:mt-0">
-									<GitPullRequest className="size-12 mb-4" />
-									Get comprehensive code reviews that save you time, not&nbsp;tokens.
-								</h1>
-
-								<div className="mt-4 max-w-lg space-y-4 text-base text-muted-foreground md:text-left sm:mt-6">
-									<p>
-										Regular AI code review tools cap model usage to protect their margins from fixed
-										monthly prices. That leads to shallow prompts, limited context, and missed
-										issues.
-									</p>
-									<p>
-										Roo Code&apos;s PR Reviewer flips the script: you bring your own key and
-										leverage it to the max – to find real issues, increase code quality and keep
-										your pull request queue moving.
-									</p>
-								</div>
-
-								{/* Cross-agent link */}
-								<div className="mt-6 flex flex-col md:flex-row md:items-center gap-2">
-									Works great with
-									<Link
-										href="/pr-fixer"
-										className="flex p-4 items-center rounded-full border border-blue-500/30 bg-blue-500/10 px-3 py-1 text-sm text-blue-600 backdrop-blur-sm transition-colors hover:bg-blue-500/20 dark:text-blue-400"
-										aria-label="Works great with PR Fixer">
-										<Wrench className="size-4 mr-2" />
-										PR Fixer Agent
-										<ArrowRight className="ml-2 h-4 w-4" />
-									</Link>
-								</div>
-							</div>
-							<div className="flex flex-col space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0 md:items-center">
-								<Button
-									size="lg"
-									className="w-full sm:w-auto backdrop-blur-sm border hover:shadow-[0_0_20px_rgba(59,130,246,0.5)] transition-all duration-300"
-									asChild>
-									<a
-										href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_PRO}
-										target="_blank"
-										rel="noopener noreferrer"
-										onClick={trackGoogleAdsConversion}
-										className="flex w-full items-center justify-center">
-										Start 14-day Free Trial
-										<ArrowRight className="ml-2" />
-									</a>
-								</Button>
-								<span className="text-sm text-center md:text-left text-muted-foreground md:ml-2">
-									(cancel anytime)
-								</span>
-							</div>
-						</div>
-						<div className="flex items-center justify-end mx-auto h-full mt-8 lg:mt-0">
-							<div className="md:w-[800px] md:h-[474px] relative overflow-clip">
-								<div className="block">
-									<Image
-										src={hero}
-										alt="Example of a code review generated by Roo Code PR Reviewer"
-										className="max-w-full h-auto"
-										width={800}
-										height={474}
-									/>
-								</div>
-							</div>
-						</div>
-					</div>
-				</div>
-			</section>
-
-			{/* How It Works Section */}
-			<section className="relative overflow-hidden border-t border-border py-32">
-				<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
-					<div className="mx-auto mb-12 md:mb-24 max-w-5xl text-center">
-						<div>
-							<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">How It Works</h2>
-						</div>
-					</div>
-
-					<div className="relative mx-auto md:max-w-[1200px]">
-						<ul className="grid grid-cols-1 place-items-center gap-6 md:grid-cols-3 lg:gap-8">
-							{workflowSteps.map((step, index) => {
-								const Icon = step.icon
-								return (
-									<li
-										key={index}
-										className="relative h-full border border-border rounded-2xl bg-background p-8 transition-all duration-300 hover:shadow-lg">
-										<Icon className="size-6 text-foreground/80" />
-										<h3 className="mb-3 mt-3 text-xl font-semibold text-foreground">
-											{step.title}
-										</h3>
-										<div className="leading-relaxed font-light text-muted-foreground">
-											{step.description}
-										</div>
-									</li>
-								)
-							})}
-						</ul>
-					</div>
-				</div>
-			</section>
-
-			<section className="relative overflow-hidden border-t border-border py-32">
-				<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
-					<div className="mx-auto mb-12 md:mb-24 max-w-5xl text-center">
-						<div>
-							<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
-								Why Roo&apos;s PR Reviewer is so much better
-							</h2>
-						</div>
-					</div>
-
-					<div className="relative mx-auto md:max-w-[1200px]">
-						<ul className="grid grid-cols-1 place-items-center gap-6 md:grid-cols-2 lg:grid-cols-3 lg:gap-8">
-							{howItWorks.map((feature, index) => {
-								const Icon = feature.icon
-								return (
-									<li
-										key={index}
-										className="relative h-full border border-border rounded-2xl bg-background p-8 transition-all duration-300">
-										<Icon className="size-6 text-foreground/80" />
-										<h3 className="mb-3 mt-3 text-xl font-semibold text-foreground">
-											{feature.title}
-										</h3>
-										<div className="leading-relaxed font-light text-muted-foreground space-y-2">
-											{feature.description}
-										</div>
-										{feature.logos && (
-											<div className="mt-4 flex flex-wrap items-center gap-4">
-												{feature.logos.map((logo) => (
-													<Image
-														key={logo}
-														width={20}
-														height={20}
-														className="w-5 h-5 overflow-clip opacity-50 dark:invert"
-														src={`/logos/${logo.toLowerCase()}.svg`}
-														alt={`${logo} Logo`}
-													/>
-												))}
-											</div>
-										)}
-									</li>
-								)
-							})}
-						</ul>
-					</div>
-				</div>
-			</section>
-
-			<section className="relative overflow-hidden border-t border-border py-32">
-				<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
-					<div className="mx-auto mb-12 max-w-4xl text-center">
-						<div>
-							<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
-								The first member of a whole new team
-							</h2>
-
-							<p className="mt-6 text-lg text-muted-foreground">
-								Architecture, coding, reviewing, testing, debugging, documenting, designing –{" "}
-								<em>almost everything</em> we do today is mostly through our agents. Now we&apos;re
-								bringing them to you.
-							</p>
-							<p className="mt-2 text-lg text-muted-foreground">
-								Roo&apos;s PR Reviewer isn&apos;t yet another single-purpose tool to add to your already
-								complicated stack.
-								<br />
-								It&apos;s the first member of your AI-powered development team. More agents are shipping
-								soon.
-							</p>
-						</div>
-					</div>
-
-					<div className="relative mx-auto md:max-w-[1200px]">
-						<AgentCarousel />
-					</div>
-				</div>
-			</section>
-
-			{/* CTA Section */}
-			<section className="py-20">
-				<div className="container mx-auto px-4 sm:px-6 lg:px-8">
-					<div className="mx-auto max-w-4xl rounded-3xl border border-border/50 bg-gradient-to-br from-blue-500/5 via-cyan-500/5 to-purple-500/5 p-8 text-center shadow-2xl backdrop-blur-xl dark:border-white/20 dark:bg-gradient-to-br dark:from-gray-800 dark:via-gray-900 dark:to-black sm:p-12">
-						<h2 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl">Stop wasting time.</h2>
-						<p className="mx-auto mb-8 max-w-2xl text-lg text-muted-foreground">
-							Give Roo Code&apos;s PR Reviewer your model key and turn painful reviews into a tangible
-							quality advantage.
-						</p>
-						<div className="flex flex-col justify-center space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0">
-							<Button
-								size="lg"
-								className="bg-black text-white hover:bg-gray-800 hover:shadow-lg hover:shadow-black/20 dark:bg-white dark:text-black dark:hover:bg-gray-200 dark:hover:shadow-white/20 transition-all duration-300"
-								asChild>
-								<a
-									href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_PRO}
-									target="_blank"
-									rel="noopener noreferrer"
-									onClick={trackGoogleAdsConversion}
-									className="flex items-center justify-center">
-									Start 14-day Free Trial
-									<ArrowRight className="ml-2 h-4 w-4" />
-								</a>
-							</Button>
-						</div>
-					</div>
-				</div>
-			</section>
-		</>
-	)
-}

+ 92 - 0
apps/web-roo-code/src/app/reviewer/content-b.ts

@@ -0,0 +1,92 @@
+import { type AgentPageContent } from "@/app/shared/agent-page-content"
+
+// Workaround for next/image choking on these for some reason
+import hero from "/public/heroes/agent-reviewer.png"
+
+// Re-export for convenience
+export type { AgentPageContent }
+
+export const content: AgentPageContent = {
+	agentName: "PR Reviewer",
+	hero: {
+		icon: "GitPullRequest",
+		heading: "Code reviews that catch what other AI tools (and most humans) miss.",
+		paragraphs: [
+			"Run-of-the-mill, token-saving AI code review tools will surely catch syntax errors and style issues, but they'll usually miss the bugs that actually matter: logic flaws, security vulnerabilities, and misunderstood requirements.",
+			"Roo Code's PR Reviewer uses advanced reasoning models and full repository context to find the issues that slip through—before they reach production.",
+		],
+		image: {
+			url: hero.src,
+			width: 800,
+			height: 474,
+			alt: "Example of a code review generated by Roo Code PR Reviewer",
+		},
+		crossAgentLink: {
+			text: "Works great with",
+			links: [
+				{
+					text: "PR Fixer Agent",
+					href: "/pr-fixer",
+					icon: "Wrench",
+				},
+			],
+		},
+		cta: {
+			buttonText: "Start 14-day Free Trial",
+			disclaimer: "(cancel anytime)",
+		},
+	},
+	howItWorks: {
+		heading: "How It Works",
+		steps: [
+			{
+				title: "1. Connect Your Repository",
+				description:
+					"Link your GitHub repository and configure which branches and pull requests should be reviewed.",
+				icon: "GitPullRequest",
+			},
+			{
+				title: "2. Add Your API Key",
+				description:
+					"Provide your AI provider API key and set your review preferences, custom rules, and quality standards.",
+				icon: "Key",
+			},
+			{
+				title: "3. Get Review Comments",
+				description:
+					"Every pull request gets detailed GitHub comments in minutes from a Roo Code agent highlighting issues and suggesting improvements.",
+				icon: "MessageSquareCode",
+			},
+		],
+	},
+	whyBetter: {
+		heading: "Why Roo's PR Reviewer is different",
+		features: [
+			{
+				title: "Bring your own key, get uncompromised reviews",
+				paragraphs: [
+					"Most AI review tools use fixed pricing, which means they skimp on tokens to protect their margins. That leads to shallow analysis and missed issues.",
+					"With Roo, you bring your own API key. We optimize prompts for depth, not cost-cutting, so reviews focus on real problems like business logic, security vulnerabilities, and architectural issues.",
+				],
+				icon: "Blocks",
+			},
+			{
+				title: "Advanced reasoning that understands what matters",
+				description:
+					"We leverage state-of-the-art reasoning models with sophisticated workflows: diff analysis, context gathering, impact mapping, and contract validation. This catches the subtle bugs that surface-level tools miss—misunderstood requirements, edge cases, and integration risks.",
+				icon: "ListChecks",
+			},
+			{
+				title: "Repository-aware, not snippet-aware",
+				description:
+					"Roo analyzes your entire codebase context—dependency graphs, code ownership, team conventions, and historical patterns. It understands how changes interact with existing systems, not just whether individual lines look correct.",
+				icon: "BookMarked",
+			},
+		],
+	},
+	cta: {
+		heading: "Ready for better code reviews?",
+		description: "Start finding the issues that matter with AI-powered reviews built for depth, not cost-cutting.",
+		buttonText: "Start 14-day Free Trial",
+	},
+}

+ 93 - 0
apps/web-roo-code/src/app/reviewer/content.ts

@@ -0,0 +1,93 @@
+import { type AgentPageContent } from "@/app/shared/agent-page-content"
+
+// Workaround for next/image choking on these for some reason
+import hero from "/public/heroes/agent-reviewer.png"
+
+// Re-export for convenience
+export type { AgentPageContent }
+
+export const content: AgentPageContent = {
+	agentName: "PR Reviewer",
+	hero: {
+		icon: "GitPullRequest",
+		heading: "Get comprehensive code reviews that save you time, not tokens.",
+		paragraphs: [
+			"Regular AI code review tools cap model usage to protect their margins from fixed monthly prices. That leads to shallow prompts, limited context, and missed issues.",
+			"Roo Code's PR Reviewer flips the script: you bring your own key and leverage it to the max – to find real issues, increase code quality and keep your pull request queue moving.",
+		],
+		image: {
+			url: hero.src,
+			width: 800,
+			height: 474,
+			alt: "Example of a code review generated by Roo Code PR Reviewer",
+		},
+		crossAgentLink: {
+			text: "Works great with",
+			links: [
+				{
+					text: "PR Fixer Agent",
+					href: "/pr-fixer",
+					icon: "Wrench",
+				},
+			],
+		},
+		cta: {
+			buttonText: "Start 14-day Free Trial",
+			disclaimer: "(cancel anytime)",
+		},
+	},
+	howItWorks: {
+		heading: "How It Works",
+		steps: [
+			{
+				title: "1. Connect Your Repository",
+				description:
+					"Link your GitHub repository and configure which branches and pull requests should be reviewed.",
+				icon: "GitPullRequest",
+			},
+			{
+				title: "2. Add Your API Key",
+				description:
+					"Provide your AI provider API key and set your review preferences, custom rules, and quality standards.",
+				icon: "Key",
+			},
+			{
+				title: "3. Get Review Comments",
+				description:
+					"Every pull request gets detailed GitHub comments in minutes from a Roo Code agent highlighting issues and suggesting improvements.",
+				icon: "MessageSquareCode",
+			},
+		],
+	},
+	whyBetter: {
+		heading: "Why Roo's PR Reviewer is so much better",
+		features: [
+			{
+				title: "Our agents, your provider keys",
+				paragraphs: [
+					"We orchestrate the review, optimize the hell out of the prompts, integrate with GitHub, keep you properly posted.",
+					"We're thoughtful about token usage, but not incentivized to skimp to grow our margins.",
+				],
+				icon: "Blocks",
+			},
+			{
+				title: "Advanced reasoning and workflows",
+				description:
+					"We optimize for state-of-the-art reasoning models and leverage powerful workflows (Diff analysis → Context Gathering → Impact Mapping → Contract checks) to produce crisp, actionable comments at the right level.",
+				icon: "ListChecks",
+			},
+			{
+				title: "Fully repository-aware",
+				description:
+					"Reviews traverse code ownership, dependency graphs, and historical patterns to surface risk and deviations, not noise.",
+				icon: "BookMarked",
+			},
+		],
+	},
+	cta: {
+		heading: "Stop wasting time.",
+		description:
+			"Give Roo Code's PR Reviewer your model key and turn painful reviews into a tangible quality advantage.",
+		buttonText: "Start 14-day Free Trial",
+	},
+}

+ 12 - 3
apps/web-roo-code/src/app/reviewer/page.tsx

@@ -2,7 +2,10 @@ import type { Metadata } from "next"
 
 import { SEO } from "@/lib/seo"
 import { ogImageUrl } from "@/lib/og"
-import { ReviewerContent } from "./ReviewerContent"
+import { AgentLandingContent } from "@/app/shared/AgentLandingContent"
+import { getContentVariant } from "@/app/shared/getContentVariant"
+import { content as contentA } from "./content"
+import { content as contentB } from "./content-b"
 
 const TITLE = "PR Reviewer"
 const DESCRIPTION =
@@ -56,6 +59,12 @@ export const metadata: Metadata = {
 	],
 }
 
-export default function AgentReviewerPage() {
-	return <ReviewerContent />
+export default async function AgentReviewerPage({ searchParams }: { searchParams: Promise<{ v?: string }> }) {
+	const params = await searchParams
+	const content = getContentVariant(params, {
+		A: contentA,
+		B: contentB,
+	})
+
+	return <AgentLandingContent content={content} />
 }

+ 103 - 102
apps/web-roo-code/src/app/pr-fixer/PrFixerContent.tsx → apps/web-roo-code/src/app/shared/AgentLandingContent.tsx

@@ -1,105 +1,90 @@
 "use client"
 
-import { ArrowRight, GitPullRequest, History, Key, MessageSquareCode, Wrench, type LucideIcon } from "lucide-react"
+import {
+	ArrowRight,
+	GitPullRequest,
+	Wrench,
+	Key,
+	MessageSquareCode,
+	Blocks,
+	ListChecks,
+	BookMarked,
+	History,
+	LucideIcon,
+} from "lucide-react"
 import Image from "next/image"
 import Link from "next/link"
 
 import { Button } from "@/components/ui"
 import { AnimatedBackground } from "@/components/homepage"
+import { AgentCarousel } from "@/components/reviewer/agent-carousel"
 import { EXTERNAL_LINKS } from "@/lib/constants"
 import { trackGoogleAdsConversion } from "@/lib/analytics/google-ads"
-
-// Workaround for next/image choking on these for some reason
-import hero from "/public/heroes/agent-pr-fixer.png"
-
-interface Feature {
-	icon: LucideIcon
-	title: string
-	description: string | React.ReactNode
-	logos?: string[]
+import { type AgentPageContent, type IconName } from "./agent-page-content"
+
+/**
+ * Maps icon names to actual Lucide icon components
+ */
+const iconMap: Record<IconName, LucideIcon> = {
+	GitPullRequest,
+	Wrench,
+	Key,
+	MessageSquareCode,
+	Blocks,
+	ListChecks,
+	BookMarked,
+	History,
 }
 
-const workflowSteps: Feature[] = [
-	{
-		icon: GitPullRequest,
-		title: "1. Connect your GitHub repositories",
-		description: "Pick which repos the PR Fixer can work on by pushing to ongoing branches.",
-	},
-	{
-		icon: MessageSquareCode,
-		title: "2. Invoke from a comment",
-		description:
-			'Ask the agent to fix issues directly from GitHub PR comments (e.g. "@roomote: fix these review comments"). It’s fully aware of the entire comment history and latest diffs and focuses on fixing them – not random changes to your code.',
-	},
-	{
-		icon: Wrench,
-		title: "3. Get clean scoped commits",
-		description: (
-			<>
-				The agent proposes targeted changes and pushes concise commits or patch suggestions you (or{" "}
-				<Link href="/pr-reviewer">PR Reviewer</Link>) can review and merge quickly.
-			</>
-		),
-	},
-]
-
-const howItWorks: Feature[] = [
-	{
-		icon: History,
-		title: "Comment-history aware",
-		description:
-			"Understands the entire conversation on the PR – previous reviews, your replies, follow-ups – and uses that context to produce accurate fixes.",
-	},
-	{
-		icon: Key,
-		title: "Bring your own key",
-		description:
-			"Use your preferred models at full strength. We optimize prompts and execution without capping your model to protect our margins.",
-	},
-	{
-		icon: GitPullRequest,
-		title: "Repository- and diff-aware",
-		description:
-			"Analyzes the full repo context and the latest diff to ensure fixes align with project conventions and pass checks.",
-	},
-]
+/**
+ * Converts an icon name string to a Lucide icon component
+ */
+function getIcon(iconName?: IconName): LucideIcon | undefined {
+	return iconName ? iconMap[iconName] : undefined
+}
 
-export function PrFixerContent() {
+export function AgentLandingContent({ content }: { content: AgentPageContent }) {
 	return (
 		<>
+			{/* Hero Section */}
 			<section className="relative flex md:h-[calc(70vh-theme(spacing.12))] items-center overflow-hidden">
 				<AnimatedBackground />
 				<div className="container relative flex items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
 					<div className="grid h-full relative gap-4 md:gap-20 lg:grid-cols-2">
 						<div className="flex flex-col px-4 justify-center space-y-6 sm:space-y-8">
 							<div>
-								<h1 className="text-3xl font-bold tracking-tight mt-8  md:text-left md:text-4xl lg:text-5xl lg:mt-0">
-									<Wrench className="size-12 mb-4" />
-									State-of-the-art fixes for the comments on your PRs.
+								<h1 className="text-3xl font-bold tracking-tight mt-8 md:text-left md:text-4xl lg:text-5xl lg:mt-0">
+									{content.hero.icon &&
+										(() => {
+											const Icon = getIcon(content.hero.icon)
+											return Icon ? <Icon className="size-12 mb-4" /> : null
+										})()}
+									{content.hero.heading}
 								</h1>
 
 								<div className="mt-4 max-w-lg space-y-4 text-base text-muted-foreground md:text-left sm:mt-6">
-									<p>
-										Roo Code{"'"}s PR Fixer applies high-quality changes to your PRs, right from
-										GitHub. Invoke via a PR comment and it will read the entire comment history to
-										understand context, agreements, and tradeoffs — then implement the right fix.
-									</p>
-									<p>
-										As always, you bring the model key; we orchestrate smart, efficient workflows.
-									</p>
+									{content.hero.paragraphs.map((paragraph, index) => (
+										<p key={index}>{paragraph}</p>
+									))}
 								</div>
 
 								{/* Cross-agent link */}
 								<div className="mt-6 flex flex-col md:flex-row md:items-center gap-2">
-									Works great with
-									<Link
-										href="/reviewer"
-										className="flex p-4 items-center rounded-full border border-blue-500/30 bg-blue-500/10 px-3 py-1 text-sm text-blue-600 backdrop-blur-sm transition-colors hover:bg-blue-500/20 dark:text-blue-400"
-										aria-label="Works great with PR Reviewer">
-										<GitPullRequest className="size-4 mr-2" />
-										PR Reviewer Agent
-										<ArrowRight className="ml-2 h-4 w-4" />
-									</Link>
+									{content.hero.crossAgentLink.text}
+									{content.hero.crossAgentLink.links.map((link, index) => {
+										const Icon = getIcon(link.icon)
+										return (
+											<Link
+												key={index}
+												href={link.href}
+												className="flex p-4 items-center rounded-full border border-blue-500/30 bg-blue-500/10 px-3 py-1 text-sm text-blue-600 backdrop-blur-sm transition-colors hover:bg-blue-500/20 dark:text-blue-400"
+												aria-label={`Works great with ${link.text}`}>
+												{Icon && <Icon className="size-4 mr-2" />}
+												{link.text}
+												<ArrowRight className="ml-2 h-4 w-4" />
+											</Link>
+										)
+									})}
 								</div>
 							</div>
 
@@ -114,29 +99,37 @@ export function PrFixerContent() {
 										rel="noopener noreferrer"
 										onClick={trackGoogleAdsConversion}
 										className="flex w-full items-center justify-center">
-										Start 14-day Free Trial
+										{content.hero.cta.buttonText}
 										<ArrowRight className="ml-2" />
 									</a>
 								</Button>
 								<span className="text-sm text-center md:text-left text-muted-foreground md:ml-2">
-									(cancel anytime)
+									{content.hero.cta.disclaimer}
 								</span>
 							</div>
 						</div>
 
-						<div className="flex items-center justify-end mx-auto h-full mt-8 lg:mt-0">
-							<div className="md:w-[670px] md:h-[600px] relative overflow-clip">
-								<div className="block">
-									<Image
-										src={hero}
-										alt="Example of a PR Fixer applying changes from review comments"
-										className="max-w-full h-auto"
-										width={800}
-										height={711}
-									/>
+						{content.hero.image && (
+							<div className="flex items-center justify-end mx-auto h-full mt-8 lg:mt-0">
+								<div
+									className="relative overflow-clip"
+									style={{
+										width: `${content.hero.image.width}px`,
+										height: `${content.hero.image.height}px`,
+										maxWidth: "100%",
+									}}>
+									<div className="block">
+										<Image
+											src={content.hero.image.url}
+											alt={content.hero.image.alt || "Hero image"}
+											className="max-w-full h-auto"
+											width={content.hero.image.width}
+											height={content.hero.image.height}
+										/>
+									</div>
 								</div>
 							</div>
-						</div>
+						)}
 					</div>
 				</div>
 			</section>
@@ -146,19 +139,21 @@ export function PrFixerContent() {
 				<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
 					<div className="mx-auto mb-12 md:mb-24 max-w-5xl text-center">
 						<div>
-							<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">How It Works</h2>
+							<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
+								{content.howItWorks.heading}
+							</h2>
 						</div>
 					</div>
 
 					<div className="relative mx-auto md:max-w-[1200px]">
 						<ul className="grid grid-cols-1 place-items-center gap-6 md:grid-cols-3 lg:gap-8">
-							{workflowSteps.map((step, index) => {
-								const Icon = step.icon
+							{content.howItWorks.steps.map((step, index) => {
+								const Icon = getIcon(step.icon)
 								return (
 									<li
 										key={index}
 										className="relative h-full border border-border rounded-2xl bg-background p-8 transition-all duration-300 hover:shadow-lg">
-										<Icon className="size-6 text-foreground/80" />
+										{Icon && <Icon className="size-6 text-foreground/80" />}
 										<h3 className="mb-3 mt-3 text-xl font-semibold text-foreground">
 											{step.title}
 										</h3>
@@ -173,30 +168,35 @@ export function PrFixerContent() {
 				</div>
 			</section>
 
+			{/* Why Better Section */}
 			<section className="relative overflow-hidden border-t border-border py-32">
 				<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
 					<div className="mx-auto mb-12 md:mb-24 max-w-5xl text-center">
 						<div>
 							<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
-								Why Roo Code{"'"}s PR Fixer is different
+								{content.whyBetter.heading}
 							</h2>
 						</div>
 					</div>
 
 					<div className="relative mx-auto md:max-w-[1200px]">
 						<ul className="grid grid-cols-1 place-items-center gap-6 md:grid-cols-2 lg:grid-cols-3 lg:gap-8">
-							{howItWorks.map((feature, index) => {
-								const Icon = feature.icon
+							{content.whyBetter.features.map((feature, index) => {
+								const Icon = getIcon(feature.icon)
 								return (
 									<li
 										key={index}
 										className="relative h-full border border-border rounded-2xl bg-background p-8 transition-all duration-300">
-										<Icon className="size-6 text-foreground/80" />
+										{Icon && <Icon className="size-6 text-foreground/80" />}
 										<h3 className="mb-3 mt-3 text-xl font-semibold text-foreground">
 											{feature.title}
 										</h3>
 										<div className="leading-relaxed font-light text-muted-foreground space-y-2">
-											{feature.description}
+											{feature.description && <p>{feature.description}</p>}
+											{feature.paragraphs &&
+												feature.paragraphs.map((paragraph, pIndex) => (
+													<p key={pIndex}>{paragraph}</p>
+												))}
 										</div>
 									</li>
 								)
@@ -206,15 +206,16 @@ export function PrFixerContent() {
 				</div>
 			</section>
 
+			{/* Agent Carousel */}
+			<AgentCarousel currentAgent={content.agentName} />
+
 			{/* CTA Section */}
 			<section className="py-20">
 				<div className="container mx-auto px-4 sm:px-6 lg:px-8">
 					<div className="mx-auto max-w-4xl rounded-3xl border border-border/50 bg-gradient-to-br from-blue-500/5 via-cyan-500/5 to-purple-500/5 p-8 text-center shadow-2xl backdrop-blur-xl dark:border-white/20 dark:bg-gradient-to-br dark:from-gray-800 dark:via-gray-900 dark:to-black sm:p-12">
-						<h2 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl">
-							Ship fixes, not follow-ups.
-						</h2>
+						<h2 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl">{content.cta.heading}</h2>
 						<p className="mx-auto mb-8 max-w-2xl text-lg text-muted-foreground">
-							Let Roo Code{"'"}s PR Fixer turn your review feedback into clean, ready-to-merge commits.
+							{content.cta.description}
 						</p>
 						<div className="flex flex-col justify-center space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0">
 							<Button
@@ -227,7 +228,7 @@ export function PrFixerContent() {
 									rel="noopener noreferrer"
 									onClick={trackGoogleAdsConversion}
 									className="flex items-center justify-center">
-									Start 14-day Free Trial
+									{content.cta.buttonText}
 									<ArrowRight className="ml-2 h-4 w-4" />
 								</a>
 							</Button>

+ 75 - 0
apps/web-roo-code/src/app/shared/agent-page-content.ts

@@ -0,0 +1,75 @@
+/**
+ * Supported icon names that can be used in agent page content.
+ * These strings are mapped to actual Lucide components in the client.
+ */
+export type IconName =
+	| "GitPullRequest"
+	| "Wrench"
+	| "Key"
+	| "MessageSquareCode"
+	| "Blocks"
+	| "ListChecks"
+	| "BookMarked"
+	| "History"
+
+/**
+ * Generic content structure for agent landing pages.
+ * This interface can be reused across different agent pages (PR Reviewer, PR Fixer, etc.)
+ * to maintain consistency and enable A/B testing capabilities.
+ *
+ * Note: Icons are referenced by string names (not components) to support
+ * serialization from Server Components to Client Components.
+ */
+export interface AgentPageContent {
+	/** The agent name used for the carousel display */
+	agentName: string
+	hero: {
+		/** Optional icon name to display in the hero section */
+		icon?: IconName
+		heading: string
+		paragraphs: string[]
+		image?: {
+			url: string
+			width: number
+			height: number
+			alt?: string
+		}
+		crossAgentLink: {
+			text: string
+			links: Array<{
+				text: string
+				href: string
+				icon?: IconName
+			}>
+		}
+		cta: {
+			buttonText: string
+			disclaimer: string
+		}
+	}
+	howItWorks: {
+		heading: string
+		steps: Array<{
+			title: string
+			/** Supports rich text content including React components */
+			description: string | React.ReactNode
+			icon?: IconName
+		}>
+	}
+	whyBetter: {
+		heading: string
+		features: Array<{
+			title: string
+			/** Supports rich text content including React components */
+			description?: string | React.ReactNode
+			/** Supports rich text content including React components */
+			paragraphs?: Array<string | React.ReactNode>
+			icon?: IconName
+		}>
+	}
+	cta: {
+		heading: string
+		description: string
+		buttonText: string
+	}
+}

+ 36 - 0
apps/web-roo-code/src/app/shared/getContentVariant.ts

@@ -0,0 +1,36 @@
+import type { AgentPageContent } from "./agent-page-content"
+
+/**
+ * Selects the appropriate content variant based on the query parameter.
+ *
+ * @param searchParams - The search parameters from the page props
+ * @param variants - A record mapping variant letters to content objects
+ * @returns The selected content variant, defaulting to variant 'A' if not found or invalid
+ *
+ * @example
+ * ```tsx
+ * const content = getContentVariant(searchParams, {
+ *   A: contentA,
+ *   B: contentB,
+ *   C: contentC,
+ * })
+ * ```
+ */
+export function getContentVariant(
+	searchParams: { v?: string },
+	variants: Record<string, AgentPageContent>,
+): AgentPageContent {
+	const variant = searchParams.v?.toUpperCase()
+
+	// Return the specified variant if it exists, otherwise default to 'A'
+	if (variant && variants[variant]) {
+		return variants[variant]
+	}
+
+	// Ensure 'A' variant always exists as fallback
+	if (!variants.A) {
+		throw new Error("Content variants must include variant 'A' as the default")
+	}
+
+	return variants.A
+}

+ 84 - 37
apps/web-roo-code/src/components/reviewer/agent-carousel.tsx

@@ -4,15 +4,29 @@ import { useEffect } from "react"
 import { motion } from "framer-motion"
 import useEmblaCarousel from "embla-carousel-react"
 import AutoPlay from "embla-carousel-autoplay"
-import { Bug, FileText, Gauge, Languages, Microscope, PocketKnife, TestTube, type LucideIcon } from "lucide-react"
+import {
+	Bug,
+	FileText,
+	Gauge,
+	GitPullRequest,
+	Languages,
+	Microscope,
+	PocketKnife,
+	TestTube,
+	Wrench,
+	type LucideIcon,
+} from "lucide-react"
 
 // AI Agent types for the carousel
 interface AIAgent {
 	icon: LucideIcon
 	name: string
+	page?: string
 }
 
 const aiAgents: AIAgent[] = [
+	{ icon: GitPullRequest, name: "PR Reviewer", page: "/reviewer" },
+	{ icon: Wrench, name: "PR Fixer", page: "/pr-fixer" },
 	{ icon: PocketKnife, name: "Generalist" },
 	{ icon: Bug, name: "Bug Fixer" },
 	{ icon: TestTube, name: "Test Engineer" },
@@ -22,7 +36,7 @@ const aiAgents: AIAgent[] = [
 	{ icon: Languages, name: "String Translator" },
 ]
 
-export function AgentCarousel() {
+export function AgentCarousel({ currentAgent = "" }: { currentAgent?: string } = {}) {
 	const [emblaRef, emblaApi] = useEmblaCarousel(
 		{
 			loop: true,
@@ -82,44 +96,77 @@ export function AgentCarousel() {
 	const displayAgents = [...aiAgents, ...aiAgents]
 
 	return (
-		<motion.div
-			className="relative -mx-4 md:mx-auto max-w-[1400px]"
-			variants={containerVariants}
-			initial="hidden"
-			whileInView="visible"
-			viewport={{ once: true }}>
-			{/* Gradient Overlays */}
-			<div className="absolute inset-y-0 left-0 z-10 w-[10%] bg-gradient-to-r from-background to-transparent pointer-events-none md:w-[15%]" />
-			<div className="absolute inset-y-0 right-0 z-10 w-[10%] bg-gradient-to-l from-background to-transparent pointer-events-none md:w-[15%]" />
+		<section className="relative overflow-hidden border-t border-border py-32">
+			<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
+				<div className="mx-auto mb-12 max-w-4xl text-center">
+					<div>
+						<h2 className="text-4xl font-bold tracking-tight sm:text-5xl">
+							The first members of a whole new team
+						</h2>
+						<p className="mt-6 text-lg text-muted-foreground">
+							Architecture, coding, reviewing, testing, debugging, documenting, designing – almost
+							everything we do today is mostly through our agents. Now we&apos;re bringing them to you.
+						</p>
+						<p className="mt-2 text-lg text-muted-foreground">
+							Roo&apos;s {currentAgent} isn&apos;t yet another single-purpose tool to add to your already
+							complicated stack. It&apos;s the first member of your AI-powered development team. More
+							agents are shipping soon.
+						</p>
+					</div>
+				</div>
+
+				<div className="relative mx-auto md:max-w-[1200px]">
+					<motion.div
+						className="relative -mx-4 md:mx-auto max-w-[1400px]"
+						variants={containerVariants}
+						initial="hidden"
+						whileInView="visible"
+						viewport={{ once: true }}>
+						{/* Gradient Overlays */}
+						<div className="absolute inset-y-0 left-0 z-10 w-[10%] bg-gradient-to-r from-background to-transparent pointer-events-none md:w-[15%]" />
+						<div className="absolute inset-y-0 right-0 z-10 w-[10%] bg-gradient-to-l from-background to-transparent pointer-events-none md:w-[15%]" />
 
-			{/* Embla Carousel Container */}
-			<div className="overflow-hidden" ref={emblaRef}>
-				<div className="flex pb-4">
-					{displayAgents.map((agent, index) => {
-						const Icon = agent.icon
-						return (
-							<div
-								key={`${agent.name}-${index}`}
-								className="relative min-w-0 flex-[0_0_45%] px-2 md:flex-[0_0_30%] md:px-4 lg:flex-[0_0_15%]">
-								<div className="group relative py-6 cursor-default">
-									<div
-										className="relative flex flex-col items-center justify-center rounded-full w-[150px] h-[150px] border border-border bg-background p-6 transition-all duration-500 ease-out shadow-xl
-                                    hover:scale-110 hover:-translate-y-2
-                                    hover:shadow-[0_20px_50px_rgba(39,110,226,0.25)] dark:hover:shadow-[0_20px_50px_rgba(59,130,246,0.25)]">
-										<Icon
-											strokeWidth={1}
-											className="size-9 mb-2 text-foreground transition-colors duration-300"
-										/>
-										<h3 className="text-center leading-tight tracking-tight font-medium text-foreground/90 transition-colors duration-300 dark:text-foreground">
-											{agent.name}
-										</h3>
-									</div>
-								</div>
+						{/* Embla Carousel Container */}
+						<div className="overflow-hidden" ref={emblaRef}>
+							<div className="flex pb-4">
+								{displayAgents.map((agent, index) => {
+									const Icon = agent.icon
+									return (
+										<div
+											key={`${agent.name}-${index}`}
+											className="relative min-w-0 flex-[0_0_45%] px-2 md:flex-[0_0_30%] md:px-4 lg:flex-[0_0_15%]">
+											<div className="group relative py-6 cursor-default">
+												<div
+													className="relative flex flex-col items-center justify-center rounded-full w-[150px] h-[150px] border border-border bg-background p-6 transition-all duration-500 ease-out shadow-xl
+													hover:scale-110 hover:-translate-y-2
+													hover:shadow-[0_20px_50px_rgba(39,110,226,0.25)] dark:hover:shadow-[0_20px_50px_rgba(59,130,246,0.25)]">
+													<Icon
+														strokeWidth={1}
+														className="size-9 mb-2 text-foreground transition-colors duration-300"
+													/>
+													<h3 className="text-center leading-tight tracking-tight transition-colors duration-300 dark:text-foreground">
+														{agent.page ? (
+															<a
+																href={agent.page}
+																className="text-foreground/90 font-semibold">
+																{agent.name}
+															</a>
+														) : (
+															<span className="text-foreground/60 font-medium">
+																{agent.name}
+															</span>
+														)}
+													</h3>
+												</div>
+											</div>
+										</div>
+									)
+								})}
 							</div>
-						)
-					})}
+						</div>
+					</motion.div>
 				</div>
 			</div>
-		</motion.div>
+		</section>
 	)
 }