From baf0d5c3b7387f53b3a071fec63fdde7fec412b0 Mon Sep 17 00:00:00 2001 From: Michael Preuss Date: Wed, 11 Feb 2026 19:39:45 -0800 Subject: [PATCH] feat(web-evals): redesign comparison page with glass-morphism and role themes - Atmospheric header with role-colored blur gradients - Glass-morphism containers for chart, filters, and export - Styled language toggle pills with role color accents - Themed provider checkboxes and success rate slider - Custom chart tooltip with backdrop blur - Export buttons with press feedback - framer-motion scroll-triggered animations - Bottom navigation with pill-style links - Role themes: reviewer (violet) and autonomous (cyan) added to candidates page --- .../[roleId]/compare/comparison-chart.tsx | 811 +++++++++++++----- 1 file changed, 584 insertions(+), 227 deletions(-) diff --git a/apps/web-roo-code/src/app/evals/workers/[roleId]/compare/comparison-chart.tsx b/apps/web-roo-code/src/app/evals/workers/[roleId]/compare/comparison-chart.tsx index 4b87bbb1cf..7029c90010 100644 --- a/apps/web-roo-code/src/app/evals/workers/[roleId]/compare/comparison-chart.tsx +++ b/apps/web-roo-code/src/app/evals/workers/[roleId]/compare/comparison-chart.tsx @@ -2,12 +2,188 @@ import { useState, useMemo, useCallback } from "react" import Link from "next/link" -import { ArrowLeft, Copy, Check, FileJson, FileSpreadsheet } from "lucide-react" +import { motion } from "framer-motion" +import { + ArrowLeft, + ArrowRight, + Copy, + Check, + FileJson, + FileSpreadsheet, + BarChart3, + SlidersHorizontal, + Download, + FlaskConical, +} 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" +// ── Role Color Themes (matching candidates-content.tsx) ───────────────────── + +type RoleTheme = { + accent: string + accentLight: string + accentDark: string + iconBg: string + iconText: string + buttonBg: string + buttonHover: string + glowColor: string + blurBg1: string + blurBg2: string + borderHover: string + shadowHover: string + methodologyBorder: string + scoreText: string + pillActive: string + pillActiveBg: string + checkboxAccent: string + sliderAccent: string +} + +const ROLE_THEMES: Record = { + 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", + 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", + blurBg1: "bg-emerald-500/10 dark:bg-emerald-600/20", + blurBg2: "bg-emerald-400/5 dark:bg-emerald-500/10", + borderHover: "hover:border-emerald-500/40 dark:hover:border-emerald-400/30", + shadowHover: "hover:shadow-emerald-500/10 dark:hover:shadow-emerald-400/10", + methodologyBorder: "border-emerald-500/30 hover:border-emerald-500/50", + scoreText: "text-emerald-400", + pillActive: "bg-emerald-600 text-white shadow-lg shadow-emerald-600/25", + pillActiveBg: "bg-emerald-600", + checkboxAccent: "accent-emerald-600", + sliderAccent: "accent-emerald-600", + }, + 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", + 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", + blurBg1: "bg-blue-500/10 dark:bg-blue-600/20", + blurBg2: "bg-blue-400/5 dark:bg-blue-500/10", + borderHover: "hover:border-blue-500/40 dark:hover:border-blue-400/30", + shadowHover: "hover:shadow-blue-500/10 dark:hover:shadow-blue-400/10", + methodologyBorder: "border-blue-500/30 hover:border-blue-500/50", + scoreText: "text-blue-400", + pillActive: "bg-blue-600 text-white shadow-lg shadow-blue-600/25", + pillActiveBg: "bg-blue-600", + checkboxAccent: "accent-blue-600", + sliderAccent: "accent-blue-600", + }, + 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", + 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", + blurBg1: "bg-amber-500/10 dark:bg-amber-600/20", + blurBg2: "bg-amber-400/5 dark:bg-amber-500/10", + borderHover: "hover:border-amber-500/40 dark:hover:border-amber-400/30", + shadowHover: "hover:shadow-amber-500/10 dark:hover:shadow-amber-400/10", + methodologyBorder: "border-amber-500/30 hover:border-amber-500/50", + scoreText: "text-amber-400", + pillActive: "bg-amber-600 text-white shadow-lg shadow-amber-600/25", + pillActiveBg: "bg-amber-600", + checkboxAccent: "accent-amber-600", + sliderAccent: "accent-amber-600", + }, + 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", + 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", + blurBg1: "bg-violet-500/10 dark:bg-violet-600/20", + blurBg2: "bg-violet-400/5 dark:bg-violet-500/10", + borderHover: "hover:border-violet-500/40 dark:hover:border-violet-400/30", + shadowHover: "hover:shadow-violet-500/10 dark:hover:shadow-violet-400/10", + methodologyBorder: "border-violet-500/30 hover:border-violet-500/50", + scoreText: "text-violet-400", + pillActive: "bg-violet-600 text-white shadow-lg shadow-violet-600/25", + pillActiveBg: "bg-violet-600", + checkboxAccent: "accent-violet-600", + sliderAccent: "accent-violet-600", + }, + 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", + 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", + blurBg1: "bg-cyan-500/10 dark:bg-cyan-600/20", + blurBg2: "bg-cyan-400/5 dark:bg-cyan-500/10", + borderHover: "hover:border-cyan-500/40 dark:hover:border-cyan-400/30", + shadowHover: "hover:shadow-cyan-500/10 dark:hover:shadow-cyan-400/10", + methodologyBorder: "border-cyan-500/30 hover:border-cyan-500/50", + scoreText: "text-cyan-400", + pillActive: "bg-cyan-600 text-white shadow-lg shadow-cyan-600/25", + pillActiveBg: "bg-cyan-600", + checkboxAccent: "accent-cyan-600", + sliderAccent: "accent-cyan-600", + }, +} + +const DEFAULT_THEME = ROLE_THEMES.senior! + +// ── 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, + }, + }, +} + // ── Constants ─────────────────────────────────────────────────────────────── const LANGUAGES: { key: keyof LanguageScores; label: string }[] = [ @@ -18,7 +194,17 @@ const LANGUAGES: { key: keyof LanguageScores; label: string }[] = [ { key: "rust", label: "Rust" }, ] -const PROVIDERS = ["anthropic", "openai", "google", "deepseek", "groq", "alibaba", "mistral"] as const +const PROVIDERS = [ + "anthropic", + "openai", + "google", + "deepseek", + "groq", + "alibaba", + "mistral", + "xai", + "moonshot", +] as const const PROVIDER_LABELS: Record = { anthropic: "Anthropic", @@ -28,6 +214,8 @@ const PROVIDER_LABELS: Record = { groq: "Meta/Groq", alibaba: "Alibaba", mistral: "Mistral", + xai: "xAI", + moonshot: "Moonshot", } const DIMENSION_COLORS = { @@ -124,29 +312,34 @@ function CustomTooltip({ const costPerTask = rawData?.costPerTask return ( -
-

{label}

- {payload.map( - ( - entry: { - name: string - value: number - color: string - dataKey: string - }, - index: number, - ) => ( -
- - {entry.name}: - - {entry.dataKey === "costEfficiency" && dailyCost !== undefined - ? `${entry.value} (~$${dailyCost}/day · $${costPerTask?.toFixed(3)}/task)` - : entry.value} - -
- ), - )} +
+

{label}

+
+ {payload.map( + ( + entry: { + name: string + value: number + color: string + dataKey: string + }, + index: number, + ) => ( +
+ + {entry.name}: + + {entry.dataKey === "costEfficiency" && dailyCost !== undefined + ? `${entry.value} (~$${dailyCost}/day · $${costPerTask?.toFixed(3)}/task)` + : entry.value} + +
+ ), + )} +
) } @@ -161,6 +354,7 @@ interface ComparisonChartProps { export function ComparisonChart({ recommendation, role, roleId }: ComparisonChartProps) { const { allCandidates } = recommendation + const theme = ROLE_THEMES[roleId] ?? DEFAULT_THEME // ── State ─────────────────────────────────────────────────────────────── const [selectedLanguage, setSelectedLanguage] = useState("all") @@ -186,6 +380,12 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar const chartHeight = Math.max(300, chartData.length * 60 + 80) + // Providers that actually appear in data + const activeProviders = useMemo(() => { + const providers = new Set(allCandidates.map((c) => c.provider)) + return PROVIDERS.filter((p) => providers.has(p)) + }, [allCandidates]) + // ── Handlers ──────────────────────────────────────────────────────────── const toggleProvider = useCallback((provider: string) => { @@ -229,215 +429,372 @@ export function ComparisonChart({ recommendation, role, roleId }: ComparisonChar // ── Render ────────────────────────────────────────────────────────────── return ( -
- {/* ── Breadcrumb ─────────────────────────────────────────────── */} - - - {/* ── Page Header ────────────────────────────────────────────── */} -
-

Compare Candidates — {role.name}

-

- Interactive comparison across composite score, success rate, cost efficiency, and speed. -

-
- - {/* ── Language Toggle ─────────────────────────────────────────── */} -
-

- Score View -

-
- - {LANGUAGES.map(({ key, label }) => ( - - ))} -
-
- - {/* ── Filters ────────────────────────────────────────────────── */} -
- {/* Provider checkboxes */} -
-

- Providers -

-
- {PROVIDERS.map((p) => ( - - ))} -
-
- - {/* Min success rate slider */} -
-

- Min Success Rate -

-
- setMinSuccessRate(Number(e.target.value))} - className="h-2 flex-1 cursor-pointer accent-blue-600" + <> + {/* ── Atmospheric Header ────────────────────────────────────── */} +
+ {/* Blur gradient background in role color */} + +
+
- {minSuccessRate}% +
+
+ + +
+ + {/* Breadcrumb */} + + + Evals + + / + + Hire an AI Engineer + + / + + {role.name} + + / + Compare Candidates + + + {/* Title row */} + +
+ +
+
+

Compare Candidates

+

+ {role.name} +

+

+ Interactive comparison across composite score, success rate, cost efficiency, and + speed. Filter by provider, language, and minimum success rate. +

+
+
+ + {/* Stats bar */} + + + + + {filteredCandidates.length} + + of {allCandidates.length} candidates shown + +
+ + Viewing{" "} + + {selectedLanguage === "all" + ? "All Languages" + : LANGUAGES.find((l) => l.key === selectedLanguage)?.label} + + + {minSuccessRate > 0 && ( + <> +
+ + Min success{" "} + + {minSuccessRate}% + + + + )} + +
- {/* ── Chart ──────────────────────────────────────────────────── */} -
-

- {selectedLanguage === "all" - ? "Composite Score" - : `${LANGUAGES.find((l) => l.key === selectedLanguage)?.label} Score`}{" "} - Comparison -

-

- 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). -

+ {/* ── Main Content ──────────────────────────────────────────── */} +
+ + {/* ── Filters Section ────────────────────────────────────── */} + +
+
+ +
+

+ Filters +

+
- {chartData.length === 0 ? ( -
- No candidates match the current filters. -
- ) : ( - - - `${v}`} /> - - } /> - - l.key === selectedLanguage)?.label ?? "Language"} Score` - } - fill={DIMENSION_COLORS.composite} - radius={[0, 4, 4, 0]} - barSize={12} - /> - - - - - - )} -
+
+ {/* Language toggle pills */} +
+

+ Score View +

+
+ + {LANGUAGES.map(({ key, label }) => ( + + ))} +
+
- {/* ── Export Buttons ──────────────────────────────────────────── */} -
- - - -
+ {/* Min success rate slider */} +
+

+ Min Success Rate +

+
+ setMinSuccessRate(Number(e.target.value))} + className={`h-2 flex-1 cursor-pointer appearance-none rounded-full bg-muted/50 ${theme.sliderAccent}`} + /> + + {minSuccessRate}% + +
+
+
- {/* ── Bottom Navigation ───────────────────────────────────────── */} - -
+ {/* Provider checkboxes */} +
+

+ Providers +

+
+ {activeProviders.map((p) => ( + + ))} +
+
+ + + {/* ── Chart Section ──────────────────────────────────────── */} + +
+

+ {selectedLanguage === "all" + ? "Composite Score" + : `${LANGUAGES.find((l) => l.key === selectedLanguage)?.label} Score`}{" "} + Comparison +

+
+

+ 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). +

+ + {chartData.length === 0 ? ( +
+ +

No candidates match the current filters.

+

+ Try adjusting the provider or success rate filters. +

+
+ ) : ( +
+ + + `${v}`} + stroke="hsl(var(--muted-foreground))" + strokeOpacity={0.3} + tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }} + axisLine={false} + /> + + } + cursor={{ fill: "hsl(var(--muted))", fillOpacity: 0.15 }} + /> + + l.key === selectedLanguage)?.label ?? "Language"} Score` + } + fill={DIMENSION_COLORS.composite} + radius={[0, 4, 4, 0]} + barSize={12} + /> + + + + + +
+ )} +
+ + {/* ── Export Section ──────────────────────────────────────── */} + +
+
+ +
+

+ Export Data +

+
+ +
+ + + +
+
+ + {/* ── Bottom Navigation ───────────────────────────────────── */} + +
+
+ + + Back to {role.name} candidates + +
+
+ + + All roles + + + 📋 Raw eval data + + +
+
+
+ +
+ ) }