Nora Petrova
Add project to new space
20e666e
raw
history blame
27.4 kB
// components/TaskPerformance.jsx
"use client";
import React, { useState, useMemo, useEffect } from "react";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip as RechartsTooltip,
ResponsiveContainer,
Cell,
} from "recharts";
import {
getMetricTooltip,
getScoreBadgeColor,
formatDisplayKey,
camelToTitle,
} from "../lib/utils"; // Import formatDisplayKey
// Helper component for info tooltips
const InfoTooltip = ({ text }) => {
/* ... (no change) ... */
const [isVisible, setIsVisible] = useState(false);
return (
<div className="relative inline-block ml-1 align-middle">
<button
className="text-gray-400 hover:text-gray-600 focus:outline-none"
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
onClick={(e) => {
e.stopPropagation();
setIsVisible(!isVisible);
}}
aria-label="Info"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>{" "}
</button>{" "}
{isVisible && (
<div className="absolute z-10 w-64 p-2 bg-white border rounded shadow-lg text-xs text-gray-700 -translate-x-1/2 left-1/2 mt-1">
{text}
</div>
)}{" "}
</div>
);
};
// Custom tooltip for charts
const CustomTooltip = ({ active, payload, label }) => {
/* ... (no change needed) ... */
if (active && payload && payload.length) {
const sortedPayload = [...payload].sort(
(a, b) => (b.value || 0) - (a.value || 0)
);
return (
<div className="bg-white p-3 border rounded shadow-lg max-w-xs">
<p className="font-medium text-sm">{label}</p>{" "}
{sortedPayload.map((entry, index) => (
<div key={`item-${index}`} className="flex items-center mt-1">
<div
className="w-3 h-3 mr-2 rounded-full flex-shrink-0"
style={{
backgroundColor:
entry.payload?.color || entry.color || "#8884d8",
}}
></div>{" "}
<span className="text-xs flex-grow pr-2">{entry.name}: </span>{" "}
<span className="text-xs font-medium ml-1 whitespace-nowrap">
{typeof entry.value === "number" ? entry.value.toFixed(1) : "N/A"}
</span>{" "}
</div>
))}{" "}
</div>
);
}
return null;
};
// Tab component
const TabButton = ({ active, onClick, children }) => (
<button
aria-pressed={active}
className={`px-4 py-1.5 text-sm font-medium rounded-md transition-colors duration-150 ${
active
? "bg-white shadow text-blue-600"
: "text-gray-600 hover:text-gray-800"
}`}
onClick={onClick}
>
{children}{" "}
</button>
);
// Main component
const TaskPerformance = ({
rawData,
modelsMeta,
metricsData, // Expects Title Case keys (e.g., Context Memory) containing internalMetricKey
overviewCardData,
}) => {
const [activeTab, setActiveTab] = useState("top-performers");
// *** Use Title Case metric keys from processed metricsData ***
const highLevelMetricDisplayKeys = useMemo(
() => Object.keys(metricsData?.highLevelCategories || {}).sort(),
[metricsData?.highLevelCategories]
);
const lowLevelMetricDisplayKeys = useMemo(
() => Object.keys(metricsData?.lowLevelMetrics || {}).sort(),
[metricsData?.lowLevelMetrics]
);
// **************************************************************
// Access original snake_case keys from rawData
const { taskLevelPerformance = {}, tasks = [] } = rawData || {};
const { bestModelPerTask = {} } = overviewCardData || {};
const models = modelsMeta || [];
// State for 'Model Performance' tab
const [selectedTask, setSelectedTask] = useState(
tasks.length > 0 ? tasks[0] : "all"
);
const [selectedMetricType, setSelectedMetricType] = useState("high");
// *** selectedMetric now stores the Title Case display key ***
const [selectedMetricDisplayKey, setSelectedMetricDisplayKey] = useState("");
// ***********************************************************
const [selectedModels, setSelectedModels] = useState([]);
// Determine current metrics list (Title Case display keys)
const currentMetricDisplayKeysList = useMemo(
() =>
selectedMetricType === "high"
? highLevelMetricDisplayKeys
: lowLevelMetricDisplayKeys,
[selectedMetricType, highLevelMetricDisplayKeys, lowLevelMetricDisplayKeys]
);
// Load models on mount
useEffect(() => {
if (models.length > 0 && selectedModels.length === 0) {
setSelectedModels(models.map((m) => m.model));
}
}, [models, selectedModels.length]);
// Set default metric display key when the list or type changes
useEffect(() => {
if (currentMetricDisplayKeysList.length > 0) {
if (
!selectedMetricDisplayKey ||
!currentMetricDisplayKeysList.includes(selectedMetricDisplayKey)
) {
setSelectedMetricDisplayKey(currentMetricDisplayKeysList[0]); // Set to the first Title Case key
}
} else {
setSelectedMetricDisplayKey("");
}
}, [currentMetricDisplayKeysList, selectedMetricDisplayKey]);
// Prep chart data - *** UPDATED to use internalMetricKey looked up via selectedMetricDisplayKey ***
const chartData = useMemo(() => {
if (
!taskLevelPerformance ||
!selectedMetricDisplayKey ||
selectedModels.length === 0
)
return [];
// Find the internal snake_case key using the selected Title Case display name
const allMetricsProcessed = {
...(metricsData?.highLevelCategories || {}),
...(metricsData?.lowLevelMetrics || {}),
};
const metricInfo = allMetricsProcessed[selectedMetricDisplayKey]; // Look up using Title Case key
const internalMetricKey = metricInfo?.internalMetricKey; // Access the stored snake_case key
if (!internalMetricKey) {
console.warn(
`Could not find internal key for selected metric: ${selectedMetricDisplayKey}`
);
return [];
}
let data = [];
if (selectedTask === "all") {
const modelAggregates = {};
tasks.forEach((task) => {
if (taskLevelPerformance[task]) {
Object.entries(taskLevelPerformance[task]).forEach(
([model, metrics]) => {
if (selectedModels.includes(model)) {
// *** Use the FOUND snake_case internalMetricKey ***
const score = metrics?.[internalMetricKey];
if (score !== undefined && score !== null && score !== "N/A") {
const numScore = parseFloat(score);
if (!isNaN(numScore)) {
if (!modelAggregates[model])
modelAggregates[model] = { sum: 0, count: 0 };
modelAggregates[model].sum += numScore;
modelAggregates[model].count++;
}
}
}
}
);
}
});
data = Object.entries(modelAggregates).map(([model, aggregates]) => {
const modelMeta = models.find((m) => m.model === model) || {};
return {
model: model,
score:
aggregates.count > 0 ? aggregates.sum / aggregates.count : null,
color: modelMeta.color || "#999999",
};
});
} else if (taskLevelPerformance[selectedTask]) {
data = Object.entries(taskLevelPerformance[selectedTask])
.filter(([model, _metrics]) => selectedModels.includes(model))
.map(([model, metrics]) => {
// *** Use the FOUND snake_case internalMetricKey ***
const score = metrics?.[internalMetricKey];
const modelMeta = models.find((m) => m.model === model) || {};
return {
model: model,
score:
score !== undefined && score !== null && score !== "N/A"
? parseFloat(score)
: null,
color: modelMeta.color || "#999999",
};
});
}
return data
.filter((item) => item.score !== null && !isNaN(item.score))
.sort((a, b) => b.score - a.score);
// Update dependencies
}, [
selectedTask,
selectedMetricDisplayKey,
selectedModels,
taskLevelPerformance,
models,
metricsData,
tasks,
]);
// Task definitions
const featuredTasks = useMemo(
() => [
/* ... (keep task definitions array) ... */ {
id: "Generating a Creative Idea",
title: "Generating Creative Ideas",
description: "Brainstorming unique birthday gift ideas.",
icon: (color) => (
<svg
style={{ color: color || "#6b7280" }}
className="h-8 w-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
/>
</svg>
),
},
{
id: "Creating a Travel Itinerary",
title: "Creating Travel Itinerary",
description: "Planning a European city break.",
icon: (color) => (
<svg
style={{ color: color || "#6b7280" }}
className="h-8 w-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
),
},
{
id: "Following Up on a Job Application",
title: "Following Up on Job App",
description: "Drafting a professional follow-up email.",
icon: (color) => (
<svg
style={{ color: color || "#6b7280" }}
className="h-8 w-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
),
},
{
id: "Planning Your Weekly Meals",
title: "Planning Weekly Meals",
description: "Creating a meal plan accommodating dietary restrictions.",
icon: (color) => (
<svg
style={{ color: color || "#6b7280" }}
className="h-8 w-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg>
),
},
{
id: "Making a Decision Between Options",
title: "Making a Decision",
description: "Comparing tech products for purchase.",
icon: (color) => (
<svg
style={{ color: color || "#6b7280" }}
className="h-8 w-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M14 5l7 7m0 0l-7 7m7-7H3"
/>{" "}
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10 19l-7-7m0 0l7-7m-7 7h17"
/>
</svg>
),
},
{
id: "Understanding a Complex Topic",
title: "Understanding a Complex Topic",
description: "Learning about day trading concepts.",
icon: (color) => (
<svg
style={{ color: color || "#6b7280" }}
className="h-8 w-8"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
),
},
],
[]
);
const tasksToDisplay = useMemo(() => {
const availableTaskKeys = bestModelPerTask
? Object.keys(bestModelPerTask)
: [];
return featuredTasks.filter((ft) => availableTaskKeys.includes(ft.id));
}, [bestModelPerTask, featuredTasks]);
const taskRankings = useMemo(() => {
const rankings = {};
tasksToDisplay.forEach((task) => {
const taskId = task.id;
if (!taskLevelPerformance[taskId]) {
rankings[taskId] = [];
return;
}
const taskScores = models
.map((modelMeta) => {
const modelData = taskLevelPerformance[taskId][modelMeta.model];
if (!modelData) return null;
const scores = Object.values(modelData)
.map((s) => parseFloat(s))
.filter((s) => !isNaN(s));
if (scores.length === 0) return null;
const avgScore =
scores.reduce((sum, score) => sum + score, 0) / scores.length;
return {
model: modelMeta.model,
taskAvgScore: avgScore,
color: modelMeta.color || "#999999",
};
})
.filter((item) => item !== null)
.sort((a, b) => b.taskAvgScore - a.taskAvgScore);
rankings[taskId] = taskScores;
});
return rankings;
}, [tasksToDisplay, taskLevelPerformance, models]);
const renderTopPerformersTab = () => (
<div className="mb-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{tasksToDisplay.length === 0 && (
<p className="col-span-full text-center text-gray-500 py-8">
No task performance data available.
</p>
)}
{tasksToDisplay.map((task) => {
const bestModelInfo = bestModelPerTask?.[task.id];
const topModelsForTask = taskRankings[task.id] || [];
if (!bestModelInfo || bestModelInfo.model === "N/A") return null;
const modelColor = bestModelInfo.color || "#6b7280";
return (
<div
key={task.id}
className="border rounded-lg overflow-hidden shadow-sm bg-white flex flex-col"
>
<div className="px-4 py-2 bg-gray-50 border-b flex items-center flex-shrink-0">
<h3
className="font-semibold text-sm flex-grow truncate pr-2"
title={task.title}
>
{task.title}
</h3>
<div
className="ml-1 w-2 h-2 rounded-full flex-shrink-0"
style={{ backgroundColor: modelColor }}
aria-hidden="true"
></div>
</div>
<div className="p-4 flex-grow flex flex-col">
<div className="flex items-center mb-4 flex-shrink-0">
<div
className="p-2 rounded-full flex-shrink-0"
style={{ backgroundColor: `${modelColor}20` }}
>
{task.icon(modelColor)}
</div>
<div className="ml-4 overflow-hidden">
<h4
className="text-lg font-semibold truncate"
title={bestModelInfo.model}
>
{bestModelInfo.model}
</h4>
<p className="text-sm text-gray-600">
Avg. Score: {bestModelInfo.score?.toFixed(1) ?? "N/A"}
</p>
</div>
</div>
<div className="mb-4 flex-grow">
<h5 className="text-sm font-semibold mb-2">Task Ranking</h5>
{topModelsForTask.length > 0 ? (
<ol className="space-y-1.5 list-none pl-0">
{topModelsForTask.map((rankedModel, index) => (
<li
key={rankedModel.model}
className="text-sm flex items-center justify-between"
>
<div className="flex items-center truncate mr-2">
<span className="font-medium w-4 mr-1.5 text-gray-500">
{index + 1}.
</span>
<div
className="w-2.5 h-2.5 rounded-full mr-1.5 flex-shrink-0"
style={{ backgroundColor: rankedModel.color }}
></div>
<span
className="truncate"
title={rankedModel.model}
>
{rankedModel.model}
</span>
</div>
<span
className={`font-medium flex-shrink-0 px-1.5 py-0.5 text-xs rounded ${getScoreBadgeColor(
rankedModel.taskAvgScore
)}`}
>
{rankedModel.taskAvgScore?.toFixed(1) ?? "N/A"}
</span>
</li>
))}
</ol>
) : (
<p className="text-xs text-gray-500 italic">
Ranking data not available.
</p>
)}
</div>
<p className="text-xs text-gray-600 mt-auto pt-2 flex-shrink-0">
Task Example: {task.description}
</p>
</div>
</div>
);
})}
</div>
</div>
);
// Render the model performance analysis tab - *** UPDATED SELECTOR & LABELS ***
const renderModelPerformanceTab = () => (
<div>
{/* Controls Panel */}
<div className="border rounded-lg overflow-hidden mb-6 shadow-sm">
<div className="px-4 py-3 bg-gray-50 border-b">
<h3 className="font-semibold text-gray-800">
Task Analysis Controls
</h3>
</div>
<div className="p-4 flex flex-wrap items-center gap-4">
{/* Task Selector */}
<div className="w-full sm:w-auto">
<label
htmlFor="taskSelect"
className="block text-sm font-medium text-gray-700 mb-1"
>
Task
</label>
<select
id="taskSelect"
className="w-full sm:w-64 border rounded-md px-3 py-2 bg-white shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={selectedTask}
onChange={(e) => setSelectedTask(e.target.value)}
>
<option value="all">All Tasks (Average)</option>
{tasks.sort().map((task) => (
<option key={task} value={task}>
{task}
</option>
))}
</select>
</div>
{/* Metric Type Selector Pills */}
<div className="flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">
Metric Type
</label>
<div className="flex space-x-1 p-1 bg-gray-200 rounded-lg">
<TabButton
active={selectedMetricType === "high"}
onClick={() => setSelectedMetricType("high")}
>
High-Level
</TabButton>
<TabButton
active={selectedMetricType === "low"}
onClick={() => setSelectedMetricType("low")}
>
Low-Level
</TabButton>
</div>
</div>
{/* Metric Selector - VALUE is Title Case key, displays Title Case */}
<div className="w-full sm:w-auto">
<label
htmlFor="metricSelect"
className="block text-sm font-medium text-gray-700 mb-1"
>
{selectedMetricType === "high"
? "High-Level Metric"
: "Low-Level Metric"}
</label>
<select
id="metricSelect"
className="w-full sm:w-48 border rounded-md px-3 py-2 bg-white shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
value={selectedMetricDisplayKey} // VALUE is the Title Case key
onChange={(e) => setSelectedMetricDisplayKey(e.target.value)} // Store Title Case key
disabled={currentMetricDisplayKeysList.length === 0}
>
{currentMetricDisplayKeysList.length === 0 && (
<option value="">No metrics</option>
)}
{/* Iterate through Title Case keys, display Title Case */}
{currentMetricDisplayKeysList.map((displayKey) => (
<option key={displayKey} value={displayKey}>
{displayKey}
</option>
))}
</select>
</div>
</div>
</div>
{/* Chart Visualization */}
<div className="border rounded-lg overflow-hidden mb-6 shadow-sm">
{/* Use selectedMetricDisplayKey for title */}
<div className="px-4 py-3 bg-gray-50 border-b">
<h3 className="font-semibold text-gray-800">
{`${selectedMetricDisplayKey || "Selected Metric"} Comparison for `}
<span className="font-normal">
{selectedTask === "all"
? "All Tasks (Average)"
: `"${selectedTask}"`}
</span>
</h3>
</div>
<div className="p-4">
{chartData.length > 0 ? (
<div className="h-80">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={chartData}
margin={{ top: 5, right: 5, left: 0, bottom: 5 }}
barCategoryGap="20%"
>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="model" hide />
<YAxis domain={[0, 100]} width={30} tick={{ fontSize: 11 }} />
<RechartsTooltip
content={<CustomTooltip />}
wrapperStyle={{ zIndex: 10 }}
/>
{/* Use Title Case key for Bar name */}
<Bar
dataKey="score"
name={selectedMetricDisplayKey || "Score"}
radius={[4, 4, 0, 0]}
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1 mt-4 text-xs">
{chartData.map((entry) => (
<div key={entry.model} className="flex items-center">
<div
className="w-2.5 h-2.5 rounded-full mr-1.5"
style={{ backgroundColor: entry.color }}
></div>
<span>{entry.model}</span>
</div>
))}
</div>
</div>
) : (
<div className="flex items-center justify-center h-60 bg-gray-50 rounded">
<div className="text-center p-4">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-10 w-10 mx-auto text-gray-400 mb-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V7a2 2 0 012-2h2l2-3h6l2 3h2a2 2 0 012 2v10a2 2 0 01-2 2h-1"
/>
</svg>
<h3 className="text-lg font-medium text-gray-900 mb-1">
No Data Available
</h3>
<p className="text-sm text-gray-600">
No data available for the selected task, metric, and models.
</p>
</div>
</div>
)}
<div className="mt-15 text-xs text-gray-500">
{/* Corrected margin-top */}
{/* Use Title Case key for display and lookup */}
<p>
This chart shows{" "}
<strong>
{selectedMetricDisplayKey || "the selected metric"}
</strong>{" "}
scores (0-100, higher is better) for models on
{selectedTask === "all"
? "average across all tasks"
: `the "${selectedTask}" task`}
.
{selectedMetricDisplayKey &&
` Metric definition: ${getMetricTooltip(
selectedMetricDisplayKey
)}`}
</p>
</div>
</div>
</div>
</div>
);
// Main return with tabs
return (
<div>
<div className="mb-6 flex flex-col md:flex-row justify-between items-center gap-4">
<div className="flex space-x-1 p-1 bg-gray-200 rounded-lg">
<TabButton
active={activeTab === "top-performers"}
onClick={() => setActiveTab("top-performers")}
>
Top Performing Models by Task
</TabButton>{" "}
<TabButton
active={activeTab === "model-performance"}
onClick={() => setActiveTab("model-performance")}
>
Model Performance Comparison
</TabButton>{" "}
</div>{" "}
</div>
{activeTab === "top-performers"
? renderTopPerformersTab()
: renderModelPerformanceTab()}
</div>
);
};
export default TaskPerformance;