feat: eval-evolution work in progress - recommendations and workers UI

This commit is contained in:
Michael Preuss 2026-02-12 12:07:14 -08:00
parent 3a6eaf8e07
commit 310b10cdc2
13 changed files with 1207 additions and 256 deletions

View file

@ -198,19 +198,19 @@ export function MethodologyContent() {
</Link>
<span className="text-border">/</span>
<Link href="/evals/workers" className="transition-colors hover:text-foreground">
Hire an AI Engineer
Build with Roo Code Cloud
</Link>
<span className="text-border">/</span>
<span className="font-medium text-foreground">How We Interview</span>
<span className="font-medium text-foreground">Methodology</span>
</motion.nav>
{/* Heading */}
<motion.h1
className="text-4xl font-bold tracking-tight md:text-5xl lg:text-6xl"
variants={fadeUpVariants}>
How We Interview{" "}
How We Run{" "}
<span className="bg-gradient-to-r from-blue-500 via-indigo-500 to-violet-500 bg-clip-text text-transparent">
AI Models
Evals
</span>
</motion.h1>
@ -262,7 +262,7 @@ export function MethodologyContent() {
<motion.h2
className="-mt-3 text-3xl font-bold tracking-tight md:text-4xl"
variants={fadeUpVariants}>
The Interview Process
The Eval Process
</motion.h2>
<motion.div
@ -293,7 +293,7 @@ export function MethodologyContent() {
},
{
title: "No Cherry-Picking",
desc: "Every model gets the exact same interview. No curated demos, no special treatment.",
desc: "Every model gets the exact same eval run. No curated demos, no special treatment.",
},
{
title: "Real Metrics",
@ -313,7 +313,7 @@ export function MethodologyContent() {
</motion.section>
{/*
SECTION 02: THE INTERVIEW SUITE
SECTION 02: THE EVAL SUITE
*/}
<motion.section
className="relative overflow-hidden border-t border-border/30 py-20"
@ -336,7 +336,7 @@ export function MethodologyContent() {
<motion.h2
className="-mt-3 text-3xl font-bold tracking-tight md:text-4xl"
variants={fadeUpVariants}>
The Interview Suite
The Eval Suite
</motion.h2>
<motion.p
@ -751,7 +751,7 @@ export function MethodologyContent() {
</motion.section>
{/*
SECTION 05: RUN YOUR OWN INTERVIEWS
SECTION 05: RUN YOUR OWN EVALS
*/}
<motion.section
className="relative overflow-hidden border-t border-border/30 py-20"
@ -767,13 +767,13 @@ export function MethodologyContent() {
<motion.h2
className="-mt-3 text-3xl font-bold tracking-tight md:text-4xl"
variants={fadeUpVariants}>
Run Your Own Interviews
Run Your Own Evals
</motion.h2>
<motion.p
className="mt-6 max-w-2xl text-base leading-relaxed text-muted-foreground md:text-lg"
variants={fadeUpVariants}>
Our evaluation framework is fully open source. Run the exact same interviews on your own
Our evaluation framework is fully open source. Run the exact same evals on your own
infrastructure, with your own API keys, against any model.
</motion.p>

View file

@ -7,7 +7,7 @@ import { MethodologyContent } from "./methodology-content"
// ── SEO Metadata ────────────────────────────────────────────────────────────
const TITLE = "How We Interview AI Models | Roo Code Evals"
const TITLE = "Methodology | Roo Code Evals"
const DESCRIPTION = "Our methodology for evaluating AI coding models. Transparent, reproducible, evidence-based."
const OG_DESCRIPTION = "Our methodology for evaluating AI coding models"
const PATH = "/evals/methodology"
@ -46,7 +46,7 @@ export const metadata: Metadata = {
"model benchmarking",
"coding evals",
"methodology",
"interview process",
"evaluation process",
"transparent evaluation",
],
}

View file

@ -0,0 +1,83 @@
import { notFound } from "next/navigation"
import type { Metadata } from "next"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { getEngineerRole, getRoleRecommendation } from "@/lib/mock-recommendations"
import { ComparisonChart } from "../../../workers/[roleId]/compare/comparison-chart"
type PageProps = { params: Promise<{ roleId: string }> }
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { roleId } = await params
const role = getEngineerRole(roleId)
if (!role) {
return {
title: "Role Not Found | Roo Code Evals",
description: "The requested role was not found.",
}
}
const title = `Compare Models — ${role.name} (V2 Preview) | Roo Code Evals`
const description = `Outcome-first comparison of AI models for ${role.name}. Compare composite score, success rate, cost efficiency, and speed.`
const ogDescription = `Compare Models — ${role.name} (V2 Preview)`
const path = `/evals/workers-v2/${roleId}/compare`
return {
title,
description,
alternates: {
canonical: `${SEO.url}${path}`,
},
openGraph: {
title,
description,
url: `${SEO.url}${path}`,
siteName: SEO.name,
images: [
{
url: ogImageUrl(title, ogDescription),
width: 1200,
height: 630,
alt: title,
},
],
locale: SEO.locale,
type: "website",
},
twitter: {
card: SEO.twitterCard,
title,
description,
images: [ogImageUrl(title, ogDescription)],
},
keywords: [
...SEO.keywords,
"AI coding",
"model comparison",
"coding evals",
role.name.toLowerCase(),
"outcome-first",
],
}
}
export default async function WorkersV2ComparePage({ params }: PageProps) {
const { roleId } = await params
const recommendation = getRoleRecommendation(roleId)
if (!recommendation) {
notFound()
}
return (
<ComparisonChart
recommendation={recommendation}
role={recommendation.role}
roleId={roleId}
workersRootPath="/evals/workers-v2"
/>
)
}

View file

@ -0,0 +1,100 @@
import { notFound } from "next/navigation"
import type { Metadata } from "next"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { getRoleRecommendation, getCloudSetupUrl } from "@/lib/mock-recommendations"
import { CandidatesContent } from "../../workers/[roleId]/candidates-content"
type PageProps = { params: Promise<{ roleId: string }> }
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { roleId } = await params
const recommendation = getRoleRecommendation(roleId)
if (!recommendation) {
return {
title: "Role Not Found | Roo Code Evals",
description: "The requested role was not found.",
}
}
const { role } = recommendation
const title = `${role.name} — Recommended Models (V2 Preview) | Roo Code Evals`
const description = `Outcome-first recommendations for ${role.name}. Compare models by success rate, cost, and speed across 5 languages.`
const ogDescription = `${role.name} — Recommended Models (V2 Preview)`
const path = `/evals/workers-v2/${roleId}`
return {
title,
description,
alternates: {
canonical: `${SEO.url}${path}`,
},
openGraph: {
title,
description,
url: `${SEO.url}${path}`,
siteName: SEO.name,
images: [
{
url: ogImageUrl(title, ogDescription),
width: 1200,
height: 630,
alt: title,
},
],
locale: SEO.locale,
type: "website",
},
twitter: {
card: SEO.twitterCard,
title,
description,
images: [ogImageUrl(title, ogDescription)],
},
keywords: [
...SEO.keywords,
"AI coding",
"coding agents",
"model recommendations",
"coding evals",
role.name.toLowerCase(),
"outcome-first",
],
}
}
export default async function WorkersV2RolePage({ params }: PageProps) {
const { roleId } = await params
const recommendation = getRoleRecommendation(roleId)
if (!recommendation) {
notFound()
}
const { role, best, budgetHire, speedHire, allCandidates, totalEvalRuns, totalExercises, lastUpdated } =
recommendation
const cloudUrls: Record<string, string> = {}
for (const candidate of allCandidates) {
cloudUrls[candidate.modelId] = getCloudSetupUrl(candidate)
}
return (
<CandidatesContent
roleId={roleId}
role={role}
best={best}
budgetHire={budgetHire}
speedHire={speedHire}
allCandidates={allCandidates}
totalEvalRuns={totalEvalRuns}
totalExercises={totalExercises}
lastUpdated={lastUpdated}
cloudUrls={cloudUrls}
workersRootPath="/evals/workers-v2"
/>
)
}

View file

@ -0,0 +1,91 @@
import type { Metadata } from "next"
import { Fraunces, IBM_Plex_Sans } from "next/font/google"
import { SEO } from "@/lib/seo"
import { ogImageUrl } from "@/lib/og"
import { getEngineerRoles, getAllRecommendations } from "@/lib/mock-recommendations"
import { WorkersContent } from "../workers/workers-content"
const TITLE = "Build with Roo Code Cloud (V2 Preview) | Roo Code Evals"
const DESCRIPTION =
"Outcome-first, eval-backed recommendations for shipping production code. Start from what you need to ship and pick a setup."
const OG_DESCRIPTION = "Outcome-first recommendations for shipping production code"
const PATH = "/evals/workers-v2"
const display = Fraunces({ subsets: ["latin"], variable: "--font-display" })
const body = IBM_Plex_Sans({ subsets: ["latin"], weight: ["400", "500", "600"], variable: "--font-body" })
export const metadata: Metadata = {
title: TITLE,
description: DESCRIPTION,
alternates: {
canonical: `${SEO.url}${PATH}`,
},
openGraph: {
title: TITLE,
description: DESCRIPTION,
url: `${SEO.url}${PATH}`,
siteName: SEO.name,
images: [
{
url: ogImageUrl(TITLE, OG_DESCRIPTION),
width: 1200,
height: 630,
alt: TITLE,
},
],
locale: SEO.locale,
type: "website",
},
twitter: {
card: SEO.twitterCard,
title: TITLE,
description: DESCRIPTION,
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
},
keywords: [
...SEO.keywords,
"AI coding",
"coding agents",
"roo code cloud",
"model recommendations",
"coding evals",
"shipping code",
"prototype",
"outcome-first",
],
}
export default function WorkersV2Page() {
const roles = getEngineerRoles()
const recommendations = getAllRecommendations()
const totalEvalRuns = recommendations.reduce((sum, recommendation) => sum + recommendation.totalEvalRuns, 0)
const totalExercises = recommendations.reduce((sum, recommendation) => sum + recommendation.totalExercises, 0)
const uniqueModels = new Set(
recommendations.flatMap((recommendation) => recommendation.allCandidates.map((candidate) => candidate.modelId)),
)
const totalModels = uniqueModels.size
const lastUpdated = recommendations
.map((recommendation) => recommendation.lastUpdated)
.sort()
.pop()
return (
<div className={`${body.variable} ${display.variable} [font-family:var(--font-body)]`}>
<WorkersContent
roles={roles}
recommendations={recommendations}
totalEvalRuns={totalEvalRuns}
totalExercises={totalExercises}
totalModels={totalModels}
lastUpdated={lastUpdated}
workersRootPath="/evals/workers-v2"
enableOutcomeLayer
alternateVersionHref="/evals/workers"
alternateVersionLabel="View baseline"
/>
</div>
)
}

View file

@ -21,6 +21,7 @@ import {
} from "lucide-react"
import type { LucideIcon } from "lucide-react"
import Link from "next/link"
import { useSearchParams } from "next/navigation"
import type { ModelCandidate, LanguageScores, EngineerRole } from "@/lib/mock-recommendations"
@ -485,7 +486,7 @@ function CandidateCard({
target="_blank"
rel="noopener noreferrer"
className={`inline-flex w-full items-center justify-center gap-2 rounded-xl ${theme.buttonBg} ${theme.buttonHover} px-4 py-3 text-sm font-semibold text-white transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] shadow-lg`}>
Hire This Engineer
Open in Roo Code Cloud
<ExternalLink className="size-3.5" />
</a>
<CopySettingsButton settings={copySettings} />
@ -620,7 +621,7 @@ function CompactCard({
target="_blank"
rel="noopener noreferrer"
className={`inline-flex w-full items-center justify-center gap-2 rounded-xl ${theme.buttonBg} ${theme.buttonHover} px-4 py-3 text-sm font-semibold text-white transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] shadow-lg`}>
Hire This Engineer
Open in Roo Code Cloud
<ExternalLink className="size-3.5" />
</a>
<CopySettingsButton settings={copySettings} />
@ -643,6 +644,7 @@ export type CandidatesContentProps = {
totalExercises: number
lastUpdated: string
cloudUrls: Record<string, string>
workersRootPath?: string
}
// ── Main Content Component ──────────────────────────────────────────────────
@ -658,9 +660,22 @@ export function CandidatesContent({
totalExercises,
lastUpdated,
cloudUrls,
workersRootPath = "/evals/workers",
}: CandidatesContentProps) {
const searchParams = useSearchParams()
const theme = ROLE_THEMES[roleId] ?? DEFAULT_THEME
const IconComponent = ICON_MAP[role.icon] ?? Code
const alternateWorkersRootPath = workersRootPath === "/evals/workers-v2" ? "/evals/workers" : "/evals/workers-v2"
const alternateVersionLabel = workersRootPath === "/evals/workers-v2" ? "View baseline" : "View V2 preview"
const setupQuery = (() => {
const outcome = searchParams.get("outcome")
if (!outcome) return ""
const params = new URLSearchParams()
params.set("outcome", outcome)
const mode = searchParams.get("mode")
if (mode) params.set("mode", mode)
return `?${params.toString()}`
})()
return (
<>
@ -693,8 +708,10 @@ export function CandidatesContent({
Evals
</Link>
<span className="text-border">/</span>
<Link href="/evals/workers" className="transition-colors hover:text-foreground">
Hire an AI Engineer
<Link
href={`${workersRootPath}${setupQuery}`}
className="transition-colors hover:text-foreground">
Build with Roo Code Cloud
</Link>
<span className="text-border">/</span>
<span className="font-medium text-foreground">{role.name}</span>
@ -753,9 +770,15 @@ export function CandidatesContent({
href="/evals/methodology"
className="group inline-flex items-center gap-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground">
<Beaker className="size-3.5" />
How we interview
Methodology
<ArrowRight className="size-3 transition-transform duration-200 group-hover:translate-x-0.5" />
</Link>
<div className="hidden h-4 w-px bg-border sm:block" />
<Link
href={`${alternateWorkersRootPath}/${roleId}${setupQuery}`}
className="inline-flex items-center gap-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground">
{alternateVersionLabel}
</Link>
</motion.div>
{/* Strengths + Trade-offs grid */}
@ -792,7 +815,7 @@ export function CandidatesContent({
</div>
</section>
{/* ── Top Candidates: Best Overall ────────────────────────────── */}
{/* ── Top Models: Best Overall ────────────────────────────────── */}
<section className="relative overflow-hidden pb-16">
<div className="container relative z-10 mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
<motion.div
@ -804,7 +827,7 @@ export function CandidatesContent({
className="mb-8 flex items-center gap-3 text-3xl font-bold tracking-tight"
variants={fadeUpVariants}>
<Trophy className="size-7 text-yellow-500" />
Top Candidates
Top Models
</motion.h2>
<motion.div className="grid grid-cols-1 gap-6 md:grid-cols-3" variants={containerVariants}>
@ -835,7 +858,7 @@ export function CandidatesContent({
{budgetHire && (
<CompactCard
candidate={budgetHire}
label="💰 Budget Hire"
label="💰 Best value"
icon={DollarSign}
highlight="cost"
theme={theme}
@ -845,7 +868,7 @@ export function CandidatesContent({
{speedHire && (
<CompactCard
candidate={speedHire}
label="⚡ Speed Hire"
label="⚡ Fastest"
icon={Zap}
highlight="speed"
theme={theme}
@ -857,7 +880,7 @@ export function CandidatesContent({
</section>
)}
{/* ── All Candidates Table ────────────────────────────────────── */}
{/* ── All Models Table ────────────────────────────────────────── */}
<section className="relative overflow-hidden pb-16">
{/* Subtle background */}
<motion.div
@ -878,7 +901,7 @@ export function CandidatesContent({
viewport={{ once: true }}
variants={containerVariants}>
<motion.h2 className="mb-8 text-3xl font-bold tracking-tight" variants={fadeUpVariants}>
All Candidates
All Models
</motion.h2>
<motion.div
@ -962,7 +985,7 @@ export function CandidatesContent({
{/* Compare link */}
<motion.div className="mt-6 text-center" variants={fadeUpVariants}>
<Link
href={`/evals/workers/${roleId}/compare`}
href={`${workersRootPath}/${roleId}/compare${setupQuery}`}
className={`group inline-flex items-center gap-2 rounded-full border ${theme.methodologyBorder} bg-card/50 px-5 py-2.5 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-300 hover:text-foreground`}>
📊 Compare all candidates
<ArrowRight className="size-4 transition-transform duration-200 group-hover:translate-x-0.5" />
@ -983,7 +1006,7 @@ export function CandidatesContent({
variants={containerVariants}>
<motion.div variants={fadeUpVariants}>
<Link
href="/evals/workers"
href={`${workersRootPath}${setupQuery}`}
className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-200 hover:border-border hover:text-foreground">
<ArrowLeft className="size-4" />
Back to all roles
@ -991,7 +1014,7 @@ export function CandidatesContent({
</motion.div>
<motion.div className="flex flex-wrap gap-3" variants={fadeUpVariants}>
<Link
href={`/evals/workers/${roleId}/compare`}
href={`${workersRootPath}/${roleId}/compare${setupQuery}`}
className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-200 hover:border-border hover:text-foreground">
📊 Compare candidates
</Link>

View file

@ -2,6 +2,7 @@
import { useState, useMemo, useCallback } from "react"
import Link from "next/link"
import { useSearchParams } from "next/navigation"
import { motion } from "framer-motion"
import {
ArrowLeft,
@ -409,12 +410,12 @@ function ScatterTooltip({
</div>
<div className="flex items-center gap-2.5 text-xs">
<span className="size-2.5 rounded-full bg-amber-400 ring-1 ring-white/10" />
<span className="text-muted-foreground">Daily Salary:</span>
<span className="text-muted-foreground">Daily Spend:</span>
<span className="font-semibold tabular-nums">${data.dailyCost}/day</span>
</div>
<div className="flex items-center gap-2.5 text-xs">
<span className="size-2.5 rounded-full bg-blue-400 ring-1 ring-white/10" />
<span className="text-muted-foreground">Interview Score:</span>
<span className="text-muted-foreground">Eval Score:</span>
<span className="font-semibold tabular-nums">{data.score}</span>
</div>
<div className="flex items-center gap-2.5 text-xs">
@ -433,11 +434,29 @@ interface ComparisonChartProps {
recommendation: RoleRecommendation
role: EngineerRole
roleId: string
workersRootPath?: string
}
export function ComparisonChart({ recommendation, role, roleId }: ComparisonChartProps) {
export function ComparisonChart({
recommendation,
role,
roleId,
workersRootPath = "/evals/workers",
}: ComparisonChartProps) {
const searchParams = useSearchParams()
const { allCandidates } = recommendation
const theme = ROLE_THEMES[roleId] ?? DEFAULT_THEME
const alternateWorkersRootPath = workersRootPath === "/evals/workers-v2" ? "/evals/workers" : "/evals/workers-v2"
const alternateVersionLabel = workersRootPath === "/evals/workers-v2" ? "View baseline" : "View V2 preview"
const setupQuery = (() => {
const outcome = searchParams.get("outcome")
if (!outcome) return ""
const params = new URLSearchParams()
params.set("outcome", outcome)
const mode = searchParams.get("mode")
if (mode) params.set("mode", mode)
return `?${params.toString()}`
})()
// ── State ───────────────────────────────────────────────────────────────
const [selectedLanguage, setSelectedLanguage] = useState<keyof LanguageScores | "all">("all")
@ -561,15 +580,25 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
Evals
</Link>
<span className="text-border">/</span>
<Link href="/evals/workers" className="transition-colors hover:text-foreground">
Hire an AI Engineer
<Link
href={`${workersRootPath}${setupQuery}`}
className="transition-colors hover:text-foreground">
Build with Roo Code Cloud
</Link>
<span className="text-border">/</span>
<Link href={`/evals/workers/${roleId}`} className="transition-colors hover:text-foreground">
<Link
href={`${workersRootPath}/${roleId}${setupQuery}`}
className="transition-colors hover:text-foreground">
{role.name}
</Link>
<span className="text-border">/</span>
<span className="font-medium text-foreground">Compare Candidates</span>
<span className="font-medium text-foreground">Compare Models</span>
<span className="text-border">/</span>
<Link
href={`${alternateWorkersRootPath}/${roleId}/compare${setupQuery}`}
className="font-medium text-muted-foreground transition-colors hover:text-foreground">
{alternateVersionLabel}
</Link>
</motion.nav>
{/* Title row */}
@ -579,7 +608,7 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
<BarChart3 className="size-7" />
</div>
<div className="flex-1">
<h1 className="text-4xl font-bold tracking-tight md:text-5xl">Compare Candidates</h1>
<h1 className="text-4xl font-bold tracking-tight md:text-5xl">Compare Models</h1>
<p className={`mt-1 font-semibold ${theme.accentLight} ${theme.accentDark}`}>
{role.name}
</p>
@ -599,7 +628,7 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
<span className="font-mono font-semibold text-foreground">
{filteredCandidates.length}
</span>
of {allCandidates.length} candidates shown
of {allCandidates.length} models shown
</span>
<div className="hidden h-4 w-px bg-border sm:block" />
<span className="text-sm text-muted-foreground">
@ -731,10 +760,10 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
className="rounded-2xl border border-border/50 bg-card/50 p-6 backdrop-blur-sm"
variants={fadeUpVariants}>
<div className="mb-1 flex items-center gap-2.5">
<h2 className="text-lg font-bold tracking-tight">Value Map: Salary vs Interview Score</h2>
<h2 className="text-lg font-bold tracking-tight">Value Map: Spend vs Eval Score</h2>
</div>
<p className="mb-4 text-xs leading-relaxed text-muted-foreground/80">
Upper-left = best value. Each dot is a candidate model. Size reflects success rate.
Upper-left = higher score at lower spend. Each dot is a model. Size reflects success rate.
</p>
{/* Tier legend */}
@ -753,7 +782,7 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
{scatterData.length === 0 ? (
<div className="flex h-48 flex-col items-center justify-center gap-2 rounded-xl border border-dashed border-border/50 text-muted-foreground">
<SlidersHorizontal className="size-6 text-muted-foreground/50" />
<p className="text-sm">No candidates match the current filters.</p>
<p className="text-sm">No models match the current filters.</p>
<p className="text-xs text-muted-foreground/60">
Try adjusting the provider or success rate filters.
</p>
@ -765,7 +794,7 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
<XAxis
type="number"
dataKey="dailyCost"
name="Daily Salary"
name="Daily Spend"
domain={[0, Math.ceil(scatterMaxCost * 1.1)]}
tickFormatter={(v: number) => `$${v}`}
stroke="hsl(var(--muted-foreground))"
@ -773,7 +802,7 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }}
axisLine={false}
label={{
value: "Daily Salary ($)",
value: "Daily Spend ($)",
position: "insideBottom",
offset: -10,
style: { fontSize: 11, fill: "hsl(var(--muted-foreground))" },
@ -782,14 +811,14 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
<YAxis
type="number"
dataKey="score"
name="Interview Score"
name="Eval Score"
domain={[Math.max(0, scatterMinScore - 10), 100]}
stroke="hsl(var(--muted-foreground))"
strokeOpacity={0.3}
tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }}
axisLine={false}
label={{
value: "Interview Score",
value: "Eval Score",
angle: -90,
position: "insideLeft",
offset: 10,
@ -826,7 +855,7 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
strokeOpacity: 0.3,
}}
/>
<Scatter data={scatterData} name="Candidates">
<Scatter data={scatterData} name="Models">
{scatterData.map((entry, index) => (
<Cell
key={`scatter-cell-${index}`}
@ -864,7 +893,7 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
{chartData.length === 0 ? (
<div className="flex h-48 flex-col items-center justify-center gap-2 rounded-xl border border-dashed border-border/50 text-muted-foreground">
<SlidersHorizontal className="size-6 text-muted-foreground/50" />
<p className="text-sm">No candidates match the current filters.</p>
<p className="text-sm">No models match the current filters.</p>
<p className="text-xs text-muted-foreground/60">
Try adjusting the provider or success rate filters.
</p>
@ -993,15 +1022,15 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<Link
href={`/evals/workers/${roleId}`}
href={`${workersRootPath}/${roleId}${setupQuery}`}
className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-200 hover:border-border hover:text-foreground">
<ArrowLeft className="size-4" />
Back to {role.name} candidates
Back to {role.name} models
</Link>
</div>
<div className="flex flex-wrap gap-3">
<Link
href="/evals/workers"
href={`${workersRootPath}${setupQuery}`}
className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-200 hover:border-border hover:text-foreground">
<ArrowLeft className="size-3.5" />
All roles

View file

@ -22,9 +22,9 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
}
}
const title = `Compare Candidates — ${role.name} | Roo Code Evals`
const description = `Interactive comparison of AI model candidates for the ${role.name} role. Compare composite score, success rate, cost efficiency, and speed.`
const ogDescription = `Compare Candidates — ${role.name}`
const title = `Compare Models — ${role.name} | Roo Code Evals`
const description = `Interactive comparison of AI models for the ${role.name} setup. Compare composite score, success rate, cost efficiency, and speed.`
const ogDescription = `Compare Models — ${role.name}`
const path = `/evals/workers/${roleId}/compare`
return {
@ -57,19 +57,19 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
},
keywords: [
...SEO.keywords,
"AI engineer",
"AI coding",
"model comparison",
"coding evals",
role.name.toLowerCase(),
"bar chart",
"candidate comparison",
"model comparison",
],
}
}
// ── Page Component ──────────────────────────────────────────────────────────
export default async function CompareCandidatesPage({ params }: PageProps) {
export default async function CompareModelsPage({ params }: PageProps) {
const { roleId } = await params
const recommendation = getRoleRecommendation(roleId)
@ -77,5 +77,12 @@ export default async function CompareCandidatesPage({ params }: PageProps) {
notFound()
}
return <ComparisonChart recommendation={recommendation} role={recommendation.role} roleId={roleId} />
return (
<ComparisonChart
recommendation={recommendation}
role={recommendation.role}
roleId={roleId}
workersRootPath="/evals/workers"
/>
)
}

View file

@ -23,9 +23,9 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
}
const { role } = recommendation
const title = `${role.name}AI Engineer Candidates | Roo Code Evals`
const description = `Interview results for ${role.name} AI candidates. Compare models by success rate, cost, and speed across 5 languages.`
const ogDescription = `${role.name}AI Engineer Candidates`
const title = `${role.name}Recommended Models | Roo Code Evals`
const description = `Eval-backed recommendations for ${role.name}. Compare models by success rate, cost, and speed across 5 languages.`
const ogDescription = `${role.name}Recommended Models`
const path = `/evals/workers/${roleId}`
return {
@ -58,7 +58,8 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
},
keywords: [
...SEO.keywords,
"AI engineer",
"AI coding",
"coding agents",
"model recommendations",
"coding evals",
role.name.toLowerCase(),
@ -99,6 +100,7 @@ export default async function RoleCandidatesPage({ params }: PageProps) {
totalExercises={totalExercises}
lastUpdated={lastUpdated}
cloudUrls={cloudUrls}
workersRootPath="/evals/workers"
/>
)
}

View file

@ -8,10 +8,10 @@ import { WorkersContent } from "./workers-content"
// ── SEO Metadata ────────────────────────────────────────────────────────────
const TITLE = "Hire an AI Engineer | Roo Code Evals"
const TITLE = "Build with Roo Code Cloud | Roo Code Evals"
const DESCRIPTION =
"Find the right AI coding model for your team. Compare interview results across Junior, Senior, and Staff Engineer roles."
const OG_DESCRIPTION = "Find the right AI coding model for your team"
"Eval-backed model recommendations for shipping production code. Pick a setup based on the work you're doing: single-file fixes, multi-file changes, review, and autonomous runs."
const OG_DESCRIPTION = "Eval-backed model recommendations for shipping production code"
const PATH = "/evals/workers"
export const metadata: Metadata = {
@ -44,18 +44,20 @@ export const metadata: Metadata = {
},
keywords: [
...SEO.keywords,
"AI engineer",
"AI coding",
"coding agents",
"roo code cloud",
"model recommendations",
"coding evals",
"model comparison",
"hire AI",
"talent marketplace",
"shipping code",
"prototype",
],
}
// ── Page Component ──────────────────────────────────────────────────────────
export default function HireAnAIEngineerPage() {
export default function WorkersPage() {
const roles = getEngineerRoles()
const recommendations = getAllRecommendations()
@ -80,6 +82,10 @@ export default function HireAnAIEngineerPage() {
totalExercises={totalExercises}
totalModels={totalModels}
lastUpdated={lastUpdated}
workersRootPath="/evals/workers"
enableOutcomeLayer={false}
alternateVersionHref="/evals/workers-v2"
alternateVersionLabel="View V2 preview"
/>
)
}

View file

@ -1,6 +1,6 @@
"use client"
import { useMemo } from "react"
import { useCallback, useMemo } from "react"
import { motion } from "framer-motion"
import {
Code,
@ -20,10 +20,12 @@ import {
} from "lucide-react"
import type { LucideIcon } from "lucide-react"
import Link from "next/link"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { ScatterChart, Scatter, XAxis, YAxis, ZAxis, Tooltip, ResponsiveContainer, Cell, ReferenceLine } from "recharts"
import type { EngineerRole, RoleRecommendation } from "@/lib/mock-recommendations"
import { TASKS_PER_DAY, MODEL_TIMELINE } from "@/lib/mock-recommendations"
import { EVAL_OUTCOMES, isEvalOutcomeId, type EvalOutcomeId } from "@/lib/eval-outcomes"
// ── Icon Mapping ────────────────────────────────────────────────────────────
@ -139,6 +141,37 @@ const ROLE_THEMES: Record<string, RoleTheme> = {
const DEFAULT_THEME = ROLE_THEMES.senior!
// ── Outcome Layer: Optimization Modes ──────────────────────────────────────
type EvalOptimizationMode = "best" | "fastest" | "cost"
const OPTIMIZATION_MODES: Array<{
id: EvalOptimizationMode
label: string
description: string
}> = [
{ id: "best", label: "Best", description: "Best overall quality across our eval suite." },
{ id: "fastest", label: "Fastest", description: "Lower latency per task when speed matters." },
{ id: "cost", label: "Most cost-effective", description: "Lower cost per task for high-volume work." },
]
function isEvalOptimizationMode(value: string): value is EvalOptimizationMode {
return value === "best" || value === "fastest" || value === "cost"
}
function getModeCandidate(rec: RoleRecommendation | undefined, mode: EvalOptimizationMode) {
if (!rec) return null
if (mode === "fastest") return rec.speedHire ?? rec.best[0] ?? null
if (mode === "cost") return rec.budgetHire ?? rec.best[0] ?? null
return rec.best[0] ?? null
}
function getModeLabel(mode: EvalOptimizationMode) {
if (mode === "fastest") return "Fastest"
if (mode === "cost") return "Most cost-effective"
return "Best"
}
// ── Framer Motion Variants ──────────────────────────────────────────────────
const containerVariants = {
@ -284,6 +317,10 @@ type WorkersContentProps = {
totalExercises: number
totalModels: number
lastUpdated: string | undefined
workersRootPath?: string
enableOutcomeLayer?: boolean
alternateVersionHref?: string
alternateVersionLabel?: string
}
export function WorkersContent({
@ -293,8 +330,91 @@ export function WorkersContent({
totalExercises,
totalModels,
lastUpdated,
workersRootPath = "/evals/workers",
enableOutcomeLayer = false,
alternateVersionHref,
alternateVersionLabel,
}: WorkersContentProps) {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const selectedOutcomeId = useMemo(() => {
const outcome = searchParams.get("outcome")
if (!outcome) return null
return isEvalOutcomeId(outcome) ? outcome : null
}, [searchParams])
const selectedMode = useMemo((): EvalOptimizationMode => {
const mode = searchParams.get("mode")
if (!mode) return "best"
return isEvalOptimizationMode(mode) ? mode : "best"
}, [searchParams])
const setOutcome = useCallback(
(nextOutcomeId: EvalOutcomeId | null) => {
const params = new URLSearchParams(searchParams.toString())
if (nextOutcomeId) params.set("outcome", nextOutcomeId)
else params.delete("outcome")
const query = params.toString()
router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false })
},
[pathname, router, searchParams],
)
const setMode = useCallback(
(nextMode: EvalOptimizationMode) => {
const params = new URLSearchParams(searchParams.toString())
params.set("mode", nextMode)
const query = params.toString()
router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false })
},
[pathname, router, searchParams],
)
const recByRole = new Map(recommendations.map((r) => [r.roleId, r]))
const roleById = useMemo(() => new Map(roles.map((r) => [r.id, r])), [roles])
const selectedOutcome = useMemo(() => {
if (!selectedOutcomeId) return null
return EVAL_OUTCOMES.find((o) => o.id === selectedOutcomeId) ?? null
}, [selectedOutcomeId])
const setupQuery = useMemo(() => {
if (!enableOutcomeLayer || !selectedOutcomeId) return ""
const params = new URLSearchParams()
params.set("outcome", selectedOutcomeId)
params.set("mode", selectedMode)
const query = params.toString()
return query ? `?${query}` : ""
}, [enableOutcomeLayer, selectedOutcomeId, selectedMode])
const profileTitle = selectedOutcome?.builderProfile?.title ?? "Your Builder Profile"
const profileDescription =
selectedOutcome?.builderProfile?.description ??
"A default setup built from our eval signals. Its a baseline, not a guarantee."
const profileHowItWorks = selectedOutcome?.builderProfile?.howItWorks ?? selectedOutcome?.whyItWorks ?? []
const profileCapabilities = useMemo(() => {
if (!selectedOutcome) return []
const fromProfile = selectedOutcome.builderProfile?.capabilities
if (fromProfile && fromProfile.length > 0) return fromProfile
return selectedOutcome.recommendedRoleIds.map((roleId) => {
const role = roleById.get(roleId)
return {
id: roleId,
name: role?.name ?? roleId,
description: role?.salaryRange ?? "",
roleId,
}
})
}, [selectedOutcome, roleById])
const agentCapabilities = useMemo(() => profileCapabilities.filter((c) => Boolean(c.roleId)), [profileCapabilities])
const builtInCapabilities = useMemo(() => profileCapabilities.filter((c) => !c.roleId), [profileCapabilities])
// ── Timeline scatter data ──────────────────────────────────────────────
const timelineData = useMemo(() => {
@ -340,6 +460,21 @@ export function WorkersContent({
</div>
</motion.div>
{/* Blueprint grid overlay (V2) */}
{enableOutcomeLayer ? (
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-[0.14] mix-blend-multiply dark:opacity-[0.10] dark:mix-blend-screen"
style={{
backgroundImage:
"linear-gradient(to right, hsl(var(--border)) 1px, transparent 1px), linear-gradient(to bottom, hsl(var(--border)) 1px, transparent 1px)",
backgroundSize: "72px 72px",
maskImage: "radial-gradient(circle at 50% 35%, black 10%, transparent 65%)",
WebkitMaskImage: "radial-gradient(circle at 50% 35%, black 10%, transparent 65%)",
}}
/>
) : null}
{/* Gradient fade from hero atmosphere to cards */}
<div className="absolute inset-x-0 bottom-0 z-[1] h-48 bg-gradient-to-b from-transparent via-background/60 to-background" />
@ -351,33 +486,91 @@ export function WorkersContent({
variants={containerVariants}>
{/* Badge */}
<motion.div variants={fadeUpVariants}>
<Link
href="/evals/methodology"
className="group mb-6 inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-300 hover:border-border hover:text-foreground">
<Beaker className="size-4" />
How we interview AI models
<ArrowRight className="size-3.5 transition-transform duration-200 group-hover:translate-x-0.5" />
</Link>
<div className="mb-6 flex flex-wrap items-center justify-center gap-3">
<Link
href="/evals/methodology"
className="group inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-300 hover:border-border hover:text-foreground">
<Beaker className="size-4" />
How we run evals
<ArrowRight className="size-3.5 transition-transform duration-200 group-hover:translate-x-0.5" />
</Link>
{alternateVersionHref && alternateVersionLabel ? (
<Link
href={alternateVersionHref}
className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-background/70 px-4 py-2 text-sm font-medium text-foreground/80 backdrop-blur-sm transition-colors hover:border-border hover:text-foreground">
{alternateVersionLabel}
</Link>
) : null}
</div>
</motion.div>
{enableOutcomeLayer ? (
<motion.p
className="mt-2 text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground/70"
variants={fadeUpVariants}>
Outcomes over artifacts
</motion.p>
) : null}
{/* Heading */}
<motion.h1
className="mt-6 text-5xl font-bold tracking-tight md:text-6xl lg:text-7xl"
className="mt-6 text-5xl font-semibold tracking-tight md:text-6xl lg:text-7xl [font-family:var(--font-display)]"
variants={fadeUpVariants}>
Hire an{" "}
<span className="bg-gradient-to-r from-emerald-500 via-blue-500 to-amber-500 bg-clip-text text-transparent">
AI Engineer
</span>
{enableOutcomeLayer ? (
<>
Build from outcomes.
<br className="hidden sm:block" /> Ship{" "}
<span className="bg-gradient-to-r from-emerald-500 via-blue-500 to-amber-500 bg-clip-text text-transparent">
real code
</span>
.
</>
) : (
<>
Build with{" "}
<span className="bg-gradient-to-r from-emerald-500 via-blue-500 to-amber-500 bg-clip-text text-transparent">
Roo Code Cloud
</span>
</>
)}
</motion.h1>
{/* Subheading */}
<motion.p
className="mt-6 text-lg leading-relaxed text-muted-foreground md:text-xl"
variants={fadeUpVariants}>
Every model runs the same coding tasks, same tools, same time limit. Pick the right
candidate for your team and budget.
{enableOutcomeLayer ? (
<>
Pick what you&apos;re trying to ship. We assemble a Builder Profile: the
capabilities you need, plus a default model recommendation backed by eval data.
</>
) : (
<>
Outcomes over artifacts: start from the production codebase and ship as a reviewable
PR. Every model runs the same tasks, same tools, and the same time limit. Your repo
will differtreat this as a baseline.
</>
)}
</motion.p>
{enableOutcomeLayer ? (
<motion.div
className="mt-8 flex flex-wrap items-center justify-center gap-3"
variants={fadeUpVariants}>
<Link
href={`${workersRootPath}?outcome=prototype_to_pr&mode=${selectedMode}`}
className="inline-flex items-center gap-2 rounded-full border border-foreground/20 bg-foreground/5 px-5 py-2.5 text-sm font-semibold text-foreground transition-colors hover:bg-foreground/10">
Start with Prototype PR
<ArrowRight className="size-4" />
</Link>
<a
href="#outcomes"
className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-card/40 px-5 py-2.5 text-sm font-semibold text-muted-foreground transition-colors hover:border-border hover:text-foreground">
Browse outcomes
</a>
</motion.div>
) : null}
{/* Stats bar */}
<motion.div
className="mt-10 flex flex-wrap items-center justify-center gap-x-8 gap-y-3 rounded-2xl border border-border/50 bg-card/30 px-6 py-4 backdrop-blur-sm"
@ -394,171 +587,417 @@ export function WorkersContent({
</div>
</section>
{/* ── Role Cards Grid ────────────────────────────────────────── */}
<section className="relative -mt-12 overflow-hidden pb-24">
{/* Subtle section background */}
<motion.div
className="absolute inset-0"
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={backgroundVariants}>
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2">
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-[100%] bg-foreground/[0.02] dark:bg-foreground/[0.03] blur-[100px]" />
</div>
</motion.div>
{/* ── Outcomes Overlay ───────────────────────────────────────── */}
{enableOutcomeLayer ? (
<section id="outcomes" className="relative -mt-24 overflow-hidden pb-14 scroll-mt-24">
<div className="container relative z-10 mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
<motion.div
className="mx-auto max-w-4xl"
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={containerVariants}>
<motion.div className="text-center" variants={fadeUpVariants}>
<h2 className="text-2xl font-semibold tracking-tight md:text-3xl [font-family:var(--font-display)]">
Start with an outcome
</h2>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground md:text-base">
Pick what you&apos;re trying to ship. We assemble a Builder Profile: capabilities
plus a default model recommendation. It&apos;s a baseline, not a guarantee.
</p>
</motion.div>
<div className="container relative z-10 mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
{/* Section connector */}
<motion.div
className="mb-10 flex flex-col items-center gap-2"
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={fadeUpVariants}>
<p className="text-sm font-medium uppercase tracking-widest text-muted-foreground/70">
Choose your agentic team members
</p>
<ChevronDown className="size-4 text-muted-foreground/40" />
</motion.div>
<motion.div
className="mt-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"
variants={containerVariants}>
{EVAL_OUTCOMES.map((outcome) => {
const Icon = outcome.icon
const isSelected = outcome.id === selectedOutcomeId
const isFeatured = outcome.id === "prototype_to_pr"
<motion.div
className="grid grid-cols-1 gap-6 md:grid-cols-3 md:grid-rows-[repeat(7,auto)] md:gap-x-6 md:gap-y-10 lg:gap-x-8"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}>
{roles.map((role) => {
const rec = recByRole.get(role.id)
const IconComponent = ICON_MAP[role.icon] ?? Code
const candidateCount = rec?.allCandidates.length ?? 0
const exerciseCount = rec?.totalExercises ?? 0
const theme = ROLE_THEMES[role.id] ?? DEFAULT_THEME
const topModel = rec?.best[0]
return (
<motion.div
key={role.id}
variants={cardVariants}
className="md:row-span-7 md:grid md:grid-rows-subgrid">
<div
className={`group relative flex h-full flex-col rounded-2xl border border-border/50 bg-card/50 backdrop-blur-sm transition-all duration-300 md:row-span-7 md:grid md:grid-rows-subgrid ${theme.borderHover} ${theme.shadowHover} hover:shadow-xl`}>
{/* Subtle glow on hover */}
<div
className={`absolute inset-0 rounded-2xl ${theme.glowColor} opacity-0 transition-opacity duration-300 group-hover:opacity-100`}
/>
<div className="relative z-10 flex h-full flex-col p-6 lg:p-7 md:row-span-7 md:grid md:grid-rows-subgrid">
{/* Header: Icon + role badge */}
<div className="flex items-start justify-between">
<div
className={`flex size-12 items-center justify-center rounded-xl ${theme.iconBg} ${theme.iconText}`}>
<IconComponent className="size-6" />
return (
<motion.button
key={outcome.id}
type="button"
variants={cardVariants}
aria-pressed={isSelected}
onClick={() => setOutcome(isSelected ? null : outcome.id)}
className={[
"group rounded-2xl border bg-card/40 p-5 text-left backdrop-blur-sm transition-all duration-200 hover:bg-card/60",
isSelected
? "border-foreground/20 ring-1 ring-foreground/15"
: "border-border/50 hover:border-border",
isFeatured ? "lg:col-span-2" : "",
].join(" ")}>
<div className="flex items-start gap-3">
<div className="flex size-10 items-center justify-center rounded-xl border border-border/50 bg-background/30">
<Icon className="size-5 text-foreground/70" />
</div>
{topModel && (
<span
className={`rounded-full ${theme.badgeBg} ${theme.badgeText} px-3 py-1 text-xs font-medium`}>
Top: {topModel.displayName}
</span>
)}
</div>
{/* Role name + salary */}
<h2 className="mt-5 text-2xl font-bold tracking-tight">{role.name}</h2>
<p
className={`mt-1 font-mono text-lg font-semibold ${theme.accentLight} ${theme.accentDark}`}>
{role.salaryRange}
</p>
{/* Description */}
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
{role.description}
</p>
{/* Best for */}
<div className="mt-5">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Best for
</h3>
<div className="mt-2 flex flex-wrap gap-1.5">
{role.bestFor.map((item) => (
<span
key={item}
className="rounded-md border border-border/50 bg-muted/50 px-2 py-0.5 text-xs text-foreground/70">
{item}
<div className="min-w-0 flex-1">
{isFeatured ? (
<span className="mb-1 inline-flex items-center rounded-full border border-foreground/15 bg-foreground/5 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-foreground/70">
Recommended starting point
</span>
))}
) : null}
<p className="text-sm font-semibold text-foreground">
{outcome.name}
</p>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
{outcome.description}
</p>
</div>
</div>
</motion.button>
)
})}
</motion.div>
{/* Strengths & Weaknesses side by side */}
<div className="mt-5 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-2">
{/* Strengths */}
<div>
<h3
className={`text-xs font-semibold uppercase tracking-wider ${theme.strengthColor}`}>
Strengths
</h3>
<ul className="mt-2 space-y-1.5">
{role.strengths.map((item) => (
<li
key={item}
className="flex items-start gap-1.5 text-xs text-foreground/75">
<CheckCircle2
className={`mt-0.5 size-3 shrink-0 ${theme.strengthColor}`}
/>
{item}
{selectedOutcome ? (
<motion.div
className="mt-6 rounded-2xl border border-border/50 bg-card/30 p-5 backdrop-blur-sm"
variants={fadeUpVariants}>
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div className="min-w-0 md:max-w-[44%]">
<p className="text-xs font-medium uppercase tracking-widest text-muted-foreground/70">
{profileTitle}
</p>
<p className="mt-2 text-sm font-semibold text-foreground">
{selectedOutcome.name}
</p>
<p className="mt-2 text-xs leading-relaxed text-muted-foreground">
{profileDescription}
</p>
{profileHowItWorks.length > 0 ? (
<div className="mt-4">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
{selectedOutcome.builderProfile
? "How it works"
: "Why it works"}
</p>
<ul className="mt-2 space-y-1 text-xs text-muted-foreground">
{profileHowItWorks.map((line) => (
<li key={line} className="flex items-start gap-2">
<span className="mt-1 size-1.5 shrink-0 rounded-full bg-muted-foreground/40" />
<span className="min-w-0">{line}</span>
</li>
))}
</ul>
</div>
) : null}
{/* Weaknesses */}
<div>
<h3 className="text-xs font-semibold uppercase tracking-wider text-amber-700 dark:text-amber-400">
Trade-offs
</h3>
<ul className="mt-2 space-y-1.5">
{role.weaknesses.map((item) => (
<li
key={item}
className="flex items-start gap-1.5 text-xs text-foreground/60">
<AlertTriangle className="mt-0.5 size-3 shrink-0 text-amber-600/70 dark:text-amber-400/70" />
{item}
{selectedOutcome.builderProfile?.howItWorks ? (
<div className="mt-4">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Why it works
</p>
<ul className="mt-2 space-y-1 text-xs text-muted-foreground">
{selectedOutcome.whyItWorks.map((line) => (
<li key={line} className="flex items-start gap-2">
<span className="mt-1 size-1.5 shrink-0 rounded-full bg-muted-foreground/40" />
<span className="min-w-0">{line}</span>
</li>
))}
</ul>
</div>
</div>
) : null}
</div>
{/* Bottom stats + CTA */}
<div className="mt-auto pt-6">
<div className="mb-4 flex items-center gap-4 border-t border-border/30 pt-4 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<Users className="size-3.5" />
{candidateCount} candidates
</span>
<span className="inline-flex items-center gap-1.5">
<FlaskConical className="size-3.5" />
{exerciseCount.toLocaleString()} exercises
<div className="flex flex-1 flex-col gap-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-medium text-muted-foreground">
Optimize for
</span>
{OPTIMIZATION_MODES.map((mode) => {
const isSelected = mode.id === selectedMode
return (
<button
key={mode.id}
type="button"
aria-pressed={isSelected}
title={mode.description}
onClick={() => setMode(mode.id)}
className={[
"inline-flex items-center rounded-full border px-3 py-1.5 text-xs font-semibold transition-colors",
isSelected
? "border-foreground/20 bg-foreground/5 text-foreground"
: "border-border/50 bg-background/20 text-foreground/75 hover:border-border hover:text-foreground",
].join(" ")}>
{mode.label}
</button>
)
})}
</div>
<Link
href={`/evals/workers/${role.id}`}
className={`inline-flex w-full items-center justify-center gap-2 rounded-xl ${theme.buttonBg} ${theme.buttonHover} px-4 py-3 text-sm font-semibold text-white transition-all duration-200 hover:scale-[1.02] active:scale-[0.98]`}>
View Candidates
<ArrowRight className="size-4" />
</Link>
<span className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/60">
Capability set
</span>
</div>
<div>
<div className="flex items-end justify-between gap-3">
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Agents
</p>
<p className="text-xs text-muted-foreground">
Click for candidates &amp; settings
</p>
</div>
<div className="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2">
{agentCapabilities.map((capability) => {
const roleId = capability.roleId!
const rec = recByRole.get(roleId)
const candidate = getModeCandidate(rec, selectedMode)
return (
<Link
key={capability.id}
href={`${workersRootPath}/${roleId}${setupQuery}`}
className="group rounded-xl border border-border/50 bg-background/20 p-3 text-left transition-colors hover:border-border hover:bg-background/30">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-xs font-semibold text-foreground">
{capability.name}
</p>
{capability.description ? (
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
{capability.description}
</p>
) : null}
<p className="mt-1 text-xs text-muted-foreground">
{candidate ? (
<>
<span className="font-medium text-foreground/80">
{getModeLabel(selectedMode)}:
</span>{" "}
{candidate.displayName}
</>
) : (
<span className="font-medium text-foreground/80">
View models
</span>
)}
</p>
</div>
<ArrowRight className="mt-0.5 size-3.5 shrink-0 text-muted-foreground/70 transition-transform group-hover:translate-x-0.5" />
</div>
</Link>
)
})}
</div>
</div>
{builtInCapabilities.length > 0 ? (
<div>
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Built-ins
</p>
<div className="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2">
{builtInCapabilities.map((capability) => (
<div
key={capability.id}
className="rounded-xl border border-border/50 bg-background/20 p-3">
<div className="flex items-start gap-2">
<CheckCircle2 className="mt-0.5 size-3.5 shrink-0 text-foreground/60" />
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<p className="text-xs font-semibold text-foreground">
{capability.name}
</p>
<span className="rounded-full border border-border/50 bg-background/30 px-2 py-0.5 text-[10px] font-semibold text-foreground/70">
Built-in
</span>
</div>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
{capability.description}
</p>
</div>
</div>
</div>
))}
</div>
</div>
) : null}
</div>
</div>
</motion.div>
)
})}
) : null}
</motion.div>
</div>
</section>
) : null}
{/* ── Role Cards Grid (baseline only) ────────────────────────── */}
{!enableOutcomeLayer ? (
<section className="relative -mt-12 overflow-hidden pb-24">
{/* Subtle section background */}
<motion.div
className="absolute inset-0"
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={backgroundVariants}>
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2">
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-[100%] bg-foreground/[0.02] dark:bg-foreground/[0.03] blur-[100px]" />
</div>
</motion.div>
</div>
</section>
<div className="container relative z-10 mx-auto max-w-screen-lg px-4 sm:px-6 lg:px-8">
{/* Section connector */}
<motion.div
className="mb-10 flex flex-col items-center gap-2"
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={fadeUpVariants}>
<p className="text-sm font-medium uppercase tracking-widest text-muted-foreground/70">
Choose a setup for the work
</p>
<ChevronDown className="size-4 text-muted-foreground/40" />
</motion.div>
<motion.div
className="grid grid-cols-1 gap-6 md:grid-cols-3 md:grid-rows-[repeat(7,auto)] md:gap-x-6 md:gap-y-10 lg:gap-x-8"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}>
{roles.map((role) => {
const rec = recByRole.get(role.id)
const IconComponent = ICON_MAP[role.icon] ?? Code
const candidateCount = rec?.allCandidates.length ?? 0
const exerciseCount = rec?.totalExercises ?? 0
const theme = ROLE_THEMES[role.id] ?? DEFAULT_THEME
const topModel = rec?.best[0] ?? null
return (
<motion.div
key={role.id}
variants={cardVariants}
className="md:row-span-7 md:grid md:grid-rows-subgrid">
<div
className={[
"group relative flex h-full flex-col rounded-2xl border border-border/50 bg-card/50 backdrop-blur-sm transition-all duration-300 md:row-span-7 md:grid md:grid-rows-subgrid hover:shadow-xl",
theme.borderHover,
theme.shadowHover,
].join(" ")}>
{/* Subtle glow on hover */}
<div
className={`absolute inset-0 rounded-2xl ${theme.glowColor} opacity-0 transition-opacity duration-300 group-hover:opacity-100`}
/>
<div className="relative z-10 flex h-full flex-col p-6 lg:p-7 md:row-span-7 md:grid md:grid-rows-subgrid">
{/* Header: Icon + role badge */}
<div className="flex items-start justify-between">
<div
className={`flex size-12 items-center justify-center rounded-xl ${theme.iconBg} ${theme.iconText}`}>
<IconComponent className="size-6" />
</div>
{topModel && (
<span
className={`rounded-full ${theme.badgeBg} ${theme.badgeText} px-3 py-1 text-xs font-medium`}>
Top: {topModel.displayName}
</span>
)}
</div>
{/* Profile name + descriptor */}
<h2 className="mt-5 text-2xl font-bold tracking-tight">{role.name}</h2>
<p
className={`mt-1 font-mono text-lg font-semibold ${theme.accentLight} ${theme.accentDark}`}>
{role.salaryRange}
</p>
{/* Description */}
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
{role.description}
</p>
{/* Best for */}
<div className="mt-5">
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Best for
</h3>
<div className="mt-2 flex flex-wrap gap-1.5">
{role.bestFor.map((item) => (
<span
key={item}
className="rounded-md border border-border/50 bg-muted/50 px-2 py-0.5 text-xs text-foreground/70">
{item}
</span>
))}
</div>
</div>
{/* Strengths & Weaknesses side by side */}
<div className="mt-5 grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-2">
{/* Strengths */}
<div>
<h3
className={`text-xs font-semibold uppercase tracking-wider ${theme.strengthColor}`}>
Strengths
</h3>
<ul className="mt-2 space-y-1.5">
{role.strengths.map((item) => (
<li
key={item}
className="flex items-start gap-1.5 text-xs text-foreground/75">
<CheckCircle2
className={`mt-0.5 size-3 shrink-0 ${theme.strengthColor}`}
/>
{item}
</li>
))}
</ul>
</div>
{/* Weaknesses */}
<div>
<h3 className="text-xs font-semibold uppercase tracking-wider text-amber-700 dark:text-amber-400">
Trade-offs
</h3>
<ul className="mt-2 space-y-1.5">
{role.weaknesses.map((item) => (
<li
key={item}
className="flex items-start gap-1.5 text-xs text-foreground/60">
<AlertTriangle className="mt-0.5 size-3 shrink-0 text-amber-600/70 dark:text-amber-400/70" />
{item}
</li>
))}
</ul>
</div>
</div>
{/* Bottom stats + CTA */}
<div className="mt-auto pt-6">
<div className="mb-4 flex items-center gap-4 border-t border-border/30 pt-4 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<Users className="size-3.5" />
{candidateCount} models
</span>
<span className="inline-flex items-center gap-1.5">
<FlaskConical className="size-3.5" />
{exerciseCount.toLocaleString()} exercises
</span>
</div>
<Link
href={`${workersRootPath}/${role.id}${setupQuery}`}
className={`inline-flex w-full items-center justify-center gap-2 rounded-xl ${theme.buttonBg} ${theme.buttonHover} px-4 py-3 text-sm font-semibold text-white transition-all duration-200 hover:scale-[1.02] active:scale-[0.98]`}>
View models
<ArrowRight className="size-4" />
</Link>
</div>
</div>
</div>
</motion.div>
)
})}
</motion.div>
</div>
</section>
) : null}
{/* ── AI Coding Capability Over Time ─────────────────────────── */}
<section className="relative overflow-hidden pb-24 pt-8">
@ -582,7 +1021,7 @@ export function WorkersContent({
variants={containerVariants}>
{/* Section header */}
<motion.div className="mb-8 text-center" variants={fadeUpVariants}>
<h2 className="text-3xl font-bold tracking-tight md:text-4xl">
<h2 className="text-3xl font-semibold tracking-tight md:text-4xl [font-family:var(--font-display)]">
AI Coding Capability{" "}
<span className="bg-gradient-to-r from-emerald-500 to-blue-500 bg-clip-text text-transparent">
Over Time

View file

@ -0,0 +1,171 @@
import type { LucideIcon } from "lucide-react"
import { Bug, CheckCircle2, GitPullRequest, Sparkles, Workflow } from "lucide-react"
export type EvalOutcomeId =
| "prototype_to_pr"
| "paper_cuts"
| "sentry_triage"
| "repro_to_fix"
| "review_guardrails"
| "issue_to_pr"
export type EvalOutcomeCapability = {
id: string
name: string
description: string
/**
* Optional roleId for capabilities that map directly to a role page.
* Non-role capabilities represent Roo Code Cloud behaviors (validation, PR packaging, etc.).
*/
roleId?: string
}
export type EvalOutcomeProfile = {
title: string
description: string
capabilities: EvalOutcomeCapability[]
howItWorks: string[]
}
export type EvalOutcome = {
id: EvalOutcomeId
name: string
description: string
icon: LucideIcon
/**
* Ordered list of roleIds to suggest as a "setup".
* Keep roleIds stable even if display names evolve.
*/
recommendedRoleIds: string[]
whyItWorks: string[]
/**
* Optional profile details used to render a more comprehensive exoskeleton
* for an outcome. Start with the most important outcomes and expand over time.
*/
builderProfile?: EvalOutcomeProfile
}
export const EVAL_OUTCOMES: EvalOutcome[] = [
{
id: "prototype_to_pr",
name: "Prototype → PR",
description: "Build a working prototype on the production codebase, then turn it into a reviewable diff.",
icon: Sparkles,
recommendedRoleIds: ["senior", "reviewer"],
whyItWorks: [
"Multi-file changes with a reviewer pass for coherence and edge cases.",
"Optimizes for shipping, not slides.",
],
builderProfile: {
title: "Your Builder Profile",
description:
"A default set of capabilities for turning a working prototype into a reviewable PR—on the production codebase.",
capabilities: [
{
id: "multi_file_builder",
name: "Multi-file Builder",
description: "Builds the prototype directly in your repo across the files it touches.",
roleId: "senior",
},
{
id: "reviewer_guardrails",
name: "Reviewer & Guardrails",
description: "Reviews the diff for correctness, edge cases, and coherence before you merge.",
roleId: "reviewer",
},
{
id: "environment_setup",
name: "Environment setup",
description:
"Bootstraps a working dev environment and runs the workflow without you fighting Git, installs, or tests.",
},
{
id: "validation_loop",
name: "Validation loop",
description: "Runs tests/lint/typechecks and iterates until its clean (or flags whats blocked).",
},
{
id: "pr_ready_output",
name: "PR-ready output",
description: "Produces a focused diff plus a plain-English summary and review notes.",
},
{
id: "straight_line_merge",
name: "Straight-line to merge",
description:
"No export/import step: the work is already on the production codebase, so merge is a straight line.",
},
{
id: "scope_control",
name: "Scope control",
description: "Keeps diffs tight: smaller review surface, fewer surprises, and easier merges.",
},
],
howItWorks: [
"Build a working prototype directly in the production codebase.",
"Convert the prototype into a tight diff (tests, cleanup, and safeguards).",
"Run a reviewer pass to catch edge cases and improve merge confidence.",
"Deliver a PR-ready result with context and next steps.",
],
},
},
{
id: "paper_cuts",
name: "Paper cuts & small fixes",
description: "Fix the small stuff without dragging engineers off big projects.",
icon: CheckCircle2,
recommendedRoleIds: ["junior", "reviewer"],
whyItWorks: [
"Small diffs are high-leverage when the work is well-scoped.",
"Reviewer keeps the quality bar and reduces surprise.",
],
},
{
id: "sentry_triage",
name: "Sentry triage",
description: "Turn recurring errors into concrete fixes with proof before review.",
icon: Bug,
recommendedRoleIds: ["autonomous", "reviewer"],
whyItWorks: [
"Autonomous runs handle multi-step investigation and iteration.",
"Reviewer focuses on safety, correctness, and “does this hold up?”.",
],
},
{
id: "repro_to_fix",
name: "Bug repro → fix",
description: "Make the handoff less lossy: reproduce, patch, and validate in one loop.",
icon: Workflow,
recommendedRoleIds: ["senior", "reviewer"],
whyItWorks: [
"Good default for ambiguous bugs that touch a few files.",
"Reviewer helps catch cross-team assumptions early.",
],
},
{
id: "review_guardrails",
name: "Guardrails & review",
description: "Raise the quality bar without becoming the blocker.",
icon: GitPullRequest,
recommendedRoleIds: ["reviewer"],
whyItWorks: [
"Works alongside CI, linters, and team review.",
"Scales judgement through fast, consistent feedback.",
],
},
{
id: "issue_to_pr",
name: "Issue → PR",
description: "Run end-to-end work in the background and come back to a reviewable result.",
icon: GitPullRequest,
recommendedRoleIds: ["autonomous", "reviewer"],
whyItWorks: [
"Handles out-of-band work while humans stay on the roadmap.",
"Pairs autonomy with guardrails for merge safety.",
],
},
]
export function isEvalOutcomeId(value: string): value is EvalOutcomeId {
return EVAL_OUTCOMES.some((o) => o.id === value)
}

View file

@ -1,7 +1,7 @@
// ---------------------------------------------------------------------------
// Eval Recommendations: Types + Mock Data (S1.1a)
// ---------------------------------------------------------------------------
// This file defines the API contract for the AI Engineer Talent Marketplace.
// This file defines the API contract for the /evals/workers recommendation pages.
// The backend (Sprint 3-4) will produce data matching these exact types.
// ---------------------------------------------------------------------------
@ -16,11 +16,11 @@ export const TASKS_PER_DAY = 80
// ── Types ──────────────────────────────────────────────────────────────────
/** Engineer role definition: maps task complexity to a hiring tier. */
/** Engineer role definition: maps task complexity to a recommendation tier. */
export type EngineerRole = {
id: string
name: string
/** Daily salary range string, e.g. "$338/day" */
/** Short descriptor shown under the profile name (scope, mode, etc.). */
salaryRange: string
description: string
bestFor: string[]
@ -80,51 +80,51 @@ export type RoleRecommendation = {
const ENGINEER_ROLES: EngineerRole[] = [
{
id: "junior",
name: "Junior Engineer",
salaryRange: "$210/day",
name: "Single-file Builder",
salaryRange: "Scope: single-file",
description:
"Handles well-scoped, single-file tasks: boilerplate, simple bug fixes, and test generation at the lowest cost per task.",
bestFor: ["Single-file fixes", "Boilerplate generation", "Test generation", "Simple implementations"],
strengths: ["Cheap", "High throughput", "Best cost-to-quality ratio on simple tasks"],
weaknesses: ["Struggles with multi-file changes", "Limited reasoning depth", "May miss edge cases"],
"Best for tight diffs: boilerplate, small fixes, and test updates. Great when the work is clear and bounded.",
bestFor: ["Small fixes", "Boilerplate", "Test updates", "Simple implementations"],
strengths: ["Fast iteration", "Stays close to the requested change", "Great for well-scoped diffs"],
weaknesses: ["Not ideal for cross-cutting work", "Can miss edge cases in complex systems"],
icon: "Code",
},
{
id: "senior",
name: "Senior Engineer",
salaryRange: "$1026/day",
name: "Multi-file Builder",
salaryRange: "Scope: multi-file",
description:
"The sweet spot for most engineering work. Senior-tier models balance cost and quality across multi-file refactors, feature development, and debugging.",
bestFor: ["Multi-file refactors", "Feature development", "Debugging", "Code review"],
"For most day-to-day shipping: feature work across a few files, refactors, and debugging with solid consistency.",
bestFor: ["Feature work", "Multi-file refactors", "Debugging", "Integrations"],
strengths: [
"Balanced cost/quality",
"Handles multi-file changes and cross-cutting refactors",
"Consistent pass rates across all five languages",
"Reliable for common product work",
"Handles multi-file changes and dependencies",
"Consistent across all five languages",
],
weaknesses: ["More expensive than junior", "Overkill for trivial tasks"],
weaknesses: ["Overkill for trivial diffs", "May need help on cross-cutting architecture"],
icon: "GitBranch",
},
{
id: "staff",
name: "Staff Engineer",
salaryRange: "$834/day",
name: "Architecture & Refactor",
salaryRange: "Scope: cross-cutting",
description:
"For architecture decisions, system design, and complex refactors. Staff-tier models handle ambiguous requirements and cross-cutting changes where other tiers fail.",
bestFor: ["Architecture decisions", "Complex features", "System design", "Ambiguous requirements"],
"For ambiguity and cross-cutting changes: architecture decisions, complex refactors, and work where correctness matters more than speed.",
bestFor: ["Complex refactors", "Architecture changes", "Ambiguous requirements", "System design"],
strengths: [
"Handles multi-step reasoning and ambiguous specs",
"Passes existing test suites consistently",
"Resolves underspecified requirements",
"Strong multi-step reasoning",
"Good at navigating bigger codebases",
"Better at making safe, coherent changes",
],
weaknesses: ["Most expensive", "Overkill for simple tasks", "Diminishing returns on easy work"],
weaknesses: ["Overkill for simple diffs", "Still needs human review before merge"],
icon: "Building2",
},
{
id: "reviewer",
name: "Architecture Reviewer",
salaryRange: "$1540/day",
name: "Reviewer & Guardrails",
salaryRange: "Mode: review",
description:
"For code review, PR feedback, security analysis, and design critique. Reviewer-tier models catch issues other models miss and provide actionable, context-aware suggestions.",
"For PR feedback, security review, and design critique. Use this to improve quality and reduce surprises before merge.",
bestFor: ["Code review", "PR feedback", "Security analysis", "Design critique", "Refactor guidance"],
strengths: [
"Catches subtle bugs and logic errors",
@ -132,18 +132,18 @@ const ENGINEER_ROLES: EngineerRole[] = [
"Understands cross-file impact of changes",
],
weaknesses: [
"Not for writing code from scratch",
"More expensive than running linters",
"Not for writing features end-to-end",
"Not a replacement for CI and linters",
"Review quality varies by codebase size",
],
icon: "Search",
},
{
id: "autonomous",
name: "Autonomous Agent",
salaryRange: "$530/day",
name: "Autonomous Delivery",
salaryRange: "Mode: end-to-end",
description:
"For issue-to-PR workflows, long-running tasks, and multi-step debugging with minimal supervision. Autonomous-tier models complete tasks end-to-end and recover from errors without human intervention.",
"For issue-to-PR workflows and long-running tasks. Best when you want an agent to run, iterate, and bring back a reviewable result.",
bestFor: [
"Issue-to-PR workflows",
"Multi-step debugging",