mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Add objective deep-dive pages under eval recommendations
This commit is contained in:
parent
1d03382f26
commit
268b183dee
14 changed files with 1026 additions and 56 deletions
|
|
@ -0,0 +1,631 @@
|
|||
"use client"
|
||||
|
||||
import { useCallback, useMemo, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { motion } from "framer-motion"
|
||||
import { ArrowRight, BarChart3, Copy, ExternalLink, SlidersHorizontal, Sparkles, Workflow } from "lucide-react"
|
||||
import { Cell, ResponsiveContainer, Scatter, ScatterChart, Tooltip, XAxis, YAxis, ZAxis } from "recharts"
|
||||
|
||||
import type { RoleRecommendation, ModelCandidate } from "@/lib/mock-recommendations"
|
||||
import { getCloudSetupUrl } from "@/lib/mock-recommendations"
|
||||
|
||||
type EvalOptimizationMode = "best" | "fastest" | "cost"
|
||||
|
||||
type ObjectiveDeepDive = {
|
||||
id: string
|
||||
slug: string
|
||||
name: string
|
||||
description: string
|
||||
whyItWorks: string[]
|
||||
recommendedRoleIds: string[]
|
||||
builderProfile?: {
|
||||
title: string
|
||||
description: string
|
||||
examplePrompt?: string
|
||||
howItWorks: string[]
|
||||
}
|
||||
}
|
||||
|
||||
type ObjectiveRoleRec = {
|
||||
roleId: string
|
||||
recommendation: RoleRecommendation
|
||||
}
|
||||
|
||||
type Props = {
|
||||
objective: ObjectiveDeepDive
|
||||
initialMode: EvalOptimizationMode
|
||||
recs: ObjectiveRoleRec[]
|
||||
}
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: { staggerChildren: 0.08, delayChildren: 0.08 },
|
||||
},
|
||||
}
|
||||
|
||||
const fadeUp = {
|
||||
hidden: { opacity: 0, y: 14 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.55, ease: [0.21, 0.45, 0.27, 0.9] as const } },
|
||||
}
|
||||
|
||||
function isEvalOptimizationMode(value: string): value is EvalOptimizationMode {
|
||||
return value === "best" || value === "fastest" || value === "cost"
|
||||
}
|
||||
|
||||
function pickCandidate(rec: RoleRecommendation | undefined, mode: EvalOptimizationMode): ModelCandidate | null {
|
||||
if (!rec) return null
|
||||
if (mode === "fastest") return rec.speedHire ?? rec.best[0] ?? null
|
||||
if (mode === "cost") return rec.budgetHire ?? rec.best[0] ?? null
|
||||
return rec.best[0] ?? null
|
||||
}
|
||||
|
||||
function shortProvider(provider: string) {
|
||||
switch (provider) {
|
||||
case "openai":
|
||||
return "OpenAI"
|
||||
case "anthropic":
|
||||
return "Anthropic"
|
||||
case "google":
|
||||
return "Google"
|
||||
case "xai":
|
||||
return "xAI"
|
||||
case "deepseek":
|
||||
return "DeepSeek"
|
||||
case "moonshot":
|
||||
return "Moonshot"
|
||||
default:
|
||||
return provider
|
||||
}
|
||||
}
|
||||
|
||||
function formatDollars(value: number) {
|
||||
if (!Number.isFinite(value)) return "—"
|
||||
return `$${Math.round(value)}`
|
||||
}
|
||||
|
||||
function formatSeconds(value: number) {
|
||||
if (!Number.isFinite(value)) return "—"
|
||||
return `${value.toFixed(1)}s`
|
||||
}
|
||||
|
||||
function buildObjectiveQueryString(
|
||||
searchParams: { get(name: string): string | null },
|
||||
objectiveSlug: string,
|
||||
mode: EvalOptimizationMode,
|
||||
) {
|
||||
const params = new URLSearchParams()
|
||||
params.set("objective", objectiveSlug)
|
||||
params.set("mode", mode)
|
||||
|
||||
// Preserve any existing query bits we might add later without breaking URLs.
|
||||
const view = searchParams.get("view")
|
||||
if (view) params.set("view", view)
|
||||
|
||||
return `?${params.toString()}`
|
||||
}
|
||||
|
||||
type ObjectiveTooltipPayloadEntry = { payload?: unknown }
|
||||
|
||||
function ObjectiveTooltip({ active, payload }: { active?: boolean; payload?: ObjectiveTooltipPayloadEntry[] }) {
|
||||
if (!active || !payload?.length) return null
|
||||
const entryPayload = payload[0]?.payload
|
||||
const p = entryPayload as
|
||||
| {
|
||||
name?: string
|
||||
provider?: string
|
||||
score?: number
|
||||
dailyCost?: number
|
||||
successRate?: number
|
||||
}
|
||||
| undefined
|
||||
if (!p) return null
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border/50 bg-card/95 p-3 shadow-2xl backdrop-blur-md">
|
||||
<p className="text-sm font-semibold tracking-tight">{p.name}</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{shortProvider(p.provider ?? "")} · <span className="font-mono">{p.score}</span> score ·{" "}
|
||||
<span className="font-mono">{p.successRate}%</span> success
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
Est daily cost <span className="font-mono">{formatDollars(p.dailyCost ?? NaN)}</span>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ObjectiveContent({ objective, initialMode, recs }: Props) {
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const recByRole = useMemo(() => new Map(recs.map((r) => [r.roleId, r.recommendation])), [recs])
|
||||
const lineup = useMemo(() => objective.recommendedRoleIds.filter((id) => recByRole.has(id)), [objective, recByRole])
|
||||
|
||||
const [mode, setMode] = useState<EvalOptimizationMode>(() => {
|
||||
const m = searchParams.get("mode")
|
||||
if (m && isEvalOptimizationMode(m)) return m
|
||||
return initialMode
|
||||
})
|
||||
|
||||
const [overrides, setOverrides] = useState<Record<string, string>>({})
|
||||
|
||||
const selectedByRole = useMemo(() => {
|
||||
const next = new Map<string, ModelCandidate | null>()
|
||||
for (const roleId of lineup) {
|
||||
const rec = recByRole.get(roleId)
|
||||
const overrideModelId = overrides[roleId]
|
||||
if (overrideModelId && rec) {
|
||||
next.set(roleId, rec.allCandidates.find((c) => c.modelId === overrideModelId) ?? null)
|
||||
} else {
|
||||
next.set(roleId, pickCandidate(rec, mode))
|
||||
}
|
||||
}
|
||||
return next
|
||||
}, [lineup, mode, overrides, recByRole])
|
||||
|
||||
const primaryRoleId = lineup[0] ?? null
|
||||
const primaryCandidate = primaryRoleId ? (selectedByRole.get(primaryRoleId) ?? null) : null
|
||||
|
||||
const modePill = useMemo(
|
||||
() => [
|
||||
{ id: "best" as const, label: "Quality" },
|
||||
{ id: "fastest" as const, label: "Speed" },
|
||||
{ id: "cost" as const, label: "Cost" },
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
const cloudHref = useMemo(() => {
|
||||
if (!primaryCandidate) return "https://app.roocode.com"
|
||||
return getCloudSetupUrl(primaryCandidate)
|
||||
}, [primaryCandidate])
|
||||
|
||||
const examplePrompt = objective.builderProfile?.examplePrompt?.trim() ?? ""
|
||||
const copyPrompt = useCallback(async () => {
|
||||
if (!examplePrompt) return
|
||||
await navigator.clipboard.writeText(examplePrompt)
|
||||
}, [examplePrompt])
|
||||
|
||||
const onSelectModel = useCallback((roleId: string, modelId: string) => {
|
||||
setOverrides((prev) => ({ ...prev, [roleId]: modelId }))
|
||||
}, [])
|
||||
|
||||
const onSetMode = useCallback((next: EvalOptimizationMode) => {
|
||||
setMode(next)
|
||||
}, [])
|
||||
|
||||
const roleQuery = useMemo(
|
||||
() => buildObjectiveQueryString(searchParams, objective.slug, mode),
|
||||
[mode, objective.slug, searchParams],
|
||||
)
|
||||
|
||||
const primaryScatter = useMemo(() => {
|
||||
if (!primaryRoleId) return []
|
||||
const rec = recByRole.get(primaryRoleId)
|
||||
if (!rec) return []
|
||||
return rec.allCandidates.map((c) => ({
|
||||
name: c.displayName,
|
||||
provider: c.provider,
|
||||
score: c.compositeScore,
|
||||
successRate: c.successRate,
|
||||
dailyCost: Math.round(c.estimatedDailyCost),
|
||||
dotSize: Math.round(40 + (c.successRate / 100) * 260),
|
||||
isSelected: primaryCandidate?.modelId === c.modelId,
|
||||
}))
|
||||
}, [primaryCandidate?.modelId, primaryRoleId, recByRole])
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Atmosphere */}
|
||||
<div aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||
<div className="absolute -left-32 -top-40 h-[560px] w-[560px] rounded-full bg-foreground/[0.035] blur-3xl" />
|
||||
<div className="absolute right-[-180px] top-24 h-[700px] w-[700px] rounded-full bg-foreground/[0.03] blur-3xl" />
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.45]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(to right, rgba(255,255,255,0.06) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.06) 1px, transparent 1px)",
|
||||
backgroundSize: "88px 88px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section className="relative pt-24">
|
||||
<div className="container relative z-10 mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8">
|
||||
<motion.div initial="hidden" animate="visible" variants={containerVariants}>
|
||||
<motion.nav
|
||||
className="mb-6 flex flex-wrap items-center gap-2 text-sm text-muted-foreground"
|
||||
variants={fadeUp}>
|
||||
<Link href="/evals" className="transition-colors hover:text-foreground">
|
||||
Evals
|
||||
</Link>
|
||||
<span className="text-border">/</span>
|
||||
<Link href="/evals/recommendations" className="transition-colors hover:text-foreground">
|
||||
Build with Roo Code Cloud
|
||||
</Link>
|
||||
<span className="text-border">/</span>
|
||||
<span className="font-medium text-foreground">{objective.name}</span>
|
||||
</motion.nav>
|
||||
|
||||
<motion.div
|
||||
className="grid grid-cols-1 gap-10 lg:grid-cols-12 lg:items-start"
|
||||
variants={fadeUp}>
|
||||
<div className="lg:col-span-7">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-background/30 px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.22em] text-muted-foreground backdrop-blur-sm">
|
||||
<Sparkles className="size-3.5" />
|
||||
Objective Deep Dive
|
||||
</div>
|
||||
<h1 className="mt-4 text-balance text-5xl font-semibold tracking-tight text-foreground [font-family:var(--font-display)] sm:text-6xl">
|
||||
{objective.name}
|
||||
</h1>
|
||||
<p className="mt-4 max-w-2xl text-pretty text-base leading-relaxed text-muted-foreground sm:text-lg">
|
||||
{objective.description} Explore why this setup is recommended, compare options, and
|
||||
tune it to your constraints before you start in Roo Code Cloud.
|
||||
</p>
|
||||
|
||||
<div className="mt-7 flex flex-wrap items-center gap-3">
|
||||
<Link
|
||||
href={
|
||||
primaryRoleId
|
||||
? `/evals/recommendations/roles/${primaryRoleId}${roleQuery}`
|
||||
: "/evals/recommendations"
|
||||
}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-background/20 px-5 py-2.5 text-sm font-semibold text-foreground transition-colors hover:bg-background/30">
|
||||
<BarChart3 className="size-4" />
|
||||
See model data
|
||||
</Link>
|
||||
<Link
|
||||
href={
|
||||
primaryRoleId
|
||||
? `/evals/recommendations/roles/${primaryRoleId}/compare${roleQuery}`
|
||||
: "/evals/recommendations"
|
||||
}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-background/10 px-5 py-2.5 text-sm font-semibold text-muted-foreground transition-colors hover:bg-background/20 hover:text-foreground">
|
||||
<Workflow className="size-4" />
|
||||
Compare candidates
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-5">
|
||||
<div className="relative overflow-hidden rounded-2xl border border-border/50 bg-background/25 p-6 backdrop-blur-xl">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute -right-40 -top-40 size-96 rounded-full bg-foreground/[0.04] blur-3xl"
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground/70">
|
||||
Configuration
|
||||
</p>
|
||||
<span className="rounded-full border border-border/50 bg-background/25 px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider text-foreground/70">
|
||||
<SlidersHorizontal className="mr-1 inline-block size-3" />
|
||||
Tunable
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">
|
||||
Optimized for
|
||||
</p>
|
||||
<div className="mt-2 inline-flex rounded-full border border-border/50 bg-card/30 p-1 backdrop-blur-sm">
|
||||
{modePill.map((m) => {
|
||||
const selected = m.id === mode
|
||||
return (
|
||||
<button
|
||||
key={m.id}
|
||||
type="button"
|
||||
aria-pressed={selected}
|
||||
onClick={() => onSetMode(m.id)}
|
||||
className={[
|
||||
"rounded-full px-3 py-1.5 text-xs font-semibold transition-colors",
|
||||
selected
|
||||
? "bg-foreground/10 text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
].join(" ")}>
|
||||
{m.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 rounded-2xl border border-border/50 bg-background/10 p-4">
|
||||
<p className="text-xs font-semibold text-foreground">Primary model</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{primaryCandidate ? (
|
||||
<>
|
||||
<span className="font-semibold text-foreground">
|
||||
{primaryCandidate.displayName}
|
||||
</span>{" "}
|
||||
<span className="text-muted-foreground/70">
|
||||
({shortProvider(primaryCandidate.provider)})
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-muted-foreground">
|
||||
Pick an objective to see recommendations.
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
{primaryCandidate ? (
|
||||
<div className="mt-3 grid grid-cols-3 gap-2">
|
||||
<div className="rounded-xl border border-border/50 bg-background/10 px-3 py-2">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
|
||||
Score
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-sm font-semibold text-foreground tabular-nums">
|
||||
{primaryCandidate.compositeScore}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/50 bg-background/10 px-3 py-2">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
|
||||
Success
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-sm font-semibold text-foreground tabular-nums">
|
||||
{primaryCandidate.successRate}%
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/50 bg-background/10 px-3 py-2">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
|
||||
Daily
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-sm font-semibold text-foreground tabular-nums">
|
||||
{formatDollars(primaryCandidate.estimatedDailyCost)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<a
|
||||
href={cloudHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-2xl border border-foreground/20 bg-foreground/10 px-4 py-3 text-sm font-semibold text-foreground transition-colors hover:bg-foreground/15">
|
||||
Open in Roo Code Cloud
|
||||
<ExternalLink className="size-4" />
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyPrompt}
|
||||
disabled={!examplePrompt}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-2xl border border-border/60 bg-background/10 px-4 py-3 text-sm font-semibold text-muted-foreground transition-colors hover:border-border hover:bg-background/15 hover:text-foreground disabled:cursor-not-allowed disabled:opacity-60">
|
||||
Copy example prompt
|
||||
<Copy className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-xs leading-relaxed text-muted-foreground">
|
||||
This is a starting point. Adjust the lineup and model picks to match your
|
||||
repo and delivery constraints.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="relative pb-20 pt-14">
|
||||
<div className="container relative z-10 mx-auto max-w-screen-xl px-4 sm:px-6 lg:px-8">
|
||||
<div className="grid grid-cols-1 gap-10 lg:grid-cols-12 lg:items-start">
|
||||
<div className="lg:col-span-7">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-foreground [font-family:var(--font-display)]">
|
||||
Recommended lineup
|
||||
</h2>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
For {objective.name}, this lineup balances momentum and safety. Swap models per agent if
|
||||
you have different constraints.
|
||||
</p>
|
||||
|
||||
<div className="mt-6 space-y-4">
|
||||
{lineup.map((roleId) => {
|
||||
const rec = recByRole.get(roleId)
|
||||
if (!rec) return null
|
||||
const selected = selectedByRole.get(roleId) ?? null
|
||||
|
||||
return (
|
||||
<div
|
||||
key={roleId}
|
||||
className="rounded-2xl border border-border/50 bg-background/15 p-5 backdrop-blur-sm">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-muted-foreground/70">
|
||||
For
|
||||
</p>
|
||||
<p className="mt-2 text-lg font-semibold tracking-tight text-foreground">
|
||||
{rec.role.name}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{rec.role.salaryRange}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Link
|
||||
href={`/evals/recommendations/roles/${roleId}${roleQuery}`}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-background/10 px-4 py-2 text-xs font-semibold text-muted-foreground transition-colors hover:bg-background/20 hover:text-foreground">
|
||||
Details
|
||||
<ArrowRight className="size-3.5" />
|
||||
</Link>
|
||||
<Link
|
||||
href={`/evals/recommendations/roles/${roleId}/compare${roleQuery}`}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-background/10 px-4 py-2 text-xs font-semibold text-muted-foreground transition-colors hover:bg-background/20 hover:text-foreground">
|
||||
Compare
|
||||
<ArrowRight className="size-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-5 sm:items-end">
|
||||
<div className="sm:col-span-3">
|
||||
<label className="text-xs font-semibold uppercase tracking-wider text-muted-foreground/70">
|
||||
Model pick
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
<select
|
||||
value={selected?.modelId ?? ""}
|
||||
onChange={(e) => onSelectModel(roleId, e.target.value)}
|
||||
className="w-full rounded-xl border border-border/60 bg-background/20 px-3 py-2.5 text-sm font-semibold text-foreground shadow-sm outline-none transition-colors focus:border-foreground/30">
|
||||
{rec.allCandidates.slice(0, 12).map((c) => (
|
||||
<option key={c.modelId} value={c.modelId}>
|
||||
{c.displayName} · {shortProvider(c.provider)} ·{" "}
|
||||
{c.compositeScore}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<div className="rounded-xl border border-border/50 bg-background/10 px-3 py-2">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
|
||||
Success
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-sm font-semibold text-foreground tabular-nums">
|
||||
{selected ? `${selected.successRate}%` : "—"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/50 bg-background/10 px-3 py-2">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
|
||||
Daily
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-sm font-semibold text-foreground tabular-nums">
|
||||
{selected
|
||||
? formatDollars(selected.estimatedDailyCost)
|
||||
: "—"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-border/50 bg-background/10 px-3 py-2">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/70">
|
||||
Time
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-sm font-semibold text-foreground tabular-nums">
|
||||
{selected
|
||||
? formatSeconds(selected.avgTimePerTask)
|
||||
: "—"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-5">
|
||||
<div className="rounded-2xl border border-border/50 bg-background/15 p-6 backdrop-blur-sm">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground/70">
|
||||
Why this works
|
||||
</p>
|
||||
<ul className="mt-4 space-y-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{(objective.whyItWorks ?? []).slice(0, 4).map((line) => (
|
||||
<li key={line}>- {line}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{objective.builderProfile?.howItWorks?.length ? (
|
||||
<>
|
||||
<p className="mt-6 text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground/70">
|
||||
How it runs
|
||||
</p>
|
||||
<ol className="mt-4 space-y-2 text-sm leading-relaxed text-muted-foreground">
|
||||
{objective.builderProfile.howItWorks.slice(0, 4).map((step, idx) => (
|
||||
<li key={`${idx}:${step}`} className="flex gap-3">
|
||||
<span className="mt-0.5 inline-flex size-6 shrink-0 items-center justify-center rounded-lg border border-border/60 bg-background/20 font-mono text-xs font-semibold text-foreground/80">
|
||||
{idx + 1}
|
||||
</span>
|
||||
<span>{step}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{examplePrompt ? (
|
||||
<div className="mt-6 rounded-2xl border border-border/50 bg-background/10 p-4">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Example prompt
|
||||
</p>
|
||||
<pre className="mt-3 max-h-56 overflow-auto whitespace-pre-wrap rounded-xl border border-border/50 bg-background/10 p-3 text-xs leading-relaxed text-foreground/85">
|
||||
{examplePrompt}
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 rounded-2xl border border-border/50 bg-background/15 p-6 backdrop-blur-sm">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted-foreground/70">
|
||||
Data preview
|
||||
</p>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
Composite score vs estimated daily cost for the primary agent, with dot size mapped
|
||||
to success rate.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 h-[320px] w-full overflow-hidden rounded-2xl border border-border/50 bg-background/10 p-3">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ScatterChart margin={{ top: 10, right: 10, bottom: 10, left: 0 }}>
|
||||
<XAxis
|
||||
type="number"
|
||||
dataKey="dailyCost"
|
||||
name="Daily Cost"
|
||||
tick={{ fill: "currentColor", fontSize: 12 }}
|
||||
axisLine={{ stroke: "rgba(255,255,255,0.08)" }}
|
||||
tickLine={{ stroke: "rgba(255,255,255,0.08)" }}
|
||||
/>
|
||||
<YAxis
|
||||
type="number"
|
||||
dataKey="score"
|
||||
name="Score"
|
||||
domain={[40, 100]}
|
||||
tick={{ fill: "currentColor", fontSize: 12 }}
|
||||
axisLine={{ stroke: "rgba(255,255,255,0.08)" }}
|
||||
tickLine={{ stroke: "rgba(255,255,255,0.08)" }}
|
||||
/>
|
||||
<ZAxis type="number" dataKey="dotSize" range={[60, 360]} />
|
||||
<Tooltip content={<ObjectiveTooltip />} />
|
||||
<Scatter data={primaryScatter} fill="#3b82f6">
|
||||
{primaryScatter.map((d, i) => (
|
||||
<Cell
|
||||
key={i}
|
||||
fill={d.isSelected ? "#22c55e" : "#3b82f6"}
|
||||
stroke={
|
||||
d.isSelected
|
||||
? "rgba(34,197,94,0.65)"
|
||||
: "rgba(59,130,246,0.35)"
|
||||
}
|
||||
strokeWidth={d.isSelected ? 2 : 1}
|
||||
/>
|
||||
))}
|
||||
</Scatter>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<Link
|
||||
href={
|
||||
primaryRoleId
|
||||
? `/evals/recommendations/roles/${primaryRoleId}/compare${roleQuery}`
|
||||
: "/evals/recommendations"
|
||||
}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/60 bg-background/10 px-5 py-2.5 text-sm font-semibold text-muted-foreground transition-colors hover:bg-background/20 hover:text-foreground">
|
||||
Compare the full set
|
||||
<ArrowRight className="size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
import type { Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { getRoleRecommendation } from "@/lib/mock-recommendations"
|
||||
import { getEvalOutcomeBySlug } from "@/lib/eval-outcomes"
|
||||
|
||||
import { ObjectiveContent } from "./objective-content"
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ objectiveSlug: string }>
|
||||
searchParams?: Promise<Record<string, string | string[] | undefined>>
|
||||
}
|
||||
|
||||
function isMode(value: string): value is "best" | "fastest" | "cost" {
|
||||
return value === "best" || value === "fastest" || value === "cost"
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { objectiveSlug } = await params
|
||||
const objective = getEvalOutcomeBySlug(objectiveSlug)
|
||||
if (!objective) {
|
||||
return {
|
||||
title: "Objective Not Found | Roo Code Evals",
|
||||
description: "The requested objective was not found.",
|
||||
}
|
||||
}
|
||||
|
||||
const title = `${objective.name} — Build Profile | Roo Code Evals`
|
||||
const description = `Investigate the recommended lineup for ${objective.name}. Compare options, explore data, and start in Roo Code Cloud.`
|
||||
const ogDescription = `${objective.name} — Build Profile`
|
||||
const path = `/evals/recommendations/${objective.slug}`
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: { canonical: `${SEO.url}${path}` },
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: `${SEO.url}${path}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(title, ogDescription),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: title,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title,
|
||||
description,
|
||||
images: [ogImageUrl(title, ogDescription)],
|
||||
},
|
||||
keywords: [...SEO.keywords, "AI coding", "model recommendations", "coding evals", "roo code cloud"],
|
||||
}
|
||||
}
|
||||
|
||||
export default async function ObjectiveDeepDivePage({ params, searchParams }: PageProps) {
|
||||
const { objectiveSlug } = await params
|
||||
const objective = getEvalOutcomeBySlug(objectiveSlug)
|
||||
if (!objective) notFound()
|
||||
|
||||
const sp = (await searchParams) ?? {}
|
||||
const modeRaw = typeof sp.mode === "string" ? sp.mode : undefined
|
||||
const initialMode = modeRaw && isMode(modeRaw) ? modeRaw : "best"
|
||||
|
||||
const recs: Array<{ roleId: string; recommendation: NonNullable<ReturnType<typeof getRoleRecommendation>> }> = []
|
||||
for (const roleId of objective.recommendedRoleIds) {
|
||||
const recommendation = getRoleRecommendation(roleId)
|
||||
if (recommendation) recs.push({ roleId, recommendation })
|
||||
}
|
||||
|
||||
return (
|
||||
<ObjectiveContent
|
||||
objective={{
|
||||
id: objective.id,
|
||||
slug: objective.slug,
|
||||
name: objective.name,
|
||||
description: objective.description,
|
||||
whyItWorks: objective.whyItWorks,
|
||||
recommendedRoleIds: objective.recommendedRoleIds,
|
||||
builderProfile: objective.builderProfile
|
||||
? {
|
||||
title: objective.builderProfile.title,
|
||||
description: objective.builderProfile.description,
|
||||
examplePrompt: objective.builderProfile.examplePrompt,
|
||||
howItWorks: objective.builderProfile.howItWorks,
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
initialMode={initialMode}
|
||||
recs={recs}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { generateMetadata } from "../../../workers/[roleId]/compare/page"
|
||||
export { default } from "../../../workers/[roleId]/compare/page"
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { generateMetadata } from "../../workers/[roleId]/page"
|
||||
export { default } from "../../workers/[roleId]/page"
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import type { ReactNode } from "react"
|
||||
import { Fraunces, IBM_Plex_Sans } from "next/font/google"
|
||||
|
||||
const display = Fraunces({ subsets: ["latin"], variable: "--font-display" })
|
||||
const body = IBM_Plex_Sans({ subsets: ["latin"], weight: ["400", "500", "600"], variable: "--font-body" })
|
||||
|
||||
export default function RecommendationsLayout({ children }: { children: ReactNode }) {
|
||||
return <div className={`${body.variable} ${display.variable} [font-family:var(--font-body)]`}>{children}</div>
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import type { Metadata } from "next"
|
||||
import { Fraunces, IBM_Plex_Sans } from "next/font/google"
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { getEngineerRoles, getAllRecommendations } from "@/lib/mock-recommendations"
|
||||
import { EVAL_OUTCOMES, isEvalOutcomeId } from "@/lib/eval-outcomes"
|
||||
|
||||
import { WorkersContent } from "../workers/workers-content"
|
||||
|
||||
|
|
@ -15,9 +16,6 @@ const DESCRIPTION =
|
|||
const OG_DESCRIPTION = "Outcome-first recommendations for shipping production code"
|
||||
const PATH = "/evals/recommendations"
|
||||
|
||||
const display = Fraunces({ subsets: ["latin"], variable: "--font-display" })
|
||||
const body = IBM_Plex_Sans({ subsets: ["latin"], weight: ["400", "500", "600"], variable: "--font-body" })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
|
|
@ -61,7 +59,25 @@ export const metadata: Metadata = {
|
|||
|
||||
// ── Page Component ──────────────────────────────────────────────────────────
|
||||
|
||||
export default function RecommendationsPage() {
|
||||
type PageProps = {
|
||||
searchParams?: Promise<Record<string, string | string[] | undefined>>
|
||||
}
|
||||
|
||||
export default async function RecommendationsPage({ searchParams }: PageProps) {
|
||||
const sp = (await searchParams) ?? {}
|
||||
const view = typeof sp.view === "string" ? sp.view : undefined
|
||||
const outcome = typeof sp.outcome === "string" ? sp.outcome : undefined
|
||||
const mode = typeof sp.mode === "string" ? sp.mode : undefined
|
||||
|
||||
// Legacy deep link: move profile investigations into the dedicated objective pages.
|
||||
if (view === "profile" && outcome && isEvalOutcomeId(outcome)) {
|
||||
const objective = EVAL_OUTCOMES.find((o) => o.id === outcome)
|
||||
if (objective) {
|
||||
const qs = mode ? `?mode=${encodeURIComponent(mode)}` : ""
|
||||
redirect(`/evals/recommendations/${objective.slug}${qs}`)
|
||||
}
|
||||
}
|
||||
|
||||
const roles = getEngineerRoles()
|
||||
const recommendations = getAllRecommendations()
|
||||
|
||||
|
|
@ -79,16 +95,15 @@ export default function RecommendationsPage() {
|
|||
.pop()
|
||||
|
||||
return (
|
||||
<div className={`${body.variable} ${display.variable} [font-family:var(--font-body)]`}>
|
||||
<WorkersContent
|
||||
roles={roles}
|
||||
recommendations={recommendations}
|
||||
totalEvalRuns={totalEvalRuns}
|
||||
totalExercises={totalExercises}
|
||||
totalModels={totalModels}
|
||||
lastUpdated={lastUpdated}
|
||||
workersRootPath="/evals/recommendations"
|
||||
/>
|
||||
</div>
|
||||
<WorkersContent
|
||||
roles={roles}
|
||||
recommendations={recommendations}
|
||||
totalEvalRuns={totalEvalRuns}
|
||||
totalExercises={totalExercises}
|
||||
totalModels={totalModels}
|
||||
lastUpdated={lastUpdated}
|
||||
workersRootPath="/evals/recommendations"
|
||||
roleBasePath="/evals/recommendations/roles"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
import { notFound } from "next/navigation"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { getEngineerRole, getRoleRecommendation } from "@/lib/mock-recommendations"
|
||||
|
||||
import { ComparisonChart } from "../../../../workers/[roleId]/compare/comparison-chart"
|
||||
|
||||
type PageProps = { params: Promise<{ roleId: string }> }
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { roleId } = await params
|
||||
const role = getEngineerRole(roleId)
|
||||
|
||||
if (!role) {
|
||||
return {
|
||||
title: "Role Not Found | Roo Code Evals",
|
||||
description: "The requested engineer role was not found.",
|
||||
}
|
||||
}
|
||||
|
||||
const title = `Compare Models — ${role.name} | Roo Code Evals`
|
||||
const description = `Interactive comparison of AI models for the ${role.name} setup. Compare composite score, success rate, cost efficiency, and speed.`
|
||||
const ogDescription = `Compare Models — ${role.name}`
|
||||
const path = `/evals/recommendations/roles/${roleId}/compare`
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${path}`,
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: `${SEO.url}${path}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(title, ogDescription),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: title,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title,
|
||||
description,
|
||||
images: [ogImageUrl(title, ogDescription)],
|
||||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"AI coding",
|
||||
"model comparison",
|
||||
"coding evals",
|
||||
role.name.toLowerCase(),
|
||||
"bar chart",
|
||||
"model comparison",
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export default async function RecommendationComparePage({ params }: PageProps) {
|
||||
const { roleId } = await params
|
||||
const recommendation = getRoleRecommendation(roleId)
|
||||
|
||||
if (!recommendation) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<ComparisonChart
|
||||
recommendation={recommendation}
|
||||
role={recommendation.role}
|
||||
roleId={roleId}
|
||||
workersRootPath="/evals/recommendations"
|
||||
roleBasePath="/evals/recommendations/roles"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import { notFound } from "next/navigation"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { getRoleRecommendation, getCloudSetupUrl } from "@/lib/mock-recommendations"
|
||||
|
||||
import { CandidatesContent } from "../../../workers/[roleId]/candidates-content"
|
||||
|
||||
type PageProps = { params: Promise<{ roleId: string }> }
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const { roleId } = await params
|
||||
const recommendation = getRoleRecommendation(roleId)
|
||||
|
||||
if (!recommendation) {
|
||||
return {
|
||||
title: "Role Not Found | Roo Code Evals",
|
||||
description: "The requested engineer role was not found.",
|
||||
}
|
||||
}
|
||||
|
||||
const { role } = recommendation
|
||||
const title = `${role.name} — Recommended Models | Roo Code Evals`
|
||||
const description = `Eval-backed recommendations for ${role.name}. Compare models by success rate, cost, and speed across 5 languages.`
|
||||
const ogDescription = `${role.name} — Recommended Models`
|
||||
const path = `/evals/recommendations/roles/${roleId}`
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${path}`,
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: `${SEO.url}${path}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(title, ogDescription),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: title,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title,
|
||||
description,
|
||||
images: [ogImageUrl(title, ogDescription)],
|
||||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"AI coding",
|
||||
"coding agents",
|
||||
"model recommendations",
|
||||
"coding evals",
|
||||
role.name.toLowerCase(),
|
||||
"model comparison",
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export default async function RecommendationRolePage({ params }: PageProps) {
|
||||
const { roleId } = await params
|
||||
const recommendation = getRoleRecommendation(roleId)
|
||||
|
||||
if (!recommendation) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const { role, best, budgetHire, speedHire, allCandidates, totalEvalRuns, totalExercises, lastUpdated } =
|
||||
recommendation
|
||||
|
||||
const cloudUrls: Record<string, string> = {}
|
||||
for (const candidate of allCandidates) {
|
||||
cloudUrls[candidate.modelId] = getCloudSetupUrl(candidate)
|
||||
}
|
||||
|
||||
return (
|
||||
<CandidatesContent
|
||||
roleId={roleId}
|
||||
role={role}
|
||||
best={best}
|
||||
budgetHire={budgetHire}
|
||||
speedHire={speedHire}
|
||||
allCandidates={allCandidates}
|
||||
totalEvalRuns={totalEvalRuns}
|
||||
totalExercises={totalExercises}
|
||||
lastUpdated={lastUpdated}
|
||||
cloudUrls={cloudUrls}
|
||||
workersRootPath="/evals/recommendations"
|
||||
roleBasePath="/evals/recommendations/roles"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -645,6 +645,11 @@ export type CandidatesContentProps = {
|
|||
lastUpdated: string
|
||||
cloudUrls: Record<string, string>
|
||||
workersRootPath?: string
|
||||
/**
|
||||
* Base path for role detail routes, without the role id.
|
||||
* Examples: `/evals/workers`, `/evals/recommendations/roles`.
|
||||
*/
|
||||
roleBasePath?: string
|
||||
}
|
||||
|
||||
// ── Main Content Component ──────────────────────────────────────────────────
|
||||
|
|
@ -661,19 +666,27 @@ export function CandidatesContent({
|
|||
lastUpdated,
|
||||
cloudUrls,
|
||||
workersRootPath = "/evals/recommendations",
|
||||
roleBasePath = workersRootPath,
|
||||
}: CandidatesContentProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const theme = ROLE_THEMES[roleId] ?? DEFAULT_THEME
|
||||
const IconComponent = ICON_MAP[role.icon] ?? Code
|
||||
const outcome = searchParams.get("outcome")
|
||||
const objectiveSlug = searchParams.get("objective")
|
||||
const mode = searchParams.get("mode")
|
||||
const setupQuery = (() => {
|
||||
const outcome = searchParams.get("outcome")
|
||||
if (!outcome) return ""
|
||||
if (!outcome && !objectiveSlug) return ""
|
||||
const params = new URLSearchParams()
|
||||
params.set("outcome", outcome)
|
||||
const mode = searchParams.get("mode")
|
||||
if (outcome) params.set("outcome", outcome)
|
||||
if (objectiveSlug) params.set("objective", objectiveSlug)
|
||||
if (mode) params.set("mode", mode)
|
||||
return `?${params.toString()}`
|
||||
})()
|
||||
let homeHref = `${workersRootPath}${setupQuery}`
|
||||
if (objectiveSlug && workersRootPath === "/evals/recommendations") {
|
||||
homeHref = `${workersRootPath}/${objectiveSlug}`
|
||||
if (mode) homeHref += `?mode=${encodeURIComponent(mode)}`
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -706,9 +719,7 @@ export function CandidatesContent({
|
|||
Evals
|
||||
</Link>
|
||||
<span className="text-border">/</span>
|
||||
<Link
|
||||
href={`${workersRootPath}${setupQuery}`}
|
||||
className="transition-colors hover:text-foreground">
|
||||
<Link href={homeHref} className="transition-colors hover:text-foreground">
|
||||
Build with Roo Code Cloud
|
||||
</Link>
|
||||
<span className="text-border">/</span>
|
||||
|
|
@ -977,7 +988,7 @@ export function CandidatesContent({
|
|||
{/* Compare link */}
|
||||
<motion.div className="mt-6 text-center" variants={fadeUpVariants}>
|
||||
<Link
|
||||
href={`${workersRootPath}/${roleId}/compare${setupQuery}`}
|
||||
href={`${roleBasePath}/${roleId}/compare${setupQuery}`}
|
||||
className={`group inline-flex items-center gap-2 rounded-full border ${theme.methodologyBorder} bg-card/50 px-5 py-2.5 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-300 hover:text-foreground`}>
|
||||
📊 Compare all candidates
|
||||
<ArrowRight className="size-4 transition-transform duration-200 group-hover:translate-x-0.5" />
|
||||
|
|
@ -998,7 +1009,7 @@ export function CandidatesContent({
|
|||
variants={containerVariants}>
|
||||
<motion.div variants={fadeUpVariants}>
|
||||
<Link
|
||||
href={`${workersRootPath}${setupQuery}`}
|
||||
href={homeHref}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-200 hover:border-border hover:text-foreground">
|
||||
<ArrowLeft className="size-4" />
|
||||
Back to all roles
|
||||
|
|
@ -1006,7 +1017,7 @@ export function CandidatesContent({
|
|||
</motion.div>
|
||||
<motion.div className="flex flex-wrap gap-3" variants={fadeUpVariants}>
|
||||
<Link
|
||||
href={`${workersRootPath}/${roleId}/compare${setupQuery}`}
|
||||
href={`${roleBasePath}/${roleId}/compare${setupQuery}`}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-200 hover:border-border hover:text-foreground">
|
||||
📊 Compare candidates
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -435,6 +435,11 @@ interface ComparisonChartProps {
|
|||
role: EngineerRole
|
||||
roleId: string
|
||||
workersRootPath?: string
|
||||
/**
|
||||
* Base path for role detail routes, without the role id.
|
||||
* Examples: `/evals/workers`, `/evals/recommendations/roles`.
|
||||
*/
|
||||
roleBasePath?: string
|
||||
}
|
||||
|
||||
export function ComparisonChart({
|
||||
|
|
@ -442,19 +447,27 @@ export function ComparisonChart({
|
|||
role,
|
||||
roleId,
|
||||
workersRootPath = "/evals/recommendations",
|
||||
roleBasePath = workersRootPath,
|
||||
}: ComparisonChartProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const { allCandidates } = recommendation
|
||||
const theme = ROLE_THEMES[roleId] ?? DEFAULT_THEME
|
||||
const outcome = searchParams.get("outcome")
|
||||
const objectiveSlug = searchParams.get("objective")
|
||||
const mode = searchParams.get("mode")
|
||||
const setupQuery = (() => {
|
||||
const outcome = searchParams.get("outcome")
|
||||
if (!outcome) return ""
|
||||
if (!outcome && !objectiveSlug) return ""
|
||||
const params = new URLSearchParams()
|
||||
params.set("outcome", outcome)
|
||||
const mode = searchParams.get("mode")
|
||||
if (outcome) params.set("outcome", outcome)
|
||||
if (objectiveSlug) params.set("objective", objectiveSlug)
|
||||
if (mode) params.set("mode", mode)
|
||||
return `?${params.toString()}`
|
||||
})()
|
||||
let homeHref = `${workersRootPath}${setupQuery}`
|
||||
if (objectiveSlug && workersRootPath === "/evals/recommendations") {
|
||||
homeHref = `${workersRootPath}/${objectiveSlug}`
|
||||
if (mode) homeHref += `?mode=${encodeURIComponent(mode)}`
|
||||
}
|
||||
|
||||
// ── State ───────────────────────────────────────────────────────────────
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<keyof LanguageScores | "all">("all")
|
||||
|
|
@ -578,14 +591,12 @@ export function ComparisonChart({
|
|||
Evals
|
||||
</Link>
|
||||
<span className="text-border">/</span>
|
||||
<Link
|
||||
href={`${workersRootPath}${setupQuery}`}
|
||||
className="transition-colors hover:text-foreground">
|
||||
<Link href={homeHref} className="transition-colors hover:text-foreground">
|
||||
Build with Roo Code Cloud
|
||||
</Link>
|
||||
<span className="text-border">/</span>
|
||||
<Link
|
||||
href={`${workersRootPath}/${roleId}${setupQuery}`}
|
||||
href={`${roleBasePath}/${roleId}${setupQuery}`}
|
||||
className="transition-colors hover:text-foreground">
|
||||
{role.name}
|
||||
</Link>
|
||||
|
|
@ -1014,7 +1025,7 @@ export function ComparisonChart({
|
|||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<Link
|
||||
href={`${workersRootPath}/${roleId}${setupQuery}`}
|
||||
href={`${roleBasePath}/${roleId}${setupQuery}`}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-200 hover:border-border hover:text-foreground">
|
||||
<ArrowLeft className="size-4" />
|
||||
Back to {role.name} models
|
||||
|
|
@ -1022,7 +1033,7 @@ export function ComparisonChart({
|
|||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link
|
||||
href={`${workersRootPath}${setupQuery}`}
|
||||
href={homeHref}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-border/50 bg-card/50 px-4 py-2 text-sm font-medium text-muted-foreground backdrop-blur-sm transition-all duration-200 hover:border-border hover:text-foreground">
|
||||
<ArrowLeft className="size-3.5" />
|
||||
All roles
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||
const title = `Compare Models — ${role.name} | Roo Code Evals`
|
||||
const description = `Interactive comparison of AI models for the ${role.name} setup. Compare composite score, success rate, cost efficiency, and speed.`
|
||||
const ogDescription = `Compare Models — ${role.name}`
|
||||
const path = `/evals/recommendations/${roleId}/compare`
|
||||
const path = `/evals/workers/${roleId}/compare`
|
||||
|
||||
return {
|
||||
title,
|
||||
|
|
@ -82,7 +82,7 @@ export default async function CompareModelsPage({ params }: PageProps) {
|
|||
recommendation={recommendation}
|
||||
role={recommendation.role}
|
||||
roleId={roleId}
|
||||
workersRootPath="/evals/recommendations"
|
||||
workersRootPath="/evals/workers"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||
const title = `${role.name} — Recommended Models | Roo Code Evals`
|
||||
const description = `Eval-backed recommendations for ${role.name}. Compare models by success rate, cost, and speed across 5 languages.`
|
||||
const ogDescription = `${role.name} — Recommended Models`
|
||||
const path = `/evals/recommendations/${roleId}`
|
||||
const path = `/evals/workers/${roleId}`
|
||||
|
||||
return {
|
||||
title,
|
||||
|
|
@ -100,7 +100,7 @@ export default async function RoleCandidatesPage({ params }: PageProps) {
|
|||
totalExercises={totalExercises}
|
||||
lastUpdated={lastUpdated}
|
||||
cloudUrls={cloudUrls}
|
||||
workersRootPath="/evals/recommendations"
|
||||
workersRootPath="/evals/workers"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,6 +190,11 @@ type WorkersContentProps = {
|
|||
totalModels: number
|
||||
lastUpdated: string | undefined
|
||||
workersRootPath?: string
|
||||
/**
|
||||
* Base path for role detail routes, without the role id.
|
||||
* Examples: `/evals/workers`, `/evals/recommendations/roles`.
|
||||
*/
|
||||
roleBasePath?: string
|
||||
}
|
||||
|
||||
// Outcomes-first is canonical. Baseline/V1 is removed from the UI.
|
||||
|
|
@ -203,6 +208,7 @@ export function WorkersContent({
|
|||
totalModels: _totalModels,
|
||||
lastUpdated,
|
||||
workersRootPath = "/evals/recommendations",
|
||||
roleBasePath = workersRootPath,
|
||||
}: WorkersContentProps) {
|
||||
const enableOutcomeLayer = ENABLE_OUTCOME_LAYER
|
||||
const router = useRouter()
|
||||
|
|
@ -272,15 +278,6 @@ export function WorkersContent({
|
|||
return query ? `?${query}` : ""
|
||||
}, [effectiveOutcomeId, selectedMode])
|
||||
|
||||
const profileViewQuery = useMemo(() => {
|
||||
if (!effectiveOutcomeId) return ""
|
||||
const params = new URLSearchParams()
|
||||
params.set("outcome", effectiveOutcomeId)
|
||||
params.set("mode", selectedMode)
|
||||
params.set("view", "profile")
|
||||
return `?${params.toString()}`
|
||||
}, [effectiveOutcomeId, selectedMode])
|
||||
|
||||
const isProfileView = useMemo(() => {
|
||||
return searchParams.get("view") === "profile"
|
||||
}, [searchParams])
|
||||
|
|
@ -593,7 +590,7 @@ export function WorkersContent({
|
|||
return (
|
||||
<Link
|
||||
key={capability.id}
|
||||
href={`${workersRootPath}/${roleId}${setupQuery}`}
|
||||
href={`${roleBasePath}/${roleId}${setupQuery}`}
|
||||
className="group flex items-start justify-between gap-3 rounded-xl border border-border/50 bg-background/10 px-4 py-3 transition-colors hover:bg-background/20">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
|
|
@ -890,7 +887,7 @@ export function WorkersContent({
|
|||
return (
|
||||
<Link
|
||||
key={capability.id}
|
||||
href={`${workersRootPath}/${roleId}${setupQuery}`}
|
||||
href={`${roleBasePath}/${roleId}${setupQuery}`}
|
||||
className="group relative px-4 py-3 pl-5 transition-colors hover:bg-background/25">
|
||||
<span
|
||||
aria-hidden
|
||||
|
|
@ -959,7 +956,7 @@ export function WorkersContent({
|
|||
<ArrowRight className="size-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href={`${workersRootPath}${profileViewQuery}#outcomes`}
|
||||
href={`/evals/recommendations/${selectedOutcome.slug}?mode=${selectedMode}`}
|
||||
className="inline-flex flex-1 items-center justify-center gap-2 rounded-2xl border border-border/60 bg-background/10 px-4 py-3 text-sm font-semibold text-muted-foreground transition-colors hover:border-border hover:bg-background/15 hover:text-foreground">
|
||||
Learn more / customize
|
||||
<ArrowRight className="size-4" />
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ export type EvalOutcomeProfile = {
|
|||
|
||||
export type EvalOutcome = {
|
||||
id: EvalOutcomeId
|
||||
/** URL slug used for objective deep dives: /evals/recommendations/<slug> */
|
||||
slug: string
|
||||
name: string
|
||||
description: string
|
||||
icon: LucideIcon
|
||||
|
|
@ -53,6 +55,7 @@ export type EvalOutcome = {
|
|||
export const EVAL_OUTCOMES: EvalOutcome[] = [
|
||||
{
|
||||
id: "review_guardrails",
|
||||
slug: "idea-prototype",
|
||||
name: "Idea → Prototype",
|
||||
description: "Turn a vague idea into a working demo in your real codebase.",
|
||||
icon: Sparkles,
|
||||
|
|
@ -116,6 +119,7 @@ Deliver:
|
|||
},
|
||||
{
|
||||
id: "prototype_to_pr",
|
||||
slug: "prototype-pr",
|
||||
name: "Prototype → PR",
|
||||
description: "Build a working prototype on the production codebase, then turn it into a reviewable diff.",
|
||||
icon: Sparkles,
|
||||
|
|
@ -188,6 +192,7 @@ Deliver:
|
|||
},
|
||||
{
|
||||
id: "issue_to_pr",
|
||||
slug: "issue-pr",
|
||||
name: "Issue → PR",
|
||||
description: "Run end-to-end work in the background and come back to a reviewable result.",
|
||||
icon: GitPullRequest,
|
||||
|
|
@ -249,6 +254,7 @@ Deliver:
|
|||
},
|
||||
{
|
||||
id: "sentry_triage",
|
||||
slug: "customer-escalation-resolved",
|
||||
name: "Customer Escalation → Resolved",
|
||||
description: "Triage a customer-blocking issue and ship the smallest safe fix.",
|
||||
icon: Bug,
|
||||
|
|
@ -318,6 +324,7 @@ Deliver:
|
|||
},
|
||||
{
|
||||
id: "repro_to_fix",
|
||||
slug: "bug-report-fix",
|
||||
name: "Bug Report → Fix",
|
||||
description: "Reproduce, isolate, patch, and validate in one loop.",
|
||||
icon: Workflow,
|
||||
|
|
@ -381,6 +388,7 @@ Deliver:
|
|||
},
|
||||
{
|
||||
id: "paper_cuts",
|
||||
slug: "paper-cuts-shipped",
|
||||
name: "Paper Cuts → Shipped",
|
||||
description: "Fix the small stuff without dragging engineers off big projects.",
|
||||
icon: CheckCircle2,
|
||||
|
|
@ -447,3 +455,7 @@ Deliver:
|
|||
export function isEvalOutcomeId(value: string): value is EvalOutcomeId {
|
||||
return EVAL_OUTCOMES.some((o) => o.id === value)
|
||||
}
|
||||
|
||||
export function getEvalOutcomeBySlug(slug: string): EvalOutcome | undefined {
|
||||
return EVAL_OUTCOMES.find((o) => o.slug === slug)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue