mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
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:
parent
417abeb691
commit
5a57ccf8f8
10 changed files with 4157 additions and 0 deletions
|
|
@ -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'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">
|
||||
90–95%
|
||||
</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">
|
||||
60–80%
|
||||
</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">
|
||||
30–50%
|
||||
</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 0–100 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">
|
||||
70–84
|
||||
</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">
|
||||
50–69
|
||||
</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">
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
58
apps/web-roo-code/src/app/evals/methodology/page.tsx
Normal file
58
apps/web-roo-code/src/app/evals/methodology/page.tsx
Normal 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
|
|
@ -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 (0–100). */
|
||||
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 (0–100). */
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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} />
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
104
apps/web-roo-code/src/app/evals/workers/[roleId]/page.tsx
Normal file
104
apps/web-roo-code/src/app/evals/workers/[roleId]/page.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
85
apps/web-roo-code/src/app/evals/workers/page.tsx
Normal file
85
apps/web-roo-code/src/app/evals/workers/page.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
519
apps/web-roo-code/src/app/evals/workers/workers-content.tsx
Normal file
519
apps/web-roo-code/src/app/evals/workers/workers-content.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
866
apps/web-roo-code/src/lib/mock-recommendations.ts
Normal file
866
apps/web-roo-code/src/lib/mock-recommendations.ts
Normal 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. "~$3–38/day" */
|
||||
salaryRange: string
|
||||
description: string
|
||||
bestFor: string[]
|
||||
strengths: string[]
|
||||
weaknesses: string[]
|
||||
icon: string
|
||||
}
|
||||
|
||||
/** Language-specific eval scores (0–100). */
|
||||
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: "~$2–10/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: "~$10–26/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: "~$8–34/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: "~$15–40/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: "~$5–30/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()}`
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue