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

Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
Bruno Bergher 2025-11-05 20:40:04 +00:00 committed by GitHub
parent 413ab1a95c
commit 39448f4010
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 598 additions and 440 deletions

View file

@ -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",
},
}

View file

@ -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} />
}

View file

@ -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>
</>
)
}

View file

@ -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",
},
}

View file

@ -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",
},
}

View file

@ -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} />
}

View file

@ -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"
import { type AgentPageContent, type IconName } from "./agent-page-content"
// 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[]
/**
* 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"). Its 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.
</>
),
},
]
/**
* Converts an icon name string to a Lucide icon component
*/
function getIcon(iconName?: IconName): LucideIcon | undefined {
return iconName ? iconMap[iconName] : undefined
}
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.",
},
]
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>

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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>
{/* 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>
<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 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>
)
}