|
|
|
|
|
"use client"; |
|
|
|
import React, { useState, useEffect, useMemo } from "react"; |
|
import { |
|
RadarChart, |
|
PolarGrid, |
|
PolarAngleAxis, |
|
PolarRadiusAxis, |
|
Radar, |
|
Tooltip as RechartsTooltip, |
|
Legend, |
|
ResponsiveContainer, |
|
} from "recharts"; |
|
import { getScoreColor, getMetricTooltip } from "../lib/utils"; |
|
import { Tooltip } from "./Tooltip"; |
|
|
|
|
|
const MetricsBreakdown = ({ |
|
metricsData, |
|
modelsMeta, |
|
radarData: categoryRadarDataProp, // Already processed radar data for categories |
|
}) => { |
|
const [subTab, setSubTab] = useState("categories"); |
|
const [selectedModels, setSelectedModels] = useState([]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
const { highLevelCategories, lowLevelMetrics } = metricsData || { |
|
highLevelCategories: {}, |
|
lowLevelMetrics: {}, |
|
}; |
|
|
|
const models = modelsMeta || []; |
|
|
|
|
|
const sortedCategoryNames = useMemo( |
|
() => |
|
Object.keys(highLevelCategories || {}).sort((a, b) => a.localeCompare(b)), |
|
[highLevelCategories] |
|
); |
|
const sortedMetricNames = useMemo( |
|
() => Object.keys(lowLevelMetrics || {}).sort((a, b) => a.localeCompare(b)), |
|
[lowLevelMetrics] |
|
); |
|
|
|
|
|
useEffect(() => { |
|
if (selectedModels.length === 0 && models.length > 0) { |
|
setSelectedModels(models.map((m) => m.model)); |
|
} |
|
|
|
}, [models]); |
|
|
|
|
|
|
|
|
|
const metricRadarData = useMemo(() => { |
|
if ( |
|
!lowLevelMetrics || |
|
models.length === 0 || |
|
sortedMetricNames.length === 0 |
|
) |
|
return []; |
|
return sortedMetricNames.map((metricName) => { |
|
const entry = { category: metricName }; |
|
const metricData = lowLevelMetrics[metricName]; |
|
if (metricData) { |
|
models |
|
.filter((m) => selectedModels.includes(m.model)) |
|
.forEach((model) => { |
|
|
|
entry[model.model] = |
|
Number(metricData.modelScores?.[model.model]?.nationalScore) || 0; |
|
|
|
}); |
|
} |
|
return entry; |
|
}); |
|
}, [lowLevelMetrics, models, selectedModels, sortedMetricNames]); |
|
|
|
|
|
const CustomRadarTooltip = ({ active, payload, label }) => { |
|
if (active && payload && payload.length) { |
|
return ( |
|
<div className="bg-white p-3 border rounded shadow-lg max-w-xs opacity-95"> |
|
<p className="font-medium mb-1 text-gray-800">{label}</p> |
|
{/* Get tooltip description for the category/metric itself */} |
|
<p className="text-xs mb-3 text-gray-600 border-b pb-2"> |
|
{getMetricTooltip(label)} |
|
</p> |
|
<div className="space-y-1"> |
|
{payload |
|
// Sort models by score within tooltip |
|
.sort((a, b) => (b.value || 0) - (a.value || 0)) |
|
.map((entry) => ( |
|
<div |
|
key={entry.dataKey} // dataKey is the model name here |
|
className="flex items-center text-sm" |
|
> |
|
<div |
|
className="w-2.5 h-2.5 rounded-full mr-2 flex-shrink-0" |
|
style={{ backgroundColor: entry.color || "#8884d8" }} |
|
></div> |
|
<span className="mr-1 truncate flex-grow text-gray-700"> |
|
{entry.name}: {/* name is also the model name */} |
|
</span> |
|
<span className="font-medium flex-shrink-0 text-gray-900"> |
|
{/* Ensure value exists and format */} |
|
{entry.value !== null && entry.value !== undefined |
|
? Number(entry.value).toFixed(1) |
|
: "N/A"} |
|
{/* Removed standard deviation display */} |
|
</span> |
|
</div> |
|
))} |
|
</div> |
|
</div> |
|
); |
|
} |
|
return null; |
|
}; |
|
|
|
|
|
const filteredCategoryRadarData = useMemo(() => { |
|
if (!categoryRadarDataProp || models.length === 0) return []; |
|
|
|
return categoryRadarDataProp.map((item) => { |
|
const newItem = { category: item.category }; |
|
models |
|
.filter((m) => selectedModels.includes(m.model)) |
|
.forEach((model) => { |
|
|
|
newItem[model.model] = item[model.model] ?? 0; |
|
}); |
|
return newItem; |
|
}); |
|
}, [categoryRadarDataProp, models, selectedModels]); |
|
|
|
return ( |
|
<> |
|
{/* Top Controls: Model Selector & Sub-Tab Pills (No changes needed) */} |
|
<div className="mb-6 flex flex-col md:flex-row justify-between items-center gap-4"> |
|
{/* Sub-Tab Pills */} |
|
<div className="flex space-x-1 p-1 bg-gray-200 rounded-lg"> |
|
{" "} |
|
<button |
|
aria-pressed={subTab === "categories"} |
|
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-colors duration-150 ${ |
|
subTab === "categories" |
|
? "bg-white shadow text-blue-600" |
|
: "text-gray-600 hover:text-gray-800" |
|
}`} |
|
onClick={() => setSubTab("categories")} |
|
> |
|
{" "} |
|
High-Level Categories{" "} |
|
</button>{" "} |
|
<button |
|
aria-pressed={subTab === "metrics"} |
|
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-colors duration-150 ${ |
|
subTab === "metrics" |
|
? "bg-white shadow text-blue-600" |
|
: "text-gray-600 hover:text-gray-800" |
|
}`} |
|
onClick={() => setSubTab("metrics")} |
|
> |
|
{" "} |
|
Low-Level Metrics{" "} |
|
</button>{" "} |
|
</div> |
|
{/* Model Selector */} |
|
<div className="flex items-center flex-wrap gap-1"> |
|
{" "} |
|
<span className="text-sm text-gray-500 mr-2">Models:</span>{" "} |
|
{models?.map((model) => ( |
|
<button |
|
key={model.model} |
|
className={`px-2 py-0.5 text-xs rounded border ${ |
|
selectedModels.includes(model.model) |
|
? "bg-sky-100 text-sky-800 border-sky-300 font-medium" |
|
: "bg-gray-100 text-gray-600 border-gray-300 hover:bg-gray-200" |
|
}`} |
|
onClick={() => { |
|
if (selectedModels.includes(model.model)) { |
|
if (selectedModels.length > 1) { |
|
setSelectedModels( |
|
selectedModels.filter((m) => m !== model.model) |
|
); |
|
} |
|
} else { |
|
setSelectedModels([...selectedModels, model.model]); |
|
} |
|
}} |
|
> |
|
{" "} |
|
{model.model}{" "} |
|
</button> |
|
))}{" "} |
|
</div> |
|
</div> |
|
|
|
{/* Conditional content based on sub-tab */} |
|
{subTab === "categories" && ( |
|
<div className="space-y-6"> |
|
{/* CATEGORIES VIEW */} |
|
{/* Summary Table: Models as Rows, Categories as Columns - CORRECTED ACCESSORS */} |
|
<div className="border rounded-lg overflow-hidden shadow-sm"> |
|
<div className="px-4 py-3 bg-gray-50 border-b"> |
|
<h3 className="font-semibold text-gray-800"> |
|
Category Performance Summary |
|
</h3> |
|
</div> |
|
<div className="p-4 overflow-x-auto"> |
|
{sortedCategoryNames.length > 0 ? ( |
|
<table className="min-w-full divide-y divide-gray-200 border border-gray-200"> |
|
<thead> |
|
<tr className="bg-gray-100"> |
|
<th |
|
scope="col" |
|
className="sticky left-0 bg-gray-100 px-3 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider z-10" |
|
> |
|
Model |
|
</th> |
|
{sortedCategoryNames.map((catName) => ( |
|
<th |
|
key={catName} |
|
scope="col" |
|
className="px-3 py-2 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider whitespace-nowrap" |
|
> |
|
{catName} |
|
</th> |
|
))} |
|
</tr> |
|
</thead> |
|
<tbody className="bg-white divide-y divide-gray-200"> |
|
{models |
|
?.filter((m) => selectedModels.includes(m.model)) |
|
.map((model, idx) => ( |
|
<tr |
|
key={model.model} |
|
className={ |
|
idx % 2 === 0 |
|
? "bg-white hover:bg-gray-50" |
|
: "bg-gray-50 hover:bg-gray-100" |
|
} |
|
> |
|
<td className="sticky left-0 bg-inherit px-3 py-2 whitespace-nowrap z-10 text-left"> |
|
{" "} |
|
{/* Keep sticky styles */} |
|
<div className="flex items-center"> |
|
<div |
|
className="w-3 h-3 rounded-full mr-2 shrink-0" |
|
style={{ backgroundColor: model.color }} |
|
></div> |
|
<span className="text-sm font-medium"> |
|
{model.model} |
|
</span> |
|
</div> |
|
</td> |
|
{sortedCategoryNames.map((catName) => { |
|
// Use correct camelCase keys |
|
const scoreData = |
|
highLevelCategories[catName]?.modelScores?.[ |
|
model.model |
|
]; |
|
const score = scoreData?.nationalScore; // Access camelCase key |
|
const displayScore = |
|
score !== null && score !== undefined |
|
? Number(score).toFixed(1) |
|
: "N/A"; |
|
return ( |
|
<td |
|
key={catName} |
|
className="px-3 py-2 whitespace-nowrap text-center" |
|
> |
|
<div |
|
className={`text-sm ${ |
|
displayScore === "N/A" |
|
? "text-gray-400" |
|
: getScoreColor(score) |
|
}`} |
|
> |
|
{displayScore} |
|
</div> |
|
</td> |
|
); |
|
})} |
|
</tr> |
|
))} |
|
</tbody> |
|
</table> |
|
) : ( |
|
<p className="text-center text-gray-500 py-4"> |
|
No category data available. |
|
</p> |
|
)} |
|
</div> |
|
</div> |
|
|
|
{/* Radar Chart for Categories (Uses filteredCategoryRadarData) */} |
|
<div className="border rounded-lg overflow-hidden shadow-sm"> |
|
<div className="px-4 py-3 bg-gray-50 border-b flex justify-between items-center"> |
|
<h3 className="font-semibold text-gray-800"> |
|
Performance Across Categories |
|
</h3> |
|
<div className="text-xs text-gray-500"> |
|
National Average Scores |
|
</div> |
|
</div> |
|
<div className="p-4"> |
|
{filteredCategoryRadarData && |
|
filteredCategoryRadarData.length > 0 ? ( |
|
<div className="h-96 md:h-[450px]"> |
|
<ResponsiveContainer width="100%" height="100%"> |
|
<RadarChart |
|
outerRadius="80%" |
|
data={filteredCategoryRadarData} |
|
> |
|
<PolarGrid gridType="polygon" stroke="#e5e7eb" /> |
|
<PolarAngleAxis |
|
dataKey="category" |
|
tick={{ fill: "#4b5563", fontSize: 12 }} |
|
/> |
|
<PolarRadiusAxis |
|
angle={90} |
|
domain={[0, 100]} |
|
axisLine={false} |
|
tick={{ fill: "#6b7280", fontSize: 10 }} |
|
/> |
|
{models |
|
?.filter((m) => selectedModels.includes(m.model)) |
|
.map((model) => ( |
|
<Radar |
|
key={model.model} |
|
name={model.model} |
|
dataKey={model.model} |
|
stroke={model.color} |
|
fill={model.color} |
|
fillOpacity={0.1} |
|
strokeWidth={2} |
|
/> |
|
))} |
|
{/* Use the corrected CustomRadarTooltip */} |
|
<RechartsTooltip content={<CustomRadarTooltip />} /> |
|
<Legend |
|
iconSize={10} |
|
wrapperStyle={{ fontSize: "12px", paddingTop: "20px" }} |
|
/> |
|
</RadarChart> |
|
</ResponsiveContainer> |
|
</div> |
|
) : ( |
|
<p className="text-center text-gray-500 py-4"> |
|
Radar data not available. |
|
</p> |
|
)} |
|
<p className="text-xs text-gray-500 mt-4"> |
|
This radar chart visualizes how each model performs across |
|
different high-level evaluation categories. The further out on |
|
each axis, the better the performance on that category. |
|
</p> |
|
</div> |
|
</div> |
|
</div> |
|
)} |
|
|
|
{subTab === "metrics" && ( |
|
<div className="space-y-6"> |
|
{/* METRICS VIEW */} |
|
{/* Radar Chart for Metrics (Uses metricRadarData) */} |
|
<div className="border rounded-lg overflow-hidden shadow-sm"> |
|
<div className="px-4 py-3 bg-gray-50 border-b flex justify-between items-center"> |
|
<h3 className="font-semibold text-gray-800"> |
|
Performance Across All Metrics |
|
</h3> |
|
<div className="text-xs text-gray-500"> |
|
National Average Scores |
|
</div> |
|
</div> |
|
<div className="p-4"> |
|
{metricRadarData.length > 0 ? ( |
|
<div className="h-96 md:h-[600px]"> |
|
{" "} |
|
{/* Increased height */} |
|
<ResponsiveContainer width="100%" height="100%"> |
|
<RadarChart outerRadius="80%" data={metricRadarData}> |
|
{" "} |
|
{/* Use metricRadarData */} |
|
<PolarGrid gridType="polygon" stroke="#e5e7eb" /> |
|
<PolarAngleAxis |
|
dataKey="category" |
|
tick={{ fill: "#4b5563", fontSize: 10 }} |
|
/>{" "} |
|
{/* Adjusted font size */} |
|
<PolarRadiusAxis |
|
angle={90} |
|
domain={[0, 100]} |
|
axisLine={false} |
|
tick={{ fill: "#6b7280", fontSize: 10 }} |
|
/> |
|
{models |
|
?.filter((m) => selectedModels.includes(m.model)) |
|
.map((model) => ( |
|
<Radar |
|
key={model.model} |
|
name={model.model} |
|
dataKey={model.model} |
|
stroke={model.color} |
|
fill={model.color} |
|
fillOpacity={0.1} |
|
strokeWidth={2} |
|
/> |
|
))} |
|
{/* Use the corrected CustomRadarTooltip */} |
|
<RechartsTooltip content={<CustomRadarTooltip />} /> |
|
<Legend |
|
iconSize={10} |
|
wrapperStyle={{ fontSize: "12px", paddingTop: "20px" }} |
|
/> |
|
</RadarChart> |
|
</ResponsiveContainer> |
|
</div> |
|
) : ( |
|
<p className="text-center text-gray-500 py-4"> |
|
Metric data not available for radar chart. |
|
</p> |
|
)} |
|
<p className="text-xs text-gray-500 mt-4"> |
|
This radar chart visualizes how each model performs across |
|
different low-level metrics. The further out on each axis, the |
|
better the performance on that metric. |
|
</p> |
|
</div> |
|
</div> |
|
{/* Optional: Add a table summary for low-level metrics similar to the categories one if desired */} |
|
</div> |
|
)} |
|
</> |
|
); |
|
}; |
|
|
|
export default MetricsBreakdown; |
|
|