feat(web-evals): add AI Engineer Talent Marketplace (Sprint 1)

- Add 5 engineer roles: Junior, Senior, Staff, Architecture Reviewer, Autonomous Agent
- Build role selection landing page with hiring metaphor at /evals/workers
- Build candidate rankings page with tiered recommendations at /evals/workers/[roleId]
- Build candidate comparison page with Recharts charts at /evals/workers/[roleId]/compare
- Build "How We Interview" methodology page at /evals/methodology
- Add mock data with real eval scores from 27 model runs
- Implement "Hire This Engineer" CTA linking to Roo Code Cloud
- Implement "Configure Extension" CTA with clipboard copy
- Per-language score breakdowns (Go, Java, JS, Python, Rust)
- Daily salary pricing (80 tasks/agent/day estimate)
- framer-motion animations, glass-morphism design, role color themes
- Tone-of-voice compliance (no em dashes, no hype, workflow-first copy)
- vscode:// deep link design doc at plans/vscode-deep-link-design.md
This commit is contained in:
Michael Preuss 2026-02-11 19:32:09 -08:00
parent 417abeb691
commit 5a57ccf8f8
10 changed files with 4157 additions and 0 deletions

View file

@ -0,0 +1,949 @@
"use client"
import { motion } from "framer-motion"
import {
ArrowRight,
FlaskConical,
Code,
GitBranch,
Building2,
AlertTriangle,
BarChart3,
Terminal,
ExternalLink,
CheckCircle2,
Beaker,
Timer,
DollarSign,
Zap,
Trophy,
} from "lucide-react"
import Link from "next/link"
// ── Framer Motion Variants ──────────────────────────────────────────────────
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.12,
delayChildren: 0.1,
},
},
}
const fadeUpVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.21, 0.45, 0.27, 0.9] as const,
},
},
}
const backgroundVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
duration: 1.2,
ease: "easeOut" as const,
},
},
}
const cardVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.21, 0.45, 0.27, 0.9] as const,
},
},
}
// ── Section Number Marker ───────────────────────────────────────────────────
function SectionNumber({ num }: { num: string }) {
return (
<span className="font-mono text-5xl font-black tracking-tighter text-foreground/[0.15] dark:text-foreground/[0.18] md:text-7xl">
{num}
</span>
)
}
// ── Process Step Icon ───────────────────────────────────────────────────────
function ProcessStep({
icon: Icon,
label,
isLast,
}: {
icon: React.ComponentType<{ className?: string }>
label: string
isLast?: boolean
}) {
return (
<div className="flex items-center gap-3">
<div className="flex flex-col items-center gap-1">
<div className="flex size-12 items-center justify-center rounded-xl border border-border/50 bg-card/80 backdrop-blur-sm">
<Icon className="size-5 text-foreground/70" />
</div>
<span className="text-[11px] font-medium text-muted-foreground">{label}</span>
</div>
{!isLast && (
<div className="mb-5 flex items-center">
<div className="h-px w-6 bg-gradient-to-r from-border to-border/30 sm:w-10" />
<ArrowRight className="size-3 text-muted-foreground/50" />
</div>
)}
</div>
)
}
// ── Language Card ───────────────────────────────────────────────────────────
function LanguageCard({ name, color }: { name: string; color: string }) {
return (
<div className="group flex flex-col items-center gap-2 rounded-xl border border-border/50 bg-card/50 px-4 py-4 backdrop-blur-sm transition-all duration-200 hover:border-border hover:bg-card/80">
<div className={`flex size-10 items-center justify-center rounded-lg ${color}`}>
<span className="text-sm font-bold text-white">{name.slice(0, 2).toUpperCase()}</span>
</div>
<span className="text-xs font-medium text-muted-foreground">{name}</span>
</div>
)
}
// ── Scoring Bar Component ───────────────────────────────────────────────────
function ScoringBar({
label,
icon: Icon,
color,
bgColor,
weight,
description,
}: {
label: string
icon: React.ComponentType<{ className?: string }>
color: string
bgColor: string
weight: number
description: string
}) {
return (
<div className="flex items-start gap-4 rounded-xl border border-border/50 bg-card/50 p-4 backdrop-blur-sm">
<div className={`flex size-10 shrink-0 items-center justify-center rounded-lg ${bgColor}`}>
<Icon className={`size-5 ${color}`} />
</div>
<div className="flex-1">
<div className="flex items-baseline justify-between">
<p className="text-sm font-semibold text-foreground">{label}</p>
<span className="font-mono text-xs font-bold text-muted-foreground">{weight}%</span>
</div>
<p className="mt-0.5 text-xs leading-relaxed text-muted-foreground">{description}</p>
<div className="mt-2 h-1.5 w-full overflow-hidden rounded-full bg-muted/50">
<motion.div
className={`h-full rounded-full ${color.replace("text-", "bg-")}`}
initial={{ width: 0 }}
whileInView={{ width: `${weight}%` }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: [0.21, 0.45, 0.27, 0.9], delay: 0.3 }}
/>
</div>
</div>
</div>
)
}
// ── Main Content Component ──────────────────────────────────────────────────
export function MethodologyContent() {
return (
<>
{/*
HERO SECTION
*/}
<section className="relative flex flex-col items-center overflow-hidden pt-32 pb-20">
{/* Atmospheric blur background */}
<motion.div
className="absolute inset-0"
initial="hidden"
animate="visible"
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-[35%] top-[45%] h-[600px] w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/8 dark:bg-blue-600/15 blur-[120px]" />
<div className="absolute left-[55%] top-[40%] h-[700px] w-[700px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-indigo-500/6 dark:bg-indigo-600/10 blur-[140px]" />
<div className="absolute left-[70%] top-[55%] h-[500px] w-[500px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-slate-500/5 dark:bg-slate-400/8 blur-[100px]" />
</div>
</motion.div>
<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-3xl text-center"
initial="hidden"
animate="visible"
variants={containerVariants}>
{/* Breadcrumb */}
<motion.nav
className="mb-8 flex items-center justify-center gap-2 text-sm text-muted-foreground"
variants={fadeUpVariants}>
<Link href="/evals" className="transition-colors hover:text-foreground">
Evals
</Link>
<span className="text-border">/</span>
<Link href="/evals/workers" className="transition-colors hover:text-foreground">
Hire an AI Engineer
</Link>
<span className="text-border">/</span>
<span className="font-medium text-foreground">How We Interview</span>
</motion.nav>
{/* Heading */}
<motion.h1
className="text-4xl font-bold tracking-tight md:text-5xl lg:text-6xl"
variants={fadeUpVariants}>
How We Interview{" "}
<span className="bg-gradient-to-r from-blue-500 via-indigo-500 to-violet-500 bg-clip-text text-transparent">
AI Models
</span>
</motion.h1>
{/* Subtitle */}
<motion.p
className="mt-6 text-lg leading-relaxed text-muted-foreground md:text-xl"
variants={fadeUpVariants}>
Same exercises, same environment, same scoring for every model. Every step is documented and
every eval run is reproducible.
</motion.p>
{/* Pill badge links */}
<motion.div
className="mt-8 flex flex-wrap items-center justify-center gap-3"
variants={fadeUpVariants}>
<Link
href="/evals/workers"
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">
<Trophy className="size-4" />
View recommendations
<ArrowRight className="size-3.5 transition-transform duration-200 group-hover:translate-x-0.5" />
</Link>
<Link
href="/evals"
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">
<FlaskConical className="size-4" />
Raw eval data
<ArrowRight className="size-3.5 transition-transform duration-200 group-hover:translate-x-0.5" />
</Link>
</motion.div>
</motion.div>
</div>
</section>
{/*
SECTION 01: THE INTERVIEW PROCESS
*/}
<motion.section
className="relative overflow-hidden border-t border-border/30 py-20"
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={containerVariants}>
<div className="container mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
<motion.div variants={fadeUpVariants}>
<SectionNumber num="01" />
</motion.div>
<motion.h2
className="-mt-3 text-3xl font-bold tracking-tight md:text-4xl"
variants={fadeUpVariants}>
The Interview Process
</motion.h2>
<motion.div
className="mt-6 max-w-2xl text-base leading-relaxed text-muted-foreground md:text-lg"
variants={fadeUpVariants}>
<p>
We don&apos;t test models in isolation. We test them as they work inside Roo Code. Each
model gets the same exercises, same time limit, same tools. We measure what matters.
</p>
</motion.div>
{/* Process flow */}
<motion.div
className="mt-10 flex flex-wrap items-start justify-center gap-2 sm:gap-0"
variants={fadeUpVariants}>
<ProcessStep icon={Beaker} label="Exercise" />
<ProcessStep icon={Code} label="Roo Code" />
<ProcessStep icon={CheckCircle2} label="Test Suite" />
<ProcessStep icon={BarChart3} label="Score" isLast />
</motion.div>
{/* Key principles */}
<motion.div className="mt-12 grid grid-cols-1 gap-4 sm:grid-cols-3" variants={containerVariants}>
{[
{
title: "Identical Environment",
desc: "Docker container with VS Code, Roo Code extension, and a fresh workspace per exercise.",
},
{
title: "No Cherry-Picking",
desc: "Every model gets the exact same interview. No curated demos, no special treatment.",
},
{
title: "Real Metrics",
desc: "Does it pass the tests? How much does it cost? How fast does it deliver?",
},
].map((item) => (
<motion.div
key={item.title}
className="rounded-xl border border-border/50 bg-card/50 p-5 backdrop-blur-sm"
variants={cardVariants}>
<p className="text-sm font-semibold text-foreground">{item.title}</p>
<p className="mt-1.5 text-xs leading-relaxed text-muted-foreground">{item.desc}</p>
</motion.div>
))}
</motion.div>
</div>
</motion.section>
{/*
SECTION 02: THE INTERVIEW SUITE
*/}
<motion.section
className="relative overflow-hidden border-t border-border/30 py-20"
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={containerVariants}>
{/* Subtle background glow */}
<motion.div className="absolute inset-0" 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-[600px] w-full -translate-x-1/2 -translate-y-1/2 rounded-[100%] bg-foreground/[0.015] dark:bg-foreground/[0.025] blur-[80px]" />
</div>
</motion.div>
<div className="container relative z-10 mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
<motion.div variants={fadeUpVariants}>
<SectionNumber num="02" />
</motion.div>
<motion.h2
className="-mt-3 text-3xl font-bold tracking-tight md:text-4xl"
variants={fadeUpVariants}>
The Interview Suite
</motion.h2>
<motion.p
className="mt-6 max-w-2xl text-base leading-relaxed text-muted-foreground md:text-lg"
variants={fadeUpVariants}>
Hundreds of coding exercises across <strong className="text-foreground">5 languages</strong> and{" "}
<strong className="text-foreground">3 difficulty tiers</strong>. From single-file fixes to
complex architecture decisions.
</motion.p>
{/* Language cards */}
<motion.div className="mt-10 grid grid-cols-5 gap-3" variants={containerVariants}>
<motion.div variants={cardVariants}>
<LanguageCard name="Go" color="bg-cyan-600" />
</motion.div>
<motion.div variants={cardVariants}>
<LanguageCard name="Java" color="bg-orange-600" />
</motion.div>
<motion.div variants={cardVariants}>
<LanguageCard name="JavaScript" color="bg-yellow-500" />
</motion.div>
<motion.div variants={cardVariants}>
<LanguageCard name="Python" color="bg-green-600" />
</motion.div>
<motion.div variants={cardVariants}>
<LanguageCard name="Rust" color="bg-red-600" />
</motion.div>
</motion.div>
{/* Difficulty tiers */}
<motion.div className="mt-10" variants={fadeUpVariants}>
<h3 className="mb-4 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Difficulty Tiers
</h3>
<div className="overflow-hidden rounded-xl border border-border/50 backdrop-blur-sm">
{/* Easy */}
<div className="flex items-center gap-4 border-b border-border/30 bg-card/50 p-4 sm:p-5">
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-green-500/10 dark:bg-green-500/15">
<span className="text-sm font-bold text-green-600 dark:text-green-400">E</span>
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-foreground">Easy</p>
<p className="mt-0.5 text-xs text-muted-foreground">
Single-file fixes, straightforward implementations, basic debugging
</p>
</div>
<div className="hidden shrink-0 items-center gap-3 sm:flex">
<span className="min-w-[80px] text-right font-mono text-sm font-bold text-green-600 dark:text-green-400">
9095%
</span>
<div className="h-2 w-32 overflow-hidden rounded-full bg-green-500/10">
<motion.div
className="h-full rounded-full bg-green-500"
initial={{ width: 0 }}
whileInView={{ width: "92%" }}
viewport={{ once: true }}
transition={{ duration: 0.8, delay: 0.2 }}
/>
</div>
</div>
</div>
{/* Medium */}
<div className="flex items-center gap-4 border-b border-border/30 bg-card/30 p-4 sm:p-5">
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-yellow-500/10 dark:bg-yellow-500/15">
<span className="text-sm font-bold text-yellow-600 dark:text-yellow-400">M</span>
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-foreground">Medium</p>
<p className="mt-0.5 text-xs text-muted-foreground">
Multi-file changes, refactoring, cross-file understanding
</p>
</div>
<div className="hidden shrink-0 items-center gap-3 sm:flex">
<span className="min-w-[80px] text-right font-mono text-sm font-bold text-yellow-600 dark:text-yellow-400">
6080%
</span>
<div className="h-2 w-32 overflow-hidden rounded-full bg-yellow-500/10">
<motion.div
className="h-full rounded-full bg-yellow-500"
initial={{ width: 0 }}
whileInView={{ width: "70%" }}
viewport={{ once: true }}
transition={{ duration: 0.8, delay: 0.3 }}
/>
</div>
</div>
</div>
{/* Hard */}
<div className="flex items-center gap-4 bg-card/20 p-4 sm:p-5">
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-red-500/10 dark:bg-red-500/15">
<span className="text-sm font-bold text-red-600 dark:text-red-400">H</span>
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold text-foreground">Hard</p>
<p className="mt-0.5 text-xs text-muted-foreground">
Architecture decisions, ambiguous requirements, complex system design
</p>
</div>
<div className="hidden shrink-0 items-center gap-3 sm:flex">
<span className="min-w-[80px] text-right font-mono text-sm font-bold text-red-600 dark:text-red-400">
3050%
</span>
<div className="h-2 w-32 overflow-hidden rounded-full bg-red-500/10">
<motion.div
className="h-full rounded-full bg-red-500"
initial={{ width: 0 }}
whileInView={{ width: "40%" }}
viewport={{ once: true }}
transition={{ duration: 0.8, delay: 0.4 }}
/>
</div>
</div>
</div>
</div>
</motion.div>
</div>
</motion.section>
{/*
SECTION 03: ENGINEER ROLES
*/}
<motion.section
className="relative overflow-hidden border-t border-border/30 py-20"
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={containerVariants}>
<div className="container mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
<motion.div variants={fadeUpVariants}>
<SectionNumber num="03" />
</motion.div>
<motion.h2
className="-mt-3 text-3xl font-bold tracking-tight md:text-4xl"
variants={fadeUpVariants}>
Engineer Roles
</motion.h2>
<motion.p
className="mt-6 max-w-2xl text-base leading-relaxed text-muted-foreground md:text-lg"
variants={fadeUpVariants}>
Not every task needs the same level of engineering. Three role tiers, each with different
exercise difficulty and scoring weights.
</motion.p>
{/* Role cards */}
<motion.div className="mt-10 grid grid-cols-1 gap-5 md:grid-cols-3" variants={containerVariants}>
{/* Junior */}
<motion.div
className="group relative overflow-hidden rounded-2xl border border-border/50 bg-card/50 backdrop-blur-sm transition-all duration-300 hover:border-emerald-500/40 dark:hover:border-emerald-400/30 hover:shadow-xl hover:shadow-emerald-500/5"
variants={cardVariants}>
<div className="absolute inset-0 rounded-2xl bg-emerald-500/[0.03] dark:bg-emerald-600/[0.05] opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
<div className="relative z-10 p-6">
<div className="flex size-11 items-center justify-center rounded-xl bg-emerald-100 dark:bg-emerald-900/30">
<Code className="size-5 text-emerald-700 dark:text-emerald-300" />
</div>
<h3 className="mt-4 text-lg font-bold">Junior Engineer</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
Easy + Medium exercises. Boilerplate, simple bug fixes, test generation. Scoring
emphasizes{" "}
<strong className="text-emerald-600 dark:text-emerald-400">cost efficiency</strong>.
</p>
{/* Weight breakdown */}
<div className="mt-5 space-y-2">
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
Scoring Weights
</p>
<div className="flex h-3 overflow-hidden rounded-full">
<div className="bg-green-500" style={{ width: "35%" }} title="Success 35%" />
<div className="bg-blue-500" style={{ width: "15%" }} title="Quality 15%" />
<div className="bg-amber-500" style={{ width: "35%" }} title="Cost 35%" />
<div className="bg-purple-500" style={{ width: "15%" }} title="Speed 15%" />
</div>
<div className="flex justify-between text-[10px] text-muted-foreground">
<span>Success 35%</span>
<span>Quality 15%</span>
<span>Cost 35%</span>
<span>Speed 15%</span>
</div>
</div>
</div>
</motion.div>
{/* Senior */}
<motion.div
className="group relative overflow-hidden rounded-2xl border border-border/50 bg-card/50 backdrop-blur-sm transition-all duration-300 hover:border-blue-500/40 dark:hover:border-blue-400/30 hover:shadow-xl hover:shadow-blue-500/5"
variants={cardVariants}>
<div className="absolute inset-0 rounded-2xl bg-blue-500/[0.03] dark:bg-blue-600/[0.05] opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
<div className="relative z-10 p-6">
<div className="flex size-11 items-center justify-center rounded-xl bg-blue-100 dark:bg-blue-900/30">
<GitBranch className="size-5 text-blue-700 dark:text-blue-300" />
</div>
<h3 className="mt-4 text-lg font-bold">Senior Engineer</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
Medium exercises. Feature development, debugging, code review. Balanced scoring with
emphasis on{" "}
<strong className="text-blue-600 dark:text-blue-400">success rate + quality</strong>
.
</p>
{/* Weight breakdown */}
<div className="mt-5 space-y-2">
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
Scoring Weights
</p>
<div className="flex h-3 overflow-hidden rounded-full">
<div className="bg-green-500" style={{ width: "40%" }} title="Success 40%" />
<div className="bg-blue-500" style={{ width: "25%" }} title="Quality 25%" />
<div className="bg-amber-500" style={{ width: "20%" }} title="Cost 20%" />
<div className="bg-purple-500" style={{ width: "15%" }} title="Speed 15%" />
</div>
<div className="flex justify-between text-[10px] text-muted-foreground">
<span>Success 40%</span>
<span>Quality 25%</span>
<span>Cost 20%</span>
<span>Speed 15%</span>
</div>
</div>
</div>
</motion.div>
{/* Staff */}
<motion.div
className="group relative overflow-hidden rounded-2xl border border-border/50 bg-card/50 backdrop-blur-sm transition-all duration-300 hover:border-amber-500/40 dark:hover:border-amber-400/30 hover:shadow-xl hover:shadow-amber-500/5"
variants={cardVariants}>
<div className="absolute inset-0 rounded-2xl bg-amber-500/[0.03] dark:bg-amber-600/[0.05] opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
<div className="relative z-10 p-6">
<div className="flex size-11 items-center justify-center rounded-xl bg-amber-100 dark:bg-amber-900/30">
<Building2 className="size-5 text-amber-700 dark:text-amber-300" />
</div>
<h3 className="mt-4 text-lg font-bold">Staff Engineer</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
Hard exercises. Architecture, ambiguous requirements, system design. Scoring
prioritizes{" "}
<strong className="text-amber-600 dark:text-amber-400">
reasoning quality + correctness
</strong>
.
</p>
{/* Weight breakdown */}
<div className="mt-5 space-y-2">
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
Scoring Weights
</p>
<div className="flex h-3 overflow-hidden rounded-full">
<div className="bg-green-500" style={{ width: "45%" }} title="Success 45%" />
<div className="bg-blue-500" style={{ width: "30%" }} title="Quality 30%" />
<div className="bg-amber-500" style={{ width: "10%" }} title="Cost 10%" />
<div className="bg-purple-500" style={{ width: "15%" }} title="Speed 15%" />
</div>
<div className="flex justify-between text-[10px] text-muted-foreground">
<span>Success 45%</span>
<span>Quality 30%</span>
<span>Cost 10%</span>
<span>Speed 15%</span>
</div>
</div>
</div>
</motion.div>
</motion.div>
</div>
</motion.section>
{/*
SECTION 04: SCORING
*/}
<motion.section
className="relative overflow-hidden border-t border-border/30 py-20"
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={containerVariants}>
{/* Background glow */}
<motion.div className="absolute inset-0" 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-[600px] w-full -translate-x-1/2 -translate-y-1/2 rounded-[100%] bg-foreground/[0.015] dark:bg-foreground/[0.025] blur-[80px]" />
</div>
</motion.div>
<div className="container relative z-10 mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
<motion.div variants={fadeUpVariants}>
<SectionNumber num="04" />
</motion.div>
<motion.h2
className="-mt-3 text-3xl font-bold tracking-tight md:text-4xl"
variants={fadeUpVariants}>
Scoring
</motion.h2>
<motion.p
className="mt-6 max-w-2xl text-base leading-relaxed text-muted-foreground md:text-lg"
variants={fadeUpVariants}>
Each model receives a <strong className="text-foreground">composite score</strong>, a weighted
sum of four dimensions normalized to a 0100 scale.
</motion.p>
{/* Scoring formula components */}
<motion.div className="mt-10 grid grid-cols-1 gap-4 sm:grid-cols-2" variants={containerVariants}>
<motion.div variants={cardVariants}>
<ScoringBar
label="Success Rate"
icon={CheckCircle2}
color="text-green-500"
bgColor="bg-green-500/10 dark:bg-green-500/15"
weight={40}
description="Percentage of exercises where the model produces code that passes all tests."
/>
</motion.div>
<motion.div variants={cardVariants}>
<ScoringBar
label="Code Quality"
icon={Zap}
color="text-blue-500"
bgColor="bg-blue-500/10 dark:bg-blue-500/15"
weight={25}
description="Structure, readability, and adherence to best practices of produced code."
/>
</motion.div>
<motion.div variants={cardVariants}>
<ScoringBar
label="Cost Efficiency"
icon={DollarSign}
color="text-amber-500"
bgColor="bg-amber-500/10 dark:bg-amber-500/15"
weight={20}
description="Average API cost per task. Lower cost with equal quality ranks higher."
/>
</motion.div>
<motion.div variants={cardVariants}>
<ScoringBar
label="Speed"
icon={Timer}
color="text-purple-500"
bgColor="bg-purple-500/10 dark:bg-purple-500/15"
weight={15}
description="Average time to complete each task. Faster completion ranks higher."
/>
</motion.div>
</motion.div>
{/* Tier classification */}
<motion.div className="mt-14" variants={fadeUpVariants}>
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Recommendation Tiers
</h3>
<p className="mb-6 text-sm text-muted-foreground">
Composite scores are mapped to recommendation tiers:
</p>
</motion.div>
<motion.div
className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4"
variants={containerVariants}>
{/* Best */}
<motion.div
className="relative overflow-hidden rounded-xl border border-green-500/20 bg-green-500/[0.04] p-5 backdrop-blur-sm dark:bg-green-500/[0.06]"
variants={cardVariants}>
<div className="absolute right-3 top-3 font-mono text-3xl font-black text-green-500/10 dark:text-green-500/15">
85
</div>
<div className="relative z-10">
<span className="inline-block rounded-md bg-green-500/15 px-2.5 py-0.5 text-xs font-semibold text-green-600 dark:text-green-400">
Best
</span>
<p className="mt-3 text-sm font-medium text-foreground">Top Performer</p>
<p className="mt-1 text-xs text-muted-foreground">Highly recommended for this role.</p>
</div>
</motion.div>
{/* Recommended */}
<motion.div
className="relative overflow-hidden rounded-xl border border-blue-500/20 bg-blue-500/[0.04] p-5 backdrop-blur-sm dark:bg-blue-500/[0.06]"
variants={cardVariants}>
<div className="absolute right-3 top-3 font-mono text-3xl font-black text-blue-500/10 dark:text-blue-500/15">
7084
</div>
<div className="relative z-10">
<span className="inline-block rounded-md bg-blue-500/15 px-2.5 py-0.5 text-xs font-semibold text-blue-600 dark:text-blue-400">
Recommended
</span>
<p className="mt-3 text-sm font-medium text-foreground">Solid Choice</p>
<p className="mt-1 text-xs text-muted-foreground">
Reliable for most tasks at this level.
</p>
</div>
</motion.div>
{/* Situational */}
<motion.div
className="relative overflow-hidden rounded-xl border border-yellow-500/20 bg-yellow-500/[0.04] p-5 backdrop-blur-sm dark:bg-yellow-500/[0.06]"
variants={cardVariants}>
<div className="absolute right-3 top-3 font-mono text-3xl font-black text-yellow-500/10 dark:text-yellow-500/15">
5069
</div>
<div className="relative z-10">
<span className="inline-block rounded-md bg-yellow-500/15 px-2.5 py-0.5 text-xs font-semibold text-yellow-600 dark:text-yellow-400">
Situational
</span>
<p className="mt-3 text-sm font-medium text-foreground">Usable with Caveats</p>
<p className="mt-1 text-xs text-muted-foreground">May struggle in specific areas.</p>
</div>
</motion.div>
{/* Not Recommended */}
<motion.div
className="relative overflow-hidden rounded-xl border border-red-500/20 bg-red-500/[0.04] p-5 backdrop-blur-sm dark:bg-red-500/[0.06]"
variants={cardVariants}>
<div className="absolute right-3 top-3 font-mono text-3xl font-black text-red-500/10 dark:text-red-500/15">
&lt;50
</div>
<div className="relative z-10">
<span className="inline-block rounded-md bg-red-500/15 px-2.5 py-0.5 text-xs font-semibold text-red-600 dark:text-red-400">
Not Recommended
</span>
<p className="mt-3 text-sm font-medium text-foreground">High Failure Rate</p>
<p className="mt-1 text-xs text-muted-foreground">Not suitable for this role.</p>
</div>
</motion.div>
</motion.div>
<motion.p className="mt-8 text-sm leading-relaxed text-muted-foreground" variants={fadeUpVariants}>
Per-language breakdowns reveal where each model excels or struggles. A model might score well
overall but underperform in Rust, or dominate in Python but lag in Go.
</motion.p>
</div>
</motion.section>
{/*
SECTION 05: RUN YOUR OWN INTERVIEWS
*/}
<motion.section
className="relative overflow-hidden border-t border-border/30 py-20"
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={containerVariants}>
<div className="container mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
<motion.div variants={fadeUpVariants}>
<SectionNumber num="05" />
</motion.div>
<motion.h2
className="-mt-3 text-3xl font-bold tracking-tight md:text-4xl"
variants={fadeUpVariants}>
Run Your Own Interviews
</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
infrastructure, with your own API keys, against any model.
</motion.p>
{/* Terminal card */}
<motion.div
className="mt-10 overflow-hidden rounded-2xl border border-border/50 bg-card/80 backdrop-blur-sm"
variants={cardVariants}>
{/* Terminal header */}
<div className="flex items-center gap-2 border-b border-border/50 bg-muted/30 px-4 py-3">
<div className="flex gap-1.5">
<div className="size-3 rounded-full bg-red-500/60" />
<div className="size-3 rounded-full bg-yellow-500/60" />
<div className="size-3 rounded-full bg-green-500/60" />
</div>
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Terminal className="size-3.5" />
<span>terminal</span>
</div>
</div>
{/* Terminal body */}
<div className="p-5 font-mono text-sm leading-relaxed">
<div className="text-muted-foreground">
<span className="text-green-500">$</span>{" "}
<span className="text-foreground">git clone</span>{" "}
<span className="text-blue-400">https://github.com/RooCodeInc/Roo-Code-Evals.git</span>
</div>
<div className="mt-1 text-muted-foreground">
<span className="text-green-500">$</span> <span className="text-foreground">cd</span>{" "}
<span className="text-blue-400">Roo-Code-Evals</span>
</div>
<div className="mt-1 text-muted-foreground">
<span className="text-green-500">$</span>{" "}
<span className="text-foreground/50"># Follow the README for setup instructions</span>
</div>
</div>
</motion.div>
{/* GitHub link */}
<motion.div className="mt-6" variants={fadeUpVariants}>
<a
href="https://github.com/RooCodeInc/Roo-Code-Evals"
target="_blank"
rel="noopener noreferrer"
className="group inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-5 py-2.5 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-300 hover:border-border hover:text-foreground">
<svg className="size-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
View on GitHub
<ExternalLink className="size-3.5 transition-transform duration-200 group-hover:translate-x-0.5" />
</a>
</motion.div>
</div>
</motion.section>
{/*
SECTION 06: LIMITATIONS
*/}
<motion.section
className="relative overflow-hidden border-t border-border/30 py-20"
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={containerVariants}>
<div className="container mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
<motion.div variants={fadeUpVariants}>
<SectionNumber num="06" />
</motion.div>
<motion.h2
className="-mt-3 text-3xl font-bold tracking-tight md:text-4xl"
variants={fadeUpVariants}>
Limitations
</motion.h2>
<motion.p
className="mt-6 max-w-2xl text-base leading-relaxed text-muted-foreground md:text-lg"
variants={fadeUpVariants}>
Every evaluation has blind spots. These are ours.
</motion.p>
<motion.ul className="mt-8 grid grid-cols-1 gap-3 sm:grid-cols-2" variants={containerVariants}>
{[
{
title: "Single test environment",
description:
"All evals run in Docker + VS Code. Results may differ in other IDEs or environments.",
},
{
title: "Expanding exercise coverage",
description:
"Hundreds of exercises, but the suite is continuously growing. Some niche patterns may be underrepresented.",
},
{
title: "API changes affect results",
description:
"Providers update their models. A model that scored well last month may behave differently after an update.",
},
{
title: "Point-in-time snapshots",
description:
'Each eval run captures performance at a specific point. We re-run regularly; check the "last updated" date.',
},
].map((item) => (
<motion.li
key={item.title}
className="flex items-start gap-3 rounded-xl border border-amber-500/20 bg-amber-500/[0.03] p-4 backdrop-blur-sm dark:bg-amber-500/[0.05]"
variants={cardVariants}>
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-amber-600 dark:text-amber-400" />
<div>
<p className="text-sm font-medium text-foreground">{item.title}</p>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
{item.description}
</p>
</div>
</motion.li>
))}
</motion.ul>
</div>
</motion.section>
{/*
BOTTOM NAVIGATION
*/}
<section className="border-t border-border/50 pb-24 pt-16">
<div className="container mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
<motion.div
className="mx-auto max-w-2xl text-center"
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={containerVariants}>
<motion.p className="mb-6 text-sm text-muted-foreground" variants={fadeUpVariants}>
Ready to see the results?
</motion.p>
<motion.div
className="flex flex-wrap items-center justify-center gap-4"
variants={fadeUpVariants}>
<Link
href="/evals/workers"
className="group inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-5 py-2.5 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-300 hover:border-border hover:text-foreground">
<Trophy className="size-4" />
View recommendations
<ArrowRight className="size-3.5 transition-transform duration-200 group-hover:translate-x-0.5" />
</Link>
<Link
href="/evals"
className="group inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-5 py-2.5 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-300 hover:border-border hover:text-foreground">
<FlaskConical className="size-4" />
Raw eval data
<ArrowRight className="size-3.5 transition-transform duration-200 group-hover:translate-x-0.5" />
</Link>
</motion.div>
</motion.div>
</div>
</section>
</>
)
}

View file

@ -0,0 +1,58 @@
import type { Metadata } from "next"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { MethodologyContent } from "./methodology-content"
// ── SEO Metadata ────────────────────────────────────────────────────────────
const TITLE = "How We Interview AI Models | 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"
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 evaluation",
"model benchmarking",
"coding evals",
"methodology",
"interview process",
"transparent evaluation",
],
}
// ── Page Component ──────────────────────────────────────────────────────────
export default function MethodologyPage() {
return <MethodologyContent />
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,443 @@
"use client"
import { useState, useMemo, useCallback } from "react"
import Link from "next/link"
import { ArrowLeft, Copy, Check, FileJson, FileSpreadsheet } from "lucide-react"
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from "recharts"
import type { ModelCandidate, LanguageScores, EngineerRole, RoleRecommendation } from "@/lib/mock-recommendations"
import { TASKS_PER_DAY } from "@/lib/mock-recommendations"
// ── Constants ───────────────────────────────────────────────────────────────
const LANGUAGES: { key: keyof LanguageScores; label: string }[] = [
{ key: "go", label: "Go" },
{ key: "java", label: "Java" },
{ key: "javascript", label: "JavaScript" },
{ key: "python", label: "Python" },
{ key: "rust", label: "Rust" },
]
const PROVIDERS = ["anthropic", "openai", "google", "deepseek", "groq", "alibaba", "mistral"] as const
const PROVIDER_LABELS: Record<string, string> = {
anthropic: "Anthropic",
openai: "OpenAI",
google: "Google",
deepseek: "DeepSeek",
groq: "Meta/Groq",
alibaba: "Alibaba",
mistral: "Mistral",
}
const DIMENSION_COLORS = {
composite: "#3b82f6", // blue
success: "#22c55e", // green
cost: "#f59e0b", // amber
speed: "#a855f7", // purple
}
// ── Helpers ─────────────────────────────────────────────────────────────────
/** Normalize cost: lower cost → higher bar (0100). */
function normalizeCost(cost: number, maxCost: number): number {
if (maxCost === 0) return 100
return Math.round((1 - cost / maxCost) * 100)
}
/** Normalize speed: lower time → higher bar (0100). */
function normalizeSpeed(time: number, maxTime: number): number {
if (maxTime === 0) return 100
return Math.round((1 - time / maxTime) * 100)
}
function buildChartData(
candidates: ModelCandidate[],
language: keyof LanguageScores | "all",
maxCost: number,
maxTime: number,
) {
return candidates.map((c) => ({
name: c.displayName,
composite: language === "all" ? c.compositeScore : c.languageScores[language],
success: c.successRate,
costEfficiency: normalizeCost(c.avgCostPerTask, maxCost),
speed: normalizeSpeed(c.avgTimePerTask, maxTime),
// raw daily cost for tooltip display
dailyCost: Math.round(c.estimatedDailyCost),
costPerTask: c.avgCostPerTask,
// raw data for export
_raw: c,
}))
}
function candidateToCsvRow(c: ModelCandidate): string {
return [
c.provider,
c.modelId,
c.displayName,
c.compositeScore,
c.successRate,
c.avgCostPerTask,
Math.round(c.estimatedDailyCost),
c.avgTimePerTask,
c.languageScores.go,
c.languageScores.java,
c.languageScores.javascript,
c.languageScores.python,
c.languageScores.rust,
c.tier,
`"${c.settings.temperature}"`,
`"${c.settings.reasoningEffort ?? ""}"`,
].join(",")
}
function downloadBlob(content: string, filename: string, mimeType: string) {
const blob = new Blob([content], { type: mimeType })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
// ── Custom Tooltip ──────────────────────────────────────────────────────────
function CustomTooltip({
active,
payload,
label,
}: {
active?: boolean
// eslint-disable-next-line @typescript-eslint/no-explicit-any
payload?: any[]
label?: string
}) {
if (!active || !payload || !payload.length) return null
// Extract raw daily cost from first payload entry's data
const rawData = payload[0]?.payload as { dailyCost?: number; costPerTask?: number } | undefined
const dailyCost = rawData?.dailyCost
const costPerTask = rawData?.costPerTask
return (
<div className="rounded-lg border border-border bg-card p-3 shadow-lg">
<p className="mb-2 font-semibold text-sm">{label}</p>
{payload.map(
(
entry: {
name: string
value: number
color: string
dataKey: string
},
index: number,
) => (
<div key={index} className="flex items-center gap-2 text-xs">
<span className="size-2.5 rounded-full" style={{ backgroundColor: entry.color }} />
<span className="text-muted-foreground">{entry.name}:</span>
<span className="font-medium tabular-nums">
{entry.dataKey === "costEfficiency" && dailyCost !== undefined
? `${entry.value} (~$${dailyCost}/day · $${costPerTask?.toFixed(3)}/task)`
: entry.value}
</span>
</div>
),
)}
</div>
)
}
// ── Main Component ──────────────────────────────────────────────────────────
interface ComparisonChartProps {
recommendation: RoleRecommendation
role: EngineerRole
roleId: string
}
export function ComparisonChart({ recommendation, role, roleId }: ComparisonChartProps) {
const { allCandidates } = recommendation
// ── State ───────────────────────────────────────────────────────────────
const [selectedLanguage, setSelectedLanguage] = useState<keyof LanguageScores | "all">("all")
const [enabledProviders, setEnabledProviders] = useState<Set<string>>(() => new Set(PROVIDERS))
const [minSuccessRate, setMinSuccessRate] = useState(0)
const [copiedSettings, setCopiedSettings] = useState(false)
// ── Derived ─────────────────────────────────────────────────────────────
const filteredCandidates = useMemo(
() => allCandidates.filter((c) => enabledProviders.has(c.provider) && c.successRate >= minSuccessRate),
[allCandidates, enabledProviders, minSuccessRate],
)
const maxCost = useMemo(() => Math.max(...allCandidates.map((c) => c.avgCostPerTask), 0.001), [allCandidates])
const maxTime = useMemo(() => Math.max(...allCandidates.map((c) => c.avgTimePerTask), 0.1), [allCandidates])
const chartData = useMemo(
() => buildChartData(filteredCandidates, selectedLanguage, maxCost, maxTime),
[filteredCandidates, selectedLanguage, maxCost, maxTime],
)
const chartHeight = Math.max(300, chartData.length * 60 + 80)
// ── Handlers ────────────────────────────────────────────────────────────
const toggleProvider = useCallback((provider: string) => {
setEnabledProviders((prev) => {
const next = new Set(prev)
if (next.has(provider)) {
next.delete(provider)
} else {
next.add(provider)
}
return next
})
}, [])
const handleCopySettings = useCallback(async () => {
const settings = filteredCandidates.map((c) => ({
provider: c.provider,
model: c.modelId,
displayName: c.displayName,
temperature: c.settings.temperature,
...(c.settings.reasoningEffort ? { reasoningEffort: c.settings.reasoningEffort } : {}),
}))
await navigator.clipboard.writeText(JSON.stringify(settings, null, 2))
setCopiedSettings(true)
setTimeout(() => setCopiedSettings(false), 2000)
}, [filteredCandidates])
const handleExportCsv = useCallback(() => {
const header =
"Provider,Model ID,Display Name,Composite Score,Success Rate,Avg Cost/Task,Est. Daily Cost,Avg Time/Task,Go,Java,JavaScript,Python,Rust,Tier,Temperature,Reasoning Effort"
const rows = filteredCandidates.map(candidateToCsvRow)
const csv = [header, ...rows].join("\n")
downloadBlob(csv, `${roleId}-comparison.csv`, "text/csv")
}, [filteredCandidates, roleId])
const handleExportJson = useCallback(() => {
const json = JSON.stringify(filteredCandidates, null, 2)
downloadBlob(json, `${roleId}-comparison.json`, "application/json")
}, [filteredCandidates, roleId])
// ── Render ──────────────────────────────────────────────────────────────
return (
<div className="mx-auto flex max-w-screen-lg flex-col gap-10 px-4 pt-28 pb-16 sm:px-6 lg:px-8">
{/* ── Breadcrumb ─────────────────────────────────────────────── */}
<nav className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<Link href="/evals" className="transition-colors hover:text-foreground">
Evals
</Link>
<span>/</span>
<Link href="/evals/workers" className="transition-colors hover:text-foreground">
Hire an AI Engineer
</Link>
<span>/</span>
<Link href={`/evals/workers/${roleId}`} className="transition-colors hover:text-foreground">
{role.name}
</Link>
<span>/</span>
<span className="font-medium text-foreground">Compare Candidates</span>
</nav>
{/* ── Page Header ────────────────────────────────────────────── */}
<section>
<h1 className="text-3xl font-bold tracking-tight md:text-4xl">Compare Candidates {role.name}</h1>
<p className="mt-2 text-base text-muted-foreground">
Interactive comparison across composite score, success rate, cost efficiency, and speed.
</p>
</section>
{/* ── Language Toggle ─────────────────────────────────────────── */}
<section>
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Score View
</h2>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedLanguage("all")}
className={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
selectedLanguage === "all"
? "bg-blue-600 text-white"
: "bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground"
}`}>
All Languages
</button>
{LANGUAGES.map(({ key, label }) => (
<button
key={key}
onClick={() => setSelectedLanguage(key)}
className={`rounded-lg px-3 py-1.5 text-sm font-medium transition-colors ${
selectedLanguage === key
? "bg-blue-600 text-white"
: "bg-muted text-muted-foreground hover:bg-accent hover:text-accent-foreground"
}`}>
{label}
</button>
))}
</div>
</section>
{/* ── Filters ────────────────────────────────────────────────── */}
<section className="flex flex-col gap-6 rounded-xl border border-border/50 bg-card/50 p-5 backdrop-blur-sm sm:flex-row sm:items-start sm:gap-10">
{/* Provider checkboxes */}
<div className="flex-1">
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Providers
</h3>
<div className="flex flex-wrap gap-x-4 gap-y-2">
{PROVIDERS.map((p) => (
<label key={p} className="inline-flex cursor-pointer items-center gap-2 text-sm">
<input
type="checkbox"
checked={enabledProviders.has(p)}
onChange={() => toggleProvider(p)}
className="size-4 rounded border-border accent-blue-600"
/>
{PROVIDER_LABELS[p]}
</label>
))}
</div>
</div>
{/* Min success rate slider */}
<div className="w-full sm:w-52">
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Min Success Rate
</h3>
<div className="flex items-center gap-3">
<input
type="range"
min={0}
max={100}
value={minSuccessRate}
onChange={(e) => setMinSuccessRate(Number(e.target.value))}
className="h-2 flex-1 cursor-pointer accent-blue-600"
/>
<span className="w-10 text-right text-sm font-medium tabular-nums">{minSuccessRate}%</span>
</div>
</div>
</section>
{/* ── Chart ──────────────────────────────────────────────────── */}
<section className="rounded-xl border border-border/50 bg-card/50 p-5 backdrop-blur-sm">
<h2 className="mb-1 text-lg font-semibold">
{selectedLanguage === "all"
? "Composite Score"
: `${LANGUAGES.find((l) => l.key === selectedLanguage)?.label} Score`}{" "}
Comparison
</h2>
<p className="mb-6 text-xs text-muted-foreground">
Cost Efficiency and Speed are inverted higher bars mean cheaper / faster. Daily costs assume ~
{TASKS_PER_DAY} tasks per agent per day (~6 productive hours).
</p>
{chartData.length === 0 ? (
<div className="flex h-48 items-center justify-center text-muted-foreground">
No candidates match the current filters.
</div>
) : (
<ResponsiveContainer width="100%" height={chartHeight}>
<BarChart
data={chartData}
layout="vertical"
margin={{ top: 5, right: 30, left: 10, bottom: 5 }}>
<XAxis type="number" domain={[0, 100]} tickFormatter={(v: number) => `${v}`} />
<YAxis type="category" dataKey="name" width={150} tick={{ fontSize: 12 }} />
<Tooltip content={<CustomTooltip />} />
<Legend wrapperStyle={{ fontSize: 12, paddingTop: 8 }} />
<Bar
dataKey="composite"
name={
selectedLanguage === "all"
? "Composite"
: `${LANGUAGES.find((l) => l.key === selectedLanguage)?.label ?? "Language"} Score`
}
fill={DIMENSION_COLORS.composite}
radius={[0, 4, 4, 0]}
barSize={12}
/>
<Bar
dataKey="success"
name="Success Rate"
fill={DIMENSION_COLORS.success}
radius={[0, 4, 4, 0]}
barSize={12}
/>
<Bar
dataKey="costEfficiency"
name="Cost Efficiency"
fill={DIMENSION_COLORS.cost}
radius={[0, 4, 4, 0]}
barSize={12}
/>
<Bar
dataKey="speed"
name="Speed"
fill={DIMENSION_COLORS.speed}
radius={[0, 4, 4, 0]}
barSize={12}
/>
</BarChart>
</ResponsiveContainer>
)}
</section>
{/* ── Export Buttons ──────────────────────────────────────────── */}
<section className="flex flex-wrap gap-3">
<button
onClick={handleCopySettings}
className="inline-flex items-center gap-2 rounded-lg border border-border bg-card px-4 py-2.5 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground">
{copiedSettings ? (
<>
<Check className="size-4 text-green-500" />
Copied!
</>
) : (
<>
<Copy className="size-4" />
📋 Copy Settings
</>
)}
</button>
<button
onClick={handleExportCsv}
className="inline-flex items-center gap-2 rounded-lg border border-border bg-card px-4 py-2.5 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground">
<FileSpreadsheet className="size-4" />
📄 Export CSV
</button>
<button
onClick={handleExportJson}
className="inline-flex items-center gap-2 rounded-lg border border-border bg-card px-4 py-2.5 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground">
<FileJson className="size-4" />
📦 Export JSON
</button>
</section>
{/* ── Bottom Navigation ───────────────────────────────────────── */}
<nav className="flex flex-col gap-3 border-t border-border pt-8 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-2">
<Link
href={`/evals/workers/${roleId}`}
className="inline-flex items-center gap-2 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground">
<ArrowLeft className="size-4" />
Back to {role.name} candidates
</Link>
<Link
href="/evals/workers"
className="inline-flex items-center gap-2 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground">
<ArrowLeft className="size-4" />
Back to all roles
</Link>
</div>
</nav>
</div>
)
}

View file

@ -0,0 +1,81 @@
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 "./comparison-chart"
// ── SEO Metadata ────────────────────────────────────────────────────────────
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 engineer role was not found.",
}
}
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 path = `/evals/workers/${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 engineer",
"model comparison",
"coding evals",
role.name.toLowerCase(),
"bar chart",
"candidate comparison",
],
}
}
// ── Page Component ──────────────────────────────────────────────────────────
export default async function CompareCandidatesPage({ params }: PageProps) {
const { roleId } = await params
const recommendation = getRoleRecommendation(roleId)
if (!recommendation) {
notFound()
}
return <ComparisonChart recommendation={recommendation} role={recommendation.role} roleId={roleId} />
}

View file

@ -0,0 +1,42 @@
"use client"
import { useState } from "react"
import { Copy, Check } from "lucide-react"
interface CopySettingsButtonProps {
settings: {
provider: string
model: string
temperature: number
reasoningEffort?: string
}
}
export function CopySettingsButton({ settings }: CopySettingsButtonProps) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
const json = JSON.stringify(settings, null, 2)
await navigator.clipboard.writeText(json)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<button
onClick={handleCopy}
className="inline-flex w-full items-center justify-center gap-2 rounded-xl border border-border/50 bg-card/50 px-4 py-2.5 text-sm font-medium text-foreground/80 backdrop-blur-sm transition-all duration-200 hover:bg-card/80 hover:text-foreground hover:border-border active:scale-[0.98]">
{copied ? (
<>
<Check className="size-4 text-green-400" />
Copied!
</>
) : (
<>
<Copy className="size-4 text-muted-foreground" />
🔧 Configure Extension
</>
)}
</button>
)
}

View file

@ -0,0 +1,104 @@
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 "./candidates-content"
// ── SEO Metadata ────────────────────────────────────────────────────────────
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 engineer role was not found.",
}
}
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 path = `/evals/workers/${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 engineer",
"model recommendations",
"coding evals",
role.name.toLowerCase(),
"model comparison",
],
}
}
// ── Page Component ──────────────────────────────────────────────────────────
export default async function RoleCandidatesPage({ params }: PageProps) {
const { roleId } = await params
const recommendation = getRoleRecommendation(roleId)
if (!recommendation) {
notFound()
}
const { role, best, budgetHire, speedHire, allCandidates, totalEvalRuns, totalExercises, lastUpdated } =
recommendation
// Pre-compute cloud URLs on the server so the client component receives
// only serializable data (no functions).
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}
/>
)
}

View file

@ -0,0 +1,85 @@
import type { Metadata } from "next"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { getEngineerRoles, getAllRecommendations } from "@/lib/mock-recommendations"
import { WorkersContent } from "./workers-content"
// ── SEO Metadata ────────────────────────────────────────────────────────────
const TITLE = "Hire an AI Engineer | 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"
const PATH = "/evals/workers"
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 engineer",
"model recommendations",
"coding evals",
"model comparison",
"hire AI",
"talent marketplace",
],
}
// ── Page Component ──────────────────────────────────────────────────────────
export default function HireAnAIEngineerPage() {
const roles = getEngineerRoles()
const recommendations = getAllRecommendations()
// Aggregate totals
const totalEvalRuns = recommendations.reduce((sum, r) => sum + r.totalEvalRuns, 0)
const totalExercises = recommendations.reduce((sum, r) => sum + r.totalExercises, 0)
// Unique model count across all roles
const uniqueModels = new Set(recommendations.flatMap((r) => r.allCandidates.map((c) => c.modelId)))
const totalModels = uniqueModels.size
const lastUpdated = recommendations
.map((r) => r.lastUpdated)
.sort()
.pop()
return (
<WorkersContent
roles={roles}
recommendations={recommendations}
totalEvalRuns={totalEvalRuns}
totalExercises={totalExercises}
totalModels={totalModels}
lastUpdated={lastUpdated}
/>
)
}

View file

@ -0,0 +1,519 @@
"use client"
import { motion } from "framer-motion"
import {
Code,
GitBranch,
Building2,
Search,
Bot,
ArrowRight,
ChevronDown,
CheckCircle2,
AlertTriangle,
Users,
FlaskConical,
Beaker,
Globe,
TrendingUp,
} from "lucide-react"
import type { LucideIcon } from "lucide-react"
import Link from "next/link"
import type { EngineerRole, RoleRecommendation } from "@/lib/mock-recommendations"
import { TASKS_PER_DAY } from "@/lib/mock-recommendations"
// ── Icon Mapping ────────────────────────────────────────────────────────────
const ICON_MAP: Record<string, LucideIcon> = {
Code,
GitBranch,
Building2,
Search,
Bot,
}
// ── Color Themes per Role ───────────────────────────────────────────────────
type RoleTheme = {
accent: string
accentLight: string
accentDark: string
iconBg: string
iconText: string
badgeBg: string
badgeText: string
borderHover: string
shadowHover: string
buttonBg: string
buttonHover: string
glowColor: string
dotColor: string
strengthColor: string
}
const ROLE_THEMES: Record<string, RoleTheme> = {
junior: {
accent: "emerald",
accentLight: "text-emerald-600",
accentDark: "dark:text-emerald-400",
iconBg: "bg-emerald-100 dark:bg-emerald-900/30",
iconText: "text-emerald-700 dark:text-emerald-300",
badgeBg: "bg-emerald-100 dark:bg-emerald-900/30",
badgeText: "text-emerald-700 dark:text-emerald-300",
borderHover: "hover:border-emerald-500/40 dark:hover:border-emerald-400/30",
shadowHover: "hover:shadow-emerald-500/10 dark:hover:shadow-emerald-400/10",
buttonBg: "bg-emerald-600 dark:bg-emerald-600",
buttonHover: "hover:bg-emerald-700 dark:hover:bg-emerald-500",
glowColor: "bg-emerald-500/8 dark:bg-emerald-600/15",
dotColor: "bg-emerald-500",
strengthColor: "text-emerald-600 dark:text-emerald-400",
},
senior: {
accent: "blue",
accentLight: "text-blue-600",
accentDark: "dark:text-blue-400",
iconBg: "bg-blue-100 dark:bg-blue-900/30",
iconText: "text-blue-700 dark:text-blue-300",
badgeBg: "bg-blue-100 dark:bg-blue-900/30",
badgeText: "text-blue-700 dark:text-blue-300",
borderHover: "hover:border-blue-500/40 dark:hover:border-blue-400/30",
shadowHover: "hover:shadow-blue-500/10 dark:hover:shadow-blue-400/10",
buttonBg: "bg-blue-600 dark:bg-blue-600",
buttonHover: "hover:bg-blue-700 dark:hover:bg-blue-500",
glowColor: "bg-blue-500/8 dark:bg-blue-600/15",
dotColor: "bg-blue-500",
strengthColor: "text-blue-600 dark:text-blue-400",
},
staff: {
accent: "amber",
accentLight: "text-amber-600",
accentDark: "dark:text-amber-400",
iconBg: "bg-amber-100 dark:bg-amber-900/30",
iconText: "text-amber-700 dark:text-amber-300",
badgeBg: "bg-amber-100 dark:bg-amber-900/30",
badgeText: "text-amber-700 dark:text-amber-300",
borderHover: "hover:border-amber-500/40 dark:hover:border-amber-400/30",
shadowHover: "hover:shadow-amber-500/10 dark:hover:shadow-amber-400/10",
buttonBg: "bg-amber-600 dark:bg-amber-600",
buttonHover: "hover:bg-amber-700 dark:hover:bg-amber-500",
glowColor: "bg-amber-500/8 dark:bg-amber-600/15",
dotColor: "bg-amber-500",
strengthColor: "text-amber-600 dark:text-amber-400",
},
reviewer: {
accent: "violet",
accentLight: "text-violet-600",
accentDark: "dark:text-violet-400",
iconBg: "bg-violet-100 dark:bg-violet-900/30",
iconText: "text-violet-700 dark:text-violet-300",
badgeBg: "bg-violet-100 dark:bg-violet-900/30",
badgeText: "text-violet-700 dark:text-violet-300",
borderHover: "hover:border-violet-500/40 dark:hover:border-violet-400/30",
shadowHover: "hover:shadow-violet-500/10 dark:hover:shadow-violet-400/10",
buttonBg: "bg-violet-600 dark:bg-violet-600",
buttonHover: "hover:bg-violet-700 dark:hover:bg-violet-500",
glowColor: "bg-violet-500/8 dark:bg-violet-600/15",
dotColor: "bg-violet-500",
strengthColor: "text-violet-600 dark:text-violet-400",
},
autonomous: {
accent: "cyan",
accentLight: "text-cyan-600",
accentDark: "dark:text-cyan-400",
iconBg: "bg-cyan-100 dark:bg-cyan-900/30",
iconText: "text-cyan-700 dark:text-cyan-300",
badgeBg: "bg-cyan-100 dark:bg-cyan-900/30",
badgeText: "text-cyan-700 dark:text-cyan-300",
borderHover: "hover:border-cyan-500/40 dark:hover:border-cyan-400/30",
shadowHover: "hover:shadow-cyan-500/10 dark:hover:shadow-cyan-400/10",
buttonBg: "bg-cyan-600 dark:bg-cyan-600",
buttonHover: "hover:bg-cyan-700 dark:hover:bg-cyan-500",
glowColor: "bg-cyan-500/8 dark:bg-cyan-600/15",
dotColor: "bg-cyan-500",
strengthColor: "text-cyan-600 dark:text-cyan-400",
},
}
const DEFAULT_THEME = ROLE_THEMES.senior!
// ── Framer Motion Variants ──────────────────────────────────────────────────
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.2,
},
},
}
const cardVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.21, 0.45, 0.27, 0.9] as const,
},
},
}
const fadeUpVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.21, 0.45, 0.27, 0.9] as const,
},
},
}
const backgroundVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
duration: 1.2,
ease: "easeOut" as const,
},
},
}
// ── Sub-Components ──────────────────────────────────────────────────────────
function StatPill({ icon: Icon, value, label }: { icon: LucideIcon; value: string; label: string }) {
return (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Icon className="size-4 text-foreground/60" />
<span className="font-mono font-semibold text-foreground">{value}</span>
<span>{label}</span>
</div>
)
}
// ── Main Content Component ──────────────────────────────────────────────────
type WorkersContentProps = {
roles: EngineerRole[]
recommendations: RoleRecommendation[]
totalEvalRuns: number
totalExercises: number
totalModels: number
lastUpdated: string | undefined
}
export function WorkersContent({
roles,
recommendations,
totalEvalRuns,
totalExercises,
totalModels,
lastUpdated,
}: WorkersContentProps) {
const recByRole = new Map(recommendations.map((r) => [r.roleId, r]))
return (
<>
{/* ── Hero Section ───────────────────────────────────────────── */}
<section className="relative flex flex-col items-center overflow-hidden pt-32 pb-32">
{/* Atmospheric blur background */}
<motion.div
className="absolute inset-0"
initial="hidden"
animate="visible"
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-[30%] top-[40%] h-[600px] w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-emerald-500/8 dark:bg-emerald-600/15 blur-[120px]" />
<div className="absolute left-[50%] top-[50%] h-[800px] w-[800px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/8 dark:bg-blue-600/15 blur-[140px]" />
<div className="absolute left-[70%] top-[40%] h-[600px] w-[600px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-amber-500/6 dark:bg-amber-600/10 blur-[120px]" />
</div>
</motion.div>
{/* 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" />
<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-3xl text-center"
initial="hidden"
animate="visible"
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>
</motion.div>
{/* Heading */}
<motion.h1
className="mt-6 text-5xl font-bold tracking-tight md:text-6xl lg:text-7xl"
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>
</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.
</motion.p>
{/* 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"
variants={fadeUpVariants}>
<StatPill icon={Users} value={totalModels.toString()} label="models tested" />
<div className="hidden h-4 w-px bg-border sm:block" />
<StatPill icon={FlaskConical} value={totalExercises.toLocaleString()} label="exercises" />
<div className="hidden h-4 w-px bg-border sm:block" />
<StatPill icon={Globe} value="5" label="languages" />
<div className="hidden h-4 w-px bg-border sm:block" />
<StatPill icon={TrendingUp} value={totalEvalRuns.toLocaleString()} label="eval runs" />
</motion.div>
</motion.div>
</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>
<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 member
</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-0 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" />
</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}
</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} candidates
</span>
<span className="inline-flex items-center gap-1.5">
<FlaskConical className="size-3.5" />
{exerciseCount.toLocaleString()} exercises
</span>
</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>
</div>
</div>
</div>
</motion.div>
)
})}
</motion.div>
</div>
</section>
{/* ── Footer / Methodology Section ───────────────────────────── */}
<section className="relative border-t border-border/50 pb-24 pt-16">
<div className="container mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
<motion.div
className="mx-auto max-w-2xl text-center"
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={containerVariants}>
{/* Stats summary */}
<motion.div
className="mb-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-2 text-sm text-muted-foreground"
variants={fadeUpVariants}>
<span className="font-mono font-semibold text-foreground">
{totalEvalRuns.toLocaleString()}+
</span>{" "}
eval runs
<span className="text-border"></span>
<span className="font-mono font-semibold text-foreground">5</span> languages
<span className="text-border"></span>
Last updated:{" "}
<span className="font-medium text-foreground/80">
{lastUpdated
? new Date(lastUpdated).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})
: "N/A"}
</span>
</motion.div>
{/* Assumption note */}
<motion.p className="mb-6 text-xs text-muted-foreground/60" variants={fadeUpVariants}>
Daily costs assume ~{TASKS_PER_DAY} tasks per agent per day (~6 productive hours including
overhead).
</motion.p>
{/* Links */}
<motion.div
className="flex flex-wrap items-center justify-center gap-4"
variants={fadeUpVariants}>
<Link
href="/evals/methodology"
className="inline-flex items-center gap-1.5 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">
<Beaker className="size-3.5" />
Our methodology
</Link>
<Link
href="/evals"
className="inline-flex items-center gap-1.5 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">
<FlaskConical className="size-3.5" />
Raw eval data
<ArrowRight className="size-3.5" />
</Link>
</motion.div>
</motion.div>
</div>
</section>
</>
)
}

View file

@ -0,0 +1,866 @@
// ---------------------------------------------------------------------------
// Eval Recommendations: Types + Mock Data (S1.1a)
// ---------------------------------------------------------------------------
// This file defines the API contract for the AI Engineer Talent Marketplace.
// The backend (Sprint 3-4) will produce data matching these exact types.
// ---------------------------------------------------------------------------
// ── Constants ──────────────────────────────────────────────────────────────
/**
* Estimated tasks per agent per day.
* Assumes ~6 productive hours with overhead for setup, review, and iteration.
* One human engineer typically manages 2-3 agents throughout a workday.
*/
export const TASKS_PER_DAY = 80
// ── Types ──────────────────────────────────────────────────────────────────
/** Engineer role definition: maps task complexity to a hiring tier. */
export type EngineerRole = {
id: string
name: string
/** Daily salary range string, e.g. "~$338/day" */
salaryRange: string
description: string
bestFor: string[]
strengths: string[]
weaknesses: string[]
icon: string
}
/** Language-specific eval scores (0100). */
export type LanguageScores = {
go: number
java: number
javascript: number
python: number
rust: number
}
/** Model inference settings used during evaluation. */
export type ModelSettings = {
temperature: number
reasoningEffort?: string
}
/** A model candidate evaluated for a specific role. */
export type ModelCandidate = {
provider: string
modelId: string
displayName: string
compositeScore: number
tier: "best" | "recommended" | "situational" | "not-recommended"
tags: string[]
successRate: number
avgCostPerTask: number
/** Estimated daily cost: avgCostPerTask × TASKS_PER_DAY */
estimatedDailyCost: number
avgTimePerTask: number
languageScores: LanguageScores
settings: ModelSettings
caveats?: string[]
}
/** Full recommendation payload for a single role. */
export type RoleRecommendation = {
roleId: string
role: EngineerRole
lastUpdated: string
totalEvalRuns: number
totalExercises: number
best: ModelCandidate[]
budgetHire: ModelCandidate | null
speedHire: ModelCandidate | null
allCandidates: ModelCandidate[]
}
// ── Engineer Role Configs ──────────────────────────────────────────────────
const ENGINEER_ROLES: EngineerRole[] = [
{
id: "junior",
name: "Junior Engineer",
salaryRange: "~$210/day",
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"],
icon: "Code",
},
{
id: "senior",
name: "Senior Engineer",
salaryRange: "~$1026/day",
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"],
strengths: [
"Balanced cost/quality",
"Handles multi-file changes and cross-cutting refactors",
"Consistent pass rates across all five languages",
],
weaknesses: ["More expensive than junior", "Overkill for trivial tasks"],
icon: "GitBranch",
},
{
id: "staff",
name: "Staff Engineer",
salaryRange: "~$834/day",
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"],
strengths: [
"Handles multi-step reasoning and ambiguous specs",
"Passes existing test suites consistently",
"Resolves underspecified requirements",
],
weaknesses: ["Most expensive", "Overkill for simple tasks", "Diminishing returns on easy work"],
icon: "Building2",
},
{
id: "reviewer",
name: "Architecture Reviewer",
salaryRange: "~$1540/day",
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.",
bestFor: ["Code review", "PR feedback", "Security analysis", "Design critique", "Refactor guidance"],
strengths: [
"Catches subtle bugs and logic errors",
"Provides actionable suggestions with context",
"Understands cross-file impact of changes",
],
weaknesses: [
"Not for writing code from scratch",
"More expensive than running linters",
"Review quality varies by codebase size",
],
icon: "Search",
},
{
id: "autonomous",
name: "Autonomous Agent",
salaryRange: "~$530/day",
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.",
bestFor: [
"Issue-to-PR workflows",
"Multi-step debugging",
"Feature implementation from spec",
"Long-running tasks",
"Batch operations",
],
strengths: [
"Completes tasks end-to-end with minimal guidance",
"Recovers from errors and retries automatically",
"Handles ambiguous requirements independently",
],
weaknesses: [
"Higher cost per completed task due to retries",
"May take unexpected approaches without oversight",
"Results need review before merging",
],
icon: "Bot",
},
]
// ── Model Candidates (derived from roocode.com/evals data) ─────────────────
// Cost per task = total run cost ÷ 120 exercises
// Time per task = total duration (seconds) ÷ 120 exercises
// Composite scores computed using role-specific weights:
// Junior: success 50%, speed 20%, cost 25%, quality 5%
// Senior: success 40%, quality 25%, cost 20%, speed 15%
// Staff: success 40%, quality 35%, cost 15%, speed 10%
// Quality = consistency across languages (lower variance → higher score)
// --- Junior Role Candidates -------------------------------------------------
const juniorCandidates: ModelCandidate[] = [
{
provider: "xai",
modelId: "grok-4-fast",
displayName: "Grok 4 Fast",
compositeScore: 94,
tier: "best",
tags: ["best-value"],
successRate: 97,
avgCostPerTask: 0.029,
estimatedDailyCost: 0.029 * TASKS_PER_DAY,
avgTimePerTask: 144.0,
languageScores: { go: 97, java: 96, javascript: 98, python: 100, rust: 97 },
settings: { temperature: 0 },
},
{
provider: "openai",
modelId: "gpt-5-mini",
displayName: "GPT-5 Mini",
compositeScore: 92,
tier: "best",
tags: [],
successRate: 99,
avgCostPerTask: 0.028,
estimatedDailyCost: 0.028 * TASKS_PER_DAY,
avgTimePerTask: 173.0,
languageScores: { go: 100, java: 98, javascript: 100, python: 100, rust: 97 },
settings: { temperature: 0 },
},
{
provider: "xai",
modelId: "grok-code-fast-1",
displayName: "Grok Code Fast 1",
compositeScore: 85,
tier: "best",
tags: [],
successRate: 90,
avgCostPerTask: 0.057,
estimatedDailyCost: 0.057 * TASKS_PER_DAY,
avgTimePerTask: 146.0,
languageScores: { go: 92, java: 91, javascript: 88, python: 94, rust: 83 },
settings: { temperature: 0 },
caveats: ["Weaker on Rust (83%): consider alternatives for Rust-heavy tasks"],
},
{
provider: "google",
modelId: "gemini-2.5-flash",
displayName: "Gemini 2.5 Flash",
compositeScore: 82,
tier: "recommended",
tags: ["speed-hire"],
successRate: 90,
avgCostPerTask: 0.118,
estimatedDailyCost: 0.118 * TASKS_PER_DAY,
avgTimePerTask: 109.5,
languageScores: { go: 89, java: 91, javascript: 92, python: 85, rust: 90 },
settings: { temperature: 0 },
},
{
provider: "openai",
modelId: "gpt-4.1-mini",
displayName: "GPT-4.1 Mini",
compositeScore: 77,
tier: "recommended",
tags: [],
successRate: 83,
avgCostPerTask: 0.073,
estimatedDailyCost: 0.073 * TASKS_PER_DAY,
avgTimePerTask: 158.5,
languageScores: { go: 81, java: 84, javascript: 94, python: 76, rust: 70 },
settings: { temperature: 0 },
caveats: ["Inconsistent across languages: Python (76%) to JavaScript (94%)"],
},
{
provider: "anthropic",
modelId: "claude-haiku-4-5",
displayName: "Claude Haiku 4.5",
compositeScore: 77,
tier: "recommended",
tags: [],
successRate: 95,
avgCostPerTask: 0.159,
estimatedDailyCost: 0.159 * TASKS_PER_DAY,
avgTimePerTask: 139.0,
languageScores: { go: 92, java: 93, javascript: 94, python: 97, rust: 100 },
settings: { temperature: 0 },
caveats: ["Most expensive in junior tier. Consider Grok 4 Fast for better cost-to-quality ratio."],
},
{
provider: "openai",
modelId: "gpt-5-nano",
displayName: "GPT-5 Nano",
compositeScore: 73,
tier: "situational",
tags: ["budget-hire"],
successRate: 78,
avgCostPerTask: 0.013,
estimatedDailyCost: 0.013 * TASKS_PER_DAY,
avgTimePerTask: 276.5,
languageScores: { go: 86, java: 73, javascript: 76, python: 79, rust: 77 },
settings: { temperature: 0 },
caveats: ["Cheapest option but slowest: 4.6 min/task average"],
},
{
provider: "deepseek",
modelId: "deepseek-v3",
displayName: "DeepSeek V3",
compositeScore: 66,
tier: "situational",
tags: [],
successRate: 77,
avgCostPerTask: 0.107,
estimatedDailyCost: 0.107 * TASKS_PER_DAY,
avgTimePerTask: 216.0,
languageScores: { go: 83, java: 76, javascript: 82, python: 76, rust: 67 },
settings: { temperature: 0 },
caveats: ["Weakest on Rust (67%)", "Open-source model, self-hostable"],
},
]
// --- Senior Role Candidates -------------------------------------------------
const seniorCandidates: ModelCandidate[] = [
{
provider: "moonshot",
modelId: "kimi-k2-0905",
displayName: "Kimi K2 0905",
compositeScore: 95,
tier: "best",
tags: ["budget-hire", "best-value"],
successRate: 94,
avgCostPerTask: 0.127,
estimatedDailyCost: 0.127 * TASKS_PER_DAY,
avgTimePerTask: 112.0,
languageScores: { go: 94, java: 91, javascript: 96, python: 97, rust: 93 },
settings: { temperature: 0 },
caveats: ["Tested via Groq; latency may vary by provider"],
},
{
provider: "openai",
modelId: "gpt-4.1",
displayName: "GPT-4.1",
compositeScore: 87,
tier: "best",
tags: [],
successRate: 91,
avgCostPerTask: 0.322,
estimatedDailyCost: 0.322 * TASKS_PER_DAY,
avgTimePerTask: 139.5,
languageScores: { go: 92, java: 91, javascript: 90, python: 94, rust: 90 },
settings: { temperature: 0 },
},
{
provider: "anthropic",
modelId: "claude-sonnet-4",
displayName: "Claude Sonnet 4",
compositeScore: 84,
tier: "best",
tags: ["top-performer"],
successRate: 98,
avgCostPerTask: 0.33,
estimatedDailyCost: 0.33 * TASKS_PER_DAY,
avgTimePerTask: 167.5,
languageScores: { go: 94, java: 100, javascript: 98, python: 100, rust: 97 },
settings: { temperature: 0 },
},
{
provider: "openai",
modelId: "gpt-5-medium",
displayName: "GPT-5 (Medium)",
compositeScore: 81,
tier: "recommended",
tags: [],
successRate: 98,
avgCostPerTask: 0.193,
estimatedDailyCost: 0.193 * TASKS_PER_DAY,
avgTimePerTask: 260.0,
languageScores: { go: 97, java: 98, javascript: 100, python: 100, rust: 93 },
settings: { temperature: 0, reasoningEffort: "medium" },
caveats: ["Slowest in tier: 4.3 min/task average"],
},
{
provider: "anthropic",
modelId: "claude-3.7-sonnet",
displayName: "Claude 3.7 Sonnet",
compositeScore: 79,
tier: "recommended",
tags: [],
successRate: 95,
avgCostPerTask: 0.313,
estimatedDailyCost: 0.313 * TASKS_PER_DAY,
avgTimePerTask: 176.5,
languageScores: { go: 92, java: 98, javascript: 94, python: 100, rust: 93 },
settings: { temperature: 0 },
},
{
provider: "anthropic",
modelId: "claude-3.5-sonnet",
displayName: "Claude 3.5 Sonnet",
compositeScore: 78,
tier: "recommended",
tags: ["speed-hire"],
successRate: 90,
avgCostPerTask: 0.208,
estimatedDailyCost: 0.208 * TASKS_PER_DAY,
avgTimePerTask: 108.5,
languageScores: { go: 94, java: 91, javascript: 92, python: 88, rust: 80 },
settings: { temperature: 0 },
caveats: ["Previous generation; weaker on Rust (80%)"],
},
{
provider: "openai",
modelId: "gpt-5-low",
displayName: "GPT-5 (Low)",
compositeScore: 76,
tier: "situational",
tags: [],
successRate: 95,
avgCostPerTask: 0.135,
estimatedDailyCost: 0.135 * TASKS_PER_DAY,
avgTimePerTask: 175.0,
languageScores: { go: 100, java: 96, javascript: 86, python: 100, rust: 100 },
settings: { temperature: 0, reasoningEffort: "low" },
caveats: ["Weak on JavaScript (86%) compared to other languages"],
},
{
provider: "google",
modelId: "gemini-2.5-pro",
displayName: "Gemini 2.5 Pro",
compositeScore: 73,
tier: "situational",
tags: [],
successRate: 96,
avgCostPerTask: 0.482,
estimatedDailyCost: 0.482 * TASKS_PER_DAY,
avgTimePerTask: 188.5,
languageScores: { go: 97, java: 91, javascript: 96, python: 100, rust: 97 },
settings: { temperature: 0 },
caveats: ["Most expensive in this tier: ~$39/day ($0.48/task)"],
},
]
// --- Staff Role Candidates --------------------------------------------------
const staffCandidates: ModelCandidate[] = [
{
provider: "openai",
modelId: "gpt-5.2-med",
displayName: "GPT 5.2 (Med)",
compositeScore: 99,
tier: "best",
tags: ["budget-hire", "best-value"],
successRate: 100,
avgCostPerTask: 0.104,
estimatedDailyCost: 0.104 * TASKS_PER_DAY,
avgTimePerTask: 105.5,
languageScores: { go: 100, java: 100, javascript: 100, python: 100, rust: 100 },
settings: { temperature: 0, reasoningEffort: "medium" },
caveats: ["100% pass rate at ~$8/day ($0.10/task): best cost-to-quality ratio in this role"],
},
{
provider: "anthropic",
modelId: "claude-opus-4-6",
displayName: "Claude Opus 4.6",
compositeScore: 98,
tier: "best",
tags: ["speed-hire", "top-performer"],
successRate: 100,
avgCostPerTask: 0.412,
estimatedDailyCost: 0.412 * TASKS_PER_DAY,
avgTimePerTask: 76.5,
languageScores: { go: 100, java: 100, javascript: 100, python: 100, rust: 100 },
settings: { temperature: 0 },
},
{
provider: "anthropic",
modelId: "claude-sonnet-4-5",
displayName: "Claude Sonnet 4.5",
compositeScore: 97,
tier: "best",
tags: [],
successRate: 100,
avgCostPerTask: 0.32,
estimatedDailyCost: 0.32 * TASKS_PER_DAY,
avgTimePerTask: 103.0,
languageScores: { go: 100, java: 100, javascript: 100, python: 100, rust: 100 },
settings: { temperature: 0 },
},
{
provider: "anthropic",
modelId: "claude-opus-4-5",
displayName: "Claude Opus 4.5",
compositeScore: 96,
tier: "recommended",
tags: [],
successRate: 100,
avgCostPerTask: 0.419,
estimatedDailyCost: 0.419 * TASKS_PER_DAY,
avgTimePerTask: 124.0,
languageScores: { go: 100, java: 100, javascript: 100, python: 100, rust: 100 },
settings: { temperature: 0 },
},
{
provider: "google",
modelId: "gemini-3-pro-preview",
displayName: "Gemini 3 Pro Preview",
compositeScore: 95,
tier: "recommended",
tags: [],
successRate: 100,
avgCostPerTask: 0.276,
estimatedDailyCost: 0.276 * TASKS_PER_DAY,
avgTimePerTask: 164.0,
languageScores: { go: 100, java: 100, javascript: 100, python: 100, rust: 100 },
settings: { temperature: 0 },
},
{
provider: "anthropic",
modelId: "claude-opus-4-1",
displayName: "Claude Opus 4.1",
compositeScore: 73,
tier: "situational",
tags: [],
successRate: 98,
avgCostPerTask: 1.168,
estimatedDailyCost: 1.168 * TASKS_PER_DAY,
avgTimePerTask: 211.5,
languageScores: { go: 97, java: 96, javascript: 98, python: 100, rust: 100 },
settings: { temperature: 0 },
caveats: ["~$93/day ($1.17/task), 11× the cost of the top pick"],
},
{
provider: "openai",
modelId: "gpt-5-medium",
displayName: "GPT-5 (Medium)",
compositeScore: 71,
tier: "situational",
tags: [],
successRate: 98,
avgCostPerTask: 0.193,
estimatedDailyCost: 0.193 * TASKS_PER_DAY,
avgTimePerTask: 260.0,
languageScores: { go: 97, java: 98, javascript: 100, python: 100, rust: 93 },
settings: { temperature: 0, reasoningEffort: "medium" },
caveats: ["Slowest in tier: 4.3 min/task average"],
},
{
provider: "anthropic",
modelId: "claude-opus-4",
displayName: "Claude Opus 4",
compositeScore: 57,
tier: "not-recommended",
tags: [],
successRate: 94,
avgCostPerTask: 1.436,
estimatedDailyCost: 1.436 * TASKS_PER_DAY,
avgTimePerTask: 235.0,
languageScores: { go: 92, java: 91, javascript: 94, python: 94, rust: 100 },
settings: { temperature: 0 },
caveats: [
"Most expensive model tested: ~$115/day ($1.44/task)",
"Lower success rate (94%) despite highest cost",
],
},
]
// --- Architecture Reviewer Candidates ---------------------------------------
// Composite scoring: quality 50%, success 30%, cost 15%, speed 5%
// Quality = consistency across languages (lower variance → higher score)
const reviewerCandidates: ModelCandidate[] = [
{
provider: "openai",
modelId: "gpt-5.2-med",
displayName: "GPT 5.2 (Med)",
compositeScore: 98,
tier: "best",
tags: ["budget-hire", "best-value"],
successRate: 100,
avgCostPerTask: 0.104,
estimatedDailyCost: 0.104 * TASKS_PER_DAY,
avgTimePerTask: 105.5,
languageScores: { go: 100, java: 100, javascript: 100, python: 100, rust: 100 },
settings: { temperature: 0, reasoningEffort: "medium" },
caveats: ["100% consistency across all languages: ideal reviewer at ~$8/day"],
},
{
provider: "anthropic",
modelId: "claude-opus-4-6",
displayName: "Claude Opus 4.6",
compositeScore: 95,
tier: "best",
tags: ["speed-hire", "top-performer"],
successRate: 100,
avgCostPerTask: 0.412,
estimatedDailyCost: 0.412 * TASKS_PER_DAY,
avgTimePerTask: 76.5,
languageScores: { go: 100, java: 100, javascript: 100, python: 100, rust: 100 },
settings: { temperature: 0 },
},
{
provider: "anthropic",
modelId: "claude-sonnet-4-5",
displayName: "Claude Sonnet 4.5",
compositeScore: 94,
tier: "best",
tags: [],
successRate: 100,
avgCostPerTask: 0.32,
estimatedDailyCost: 0.32 * TASKS_PER_DAY,
avgTimePerTask: 103.0,
languageScores: { go: 100, java: 100, javascript: 100, python: 100, rust: 100 },
settings: { temperature: 0 },
},
{
provider: "anthropic",
modelId: "claude-sonnet-4",
displayName: "Claude Sonnet 4",
compositeScore: 90,
tier: "recommended",
tags: ["top-performer"],
successRate: 98,
avgCostPerTask: 0.33,
estimatedDailyCost: 0.33 * TASKS_PER_DAY,
avgTimePerTask: 167.5,
languageScores: { go: 94, java: 100, javascript: 98, python: 100, rust: 97 },
settings: { temperature: 0 },
},
{
provider: "anthropic",
modelId: "claude-haiku-4-5",
displayName: "Claude Haiku 4.5",
compositeScore: 88,
tier: "recommended",
tags: [],
successRate: 95,
avgCostPerTask: 0.159,
estimatedDailyCost: 0.159 * TASKS_PER_DAY,
avgTimePerTask: 139.0,
languageScores: { go: 92, java: 93, javascript: 94, python: 97, rust: 100 },
settings: { temperature: 0 },
caveats: ["Budget reviewer option: good consistency at lower cost"],
},
{
provider: "anthropic",
modelId: "claude-3.7-sonnet",
displayName: "Claude 3.7 Sonnet",
compositeScore: 86,
tier: "recommended",
tags: [],
successRate: 95,
avgCostPerTask: 0.313,
estimatedDailyCost: 0.313 * TASKS_PER_DAY,
avgTimePerTask: 176.5,
languageScores: { go: 92, java: 98, javascript: 94, python: 100, rust: 93 },
settings: { temperature: 0 },
},
{
provider: "google",
modelId: "gemini-2.5-pro",
displayName: "Gemini 2.5 Pro",
compositeScore: 82,
tier: "situational",
tags: [],
successRate: 96,
avgCostPerTask: 0.482,
estimatedDailyCost: 0.482 * TASKS_PER_DAY,
avgTimePerTask: 188.5,
languageScores: { go: 97, java: 91, javascript: 96, python: 100, rust: 97 },
settings: { temperature: 0 },
caveats: ["Most expensive reviewer: ~$39/day ($0.48/task)", "More variable across languages than top picks"],
},
{
provider: "openai",
modelId: "gpt-4.1",
displayName: "GPT-4.1",
compositeScore: 80,
tier: "situational",
tags: [],
successRate: 91,
avgCostPerTask: 0.322,
estimatedDailyCost: 0.322 * TASKS_PER_DAY,
avgTimePerTask: 139.5,
languageScores: { go: 92, java: 91, javascript: 90, python: 94, rust: 90 },
settings: { temperature: 0 },
caveats: ["Lower consistency across languages than Anthropic alternatives"],
},
]
// --- Autonomous Agent Candidates --------------------------------------------
// Composite scoring: success 35%, quality 35%, cost 20%, speed 10%
// Focused on end-to-end task completion and error recovery
const autonomousCandidates: ModelCandidate[] = [
{
provider: "openai",
modelId: "gpt-5.2-med",
displayName: "GPT 5.2 (Med)",
compositeScore: 97,
tier: "best",
tags: ["best-value", "speed-hire"],
successRate: 100,
avgCostPerTask: 0.104,
estimatedDailyCost: 0.104 * TASKS_PER_DAY,
avgTimePerTask: 105.5,
languageScores: { go: 100, java: 100, javascript: 100, python: 100, rust: 100 },
settings: { temperature: 0, reasoningEffort: "medium" },
caveats: ["Perfect success rate + fast completion: ideal autonomous agent at ~$8/day"],
},
{
provider: "openai",
modelId: "gpt-5-mini",
displayName: "GPT-5 Mini",
compositeScore: 93,
tier: "best",
tags: ["budget-hire"],
successRate: 99,
avgCostPerTask: 0.028,
estimatedDailyCost: 0.028 * TASKS_PER_DAY,
avgTimePerTask: 173.0,
languageScores: { go: 100, java: 98, javascript: 100, python: 100, rust: 97 },
settings: { temperature: 0 },
caveats: ["Cheapest autonomous option at ~$2/day with near-perfect success"],
},
{
provider: "xai",
modelId: "grok-4-fast",
displayName: "Grok 4 Fast",
compositeScore: 92,
tier: "best",
tags: [],
successRate: 97,
avgCostPerTask: 0.029,
estimatedDailyCost: 0.029 * TASKS_PER_DAY,
avgTimePerTask: 144.0,
languageScores: { go: 97, java: 96, javascript: 98, python: 100, rust: 97 },
settings: { temperature: 0 },
},
{
provider: "anthropic",
modelId: "claude-sonnet-4",
displayName: "Claude Sonnet 4",
compositeScore: 87,
tier: "recommended",
tags: ["top-performer"],
successRate: 98,
avgCostPerTask: 0.33,
estimatedDailyCost: 0.33 * TASKS_PER_DAY,
avgTimePerTask: 167.5,
languageScores: { go: 94, java: 100, javascript: 98, python: 100, rust: 97 },
settings: { temperature: 0 },
},
{
provider: "moonshot",
modelId: "kimi-k2-0905",
displayName: "Kimi K2 0905",
compositeScore: 86,
tier: "recommended",
tags: [],
successRate: 94,
avgCostPerTask: 0.127,
estimatedDailyCost: 0.127 * TASKS_PER_DAY,
avgTimePerTask: 112.0,
languageScores: { go: 94, java: 91, javascript: 96, python: 97, rust: 93 },
settings: { temperature: 0 },
caveats: ["Tested via Groq; latency may vary by provider"],
},
{
provider: "anthropic",
modelId: "claude-haiku-4-5",
displayName: "Claude Haiku 4.5",
compositeScore: 85,
tier: "recommended",
tags: [],
successRate: 95,
avgCostPerTask: 0.159,
estimatedDailyCost: 0.159 * TASKS_PER_DAY,
avgTimePerTask: 139.0,
languageScores: { go: 92, java: 93, javascript: 94, python: 97, rust: 100 },
settings: { temperature: 0 },
},
{
provider: "openai",
modelId: "gpt-5-low",
displayName: "GPT-5 (Low)",
compositeScore: 82,
tier: "situational",
tags: [],
successRate: 95,
avgCostPerTask: 0.135,
estimatedDailyCost: 0.135 * TASKS_PER_DAY,
avgTimePerTask: 175.0,
languageScores: { go: 100, java: 96, javascript: 86, python: 100, rust: 100 },
settings: { temperature: 0, reasoningEffort: "low" },
caveats: ["Weak on JavaScript (86%) compared to other languages"],
},
{
provider: "openai",
modelId: "gpt-5-medium",
displayName: "GPT-5 (Medium)",
compositeScore: 80,
tier: "situational",
tags: [],
successRate: 98,
avgCostPerTask: 0.193,
estimatedDailyCost: 0.193 * TASKS_PER_DAY,
avgTimePerTask: 260.0,
languageScores: { go: 97, java: 98, javascript: 100, python: 100, rust: 93 },
settings: { temperature: 0, reasoningEffort: "medium" },
caveats: ["Slowest in tier: 4.3 min/task average"],
},
]
// ── Recommendation Builders ────────────────────────────────────────────────
function findBudgetHire(candidates: ModelCandidate[]): ModelCandidate | null {
const budget = candidates
.filter((c) => c.tags.includes("budget-hire"))
.sort((a, b) => a.avgCostPerTask - b.avgCostPerTask)
return budget[0] ?? null
}
function findSpeedHire(candidates: ModelCandidate[]): ModelCandidate | null {
const fast = [...candidates]
.filter((c) => c.tier !== "not-recommended")
.sort((a, b) => a.avgTimePerTask - b.avgTimePerTask)
return fast[0] ?? null
}
function buildRecommendation(
role: EngineerRole,
candidates: ModelCandidate[],
totalEvalRuns: number,
totalExercises: number,
): RoleRecommendation {
const sorted = [...candidates].sort((a, b) => b.compositeScore - a.compositeScore)
return {
roleId: role.id,
role,
lastUpdated: "2026-02-11T00:00:00Z",
totalEvalRuns,
totalExercises,
best: sorted.filter((c) => c.tier === "best").slice(0, 3),
budgetHire: findBudgetHire(sorted),
speedHire: findSpeedHire(sorted),
allCandidates: sorted,
}
}
// ── Pre-built Recommendations ──────────────────────────────────────────────
const RECOMMENDATIONS: Record<string, RoleRecommendation> = {
junior: buildRecommendation(ENGINEER_ROLES[0]!, juniorCandidates, 27, 120),
senior: buildRecommendation(ENGINEER_ROLES[1]!, seniorCandidates, 27, 120),
staff: buildRecommendation(ENGINEER_ROLES[2]!, staffCandidates, 27, 120),
reviewer: buildRecommendation(ENGINEER_ROLES[3]!, reviewerCandidates, 27, 120),
autonomous: buildRecommendation(ENGINEER_ROLES[4]!, autonomousCandidates, 27, 120),
}
// ── Public API ─────────────────────────────────────────────────────────────
/** Returns all engineer role configurations. */
export function getEngineerRoles(): EngineerRole[] {
return ENGINEER_ROLES
}
/** Returns a single engineer role by id, or `undefined` if not found. */
export function getEngineerRole(roleId: string): EngineerRole | undefined {
return ENGINEER_ROLES.find((r) => r.id === roleId)
}
/** Returns the full recommendation payload for a role, or `undefined` if not found. */
export function getRoleRecommendation(roleId: string): RoleRecommendation | undefined {
return RECOMMENDATIONS[roleId]
}
/** Returns recommendation payloads for all roles. */
export function getAllRecommendations(): RoleRecommendation[] {
return Object.values(RECOMMENDATIONS)
}
/** Generates a Cloud signup URL pre-configured with the candidate's model settings. */
export function getCloudSetupUrl(candidate: ModelCandidate): string {
const params = new URLSearchParams({
redirect_url: `/cloud-agents/setup?model=${candidate.modelId}&provider=${candidate.provider}&temperature=${candidate.settings.temperature}`,
})
return `https://app.roocode.com/sign-up?${params.toString()}`
}