mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: eval-evolution work in progress - recommendations and workers UI
This commit is contained in:
parent
3a6eaf8e07
commit
310b10cdc2
13 changed files with 1207 additions and 256 deletions
|
|
@ -198,19 +198,19 @@ export function MethodologyContent() {
|
|||
</Link>
|
||||
<span className="text-border">/</span>
|
||||
<Link href="/evals/workers" className="transition-colors hover:text-foreground">
|
||||
Hire an AI Engineer
|
||||
Build with Roo Code Cloud
|
||||
</Link>
|
||||
<span className="text-border">/</span>
|
||||
<span className="font-medium text-foreground">How We Interview</span>
|
||||
<span className="font-medium text-foreground">Methodology</span>
|
||||
</motion.nav>
|
||||
|
||||
{/* Heading */}
|
||||
<motion.h1
|
||||
className="text-4xl font-bold tracking-tight md:text-5xl lg:text-6xl"
|
||||
variants={fadeUpVariants}>
|
||||
How We Interview{" "}
|
||||
How We Run{" "}
|
||||
<span className="bg-gradient-to-r from-blue-500 via-indigo-500 to-violet-500 bg-clip-text text-transparent">
|
||||
AI Models
|
||||
Evals
|
||||
</span>
|
||||
</motion.h1>
|
||||
|
||||
|
|
@ -262,7 +262,7 @@ export function MethodologyContent() {
|
|||
<motion.h2
|
||||
className="-mt-3 text-3xl font-bold tracking-tight md:text-4xl"
|
||||
variants={fadeUpVariants}>
|
||||
The Interview Process
|
||||
The Eval Process
|
||||
</motion.h2>
|
||||
|
||||
<motion.div
|
||||
|
|
@ -293,7 +293,7 @@ export function MethodologyContent() {
|
|||
},
|
||||
{
|
||||
title: "No Cherry-Picking",
|
||||
desc: "Every model gets the exact same interview. No curated demos, no special treatment.",
|
||||
desc: "Every model gets the exact same eval run. No curated demos, no special treatment.",
|
||||
},
|
||||
{
|
||||
title: "Real Metrics",
|
||||
|
|
@ -313,7 +313,7 @@ export function MethodologyContent() {
|
|||
</motion.section>
|
||||
|
||||
{/* ════════════════════════════════════════════════════════════════
|
||||
SECTION 02: THE INTERVIEW SUITE
|
||||
SECTION 02: THE EVAL SUITE
|
||||
════════════════════════════════════════════════════════════════ */}
|
||||
<motion.section
|
||||
className="relative overflow-hidden border-t border-border/30 py-20"
|
||||
|
|
@ -336,7 +336,7 @@ export function MethodologyContent() {
|
|||
<motion.h2
|
||||
className="-mt-3 text-3xl font-bold tracking-tight md:text-4xl"
|
||||
variants={fadeUpVariants}>
|
||||
The Interview Suite
|
||||
The Eval Suite
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
|
|
@ -751,7 +751,7 @@ export function MethodologyContent() {
|
|||
</motion.section>
|
||||
|
||||
{/* ════════════════════════════════════════════════════════════════
|
||||
SECTION 05: RUN YOUR OWN INTERVIEWS
|
||||
SECTION 05: RUN YOUR OWN EVALS
|
||||
════════════════════════════════════════════════════════════════ */}
|
||||
<motion.section
|
||||
className="relative overflow-hidden border-t border-border/30 py-20"
|
||||
|
|
@ -767,13 +767,13 @@ export function MethodologyContent() {
|
|||
<motion.h2
|
||||
className="-mt-3 text-3xl font-bold tracking-tight md:text-4xl"
|
||||
variants={fadeUpVariants}>
|
||||
Run Your Own Interviews
|
||||
Run Your Own Evals
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
className="mt-6 max-w-2xl text-base leading-relaxed text-muted-foreground md:text-lg"
|
||||
variants={fadeUpVariants}>
|
||||
Our evaluation framework is fully open source. Run the exact same interviews on your own
|
||||
Our evaluation framework is fully open source. Run the exact same evals on your own
|
||||
infrastructure, with your own API keys, against any model.
|
||||
</motion.p>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { MethodologyContent } from "./methodology-content"
|
|||
|
||||
// ── SEO Metadata ────────────────────────────────────────────────────────────
|
||||
|
||||
const TITLE = "How We Interview AI Models | Roo Code Evals"
|
||||
const TITLE = "Methodology | Roo Code Evals"
|
||||
const DESCRIPTION = "Our methodology for evaluating AI coding models. Transparent, reproducible, evidence-based."
|
||||
const OG_DESCRIPTION = "Our methodology for evaluating AI coding models"
|
||||
const PATH = "/evals/methodology"
|
||||
|
|
@ -46,7 +46,7 @@ export const metadata: Metadata = {
|
|||
"model benchmarking",
|
||||
"coding evals",
|
||||
"methodology",
|
||||
"interview process",
|
||||
"evaluation process",
|
||||
"transparent evaluation",
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import { notFound } from "next/navigation"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { getEngineerRole, getRoleRecommendation } from "@/lib/mock-recommendations"
|
||||
|
||||
import { ComparisonChart } from "../../../workers/[roleId]/compare/comparison-chart"
|
||||
|
||||
type PageProps = { params: Promise<{ roleId: string }> }
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { roleId } = await params
|
||||
const role = getEngineerRole(roleId)
|
||||
|
||||
if (!role) {
|
||||
return {
|
||||
title: "Role Not Found | Roo Code Evals",
|
||||
description: "The requested role was not found.",
|
||||
}
|
||||
}
|
||||
|
||||
const title = `Compare Models — ${role.name} (V2 Preview) | Roo Code Evals`
|
||||
const description = `Outcome-first comparison of AI models for ${role.name}. Compare composite score, success rate, cost efficiency, and speed.`
|
||||
const ogDescription = `Compare Models — ${role.name} (V2 Preview)`
|
||||
const path = `/evals/workers-v2/${roleId}/compare`
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${path}`,
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: `${SEO.url}${path}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(title, ogDescription),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: title,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title,
|
||||
description,
|
||||
images: [ogImageUrl(title, ogDescription)],
|
||||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"AI coding",
|
||||
"model comparison",
|
||||
"coding evals",
|
||||
role.name.toLowerCase(),
|
||||
"outcome-first",
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export default async function WorkersV2ComparePage({ params }: PageProps) {
|
||||
const { roleId } = await params
|
||||
const recommendation = getRoleRecommendation(roleId)
|
||||
|
||||
if (!recommendation) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<ComparisonChart
|
||||
recommendation={recommendation}
|
||||
role={recommendation.role}
|
||||
roleId={roleId}
|
||||
workersRootPath="/evals/workers-v2"
|
||||
/>
|
||||
)
|
||||
}
|
||||
100
apps/web-roo-code/src/app/evals/workers-v2/[roleId]/page.tsx
Normal file
100
apps/web-roo-code/src/app/evals/workers-v2/[roleId]/page.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { notFound } from "next/navigation"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { getRoleRecommendation, getCloudSetupUrl } from "@/lib/mock-recommendations"
|
||||
|
||||
import { CandidatesContent } from "../../workers/[roleId]/candidates-content"
|
||||
|
||||
type PageProps = { params: Promise<{ roleId: string }> }
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { roleId } = await params
|
||||
const recommendation = getRoleRecommendation(roleId)
|
||||
|
||||
if (!recommendation) {
|
||||
return {
|
||||
title: "Role Not Found | Roo Code Evals",
|
||||
description: "The requested role was not found.",
|
||||
}
|
||||
}
|
||||
|
||||
const { role } = recommendation
|
||||
const title = `${role.name} — Recommended Models (V2 Preview) | Roo Code Evals`
|
||||
const description = `Outcome-first recommendations for ${role.name}. Compare models by success rate, cost, and speed across 5 languages.`
|
||||
const ogDescription = `${role.name} — Recommended Models (V2 Preview)`
|
||||
const path = `/evals/workers-v2/${roleId}`
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${path}`,
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: `${SEO.url}${path}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(title, ogDescription),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: title,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title,
|
||||
description,
|
||||
images: [ogImageUrl(title, ogDescription)],
|
||||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"AI coding",
|
||||
"coding agents",
|
||||
"model recommendations",
|
||||
"coding evals",
|
||||
role.name.toLowerCase(),
|
||||
"outcome-first",
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export default async function WorkersV2RolePage({ params }: PageProps) {
|
||||
const { roleId } = await params
|
||||
const recommendation = getRoleRecommendation(roleId)
|
||||
|
||||
if (!recommendation) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const { role, best, budgetHire, speedHire, allCandidates, totalEvalRuns, totalExercises, lastUpdated } =
|
||||
recommendation
|
||||
|
||||
const cloudUrls: Record<string, string> = {}
|
||||
for (const candidate of allCandidates) {
|
||||
cloudUrls[candidate.modelId] = getCloudSetupUrl(candidate)
|
||||
}
|
||||
|
||||
return (
|
||||
<CandidatesContent
|
||||
roleId={roleId}
|
||||
role={role}
|
||||
best={best}
|
||||
budgetHire={budgetHire}
|
||||
speedHire={speedHire}
|
||||
allCandidates={allCandidates}
|
||||
totalEvalRuns={totalEvalRuns}
|
||||
totalExercises={totalExercises}
|
||||
lastUpdated={lastUpdated}
|
||||
cloudUrls={cloudUrls}
|
||||
workersRootPath="/evals/workers-v2"
|
||||
/>
|
||||
)
|
||||
}
|
||||
91
apps/web-roo-code/src/app/evals/workers-v2/page.tsx
Normal file
91
apps/web-roo-code/src/app/evals/workers-v2/page.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import type { Metadata } from "next"
|
||||
import { Fraunces, IBM_Plex_Sans } from "next/font/google"
|
||||
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { getEngineerRoles, getAllRecommendations } from "@/lib/mock-recommendations"
|
||||
|
||||
import { WorkersContent } from "../workers/workers-content"
|
||||
|
||||
const TITLE = "Build with Roo Code Cloud (V2 Preview) | Roo Code Evals"
|
||||
const DESCRIPTION =
|
||||
"Outcome-first, eval-backed recommendations for shipping production code. Start from what you need to ship and pick a setup."
|
||||
const OG_DESCRIPTION = "Outcome-first recommendations for shipping production code"
|
||||
const PATH = "/evals/workers-v2"
|
||||
|
||||
const display = Fraunces({ subsets: ["latin"], variable: "--font-display" })
|
||||
const body = IBM_Plex_Sans({ subsets: ["latin"], weight: ["400", "500", "600"], variable: "--font-body" })
|
||||
|
||||
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,
|
||||
"AI coding",
|
||||
"coding agents",
|
||||
"roo code cloud",
|
||||
"model recommendations",
|
||||
"coding evals",
|
||||
"shipping code",
|
||||
"prototype",
|
||||
"outcome-first",
|
||||
],
|
||||
}
|
||||
|
||||
export default function WorkersV2Page() {
|
||||
const roles = getEngineerRoles()
|
||||
const recommendations = getAllRecommendations()
|
||||
|
||||
const totalEvalRuns = recommendations.reduce((sum, recommendation) => sum + recommendation.totalEvalRuns, 0)
|
||||
const totalExercises = recommendations.reduce((sum, recommendation) => sum + recommendation.totalExercises, 0)
|
||||
const uniqueModels = new Set(
|
||||
recommendations.flatMap((recommendation) => recommendation.allCandidates.map((candidate) => candidate.modelId)),
|
||||
)
|
||||
const totalModels = uniqueModels.size
|
||||
const lastUpdated = recommendations
|
||||
.map((recommendation) => recommendation.lastUpdated)
|
||||
.sort()
|
||||
.pop()
|
||||
|
||||
return (
|
||||
<div className={`${body.variable} ${display.variable} [font-family:var(--font-body)]`}>
|
||||
<WorkersContent
|
||||
roles={roles}
|
||||
recommendations={recommendations}
|
||||
totalEvalRuns={totalEvalRuns}
|
||||
totalExercises={totalExercises}
|
||||
totalModels={totalModels}
|
||||
lastUpdated={lastUpdated}
|
||||
workersRootPath="/evals/workers-v2"
|
||||
enableOutcomeLayer
|
||||
alternateVersionHref="/evals/workers"
|
||||
alternateVersionLabel="View baseline"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from "lucide-react"
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
|
||||
import type { ModelCandidate, LanguageScores, EngineerRole } from "@/lib/mock-recommendations"
|
||||
|
||||
|
|
@ -485,7 +486,7 @@ function CandidateCard({
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`inline-flex w-full items-center justify-center gap-2 rounded-xl ${theme.buttonBg} ${theme.buttonHover} px-4 py-3 text-sm font-semibold text-white transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] shadow-lg`}>
|
||||
☁️ Hire This Engineer
|
||||
☁️ Open in Roo Code Cloud
|
||||
<ExternalLink className="size-3.5" />
|
||||
</a>
|
||||
<CopySettingsButton settings={copySettings} />
|
||||
|
|
@ -620,7 +621,7 @@ function CompactCard({
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`inline-flex w-full items-center justify-center gap-2 rounded-xl ${theme.buttonBg} ${theme.buttonHover} px-4 py-3 text-sm font-semibold text-white transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] shadow-lg`}>
|
||||
☁️ Hire This Engineer
|
||||
☁️ Open in Roo Code Cloud
|
||||
<ExternalLink className="size-3.5" />
|
||||
</a>
|
||||
<CopySettingsButton settings={copySettings} />
|
||||
|
|
@ -643,6 +644,7 @@ export type CandidatesContentProps = {
|
|||
totalExercises: number
|
||||
lastUpdated: string
|
||||
cloudUrls: Record<string, string>
|
||||
workersRootPath?: string
|
||||
}
|
||||
|
||||
// ── Main Content Component ──────────────────────────────────────────────────
|
||||
|
|
@ -658,9 +660,22 @@ export function CandidatesContent({
|
|||
totalExercises,
|
||||
lastUpdated,
|
||||
cloudUrls,
|
||||
workersRootPath = "/evals/workers",
|
||||
}: CandidatesContentProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const theme = ROLE_THEMES[roleId] ?? DEFAULT_THEME
|
||||
const IconComponent = ICON_MAP[role.icon] ?? Code
|
||||
const alternateWorkersRootPath = workersRootPath === "/evals/workers-v2" ? "/evals/workers" : "/evals/workers-v2"
|
||||
const alternateVersionLabel = workersRootPath === "/evals/workers-v2" ? "View baseline" : "View V2 preview"
|
||||
const setupQuery = (() => {
|
||||
const outcome = searchParams.get("outcome")
|
||||
if (!outcome) return ""
|
||||
const params = new URLSearchParams()
|
||||
params.set("outcome", outcome)
|
||||
const mode = searchParams.get("mode")
|
||||
if (mode) params.set("mode", mode)
|
||||
return `?${params.toString()}`
|
||||
})()
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -693,8 +708,10 @@ export function CandidatesContent({
|
|||
Evals
|
||||
</Link>
|
||||
<span className="text-border">/</span>
|
||||
<Link href="/evals/workers" className="transition-colors hover:text-foreground">
|
||||
Hire an AI Engineer
|
||||
<Link
|
||||
href={`${workersRootPath}${setupQuery}`}
|
||||
className="transition-colors hover:text-foreground">
|
||||
Build with Roo Code Cloud
|
||||
</Link>
|
||||
<span className="text-border">/</span>
|
||||
<span className="font-medium text-foreground">{role.name}</span>
|
||||
|
|
@ -753,9 +770,15 @@ export function CandidatesContent({
|
|||
href="/evals/methodology"
|
||||
className="group inline-flex items-center gap-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground">
|
||||
<Beaker className="size-3.5" />
|
||||
How we interview
|
||||
Methodology
|
||||
<ArrowRight className="size-3 transition-transform duration-200 group-hover:translate-x-0.5" />
|
||||
</Link>
|
||||
<div className="hidden h-4 w-px bg-border sm:block" />
|
||||
<Link
|
||||
href={`${alternateWorkersRootPath}/${roleId}${setupQuery}`}
|
||||
className="inline-flex items-center gap-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground">
|
||||
{alternateVersionLabel}
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{/* Strengths + Trade-offs grid */}
|
||||
|
|
@ -792,7 +815,7 @@ export function CandidatesContent({
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Top Candidates: Best Overall ────────────────────────────── */}
|
||||
{/* ── Top Models: Best Overall ────────────────────────────────── */}
|
||||
<section className="relative overflow-hidden pb-16">
|
||||
<div className="container relative z-10 mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
|
|
@ -804,7 +827,7 @@ export function CandidatesContent({
|
|||
className="mb-8 flex items-center gap-3 text-3xl font-bold tracking-tight"
|
||||
variants={fadeUpVariants}>
|
||||
<Trophy className="size-7 text-yellow-500" />
|
||||
Top Candidates
|
||||
Top Models
|
||||
</motion.h2>
|
||||
|
||||
<motion.div className="grid grid-cols-1 gap-6 md:grid-cols-3" variants={containerVariants}>
|
||||
|
|
@ -835,7 +858,7 @@ export function CandidatesContent({
|
|||
{budgetHire && (
|
||||
<CompactCard
|
||||
candidate={budgetHire}
|
||||
label="💰 Budget Hire"
|
||||
label="💰 Best value"
|
||||
icon={DollarSign}
|
||||
highlight="cost"
|
||||
theme={theme}
|
||||
|
|
@ -845,7 +868,7 @@ export function CandidatesContent({
|
|||
{speedHire && (
|
||||
<CompactCard
|
||||
candidate={speedHire}
|
||||
label="⚡ Speed Hire"
|
||||
label="⚡ Fastest"
|
||||
icon={Zap}
|
||||
highlight="speed"
|
||||
theme={theme}
|
||||
|
|
@ -857,7 +880,7 @@ export function CandidatesContent({
|
|||
</section>
|
||||
)}
|
||||
|
||||
{/* ── All Candidates Table ────────────────────────────────────── */}
|
||||
{/* ── All Models Table ────────────────────────────────────────── */}
|
||||
<section className="relative overflow-hidden pb-16">
|
||||
{/* Subtle background */}
|
||||
<motion.div
|
||||
|
|
@ -878,7 +901,7 @@ export function CandidatesContent({
|
|||
viewport={{ once: true }}
|
||||
variants={containerVariants}>
|
||||
<motion.h2 className="mb-8 text-3xl font-bold tracking-tight" variants={fadeUpVariants}>
|
||||
All Candidates
|
||||
All Models
|
||||
</motion.h2>
|
||||
|
||||
<motion.div
|
||||
|
|
@ -962,7 +985,7 @@ export function CandidatesContent({
|
|||
{/* Compare link */}
|
||||
<motion.div className="mt-6 text-center" variants={fadeUpVariants}>
|
||||
<Link
|
||||
href={`/evals/workers/${roleId}/compare`}
|
||||
href={`${workersRootPath}/${roleId}/compare${setupQuery}`}
|
||||
className={`group inline-flex items-center gap-2 rounded-full border ${theme.methodologyBorder} bg-card/50 px-5 py-2.5 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-300 hover:text-foreground`}>
|
||||
📊 Compare all candidates
|
||||
<ArrowRight className="size-4 transition-transform duration-200 group-hover:translate-x-0.5" />
|
||||
|
|
@ -983,7 +1006,7 @@ export function CandidatesContent({
|
|||
variants={containerVariants}>
|
||||
<motion.div variants={fadeUpVariants}>
|
||||
<Link
|
||||
href="/evals/workers"
|
||||
href={`${workersRootPath}${setupQuery}`}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-200 hover:border-border hover:text-foreground">
|
||||
<ArrowLeft className="size-4" />
|
||||
Back to all roles
|
||||
|
|
@ -991,7 +1014,7 @@ export function CandidatesContent({
|
|||
</motion.div>
|
||||
<motion.div className="flex flex-wrap gap-3" variants={fadeUpVariants}>
|
||||
<Link
|
||||
href={`/evals/workers/${roleId}/compare`}
|
||||
href={`${workersRootPath}/${roleId}/compare${setupQuery}`}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-200 hover:border-border hover:text-foreground">
|
||||
📊 Compare candidates
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useState, useMemo, useCallback } from "react"
|
||||
import Link from "next/link"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { motion } from "framer-motion"
|
||||
import {
|
||||
ArrowLeft,
|
||||
|
|
@ -409,12 +410,12 @@ function ScatterTooltip({
|
|||
</div>
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
<span className="size-2.5 rounded-full bg-amber-400 ring-1 ring-white/10" />
|
||||
<span className="text-muted-foreground">Daily Salary:</span>
|
||||
<span className="text-muted-foreground">Daily Spend:</span>
|
||||
<span className="font-semibold tabular-nums">${data.dailyCost}/day</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
<span className="size-2.5 rounded-full bg-blue-400 ring-1 ring-white/10" />
|
||||
<span className="text-muted-foreground">Interview Score:</span>
|
||||
<span className="text-muted-foreground">Eval Score:</span>
|
||||
<span className="font-semibold tabular-nums">{data.score}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
|
|
@ -433,11 +434,29 @@ interface ComparisonChartProps {
|
|||
recommendation: RoleRecommendation
|
||||
role: EngineerRole
|
||||
roleId: string
|
||||
workersRootPath?: string
|
||||
}
|
||||
|
||||
export function ComparisonChart({ recommendation, role, roleId }: ComparisonChartProps) {
|
||||
export function ComparisonChart({
|
||||
recommendation,
|
||||
role,
|
||||
roleId,
|
||||
workersRootPath = "/evals/workers",
|
||||
}: ComparisonChartProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const { allCandidates } = recommendation
|
||||
const theme = ROLE_THEMES[roleId] ?? DEFAULT_THEME
|
||||
const alternateWorkersRootPath = workersRootPath === "/evals/workers-v2" ? "/evals/workers" : "/evals/workers-v2"
|
||||
const alternateVersionLabel = workersRootPath === "/evals/workers-v2" ? "View baseline" : "View V2 preview"
|
||||
const setupQuery = (() => {
|
||||
const outcome = searchParams.get("outcome")
|
||||
if (!outcome) return ""
|
||||
const params = new URLSearchParams()
|
||||
params.set("outcome", outcome)
|
||||
const mode = searchParams.get("mode")
|
||||
if (mode) params.set("mode", mode)
|
||||
return `?${params.toString()}`
|
||||
})()
|
||||
|
||||
// ── State ───────────────────────────────────────────────────────────────
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<keyof LanguageScores | "all">("all")
|
||||
|
|
@ -561,15 +580,25 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
|
|||
Evals
|
||||
</Link>
|
||||
<span className="text-border">/</span>
|
||||
<Link href="/evals/workers" className="transition-colors hover:text-foreground">
|
||||
Hire an AI Engineer
|
||||
<Link
|
||||
href={`${workersRootPath}${setupQuery}`}
|
||||
className="transition-colors hover:text-foreground">
|
||||
Build with Roo Code Cloud
|
||||
</Link>
|
||||
<span className="text-border">/</span>
|
||||
<Link href={`/evals/workers/${roleId}`} className="transition-colors hover:text-foreground">
|
||||
<Link
|
||||
href={`${workersRootPath}/${roleId}${setupQuery}`}
|
||||
className="transition-colors hover:text-foreground">
|
||||
{role.name}
|
||||
</Link>
|
||||
<span className="text-border">/</span>
|
||||
<span className="font-medium text-foreground">Compare Candidates</span>
|
||||
<span className="font-medium text-foreground">Compare Models</span>
|
||||
<span className="text-border">/</span>
|
||||
<Link
|
||||
href={`${alternateWorkersRootPath}/${roleId}/compare${setupQuery}`}
|
||||
className="font-medium text-muted-foreground transition-colors hover:text-foreground">
|
||||
{alternateVersionLabel}
|
||||
</Link>
|
||||
</motion.nav>
|
||||
|
||||
{/* Title row */}
|
||||
|
|
@ -579,7 +608,7 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
|
|||
<BarChart3 className="size-7" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-4xl font-bold tracking-tight md:text-5xl">Compare Candidates</h1>
|
||||
<h1 className="text-4xl font-bold tracking-tight md:text-5xl">Compare Models</h1>
|
||||
<p className={`mt-1 font-semibold ${theme.accentLight} ${theme.accentDark}`}>
|
||||
{role.name}
|
||||
</p>
|
||||
|
|
@ -599,7 +628,7 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
|
|||
<span className="font-mono font-semibold text-foreground">
|
||||
{filteredCandidates.length}
|
||||
</span>
|
||||
of {allCandidates.length} candidates shown
|
||||
of {allCandidates.length} models shown
|
||||
</span>
|
||||
<div className="hidden h-4 w-px bg-border sm:block" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
|
|
@ -731,10 +760,10 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
|
|||
className="rounded-2xl border border-border/50 bg-card/50 p-6 backdrop-blur-sm"
|
||||
variants={fadeUpVariants}>
|
||||
<div className="mb-1 flex items-center gap-2.5">
|
||||
<h2 className="text-lg font-bold tracking-tight">Value Map: Salary vs Interview Score</h2>
|
||||
<h2 className="text-lg font-bold tracking-tight">Value Map: Spend vs Eval Score</h2>
|
||||
</div>
|
||||
<p className="mb-4 text-xs leading-relaxed text-muted-foreground/80">
|
||||
Upper-left = best value. Each dot is a candidate model. Size reflects success rate.
|
||||
Upper-left = higher score at lower spend. Each dot is a model. Size reflects success rate.
|
||||
</p>
|
||||
|
||||
{/* Tier legend */}
|
||||
|
|
@ -753,7 +782,7 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
|
|||
{scatterData.length === 0 ? (
|
||||
<div className="flex h-48 flex-col items-center justify-center gap-2 rounded-xl border border-dashed border-border/50 text-muted-foreground">
|
||||
<SlidersHorizontal className="size-6 text-muted-foreground/50" />
|
||||
<p className="text-sm">No candidates match the current filters.</p>
|
||||
<p className="text-sm">No models match the current filters.</p>
|
||||
<p className="text-xs text-muted-foreground/60">
|
||||
Try adjusting the provider or success rate filters.
|
||||
</p>
|
||||
|
|
@ -765,7 +794,7 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
|
|||
<XAxis
|
||||
type="number"
|
||||
dataKey="dailyCost"
|
||||
name="Daily Salary"
|
||||
name="Daily Spend"
|
||||
domain={[0, Math.ceil(scatterMaxCost * 1.1)]}
|
||||
tickFormatter={(v: number) => `$${v}`}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
|
|
@ -773,7 +802,7 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
|
|||
tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }}
|
||||
axisLine={false}
|
||||
label={{
|
||||
value: "Daily Salary ($)",
|
||||
value: "Daily Spend ($)",
|
||||
position: "insideBottom",
|
||||
offset: -10,
|
||||
style: { fontSize: 11, fill: "hsl(var(--muted-foreground))" },
|
||||
|
|
@ -782,14 +811,14 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
|
|||
<YAxis
|
||||
type="number"
|
||||
dataKey="score"
|
||||
name="Interview Score"
|
||||
name="Eval Score"
|
||||
domain={[Math.max(0, scatterMinScore - 10), 100]}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
strokeOpacity={0.3}
|
||||
tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }}
|
||||
axisLine={false}
|
||||
label={{
|
||||
value: "Interview Score",
|
||||
value: "Eval Score",
|
||||
angle: -90,
|
||||
position: "insideLeft",
|
||||
offset: 10,
|
||||
|
|
@ -826,7 +855,7 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
|
|||
strokeOpacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
<Scatter data={scatterData} name="Candidates">
|
||||
<Scatter data={scatterData} name="Models">
|
||||
{scatterData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`scatter-cell-${index}`}
|
||||
|
|
@ -864,7 +893,7 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
|
|||
{chartData.length === 0 ? (
|
||||
<div className="flex h-48 flex-col items-center justify-center gap-2 rounded-xl border border-dashed border-border/50 text-muted-foreground">
|
||||
<SlidersHorizontal className="size-6 text-muted-foreground/50" />
|
||||
<p className="text-sm">No candidates match the current filters.</p>
|
||||
<p className="text-sm">No models match the current filters.</p>
|
||||
<p className="text-xs text-muted-foreground/60">
|
||||
Try adjusting the provider or success rate filters.
|
||||
</p>
|
||||
|
|
@ -993,15 +1022,15 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
|
|||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<Link
|
||||
href={`/evals/workers/${roleId}`}
|
||||
href={`${workersRootPath}/${roleId}${setupQuery}`}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-200 hover:border-border hover:text-foreground">
|
||||
<ArrowLeft className="size-4" />
|
||||
Back to {role.name} candidates
|
||||
Back to {role.name} models
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/evals/workers"
|
||||
href={`${workersRootPath}${setupQuery}`}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-200 hover:border-border hover:text-foreground">
|
||||
<ArrowLeft className="size-3.5" />
|
||||
All roles
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||
}
|
||||
}
|
||||
|
||||
const title = `Compare Candidates — ${role.name} | Roo Code Evals`
|
||||
const description = `Interactive comparison of AI model candidates for the ${role.name} role. Compare composite score, success rate, cost efficiency, and speed.`
|
||||
const ogDescription = `Compare Candidates — ${role.name}`
|
||||
const title = `Compare Models — ${role.name} | Roo Code Evals`
|
||||
const description = `Interactive comparison of AI models for the ${role.name} setup. Compare composite score, success rate, cost efficiency, and speed.`
|
||||
const ogDescription = `Compare Models — ${role.name}`
|
||||
const path = `/evals/workers/${roleId}/compare`
|
||||
|
||||
return {
|
||||
|
|
@ -57,19 +57,19 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"AI engineer",
|
||||
"AI coding",
|
||||
"model comparison",
|
||||
"coding evals",
|
||||
role.name.toLowerCase(),
|
||||
"bar chart",
|
||||
"candidate comparison",
|
||||
"model comparison",
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
// ── Page Component ──────────────────────────────────────────────────────────
|
||||
|
||||
export default async function CompareCandidatesPage({ params }: PageProps) {
|
||||
export default async function CompareModelsPage({ params }: PageProps) {
|
||||
const { roleId } = await params
|
||||
const recommendation = getRoleRecommendation(roleId)
|
||||
|
||||
|
|
@ -77,5 +77,12 @@ export default async function CompareCandidatesPage({ params }: PageProps) {
|
|||
notFound()
|
||||
}
|
||||
|
||||
return <ComparisonChart recommendation={recommendation} role={recommendation.role} roleId={roleId} />
|
||||
return (
|
||||
<ComparisonChart
|
||||
recommendation={recommendation}
|
||||
role={recommendation.role}
|
||||
roleId={roleId}
|
||||
workersRootPath="/evals/workers"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||
}
|
||||
|
||||
const { role } = recommendation
|
||||
const title = `${role.name} — AI Engineer Candidates | Roo Code Evals`
|
||||
const description = `Interview results for ${role.name} AI candidates. Compare models by success rate, cost, and speed across 5 languages.`
|
||||
const ogDescription = `${role.name} — AI Engineer Candidates`
|
||||
const title = `${role.name} — Recommended Models | Roo Code Evals`
|
||||
const description = `Eval-backed recommendations for ${role.name}. Compare models by success rate, cost, and speed across 5 languages.`
|
||||
const ogDescription = `${role.name} — Recommended Models`
|
||||
const path = `/evals/workers/${roleId}`
|
||||
|
||||
return {
|
||||
|
|
@ -58,7 +58,8 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"AI engineer",
|
||||
"AI coding",
|
||||
"coding agents",
|
||||
"model recommendations",
|
||||
"coding evals",
|
||||
role.name.toLowerCase(),
|
||||
|
|
@ -99,6 +100,7 @@ export default async function RoleCandidatesPage({ params }: PageProps) {
|
|||
totalExercises={totalExercises}
|
||||
lastUpdated={lastUpdated}
|
||||
cloudUrls={cloudUrls}
|
||||
workersRootPath="/evals/workers"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ import { WorkersContent } from "./workers-content"
|
|||
|
||||
// ── SEO Metadata ────────────────────────────────────────────────────────────
|
||||
|
||||
const TITLE = "Hire an AI Engineer | Roo Code Evals"
|
||||
const TITLE = "Build with Roo Code Cloud | Roo Code Evals"
|
||||
const DESCRIPTION =
|
||||
"Find the right AI coding model for your team. Compare interview results across Junior, Senior, and Staff Engineer roles."
|
||||
const OG_DESCRIPTION = "Find the right AI coding model for your team"
|
||||
"Eval-backed model recommendations for shipping production code. Pick a setup based on the work you're doing: single-file fixes, multi-file changes, review, and autonomous runs."
|
||||
const OG_DESCRIPTION = "Eval-backed model recommendations for shipping production code"
|
||||
const PATH = "/evals/workers"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
|
@ -44,18 +44,20 @@ export const metadata: Metadata = {
|
|||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"AI engineer",
|
||||
"AI coding",
|
||||
"coding agents",
|
||||
"roo code cloud",
|
||||
"model recommendations",
|
||||
"coding evals",
|
||||
"model comparison",
|
||||
"hire AI",
|
||||
"talent marketplace",
|
||||
"shipping code",
|
||||
"prototype",
|
||||
],
|
||||
}
|
||||
|
||||
// ── Page Component ──────────────────────────────────────────────────────────
|
||||
|
||||
export default function HireAnAIEngineerPage() {
|
||||
export default function WorkersPage() {
|
||||
const roles = getEngineerRoles()
|
||||
const recommendations = getAllRecommendations()
|
||||
|
||||
|
|
@ -80,6 +82,10 @@ export default function HireAnAIEngineerPage() {
|
|||
totalExercises={totalExercises}
|
||||
totalModels={totalModels}
|
||||
lastUpdated={lastUpdated}
|
||||
workersRootPath="/evals/workers"
|
||||
enableOutcomeLayer={false}
|
||||
alternateVersionHref="/evals/workers-v2"
|
||||
alternateVersionLabel="View V2 preview"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useMemo } from "react"
|
||||
import { useCallback, useMemo } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import {
|
||||
Code,
|
||||
|
|
@ -20,10 +20,12 @@ import {
|
|||
} from "lucide-react"
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { ScatterChart, Scatter, XAxis, YAxis, ZAxis, Tooltip, ResponsiveContainer, Cell, ReferenceLine } from "recharts"
|
||||
|
||||
import type { EngineerRole, RoleRecommendation } from "@/lib/mock-recommendations"
|
||||
import { TASKS_PER_DAY, MODEL_TIMELINE } from "@/lib/mock-recommendations"
|
||||
import { EVAL_OUTCOMES, isEvalOutcomeId, type EvalOutcomeId } from "@/lib/eval-outcomes"
|
||||
|
||||
// ── Icon Mapping ────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -139,6 +141,37 @@ const ROLE_THEMES: Record<string, RoleTheme> = {
|
|||
|
||||
const DEFAULT_THEME = ROLE_THEMES.senior!
|
||||
|
||||
// ── Outcome Layer: Optimization Modes ──────────────────────────────────────
|
||||
|
||||
type EvalOptimizationMode = "best" | "fastest" | "cost"
|
||||
|
||||
const OPTIMIZATION_MODES: Array<{
|
||||
id: EvalOptimizationMode
|
||||
label: string
|
||||
description: string
|
||||
}> = [
|
||||
{ id: "best", label: "Best", description: "Best overall quality across our eval suite." },
|
||||
{ id: "fastest", label: "Fastest", description: "Lower latency per task when speed matters." },
|
||||
{ id: "cost", label: "Most cost-effective", description: "Lower cost per task for high-volume work." },
|
||||
]
|
||||
|
||||
function isEvalOptimizationMode(value: string): value is EvalOptimizationMode {
|
||||
return value === "best" || value === "fastest" || value === "cost"
|
||||
}
|
||||
|
||||
function getModeCandidate(rec: RoleRecommendation | undefined, mode: EvalOptimizationMode) {
|
||||
if (!rec) return null
|
||||
if (mode === "fastest") return rec.speedHire ?? rec.best[0] ?? null
|
||||
if (mode === "cost") return rec.budgetHire ?? rec.best[0] ?? null
|
||||
return rec.best[0] ?? null
|
||||
}
|
||||
|
||||
function getModeLabel(mode: EvalOptimizationMode) {
|
||||
if (mode === "fastest") return "Fastest"
|
||||
if (mode === "cost") return "Most cost-effective"
|
||||
return "Best"
|
||||
}
|
||||
|
||||
// ── Framer Motion Variants ──────────────────────────────────────────────────
|
||||
|
||||
const containerVariants = {
|
||||
|
|
@ -284,6 +317,10 @@ type WorkersContentProps = {
|
|||
totalExercises: number
|
||||
totalModels: number
|
||||
lastUpdated: string | undefined
|
||||
workersRootPath?: string
|
||||
enableOutcomeLayer?: boolean
|
||||
alternateVersionHref?: string
|
||||
alternateVersionLabel?: string
|
||||
}
|
||||
|
||||
export function WorkersContent({
|
||||
|
|
@ -293,8 +330,91 @@ export function WorkersContent({
|
|||
totalExercises,
|
||||
totalModels,
|
||||
lastUpdated,
|
||||
workersRootPath = "/evals/workers",
|
||||
enableOutcomeLayer = false,
|
||||
alternateVersionHref,
|
||||
alternateVersionLabel,
|
||||
}: WorkersContentProps) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const selectedOutcomeId = useMemo(() => {
|
||||
const outcome = searchParams.get("outcome")
|
||||
if (!outcome) return null
|
||||
return isEvalOutcomeId(outcome) ? outcome : null
|
||||
}, [searchParams])
|
||||
|
||||
const selectedMode = useMemo((): EvalOptimizationMode => {
|
||||
const mode = searchParams.get("mode")
|
||||
if (!mode) return "best"
|
||||
return isEvalOptimizationMode(mode) ? mode : "best"
|
||||
}, [searchParams])
|
||||
|
||||
const setOutcome = useCallback(
|
||||
(nextOutcomeId: EvalOutcomeId | null) => {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
if (nextOutcomeId) params.set("outcome", nextOutcomeId)
|
||||
else params.delete("outcome")
|
||||
|
||||
const query = params.toString()
|
||||
router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false })
|
||||
},
|
||||
[pathname, router, searchParams],
|
||||
)
|
||||
|
||||
const setMode = useCallback(
|
||||
(nextMode: EvalOptimizationMode) => {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
params.set("mode", nextMode)
|
||||
|
||||
const query = params.toString()
|
||||
router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false })
|
||||
},
|
||||
[pathname, router, searchParams],
|
||||
)
|
||||
|
||||
const recByRole = new Map(recommendations.map((r) => [r.roleId, r]))
|
||||
const roleById = useMemo(() => new Map(roles.map((r) => [r.id, r])), [roles])
|
||||
|
||||
const selectedOutcome = useMemo(() => {
|
||||
if (!selectedOutcomeId) return null
|
||||
return EVAL_OUTCOMES.find((o) => o.id === selectedOutcomeId) ?? null
|
||||
}, [selectedOutcomeId])
|
||||
|
||||
const setupQuery = useMemo(() => {
|
||||
if (!enableOutcomeLayer || !selectedOutcomeId) return ""
|
||||
const params = new URLSearchParams()
|
||||
params.set("outcome", selectedOutcomeId)
|
||||
params.set("mode", selectedMode)
|
||||
const query = params.toString()
|
||||
return query ? `?${query}` : ""
|
||||
}, [enableOutcomeLayer, selectedOutcomeId, selectedMode])
|
||||
|
||||
const profileTitle = selectedOutcome?.builderProfile?.title ?? "Your Builder Profile"
|
||||
const profileDescription =
|
||||
selectedOutcome?.builderProfile?.description ??
|
||||
"A default setup built from our eval signals. It’s a baseline, not a guarantee."
|
||||
const profileHowItWorks = selectedOutcome?.builderProfile?.howItWorks ?? selectedOutcome?.whyItWorks ?? []
|
||||
|
||||
const profileCapabilities = useMemo(() => {
|
||||
if (!selectedOutcome) return []
|
||||
const fromProfile = selectedOutcome.builderProfile?.capabilities
|
||||
if (fromProfile && fromProfile.length > 0) return fromProfile
|
||||
return selectedOutcome.recommendedRoleIds.map((roleId) => {
|
||||
const role = roleById.get(roleId)
|
||||
return {
|
||||
id: roleId,
|
||||
name: role?.name ?? roleId,
|
||||
description: role?.salaryRange ?? "",
|
||||
roleId,
|
||||
}
|
||||
})
|
||||
}, [selectedOutcome, roleById])
|
||||
|
||||
const agentCapabilities = useMemo(() => profileCapabilities.filter((c) => Boolean(c.roleId)), [profileCapabilities])
|
||||
|
||||
const builtInCapabilities = useMemo(() => profileCapabilities.filter((c) => !c.roleId), [profileCapabilities])
|
||||
|
||||
// ── Timeline scatter data ──────────────────────────────────────────────
|
||||
const timelineData = useMemo(() => {
|
||||
|
|
@ -340,6 +460,21 @@ export function WorkersContent({
|
|||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Blueprint grid overlay (V2) */}
|
||||
{enableOutcomeLayer ? (
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 opacity-[0.14] mix-blend-multiply dark:opacity-[0.10] dark:mix-blend-screen"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(to right, hsl(var(--border)) 1px, transparent 1px), linear-gradient(to bottom, hsl(var(--border)) 1px, transparent 1px)",
|
||||
backgroundSize: "72px 72px",
|
||||
maskImage: "radial-gradient(circle at 50% 35%, black 10%, transparent 65%)",
|
||||
WebkitMaskImage: "radial-gradient(circle at 50% 35%, black 10%, transparent 65%)",
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* Gradient fade from hero atmosphere to cards */}
|
||||
<div className="absolute inset-x-0 bottom-0 z-[1] h-48 bg-gradient-to-b from-transparent via-background/60 to-background" />
|
||||
|
||||
|
|
@ -351,33 +486,91 @@ export function WorkersContent({
|
|||
variants={containerVariants}>
|
||||
{/* Badge */}
|
||||
<motion.div variants={fadeUpVariants}>
|
||||
<Link
|
||||
href="/evals/methodology"
|
||||
className="group mb-6 inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-300 hover:border-border hover:text-foreground">
|
||||
<Beaker className="size-4" />
|
||||
How we interview AI models
|
||||
<ArrowRight className="size-3.5 transition-transform duration-200 group-hover:translate-x-0.5" />
|
||||
</Link>
|
||||
<div className="mb-6 flex flex-wrap items-center justify-center gap-3">
|
||||
<Link
|
||||
href="/evals/methodology"
|
||||
className="group inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-300 hover:border-border hover:text-foreground">
|
||||
<Beaker className="size-4" />
|
||||
How we run evals
|
||||
<ArrowRight className="size-3.5 transition-transform duration-200 group-hover:translate-x-0.5" />
|
||||
</Link>
|
||||
{alternateVersionHref && alternateVersionLabel ? (
|
||||
<Link
|
||||
href={alternateVersionHref}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-background/70 px-4 py-2 text-sm font-medium text-foreground/80 backdrop-blur-sm transition-colors hover:border-border hover:text-foreground">
|
||||
{alternateVersionLabel}
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{enableOutcomeLayer ? (
|
||||
<motion.p
|
||||
className="mt-2 text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground/70"
|
||||
variants={fadeUpVariants}>
|
||||
Outcomes over artifacts
|
||||
</motion.p>
|
||||
) : null}
|
||||
|
||||
{/* Heading */}
|
||||
<motion.h1
|
||||
className="mt-6 text-5xl font-bold tracking-tight md:text-6xl lg:text-7xl"
|
||||
className="mt-6 text-5xl font-semibold tracking-tight md:text-6xl lg:text-7xl [font-family:var(--font-display)]"
|
||||
variants={fadeUpVariants}>
|
||||
Hire an{" "}
|
||||
<span className="bg-gradient-to-r from-emerald-500 via-blue-500 to-amber-500 bg-clip-text text-transparent">
|
||||
AI Engineer
|
||||
</span>
|
||||
{enableOutcomeLayer ? (
|
||||
<>
|
||||
Build from outcomes.
|
||||
<br className="hidden sm:block" /> Ship{" "}
|
||||
<span className="bg-gradient-to-r from-emerald-500 via-blue-500 to-amber-500 bg-clip-text text-transparent">
|
||||
real code
|
||||
</span>
|
||||
.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Build with{" "}
|
||||
<span className="bg-gradient-to-r from-emerald-500 via-blue-500 to-amber-500 bg-clip-text text-transparent">
|
||||
Roo Code Cloud
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</motion.h1>
|
||||
|
||||
{/* Subheading */}
|
||||
<motion.p
|
||||
className="mt-6 text-lg leading-relaxed text-muted-foreground md:text-xl"
|
||||
variants={fadeUpVariants}>
|
||||
Every model runs the same coding tasks, same tools, same time limit. Pick the right
|
||||
candidate for your team and budget.
|
||||
{enableOutcomeLayer ? (
|
||||
<>
|
||||
Pick what you're trying to ship. We assemble a Builder Profile: the
|
||||
capabilities you need, plus a default model recommendation backed by eval data.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Outcomes over artifacts: start from the production codebase and ship as a reviewable
|
||||
PR. Every model runs the same tasks, same tools, and the same time limit. Your repo
|
||||
will differ—treat this as a baseline.
|
||||
</>
|
||||
)}
|
||||
</motion.p>
|
||||
|
||||
{enableOutcomeLayer ? (
|
||||
<motion.div
|
||||
className="mt-8 flex flex-wrap items-center justify-center gap-3"
|
||||
variants={fadeUpVariants}>
|
||||
<Link
|
||||
href={`${workersRootPath}?outcome=prototype_to_pr&mode=${selectedMode}`}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-foreground/20 bg-foreground/5 px-5 py-2.5 text-sm font-semibold text-foreground transition-colors hover:bg-foreground/10">
|
||||
Start with Prototype → PR
|
||||
<ArrowRight className="size-4" />
|
||||
</Link>
|
||||
<a
|
||||
href="#outcomes"
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-card/40 px-5 py-2.5 text-sm font-semibold text-muted-foreground transition-colors hover:border-border hover:text-foreground">
|
||||
Browse outcomes
|
||||
</a>
|
||||
</motion.div>
|
||||
) : null}
|
||||
|
||||
{/* Stats bar */}
|
||||
<motion.div
|
||||
className="mt-10 flex flex-wrap items-center justify-center gap-x-8 gap-y-3 rounded-2xl border border-border/50 bg-card/30 px-6 py-4 backdrop-blur-sm"
|
||||
|
|
@ -394,171 +587,417 @@ export function WorkersContent({
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── Role Cards Grid ────────────────────────────────────────── */}
|
||||
<section className="relative -mt-12 overflow-hidden pb-24">
|
||||
{/* Subtle section background */}
|
||||
<motion.div
|
||||
className="absolute inset-0"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={backgroundVariants}>
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2">
|
||||
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-[100%] bg-foreground/[0.02] dark:bg-foreground/[0.03] blur-[100px]" />
|
||||
</div>
|
||||
</motion.div>
|
||||
{/* ── Outcomes Overlay ───────────────────────────────────────── */}
|
||||
{enableOutcomeLayer ? (
|
||||
<section id="outcomes" className="relative -mt-24 overflow-hidden pb-14 scroll-mt-24">
|
||||
<div className="container relative z-10 mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
|
||||
<motion.div
|
||||
className="mx-auto max-w-4xl"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={containerVariants}>
|
||||
<motion.div className="text-center" variants={fadeUpVariants}>
|
||||
<h2 className="text-2xl font-semibold tracking-tight md:text-3xl [font-family:var(--font-display)]">
|
||||
Start with an outcome
|
||||
</h2>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground md:text-base">
|
||||
Pick what you're trying to ship. We assemble a Builder Profile: capabilities
|
||||
plus a default model recommendation. It's a baseline, not a guarantee.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="container relative z-10 mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
|
||||
{/* Section connector */}
|
||||
<motion.div
|
||||
className="mb-10 flex flex-col items-center gap-2"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={fadeUpVariants}>
|
||||
<p className="text-sm font-medium uppercase tracking-widest text-muted-foreground/70">
|
||||
Choose your agentic team members
|
||||
</p>
|
||||
<ChevronDown className="size-4 text-muted-foreground/40" />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="mt-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"
|
||||
variants={containerVariants}>
|
||||
{EVAL_OUTCOMES.map((outcome) => {
|
||||
const Icon = outcome.icon
|
||||
const isSelected = outcome.id === selectedOutcomeId
|
||||
const isFeatured = outcome.id === "prototype_to_pr"
|
||||
|
||||
<motion.div
|
||||
className="grid grid-cols-1 gap-6 md:grid-cols-3 md:grid-rows-[repeat(7,auto)] md:gap-x-6 md:gap-y-10 lg:gap-x-8"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}>
|
||||
{roles.map((role) => {
|
||||
const rec = recByRole.get(role.id)
|
||||
const IconComponent = ICON_MAP[role.icon] ?? Code
|
||||
const candidateCount = rec?.allCandidates.length ?? 0
|
||||
const exerciseCount = rec?.totalExercises ?? 0
|
||||
const theme = ROLE_THEMES[role.id] ?? DEFAULT_THEME
|
||||
const topModel = rec?.best[0]
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={role.id}
|
||||
variants={cardVariants}
|
||||
className="md:row-span-7 md:grid md:grid-rows-subgrid">
|
||||
<div
|
||||
className={`group relative flex h-full flex-col rounded-2xl border border-border/50 bg-card/50 backdrop-blur-sm transition-all duration-300 md:row-span-7 md:grid md:grid-rows-subgrid ${theme.borderHover} ${theme.shadowHover} hover:shadow-xl`}>
|
||||
{/* Subtle glow on hover */}
|
||||
<div
|
||||
className={`absolute inset-0 rounded-2xl ${theme.glowColor} opacity-0 transition-opacity duration-300 group-hover:opacity-100`}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex h-full flex-col p-6 lg:p-7 md:row-span-7 md:grid md:grid-rows-subgrid">
|
||||
{/* Header: Icon + role badge */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div
|
||||
className={`flex size-12 items-center justify-center rounded-xl ${theme.iconBg} ${theme.iconText}`}>
|
||||
<IconComponent className="size-6" />
|
||||
return (
|
||||
<motion.button
|
||||
key={outcome.id}
|
||||
type="button"
|
||||
variants={cardVariants}
|
||||
aria-pressed={isSelected}
|
||||
onClick={() => setOutcome(isSelected ? null : outcome.id)}
|
||||
className={[
|
||||
"group rounded-2xl border bg-card/40 p-5 text-left backdrop-blur-sm transition-all duration-200 hover:bg-card/60",
|
||||
isSelected
|
||||
? "border-foreground/20 ring-1 ring-foreground/15"
|
||||
: "border-border/50 hover:border-border",
|
||||
isFeatured ? "lg:col-span-2" : "",
|
||||
].join(" ")}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-xl border border-border/50 bg-background/30">
|
||||
<Icon className="size-5 text-foreground/70" />
|
||||
</div>
|
||||
{topModel && (
|
||||
<span
|
||||
className={`rounded-full ${theme.badgeBg} ${theme.badgeText} px-3 py-1 text-xs font-medium`}>
|
||||
Top: {topModel.displayName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Role name + salary */}
|
||||
<h2 className="mt-5 text-2xl font-bold tracking-tight">{role.name}</h2>
|
||||
<p
|
||||
className={`mt-1 font-mono text-lg font-semibold ${theme.accentLight} ${theme.accentDark}`}>
|
||||
{role.salaryRange}
|
||||
</p>
|
||||
|
||||
{/* Description */}
|
||||
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
|
||||
{role.description}
|
||||
</p>
|
||||
|
||||
{/* Best for */}
|
||||
<div className="mt-5">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Best for
|
||||
</h3>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{role.bestFor.map((item) => (
|
||||
<span
|
||||
key={item}
|
||||
className="rounded-md border border-border/50 bg-muted/50 px-2 py-0.5 text-xs text-foreground/70">
|
||||
{item}
|
||||
<div className="min-w-0 flex-1">
|
||||
{isFeatured ? (
|
||||
<span className="mb-1 inline-flex items-center rounded-full border border-foreground/15 bg-foreground/5 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-foreground/70">
|
||||
Recommended starting point
|
||||
</span>
|
||||
))}
|
||||
) : null}
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{outcome.name}
|
||||
</p>
|
||||
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
||||
{outcome.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.button>
|
||||
)
|
||||
})}
|
||||
</motion.div>
|
||||
|
||||
{/* Strengths & Weaknesses side by side */}
|
||||
<div className="mt-5 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-2">
|
||||
{/* Strengths */}
|
||||
<div>
|
||||
<h3
|
||||
className={`text-xs font-semibold uppercase tracking-wider ${theme.strengthColor}`}>
|
||||
Strengths
|
||||
</h3>
|
||||
<ul className="mt-2 space-y-1.5">
|
||||
{role.strengths.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="flex items-start gap-1.5 text-xs text-foreground/75">
|
||||
<CheckCircle2
|
||||
className={`mt-0.5 size-3 shrink-0 ${theme.strengthColor}`}
|
||||
/>
|
||||
{item}
|
||||
{selectedOutcome ? (
|
||||
<motion.div
|
||||
className="mt-6 rounded-2xl border border-border/50 bg-card/30 p-5 backdrop-blur-sm"
|
||||
variants={fadeUpVariants}>
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div className="min-w-0 md:max-w-[44%]">
|
||||
<p className="text-xs font-medium uppercase tracking-widest text-muted-foreground/70">
|
||||
{profileTitle}
|
||||
</p>
|
||||
<p className="mt-2 text-sm font-semibold text-foreground">
|
||||
{selectedOutcome.name}
|
||||
</p>
|
||||
<p className="mt-2 text-xs leading-relaxed text-muted-foreground">
|
||||
{profileDescription}
|
||||
</p>
|
||||
|
||||
{profileHowItWorks.length > 0 ? (
|
||||
<div className="mt-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{selectedOutcome.builderProfile
|
||||
? "How it works"
|
||||
: "Why it works"}
|
||||
</p>
|
||||
<ul className="mt-2 space-y-1 text-xs text-muted-foreground">
|
||||
{profileHowItWorks.map((line) => (
|
||||
<li key={line} className="flex items-start gap-2">
|
||||
<span className="mt-1 size-1.5 shrink-0 rounded-full bg-muted-foreground/40" />
|
||||
<span className="min-w-0">{line}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Weaknesses */}
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-amber-700 dark:text-amber-400">
|
||||
Trade-offs
|
||||
</h3>
|
||||
<ul className="mt-2 space-y-1.5">
|
||||
{role.weaknesses.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="flex items-start gap-1.5 text-xs text-foreground/60">
|
||||
<AlertTriangle className="mt-0.5 size-3 shrink-0 text-amber-600/70 dark:text-amber-400/70" />
|
||||
{item}
|
||||
{selectedOutcome.builderProfile?.howItWorks ? (
|
||||
<div className="mt-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Why it works
|
||||
</p>
|
||||
<ul className="mt-2 space-y-1 text-xs text-muted-foreground">
|
||||
{selectedOutcome.whyItWorks.map((line) => (
|
||||
<li key={line} className="flex items-start gap-2">
|
||||
<span className="mt-1 size-1.5 shrink-0 rounded-full bg-muted-foreground/40" />
|
||||
<span className="min-w-0">{line}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Bottom stats + CTA */}
|
||||
<div className="mt-auto pt-6">
|
||||
<div className="mb-4 flex items-center gap-4 border-t border-border/30 pt-4 text-xs text-muted-foreground">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Users className="size-3.5" />
|
||||
{candidateCount} candidates
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<FlaskConical className="size-3.5" />
|
||||
{exerciseCount.toLocaleString()} exercises
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
Optimize for
|
||||
</span>
|
||||
{OPTIMIZATION_MODES.map((mode) => {
|
||||
const isSelected = mode.id === selectedMode
|
||||
return (
|
||||
<button
|
||||
key={mode.id}
|
||||
type="button"
|
||||
aria-pressed={isSelected}
|
||||
title={mode.description}
|
||||
onClick={() => setMode(mode.id)}
|
||||
className={[
|
||||
"inline-flex items-center rounded-full border px-3 py-1.5 text-xs font-semibold transition-colors",
|
||||
isSelected
|
||||
? "border-foreground/20 bg-foreground/5 text-foreground"
|
||||
: "border-border/50 bg-background/20 text-foreground/75 hover:border-border hover:text-foreground",
|
||||
].join(" ")}>
|
||||
{mode.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={`/evals/workers/${role.id}`}
|
||||
className={`inline-flex w-full items-center justify-center gap-2 rounded-xl ${theme.buttonBg} ${theme.buttonHover} px-4 py-3 text-sm font-semibold text-white transition-all duration-200 hover:scale-[1.02] active:scale-[0.98]`}>
|
||||
View Candidates
|
||||
<ArrowRight className="size-4" />
|
||||
</Link>
|
||||
<span className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/60">
|
||||
Capability set
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-end justify-between gap-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Agents
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Click for candidates & settings
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{agentCapabilities.map((capability) => {
|
||||
const roleId = capability.roleId!
|
||||
const rec = recByRole.get(roleId)
|
||||
const candidate = getModeCandidate(rec, selectedMode)
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={capability.id}
|
||||
href={`${workersRootPath}/${roleId}${setupQuery}`}
|
||||
className="group rounded-xl border border-border/50 bg-background/20 p-3 text-left transition-colors hover:border-border hover:bg-background/30">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold text-foreground">
|
||||
{capability.name}
|
||||
</p>
|
||||
{capability.description ? (
|
||||
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
||||
{capability.description}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{candidate ? (
|
||||
<>
|
||||
<span className="font-medium text-foreground/80">
|
||||
{getModeLabel(selectedMode)}:
|
||||
</span>{" "}
|
||||
{candidate.displayName}
|
||||
</>
|
||||
) : (
|
||||
<span className="font-medium text-foreground/80">
|
||||
View models
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<ArrowRight className="mt-0.5 size-3.5 shrink-0 text-muted-foreground/70 transition-transform group-hover:translate-x-0.5" />
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{builtInCapabilities.length > 0 ? (
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Built-ins
|
||||
</p>
|
||||
<div className="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{builtInCapabilities.map((capability) => (
|
||||
<div
|
||||
key={capability.id}
|
||||
className="rounded-xl border border-border/50 bg-background/20 p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<CheckCircle2 className="mt-0.5 size-3.5 shrink-0 text-foreground/60" />
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="text-xs font-semibold text-foreground">
|
||||
{capability.name}
|
||||
</p>
|
||||
<span className="rounded-full border border-border/50 bg-background/30 px-2 py-0.5 text-[10px] font-semibold text-foreground/70">
|
||||
Built-in
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
||||
{capability.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
) : null}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{/* ── Role Cards Grid (baseline only) ────────────────────────── */}
|
||||
{!enableOutcomeLayer ? (
|
||||
<section className="relative -mt-12 overflow-hidden pb-24">
|
||||
{/* Subtle section background */}
|
||||
<motion.div
|
||||
className="absolute inset-0"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={backgroundVariants}>
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2">
|
||||
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-[100%] bg-foreground/[0.02] dark:bg-foreground/[0.03] blur-[100px]" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="container relative z-10 mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
|
||||
{/* Section connector */}
|
||||
<motion.div
|
||||
className="mb-10 flex flex-col items-center gap-2"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={fadeUpVariants}>
|
||||
<p className="text-sm font-medium uppercase tracking-widest text-muted-foreground/70">
|
||||
Choose a setup for the work
|
||||
</p>
|
||||
<ChevronDown className="size-4 text-muted-foreground/40" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="grid grid-cols-1 gap-6 md:grid-cols-3 md:grid-rows-[repeat(7,auto)] md:gap-x-6 md:gap-y-10 lg:gap-x-8"
|
||||
variants={containerVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}>
|
||||
{roles.map((role) => {
|
||||
const rec = recByRole.get(role.id)
|
||||
const IconComponent = ICON_MAP[role.icon] ?? Code
|
||||
const candidateCount = rec?.allCandidates.length ?? 0
|
||||
const exerciseCount = rec?.totalExercises ?? 0
|
||||
const theme = ROLE_THEMES[role.id] ?? DEFAULT_THEME
|
||||
const topModel = rec?.best[0] ?? null
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={role.id}
|
||||
variants={cardVariants}
|
||||
className="md:row-span-7 md:grid md:grid-rows-subgrid">
|
||||
<div
|
||||
className={[
|
||||
"group relative flex h-full flex-col rounded-2xl border border-border/50 bg-card/50 backdrop-blur-sm transition-all duration-300 md:row-span-7 md:grid md:grid-rows-subgrid hover:shadow-xl",
|
||||
theme.borderHover,
|
||||
theme.shadowHover,
|
||||
].join(" ")}>
|
||||
{/* Subtle glow on hover */}
|
||||
<div
|
||||
className={`absolute inset-0 rounded-2xl ${theme.glowColor} opacity-0 transition-opacity duration-300 group-hover:opacity-100`}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex h-full flex-col p-6 lg:p-7 md:row-span-7 md:grid md:grid-rows-subgrid">
|
||||
{/* Header: Icon + role badge */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div
|
||||
className={`flex size-12 items-center justify-center rounded-xl ${theme.iconBg} ${theme.iconText}`}>
|
||||
<IconComponent className="size-6" />
|
||||
</div>
|
||||
{topModel && (
|
||||
<span
|
||||
className={`rounded-full ${theme.badgeBg} ${theme.badgeText} px-3 py-1 text-xs font-medium`}>
|
||||
Top: {topModel.displayName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Profile name + descriptor */}
|
||||
<h2 className="mt-5 text-2xl font-bold tracking-tight">{role.name}</h2>
|
||||
<p
|
||||
className={`mt-1 font-mono text-lg font-semibold ${theme.accentLight} ${theme.accentDark}`}>
|
||||
{role.salaryRange}
|
||||
</p>
|
||||
|
||||
{/* Description */}
|
||||
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
|
||||
{role.description}
|
||||
</p>
|
||||
|
||||
{/* Best for */}
|
||||
<div className="mt-5">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Best for
|
||||
</h3>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{role.bestFor.map((item) => (
|
||||
<span
|
||||
key={item}
|
||||
className="rounded-md border border-border/50 bg-muted/50 px-2 py-0.5 text-xs text-foreground/70">
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Strengths & Weaknesses side by side */}
|
||||
<div className="mt-5 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-2">
|
||||
{/* Strengths */}
|
||||
<div>
|
||||
<h3
|
||||
className={`text-xs font-semibold uppercase tracking-wider ${theme.strengthColor}`}>
|
||||
Strengths
|
||||
</h3>
|
||||
<ul className="mt-2 space-y-1.5">
|
||||
{role.strengths.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="flex items-start gap-1.5 text-xs text-foreground/75">
|
||||
<CheckCircle2
|
||||
className={`mt-0.5 size-3 shrink-0 ${theme.strengthColor}`}
|
||||
/>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Weaknesses */}
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-amber-700 dark:text-amber-400">
|
||||
Trade-offs
|
||||
</h3>
|
||||
<ul className="mt-2 space-y-1.5">
|
||||
{role.weaknesses.map((item) => (
|
||||
<li
|
||||
key={item}
|
||||
className="flex items-start gap-1.5 text-xs text-foreground/60">
|
||||
<AlertTriangle className="mt-0.5 size-3 shrink-0 text-amber-600/70 dark:text-amber-400/70" />
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom stats + CTA */}
|
||||
<div className="mt-auto pt-6">
|
||||
<div className="mb-4 flex items-center gap-4 border-t border-border/30 pt-4 text-xs text-muted-foreground">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Users className="size-3.5" />
|
||||
{candidateCount} models
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<FlaskConical className="size-3.5" />
|
||||
{exerciseCount.toLocaleString()} exercises
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={`${workersRootPath}/${role.id}${setupQuery}`}
|
||||
className={`inline-flex w-full items-center justify-center gap-2 rounded-xl ${theme.buttonBg} ${theme.buttonHover} px-4 py-3 text-sm font-semibold text-white transition-all duration-200 hover:scale-[1.02] active:scale-[0.98]`}>
|
||||
View models
|
||||
<ArrowRight className="size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{/* ── AI Coding Capability Over Time ─────────────────────────── */}
|
||||
<section className="relative overflow-hidden pb-24 pt-8">
|
||||
|
|
@ -582,7 +1021,7 @@ export function WorkersContent({
|
|||
variants={containerVariants}>
|
||||
{/* Section header */}
|
||||
<motion.div className="mb-8 text-center" variants={fadeUpVariants}>
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl">
|
||||
<h2 className="text-3xl font-semibold tracking-tight md:text-4xl [font-family:var(--font-display)]">
|
||||
AI Coding Capability{" "}
|
||||
<span className="bg-gradient-to-r from-emerald-500 to-blue-500 bg-clip-text text-transparent">
|
||||
Over Time
|
||||
|
|
|
|||
171
apps/web-roo-code/src/lib/eval-outcomes.ts
Normal file
171
apps/web-roo-code/src/lib/eval-outcomes.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import type { LucideIcon } from "lucide-react"
|
||||
import { Bug, CheckCircle2, GitPullRequest, Sparkles, Workflow } from "lucide-react"
|
||||
|
||||
export type EvalOutcomeId =
|
||||
| "prototype_to_pr"
|
||||
| "paper_cuts"
|
||||
| "sentry_triage"
|
||||
| "repro_to_fix"
|
||||
| "review_guardrails"
|
||||
| "issue_to_pr"
|
||||
|
||||
export type EvalOutcomeCapability = {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
/**
|
||||
* Optional roleId for capabilities that map directly to a role page.
|
||||
* Non-role capabilities represent Roo Code Cloud behaviors (validation, PR packaging, etc.).
|
||||
*/
|
||||
roleId?: string
|
||||
}
|
||||
|
||||
export type EvalOutcomeProfile = {
|
||||
title: string
|
||||
description: string
|
||||
capabilities: EvalOutcomeCapability[]
|
||||
howItWorks: string[]
|
||||
}
|
||||
|
||||
export type EvalOutcome = {
|
||||
id: EvalOutcomeId
|
||||
name: string
|
||||
description: string
|
||||
icon: LucideIcon
|
||||
/**
|
||||
* Ordered list of roleIds to suggest as a "setup".
|
||||
* Keep roleIds stable even if display names evolve.
|
||||
*/
|
||||
recommendedRoleIds: string[]
|
||||
whyItWorks: string[]
|
||||
/**
|
||||
* Optional profile details used to render a more comprehensive “exoskeleton”
|
||||
* for an outcome. Start with the most important outcomes and expand over time.
|
||||
*/
|
||||
builderProfile?: EvalOutcomeProfile
|
||||
}
|
||||
|
||||
export const EVAL_OUTCOMES: EvalOutcome[] = [
|
||||
{
|
||||
id: "prototype_to_pr",
|
||||
name: "Prototype → PR",
|
||||
description: "Build a working prototype on the production codebase, then turn it into a reviewable diff.",
|
||||
icon: Sparkles,
|
||||
recommendedRoleIds: ["senior", "reviewer"],
|
||||
whyItWorks: [
|
||||
"Multi-file changes with a reviewer pass for coherence and edge cases.",
|
||||
"Optimizes for shipping, not slides.",
|
||||
],
|
||||
builderProfile: {
|
||||
title: "Your Builder Profile",
|
||||
description:
|
||||
"A default set of capabilities for turning a working prototype into a reviewable PR—on the production codebase.",
|
||||
capabilities: [
|
||||
{
|
||||
id: "multi_file_builder",
|
||||
name: "Multi-file Builder",
|
||||
description: "Builds the prototype directly in your repo across the files it touches.",
|
||||
roleId: "senior",
|
||||
},
|
||||
{
|
||||
id: "reviewer_guardrails",
|
||||
name: "Reviewer & Guardrails",
|
||||
description: "Reviews the diff for correctness, edge cases, and coherence before you merge.",
|
||||
roleId: "reviewer",
|
||||
},
|
||||
{
|
||||
id: "environment_setup",
|
||||
name: "Environment setup",
|
||||
description:
|
||||
"Bootstraps a working dev environment and runs the workflow without you fighting Git, installs, or tests.",
|
||||
},
|
||||
{
|
||||
id: "validation_loop",
|
||||
name: "Validation loop",
|
||||
description: "Runs tests/lint/typechecks and iterates until it’s clean (or flags what’s blocked).",
|
||||
},
|
||||
{
|
||||
id: "pr_ready_output",
|
||||
name: "PR-ready output",
|
||||
description: "Produces a focused diff plus a plain-English summary and review notes.",
|
||||
},
|
||||
{
|
||||
id: "straight_line_merge",
|
||||
name: "Straight-line to merge",
|
||||
description:
|
||||
"No export/import step: the work is already on the production codebase, so merge is a straight line.",
|
||||
},
|
||||
{
|
||||
id: "scope_control",
|
||||
name: "Scope control",
|
||||
description: "Keeps diffs tight: smaller review surface, fewer surprises, and easier merges.",
|
||||
},
|
||||
],
|
||||
howItWorks: [
|
||||
"Build a working prototype directly in the production codebase.",
|
||||
"Convert the prototype into a tight diff (tests, cleanup, and safeguards).",
|
||||
"Run a reviewer pass to catch edge cases and improve merge confidence.",
|
||||
"Deliver a PR-ready result with context and next steps.",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "paper_cuts",
|
||||
name: "Paper cuts & small fixes",
|
||||
description: "Fix the small stuff without dragging engineers off big projects.",
|
||||
icon: CheckCircle2,
|
||||
recommendedRoleIds: ["junior", "reviewer"],
|
||||
whyItWorks: [
|
||||
"Small diffs are high-leverage when the work is well-scoped.",
|
||||
"Reviewer keeps the quality bar and reduces surprise.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "sentry_triage",
|
||||
name: "Sentry triage",
|
||||
description: "Turn recurring errors into concrete fixes with proof before review.",
|
||||
icon: Bug,
|
||||
recommendedRoleIds: ["autonomous", "reviewer"],
|
||||
whyItWorks: [
|
||||
"Autonomous runs handle multi-step investigation and iteration.",
|
||||
"Reviewer focuses on safety, correctness, and “does this hold up?”.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "repro_to_fix",
|
||||
name: "Bug repro → fix",
|
||||
description: "Make the handoff less lossy: reproduce, patch, and validate in one loop.",
|
||||
icon: Workflow,
|
||||
recommendedRoleIds: ["senior", "reviewer"],
|
||||
whyItWorks: [
|
||||
"Good default for ambiguous bugs that touch a few files.",
|
||||
"Reviewer helps catch cross-team assumptions early.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "review_guardrails",
|
||||
name: "Guardrails & review",
|
||||
description: "Raise the quality bar without becoming the blocker.",
|
||||
icon: GitPullRequest,
|
||||
recommendedRoleIds: ["reviewer"],
|
||||
whyItWorks: [
|
||||
"Works alongside CI, linters, and team review.",
|
||||
"Scales judgement through fast, consistent feedback.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "issue_to_pr",
|
||||
name: "Issue → PR",
|
||||
description: "Run end-to-end work in the background and come back to a reviewable result.",
|
||||
icon: GitPullRequest,
|
||||
recommendedRoleIds: ["autonomous", "reviewer"],
|
||||
whyItWorks: [
|
||||
"Handles out-of-band work while humans stay on the roadmap.",
|
||||
"Pairs autonomy with guardrails for merge safety.",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export function isEvalOutcomeId(value: string): value is EvalOutcomeId {
|
||||
return EVAL_OUTCOMES.some((o) => o.id === value)
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// ---------------------------------------------------------------------------
|
||||
// Eval Recommendations: Types + Mock Data (S1.1a)
|
||||
// ---------------------------------------------------------------------------
|
||||
// This file defines the API contract for the AI Engineer Talent Marketplace.
|
||||
// This file defines the API contract for the /evals/workers recommendation pages.
|
||||
// The backend (Sprint 3-4) will produce data matching these exact types.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
|
@ -16,11 +16,11 @@ export const TASKS_PER_DAY = 80
|
|||
|
||||
// ── Types ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Engineer role definition: maps task complexity to a hiring tier. */
|
||||
/** Engineer role definition: maps task complexity to a recommendation tier. */
|
||||
export type EngineerRole = {
|
||||
id: string
|
||||
name: string
|
||||
/** Daily salary range string, e.g. "$3–38/day" */
|
||||
/** Short descriptor shown under the profile name (scope, mode, etc.). */
|
||||
salaryRange: string
|
||||
description: string
|
||||
bestFor: string[]
|
||||
|
|
@ -80,51 +80,51 @@ export type RoleRecommendation = {
|
|||
const ENGINEER_ROLES: EngineerRole[] = [
|
||||
{
|
||||
id: "junior",
|
||||
name: "Junior Engineer",
|
||||
salaryRange: "$2–10/day",
|
||||
name: "Single-file Builder",
|
||||
salaryRange: "Scope: single-file",
|
||||
description:
|
||||
"Handles well-scoped, single-file tasks: boilerplate, simple bug fixes, and test generation at the lowest cost per task.",
|
||||
bestFor: ["Single-file fixes", "Boilerplate generation", "Test generation", "Simple implementations"],
|
||||
strengths: ["Cheap", "High throughput", "Best cost-to-quality ratio on simple tasks"],
|
||||
weaknesses: ["Struggles with multi-file changes", "Limited reasoning depth", "May miss edge cases"],
|
||||
"Best for tight diffs: boilerplate, small fixes, and test updates. Great when the work is clear and bounded.",
|
||||
bestFor: ["Small fixes", "Boilerplate", "Test updates", "Simple implementations"],
|
||||
strengths: ["Fast iteration", "Stays close to the requested change", "Great for well-scoped diffs"],
|
||||
weaknesses: ["Not ideal for cross-cutting work", "Can miss edge cases in complex systems"],
|
||||
icon: "Code",
|
||||
},
|
||||
{
|
||||
id: "senior",
|
||||
name: "Senior Engineer",
|
||||
salaryRange: "$10–26/day",
|
||||
name: "Multi-file Builder",
|
||||
salaryRange: "Scope: multi-file",
|
||||
description:
|
||||
"The sweet spot for most engineering work. Senior-tier models balance cost and quality across multi-file refactors, feature development, and debugging.",
|
||||
bestFor: ["Multi-file refactors", "Feature development", "Debugging", "Code review"],
|
||||
"For most day-to-day shipping: feature work across a few files, refactors, and debugging with solid consistency.",
|
||||
bestFor: ["Feature work", "Multi-file refactors", "Debugging", "Integrations"],
|
||||
strengths: [
|
||||
"Balanced cost/quality",
|
||||
"Handles multi-file changes and cross-cutting refactors",
|
||||
"Consistent pass rates across all five languages",
|
||||
"Reliable for common product work",
|
||||
"Handles multi-file changes and dependencies",
|
||||
"Consistent across all five languages",
|
||||
],
|
||||
weaknesses: ["More expensive than junior", "Overkill for trivial tasks"],
|
||||
weaknesses: ["Overkill for trivial diffs", "May need help on cross-cutting architecture"],
|
||||
icon: "GitBranch",
|
||||
},
|
||||
{
|
||||
id: "staff",
|
||||
name: "Staff Engineer",
|
||||
salaryRange: "$8–34/day",
|
||||
name: "Architecture & Refactor",
|
||||
salaryRange: "Scope: cross-cutting",
|
||||
description:
|
||||
"For architecture decisions, system design, and complex refactors. Staff-tier models handle ambiguous requirements and cross-cutting changes where other tiers fail.",
|
||||
bestFor: ["Architecture decisions", "Complex features", "System design", "Ambiguous requirements"],
|
||||
"For ambiguity and cross-cutting changes: architecture decisions, complex refactors, and work where correctness matters more than speed.",
|
||||
bestFor: ["Complex refactors", "Architecture changes", "Ambiguous requirements", "System design"],
|
||||
strengths: [
|
||||
"Handles multi-step reasoning and ambiguous specs",
|
||||
"Passes existing test suites consistently",
|
||||
"Resolves underspecified requirements",
|
||||
"Strong multi-step reasoning",
|
||||
"Good at navigating bigger codebases",
|
||||
"Better at making safe, coherent changes",
|
||||
],
|
||||
weaknesses: ["Most expensive", "Overkill for simple tasks", "Diminishing returns on easy work"],
|
||||
weaknesses: ["Overkill for simple diffs", "Still needs human review before merge"],
|
||||
icon: "Building2",
|
||||
},
|
||||
{
|
||||
id: "reviewer",
|
||||
name: "Architecture Reviewer",
|
||||
salaryRange: "$15–40/day",
|
||||
name: "Reviewer & Guardrails",
|
||||
salaryRange: "Mode: review",
|
||||
description:
|
||||
"For code review, PR feedback, security analysis, and design critique. Reviewer-tier models catch issues other models miss and provide actionable, context-aware suggestions.",
|
||||
"For PR feedback, security review, and design critique. Use this to improve quality and reduce surprises before merge.",
|
||||
bestFor: ["Code review", "PR feedback", "Security analysis", "Design critique", "Refactor guidance"],
|
||||
strengths: [
|
||||
"Catches subtle bugs and logic errors",
|
||||
|
|
@ -132,18 +132,18 @@ const ENGINEER_ROLES: EngineerRole[] = [
|
|||
"Understands cross-file impact of changes",
|
||||
],
|
||||
weaknesses: [
|
||||
"Not for writing code from scratch",
|
||||
"More expensive than running linters",
|
||||
"Not for writing features end-to-end",
|
||||
"Not a replacement for CI and linters",
|
||||
"Review quality varies by codebase size",
|
||||
],
|
||||
icon: "Search",
|
||||
},
|
||||
{
|
||||
id: "autonomous",
|
||||
name: "Autonomous Agent",
|
||||
salaryRange: "$5–30/day",
|
||||
name: "Autonomous Delivery",
|
||||
salaryRange: "Mode: end-to-end",
|
||||
description:
|
||||
"For issue-to-PR workflows, long-running tasks, and multi-step debugging with minimal supervision. Autonomous-tier models complete tasks end-to-end and recover from errors without human intervention.",
|
||||
"For issue-to-PR workflows and long-running tasks. Best when you want an agent to run, iterate, and bring back a reviewable result.",
|
||||
bestFor: [
|
||||
"Issue-to-PR workflows",
|
||||
"Multi-step debugging",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue