mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
web: Simplifies the website to be almost strictly about the extension (#12180)
Some checks failed
Code QA Roo Code / check-translations (push) Has been cancelled
Code QA Roo Code / knip (push) Has been cancelled
Code QA Roo Code / compile (push) Has been cancelled
Code QA Roo Code / platform-unit-test (ubuntu-latest) (push) Has been cancelled
Code QA Roo Code / platform-unit-test (windows-latest) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Nightly Publish / publish-nightly (push) Has been cancelled
Deploy roocode.com / check-secrets (push) Has been cancelled
Deploy roocode.com / deploy (push) Has been cancelled
Some checks failed
Code QA Roo Code / check-translations (push) Has been cancelled
Code QA Roo Code / knip (push) Has been cancelled
Code QA Roo Code / compile (push) Has been cancelled
Code QA Roo Code / platform-unit-test (ubuntu-latest) (push) Has been cancelled
Code QA Roo Code / platform-unit-test (windows-latest) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Nightly Publish / publish-nightly (push) Has been cancelled
Deploy roocode.com / check-secrets (push) Has been cancelled
Deploy roocode.com / deploy (push) Has been cancelled
* Remove pricing/enterprise and Roo Code for pages * Remove cloud team and router pages * Refine homepage hero and CTA sections * nav * more footer * Fix knip by removing orphaned web files
This commit is contained in:
parent
96d6e43643
commit
ad25634905
33 changed files with 175 additions and 5446 deletions
|
|
@ -98,7 +98,7 @@ module.exports = {
|
|||
if (path === '/') {
|
||||
priority = 1.0;
|
||||
changefreq = 'yearly';
|
||||
} else if (path === '/enterprise' || path === '/evals') {
|
||||
} else if (path === '/evals') {
|
||||
priority = 0.8;
|
||||
changefreq = 'monthly';
|
||||
} else if (path === '/privacy' || path === '/terms') {
|
||||
|
|
|
|||
|
|
@ -27,9 +27,64 @@ const nextConfig: NextConfig = {
|
|||
destination: "https://roo-code.notion.site/238fd1401b0a8087b858e1ad431507cf?pvs=105",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/extension",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/provider/pricing",
|
||||
destination: "/provider",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/pricing",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/provider",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/cloud",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/cloud/team",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/enterprise",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/slack",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/linear",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/reviewer",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/pr-fixer",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/github",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,296 +0,0 @@
|
|||
import {
|
||||
ArrowRight,
|
||||
Bot,
|
||||
Brain,
|
||||
ChartLine,
|
||||
Github,
|
||||
History,
|
||||
ListChecks,
|
||||
LucideIcon,
|
||||
Pencil,
|
||||
Share2,
|
||||
Slack,
|
||||
Users,
|
||||
Users2,
|
||||
} from "lucide-react"
|
||||
import type { Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
|
||||
import { Button } from "@/components/ui"
|
||||
import { AnimatedBackground, UseExamplesSection } from "@/components/homepage"
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
// Workaround for next/image choking on these for some reason
|
||||
import screenshotDark from "../../../public/heroes/cloud-screen.png"
|
||||
|
||||
const TITLE = "Roo Code Cloud"
|
||||
const DESCRIPTION =
|
||||
"Your AI Software Engineering Team in the Cloud. Delegate tasks to autonomous agents, review PRs, and collaborate with your team."
|
||||
const OG_DESCRIPTION = "Your AI Team in the Cloud"
|
||||
const PATH = "/cloud"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${PATH}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: `${SEO.url}${PATH}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(TITLE, OG_DESCRIPTION),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: TITLE,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
|
||||
},
|
||||
keywords: [...SEO.keywords, "cloud", "subscription", "cloud agents", "AI cloud development", "autonomous agents"],
|
||||
}
|
||||
|
||||
const howItWorks = [
|
||||
{
|
||||
title: "1. Connect your GitHub account",
|
||||
description:
|
||||
"Pick which repos the agents can work with in their isolated containers and choose what model you want to power each of them. You're in control.",
|
||||
icon: Github,
|
||||
},
|
||||
{
|
||||
title: "2. Set up your agent team",
|
||||
description:
|
||||
"Choose the roles you want filled, like Explainer, Planner, Coder, PR Reviewer and PR Fixer. They know how to act in each situation and stay on-task with no deviations.",
|
||||
icon: Users2,
|
||||
},
|
||||
{
|
||||
title: "3. Start giving them tasks",
|
||||
description:
|
||||
"Describe what you want them to do from the web UI, get the Reviewer automatically reviewing PRs, and much more. They're now part of your team.",
|
||||
icon: Pencil,
|
||||
},
|
||||
]
|
||||
|
||||
interface Feature {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const features: Feature[] = [
|
||||
{
|
||||
icon: Bot,
|
||||
title: "Autonomous Cloud Agents",
|
||||
description:
|
||||
"Delegate work to specialized agents like the Planner, Coder, Explainer, Reviewer, and Fixer that run 24/7.",
|
||||
},
|
||||
{
|
||||
icon: Brain,
|
||||
title: "Model Agnostic",
|
||||
description: "Bring your own keys or use the Roo Code Router with access to all top models with no markup.",
|
||||
},
|
||||
{
|
||||
icon: Github,
|
||||
title: "GitHub PR Reviews",
|
||||
description:
|
||||
"Agents can automatically review Pull Requests, provide feedback, and even push fixes directly to your repository.",
|
||||
},
|
||||
{
|
||||
icon: Slack,
|
||||
title: "Slack Integration",
|
||||
description: "Start tasks, get updates, and collaborate with agents directly from your team's Slack channels.",
|
||||
},
|
||||
{
|
||||
icon: ListChecks,
|
||||
title: "Linear Integration",
|
||||
description: "Assign issues to Roo Code directly from Linear. Get PRs back without switching tools.",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "Team Collaboration",
|
||||
description:
|
||||
"Manage your team and their access to tasks and resources, with centralized billing and configuration.",
|
||||
},
|
||||
{
|
||||
icon: ChartLine,
|
||||
title: "Usage Analytics",
|
||||
description: "Detailed token analytics to help you optimize your costs and usage across your team.",
|
||||
},
|
||||
{
|
||||
icon: History,
|
||||
title: "Task History",
|
||||
description: "Access from anywhere all of your tasks, from the cloud and the extension",
|
||||
},
|
||||
{
|
||||
icon: Share2,
|
||||
title: "Task Sharing",
|
||||
description: "Share tasks with friends and co-workers and let them follow your work in real-time.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function CloudPage() {
|
||||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex pt-32 pb-20 items-center overflow-hidden">
|
||||
<AnimatedBackground />
|
||||
<div className="container relative flex flex-col items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center max-w-4xl mx-auto mb-12">
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-6 md:text-5xl lg:text-6xl">
|
||||
Your AI Team <span className="text-violet-500">in the Cloud</span>
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
|
||||
Create your agent team in the Cloud, give them access to GitHub, and start delegating tasks
|
||||
from the web, Slack, Linear, and more.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
size="xl"
|
||||
className="bg-violet-600 hover:bg-violet-700 text-white transition-all duration-300 shadow-lg hover:shadow-violet-500/25"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Try Cloud for Free
|
||||
<ArrowRight className="ml-2 size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" size="xl" className="backdrop-blur-sm" asChild>
|
||||
<a href="/pricing" className="flex items-center justify-center">
|
||||
View Pricing
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Screenshot */}
|
||||
<div className="relative mx-auto mt-4 md:max-w-[1000px]">
|
||||
<Image
|
||||
src={screenshotDark}
|
||||
alt="Roo Code Cloud Interface"
|
||||
className="w-full h-auto"
|
||||
width={1390}
|
||||
height={1012}
|
||||
priority
|
||||
/>
|
||||
</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="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mb-12 md:mb-24 max-w-5xl text-center">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-5xl mb-4">How it works</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
It only takes 2 minutes to expand your team by 10x.
|
||||
</p>
|
||||
</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">
|
||||
{howItWorks.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 && <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>
|
||||
|
||||
{/* Use Cases Section */}
|
||||
<UseExamplesSection />
|
||||
|
||||
{/* Features Grid */}
|
||||
<section className="py-24 bg-muted/30">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-violet-500/10 dark:bg-violet-700/20 blur-[140px]" />
|
||||
</div>
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
Powering the next generation of software development
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Everything you need to scale your development capacity with AI.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto relative">
|
||||
{features.map((feature, index) => {
|
||||
const Icon = feature.icon
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-background p-8 rounded-2xl border border-border hover:shadow-lg transition-all duration-300">
|
||||
<div className="bg-violet-100 dark:bg-violet-900/20 w-12 h-12 rounded-lg flex items-center justify-center mb-6">
|
||||
<Icon className="size-6 text-violet-600 dark:text-violet-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-3">{feature.title}</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">{feature.description}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24">
|
||||
<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-violet-500/10 via-purple-500/5 to-blue-500/5 p-8 text-center shadow-2xl backdrop-blur-xl dark:border-white/10 sm:p-16">
|
||||
<h2 className="mb-6 text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
Try a completely new way of working.
|
||||
</h2>
|
||||
<p className="mx-auto mb-10 max-w-2xl text-lg text-muted-foreground">Start for free today.</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-foreground text-background hover:bg-foreground/90 transition-all duration-300"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Sign up now
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,297 +0,0 @@
|
|||
import {
|
||||
ArrowRight,
|
||||
Users,
|
||||
Settings,
|
||||
BarChart3,
|
||||
Lock,
|
||||
Puzzle,
|
||||
ShieldCheck,
|
||||
DollarSign,
|
||||
Share2,
|
||||
LucideIcon,
|
||||
Server,
|
||||
ServerIcon,
|
||||
RefreshCcw,
|
||||
} from "lucide-react"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import { Button } from "@/components/ui"
|
||||
import { AnimatedBackground } from "@/components/homepage"
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
|
||||
const TITLE = "Roo Code Cloud Team Plan"
|
||||
const DESCRIPTION =
|
||||
"Scale your development with team collaboration features. Centralized billing, shared configuration, team-wide analytics, and unified GitHub, Slack, and Linear integrations."
|
||||
const OG_DESCRIPTION = "Team collaboration for AI-powered development"
|
||||
const PATH = "/cloud/team"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${PATH}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: `${SEO.url}${PATH}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(TITLE, OG_DESCRIPTION),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: TITLE,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
|
||||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"team",
|
||||
"collaboration",
|
||||
"enterprise",
|
||||
"organization",
|
||||
"centralized billing",
|
||||
"team management",
|
||||
],
|
||||
}
|
||||
|
||||
const keyBenefits = [
|
||||
{
|
||||
title: "No Per-Seat Costs",
|
||||
description: "Add unlimited team members without worrying about escalating per-seat charges.",
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: "Centralized Billing",
|
||||
description:
|
||||
"Single billing point for all team members using Cloud Agents and the Roo Code Router. No more API key management.",
|
||||
icon: DollarSign,
|
||||
},
|
||||
{
|
||||
title: "Unified Integrations",
|
||||
description:
|
||||
"Connect GitHub, Slack, and Linear once for the entire team. No need for each member to set up individual integrations.",
|
||||
icon: Settings,
|
||||
},
|
||||
{
|
||||
title: "Team-Wide Visibility",
|
||||
description: "Access task history and usage analytics across your entire team with granular per-user filters.",
|
||||
icon: BarChart3,
|
||||
},
|
||||
{
|
||||
title: "Configuration Enforcement",
|
||||
description:
|
||||
"Set policies for providers, models, and MCP servers to ensure your team follows organizational standards.",
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
{
|
||||
title: "Secure Environment Variables",
|
||||
description:
|
||||
"Centrally manage secrets, API keys, and environment variables for Cloud Agents in our encrypted secret store.",
|
||||
icon: Lock,
|
||||
},
|
||||
]
|
||||
|
||||
interface Feature {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const features: Feature[] = [
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: "Configuration Enforcement",
|
||||
description:
|
||||
"Require team members to log in to the VS Code Extension so policies can be enforced via MDM distribution.",
|
||||
},
|
||||
{
|
||||
icon: Server,
|
||||
title: "Provider Management",
|
||||
description:
|
||||
"Configure and manage the model providers your team can access for both the Extension and Cloud Agents, with API-key-free management.",
|
||||
},
|
||||
{
|
||||
icon: Puzzle,
|
||||
title: "Centralized Integration",
|
||||
description:
|
||||
"Centralized GitHub, Slack, and Linear connection for the entire team. Agents can review PRs, collaborate on your repositories, respond on your team Slack channels, and work on issues in Linear.",
|
||||
},
|
||||
{
|
||||
icon: RefreshCcw,
|
||||
title: "Extension Task Sync Config",
|
||||
description:
|
||||
"Require task syncing from VS Code Extension and control visibility settings for who can view each other's tasks.",
|
||||
},
|
||||
{
|
||||
icon: Share2,
|
||||
title: "Task Sharing Controls",
|
||||
description: "Enable per-task sharing with customizable audience controls and link expiration times.",
|
||||
},
|
||||
{
|
||||
icon: ServerIcon,
|
||||
title: "MCP Server Controls",
|
||||
description: "Control access to the Roo MCP Marketplace and what custom MCPs to make available to your team.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function CloudTeamPage() {
|
||||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex pt-32 pb-20 items-center overflow-hidden">
|
||||
<AnimatedBackground />
|
||||
<div className="container relative flex flex-col items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center max-w-4xl mx-auto mb-12">
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-6 md:text-6xl">Roo Code Cloud Team</h1>
|
||||
<h2 className="text-2xl font-bold tracking-tight mb-6 md:text-4xl">
|
||||
Built for <span className="text-violet-500">AI-Forward Teams</span>
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground mb-8 mx-auto">
|
||||
Empower your entire team with confidence with team-wide configuration, centralized billing,
|
||||
analytics and more. No per-seat costs, no API key juggling.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
size="xl"
|
||||
className="bg-violet-600 hover:bg-violet-700 text-white transition-all duration-300 shadow-lg hover:shadow-violet-500/25"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP + "?redirect_url=/checkout/team"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Start 14-Day Free Trial
|
||||
<ArrowRight className="ml-2 size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" size="xl" className="backdrop-blur-sm" asChild>
|
||||
<a href="/pricing" className="flex items-center justify-center">
|
||||
View Pricing
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Key Benefits 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="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mb-12 md:mb-24 max-w-5xl text-center">
|
||||
<div>
|
||||
<h2 className="text-4xl font-bold tracking-tight mb-4">Why Teams Choose Roo</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Streamline collaboration and scale your development capacity with team-first features.
|
||||
</p>
|
||||
</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-6">
|
||||
{keyBenefits.map((benefit, index) => {
|
||||
const Icon = benefit.icon
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className="rounded-3xl bg-card outline outline-border/50 hover:outline-8 shadow-xl p-8 h-full w-full group transition-all hover:shadow-2xl hover:shadow-violet-800/30 relative">
|
||||
{Icon && (
|
||||
<div className="size-15 p-3 rounded-full flex items-center justify-center shadow-lg absolute -top-4 -right-2 transition-all outline outline-foreground/5 bg-card group-hover:outline-3 group-hover:scale-105">
|
||||
<Icon className="size-5 text-violet-500" />
|
||||
</div>
|
||||
)}
|
||||
<h3 className="text-xl font-bold tracking-tight mb-2">{benefit.title}</h3>
|
||||
<div className="text-sm text-muted-foreground leading-relaxed">
|
||||
{benefit.description}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Grid */}
|
||||
<section className="py-24 bg-muted/30">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-violet-500/10 dark:bg-violet-700/20 blur-[140px]" />
|
||||
</div>
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl font-bold tracking-tight mb-4">Complete Team Management</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Access all capabilities from your Organization Settings. Everything you need to manage your
|
||||
team in one place.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto relative">
|
||||
{features.map((feature, index) => {
|
||||
const Icon = feature.icon
|
||||
return (
|
||||
<div
|
||||
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>
|
||||
<p className="leading-relaxed font-light text-muted-foreground">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24">
|
||||
<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-violet-500/10 via-purple-500/5 to-blue-500/5 p-8 text-center shadow-2xl backdrop-blur-xl dark:border-white/10 sm:p-16">
|
||||
<h2 className="mb-6 text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
Ready to scale your team's development?
|
||||
</h2>
|
||||
<p className="mx-auto mb-10 max-w-2xl text-lg text-muted-foreground">
|
||||
Start your free 14-day trial today. Got questions? Get in touch.
|
||||
</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-foreground text-background hover:bg-foreground/90 transition-all duration-300"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP + "?redirect_url=/checkout/team"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Start Free Trial
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" className="backdrop-blur-sm" asChild>
|
||||
<a href={EXTERNAL_LINKS.SUPPORT} className="flex items-center justify-center">
|
||||
Contact Support
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,559 +0,0 @@
|
|||
import { Code, CheckCircle, Shield, Zap, Workflow, Lock, ArrowRight, DollarSign, Search, Network } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui"
|
||||
import { AnimatedText } from "@/components/animated-text"
|
||||
import { AnimatedBackground } from "@/components/homepage"
|
||||
import { ContactForm } from "@/components/enterprise/contact-form"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
import type { Metadata } from "next"
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
|
||||
const TITLE = "Roo Code Cloud Enterprise"
|
||||
const DESCRIPTION =
|
||||
"The control-plane for AI-powered software development. Gain visibility, governance, and control over your AI coding initiatives."
|
||||
const OG_DESCRIPTION = "The control-plane for AI-powered software development"
|
||||
const PATH = "/enterprise"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${PATH}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: `${SEO.url}${PATH}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(TITLE, OG_DESCRIPTION),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: TITLE,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
|
||||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"Enterprise AI",
|
||||
"AI governance",
|
||||
"AI control-plane",
|
||||
"developer productivity",
|
||||
"SAML",
|
||||
"SCIM",
|
||||
"cost management",
|
||||
],
|
||||
}
|
||||
|
||||
export default async function Enterprise() {
|
||||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex h-[calc(100vh-theme(spacing.12))] items-center overflow-hidden">
|
||||
<AnimatedBackground />
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid gap-8 md:gap-12 lg:grid-cols-2 lg:gap-16">
|
||||
<div className="flex flex-col justify-center space-y-6 sm:space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight sm:text-4xl md:text-5xl lg:text-6xl">
|
||||
<span className="block">Roo Code Cloud for</span>
|
||||
<AnimatedText className="bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
Enterprise
|
||||
</AnimatedText>
|
||||
</h1>
|
||||
<p className="mt-4 max-w-md text-base text-muted-foreground sm:mt-6 sm:text-lg">
|
||||
The{" "}
|
||||
<AnimatedText className="bg-gradient-to-r from-blue-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
control-plane
|
||||
</AnimatedText>{" "}
|
||||
for AI-powered software development. Gain visibility, governance, and control over
|
||||
your AI coding initiatives.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full 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 sm:w-auto"
|
||||
asChild>
|
||||
<a href="#contact" className="flex w-full items-center justify-center">
|
||||
Request a Demo
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="w-full sm:w-auto bg-white/20 dark:bg-white/10 backdrop-blur-sm border border-black/40 dark:border-white/30 hover:border-blue-400 hover:bg-white/30 dark:hover:bg-white/20 hover:shadow-[0_0_20px_rgba(59,130,246,0.5)] transition-all duration-300">
|
||||
<a href="#benefits" className="flex w-full items-center justify-center">
|
||||
Why Roo Code
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mt-8 flex items-center justify-center lg:mt-0">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-[250px] w-[250px] rounded-full bg-blue-500/20 blur-[100px] sm:h-[300px] sm:w-[300px] md:h-[350px] md:w-[350px]" />
|
||||
</div>
|
||||
<div className="relative z-10 rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<Code className="h-6 w-6 text-blue-400" />
|
||||
<h3 className="text-lg font-semibold">Roo Code Cloud Control-Plane</h3>
|
||||
</div>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
A unified control system for managing Roo Code across your organization, with the
|
||||
flexibility to extend governance to your broader AI toolkit.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span className="text-sm">Centralized Roo Code management</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span className="text-sm">Real-time usage visibility</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span className="text-sm">Enterprise policy enforcement</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span className="text-sm">Extensible to other AI tools</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Key Messaging Sections */}
|
||||
<section id="benefits" className="bg-secondary/50 py-16">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-12 text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
Take Control of Your AI Development
|
||||
</h2>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">
|
||||
Roo Code Cloud provides enterprise-grade control and visibility for Roo Code deployments,
|
||||
with an extensible architecture for your evolving AI strategy.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{/* Card 1 */}
|
||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm transition-all hover:border-blue-500/50 hover:shadow-lg hover:shadow-blue-500/10 dark:hover:border-blue-400/50 dark:hover:shadow-blue-400/10">
|
||||
<div className="mb-5 inline-flex items-center justify-center rounded-xl bg-gradient-to-r from-blue-500/10 to-cyan-500/10 p-2.5 dark:from-blue-500/20 dark:to-cyan-500/20">
|
||||
<div className="rounded-lg bg-gradient-to-r from-blue-500/80 to-cyan-500/80 p-2.5">
|
||||
<Network className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-bold">Centralized AI Management Hub</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Manage Roo Code deployments enterprise-wide, with an extensible platform ready for your
|
||||
broader AI ecosystem.
|
||||
</p>
|
||||
<ul className="mt-4 space-y-2">
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>Centralized token management</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>Multi-model support for Roo</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>Extensible architecture</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Card 2 */}
|
||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm transition-all hover:border-blue-500/50 hover:shadow-lg hover:shadow-blue-500/10 dark:hover:border-blue-400/50 dark:hover:shadow-blue-400/10">
|
||||
<div className="mb-5 inline-flex items-center justify-center rounded-xl bg-gradient-to-r from-blue-500/10 to-cyan-500/10 p-2.5 dark:from-blue-500/20 dark:to-cyan-500/20">
|
||||
<div className="rounded-lg bg-gradient-to-r from-blue-500/80 to-cyan-500/80 p-2.5">
|
||||
<Search className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-bold">Real-Time Usage Visibility</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Track Roo Code usage across teams with detailed analytics and cost attribution.
|
||||
</p>
|
||||
<ul className="mt-4 space-y-2">
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>Token consumption tracking</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>Cost attribution by team</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>AI adoption insights</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Card 3 */}
|
||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm transition-all hover:border-blue-500/50 hover:shadow-lg hover:shadow-blue-500/10 dark:hover:border-blue-400/50 dark:hover:shadow-blue-400/10">
|
||||
<div className="mb-5 inline-flex items-center justify-center rounded-xl bg-gradient-to-r from-blue-500/10 to-cyan-500/10 p-2.5 dark:from-blue-500/20 dark:to-cyan-500/20">
|
||||
<div className="rounded-lg bg-gradient-to-r from-blue-500/80 to-cyan-500/80 p-2.5">
|
||||
<Shield className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-bold">Enterprise-Grade Governance</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Implement security policies for Roo Code that align with your enterprise AI governance
|
||||
framework.
|
||||
</p>
|
||||
<ul className="mt-4 space-y-2">
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>Model allow-lists</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>Data residency controls</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>Audit trail compliance</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Card 4 */}
|
||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm transition-all hover:border-blue-500/50 hover:shadow-lg hover:shadow-blue-500/10 dark:hover:border-blue-400/50 dark:hover:shadow-blue-400/10">
|
||||
<div className="mb-5 inline-flex items-center justify-center rounded-xl bg-gradient-to-r from-blue-500/10 to-cyan-500/10 p-2.5 dark:from-blue-500/20 dark:to-cyan-500/20">
|
||||
<div className="rounded-lg bg-gradient-to-r from-blue-500/80 to-cyan-500/80 p-2.5">
|
||||
<Workflow className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-bold">5-Minute Control-Plane Setup</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Deploy your Roo Code control-plane instantly with our SaaS solution. No infrastructure
|
||||
required.
|
||||
</p>
|
||||
<ul className="mt-4 space-y-2">
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>Instant deployment</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>SAML/SCIM integration</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>REST API access</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Card 5 */}
|
||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm transition-all hover:border-blue-500/50 hover:shadow-lg hover:shadow-blue-500/10 dark:hover:border-blue-400/50 dark:hover:shadow-blue-400/10">
|
||||
<div className="mb-5 inline-flex items-center justify-center rounded-xl bg-gradient-to-r from-blue-500/10 to-cyan-500/10 p-2.5 dark:from-blue-500/20 dark:to-cyan-500/20">
|
||||
<div className="rounded-lg bg-gradient-to-r from-blue-500/80 to-cyan-500/80 p-2.5">
|
||||
<DollarSign className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-bold">Manage AI Development Costs</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Track and control Roo Code costs with detailed analytics and budget controls.
|
||||
</p>
|
||||
<ul className="mt-4 space-y-2">
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>Unified cost visibility</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>Department chargebacks</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>Usage optimization</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Card 6 */}
|
||||
<div className="rounded-lg border border-border bg-card p-6 shadow-sm transition-all hover:border-blue-500/50 hover:shadow-lg hover:shadow-blue-500/10 dark:hover:border-blue-400/50 dark:hover:shadow-blue-400/10">
|
||||
<div className="mb-5 inline-flex items-center justify-center rounded-xl bg-gradient-to-r from-blue-500/10 to-cyan-500/10 p-2.5 dark:from-blue-500/20 dark:to-cyan-500/20">
|
||||
<div className="rounded-lg bg-gradient-to-r from-blue-500/80 to-cyan-500/80 p-2.5">
|
||||
<Zap className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-bold">Zero Friction for Developers</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Developers get seamless Roo Code access while you maintain governance and visibility.
|
||||
</p>
|
||||
<ul className="mt-4 space-y-2">
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>Automatic token refresh</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>Local sidecar architecture</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 shrink-0 text-green-500" />
|
||||
<span>No workflow disruption</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Differentiator Section */}
|
||||
<section className="py-16">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mb-12 text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Why You Need a Control-Plane</h2>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">
|
||||
See how Roo Code Cloud brings enterprise control to AI-powered development.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
<div className="rounded-lg border border-border bg-card p-8 shadow-sm">
|
||||
<h3 className="mb-4 text-2xl font-bold">Current State: AI Tool Sprawl</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start">
|
||||
<svg
|
||||
className="mr-2 mt-0.5 h-5 w-5 text-red-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<span>Roo Code tokens managed individually by developers</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<svg
|
||||
className="mr-2 mt-0.5 h-5 w-5 text-red-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<span>No visibility into AI tool usage or costs</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<svg
|
||||
className="mr-2 mt-0.5 h-5 w-5 text-red-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<span>Inconsistent security practices</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<svg
|
||||
className="mr-2 mt-0.5 h-5 w-5 text-red-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<span>Shadow AI spend on corporate cards</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<svg
|
||||
className="mr-2 mt-0.5 h-5 w-5 text-red-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<span>No centralized governance framework</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border bg-card p-8 shadow-sm">
|
||||
<h3 className="mb-4 text-2xl font-bold text-blue-400">Roo Code Cloud Control-Plane</h3>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 text-green-500" />
|
||||
<span>Centralized Roo Code management dashboard</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 text-green-500" />
|
||||
<span>Complete visibility into usage and costs</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 text-green-500" />
|
||||
<span>Consistent policy enforcement</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 text-green-500" />
|
||||
<span>Controlled, trackable AI investments</span>
|
||||
</li>
|
||||
<li className="flex items-start">
|
||||
<CheckCircle className="mr-2 mt-0.5 h-5 w-5 text-green-500" />
|
||||
<span>Enterprise-ready governance platform</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Security Hook Section */}
|
||||
<section className="py-16">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="rounded-lg border border-border bg-card p-8 shadow-sm">
|
||||
<div className="grid gap-8 md:grid-cols-2 md:items-center">
|
||||
<div>
|
||||
<div className="mb-5 inline-flex items-center justify-center rounded-xl bg-gradient-to-r from-blue-500/10 to-cyan-500/10 p-2.5 dark:from-blue-500/20 dark:to-cyan-500/20">
|
||||
<div className="rounded-lg bg-gradient-to-r from-blue-500/80 to-cyan-500/80 p-2.5">
|
||||
<Shield className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="mb-4 text-2xl font-bold">Enterprise-Grade Security</h3>
|
||||
<p className="mb-6 text-muted-foreground">
|
||||
Built with security-first principles to meet stringent enterprise requirements while
|
||||
maintaining developer productivity.
|
||||
</p>
|
||||
<ul className="space-y-3">
|
||||
<li className="flex items-center space-x-3">
|
||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
||||
<span>SOC 2 Type I Certified with Type II in observation</span>
|
||||
</li>
|
||||
<li className="flex items-center space-x-3">
|
||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
||||
<span>End-to-end encryption for all data transmission</span>
|
||||
</li>
|
||||
<li className="flex items-center space-x-3">
|
||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
||||
<span>Security-first architecture with explicit permissions</span>
|
||||
</li>
|
||||
<li className="flex items-center space-x-3">
|
||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
||||
<span>Complete audit trails and compliance reporting</span>
|
||||
</li>
|
||||
<li className="flex items-center space-x-3">
|
||||
<CheckCircle className="h-5 w-5 text-green-500" />
|
||||
<span>Open-source transparency for security verification</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="rounded-lg border border-border bg-secondary/50 p-6 text-center">
|
||||
<div className="mb-4 inline-flex items-center justify-center rounded-xl bg-gradient-to-r from-blue-500/10 to-cyan-500/10 p-2.5 dark:from-blue-500/20 dark:to-cyan-500/20">
|
||||
<div className="rounded-lg bg-gradient-to-r from-blue-500/80 to-cyan-500/80 p-2.5">
|
||||
<Lock className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="mb-2 text-lg font-semibold">Security-First Design</h4>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Every feature built with enterprise security requirements in mind
|
||||
</p>
|
||||
<Button
|
||||
size="lg"
|
||||
asChild
|
||||
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">
|
||||
<a
|
||||
href={EXTERNAL_LINKS.SECURITY}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center">
|
||||
View Security Details
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section id="contact" className="relative overflow-hidden py-20 sm:py-24">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-blue-500/5 via-cyan-500/5 to-purple-500/5 dark:from-blue-500/10 dark:via-cyan-500/10 dark:to-purple-500/10" />
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<div className="relative rounded-3xl border border-border/50 bg-gradient-to-br from-blue-500/5 via-cyan-500/5 to-purple-500/5 p-8 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 dark:shadow-[0_30px_90px_rgba(255,255,255,0.15)] sm:p-12">
|
||||
<div className="absolute inset-0 rounded-3xl bg-gradient-to-br from-blue-500/5 via-transparent to-purple-500/5 dark:bg-gradient-to-br dark:from-white/[0.05] dark:via-transparent dark:to-white/[0.03]" />
|
||||
<div className="relative text-center">
|
||||
<h2 className="mb-4 text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
Ready to Transform Your Development Process?
|
||||
</h2>
|
||||
<p className="mb-8 text-lg text-muted-foreground">
|
||||
Join our early access program and be among the first to experience the power of Roo
|
||||
Code Cloud for Enterprise.
|
||||
</p>
|
||||
<div className="grid gap-4 sm:grid-cols-2 sm:gap-6">
|
||||
<div className="rounded-lg border border-border bg-card/80 backdrop-blur-sm p-6 text-center shadow-lg hover:shadow-xl transition-all duration-300 dark:border-white/20 dark:bg-gray-800/80 dark:hover:border-white/40 dark:hover:bg-gray-700/90 dark:hover:shadow-[0_20px_50px_rgba(255,255,255,0.2)] dark:hover:scale-[1.02]">
|
||||
<h3 className="mb-2 text-xl font-bold">Become an Early Access Partner</h3>
|
||||
<p className="mb-4 text-muted-foreground">
|
||||
Collaborate in shaping Roo Code's enterprise solution.
|
||||
</p>
|
||||
<ContactForm
|
||||
formType="early-access"
|
||||
buttonText="Apply Now"
|
||||
buttonClassName="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"
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-lg border border-border bg-card/80 backdrop-blur-sm p-6 text-center shadow-lg hover:shadow-xl transition-all duration-300 dark:border-white/20 dark:bg-gray-800/80 dark:hover:border-white/40 dark:hover:bg-gray-700/90 dark:hover:shadow-[0_20px_50px_rgba(255,255,255,0.2)] dark:hover:scale-[1.02]">
|
||||
<h3 className="mb-2 text-xl font-bold">Request a Demo</h3>
|
||||
<p className="mb-4 text-muted-foreground">
|
||||
See Roo Code's enterprise capabilities in action.
|
||||
</p>
|
||||
<ContactForm
|
||||
formType="demo"
|
||||
buttonText="Contact Us"
|
||||
buttonClassName="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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
/* eslint-disable react/jsx-no-target-blank */
|
||||
|
||||
import { getVSCodeDownloads } from "@/lib/stats"
|
||||
|
||||
import { Button } from "@/components/ui"
|
||||
import {
|
||||
AnimatedBackground,
|
||||
CodeExample,
|
||||
CompanyLogos,
|
||||
FAQSection,
|
||||
Features,
|
||||
InstallSection,
|
||||
Testimonials,
|
||||
} from "@/components/homepage"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
import { ArrowRight } from "lucide-react"
|
||||
import { StructuredData } from "@/components/structured-data"
|
||||
|
||||
// Invalidate cache when a request comes in, at most once every hour.
|
||||
export const revalidate = 3600
|
||||
|
||||
export default async function ExtensionPage() {
|
||||
const downloads = await getVSCodeDownloads()
|
||||
|
||||
return (
|
||||
<>
|
||||
<StructuredData />
|
||||
<section className="relative flex h-[calc(125vh-theme(spacing.12))] items-center overflow-hidden md:h-[calc(80svh-theme(spacing.12))]">
|
||||
<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-8 md:gap-12 lg:grid-cols-2 lg:gap-16">
|
||||
<div className="flex flex-col justify-center space-y-4 sm:space-y-8">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold tracking-tight mt-8 text-center md:text-left md:text-4xl lg:text-5xl lg:mt-0">
|
||||
The Open Source AI Coding Assistant for serious work.
|
||||
</h1>
|
||||
<p className="mt-4 max-w-lg text-lg text-muted-foreground text-center md:text-left sm:mt-6">
|
||||
Specialized modes stay on task and ship great code.
|
||||
<br />
|
||||
Fully model-agnostic so you can use the best (or most cost-effective) model for
|
||||
each task.
|
||||
</p>
|
||||
<p className="max-w-lg text-lg text-muted-foreground text-center md:text-left sm:mt-6">
|
||||
Stop chasing this week's hot new model or CLI tool and go deep with Roo Code.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full hover:bg-gray-200 dark:bg-white dark:text-black sm:w-auto">
|
||||
<a
|
||||
href={EXTERNAL_LINKS.MARKETPLACE}
|
||||
target="_blank"
|
||||
className="flex w-full items-center justify-center">
|
||||
Install VS Code Extension
|
||||
<ArrowRight className="ml-2" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="md:max-h-[70px] md:overflow-clip text-center md:text-left pt-6 md:pt-0">
|
||||
<CompanyLogos />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex items-center mx-auto h-full mt-8 lg:mt-0">
|
||||
<div className="flex items-center justify-center">
|
||||
<CodeExample />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div id="product">
|
||||
<Features />
|
||||
</div>
|
||||
<div id="testimonials">
|
||||
<Testimonials />
|
||||
</div>
|
||||
<div id="faq">
|
||||
<FAQSection />
|
||||
</div>
|
||||
<InstallSection downloads={downloads} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,413 +0,0 @@
|
|||
import {
|
||||
ArrowRight,
|
||||
CheckCircle,
|
||||
CreditCard,
|
||||
Eye,
|
||||
GitBranch,
|
||||
GitPullRequest,
|
||||
Link2,
|
||||
MessageSquare,
|
||||
Settings,
|
||||
Shield,
|
||||
} from "lucide-react"
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import { AnimatedBackground } from "@/components/homepage"
|
||||
import { LinearIssueDemo } from "@/components/linear/linear-issue-demo"
|
||||
import { Button } from "@/components/ui"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
|
||||
const TITLE = "Roo Code for Linear"
|
||||
const DESCRIPTION = "Assign development work to @Roo Code directly from Linear. Get PRs back without switching tools."
|
||||
const OG_DESCRIPTION = "Turn Linear Issues into Pull Requests"
|
||||
const PATH = "/linear"
|
||||
|
||||
// Featured Workflow section is temporarily commented out until video is ready
|
||||
// const LINEAR_DEMO_YOUTUBE_ID = ""
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${PATH}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: `${SEO.url}${PATH}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(TITLE, OG_DESCRIPTION),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: TITLE,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
|
||||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"linear integration",
|
||||
"issue to PR",
|
||||
"AI in Linear",
|
||||
"engineering workflow automation",
|
||||
"Roo Code Cloud",
|
||||
],
|
||||
}
|
||||
|
||||
// Invalidate cache when a request comes in, at most once every hour.
|
||||
export const revalidate = 3600
|
||||
|
||||
type ValueProp = {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const VALUE_PROPS: ValueProp[] = [
|
||||
{
|
||||
icon: GitBranch,
|
||||
title: "Work where you already work.",
|
||||
description:
|
||||
"Assign development work to @Roo Code directly from Linear. No new tools to learn, no context switching required.",
|
||||
},
|
||||
{
|
||||
icon: Eye,
|
||||
title: "Progress is visible.",
|
||||
description:
|
||||
"Watch progress unfold in real-time. Roo Code posts updates as comments, so your whole team stays in the loop.",
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: "Mention for refinement.",
|
||||
description:
|
||||
'Need changes? Just comment "@Roo Code also add dark mode support" and the agent picks up where it left off.',
|
||||
},
|
||||
{
|
||||
icon: Link2,
|
||||
title: "Full traceability.",
|
||||
description:
|
||||
"Every PR links back to the originating issue. Every issue shows its linked PR. Your audit trail stays clean.",
|
||||
},
|
||||
{
|
||||
icon: Settings,
|
||||
title: "Organization-level setup.",
|
||||
description:
|
||||
"Connect once, use everywhere. Your team members can assign issues to @Roo Code without individual configuration.",
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: "Safe by design.",
|
||||
description:
|
||||
"Agents never touch main/master directly. They produce branches and PRs. You review and approve before merge.",
|
||||
},
|
||||
]
|
||||
|
||||
// type WorkflowStep = {
|
||||
// step: number
|
||||
// title: string
|
||||
// description: string
|
||||
// }
|
||||
|
||||
// const WORKFLOW_STEPS: WorkflowStep[] = [
|
||||
// {
|
||||
// step: 1,
|
||||
// title: "Create an issue",
|
||||
// description: "Write your issue with acceptance criteria. Be as detailed as you like.",
|
||||
// },
|
||||
// {
|
||||
// step: 2,
|
||||
// title: "Call @Roo Code",
|
||||
// description: "Mention @Roo Code in a comment to start. The agent begins working immediately.",
|
||||
// },
|
||||
// {
|
||||
// step: 3,
|
||||
// title: "Watch progress",
|
||||
// description: "Roo Code posts status updates as comments. Refine with @-mentions if needed.",
|
||||
// },
|
||||
// {
|
||||
// step: 4,
|
||||
// title: "Review the PR",
|
||||
// description: "When ready, the PR link appears in the issue. Review, iterate, and ship.",
|
||||
// },
|
||||
// ]
|
||||
|
||||
type OnboardingStep = {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string
|
||||
link?: {
|
||||
href: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
const ONBOARDING_STEPS: OnboardingStep[] = [
|
||||
{
|
||||
icon: CreditCard,
|
||||
title: "1. Team Plan",
|
||||
description: "Linear integration requires a Team plan.",
|
||||
link: {
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_TEAM_TRIAL,
|
||||
text: "Start a free trial",
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: GitPullRequest,
|
||||
title: "2. Connect GitHub",
|
||||
description: "Link your repositories so Roo Code can open PRs on your behalf.",
|
||||
},
|
||||
{
|
||||
icon: Settings,
|
||||
title: "3. Connect Linear",
|
||||
description: "Authorize via OAuth. No API keys to manage or rotate.",
|
||||
},
|
||||
{
|
||||
icon: CheckCircle,
|
||||
title: "4. Link & Start",
|
||||
description: "Map your Linear project to a repo, then assign or mention @Roo Code.",
|
||||
},
|
||||
]
|
||||
|
||||
function LinearIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 100 100" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.22541 61.5228c-.2225-.9485.90748-1.5459 1.59638-.857L39.3342 97.1782c.6889.6889.0915 1.8189-.857 1.5964C20.0515 94.4522 5.54779 79.9485 1.22541 61.5228ZM.00189135 46.8891c-.01764375.2833.08887215.5599.28957165.7606L52.3503 99.7085c.2007.2007.4773.3075.7606.2896 2.3692-.1476 4.6938-.46 6.9624-.9259.7645-.157 1.0301-1.0963.4782-1.6481L2.57595 39.4485c-.55186-.5519-1.49117-.2863-1.648174.4782-.465915 2.2686-.77832 4.5932-.92588465 6.9624ZM4.21093 29.7054c-.16649.3738-.08169.8106.20765 1.1l64.77602 64.776c.2894.2894.7262.3742 1.1.2077 1.7861-.7956 3.5171-1.6927 5.1855-2.684.5521-.328.6373-1.0867.1832-1.5407L8.43566 24.3367c-.45409-.4541-1.21271-.3689-1.54074.1832-.99132 1.6684-1.88843 3.3994-2.68399 5.1855ZM12.6587 18.074c-.3701-.3701-.393-.9637-.0443-1.3541C21.7795 6.45931 35.1114 0 49.9519 0 77.5927 0 100 22.4073 100 50.0481c0 14.8405-6.4593 28.1724-16.7199 37.3375-.3903.3487-.984.3258-1.3542-.0443L12.6587 18.074Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default function LinearPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex pt-32 pb-20 items-center overflow-hidden">
|
||||
<AnimatedBackground />
|
||||
<div className="container relative flex flex-col items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 items-center gap-10 lg:grid-cols-2 lg:gap-12">
|
||||
<div className="text-center lg:text-left">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 text-sm font-medium mb-6">
|
||||
<LinearIcon className="size-4" />
|
||||
Powered by Roo Code Cloud
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-6 md:text-5xl lg:text-6xl">
|
||||
Turn Linear Issues into <span className="text-indigo-500">Pull Requests</span>
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto lg:mx-0">
|
||||
Assign development work to @Roo Code directly from Linear. Get PRs back without
|
||||
switching tools.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start">
|
||||
<Button
|
||||
size="xl"
|
||||
className="bg-indigo-600 hover:bg-indigo-700 text-white transition-all duration-300 shadow-lg hover:shadow-indigo-500/25"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_HOME}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Get Started
|
||||
<ArrowRight className="ml-2 size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center lg:justify-end">
|
||||
<LinearIssueDemo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Value Props Section */}
|
||||
<section className="py-24 bg-muted/30">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-indigo-500/10 dark:bg-indigo-700/20 blur-[140px]" />
|
||||
</div>
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
Why your team will love using Roo Code in Linear
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
AI agents that understand context, keep your team in the loop, and deliver PRs you can
|
||||
review.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto relative">
|
||||
{VALUE_PROPS.map((prop, index) => {
|
||||
const Icon = prop.icon
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-background p-8 rounded-2xl border border-border hover:shadow-lg transition-all duration-300">
|
||||
<div className="bg-indigo-100 dark:bg-indigo-900/20 w-12 h-12 rounded-lg flex items-center justify-center mb-6">
|
||||
<Icon className="size-6 text-indigo-600 dark:text-indigo-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-3">{prop.title}</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">{prop.description}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Featured Workflow Section - temporarily commented out until video is ready
|
||||
<section id="demo" className="relative overflow-hidden border-t border-border py-24 lg:py-32">
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mb-12 max-w-5xl text-center">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm font-medium mb-6">
|
||||
<Zap className="size-4" />
|
||||
Featured Workflow
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-5xl mb-4">Issue to Shipped Feature</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Stay in Linear from assignment to review. Roo Code keeps the issue updated and links the PR
|
||||
when it's ready.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto max-w-6xl">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-10 items-center">
|
||||
{/* YouTube Video Embed or Placeholder */}
|
||||
{/*<div className="lg:col-span-3 overflow-hidden rounded-2xl border border-border bg-background shadow-lg">
|
||||
{LINEAR_DEMO_YOUTUBE_ID ? (
|
||||
<iframe
|
||||
className="aspect-video w-full"
|
||||
src={`https://www.youtube-nocookie.com/embed/${LINEAR_DEMO_YOUTUBE_ID}?rel=0`}
|
||||
title="Roo Code Linear Integration Demo"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allowFullScreen
|
||||
/>
|
||||
) : (
|
||||
<div className="aspect-video w-full flex flex-col items-center justify-center bg-gradient-to-br from-indigo-500/10 via-blue-500/5 to-purple-500/10 text-center p-8">
|
||||
<LinearIcon className="size-16 text-indigo-500/50 mb-4" />
|
||||
<p className="text-lg font-semibold text-foreground mb-2">
|
||||
Demo Video Coming Soon
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground max-w-md">
|
||||
See the workflow in action: assign an issue to @Roo Code and watch as it
|
||||
analyzes requirements, writes code, and opens a PR.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Workflow Steps */}
|
||||
{/*<div className="lg:col-span-2 space-y-3">
|
||||
{WORKFLOW_STEPS.map((step) => (
|
||||
<div
|
||||
key={step.step}
|
||||
className="relative border border-border rounded-xl bg-background p-4 transition-all duration-300 hover:shadow-md hover:border-blue-500/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-blue-100 dark:bg-blue-900/30 w-7 h-7 rounded-full flex items-center justify-center text-blue-700 dark:text-blue-300 font-bold text-xs shrink-0 mt-0.5">
|
||||
{step.step}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-base font-semibold text-foreground mb-0.5">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-sm leading-snug text-muted-foreground">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
*/}
|
||||
|
||||
{/* Onboarding Section */}
|
||||
<section className="py-24 bg-muted/30">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">Get started in minutes</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Connect Linear and start assigning issues to AI.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-5xl mx-auto">
|
||||
{ONBOARDING_STEPS.map((step, index) => {
|
||||
const Icon = step.icon
|
||||
return (
|
||||
<div key={index} className="text-center">
|
||||
<div className="bg-indigo-100 dark:bg-indigo-900/20 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<Icon className="size-8 text-indigo-600 dark:text-indigo-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">{step.title}</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{step.description}
|
||||
{step.link && (
|
||||
<>
|
||||
{" "}
|
||||
<a
|
||||
href={step.link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-indigo-600 dark:text-indigo-400 hover:underline">
|
||||
{step.link.text} →
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24">
|
||||
<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-indigo-500/10 via-purple-500/5 to-blue-500/5 p-8 text-center shadow-2xl backdrop-blur-xl dark:border-white/10 sm:p-16">
|
||||
<h2 className="mb-6 text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
Start using Roo Code in Linear
|
||||
</h2>
|
||||
<p className="mx-auto mb-10 max-w-2xl text-lg text-muted-foreground">
|
||||
Start a free 14 day Team trial.
|
||||
</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-foreground text-background hover:bg-foreground/90 transition-all duration-300"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_TEAM_TRIAL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Start free trial
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
import { Button } from "@/components/ui"
|
||||
import {
|
||||
AnimatedBackground,
|
||||
CodeExample,
|
||||
CompanyLogos,
|
||||
FAQSection,
|
||||
Features,
|
||||
Testimonials,
|
||||
CTASection,
|
||||
OptionOverviewSection,
|
||||
PillarsSection,
|
||||
UseExamplesSection,
|
||||
} from "@/components/homepage"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
import { ArrowRight } from "lucide-react"
|
||||
|
|
@ -19,63 +20,62 @@ export default async function Home() {
|
|||
return (
|
||||
<>
|
||||
<StructuredData />
|
||||
<section className="relative flex flex-col items-center overflow-hidden pt-20 pb-12 md:pt-32 md:pb-16">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-violet-500/10 dark:bg-violet-700/20 blur-[140px]" />
|
||||
</div>
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8 flex flex-col items-center text-center">
|
||||
<h1 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground max-w-4xl mb-6">
|
||||
Your AI Software Engineering Team is here.
|
||||
<br />
|
||||
<span className="text-muted-foreground">Interactive in the IDE, autonomous in the cloud.</span>
|
||||
</h1>
|
||||
<div className="mt-2 max-w-3xl text-lg text-muted-foreground mb-10 space-y-3">
|
||||
<p>
|
||||
Use the <strong className="text-nowrap">Roo Code Extension</strong> on your computer for
|
||||
full control, or delegate work to your{" "}
|
||||
<strong className="text-nowrap">Roo Code Cloud Agents</strong> from the web, Slack, GitHub
|
||||
or wherever your team is.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-4 mb-16">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Button size="xl" className="w-full">
|
||||
<a
|
||||
href={EXTERNAL_LINKS.MARKETPLACE}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Install VS Code Extension
|
||||
<ArrowRight className="ml-2 size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground">Free and Open Source</span>
|
||||
<section className="relative flex h-[calc(125vh-theme(spacing.12))] items-center overflow-hidden md:h-[calc(80svh-theme(spacing.12))]">
|
||||
<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-8 md:gap-12 lg:grid-cols-2 lg:gap-16">
|
||||
<div className="flex flex-col justify-center space-y-4 sm:space-y-8">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold tracking-tight mt-8 text-center md:text-left md:text-4xl lg:text-5xl lg:mt-0">
|
||||
The Open Source AI Coding Assistant for serious work.
|
||||
</h1>
|
||||
<p className="mt-4 max-w-lg text-lg text-muted-foreground text-center md:text-left sm:mt-6">
|
||||
Specialized modes stay on task and ship great code.
|
||||
<br />
|
||||
Fully model-agnostic so you can use the best (or most cost-effective) model for each
|
||||
task.
|
||||
</p>
|
||||
<p className="max-w-lg text-lg text-muted-foreground text-center md:text-left sm:mt-6">
|
||||
Stop chasing this week's hot new model or CLI tool and go deep with Roo Code.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full hover:bg-gray-200 dark:bg-white dark:text-black sm:w-auto">
|
||||
<a
|
||||
href={EXTERNAL_LINKS.MARKETPLACE}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex w-full items-center justify-center">
|
||||
Install VS Code Extension
|
||||
<ArrowRight className="ml-2" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="md:max-h-[70px] md:overflow-clip text-center md:text-left pt-6 md:pt-0">
|
||||
<CompanyLogos />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Button size="xl" className="w-full">
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_HOME}
|
||||
className="flex items-center justify-center">
|
||||
Try Cloud for Free
|
||||
<ArrowRight className="ml-2 size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground">No credit card needed</span>
|
||||
<div className="relative flex items-center mx-auto h-full mt-8 lg:mt-0">
|
||||
<div className="flex items-center justify-center">
|
||||
<CodeExample />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-12 px-4">
|
||||
<CompanyLogos />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PillarsSection />
|
||||
<OptionOverviewSection />
|
||||
<UseExamplesSection />
|
||||
<Testimonials />
|
||||
<FAQSection />
|
||||
<div id="product">
|
||||
<Features />
|
||||
</div>
|
||||
<div id="testimonials">
|
||||
<Testimonials />
|
||||
</div>
|
||||
<div id="faq">
|
||||
<FAQSection />
|
||||
</div>
|
||||
<CTASection />
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,95 +0,0 @@
|
|||
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: "Try now for free",
|
||||
disclaimer: "",
|
||||
tracking: "&agent=pr-fixer",
|
||||
},
|
||||
},
|
||||
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: "Try now for free",
|
||||
},
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import type { Metadata } from "next"
|
||||
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
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 =
|
||||
"Automatically apply high-quality fixes to your pull requests with comment-aware, GitHub-native workflows."
|
||||
const OG_DESCRIPTION = "Transform review feedback into clean commits"
|
||||
const PATH = "/pr-fixer"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${PATH}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: `${SEO.url}${PATH}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(TITLE, OG_DESCRIPTION),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: TITLE,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
|
||||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"PR fixer",
|
||||
"pull request fixes",
|
||||
"code fixes",
|
||||
"GitHub PR",
|
||||
"automated code fixes",
|
||||
"comment-aware agent",
|
||||
"repository-aware fixes",
|
||||
"bring your own key",
|
||||
"BYOK AI",
|
||||
"code quality",
|
||||
"cloud agents",
|
||||
"AI development team",
|
||||
],
|
||||
}
|
||||
|
||||
export default async function AgentPrFixerPage({ searchParams }: { searchParams: Promise<{ v?: string }> }) {
|
||||
const params = await searchParams
|
||||
const content = getContentVariant(params, {
|
||||
A: contentA,
|
||||
})
|
||||
|
||||
return <AgentLandingContent content={content} />
|
||||
}
|
||||
|
|
@ -1,395 +0,0 @@
|
|||
import { Users, ArrowRight, LucideIcon, Check, SquareTerminal, CornerRightDown, Cloud } from "lucide-react"
|
||||
import type { Metadata } from "next"
|
||||
import Link from "next/link"
|
||||
|
||||
import { Button } from "@/components/ui"
|
||||
import { AnimatedBackground } from "@/components/homepage"
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
|
||||
const TITLE = "Roo Code Pricing"
|
||||
const DESCRIPTION =
|
||||
"Simple, transparent pricing for all Roo Code products. The VS Code extension is free forever. Choose the cloud plan that fits your needs."
|
||||
const OG_DESCRIPTION = ""
|
||||
const PATH = "/pricing"
|
||||
|
||||
const PRICE_CREDITS = 5
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${PATH}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: `${SEO.url}${PATH}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(TITLE, OG_DESCRIPTION),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: TITLE,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
|
||||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"pricing",
|
||||
"plans",
|
||||
"subscription",
|
||||
"cloud pricing",
|
||||
"AI development pricing",
|
||||
"team pricing",
|
||||
"enterprise pricing",
|
||||
],
|
||||
}
|
||||
|
||||
interface PricingTier {
|
||||
name: string
|
||||
icon: LucideIcon
|
||||
price: string
|
||||
priceSuffix: string
|
||||
period?: string
|
||||
creditPrice?: string
|
||||
trial?: string
|
||||
description: string
|
||||
featuresIntro?: string
|
||||
features: string[]
|
||||
cta: {
|
||||
text: string
|
||||
href?: string
|
||||
}
|
||||
learnMoreLink?: string
|
||||
}
|
||||
|
||||
const pricingTiers: PricingTier[] = [
|
||||
{
|
||||
name: "VS Code Extension",
|
||||
icon: SquareTerminal,
|
||||
price: "Free",
|
||||
priceSuffix: "inference",
|
||||
description: "The best local coding agent",
|
||||
features: ["Unlimited local use", "Bring your own model", "Powerful, extensible modes", "Community support"],
|
||||
cta: {
|
||||
text: "Install Now",
|
||||
href: EXTERNAL_LINKS.MARKETPLACE,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Cloud Free",
|
||||
icon: Cloud,
|
||||
price: "$0",
|
||||
period: "/mo",
|
||||
priceSuffix: "credits",
|
||||
creditPrice: `$${PRICE_CREDITS}`,
|
||||
description: "For AI-forward engineers",
|
||||
featuresIntro: "Go beyond the extension with",
|
||||
features: [
|
||||
"Access to Cloud Agents: fully autonomous development you can kick off from GitHub and the web",
|
||||
"Access to the Roo Code Router",
|
||||
"Follow your tasks from anywhere",
|
||||
"Share tasks with friends and co-workers",
|
||||
"Token usage analytics",
|
||||
"Professional support",
|
||||
],
|
||||
cta: {
|
||||
text: "Sign up",
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Cloud Team",
|
||||
icon: Users,
|
||||
price: "$99",
|
||||
priceSuffix: "credits",
|
||||
period: "/mo",
|
||||
creditPrice: `$${PRICE_CREDITS}`,
|
||||
trial: "Free for 14 days, then",
|
||||
description: "For AI-forward teams",
|
||||
featuresIntro: "Everything in Free +",
|
||||
features: [
|
||||
"Unlimited users (no per-seat cost)",
|
||||
"Shared configuration & policies",
|
||||
"Centralized billing",
|
||||
"Slack and Linear integrations",
|
||||
],
|
||||
cta: {
|
||||
text: "Sign up",
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP + "?redirect_url=/billing",
|
||||
},
|
||||
learnMoreLink: "/cloud/team",
|
||||
},
|
||||
]
|
||||
|
||||
export default function PricingPage() {
|
||||
return (
|
||||
<>
|
||||
<AnimatedBackground />
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative overflow-hidden pt-12 pb-10">
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-5xl font-bold tracking-tight">Roo Code Pricing</h1>
|
||||
<p className="mt-4 text-lg text-muted-foreground">
|
||||
For all of our products: the Roo Code VS Code Extension, Roo Code Cloud and the Roo Code
|
||||
Router.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Pricing Tiers */}
|
||||
<section className="">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto grid max-w-6xl gap-4 md:grid-cols-3 md:px-4">
|
||||
{pricingTiers.map((tier) => {
|
||||
const Icon = tier.icon
|
||||
return (
|
||||
<div
|
||||
key={tier.name}
|
||||
className="relative group p-6 flex flex-col justify-start bg-background rounded-2xl outline outline-2 outline-border/50 hover:outline-8 transition-all shadow-xl hover:shadow-2xl hover:outline-6">
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-2xl font-bold tracking-tight">{tier.name}</h3>
|
||||
</div>
|
||||
<p className="text-sm font-medium">{tier.description}</p>
|
||||
</div>
|
||||
<div className="absolute -right-2 -top-4 rounded-full bg-card shadow-md p-4 outline outline-2 outline-border/50 group-hover:scale-105 group-hover:outline-8 transition-all">
|
||||
<Icon className="size-6" strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
<div className="grow mb-8 md:h-[214px]">
|
||||
<p className="text-sm text-muted-foreground font-light mb-2">
|
||||
{tier.featuresIntro}
|
||||
</p>
|
||||
<ul className="space-y-3 my-0">
|
||||
{tier.features.map((feature) => (
|
||||
<li key={feature} className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<span className="text-sm">{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{tier.learnMoreLink && (
|
||||
<div className="mt-2">
|
||||
<Link
|
||||
href={tier.learnMoreLink}
|
||||
className="text-sm text-violet-600 dark:text-violet-400 hover:underline">
|
||||
Learn more →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-base font-light">{tier.trial}</p>
|
||||
|
||||
<p className="text-xl mb-1 tracking-tight font-light">
|
||||
<strong className="font-bold">{tier.price}</strong>
|
||||
{tier.period} + {tier.priceSuffix}
|
||||
<CornerRightDown className="inline size-4 ml-1 relative top-0.5" />
|
||||
</p>
|
||||
|
||||
<p className="text-sm text-muted-foreground mb-5">
|
||||
{tier.creditPrice && (
|
||||
<>
|
||||
Cloud Agents: {tier.creditPrice}/hour in credits
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
Inference:{" "}
|
||||
<Link href="/provider" className="underline hover:no-underline">
|
||||
Roo Provider pricing
|
||||
</Link>{" "}
|
||||
credits or{" "}
|
||||
<abbr title="Bring Your Own Model" className="cursor-help">
|
||||
BYOM
|
||||
</abbr>
|
||||
</p>
|
||||
|
||||
<Button size="lg" className="w-full transition-all duration-300" asChild>
|
||||
<Link href={tier.cta.href!} className="flex items-center justify-center">
|
||||
{tier.cta.text}
|
||||
<ArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
{/* <div className="bg-foreground/20 h-8 absolute -bottom-8 left-1/2 w-[1px]" /> */}
|
||||
<div className="h-[28px] absolute bottom-[-31px] left-1/2 w-[4px] transition-colors bg-gradient-to-b from-transparent to-violet-700/20 group-hover:from-violet-500/50 group-hover:to-violet-500/20" />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="max-w-6xl mx-auto mt-8 p-7 flex flex-col md:flex-row gap-8 md:gap-4 bg-violet-200/20 outline-violet-700/20 outline outline-1 rounded-2xl transition-all shadow-none">
|
||||
<div className="md:border-r md:pr-4">
|
||||
<h3 className="text-lg font-medium mb-1">Roo Code Router</h3>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<p className="">
|
||||
On any plan, you can use your own LLM provider API key or use the built-in Roo Code
|
||||
Router – curated models to work with Roo with no markup, including the latest
|
||||
Gemini, GPT and Claude. Paid with credits.
|
||||
<Link href="/provider" className="underline hover:no-underline ml-1">
|
||||
See per model pricing.
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<h3 className="text-lg font-medium mb-1">Credits</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Credits are pre-paid, in dollars, and are deducted with usage for inference and Cloud
|
||||
Agent runs. You're always in control of your spend, no surprises.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Additional Information */}
|
||||
<section className="bg-background py-16 my-16 border-t border-b relative z-50">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-3xl text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Frequently Asked Questions</h2>
|
||||
</div>
|
||||
<div className="mx-auto mt-12 grid max-w-5xl gap-8 md:grid-cols-2">
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Wait, is Roo Code free or not?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes! The Roo Code VS Code extension is open source and free forever. The extension acts
|
||||
as a powerful AI coding assistant right in your editor. These are the prices for Roo
|
||||
Code Cloud.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Is there a free trial?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes, all paid plans come with a 14-day free trial to try out functionality.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
To use Cloud Agents, you can buy credits.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">How do credits work?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Roo Code Cloud credits can be used in two ways:
|
||||
</p>
|
||||
<ul className="mt-2 list-disc pl-5 text-sm text-muted-foreground">
|
||||
<li>To pay for Cloud Agents running time (${PRICE_CREDITS}/hour)</li>
|
||||
<li>
|
||||
To pay for AI model inference costs (
|
||||
<a
|
||||
href="/provider"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline">
|
||||
varies by model
|
||||
</a>
|
||||
)
|
||||
</li>
|
||||
</ul>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
To cover our infrastructure costs, we charge ${PRICE_CREDITS}/hour while the agent is
|
||||
running (independent of inference costs).
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
There are no markups, no tiers, no dumbing-down of models to increase our profit.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Do I need a credit card for the free trial?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes, but you won't be charged until your trial ends, except for credit purchases.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">You can cancel anytime with one click.</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">What payment methods do you accept?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
We accept all major credit cards, debit cards, and can arrange invoice billing for
|
||||
Enterprise customers.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">Can I cancel or change plans?</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Yes, you can upgrade, downgrade or cancel your plan at any time. Changes will be
|
||||
reflected in your next billing cycle.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border bg-card p-6 md:col-span-2">
|
||||
<h3 className="font-semibold">
|
||||
What if I have enterprise-level needs like SAML/SCIM, large-scale deployments, specific
|
||||
integrations and custom terms?
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
We have an Enterprise plan which can be a fit. Please{" "}
|
||||
<Link href="/enterprise#contact" className="underline hover:no-underline">
|
||||
reach out to our sales team
|
||||
</Link>{" "}
|
||||
to discuss it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 text-center">
|
||||
<p className="text-muted-foreground">
|
||||
Still have questions?{" "}
|
||||
<a
|
||||
href={EXTERNAL_LINKS.DISCORD}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
Join our Discord
|
||||
</a>{" "}
|
||||
or{" "}
|
||||
<Link
|
||||
href="/enterprise#contact"
|
||||
className="font-medium text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300">
|
||||
contact our sales team
|
||||
</Link>
|
||||
</p>
|
||||
</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">Try Roo Code Cloud now</h2>
|
||||
<p className="mx-auto mb-8 max-w-2xl text-lg text-muted-foreground">Code from anywhere.</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}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Create a free Cloud account
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,263 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { ModelCard } from "./pricing/components/model-card"
|
||||
import { Model, ModelWithTotalPrice, ModelsResponse, SortOption } from "@/lib/types/models"
|
||||
import Link from "next/link"
|
||||
import { ChevronDown, CircleX, Cloud, Loader, LoaderCircle, Puzzle, Search } from "lucide-react"
|
||||
|
||||
const API_URL = "https://api.roocode.com/proxy/v1/models?include_paid=true"
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "What are AI model providers?",
|
||||
answer: "AI model providers offer various language models with different capabilities and pricing.",
|
||||
},
|
||||
{
|
||||
question: "What is the Roo Code Router?",
|
||||
answer: (
|
||||
<>
|
||||
<p>This is our very own model router, optimized to work seamlessly with Roo Code Cloud.</p>
|
||||
<p>You don't have to use it to use Roo Code, but it's the easiest way to do it.</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
question: "Do I have to use the Roo Code Router to use the Roo Code products?",
|
||||
answer: "Not at all! You can bring your own provider key, no problem. This is just meant to make it easier.",
|
||||
},
|
||||
{
|
||||
question: "How is pricing calculated?",
|
||||
answer: "Pricing is based on token usage for input and output, measured per million tokens, like pretty much any other provider out there.",
|
||||
},
|
||||
{
|
||||
question: "How is my data treated?",
|
||||
answer: "The Roo Code Router doesn't keep any of your data, the service only aims to make it easier to use Roo Code. Each model vendor has their own privacy policy though, and usually free models use your data for training, so keep that in mind.",
|
||||
},
|
||||
{
|
||||
question: "How much does the Roo Code Cloud service cost?",
|
||||
answer: (
|
||||
<>
|
||||
Our{" "}
|
||||
<Link href="/pricing" className="underline hover:no-underline">
|
||||
service pricing is here.
|
||||
</Link>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
function calculateTotalPrice(model: Model): number {
|
||||
return parseFloat(model.pricing.input) + parseFloat(model.pricing.output)
|
||||
}
|
||||
|
||||
function enrichModelWithTotalPrice(model: Model): ModelWithTotalPrice {
|
||||
return {
|
||||
...model,
|
||||
totalPrice: calculateTotalPrice(model),
|
||||
}
|
||||
}
|
||||
|
||||
export default function ProviderPage() {
|
||||
const [models, setModels] = useState<ModelWithTotalPrice[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [sortOption, setSortOption] = useState<SortOption>("alphabetical")
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchModels() {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const response = await fetch(API_URL)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch models: ${response.statusText}`)
|
||||
}
|
||||
const data: ModelsResponse = await response.json()
|
||||
const enrichedModels = data.data.map(enrichModelWithTotalPrice)
|
||||
setModels(enrichedModels)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "An error occurred while fetching models")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchModels()
|
||||
}, [])
|
||||
|
||||
const filteredAndSortedModels = useMemo(() => {
|
||||
// Filter out deprecated models
|
||||
let filtered = models.filter((model) => !model.deprecated)
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase()
|
||||
filtered = filtered.filter((model) => {
|
||||
return (
|
||||
model.name.toLowerCase().includes(query) ||
|
||||
model.owned_by?.toLowerCase().includes(query) ||
|
||||
model.description.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Sort filtered results
|
||||
const sorted = [...filtered]
|
||||
switch (sortOption) {
|
||||
case "alphabetical":
|
||||
sorted.sort((a, b) => a.name.localeCompare(b.name))
|
||||
break
|
||||
case "price-asc":
|
||||
sorted.sort((a, b) => a.totalPrice - b.totalPrice)
|
||||
break
|
||||
case "price-desc":
|
||||
sorted.sort((a, b) => b.totalPrice - a.totalPrice)
|
||||
break
|
||||
case "context-window-asc":
|
||||
sorted.sort((a, b) => a.context_window - b.context_window)
|
||||
break
|
||||
case "context-window-desc":
|
||||
sorted.sort((a, b) => b.context_window - a.context_window)
|
||||
break
|
||||
}
|
||||
|
||||
return sorted
|
||||
}, [models, searchQuery, sortOption])
|
||||
|
||||
// Count non-deprecated models for the display
|
||||
const nonDeprecatedCount = useMemo(() => models.filter((model) => !model.deprecated).length, [models])
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="relative overflow-hidden py-16">
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl md:text-5xl font-bold tracking-tight">The Roo Code Router</h1>
|
||||
<p className="mx-auto mt-4 max-w-3xl md:text-lg text-muted-foreground">
|
||||
The easiest way to use Roo Code (in the{" "}
|
||||
<Link href="/cloud" className="underline hover:no-underline">
|
||||
<Cloud className="inline size-5 mx-1 -mt-0.5" />
|
||||
Cloud
|
||||
</Link>{" "}
|
||||
or the{" "}
|
||||
<Link href="/extension" className="underline hover:no-underline">
|
||||
<Puzzle className="inline size-5 mx-1 -mt-0.5" />
|
||||
Extension
|
||||
</Link>
|
||||
) with the best free and paid models – no separate accounts, no messing with API keys.
|
||||
</p>
|
||||
<p className="mx-auto mt-2 max-w-2xl md:text-lg text-muted-foreground">
|
||||
And you can always bring your own key (
|
||||
<Link href="#faq" className="underline hover:no-underline">
|
||||
FAQ
|
||||
</Link>
|
||||
).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-10 relative border-t border-b">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-violet-500/0 via-violet-500/10 to-violet-500/0 dark:from-blue-500/10 dark:via-cyan-500/10 dark:to-purple-500/10" />
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-4xl">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search models..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full rounded-full border border-input bg-background px-10 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
/>
|
||||
|
||||
<div className="text-sm cursor-default text-muted-foreground absolute bg-background right-0 top-0 m-0.5 px-3 py-2 rounded-full">
|
||||
{filteredAndSortedModels.length} of {nonDeprecatedCount} models
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="flex items-center gap-2 relative">
|
||||
<select
|
||||
id="sort"
|
||||
value={sortOption}
|
||||
onChange={(e) => setSortOption(e.target.value as SortOption)}
|
||||
className="rounded-full cursor-pointer border border-input bg-background hover:bg-muted pl-4 w-full md:w-auto pr-9 py-2.5 text-base ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 relative appearance-none">
|
||||
<option value="alphabetical">Alphabetical</option>
|
||||
<option value="price-asc">Price: Low to High</option>
|
||||
<option value="price-desc">Price: High to Low</option>
|
||||
<option value="context-window-asc">Context Window: Small to Large</option>
|
||||
<option value="context-window-desc">Context Window: Large to Small</option>
|
||||
</select>
|
||||
<ChevronDown className="size-4 absolute right-3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 ">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
{loading && (
|
||||
<div className="text-center pt-12 space-y-2 mb-4">
|
||||
<LoaderCircle className="size-8 text-muted-foreground mx-auto animate-spin" />
|
||||
<p className="text-lg">Loading model list...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="text-center pt-12 space-y-2">
|
||||
<CircleX className="size-8 text-muted-foreground mx-auto mb-4" />
|
||||
<p className="text-lg">Oops, couldn't load the model list.</p>
|
||||
<p className="text-muted-foreground">Try again in a bit please.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && filteredAndSortedModels.length === 0 && (
|
||||
<div className="text-center pt-12 space-y-2">
|
||||
<Loader className="size-8 text-muted-foreground mx-auto mb-4" />
|
||||
<p className="text-lg">No models match your search.</p>
|
||||
<p className="text-muted-foreground">
|
||||
Keep in mind we don't have every model under the sun – only the ones we think
|
||||
are worth using.
|
||||
<br />
|
||||
You can always use a third-party provider to access a wider selection.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && !error && filteredAndSortedModels.length > 0 && (
|
||||
<div className="grid gap-4 pt-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredAndSortedModels.map((model) => (
|
||||
<ModelCard key={model.id} model={model} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section className="bg-background my-16 relative z-50">
|
||||
<a id="faq" />
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-3xl text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl">Frequently Asked Questions</h2>
|
||||
</div>
|
||||
<div className="mx-auto mt-12 grid max-w-5xl gap-8 md:grid-cols-2">
|
||||
{faqs.map((faq, index) => (
|
||||
<div key={index} className="rounded-2xl border border-border bg-card p-6">
|
||||
<h3 className="font-semibold">{faq.question}</h3>
|
||||
<div className="mt-2 text-sm text-muted-foreground">{faq.answer}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
import { ModelWithTotalPrice } from "@/lib/types/models"
|
||||
import { formatCurrency, formatTokens } from "@/lib/formatters"
|
||||
import {
|
||||
ArrowLeftToLine,
|
||||
ArrowRightToLine,
|
||||
Building2,
|
||||
Check,
|
||||
Expand,
|
||||
Gift,
|
||||
HardDriveDownload,
|
||||
HardDriveUpload,
|
||||
RulerDimensionLine,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
} from "lucide-react"
|
||||
import { useState } from "react"
|
||||
|
||||
interface ModelCardProps {
|
||||
model: ModelWithTotalPrice
|
||||
}
|
||||
|
||||
export function ModelCard({ model }: ModelCardProps) {
|
||||
// Prices are per token, multiply by 1M to get price per million tokens
|
||||
const inputPrice = parseFloat(model.pricing.input) * 1_000_000
|
||||
const outputPrice = parseFloat(model.pricing.output) * 1_000_000
|
||||
const cacheReadPrice = parseFloat(model.pricing.input_cache_read || "0") * 1_000_000
|
||||
const cacheWritePrice = parseFloat(model.pricing.input_cache_write || "0") * 1_000_000
|
||||
|
||||
const free = model.tags.includes("free")
|
||||
// Filter tags to only show vision and reasoning
|
||||
const displayTags = model.tags.filter((tag) => tag === "vision" || tag === "reasoning")
|
||||
|
||||
// Mobile collapsed/expanded state
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
"relative cursor-default px-8 pt-7 pb-5 flex flex-col justify-start bg-background border rounded-3xl transition-all hover:shadow-xl",
|
||||
// On mobile, visually hint at expandability
|
||||
"sm:cursor-default",
|
||||
].join(" ")}>
|
||||
{/* Header: always visible */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xl font-semibold tracking-tight mb-2 flex items-center gap-2 justify-between">
|
||||
{model.name}
|
||||
{free && (
|
||||
<span className="inline-flex items-center text-sm font-medium text-green-500">
|
||||
<Gift className="size-4 mr-1" />
|
||||
Free!
|
||||
</span>
|
||||
)}
|
||||
</h3>
|
||||
<p
|
||||
className={[
|
||||
"text-sm text-muted-foreground",
|
||||
// On mobile + collapsed: clamp description
|
||||
"sm:line-clamp-none",
|
||||
!expanded ? "line-clamp-2" : "",
|
||||
]
|
||||
.join(" ")
|
||||
.trim()}>
|
||||
{model.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content - pinned to bottom */}
|
||||
<div className="overflow-x-auto mt-auto">
|
||||
<table className="w-full text-xs">
|
||||
<tbody>
|
||||
{/* Provider: always visible if present */}
|
||||
{model.owned_by && (
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<Building2 className="size-4 inline-block mr-1.5" />
|
||||
Provider
|
||||
</td>
|
||||
<td className="py-1.5 text-right">{model.owned_by}</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{/* Context Window: always visible */}
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<RulerDimensionLine className="size-4 inline-block mr-1.5" />
|
||||
Context Window
|
||||
</td>
|
||||
<td className="py-1.5 text-right font-mono">{formatTokens(model.context_window)}</td>
|
||||
</tr>
|
||||
|
||||
{/* Max Output Tokens: always visible on >=sm, expandable on mobile */}
|
||||
<tr
|
||||
className={["border-b border-border", expanded ? "table-row" : "hidden sm:table-row"].join(
|
||||
" ",
|
||||
)}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<Expand className="size-4 inline-block mr-1.5" />
|
||||
Max Output Tokens
|
||||
</td>
|
||||
<td className="py-1.5 text-right font-mono">{formatTokens(model.max_tokens)}</td>
|
||||
</tr>
|
||||
|
||||
{/* Input Price: always visible */}
|
||||
<tr className="border-b border-border">
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<ArrowRightToLine className="size-4 inline-block mr-1.5" />
|
||||
Input Price
|
||||
</td>
|
||||
<td className="py-1.5 text-right">
|
||||
{inputPrice === 0 ? "Free" : `${formatCurrency(inputPrice)}/1M tokens`}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Output Price: always visible */}
|
||||
<tr
|
||||
className={[
|
||||
"border-b border-border",
|
||||
// Add subtle separation from toggle on mobile
|
||||
].join(" ")}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<ArrowLeftToLine className="size-4 inline-block mr-1.5" />
|
||||
Output Price
|
||||
</td>
|
||||
<td className="py-1.5 text-right">
|
||||
{outputPrice === 0 ? "Free" : `${formatCurrency(outputPrice)}/1M tokens`}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Cache pricing: only visible on mobile when expanded, always visible on >=sm */}
|
||||
{cacheReadPrice > 0 && (
|
||||
<tr
|
||||
className={[
|
||||
"border-b border-border",
|
||||
expanded ? "table-row" : "hidden sm:table-row",
|
||||
].join(" ")}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<HardDriveUpload className="size-4 inline-block mr-1.5" />
|
||||
Cache Read
|
||||
</td>
|
||||
<td className="py-1.5 text-right">{formatCurrency(cacheReadPrice)}/1M tokens</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{cacheWritePrice > 0 && (
|
||||
<tr
|
||||
className={[
|
||||
"border-b border-border",
|
||||
expanded ? "table-row" : "hidden sm:table-row",
|
||||
].join(" ")}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground">
|
||||
<HardDriveDownload className="size-4 inline-block mr-1.5" />
|
||||
Cache Write
|
||||
</td>
|
||||
<td className="py-1.5 text-right">{formatCurrency(cacheWritePrice)}/1M tokens</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{/* Tags row: only show if there are vision or reasoning tags */}
|
||||
{displayTags.length > 0 && (
|
||||
<tr className={[expanded ? "table-row" : "hidden sm:table-row"].join(" ")}>
|
||||
<td className="py-1.5 font-medium text-muted-foreground align-top">Features</td>
|
||||
<td className="py-1.5">
|
||||
{displayTags.map((tag) => (
|
||||
<span key={tag} className="flex justify-end items-center text-xs capitalize">
|
||||
<Check className="size-3 m-1" />
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{/* Mobile-only toggle row */}
|
||||
<tr className="sm:hidden">
|
||||
<td colSpan={2} className="pt-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
className="w-full inline-flex items-center justify-center gap-1.5 text-xs font-medium text-primary">
|
||||
{expanded ? "Less" : "More"}
|
||||
{expanded ? <ChevronUp className="size-3" /> : <ChevronDown className="size-3" />}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
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: "Try now for free",
|
||||
disclaimer: "",
|
||||
tracking: "&agent=reviewer",
|
||||
},
|
||||
},
|
||||
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: "Try now for free",
|
||||
},
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
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: "Try now for free",
|
||||
disclaimer: "",
|
||||
tracking: "&agent=reviewer",
|
||||
},
|
||||
},
|
||||
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: "Try now for free",
|
||||
},
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import type { Metadata } from "next"
|
||||
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
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 =
|
||||
"Get comprehensive AI-powered PR reviews that save you time, not tokens. Bring your own API key and leverage advanced reasoning, repository-aware analysis, and actionable feedback to keep your PR queue moving."
|
||||
const OG_DESCRIPTION = "AI-powered PR reviews that save you time, not tokens"
|
||||
const PATH = "/reviewer"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${PATH}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: `${SEO.url}${PATH}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(TITLE, OG_DESCRIPTION),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: TITLE,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
|
||||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"PR reviewer",
|
||||
"code review",
|
||||
"pull request review",
|
||||
"AI code review",
|
||||
"GitHub PR review",
|
||||
"automated code review",
|
||||
"repository-aware review",
|
||||
"bring your own key",
|
||||
"BYOK AI",
|
||||
"code quality",
|
||||
"development workflow",
|
||||
"cloud agents",
|
||||
"AI development team",
|
||||
],
|
||||
}
|
||||
|
||||
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} />
|
||||
}
|
||||
|
|
@ -1,235 +0,0 @@
|
|||
"use client"
|
||||
|
||||
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, UseExamplesSection } from "@/components/homepage"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
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,
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an icon name string to a Lucide icon component
|
||||
*/
|
||||
function getIcon(iconName?: IconName): LucideIcon | undefined {
|
||||
return iconName ? iconMap[iconName] : undefined
|
||||
}
|
||||
|
||||
export function AgentLandingContent({ content }: { content: AgentPageContent }) {
|
||||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex min-h-screen md:min-h-[calc(70vh-theme(spacing.12))] items-center overflow-hidden py-12 md:py-0">
|
||||
<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-8 md:gap-12 lg:gap-20 lg:grid-cols-2">
|
||||
<div className="flex flex-col justify-center space-y-6 sm:space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight md:text-left md:text-4xl lg:text-5xl">
|
||||
{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-full lg:max-w-lg space-y-4 text-base text-muted-foreground md:text-left sm:mt-6">
|
||||
{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">
|
||||
{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>
|
||||
|
||||
<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}${content.hero.cta.tracking}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex w-full items-center justify-center">
|
||||
{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">
|
||||
{content.hero.cta.disclaimer}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{content.hero.image && (
|
||||
<div className="flex items-center justify-center lg:justify-end mx-auto h-full w-full">
|
||||
<div className="relative w-full max-w-full overflow-hidden rounded-lg">
|
||||
<Image
|
||||
src={content.hero.image.url}
|
||||
alt={content.hero.image.alt || "Hero image"}
|
||||
className="w-full h-auto"
|
||||
width={content.hero.image.width}
|
||||
height={content.hero.image.height}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* How It Works Section */}
|
||||
<section className="relative overflow-hidden border-t border-border py-32">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-violet-500/10 dark:bg-violet-700/20 blur-[140px]" />
|
||||
</div>
|
||||
<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">
|
||||
{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">
|
||||
{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 && <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>
|
||||
|
||||
{/* Why Better Section */}
|
||||
<section className="relative overflow-hidden border-t border-border py-32">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
|
||||
</div>
|
||||
<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">
|
||||
{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">
|
||||
{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 && <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 && <p>{feature.description}</p>}
|
||||
{feature.paragraphs &&
|
||||
feature.paragraphs.map((paragraph, pIndex) => (
|
||||
<p key={pIndex}>{paragraph}</p>
|
||||
))}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<UseExamplesSection agentTitle={true} />
|
||||
|
||||
{/* 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">{content.cta.heading}</h2>
|
||||
<p className="mx-auto mb-8 max-w-2xl text-lg text-muted-foreground">
|
||||
{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
|
||||
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}${content.hero.cta.tracking}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
{content.hero.cta.buttonText}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
/**
|
||||
* 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 {
|
||||
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
|
||||
tracking: 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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -1,401 +0,0 @@
|
|||
import {
|
||||
ArrowRight,
|
||||
Brain,
|
||||
CreditCard,
|
||||
GitBranch,
|
||||
GraduationCap,
|
||||
Link2,
|
||||
MessageSquare,
|
||||
Settings,
|
||||
Shield,
|
||||
Slack,
|
||||
Users,
|
||||
Zap,
|
||||
} from "lucide-react"
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import { AnimatedBackground } from "@/components/homepage"
|
||||
import { SlackThreadDemo } from "@/components/slack/slack-thread-demo"
|
||||
import { Button } from "@/components/ui"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
|
||||
const TITLE = "Roo Code for Slack"
|
||||
const DESCRIPTION =
|
||||
"Mention @Roomote in any channel to explain code, plan features, or ship a PR, all without leaving the conversation."
|
||||
const OG_DESCRIPTION = "Your AI Team in Slack"
|
||||
const PATH = "/slack"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${PATH}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: `${SEO.url}${PATH}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(TITLE, OG_DESCRIPTION),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: TITLE,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
|
||||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"slack integration",
|
||||
"slack bot",
|
||||
"AI in slack",
|
||||
"code assistant slack",
|
||||
"@Roomote",
|
||||
"team collaboration",
|
||||
],
|
||||
}
|
||||
|
||||
// Invalidate cache when a request comes in, at most once every hour.
|
||||
export const revalidate = 3600
|
||||
|
||||
type ValueProp = {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const VALUE_PROPS: ValueProp[] = [
|
||||
{
|
||||
icon: GitBranch,
|
||||
title: "Discussion to PR.",
|
||||
description:
|
||||
"Your team discusses a feature in Slack. @Roomote turns the discussion into a plan. Then builds it. All without leaving the conversation.",
|
||||
},
|
||||
{
|
||||
icon: Brain,
|
||||
title: "Thread-aware.",
|
||||
description:
|
||||
'@Roomote reads the full thread before responding. Ask "Can we add caching here?" and it knows exactly what code you mean.',
|
||||
},
|
||||
{
|
||||
icon: Link2,
|
||||
title: "Chain agents.",
|
||||
description:
|
||||
"Start with a Planner to spec it out. Then call the Coder to build it. Multi-step workflows, one Slack thread.",
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
title: "Open to all.",
|
||||
description:
|
||||
"Anyone on your team can ask @Roomote to fix bugs, build features, or investigate issues. Engineering gets looped in only when needed.",
|
||||
},
|
||||
{
|
||||
icon: GraduationCap,
|
||||
title: "Built-in learning.",
|
||||
description: "Public channel mentions show everyone how to leverage agents. Learn by watching.",
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: "Safe by design.",
|
||||
description: "Agents never touch main/master directly. They produce branches and PRs. You approve.",
|
||||
},
|
||||
]
|
||||
|
||||
type WorkflowStep = {
|
||||
step: number
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const WORKFLOW_STEPS: WorkflowStep[] = [
|
||||
{
|
||||
step: 1,
|
||||
title: "Turn the discussion into a plan",
|
||||
description: "Your team discusses a feature. When it gets complex, summon the Planner agent.",
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: "Refine the plan in the thread",
|
||||
description:
|
||||
"The team reviews the spec in the thread, suggests changes, asks questions. Mention @Roomote again to refine.",
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: "Build the plan",
|
||||
description: "Once the plan looks good, hand it off to the Coder agent to implement.",
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: "Review and ship",
|
||||
description: "The Coder creates a branch and opens a PR. The team reviews, and the feature ships.",
|
||||
},
|
||||
]
|
||||
|
||||
type OnboardingStep = {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string
|
||||
link?: {
|
||||
href: string
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
const ONBOARDING_STEPS: OnboardingStep[] = [
|
||||
{
|
||||
icon: CreditCard,
|
||||
title: "1. Team Plan",
|
||||
description: "Slack requires a Team plan.",
|
||||
link: {
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_TEAM_TRIAL,
|
||||
text: "Start a free trial",
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Settings,
|
||||
title: "2. Connect",
|
||||
description: 'Sign in to Roo Code Cloud and go to Settings. Click "Connect" next to Slack.',
|
||||
},
|
||||
{
|
||||
icon: Slack,
|
||||
title: "3. Authorize",
|
||||
description: "Authorize the Roo Code app to access your Slack workspace.",
|
||||
},
|
||||
{
|
||||
icon: MessageSquare,
|
||||
title: "4. Add to channels",
|
||||
description: "Add @Roomote to the channels where you want it available.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function SlackPage(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex pt-32 pb-20 items-center overflow-hidden">
|
||||
<AnimatedBackground />
|
||||
<div className="container relative flex flex-col items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid w-full max-w-6xl grid-cols-1 items-center gap-10 lg:grid-cols-2 lg:gap-12">
|
||||
<div className="text-center lg:text-left">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 text-sm font-medium mb-6">
|
||||
<Slack className="size-4" />
|
||||
Powered by Roo Code Cloud
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-6 md:text-5xl lg:text-6xl">
|
||||
<span className="text-violet-500">@Roomote:</span> Your AI Team in Slack
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto lg:mx-0">
|
||||
Mention @Roomote in any channel to explain code, plan features, or ship a PR, all
|
||||
without leaving the conversation.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center lg:justify-start">
|
||||
<Button
|
||||
size="xl"
|
||||
className="bg-violet-600 hover:bg-violet-700 text-white transition-all duration-300 shadow-lg hover:shadow-violet-500/25"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Get Started
|
||||
<ArrowRight className="ml-2 size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" size="xl" className="backdrop-blur-sm" asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.SLACK_DOCS}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Read the Docs
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center lg:justify-end">
|
||||
<SlackThreadDemo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Value Props Section */}
|
||||
<section className="py-24 bg-muted/30">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-violet-500/10 dark:bg-violet-700/20 blur-[140px]" />
|
||||
</div>
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">
|
||||
Why your team will love using Roo Code in Slack
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
AI agents that understand context, chain together for complex work, and keep your team in
|
||||
control.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto relative">
|
||||
{VALUE_PROPS.map((prop, index) => {
|
||||
const Icon = prop.icon
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-background p-8 rounded-2xl border border-border hover:shadow-lg transition-all duration-300">
|
||||
<div className="bg-violet-100 dark:bg-violet-900/20 w-12 h-12 rounded-lg flex items-center justify-center mb-6">
|
||||
<Icon className="size-6 text-violet-600 dark:text-violet-400" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-3">{prop.title}</h3>
|
||||
<p className="text-muted-foreground leading-relaxed">{prop.description}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Featured Workflow Section */}
|
||||
<section className="relative overflow-hidden border-t border-border py-24 lg:py-32">
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mb-12 max-w-5xl text-center">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 text-sm font-medium mb-6">
|
||||
<Zap className="size-4" />
|
||||
Featured Workflow
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-5xl mb-4">
|
||||
Thread to Shipped Feature
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Turn Slack discussions into working code. No context lost, no meetings needed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto max-w-6xl">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8 lg:gap-10 items-center">
|
||||
{/* YouTube Video Embed */}
|
||||
<div className="lg:col-span-3 overflow-hidden rounded-2xl border border-border bg-background shadow-lg">
|
||||
<iframe
|
||||
className="aspect-video w-full"
|
||||
src="https://www.youtube-nocookie.com/embed/dJM_8HHGe1E?rel=0"
|
||||
title="Roo Code Slack Integration Demo"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Workflow Steps */}
|
||||
<div className="lg:col-span-2 space-y-3">
|
||||
{WORKFLOW_STEPS.map((step) => (
|
||||
<div
|
||||
key={step.step}
|
||||
className="relative border border-border rounded-xl bg-background p-4 transition-all duration-300 hover:shadow-md hover:border-blue-500/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-blue-100 dark:bg-blue-900/30 w-7 h-7 rounded-full flex items-center justify-center text-blue-700 dark:text-blue-300 font-bold text-xs shrink-0 mt-0.5">
|
||||
{step.step}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-base font-semibold text-foreground mb-0.5">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="text-sm leading-snug text-muted-foreground">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Onboarding Section */}
|
||||
<section className="py-24 bg-muted/30">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-4xl mb-4">Get started in minutes</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Connect your Slack workspace and start working with AI agents.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 max-w-5xl mx-auto">
|
||||
{ONBOARDING_STEPS.map((step, index) => {
|
||||
const Icon = step.icon
|
||||
return (
|
||||
<div key={index} className="text-center">
|
||||
<div className="bg-violet-100 dark:bg-violet-900/20 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<Icon className="size-8 text-violet-600 dark:text-violet-400" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-2">{step.title}</h3>
|
||||
<p className="text-muted-foreground">
|
||||
{step.description}
|
||||
{step.link && (
|
||||
<>
|
||||
{" "}
|
||||
<a
|
||||
href={step.link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-violet-600 dark:text-violet-400 hover:underline">
|
||||
{step.link.text} →
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24">
|
||||
<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-violet-500/10 via-purple-500/5 to-blue-500/5 p-8 text-center shadow-2xl backdrop-blur-xl dark:border-white/10 sm:p-16">
|
||||
<h2 className="mb-6 text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
Start using Roo Code in Slack
|
||||
</h2>
|
||||
<p className="mx-auto mb-10 max-w-2xl text-lg text-muted-foreground">
|
||||
Start a free 14 day Team trial.
|
||||
</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-foreground text-background hover:bg-foreground/90 transition-all duration-300"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_TEAM_TRIAL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Start free trial
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -12,9 +12,9 @@ const dmSans = DM_Sans({
|
|||
export function RoomoteAnnouncementBanner() {
|
||||
return (
|
||||
<div className={`relative overflow-hidden bg-[#d8f14b] text-black ${dmSans.className}`}>
|
||||
<div className="relative flex flex-col items-center justify-center gap-3 px-6 py-5 sm:py-6">
|
||||
<div className="relative flex flex-col md:flex-row items-center justify-center gap-3 px-6 py-8 md:py-10 border-b-2 border-black">
|
||||
<p className="text-base sm:text-lg md:text-xl font-extrabold tracking-tight">
|
||||
The Roo Code team just launched something new.
|
||||
The Roo Code team has something new.
|
||||
</p>
|
||||
<a
|
||||
href="https://roomote.dev"
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { motion } from "framer-motion"
|
||||
|
||||
interface AnimatedTextProps {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function AnimatedText({ children, className }: AnimatedTextProps) {
|
||||
return (
|
||||
<motion.span
|
||||
className={className}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
ease: [0.2, 0.65, 0.3, 0.9],
|
||||
}}>
|
||||
{children}
|
||||
</motion.span>
|
||||
)
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import { useState, useRef, useEffect } from "react"
|
|||
import Link from "next/link"
|
||||
import Image from "next/image"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { EXTERNAL_LINKS, INTERNAL_LINKS } from "@/lib/constants"
|
||||
import { useLogoSrc } from "@/lib/hooks/use-logo-src"
|
||||
|
|
@ -12,11 +11,8 @@ import { ScrollButton } from "@/components/ui"
|
|||
|
||||
export function Footer() {
|
||||
const [privacyDropdownOpen, setPrivacyDropdownOpen] = useState(false)
|
||||
const [cloudDropdownOpen, setCloudDropdownOpen] = useState(false)
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
const cloudDropdownRef = useRef<HTMLDivElement>(null)
|
||||
const logoSrc = useLogoSrc()
|
||||
const { resolvedTheme } = useTheme()
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
|
|
@ -25,9 +21,6 @@ export function Footer() {
|
|||
if (dropdownRef.current && !dropdownRef.current.contains(target)) {
|
||||
setPrivacyDropdownOpen(false)
|
||||
}
|
||||
if (cloudDropdownRef.current && !cloudDropdownRef.current.contains(target)) {
|
||||
setCloudDropdownOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside)
|
||||
|
|
@ -46,21 +39,6 @@ export function Footer() {
|
|||
<p className="max-w-md text-sm leading-6 text-muted-foreground md:pr-16 lg:pr-32">
|
||||
Empowering developers to build better software faster with AI-powered tools and insights.
|
||||
</p>
|
||||
|
||||
{/* Made with Roo Code */}
|
||||
<a
|
||||
href="https://roocode.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center space-x-2 group">
|
||||
<Image
|
||||
src={resolvedTheme === "light" ? "/RooCode-Badge-blk.svg" : "/RooCode-Badge-white.svg"}
|
||||
alt="Made with Roo Code"
|
||||
width={120}
|
||||
height={40}
|
||||
className="h-8 w-auto opacity-70 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="mt-16 grid grid-cols-2 gap-8 xl:col-span-2 xl:mt-0">
|
||||
|
|
@ -75,45 +53,6 @@ export function Footer() {
|
|||
Features
|
||||
</ScrollButton>
|
||||
</li>
|
||||
<li>
|
||||
<div className="relative z-10" ref={cloudDropdownRef}>
|
||||
<button
|
||||
onClick={() => setCloudDropdownOpen(!cloudDropdownOpen)}
|
||||
className="flex items-center text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground"
|
||||
aria-expanded={cloudDropdownOpen}
|
||||
aria-haspopup="true">
|
||||
<span>Cloud Agents</span>
|
||||
<ChevronDown
|
||||
className={`ml-1 h-4 w-4 transition-transform ${cloudDropdownOpen ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{cloudDropdownOpen && (
|
||||
<div className="absolute z-50 mt-2 w-44 origin-top-left scale-95 rounded-md border border-border bg-background shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-100 ease-out data-[state=open]:scale-100 max-xs:right-0 max-xs:origin-top-right xs:left-0">
|
||||
<div className="flex flex-col gap-1 p-2 text-sm text-muted-foreground">
|
||||
<Link
|
||||
href="/cloud"
|
||||
onClick={() => setCloudDropdownOpen(false)}
|
||||
className="rounded-md px-3 py-2 transition-colors hover:bg-accent/50 hover:text-foreground">
|
||||
Cloud
|
||||
</Link>
|
||||
<Link
|
||||
href="/reviewer"
|
||||
onClick={() => setCloudDropdownOpen(false)}
|
||||
className="rounded-md px-3 py-2 transition-colors hover:bg-accent/50 hover:text-foreground">
|
||||
PR Reviewer
|
||||
</Link>
|
||||
<Link
|
||||
href="/pr-fixer"
|
||||
onClick={() => setCloudDropdownOpen(false)}
|
||||
className="rounded-md px-3 py-2 transition-colors hover:bg-accent/50 hover:text-foreground">
|
||||
PR Fixer
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.DOCUMENTATION}
|
||||
|
|
@ -132,31 +71,6 @@ export function Footer() {
|
|||
Changelog
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.TESTIMONIALS}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground">
|
||||
Testimonials
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/enterprise"
|
||||
className="text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground">
|
||||
Enterprise
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.SECURITY}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground">
|
||||
Security Center
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-10 md:mt-0">
|
||||
|
|
@ -178,15 +92,6 @@ export function Footer() {
|
|||
Evals
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.FAQ}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground">
|
||||
FAQ
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.TUTORIALS}
|
||||
|
|
@ -237,15 +142,6 @@ export function Footer() {
|
|||
Contact
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CAREERS}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground">
|
||||
Careers
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/terms"
|
||||
|
|
@ -345,33 +241,6 @@ export function Footer() {
|
|||
X / Twitter
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.LINKEDIN}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground">
|
||||
LinkedIn
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.BLUESKY}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground">
|
||||
Bluesky
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.TIKTOK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm leading-6 text-muted-foreground transition-colors hover:text-foreground">
|
||||
TikTok
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.YOUTUBE}
|
||||
|
|
|
|||
|
|
@ -13,26 +13,16 @@ import { EXTERNAL_LINKS } from "@/lib/constants"
|
|||
import { useLogoSrc } from "@/lib/hooks/use-logo-src"
|
||||
import { ScrollButton } from "@/components/ui"
|
||||
import ThemeToggle from "@/components/chromes/theme-toggle"
|
||||
import { Brain, Cloud, Puzzle, Slack, X } from "lucide-react"
|
||||
import { X } from "lucide-react"
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
} from "@/components/ui/navigation-menu"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function LinearIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 100 100" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.22541 61.5228c-.2225-.9485.90748-1.5459 1.59638-.857L39.3342 97.1782c.6889.6889.0915 1.8189-.857 1.5964C20.0515 94.4522 5.54779 79.9485 1.22541 61.5228ZM.00189135 46.8891c-.01764375.2833.08887215.5599.28957165.7606L52.3503 99.7085c.2007.2007.4773.3075.7606.2896 2.3692-.1476 4.6938-.46 6.9624-.9259.7645-.157 1.0301-1.0963.4782-1.6481L2.57595 39.4485c-.55186-.5519-1.49117-.2863-1.648174.4782-.465915 2.2686-.77832 4.5932-.92588465 6.9624ZM4.21093 29.7054c-.16649.3738-.08169.8106.20765 1.1l64.77602 64.776c.2894.2894.7262.3742 1.1.2077 1.7861-.7956 3.5171-1.6927 5.1855-2.684.5521-.328.6373-1.0867.1832-1.5407L8.43566 24.3367c-.45409-.4541-1.21271-.3689-1.54074.1832-.99132 1.6684-1.88843 3.3994-2.68399 5.1855ZM12.6587 18.074c-.3701-.3701-.393-.9637-.0443-1.3541C21.7795 6.45931 35.1114 0 49.9519 0 77.5927 0 100 22.4073 100 50.0481c0 14.8405-6.4593 28.1724-16.7199 37.3375-.3903.3487-.984.3258-1.3542-.0443L12.6587 18.074Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
interface NavBarProps {
|
||||
stars: string | null
|
||||
downloads: string | null
|
||||
|
|
@ -54,117 +44,6 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
{/* Desktop Navigation */}
|
||||
<NavigationMenu className="grow ml-6 hidden text-sm md:flex">
|
||||
<NavigationMenuList>
|
||||
{/* Product Dropdown */}
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger className="bg-transparent font-light">Product</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<ul className="grid min-w-[260px] gap-1 p-2">
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/extension"
|
||||
className="flex items-center select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
|
||||
<Puzzle className="size-3 mr-2" />
|
||||
Roo Code VS Code Extension
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/cloud"
|
||||
className="flex items-center select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
|
||||
<Cloud className="size-3 mr-2" />
|
||||
Roo Code Cloud
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/slack"
|
||||
className="flex items-center select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
|
||||
<Slack className="size-3 mr-2" />
|
||||
Roo Code for Slack
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/linear"
|
||||
className="flex items-center select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
|
||||
<LinearIcon className="size-3 mr-2" />
|
||||
Roo Code for Linear
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/provider"
|
||||
className="flex items-center select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
|
||||
<Brain className="size-3 mr-2" />
|
||||
Roo Code Router
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
|
||||
{/* Resources Dropdown */}
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger className="bg-transparent font-light">
|
||||
Resources
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<ul className="grid min-w-[260px] gap-1 p-2">
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="block select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
|
||||
Blog
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/evals"
|
||||
className="block select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
|
||||
Evals
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.DISCORD}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
|
||||
Discord
|
||||
</a>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<li>
|
||||
<NavigationMenuLink asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.SECURITY}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block select-none rounded-md px-3 py-2 text-sm leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground">
|
||||
Trust Center
|
||||
</a>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
|
||||
{/* Docs Link */}
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
|
|
@ -175,12 +54,29 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
|
||||
{/* Pricing Link */}
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
className={cn(navigationMenuTriggerStyle(), "bg-transparent font-light")}>
|
||||
<Link href="/pricing">Pricing</Link>
|
||||
<Link href="/blog">Blog</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
className={cn(navigationMenuTriggerStyle(), "bg-transparent font-light")}>
|
||||
<Link href="/evals">Evals</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
className={cn(navigationMenuTriggerStyle(), "bg-transparent font-light")}>
|
||||
<a href={EXTERNAL_LINKS.DISCORD} target="_blank" rel="noopener noreferrer">
|
||||
Discord
|
||||
</a>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
|
|
@ -204,13 +100,6 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
className="hidden items-center gap-1.5 rounded-md py-2 text-sm border border-primary-background px-4 text-primary-background transition-all duration-200 hover:shadow-lg hover:scale-105 lg:flex">
|
||||
Log in
|
||||
</a>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_HOME}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hidden items-center gap-1.5 rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground transition-all duration-200 hover:shadow-lg hover:scale-105 md:flex">
|
||||
Sign Up
|
||||
</a>
|
||||
<Link
|
||||
href={EXTERNAL_LINKS.MARKETPLACE}
|
||||
target="_blank"
|
||||
|
|
@ -243,88 +132,37 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
<a
|
||||
href={EXTERNAL_LINKS.DOCUMENTATION}
|
||||
target="_blank"
|
||||
className="block w-full p-5 text-left text-foreground active:opacity-50"
|
||||
className="block w-full p-5 py-4 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Docs
|
||||
</a>
|
||||
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="block w-full p-5 text-left text-foreground active:opacity-50"
|
||||
href="/blog"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Pricing
|
||||
Blog
|
||||
</Link>
|
||||
|
||||
{/* Product Section */}
|
||||
<div className="mt-4 w-full">
|
||||
<div className="px-5 pb-2 pt-4 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Product
|
||||
</div>
|
||||
<Link
|
||||
href="/extension"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Roo Code VS Code Extension
|
||||
</Link>
|
||||
<Link
|
||||
href="/cloud"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Roo Code Cloud
|
||||
</Link>
|
||||
<Link
|
||||
href="/slack"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Roo Code for Slack
|
||||
</Link>
|
||||
<Link
|
||||
href="/provider"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Roo Code Router
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Resources Section */}
|
||||
<div className="mt-4 w-full">
|
||||
<div className="px-5 pb-2 pt-4 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Resources
|
||||
</div>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Blog
|
||||
</Link>
|
||||
<ScrollButton
|
||||
targetId="faq"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
FAQ
|
||||
</ScrollButton>
|
||||
<Link
|
||||
href="/evals"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Evals
|
||||
</Link>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.DISCORD}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Discord
|
||||
</a>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.SECURITY}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Security Center
|
||||
</a>
|
||||
</div>
|
||||
<ScrollButton
|
||||
targetId="faq"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
FAQ
|
||||
</ScrollButton>
|
||||
<Link
|
||||
href="/evals"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Evals
|
||||
</Link>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.DISCORD}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block w-full p-5 py-3 text-left text-foreground active:opacity-50"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Discord
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Bottom section with Cloud Login and stats */}
|
||||
|
|
@ -351,14 +189,6 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
</Link>
|
||||
</div>
|
||||
<div className="flex gap-2 px-4 pb-4">
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_HOME}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 rounded-full border border-primary bg-foreground p-4 w-full text-base font-semibold text-background"
|
||||
onClick={() => setIsMenuOpen(false)}>
|
||||
Sign up
|
||||
</a>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_LOGIN}
|
||||
target="_blank"
|
||||
|
|
|
|||
|
|
@ -1,291 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useRef } from "react"
|
||||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui"
|
||||
|
||||
// Define the form schema using Zod
|
||||
const contactFormSchema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
company: z.string().min(1, "Company is required"),
|
||||
email: z.string().email("Invalid email address"),
|
||||
website: z.string().url("Invalid website URL").or(z.string().length(0)),
|
||||
engineerCount: z.enum(["1-10", "11-50", "51-200", "201-500", "501-1000", "1000+"]),
|
||||
formType: z.enum(["early-access", "demo"]),
|
||||
_honeypot: z.string().optional(),
|
||||
})
|
||||
|
||||
interface ContactFormProps {
|
||||
formType: "early-access" | "demo"
|
||||
buttonText: string
|
||||
buttonClassName?: string
|
||||
}
|
||||
|
||||
export function ContactForm({ formType, buttonText, buttonClassName }: ContactFormProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [formErrors, setFormErrors] = useState<Record<string, string>>({})
|
||||
const [submitStatus, setSubmitStatus] = useState<"idle" | "success" | "error">("idle")
|
||||
const formRef = useRef<HTMLFormElement>(null)
|
||||
|
||||
const formTitle = formType === "early-access" ? "Become an Early Access Partner" : "Request a Demo"
|
||||
|
||||
const formDescription =
|
||||
formType === "early-access"
|
||||
? "Fill out the form below to collaborate in shaping Roo Code's enterprise solution."
|
||||
: "Fill out the form below to see Roo Code's enterprise capabilities in action."
|
||||
|
||||
// Get Basin endpoint from environment variable
|
||||
// This should be set in .env.local as NEXT_PUBLIC_BASIN_ENDPOINT="https://usebasin.com/f/your-form-id"
|
||||
const BASIN_ENDPOINT = process.env.NEXT_PUBLIC_BASIN_ENDPOINT || ""
|
||||
|
||||
// Check if Basin endpoint is configured
|
||||
if (!BASIN_ENDPOINT) {
|
||||
console.warn("NEXT_PUBLIC_BASIN_ENDPOINT is not configured. Form submissions will not work.")
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
setIsSubmitting(true)
|
||||
setFormErrors({})
|
||||
setSubmitStatus("idle")
|
||||
|
||||
const form = e.currentTarget
|
||||
const formData = new FormData(form)
|
||||
|
||||
// Create a data object for validation and submission
|
||||
const data = {
|
||||
name: formData.get("name") as string,
|
||||
company: formData.get("company") as string,
|
||||
email: formData.get("email") as string,
|
||||
website: formData.get("website") as string,
|
||||
engineerCount: formData.get("engineerCount") as string,
|
||||
formType,
|
||||
// Include honeypot field for spam protection
|
||||
_honeypot: formData.get("_honeypot") as string,
|
||||
}
|
||||
|
||||
// Validate form data on client side
|
||||
try {
|
||||
contactFormSchema.parse(data)
|
||||
|
||||
// Submit data to Basin
|
||||
try {
|
||||
const response = await fetch(BASIN_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
mode: "cors", // Ensure proper CORS handling
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
|
||||
// Basin returns a 200 status code on success
|
||||
if (response.ok) {
|
||||
try {
|
||||
const responseData = await response.json()
|
||||
|
||||
// Basin JSON API typically returns a 'status' property of 'success' when submission succeeds
|
||||
if (responseData && (responseData.success === true || responseData.status === "success")) {
|
||||
setSubmitStatus("success")
|
||||
// Reset form safely
|
||||
if (form) {
|
||||
form.reset()
|
||||
}
|
||||
} else {
|
||||
console.error("Basin error:", responseData)
|
||||
setSubmitStatus("error")
|
||||
}
|
||||
} catch (jsonError) {
|
||||
// In case response parsing fails but status was OK, assume success
|
||||
console.error("Error parsing JSON response:", jsonError)
|
||||
setSubmitStatus("success")
|
||||
if (form) {
|
||||
form.reset()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle error response from Basin (4xx or 5xx)
|
||||
try {
|
||||
const errorData = await response.json()
|
||||
console.error("Basin API error:", response.status, errorData)
|
||||
} catch {
|
||||
console.error("Basin returned error status:", response.status)
|
||||
}
|
||||
setSubmitStatus("error")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error)
|
||||
setSubmitStatus("error")
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const errors: Record<string, string> = {}
|
||||
error.errors.forEach((err) => {
|
||||
if (err.path[0]) {
|
||||
errors[err.path[0] as string] = err.message
|
||||
}
|
||||
})
|
||||
setFormErrors(errors)
|
||||
} else {
|
||||
setSubmitStatus("error")
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className={buttonClassName || ""}>{buttonText}</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{formTitle}</DialogTitle>
|
||||
<DialogDescription>{formDescription}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{submitStatus === "success" ? (
|
||||
<div className="flex flex-col items-center justify-center py-6">
|
||||
<div className="mb-4 rounded-full bg-green-100 p-3 text-green-600 dark:bg-green-900/20 dark:text-green-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="mb-2 text-xl font-bold">Thank You!</h3>
|
||||
<p className="text-center text-muted-foreground">
|
||||
Your information has been submitted successfully. Our team will be in touch with you
|
||||
shortly.
|
||||
</p>
|
||||
<Button className="mt-4" onClick={() => setIsOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<form ref={formRef} onSubmit={handleSubmit} className="space-y-4" data-basin-form>
|
||||
{/* Basin honeypot field for spam protection - should remain empty and hidden */}
|
||||
<input type="text" name="_honeypot" className="hidden" style={{ display: "none" }} />
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="name" className="text-sm font-medium">
|
||||
Name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
className={`w-full rounded-md border ${
|
||||
formErrors.name ? "border-red-500" : "border-input"
|
||||
} bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring`}
|
||||
placeholder="Your name"
|
||||
/>
|
||||
{formErrors.name && <p className="text-xs text-red-500">{formErrors.name}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="company" className="text-sm font-medium">
|
||||
Company <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="company"
|
||||
name="company"
|
||||
type="text"
|
||||
className={`w-full rounded-md border ${
|
||||
formErrors.company ? "border-red-500" : "border-input"
|
||||
} bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring`}
|
||||
placeholder="Your company"
|
||||
/>
|
||||
{formErrors.company && <p className="text-xs text-red-500">{formErrors.company}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="text-sm font-medium">
|
||||
Email <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
className={`w-full rounded-md border ${
|
||||
formErrors.email ? "border-red-500" : "border-input"
|
||||
} bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring`}
|
||||
placeholder="your.email@example.com"
|
||||
/>
|
||||
{formErrors.email && <p className="text-xs text-red-500">{formErrors.email}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="website" className="text-sm font-medium">
|
||||
Website
|
||||
</label>
|
||||
<input
|
||||
id="website"
|
||||
name="website"
|
||||
type="url"
|
||||
className={`w-full rounded-md border ${
|
||||
formErrors.website ? "border-red-500" : "border-input"
|
||||
} bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring`}
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
{formErrors.website && <p className="text-xs text-red-500">{formErrors.website}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="engineerCount" className="text-sm font-medium">
|
||||
Number of Software Engineers <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
id="engineerCount"
|
||||
name="engineerCount"
|
||||
className={`w-full rounded-md border ${
|
||||
formErrors.engineerCount ? "border-red-500" : "border-input"
|
||||
} bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring`}
|
||||
defaultValue="1-10">
|
||||
<option value="1-10">1-10</option>
|
||||
<option value="11-50">11-50</option>
|
||||
<option value="51-200">51-200</option>
|
||||
<option value="201-500">201-500</option>
|
||||
<option value="501-1000">501-1000</option>
|
||||
<option value="1000+">1000+</option>
|
||||
</select>
|
||||
{formErrors.engineerCount && (
|
||||
<p className="text-xs text-red-500">{formErrors.engineerCount}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{submitStatus === "error" && (
|
||||
<div className="rounded-md bg-red-50 p-3 text-sm text-red-500 dark:bg-red-900/20">
|
||||
There was an error submitting your request. Please try again later.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Submitting..." : "Submit"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { Button } from "@/components/ui"
|
||||
import { ArrowRight, Download } from "lucide-react"
|
||||
import { Download } from "lucide-react"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
|
||||
export function CTASection() {
|
||||
return (
|
||||
<section className="py-24 bg-muted/30 border-t border-border">
|
||||
<div className="container px-4 mx-auto sm:px-6 lg:px-8 text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-5xl mb-8">Build faster. Solo or Together.</h2>
|
||||
<h2 className="text-3xl font-bold tracking-tight sm:text-5xl mb-8">Build better and faster</h2>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<Button size="lg" className="w-full sm:w-auto h-12 px-8 text-base">
|
||||
|
|
@ -19,17 +19,6 @@ export function CTASection() {
|
|||
Install on VS Code
|
||||
</a>
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="lg" className="w-full sm:w-auto h-12 px-8 text-base">
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2">
|
||||
Try Cloud for Free
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { useState } from "react"
|
|||
import { motion } from "framer-motion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import Link from "next/link"
|
||||
|
||||
interface FAQItem {
|
||||
question: string
|
||||
|
|
@ -108,9 +107,13 @@ const faqs: FAQItem[] = [
|
|||
Absolutely. Roo Code uses efficient strategies (like partial-file analysis, summarization, or
|
||||
user-specified context) to handle large codebases. Enterprises especially appreciate the on-prem or
|
||||
self-hosted model option for compliance and security needs.{" "}
|
||||
<Link href="/enterprise" className="text-primary underline-offset-4 hover:underline">
|
||||
Learn more about Roo Code for enterprise.
|
||||
</Link>
|
||||
<a
|
||||
href="https://docs.roocode.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline-offset-4 hover:underline">
|
||||
Learn more in our docs.
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
|
@ -123,11 +126,11 @@ const faqs: FAQItem[] = [
|
|||
approval. And because Roo Code is fully open-source, it's auditable—you can review exactly how it
|
||||
works before deploying it.{" "}
|
||||
<a
|
||||
href="https://roocode.com/enterprise"
|
||||
href="https://docs.roocode.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline-offset-4 hover:underline">
|
||||
Learn more about Roo Code for enterprise.
|
||||
Learn more in our docs.
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -86,8 +86,12 @@ export function OptionOverviewSection() {
|
|||
</div>
|
||||
|
||||
<Button size="lg" variant="default" className="bg-violet-600 hover:bg-violet-600/80">
|
||||
<a href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP_HOME} className="flex items-center justify-center">
|
||||
Try now for free
|
||||
<a
|
||||
href={EXTERNAL_LINKS.DOCUMENTATION}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Read Docs
|
||||
<ArrowRight className="ml-2" />
|
||||
</a>
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -76,9 +76,8 @@ export function PillarsSection() {
|
|||
</p>
|
||||
<p>Roo doesn't care.</p>
|
||||
<p>
|
||||
It works great with 10s of models, from frontier to open weight. Choose from{" "}
|
||||
<Link href="/provider">the curated selection we offer at-cost</Link> or bring
|
||||
your own key.
|
||||
It works great with 10s of models, from frontier to open weight. Bring your own
|
||||
key and choose what works best for your workflow.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
|
|
|
|||
|
|
@ -1,442 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import type { ReactNode } from "react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { ChevronRight, GitPullRequest, Paperclip, Send } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type ActivityItem = {
|
||||
id: string
|
||||
kind: "comment" | "event" | "pr-link"
|
||||
author?: string
|
||||
avatarText?: string
|
||||
avatarClassName?: string
|
||||
body: ReactNode
|
||||
timeLabel: string
|
||||
}
|
||||
|
||||
function usePrefersReducedMotion(): boolean {
|
||||
const [reduced, setReduced] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia("(prefers-reduced-motion: reduce)")
|
||||
const onChange = () => setReduced(media.matches)
|
||||
onChange()
|
||||
|
||||
if (typeof media.addEventListener === "function") {
|
||||
media.addEventListener("change", onChange)
|
||||
return () => media.removeEventListener("change", onChange)
|
||||
}
|
||||
|
||||
media.addListener?.(onChange)
|
||||
return () => media.removeListener?.(onChange)
|
||||
}, [])
|
||||
|
||||
return reduced
|
||||
}
|
||||
|
||||
type TypingDotsProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
function TypingDots({ className }: TypingDotsProps): JSX.Element {
|
||||
return (
|
||||
<span className={cn("inline-flex items-center gap-0.5", className)} aria-hidden="true">
|
||||
<span className="h-1 w-1 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:0ms]" />
|
||||
<span className="h-1 w-1 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:180ms]" />
|
||||
<span className="h-1 w-1 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:360ms]" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function LinearIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 100 100" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.22541 61.5228c-.2225-.9485.90748-1.5459 1.59638-.857L39.3342 97.1782c.6889.6889.0915 1.8189-.857 1.5964C20.0515 94.4522 5.54779 79.9485 1.22541 61.5228ZM.00189135 46.8891c-.01764375.2833.08887215.5599.28957165.7606L52.3503 99.7085c.2007.2007.4773.3075.7606.2896 2.3692-.1476 4.6938-.46 6.9624-.9259.7645-.157 1.0301-1.0963.4782-1.6481L2.57595 39.4485c-.55186-.5519-1.49117-.2863-1.648174.4782-.465915 2.2686-.77832 4.5932-.92588465 6.9624ZM4.21093 29.7054c-.16649.3738-.08169.8106.20765 1.1l64.77602 64.776c.2894.2894.7262.3742 1.1.2077 1.7861-.7956 3.5171-1.6927 5.1855-2.684.5521-.328.6373-1.0867.1832-1.5407L8.43566 24.3367c-.45409-.4541-1.21271-.3689-1.54074.1832-.99132 1.6684-1.88843 3.3994-2.68399 5.1855ZM12.6587 18.074c-.3701-.3701-.393-.9637-.0443-1.3541C21.7795 6.45931 35.1114 0 49.9519 0 77.5927 0 100 22.4073 100 50.0481c0 14.8405-6.4593 28.1724-16.7199 37.3375-.3903.3487-.984.3258-1.3542-.0443L12.6587 18.074Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
type ActivityRowProps = {
|
||||
item: ActivityItem
|
||||
isNew: boolean
|
||||
reduceMotion: boolean
|
||||
}
|
||||
|
||||
function ActivityRow({ item, isNew, reduceMotion }: ActivityRowProps): JSX.Element {
|
||||
let animation = ""
|
||||
if (!reduceMotion && isNew) {
|
||||
animation = "animate-in fade-in slide-in-from-bottom-1 duration-300"
|
||||
}
|
||||
|
||||
// Event items (status changes, etc.) - compact inline format
|
||||
if (item.kind === "event") {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2 text-[13px] text-[#8B8D91]", animation)}>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[9px] font-semibold",
|
||||
item.avatarClassName,
|
||||
)}>
|
||||
{item.avatarText}
|
||||
</div>
|
||||
<span className="text-[#F8F8F9]">{item.author}</span>
|
||||
<span>{item.body}</span>
|
||||
<span className="text-[#5C5F66]">·</span>
|
||||
<span>{item.timeLabel}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// PR link events
|
||||
if (item.kind === "pr-link") {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-2 text-[13px] text-[#8B8D91]", animation)}>
|
||||
<GitPullRequest className="h-4 w-4 shrink-0 text-emerald-500" />
|
||||
<span>{item.body}</span>
|
||||
<span className="text-[#5C5F66]">·</span>
|
||||
<span>{item.timeLabel}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Comment items - more substantial with message body
|
||||
return (
|
||||
<div className={cn("flex gap-2.5", animation)}>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[9px] font-semibold",
|
||||
item.avatarClassName,
|
||||
)}>
|
||||
{item.avatarText}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 text-[13px]">
|
||||
<span className="font-medium text-[#F8F8F9]">{item.author}</span>
|
||||
<span className="text-[#5C5F66]">·</span>
|
||||
<span className="text-[#8B8D91]">{item.timeLabel}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-[13px] leading-relaxed text-[#D1D2D3]">{item.body}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export type LinearIssueDemoProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function LinearIssueDemo({ className }: LinearIssueDemoProps): JSX.Element {
|
||||
const reduceMotion = usePrefersReducedMotion()
|
||||
const [stepIndex, setStepIndex] = useState(0)
|
||||
const scrollViewportRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const activityItems: ActivityItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "a1",
|
||||
kind: "comment",
|
||||
author: "Jordan",
|
||||
avatarText: "J",
|
||||
avatarClassName: "bg-amber-600 text-white",
|
||||
body: (
|
||||
<span>
|
||||
<span className="text-indigo-400">@Roo Code</span> Can you implement this feature?
|
||||
</span>
|
||||
),
|
||||
timeLabel: "2m ago",
|
||||
},
|
||||
{
|
||||
id: "a2",
|
||||
kind: "comment",
|
||||
author: "Roo Code",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-indigo-600 text-white",
|
||||
body: <span>Analyzing issue requirements and codebase...</span>,
|
||||
timeLabel: "2m ago",
|
||||
},
|
||||
{
|
||||
id: "a3",
|
||||
kind: "event",
|
||||
author: "Roo Code",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-indigo-600 text-white",
|
||||
body: <span>moved to In Progress</span>,
|
||||
timeLabel: "2m ago",
|
||||
},
|
||||
{
|
||||
id: "a4",
|
||||
kind: "comment",
|
||||
author: "Roo Code",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-indigo-600 text-white",
|
||||
body: <span>Planning implementation: Settings component with light/dark toggle.</span>,
|
||||
timeLabel: "1m ago",
|
||||
},
|
||||
{
|
||||
id: "a5",
|
||||
kind: "comment",
|
||||
author: "Jordan",
|
||||
avatarText: "J",
|
||||
avatarClassName: "bg-amber-600 text-white",
|
||||
body: (
|
||||
<span>
|
||||
<span className="text-indigo-400">@Roo Code</span> Please also add a "system" option
|
||||
that follows OS preference.
|
||||
</span>
|
||||
),
|
||||
timeLabel: "1m ago",
|
||||
},
|
||||
{
|
||||
id: "a6",
|
||||
kind: "comment",
|
||||
author: "Roo Code",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-indigo-600 text-white",
|
||||
body: (
|
||||
<span>
|
||||
Got it! Adding system preference detection using{" "}
|
||||
<code className="rounded bg-white/10 px-1 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
prefers-color-scheme
|
||||
</code>
|
||||
</span>
|
||||
),
|
||||
timeLabel: "30s ago",
|
||||
},
|
||||
{
|
||||
id: "a7",
|
||||
kind: "pr-link",
|
||||
body: (
|
||||
<span>
|
||||
<span className="text-[#F8F8F9]">Roo Code</span> linked{" "}
|
||||
<span className="text-emerald-400">PR #847</span>
|
||||
</span>
|
||||
),
|
||||
timeLabel: "just now",
|
||||
},
|
||||
{
|
||||
id: "a8",
|
||||
kind: "comment",
|
||||
author: "Roo Code",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-indigo-600 text-white",
|
||||
body: (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
PR ready for review:{" "}
|
||||
<span className="text-indigo-400 hover:underline cursor-default">#847</span>
|
||||
</div>
|
||||
<div className="rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-[12px]">
|
||||
<div className="flex items-center gap-2 text-emerald-400">
|
||||
<GitPullRequest className="h-3.5 w-3.5" />
|
||||
<span className="font-medium">feat: add theme toggle with system preference</span>
|
||||
</div>
|
||||
<div className="mt-1 text-[#8B8D91]">+142 -12 · 3 files changed</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
timeLabel: "just now",
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
type DemoPhase =
|
||||
| { kind: "issue" }
|
||||
| { kind: "show"; activityIndex: number }
|
||||
| { kind: "typing"; activityIndex: number }
|
||||
| { kind: "reset" }
|
||||
|
||||
const phases: DemoPhase[] = useMemo(() => {
|
||||
const next: DemoPhase[] = []
|
||||
|
||||
next.push({ kind: "issue" })
|
||||
|
||||
for (let activityIndex = 0; activityIndex < activityItems.length; activityIndex += 1) {
|
||||
const item = activityItems[activityIndex]
|
||||
if (item?.kind === "comment") {
|
||||
next.push({ kind: "typing", activityIndex })
|
||||
}
|
||||
next.push({ kind: "show", activityIndex })
|
||||
}
|
||||
next.push({ kind: "reset" })
|
||||
return next
|
||||
}, [activityItems])
|
||||
|
||||
const lastShowPhaseIndex = useMemo(() => {
|
||||
let lastIndex = -1
|
||||
for (let idx = 0; idx < phases.length; idx += 1) {
|
||||
if (phases[idx]?.kind === "show") lastIndex = idx
|
||||
}
|
||||
return lastIndex
|
||||
}, [phases])
|
||||
|
||||
useEffect(() => {
|
||||
if (reduceMotion) {
|
||||
setStepIndex(lastShowPhaseIndex >= 0 ? lastShowPhaseIndex : 0)
|
||||
return
|
||||
}
|
||||
|
||||
const active = phases[stepIndex] ?? phases.at(0)
|
||||
const isLastMessageShow = active?.kind === "show" && stepIndex === lastShowPhaseIndex
|
||||
const durationMs = (() => {
|
||||
const base = 2000
|
||||
if (active?.kind === "reset") return 500
|
||||
if (active?.kind === "issue") return 1500
|
||||
if (active?.kind === "typing") return 800
|
||||
return isLastMessageShow ? base * 2.5 : base
|
||||
})()
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
const nextIndex = (stepIndex + 1) % phases.length
|
||||
setStepIndex(nextIndex)
|
||||
}, durationMs)
|
||||
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [lastShowPhaseIndex, phases, reduceMotion, stepIndex])
|
||||
|
||||
const activePhase = phases[stepIndex] ?? phases.at(0) ?? { kind: "issue" }
|
||||
|
||||
function getVisibleCount(phase: DemoPhase): number {
|
||||
if (phase.kind === "reset" || phase.kind === "issue") return 0
|
||||
if (phase.kind === "typing") return phase.activityIndex
|
||||
return phase.activityIndex + 1
|
||||
}
|
||||
|
||||
const visibleCount = getVisibleCount(activePhase)
|
||||
const visibleActivities = activityItems.slice(0, visibleCount)
|
||||
const typingTarget = activePhase.kind === "typing" ? activityItems[activePhase.activityIndex] : undefined
|
||||
|
||||
useEffect(() => {
|
||||
const viewport = scrollViewportRef.current
|
||||
if (!viewport) return
|
||||
|
||||
if (activePhase.kind === "reset" || activePhase.kind === "issue" || visibleCount <= 1) {
|
||||
viewport.scrollTo({ top: 0, behavior: "auto" })
|
||||
return
|
||||
}
|
||||
|
||||
viewport.scrollTo({
|
||||
top: viewport.scrollHeight,
|
||||
behavior: reduceMotion ? "auto" : "smooth",
|
||||
})
|
||||
}, [activePhase.kind, reduceMotion, visibleCount])
|
||||
|
||||
const issueVisible = activePhase.kind !== "reset"
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("w-full max-w-[540px] h-[520px] sm:h-[560px]", className)}
|
||||
role="img"
|
||||
aria-label="Animated Linear issue showing Roo Code responding to a comment">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="relative flex h-full flex-col overflow-hidden rounded-2xl border border-white/10 bg-[#1F2023] shadow-2xl shadow-black/40">
|
||||
{/* Linear-style Header with breadcrumb */}
|
||||
<div className="flex items-center gap-2 border-b border-white/10 px-4 py-2.5 text-[13px]">
|
||||
<LinearIcon className="h-4 w-4 text-[#8B8D91]" />
|
||||
<span className="text-[#8B8D91]">Frontend</span>
|
||||
<ChevronRight className="h-3 w-3 text-[#5C5F66]" />
|
||||
<span className="text-[#F8F8F9]">FE-312</span>
|
||||
<div className="ml-auto flex items-center gap-2 text-[11px] text-[#8B8D91]">
|
||||
<span className="h-2 w-2 rounded-full bg-[#27AE60]" />
|
||||
Live demo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Issue Content */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col flex-1 overflow-hidden transition-opacity duration-300 will-change-opacity",
|
||||
issueVisible ? "opacity-100" : "opacity-0",
|
||||
)}>
|
||||
{/* Issue Title */}
|
||||
<div className="px-4 pt-4 pb-3">
|
||||
<h3 className="text-lg font-semibold text-[#F8F8F9] leading-tight">
|
||||
Add dark mode toggle to settings
|
||||
</h3>
|
||||
<p className="mt-2 text-[13px] text-[#8B8D91] leading-relaxed">
|
||||
Users should be able to switch between light and dark themes from the settings page. Persist
|
||||
preference to localStorage and apply immediately.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Activity Section */}
|
||||
<div className="flex-1 overflow-hidden flex flex-col border-t border-white/10">
|
||||
<div className="px-4 py-2.5 flex items-center justify-between">
|
||||
<span className="text-[13px] font-medium text-[#F8F8F9]">Activity</span>
|
||||
<span className="text-[12px] text-[#5C5F66]">Unsubscribe</span>
|
||||
</div>
|
||||
<div
|
||||
ref={scrollViewportRef}
|
||||
className="flex-1 overflow-y-auto px-4 pb-3 [scrollbar-width:thin] [scrollbar-color:rgba(255,255,255,0.1)_transparent]">
|
||||
<div className="space-y-3">
|
||||
{visibleActivities.map((item) => (
|
||||
<ActivityRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
reduceMotion={reduceMotion}
|
||||
isNew={
|
||||
activePhase.kind === "show" &&
|
||||
activityItems[activePhase.activityIndex]?.id === item.id
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{typingTarget && typingTarget.kind === "comment" && (
|
||||
<div
|
||||
className={cn(
|
||||
reduceMotion ? "" : "animate-in fade-in duration-300",
|
||||
"flex gap-2.5",
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[9px] font-semibold",
|
||||
typingTarget.avatarClassName,
|
||||
)}>
|
||||
{typingTarget.avatarText}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 text-[13px]">
|
||||
<span className="font-medium text-[#F8F8F9]">
|
||||
{typingTarget.author}
|
||||
</span>
|
||||
<span className="text-[#8B8D91]">typing</span>
|
||||
<TypingDots />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Comment Input */}
|
||||
<div className="border-t border-white/10 px-4 py-3">
|
||||
<div className="flex items-center gap-2 rounded-lg border border-white/10 bg-black/20 px-3 py-2">
|
||||
<span className="flex-1 text-[13px] text-[#5C5F66]">Leave a comment...</span>
|
||||
<Paperclip className="h-4 w-4 text-[#5C5F66]" />
|
||||
<Send className="h-4 w-4 text-[#5C5F66]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress indicator */}
|
||||
<div className="flex items-center justify-center border-t border-white/10 px-4 py-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{activityItems.map((item, idx) => (
|
||||
<span
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"h-1 w-3 rounded-full transition-colors duration-300",
|
||||
Math.max(0, visibleCount - 1) === idx ? "bg-indigo-400" : "bg-white/10",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,548 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import type { ReactNode } from "react"
|
||||
import { useEffect, useMemo, useRef, useState } from "react"
|
||||
import { CheckCircle2, Paperclip } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type SlackMessage = {
|
||||
id: string
|
||||
author: string
|
||||
timeLabel: string
|
||||
body: ReactNode
|
||||
avatarText: string
|
||||
avatarClassName: string
|
||||
kind: "human" | "bot"
|
||||
}
|
||||
|
||||
function usePrefersReducedMotion(): boolean {
|
||||
const [reduced, setReduced] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia("(prefers-reduced-motion: reduce)")
|
||||
const onChange = () => setReduced(media.matches)
|
||||
onChange()
|
||||
|
||||
if (typeof media.addEventListener === "function") {
|
||||
media.addEventListener("change", onChange)
|
||||
return () => media.removeEventListener("change", onChange)
|
||||
}
|
||||
|
||||
media.addListener?.(onChange)
|
||||
return () => media.removeListener?.(onChange)
|
||||
}, [])
|
||||
|
||||
return reduced
|
||||
}
|
||||
|
||||
type TypingDotsProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
function TypingDots({ className }: TypingDotsProps): JSX.Element {
|
||||
return (
|
||||
<span className={cn("inline-flex items-center gap-1", className)} aria-hidden="true">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:0ms]" />
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:180ms]" />
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-[#8B8D91] animate-pulse [animation-delay:360ms]" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
type FakeLinkProps = {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
function FakeLink({ children, className }: FakeLinkProps): JSX.Element {
|
||||
return (
|
||||
<span className={cn("text-violet-300 underline underline-offset-2", "cursor-default", className)}>
|
||||
{children}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
type SlackMessageRowProps = {
|
||||
message: SlackMessage
|
||||
isNew: boolean
|
||||
reduceMotion: boolean
|
||||
}
|
||||
|
||||
function SlackMessageRow({ message, isNew, reduceMotion }: SlackMessageRowProps): JSX.Element {
|
||||
let animation = ""
|
||||
if (!reduceMotion && isNew) {
|
||||
animation = "animate-in fade-in slide-in-from-bottom-2 duration-500"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex gap-3", animation)}>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-xs font-semibold",
|
||||
message.avatarClassName,
|
||||
)}>
|
||||
{message.avatarText}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||
<span className="text-[13px] font-semibold text-[#F8F8F9]">{message.author}</span>
|
||||
<span className="text-[11px] text-[#8B8D91]">{message.timeLabel}</span>
|
||||
{message.kind === "bot" && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-violet-500/20 px-2 py-0.5 text-[10px] font-medium text-violet-200">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
App
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 text-[13px] leading-relaxed text-[#D1D2D3]">{message.body}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export type SlackThreadDemoProps = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function SlackThreadDemo({ className }: SlackThreadDemoProps): JSX.Element {
|
||||
const reduceMotion = usePrefersReducedMotion()
|
||||
const [stepIndex, setStepIndex] = useState(0)
|
||||
const scrollViewportRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const messages: SlackMessage[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: "m1",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 2:56 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<span>We need to add a page to our Marketing site that highlights using Roo Code from Slack.</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m2",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 2:58 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
The documentation for using Roo Code from Slack is here:{" "}
|
||||
<FakeLink className="hover:text-violet-200">
|
||||
https://docs.roocode.com/roo-code-cloud/slack-integration
|
||||
</FakeLink>
|
||||
</div>
|
||||
<div className="text-[#B8BBC0]">Here are some pages from our site we can use for guidance:</div>
|
||||
<ol className="list-decimal pl-5 text-[#D1D2D3]">
|
||||
<li>
|
||||
<FakeLink className="hover:text-violet-200">https://roocode.com</FakeLink>
|
||||
</li>
|
||||
<li>
|
||||
<FakeLink className="hover:text-violet-200">https://roocode.com/extension</FakeLink>
|
||||
</li>
|
||||
<li>
|
||||
<FakeLink className="hover:text-violet-200">https://roocode.com/cloud</FakeLink>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m3",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 3:08 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<div className="space-y-3">
|
||||
<div>This is the start of a wireframe I have in mind for this page</div>
|
||||
<div className="w-full max-w-[420px] rounded-lg border border-white/10 bg-black/20 p-3">
|
||||
<div className="flex items-center gap-2 text-[12px] text-[#B8BBC0]">
|
||||
<Paperclip className="h-4 w-4" />
|
||||
IMG_9721.heic
|
||||
</div>
|
||||
<div className="mt-3 h-24 w-full rounded-md bg-gradient-to-br from-white/10 via-white/5 to-white/0" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m4",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 3:09 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<span>
|
||||
<FakeLink className="no-underline hover:text-violet-200">@Roomote</FakeLink> let's create
|
||||
the plan to deliver this
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m5",
|
||||
author: "Roomote",
|
||||
timeLabel: "Monday at 3:09 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2 text-[#D1D2D3]">
|
||||
Calling <span className="font-semibold text-[#F8F8F9]">Planneroo</span> to get started on
|
||||
your task on{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
RooCodeInc/Roo-Code
|
||||
</code>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center rounded-md border border-white/10 bg-transparent px-2 py-1 text-[12px] font-medium text-[#D1D2D3] hover:bg-white/5">
|
||||
Cancel ✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m6",
|
||||
author: "Roomote",
|
||||
timeLabel: "Monday at 3:10 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-x-2">
|
||||
<span>Cool, I'll knock this out real quick.</span>
|
||||
<FakeLink className="hover:text-violet-200">Follow along</FakeLink>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m7",
|
||||
author: "Roomote",
|
||||
timeLabel: "Monday at 3:12 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-y-2">
|
||||
<div className="font-semibold text-[#F8F8F9]">Todo List:</div>
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
|
||||
<ul className="space-y-1">
|
||||
{[
|
||||
"Analyze existing page structures and component patterns",
|
||||
"Review marketing content requirements and wireframe details",
|
||||
"Create detailed component architecture plan",
|
||||
"Design page structure and section breakdown",
|
||||
"Plan navigation updates and integration points",
|
||||
"Test the page and verify all sections work",
|
||||
].map((item) => (
|
||||
<li key={item} className="text-[#D1D2D3]">
|
||||
<span className="mr-2">•</span>
|
||||
<span className="line-through opacity-80">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="text-[12px] text-[#8B8D91]">(edited)</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m8",
|
||||
author: "Roomote",
|
||||
timeLabel: "Monday at 3:16 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-y-3">
|
||||
<p>
|
||||
I've created a comprehensive implementation plan for the Roo Code Slack integration
|
||||
marketing page at{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
plans/slack-marketing-page-plan.md
|
||||
</code>
|
||||
.
|
||||
</p>
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
|
||||
<div className="text-[12px] font-semibold text-[#F8F8F9]">Plan Overview</div>
|
||||
<ul className="mt-2 space-y-1 text-[#D1D2D3]">
|
||||
<li>
|
||||
<span className="mr-2">•</span>Hero + dual CTAs
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>Value props grid
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>“Thread to Shipped Feature” workflow
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>Onboarding steps + CTA
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-x-2">
|
||||
<span className="text-[#B8BBC0]">Full document:</span>
|
||||
<FakeLink className="hover:text-violet-200">View artifact</FakeLink>
|
||||
</div>
|
||||
<div className="text-[12px] italic text-[#8B8D91]">
|
||||
Want to follow up? Just @-mention me in your response.
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m9",
|
||||
author: "Avery Lee",
|
||||
timeLabel: "Monday at 3:17 PM",
|
||||
avatarText: "AL",
|
||||
avatarClassName: "bg-[#2B2D31] text-[#F8F8F9] ring-1 ring-white/10",
|
||||
kind: "human",
|
||||
body: (
|
||||
<div className="space-x-2">
|
||||
<FakeLink className="no-underline hover:text-violet-200">@Roomote</FakeLink>
|
||||
<span>this looks great, let's use Coderoo to build this</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "m10",
|
||||
author: "Roomote",
|
||||
timeLabel: "Monday at 3:23 PM",
|
||||
avatarText: "R",
|
||||
avatarClassName: "bg-violet-500/20 text-violet-200 ring-1 ring-violet-500/30",
|
||||
kind: "bot",
|
||||
body: (
|
||||
<div className="space-y-3">
|
||||
<p>
|
||||
I've built the Roo Code Slack integration marketing page. Here's what was
|
||||
implemented:
|
||||
</p>
|
||||
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
|
||||
<div className="text-[12px] font-semibold text-[#F8F8F9]">Files</div>
|
||||
<ul className="mt-2 space-y-1 text-[#D1D2D3]">
|
||||
<li>
|
||||
<span className="mr-2">•</span>
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
apps/web-roo-code/src/app/slack/page.tsx
|
||||
</code>{" "}
|
||||
— Slack marketing page
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
apps/web-roo-code/src/lib/constants.ts
|
||||
</code>{" "}
|
||||
— added{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
SLACK_DOCS
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
<span className="mr-2">•</span>
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">
|
||||
apps/web-roo-code/src/components/chromes/nav-bar.tsx
|
||||
</code>{" "}
|
||||
— added Slack to Product dropdown
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-white/10 bg-black/20 px-3 py-2">
|
||||
<div className="text-[12px] font-semibold text-[#F8F8F9]">Pull Request</div>
|
||||
<div className="mt-2">
|
||||
<span className="font-semibold text-[#F8F8F9]">PR #10853</span>:{" "}
|
||||
<FakeLink className="hover:text-violet-200">
|
||||
https://github.com/RooCodeInc/Roo-Code/pull/10853
|
||||
</FakeLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[#B8BBC0]">
|
||||
The page is accessible at{" "}
|
||||
<code className="rounded bg-white/10 px-1.5 py-0.5 text-[12px] text-[#F8F8F9]">/slack</code>{" "}
|
||||
and includes navigation links in desktop and mobile.
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
type DemoPhase =
|
||||
| { kind: "show"; messageIndex: number }
|
||||
| { kind: "typing"; messageIndex: number }
|
||||
| { kind: "reset" }
|
||||
|
||||
const phases: DemoPhase[] = useMemo(() => {
|
||||
const next: DemoPhase[] = []
|
||||
if (messages.length === 0) return [{ kind: "reset" }]
|
||||
|
||||
next.push({ kind: "typing", messageIndex: 0 })
|
||||
next.push({ kind: "show", messageIndex: 0 })
|
||||
for (let messageIndex = 1; messageIndex < messages.length; messageIndex += 1) {
|
||||
next.push({ kind: "typing", messageIndex })
|
||||
next.push({ kind: "show", messageIndex })
|
||||
}
|
||||
next.push({ kind: "reset" })
|
||||
return next
|
||||
}, [messages])
|
||||
|
||||
const lastShowPhaseIndex = useMemo(() => {
|
||||
let lastIndex = -1
|
||||
for (let idx = 0; idx < phases.length; idx += 1) {
|
||||
if (phases[idx]?.kind === "show") lastIndex = idx
|
||||
}
|
||||
return lastIndex
|
||||
}, [phases])
|
||||
|
||||
useEffect(() => {
|
||||
if (reduceMotion) {
|
||||
setStepIndex(lastShowPhaseIndex >= 0 ? lastShowPhaseIndex : 0)
|
||||
return
|
||||
}
|
||||
|
||||
const active = phases[stepIndex] ?? phases.at(0)
|
||||
const isLastMessageShow = active?.kind === "show" && stepIndex === lastShowPhaseIndex
|
||||
const durationMs = (() => {
|
||||
const base = 2200
|
||||
if (active?.kind === "reset") return 500
|
||||
if (active?.kind === "typing") return 900
|
||||
return isLastMessageShow ? base * 2 : base
|
||||
})()
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
setStepIndex((prev) => (prev + 1) % phases.length)
|
||||
}, durationMs)
|
||||
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [lastShowPhaseIndex, phases, reduceMotion, stepIndex])
|
||||
|
||||
const activePhase = phases[stepIndex] ?? phases.at(0) ?? { kind: "reset" }
|
||||
|
||||
function getVisibleCount(phase: DemoPhase): number {
|
||||
if (phase.kind === "reset") return 0
|
||||
if (phase.kind === "typing") return phase.messageIndex
|
||||
return phase.messageIndex + 1
|
||||
}
|
||||
|
||||
const visibleCount = getVisibleCount(activePhase)
|
||||
const visibleMessages = messages.slice(0, visibleCount)
|
||||
const typingTarget = activePhase.kind === "typing" ? messages[activePhase.messageIndex] : undefined
|
||||
|
||||
useEffect(() => {
|
||||
const viewport = scrollViewportRef.current
|
||||
if (!viewport) return
|
||||
|
||||
if (activePhase.kind === "reset" || visibleCount <= 1) {
|
||||
viewport.scrollTo({ top: 0, behavior: "auto" })
|
||||
return
|
||||
}
|
||||
|
||||
viewport.scrollTo({
|
||||
top: viewport.scrollHeight,
|
||||
behavior: reduceMotion ? "auto" : "smooth",
|
||||
})
|
||||
}, [activePhase.kind, reduceMotion, visibleCount])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("w-full max-w-[620px] h-[520px] sm:h-[560px]", className)}
|
||||
role="img"
|
||||
aria-label="Animated Slack thread showing Roo Code responding as @Roomote">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="relative flex h-full flex-col overflow-hidden rounded-2xl border border-white/10 bg-[#1A1D21] shadow-2xl shadow-black/30">
|
||||
<div className="flex items-center justify-between border-b border-white/10 px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-[#F24A4A]" />
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-[#F2C94C]" />
|
||||
<div className="h-2.5 w-2.5 rounded-full bg-[#27AE60]" />
|
||||
<div className="ml-3 text-sm font-semibold text-[#F8F8F9]">Thread</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[11px] text-[#8B8D91]">
|
||||
<span className="h-2 w-2 rounded-full bg-[#27AE60]" />
|
||||
Live demo
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={scrollViewportRef}
|
||||
className="flex-1 overflow-y-auto px-4 py-5 [scrollbar-width:thin] [scrollbar-color:rgba(255,255,255,0.18)_transparent]">
|
||||
<div
|
||||
className={cn(
|
||||
"space-y-5 transition-opacity duration-300 will-change-opacity",
|
||||
activePhase.kind === "reset" ? "opacity-0" : "opacity-100",
|
||||
)}>
|
||||
{visibleMessages.map((message) => (
|
||||
<SlackMessageRow
|
||||
key={message.id}
|
||||
message={message}
|
||||
reduceMotion={reduceMotion}
|
||||
isNew={
|
||||
activePhase.kind === "show" && messages[activePhase.messageIndex]?.id === message.id
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
{typingTarget && (
|
||||
<div className={cn(reduceMotion ? "" : "animate-in fade-in duration-300", "flex gap-3")}>
|
||||
<div
|
||||
className={cn(
|
||||
"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-xs font-semibold",
|
||||
typingTarget.avatarClassName,
|
||||
)}>
|
||||
{typingTarget.avatarText}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-baseline gap-x-2">
|
||||
<span className="text-[13px] font-semibold text-[#F8F8F9]">
|
||||
{typingTarget.author}
|
||||
</span>
|
||||
{typingTarget.kind === "bot" && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-violet-500/20 px-2 py-0.5 text-[10px] font-medium text-violet-200">
|
||||
<CheckCircle2 className="h-3 w-3" />
|
||||
App
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[11px] text-[#8B8D91]">typing…</span>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<TypingDots />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-white/10 px-4 py-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{messages.map((message, idx) => (
|
||||
<span
|
||||
key={message.id}
|
||||
className={cn(
|
||||
"h-1.5 w-5 rounded-full transition-colors duration-300",
|
||||
Math.max(0, visibleCount - 1) === idx ? "bg-violet-300" : "bg-white/10",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
const formatter = new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
})
|
||||
|
||||
export const formatCurrency = (amount: number) => formatter.format(amount)
|
||||
|
||||
export const formatTokens = (tokens: number) => {
|
||||
if (tokens < 1000) {
|
||||
return tokens.toString()
|
||||
}
|
||||
|
||||
if (tokens < 1000000) {
|
||||
return `${(tokens / 1000).toFixed(1)}K`
|
||||
}
|
||||
|
||||
if (tokens < 1000000000) {
|
||||
return `${(tokens / 1000000).toFixed(1)}M`
|
||||
}
|
||||
|
||||
return `${(tokens / 1000000000).toFixed(1)}B`
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
export interface ModelPricing {
|
||||
input: string
|
||||
output: string
|
||||
input_cache_read: string
|
||||
input_cache_write: string
|
||||
}
|
||||
|
||||
export interface Model {
|
||||
id: string
|
||||
object: string
|
||||
created: number
|
||||
owned_by: string
|
||||
name: string
|
||||
description: string
|
||||
context_window: number
|
||||
max_tokens: number
|
||||
type: string
|
||||
tags: string[]
|
||||
pricing: ModelPricing
|
||||
deprecated?: boolean
|
||||
}
|
||||
|
||||
export interface ModelsResponse {
|
||||
data: Model[]
|
||||
}
|
||||
|
||||
export interface ModelWithTotalPrice extends Model {
|
||||
totalPrice: number
|
||||
}
|
||||
|
||||
export type SortOption = "alphabetical" | "price-asc" | "price-desc" | "context-window-asc" | "context-window-desc"
|
||||
Loading…
Add table
Reference in a new issue