mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat(web-evals): add scatter plot charts (value map + capability timeline)
- Add "Value Map: Salary vs Interview Score" scatter to comparison page - Dots colored by tier, sized by success rate - Sweet Spot quadrant highlight (upper-left) - Respects existing provider/success-rate filters - Add "AI Coding Capability Over Time" scatter to landing page - 10 models from Jun 2025 to Feb 2026 - Dots colored by provider, sized by cost efficiency - Dashed trend line showing upward trajectory - Add MODEL_TIMELINE data to mock-recommendations.ts
This commit is contained in:
parent
336a6726f9
commit
bf55aa0103
3 changed files with 509 additions and 2 deletions
|
|
@ -15,7 +15,20 @@ import {
|
|||
Download,
|
||||
FlaskConical,
|
||||
} from "lucide-react"
|
||||
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from "recharts"
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
Legend,
|
||||
ScatterChart,
|
||||
Scatter,
|
||||
ZAxis,
|
||||
Cell,
|
||||
ReferenceArea,
|
||||
} from "recharts"
|
||||
|
||||
import type { ModelCandidate, LanguageScores, EngineerRole, RoleRecommendation } from "@/lib/mock-recommendations"
|
||||
import { TASKS_PER_DAY } from "@/lib/mock-recommendations"
|
||||
|
|
@ -225,6 +238,20 @@ const DIMENSION_COLORS = {
|
|||
speed: "#a855f7", // purple
|
||||
}
|
||||
|
||||
const TIER_COLORS: Record<string, string> = {
|
||||
best: "#22c55e", // green
|
||||
recommended: "#3b82f6", // blue
|
||||
situational: "#eab308", // yellow
|
||||
"not-recommended": "#ef4444", // red
|
||||
}
|
||||
|
||||
const TIER_LABELS: Record<string, string> = {
|
||||
best: "Best",
|
||||
recommended: "Recommended",
|
||||
situational: "Situational",
|
||||
"not-recommended": "Not Recommended",
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Normalize cost: lower cost → higher bar (0–100). */
|
||||
|
|
@ -344,6 +371,62 @@ function CustomTooltip({
|
|||
)
|
||||
}
|
||||
|
||||
// ── Scatter Tooltip ─────────────────────────────────────────────────────────
|
||||
|
||||
function ScatterTooltip({
|
||||
active,
|
||||
payload,
|
||||
}: {
|
||||
active?: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
payload?: any[]
|
||||
}) {
|
||||
if (!active || !payload || !payload.length) return null
|
||||
|
||||
const data = payload[0]?.payload as
|
||||
| {
|
||||
name?: string
|
||||
dailyCost?: number
|
||||
score?: number
|
||||
successRate?: number
|
||||
tier?: string
|
||||
}
|
||||
| undefined
|
||||
|
||||
if (!data) return null
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border/50 bg-card/95 p-4 shadow-2xl backdrop-blur-md">
|
||||
<p className="mb-2.5 text-sm font-bold tracking-tight">{data.name}</p>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
<span
|
||||
className="size-2.5 rounded-full ring-1 ring-white/10"
|
||||
style={{ backgroundColor: TIER_COLORS[data.tier ?? "situational"] }}
|
||||
/>
|
||||
<span className="text-muted-foreground">Tier:</span>
|
||||
<span className="font-semibold">{TIER_LABELS[data.tier ?? "situational"]}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
<span className="size-2.5 rounded-full bg-amber-400 ring-1 ring-white/10" />
|
||||
<span className="text-muted-foreground">Daily Salary:</span>
|
||||
<span className="font-semibold tabular-nums">${data.dailyCost}/day</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
<span className="size-2.5 rounded-full bg-blue-400 ring-1 ring-white/10" />
|
||||
<span className="text-muted-foreground">Interview Score:</span>
|
||||
<span className="font-semibold tabular-nums">{data.score}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
<span className="size-2.5 rounded-full bg-green-400 ring-1 ring-white/10" />
|
||||
<span className="text-muted-foreground">Success Rate:</span>
|
||||
<span className="font-semibold tabular-nums">{data.successRate}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Main Component ──────────────────────────────────────────────────────────
|
||||
|
||||
interface ComparisonChartProps {
|
||||
|
|
@ -380,6 +463,25 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
|
|||
|
||||
const chartHeight = Math.max(400, chartData.length * 100)
|
||||
|
||||
// Scatter plot data: value map of daily cost vs composite score
|
||||
const scatterData = useMemo(
|
||||
() =>
|
||||
filteredCandidates.map((c) => ({
|
||||
name: c.displayName,
|
||||
dailyCost: Math.round(c.estimatedDailyCost),
|
||||
score: c.compositeScore,
|
||||
successRate: c.successRate,
|
||||
tier: c.tier,
|
||||
// ZAxis size: map success rate to dot size (60–400 range)
|
||||
dotSize: Math.round(60 + (c.successRate / 100) * 340),
|
||||
})),
|
||||
[filteredCandidates],
|
||||
)
|
||||
|
||||
// Determine axis domains for scatter plot
|
||||
const scatterMaxCost = useMemo(() => Math.max(...scatterData.map((d) => d.dailyCost), 10), [scatterData])
|
||||
const scatterMinScore = useMemo(() => Math.min(...scatterData.map((d) => d.score), 50), [scatterData])
|
||||
|
||||
// Providers that actually appear in data
|
||||
const activeProviders = useMemo(() => {
|
||||
const providers = new Set(allCandidates.map((c) => c.provider))
|
||||
|
|
@ -624,6 +726,124 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar
|
|||
</div>
|
||||
</motion.section>
|
||||
|
||||
{/* ── Value Map Scatter Chart ────────────────────────────── */}
|
||||
<motion.section
|
||||
className="rounded-2xl border border-border/50 bg-card/50 p-6 backdrop-blur-sm"
|
||||
variants={fadeUpVariants}>
|
||||
<div className="mb-1 flex items-center gap-2.5">
|
||||
<h2 className="text-lg font-bold tracking-tight">Value Map: Salary vs Interview Score</h2>
|
||||
</div>
|
||||
<p className="mb-4 text-xs leading-relaxed text-muted-foreground/80">
|
||||
Upper-left = best value. Each dot is a candidate model. Size reflects success rate.
|
||||
</p>
|
||||
|
||||
{/* Tier legend */}
|
||||
<div className="mb-4 flex flex-wrap items-center gap-4 text-xs text-muted-foreground">
|
||||
{Object.entries(TIER_COLORS).map(([tier, color]) => (
|
||||
<div key={tier} className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="size-2.5 rounded-full ring-1 ring-white/10"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<span>{TIER_LABELS[tier]}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{scatterData.length === 0 ? (
|
||||
<div className="flex h-48 flex-col items-center justify-center gap-2 rounded-xl border border-dashed border-border/50 text-muted-foreground">
|
||||
<SlidersHorizontal className="size-6 text-muted-foreground/50" />
|
||||
<p className="text-sm">No candidates match the current filters.</p>
|
||||
<p className="text-xs text-muted-foreground/60">
|
||||
Try adjusting the provider or success rate filters.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl bg-background/30 p-2">
|
||||
<ResponsiveContainer width="100%" height={420}>
|
||||
<ScatterChart margin={{ top: 20, right: 30, bottom: 20, left: 10 }}>
|
||||
<XAxis
|
||||
type="number"
|
||||
dataKey="dailyCost"
|
||||
name="Daily Salary"
|
||||
domain={[0, Math.ceil(scatterMaxCost * 1.1)]}
|
||||
tickFormatter={(v: number) => `$${v}`}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
strokeOpacity={0.3}
|
||||
tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }}
|
||||
axisLine={false}
|
||||
label={{
|
||||
value: "Daily Salary ($)",
|
||||
position: "insideBottom",
|
||||
offset: -10,
|
||||
style: { fontSize: 11, fill: "hsl(var(--muted-foreground))" },
|
||||
}}
|
||||
/>
|
||||
<YAxis
|
||||
type="number"
|
||||
dataKey="score"
|
||||
name="Interview Score"
|
||||
domain={[Math.max(0, scatterMinScore - 10), 100]}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
strokeOpacity={0.3}
|
||||
tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }}
|
||||
axisLine={false}
|
||||
label={{
|
||||
value: "Interview Score",
|
||||
angle: -90,
|
||||
position: "insideLeft",
|
||||
offset: 10,
|
||||
style: { fontSize: 11, fill: "hsl(var(--muted-foreground))" },
|
||||
}}
|
||||
/>
|
||||
<ZAxis type="number" dataKey="dotSize" range={[60, 400]} />
|
||||
{/* Sweet spot reference zone: upper-left quadrant */}
|
||||
<ReferenceArea
|
||||
x1={0}
|
||||
x2={Math.ceil(scatterMaxCost * 0.4)}
|
||||
y1={80}
|
||||
y2={100}
|
||||
fill="hsl(var(--foreground))"
|
||||
fillOpacity={0.03}
|
||||
stroke="hsl(var(--foreground))"
|
||||
strokeOpacity={0.08}
|
||||
strokeDasharray="4 4"
|
||||
label={{
|
||||
value: "Sweet Spot",
|
||||
position: "insideTopLeft",
|
||||
style: {
|
||||
fontSize: 10,
|
||||
fill: "hsl(var(--muted-foreground))",
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
content={<ScatterTooltip />}
|
||||
cursor={{
|
||||
strokeDasharray: "3 3",
|
||||
stroke: "hsl(var(--muted-foreground))",
|
||||
strokeOpacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
<Scatter data={scatterData} name="Candidates">
|
||||
{scatterData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`scatter-cell-${index}`}
|
||||
fill={TIER_COLORS[entry.tier] ?? "#94a3b8"}
|
||||
fillOpacity={0.85}
|
||||
stroke={TIER_COLORS[entry.tier] ?? "#94a3b8"}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={0.4}
|
||||
/>
|
||||
))}
|
||||
</Scatter>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</motion.section>
|
||||
|
||||
{/* ── Chart Section ──────────────────────────────────────── */}
|
||||
<motion.section
|
||||
className="rounded-2xl border border-border/50 bg-card/50 p-6 backdrop-blur-sm"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useMemo } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import {
|
||||
Code,
|
||||
|
|
@ -19,9 +20,10 @@ import {
|
|||
} from "lucide-react"
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { ScatterChart, Scatter, XAxis, YAxis, ZAxis, Tooltip, ResponsiveContainer, Cell, ReferenceLine } from "recharts"
|
||||
|
||||
import type { EngineerRole, RoleRecommendation } from "@/lib/mock-recommendations"
|
||||
import { TASKS_PER_DAY } from "@/lib/mock-recommendations"
|
||||
import { TASKS_PER_DAY, MODEL_TIMELINE } from "@/lib/mock-recommendations"
|
||||
|
||||
// ── Icon Mapping ────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -185,6 +187,82 @@ const backgroundVariants = {
|
|||
},
|
||||
}
|
||||
|
||||
// ── Provider Colors ─────────────────────────────────────────────────────────
|
||||
|
||||
const PROVIDER_COLORS: Record<string, string> = {
|
||||
anthropic: "#fb923c", // orange-400
|
||||
openai: "#4ade80", // green-400
|
||||
google: "#60a5fa", // blue-400
|
||||
xai: "#c084fc", // purple-400
|
||||
deepseek: "#22d3ee", // cyan-400
|
||||
moonshot: "#f472b6", // pink-400
|
||||
}
|
||||
|
||||
const PROVIDER_DISPLAY: Record<string, string> = {
|
||||
anthropic: "Anthropic",
|
||||
openai: "OpenAI",
|
||||
google: "Google",
|
||||
xai: "xAI",
|
||||
deepseek: "DeepSeek",
|
||||
moonshot: "Moonshot",
|
||||
}
|
||||
|
||||
// ── Timeline Tooltip ────────────────────────────────────────────────────────
|
||||
|
||||
function TimelineTooltip({
|
||||
active,
|
||||
payload,
|
||||
}: {
|
||||
active?: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
payload?: any[]
|
||||
}) {
|
||||
if (!active || !payload || !payload.length) return null
|
||||
|
||||
const data = payload[0]?.payload as
|
||||
| {
|
||||
modelName?: string
|
||||
provider?: string
|
||||
score?: number
|
||||
costPerRun?: number
|
||||
dateLabel?: string
|
||||
}
|
||||
| undefined
|
||||
|
||||
if (!data) return null
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-border/50 bg-card/95 p-4 shadow-2xl backdrop-blur-md">
|
||||
<p className="mb-2.5 text-sm font-bold tracking-tight">{data.modelName}</p>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
<span
|
||||
className="size-2.5 rounded-full ring-1 ring-white/10"
|
||||
style={{ backgroundColor: PROVIDER_COLORS[data.provider ?? ""] ?? "#94a3b8" }}
|
||||
/>
|
||||
<span className="text-muted-foreground">Provider:</span>
|
||||
<span className="font-semibold">{PROVIDER_DISPLAY[data.provider ?? ""] ?? data.provider}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
<span className="size-2.5 rounded-full bg-blue-400 ring-1 ring-white/10" />
|
||||
<span className="text-muted-foreground">Release:</span>
|
||||
<span className="font-semibold">{data.dateLabel}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
<span className="size-2.5 rounded-full bg-emerald-400 ring-1 ring-white/10" />
|
||||
<span className="text-muted-foreground">Eval Score:</span>
|
||||
<span className="font-semibold tabular-nums">{data.score}%</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5 text-xs">
|
||||
<span className="size-2.5 rounded-full bg-amber-400 ring-1 ring-white/10" />
|
||||
<span className="text-muted-foreground">Cost per Run:</span>
|
||||
<span className="font-semibold tabular-nums">${data.costPerRun?.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Sub-Components ──────────────────────────────────────────────────────────
|
||||
|
||||
function StatPill({ icon: Icon, value, label }: { icon: LucideIcon; value: string; label: string }) {
|
||||
|
|
@ -218,6 +296,33 @@ export function WorkersContent({
|
|||
}: WorkersContentProps) {
|
||||
const recByRole = new Map(recommendations.map((r) => [r.roleId, r]))
|
||||
|
||||
// ── Timeline scatter data ──────────────────────────────────────────────
|
||||
const timelineData = useMemo(() => {
|
||||
const maxCost = Math.max(...MODEL_TIMELINE.map((m) => m.costPerRun))
|
||||
return MODEL_TIMELINE.map((m) => {
|
||||
const date = new Date(m.releaseDate)
|
||||
return {
|
||||
modelName: m.modelName,
|
||||
provider: m.provider,
|
||||
score: m.score,
|
||||
costPerRun: m.costPerRun,
|
||||
// numeric X for scatter: days since epoch
|
||||
dateNum: date.getTime(),
|
||||
dateLabel: date.toLocaleDateString("en-US", { month: "short", year: "numeric" }),
|
||||
// Dot size: inversely proportional to cost (cheaper = bigger dot)
|
||||
dotSize: Math.round(60 + (1 - m.costPerRun / maxCost) * 340),
|
||||
}
|
||||
}).sort((a, b) => a.dateNum - b.dateNum)
|
||||
}, [])
|
||||
|
||||
// Trend line endpoints for the timeline
|
||||
const trendLine = useMemo(() => {
|
||||
if (timelineData.length < 2) return null
|
||||
const first = timelineData[0]!
|
||||
const last = timelineData[timelineData.length - 1]!
|
||||
return { x1: first.dateNum, y1: first.score, x2: last.dateNum, y2: last.score }
|
||||
}, [timelineData])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ── Hero Section ───────────────────────────────────────────── */}
|
||||
|
|
@ -455,6 +560,164 @@ export function WorkersContent({
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── AI Coding Capability Over Time ─────────────────────────── */}
|
||||
<section className="relative overflow-hidden pb-24 pt-8">
|
||||
{/* Subtle atmospheric 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-[600px] w-full -translate-x-1/2 -translate-y-1/2 rounded-[100%] bg-emerald-500/5 dark:bg-emerald-600/8 blur-[120px]" />
|
||||
</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
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={containerVariants}>
|
||||
{/* Section header */}
|
||||
<motion.div className="mb-8 text-center" variants={fadeUpVariants}>
|
||||
<h2 className="text-3xl font-bold tracking-tight md:text-4xl">
|
||||
AI Coding Capability{" "}
|
||||
<span className="bg-gradient-to-r from-emerald-500 to-blue-500 bg-clip-text text-transparent">
|
||||
Over Time
|
||||
</span>
|
||||
</h2>
|
||||
<p className="mt-3 text-base leading-relaxed text-muted-foreground md:text-lg">
|
||||
Pass rates on our eval suite, by model release date. The best ones now score 100%.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Chart container */}
|
||||
<motion.div
|
||||
className="rounded-2xl border border-border/50 bg-card/50 p-6 backdrop-blur-sm"
|
||||
variants={fadeUpVariants}>
|
||||
{/* Provider legend */}
|
||||
<div className="mb-4 flex flex-wrap items-center justify-center gap-5 text-xs text-muted-foreground">
|
||||
{Object.entries(PROVIDER_COLORS)
|
||||
.filter(([provider]) => MODEL_TIMELINE.some((m) => m.provider === provider))
|
||||
.map(([provider, color]) => (
|
||||
<div key={provider} className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="size-2.5 rounded-full ring-1 ring-white/10"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<span>{PROVIDER_DISPLAY[provider] ?? provider}</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground/60">
|
||||
<span className="text-[10px]">●</span>
|
||||
<span>Bigger dot = lower cost</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl bg-background/30 p-2">
|
||||
<ResponsiveContainer width="100%" height={420}>
|
||||
<ScatterChart margin={{ top: 20, right: 30, bottom: 20, left: 10 }}>
|
||||
<XAxis
|
||||
type="number"
|
||||
dataKey="dateNum"
|
||||
name="Release Date"
|
||||
domain={["dataMin", "dataMax"]}
|
||||
tickFormatter={(v: number) => {
|
||||
const d = new Date(v)
|
||||
return d.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
year: "2-digit",
|
||||
})
|
||||
}}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
strokeOpacity={0.3}
|
||||
tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }}
|
||||
axisLine={false}
|
||||
label={{
|
||||
value: "Release Date",
|
||||
position: "insideBottom",
|
||||
offset: -10,
|
||||
style: { fontSize: 11, fill: "hsl(var(--muted-foreground))" },
|
||||
}}
|
||||
/>
|
||||
<YAxis
|
||||
type="number"
|
||||
dataKey="score"
|
||||
name="Eval Score"
|
||||
domain={[85, 102]}
|
||||
tickFormatter={(v: number) => `${v}%`}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
strokeOpacity={0.3}
|
||||
tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }}
|
||||
axisLine={false}
|
||||
label={{
|
||||
value: "Eval Score (%)",
|
||||
angle: -90,
|
||||
position: "insideLeft",
|
||||
offset: 10,
|
||||
style: { fontSize: 11, fill: "hsl(var(--muted-foreground))" },
|
||||
}}
|
||||
/>
|
||||
<ZAxis type="number" dataKey="dotSize" range={[60, 400]} />
|
||||
{/* Trend line: dashed line from first to last */}
|
||||
{trendLine && (
|
||||
<ReferenceLine
|
||||
segment={[
|
||||
{ x: trendLine.x1, y: trendLine.y1 },
|
||||
{ x: trendLine.x2, y: trendLine.y2 },
|
||||
]}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
strokeOpacity={0.25}
|
||||
strokeDasharray="6 4"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)}
|
||||
{/* 100% reference line */}
|
||||
<ReferenceLine
|
||||
y={100}
|
||||
stroke="hsl(var(--muted-foreground))"
|
||||
strokeOpacity={0.15}
|
||||
strokeDasharray="3 3"
|
||||
label={{
|
||||
value: "Perfect Score",
|
||||
position: "right",
|
||||
style: {
|
||||
fontSize: 10,
|
||||
fill: "hsl(var(--muted-foreground))",
|
||||
fillOpacity: 0.5,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
content={<TimelineTooltip />}
|
||||
cursor={{
|
||||
strokeDasharray: "3 3",
|
||||
stroke: "hsl(var(--muted-foreground))",
|
||||
strokeOpacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
<Scatter data={timelineData} name="Models">
|
||||
{timelineData.map((entry, index) => (
|
||||
<Cell
|
||||
key={`timeline-cell-${index}`}
|
||||
fill={PROVIDER_COLORS[entry.provider] ?? "#94a3b8"}
|
||||
fillOpacity={0.85}
|
||||
stroke={PROVIDER_COLORS[entry.provider] ?? "#94a3b8"}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={0.4}
|
||||
/>
|
||||
))}
|
||||
</Scatter>
|
||||
</ScatterChart>
|
||||
</ResponsiveContainer>
|
||||
</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">
|
||||
|
|
|
|||
|
|
@ -864,3 +864,27 @@ export function getCloudSetupUrl(candidate: ModelCandidate): string {
|
|||
})
|
||||
return `https://app.roocode.com/sign-up?${params.toString()}`
|
||||
}
|
||||
|
||||
// ── Model Timeline Data ────────────────────────────────────────────────────
|
||||
// Historical model performance over time for the landing page chart.
|
||||
|
||||
export type ModelTimelineEntry = {
|
||||
modelName: string
|
||||
provider: string
|
||||
releaseDate: string // ISO date
|
||||
score: number // our eval score (total %)
|
||||
costPerRun: number // total cost for the full eval run
|
||||
}
|
||||
|
||||
export const MODEL_TIMELINE: ModelTimelineEntry[] = [
|
||||
{ modelName: "Claude 3.5 Sonnet", provider: "anthropic", releaseDate: "2025-06-20", score: 90, costPerRun: 24.98 },
|
||||
{ modelName: "GPT-4.1", provider: "openai", releaseDate: "2025-08-14", score: 91, costPerRun: 38.64 },
|
||||
{ modelName: "Claude 3.7 Sonnet", provider: "anthropic", releaseDate: "2025-09-15", score: 95, costPerRun: 37.58 },
|
||||
{ modelName: "Gemini 2.5 Pro", provider: "google", releaseDate: "2025-10-01", score: 96, costPerRun: 57.8 },
|
||||
{ modelName: "Claude Sonnet 4", provider: "anthropic", releaseDate: "2025-11-01", score: 98, costPerRun: 39.61 },
|
||||
{ modelName: "GPT-5 Mini", provider: "openai", releaseDate: "2025-12-01", score: 99, costPerRun: 3.34 },
|
||||
{ modelName: "Claude Sonnet 4.5", provider: "anthropic", releaseDate: "2026-01-15", score: 100, costPerRun: 38.43 },
|
||||
{ modelName: "GPT 5.2 (Med)", provider: "openai", releaseDate: "2026-01-20", score: 100, costPerRun: 12.5 },
|
||||
{ modelName: "Claude Opus 4.6", provider: "anthropic", releaseDate: "2026-02-01", score: 100, costPerRun: 49.48 },
|
||||
{ modelName: "Gemini 3 Pro", provider: "google", releaseDate: "2026-02-05", score: 100, costPerRun: 33.06 },
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue