Spaces:
Running
Running
"use client" | |
import { useState, useMemo } from "react" | |
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" | |
import { Badge } from "@/components/ui/badge" | |
import { Progress } from "@/components/ui/progress" | |
import { Button } from "@/components/ui/button" | |
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" | |
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" | |
import { Checkbox } from "@/components/ui/checkbox" | |
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" | |
import { Trophy, Target, TrendingUp, BarChart3, Grid3X3, List, Filter, Brain, Shield, CheckCircle, ChevronDown } from "lucide-react" | |
// Category mappings from schema | |
const CATEGORY_NAMES: { [key: string]: string } = { | |
'language-communication': 'Language & Communication', | |
'social-intelligence': 'Social Intelligence & Interaction', | |
'problem-solving': 'Problem Solving', | |
'creativity-innovation': 'Creativity & Innovation', | |
'learning-memory': 'Learning & Memory', | |
'perception-vision': 'Perception & Vision', | |
'physical-manipulation': 'Physical Manipulation & Motor Skills', | |
'metacognition': 'Metacognition & Self-Awareness', | |
'robotic-intelligence': 'Robotic Intelligence & Autonomy', | |
'harmful-content': 'Harmful Content Generation', | |
'information-integrity': 'Information Integrity & Misinformation', | |
'privacy-data': 'Privacy & Data Protection', | |
'bias-fairness': 'Bias & Fairness', | |
'security-robustness': 'Security & Robustness', | |
'dangerous-capabilities': 'Dangerous Capabilities & Misuse', | |
'human-ai-interaction': 'Human-AI Interaction Risks', | |
'environmental-impact': 'Environmental & Resource Impact', | |
'economic-displacement': 'Economic & Labor Displacement', | |
'governance-accountability': 'Governance & Accountability', | |
'value-chain': 'Value Chain & Supply Chain Risks' | |
} | |
const CAPABILITY_CATEGORIES = [ | |
'language-communication', | |
'social-intelligence', | |
'problem-solving', | |
'creativity-innovation', | |
'learning-memory', | |
'perception-vision', | |
'physical-manipulation', | |
'metacognition', | |
'robotic-intelligence' | |
] | |
const RISK_CATEGORIES = [ | |
'harmful-content', | |
'information-integrity', | |
'privacy-data', | |
'bias-fairness', | |
'security-robustness', | |
'dangerous-capabilities', | |
'human-ai-interaction', | |
'environmental-impact', | |
'economic-displacement', | |
'governance-accountability', | |
'value-chain' | |
] | |
const MODALITIES = [ | |
"Text", "Vision", "Audio", "Video", "Code", "Robotics/Action", "Other" | |
] | |
interface Evaluation { | |
id: string | |
name: string | |
organization: string | |
overallScore: number | |
modality: string[] | |
submittedDate: string | |
categoryEvaluations?: { | |
[categoryId: string]: { | |
benchmarkAnswers?: { [questionId: string]: string } | |
processAnswers?: { [questionId: string]: string } | |
} | |
} | |
} | |
interface AnalyticsDashboardProps { | |
evaluations?: Evaluation[] | |
} | |
export default function AnalyticsDashboard({ evaluations = [] }: AnalyticsDashboardProps) { | |
const [loading, setLoading] = useState(false) | |
// Filter states | |
const [modalityFilter, setModalityFilter] = useState("all") | |
const [organizationFilter, setOrganizationFilter] = useState("all") | |
const [sortBy, setSortBy] = useState<"score" | "date" | "name">("score") | |
const [viewMode, setViewMode] = useState<"grid" | "list">("list") | |
// Category selection state | |
const [selectedCategories, setSelectedCategories] = useState<string[]>(Object.keys(CATEGORY_NAMES)) | |
// Get unique organizations | |
const ORGANIZATIONS = useMemo(() => | |
Array.from(new Set(evaluations.map(e => e.organization))) | |
, [evaluations]) | |
// Calculate scores dynamically based on selected categories | |
const recalculatedEvaluations = useMemo(() => { | |
return evaluations.map(evaluation => { | |
if (!evaluation.categoryEvaluations || selectedCategories.length === 0) { | |
return { ...evaluation, overallScore: 0 } | |
} | |
const scores = selectedCategories.map(categoryId => { | |
const categoryData = evaluation.categoryEvaluations?.[categoryId] | |
if (!categoryData) return 0 | |
// Count total questions and answered questions | |
const benchmarkAnswers = categoryData.benchmarkAnswers || {} | |
const processAnswers = categoryData.processAnswers || {} | |
const allAnswers = { ...benchmarkAnswers, ...processAnswers } | |
const totalQuestions = Object.keys(allAnswers).length | |
if (totalQuestions === 0) return 0 | |
// Count answered questions (not N/A, null, undefined, or empty) | |
const answeredQuestions = Object.values(allAnswers).filter( | |
(answer: any) => answer && answer !== "N/A" && String(answer).trim() !== "" | |
).length | |
return (answeredQuestions / totalQuestions) * 100 | |
}) | |
const overallScore = scores.length > 0 | |
? scores.reduce((sum, score) => sum + score, 0) / scores.length | |
: 0 | |
return { ...evaluation, overallScore } | |
}) | |
}, [evaluations, selectedCategories]) | |
// Apply filters | |
const filteredEvaluations = useMemo(() => { | |
return recalculatedEvaluations | |
.filter(evaluation => { | |
const matchesModality = modalityFilter === "all" || evaluation.modality.includes(modalityFilter) | |
const matchesOrganization = organizationFilter === "all" || evaluation.organization === organizationFilter | |
return matchesModality && matchesOrganization | |
}) | |
.sort((a, b) => { | |
switch (sortBy) { | |
case "score": | |
return b.overallScore - a.overallScore | |
case "date": | |
return new Date(b.submittedDate).getTime() - new Date(a.submittedDate).getTime() | |
case "name": | |
return a.name.localeCompare(b.name) | |
default: | |
return 0 | |
} | |
}) | |
}, [recalculatedEvaluations, modalityFilter, organizationFilter, sortBy]) | |
// Calculate category data based on selected categories | |
const categoryData = useMemo(() => { | |
return selectedCategories.map(categoryId => { | |
const scores = evaluations.map(evaluation => { | |
const categoryData = evaluation.categoryEvaluations?.[categoryId] | |
if (!categoryData) return 0 | |
// Count total questions and answered questions | |
const benchmarkAnswers = categoryData.benchmarkAnswers || {} | |
const processAnswers = categoryData.processAnswers || {} | |
const allAnswers = { ...benchmarkAnswers, ...processAnswers } | |
const totalQuestions = Object.keys(allAnswers).length | |
if (totalQuestions === 0) return 0 | |
// Count answered questions (not N/A, null, undefined, or empty) | |
const answeredQuestions = Object.values(allAnswers).filter( | |
(answer: any) => answer && answer !== "N/A" && String(answer).trim() !== "" | |
).length | |
return (answeredQuestions / totalQuestions) * 100 | |
}) | |
const averageScore = scores.length > 0 | |
? scores.reduce((sum, score) => sum + score, 0) / scores.length | |
: 0 | |
return { | |
id: categoryId, | |
name: CATEGORY_NAMES[categoryId], | |
type: CAPABILITY_CATEGORIES.includes(categoryId) ? "capability" : "risk", | |
averageScore, | |
evaluationCount: evaluations.length | |
} | |
}) | |
}, [evaluations, selectedCategories]) | |
const getScoreBadgeVariant = (score: number) => { | |
if (score >= 80) return "default" | |
if (score >= 60) return "secondary" | |
if (score >= 40) return "outline" | |
return "destructive" | |
} | |
const getScoreColor = (score: number) => { | |
if (score >= 80) return "text-green-600 dark:text-green-400" | |
if (score >= 60) return "text-blue-600 dark:text-blue-400" | |
if (score >= 40) return "text-yellow-600 dark:text-yellow-400" | |
return "text-red-600 dark:text-red-400" | |
} | |
const handleCategoryToggle = (categoryId: string) => { | |
setSelectedCategories(prev => | |
prev.includes(categoryId) | |
? prev.filter(id => id !== categoryId) | |
: [...prev, categoryId] | |
) | |
} | |
const selectAllCategories = () => { | |
setSelectedCategories(Object.keys(CATEGORY_NAMES)) | |
} | |
const selectOnlyCapabilities = () => { | |
setSelectedCategories(CAPABILITY_CATEGORIES) | |
} | |
const selectOnlyRisks = () => { | |
setSelectedCategories(RISK_CATEGORIES) | |
} | |
const clearAllCategories = () => { | |
setSelectedCategories([]) | |
} | |
if (loading) { | |
return ( | |
<div className="flex items-center justify-center h-64"> | |
<div className="text-center"> | |
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div> | |
<p className="text-muted-foreground">Loading analytics...</p> | |
</div> | |
</div> | |
) | |
} | |
return ( | |
<div className="space-y-8"> | |
{/* Overview Stats */} | |
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> | |
<Card> | |
<CardContent className="p-6"> | |
<div className="flex items-center space-x-2"> | |
<Trophy className="h-5 w-5 text-primary" /> | |
<div> | |
<p className="text-2xl font-bold">{evaluations.length}</p> | |
<p className="text-sm text-muted-foreground">Total Evaluations</p> | |
</div> | |
</div> | |
</CardContent> | |
</Card> | |
<Card> | |
<CardContent className="p-6"> | |
<div className="flex items-center space-x-2"> | |
<Target className="h-5 w-5 text-primary" /> | |
<div> | |
<p className="text-2xl font-bold"> | |
{filteredEvaluations.length > 0 ? | |
Math.round(filteredEvaluations.reduce((sum, evaluation) => sum + evaluation.overallScore, 0) / filteredEvaluations.length) : 0}% | |
</p> | |
<p className="text-sm text-muted-foreground">Average Score</p> | |
</div> | |
</div> | |
</CardContent> | |
</Card> | |
<Card> | |
<CardContent className="p-6"> | |
<div className="flex items-center space-x-2"> | |
<TrendingUp className="h-5 w-5 text-primary" /> | |
<div> | |
<p className="text-2xl font-bold"> | |
{filteredEvaluations.length > 0 ? Math.round(Math.max(...filteredEvaluations.map(e => e.overallScore))) : 0}% | |
</p> | |
<p className="text-sm text-muted-foreground">Highest Score</p> | |
</div> | |
</div> | |
</CardContent> | |
</Card> | |
<Card> | |
<CardContent className="p-6"> | |
<div className="flex items-center space-x-2"> | |
<BarChart3 className="h-5 w-5 text-primary" /> | |
<div> | |
<p className="text-2xl font-bold">{selectedCategories.length}/{Object.keys(CATEGORY_NAMES).length}</p> | |
<p className="text-sm text-muted-foreground">Selected Categories</p> | |
</div> | |
</div> | |
</CardContent> | |
</Card> | |
</div> | |
<Tabs defaultValue="overall" className="space-y-6"> | |
<TabsList className="grid w-full grid-cols-2"> | |
<TabsTrigger value="overall">Overall Leaderboard</TabsTrigger> | |
<TabsTrigger value="insights">Insights</TabsTrigger> | |
</TabsList> | |
<TabsContent value="overall" className="space-y-6"> | |
{/* Filters */} | |
<Card> | |
<CardHeader> | |
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> | |
<div> | |
<CardTitle className="flex items-center gap-2"> | |
<Filter className="h-5 w-5" /> | |
Filters & View | |
</CardTitle> | |
<CardDescription>Filter and sort evaluation results</CardDescription> | |
</div> | |
<div className="flex items-center gap-2"> | |
<Button | |
variant={viewMode === "grid" ? "default" : "outline"} | |
size="sm" | |
onClick={() => setViewMode("grid")} | |
> | |
<Grid3X3 className="h-4 w-4" /> | |
</Button> | |
<Button | |
variant={viewMode === "list" ? "default" : "outline"} | |
size="sm" | |
onClick={() => setViewMode("list")} | |
> | |
<List className="h-4 w-4" /> | |
</Button> | |
</div> | |
</div> | |
</CardHeader> | |
<CardContent> | |
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4"> | |
{/* Category Selection Dropdown */} | |
<div> | |
<label className="text-sm font-medium mb-2 block">Categories ({selectedCategories.length} selected)</label> | |
<Popover> | |
<PopoverTrigger asChild> | |
<Button variant="outline" className="w-full justify-between"> | |
Select Categories | |
<ChevronDown className="h-4 w-4 opacity-50" /> | |
</Button> | |
</PopoverTrigger> | |
<PopoverContent className="w-80 p-0" align="start"> | |
<div className="p-4 space-y-4"> | |
{/* Quick Actions */} | |
<div className="flex flex-wrap gap-2"> | |
<Button size="sm" variant="outline" onClick={selectAllCategories}> | |
All | |
</Button> | |
<Button size="sm" variant="outline" onClick={selectOnlyCapabilities}> | |
<Brain className="h-3 w-3 mr-1" /> | |
Capabilities | |
</Button> | |
<Button size="sm" variant="outline" onClick={selectOnlyRisks}> | |
<Shield className="h-3 w-3 mr-1" /> | |
Risks | |
</Button> | |
<Button size="sm" variant="outline" onClick={clearAllCategories}> | |
Clear | |
</Button> | |
</div> | |
{/* Capabilities */} | |
<div className="space-y-2"> | |
<h4 className="font-semibold text-sm flex items-center gap-2"> | |
<Brain className="h-4 w-4 text-blue-500" /> | |
Capabilities | |
</h4> | |
<div className="space-y-1 max-h-32 overflow-y-auto"> | |
{CAPABILITY_CATEGORIES.map(categoryId => ( | |
<div key={categoryId} className="flex items-center space-x-2"> | |
<Checkbox | |
id={`cap-${categoryId}`} | |
checked={selectedCategories.includes(categoryId)} | |
onCheckedChange={() => handleCategoryToggle(categoryId)} | |
/> | |
<label htmlFor={`cap-${categoryId}`} className="text-xs cursor-pointer"> | |
{CATEGORY_NAMES[categoryId]} | |
</label> | |
</div> | |
))} | |
</div> | |
</div> | |
{/* Risks */} | |
<div className="space-y-2"> | |
<h4 className="font-semibold text-sm flex items-center gap-2"> | |
<Shield className="h-4 w-4 text-red-500" /> | |
Risks | |
</h4> | |
<div className="space-y-1 max-h-40 overflow-y-auto"> | |
{RISK_CATEGORIES.map(categoryId => ( | |
<div key={categoryId} className="flex items-center space-x-2"> | |
<Checkbox | |
id={`risk-${categoryId}`} | |
checked={selectedCategories.includes(categoryId)} | |
onCheckedChange={() => handleCategoryToggle(categoryId)} | |
/> | |
<label htmlFor={`risk-${categoryId}`} className="text-xs cursor-pointer"> | |
{CATEGORY_NAMES[categoryId]} | |
</label> | |
</div> | |
))} | |
</div> | |
</div> | |
</div> | |
</PopoverContent> | |
</Popover> | |
</div> | |
<div> | |
<label className="text-sm font-medium mb-2 block">Modality</label> | |
<Select value={modalityFilter} onValueChange={setModalityFilter}> | |
<SelectTrigger> | |
<SelectValue /> | |
</SelectTrigger> | |
<SelectContent> | |
<SelectItem value="all">All Modalities</SelectItem> | |
{MODALITIES.map(modality => ( | |
<SelectItem key={modality} value={modality}>{modality}</SelectItem> | |
))} | |
</SelectContent> | |
</Select> | |
</div> | |
<div> | |
<label className="text-sm font-medium mb-2 block">Organization</label> | |
<Select value={organizationFilter} onValueChange={setOrganizationFilter}> | |
<SelectTrigger> | |
<SelectValue /> | |
</SelectTrigger> | |
<SelectContent> | |
<SelectItem value="all">All Organizations</SelectItem> | |
{ORGANIZATIONS.map(org => ( | |
<SelectItem key={org} value={org}>{org}</SelectItem> | |
))} | |
</SelectContent> | |
</Select> | |
</div> | |
<div> | |
<label className="text-sm font-medium mb-2 block">Sort By</label> | |
<Select value={sortBy} onValueChange={(value: "score" | "date" | "name") => setSortBy(value)}> | |
<SelectTrigger> | |
<SelectValue /> | |
</SelectTrigger> | |
<SelectContent> | |
<SelectItem value="score">Completeness Score</SelectItem> | |
<SelectItem value="date">Submit Date</SelectItem> | |
<SelectItem value="name">Name</SelectItem> | |
</SelectContent> | |
</Select> | |
</div> | |
<div className="flex items-end"> | |
<Button | |
variant="outline" | |
onClick={() => { | |
setModalityFilter("all") | |
setOrganizationFilter("all") | |
setSortBy("score") | |
}} | |
className="w-full" | |
> | |
Reset Filters | |
</Button> | |
</div> | |
</div> | |
</CardContent> | |
</Card> | |
{/* Overall Leaderboard */} | |
<Card> | |
<CardHeader> | |
<CardTitle>Overall Completeness Leaderboard</CardTitle> | |
<CardDescription> | |
Ranked by evaluation completeness score ({filteredEvaluations.length} {filteredEvaluations.length === 1 ? 'evaluation' : 'evaluations'}) | |
</CardDescription> | |
</CardHeader> | |
<CardContent> | |
{viewMode === "grid" ? ( | |
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
{filteredEvaluations.map((evaluation, index) => ( | |
<Card key={evaluation.id} className="relative"> | |
<CardContent className="p-6"> | |
{index < 3 && ( | |
<div className="absolute -top-2 -right-2"> | |
<Badge variant={index === 0 ? "default" : "secondary"} className="rounded-full px-3 py-1"> | |
#{index + 1} | |
</Badge> | |
</div> | |
)} | |
<div className="space-y-4"> | |
<div> | |
<h3 className="font-semibold text-lg">{evaluation.name}</h3> | |
<p className="text-sm text-muted-foreground">{evaluation.organization}</p> | |
</div> | |
<div className="space-y-2"> | |
<div className="flex items-center justify-between"> | |
<span className="text-sm font-medium">Completeness Score</span> | |
<Badge variant={getScoreBadgeVariant(evaluation.overallScore)}> | |
{Math.round(evaluation.overallScore)}% | |
</Badge> | |
</div> | |
<Progress value={evaluation.overallScore} className="h-2" /> | |
</div> | |
<div className="flex flex-wrap gap-1"> | |
{evaluation.modality.map(mod => ( | |
<Badge key={mod} variant="outline" className="text-xs"> | |
{mod} | |
</Badge> | |
))} | |
</div> | |
<div className="text-xs text-muted-foreground"> | |
Submitted: {new Date(evaluation.submittedDate).toLocaleDateString()} | |
</div> | |
</div> | |
</CardContent> | |
</Card> | |
))} | |
</div> | |
) : ( | |
<div className="space-y-3"> | |
{filteredEvaluations.map((evaluation, index) => ( | |
<div key={evaluation.id} className="group hover:bg-muted/30 transition-colors rounded-lg p-3"> | |
<div className="flex items-center gap-4"> | |
{/* Organization Logo/Icon */} | |
<div className="flex items-center gap-3 min-w-[200px]"> | |
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-xs font-bold text-primary"> | |
{evaluation.organization.charAt(0)} | |
</div> | |
<div className="text-left"> | |
<div className="font-medium text-sm">{evaluation.organization}</div> | |
<div className="font-semibold">{evaluation.name}</div> | |
</div> | |
</div> | |
{/* Horizontal Bar Chart */} | |
<div className="flex-1 relative"> | |
<div className="flex items-center gap-3"> | |
{/* Progress Bar Container */} | |
<div className="flex-1 relative h-6 bg-muted rounded-full overflow-hidden"> | |
{/* Background bar */} | |
<div className="absolute inset-0 bg-gray-200 dark:bg-gray-700"></div> | |
{/* Progress bar */} | |
<div | |
className={`absolute left-0 top-0 h-full transition-all duration-500 ease-out ${ | |
evaluation.overallScore >= 80 ? 'bg-purple-600' : | |
evaluation.overallScore >= 60 ? 'bg-purple-500' : | |
evaluation.overallScore >= 40 ? 'bg-purple-400' : | |
'bg-purple-300' | |
}`} | |
style={{ | |
width: `${Math.max(evaluation.overallScore, 5)}%`, | |
animationDelay: `${index * 100}ms` | |
}} | |
></div> | |
</div> | |
{/* Score */} | |
<div className="min-w-[50px] text-right"> | |
<span className="text-lg font-bold"> | |
{Math.round(evaluation.overallScore)}% | |
</span> | |
</div> | |
</div> | |
{/* Modality badges - shown on hover */} | |
<div className="flex flex-wrap gap-1 mt-2 opacity-0 group-hover:opacity-100 transition-opacity"> | |
{evaluation.modality.slice(0, 3).map(mod => ( | |
<Badge key={mod} variant="outline" className="text-xs"> | |
{mod} | |
</Badge> | |
))} | |
{evaluation.modality.length > 3 && ( | |
<Badge variant="outline" className="text-xs"> | |
+{evaluation.modality.length - 3} | |
</Badge> | |
)} | |
</div> | |
</div> | |
</div> | |
</div> | |
))} | |
</div> | |
)} | |
</CardContent> | |
</Card> | |
</TabsContent> | |
<TabsContent value="insights" className="space-y-6"> | |
{/* Key Insights */} | |
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
<Card> | |
<CardHeader> | |
<CardTitle>Top Performing Categories</CardTitle> | |
<CardDescription>Categories with highest average completeness from selected categories</CardDescription> | |
</CardHeader> | |
<CardContent> | |
<div className="space-y-3"> | |
{categoryData | |
.sort((a, b) => b.averageScore - a.averageScore) | |
.slice(0, 5) | |
.map((category, index) => ( | |
<div key={category.id} className="flex items-center justify-between"> | |
<div className="flex items-center space-x-3"> | |
<Badge variant="outline" className="w-8 h-8 rounded-full flex items-center justify-center p-0"> | |
{index + 1} | |
</Badge> | |
<div> | |
<p className="font-medium text-sm">{category.name}</p> | |
<Badge variant={category.type === "capability" ? "secondary" : "destructive"} className="text-xs"> | |
{category.type} | |
</Badge> | |
</div> | |
</div> | |
<div className="text-right"> | |
<p className={`font-bold ${getScoreColor(category.averageScore)}`}> | |
{Math.round(category.averageScore)}% | |
</p> | |
</div> | |
</div> | |
))} | |
{categoryData.length === 0 && ( | |
<p className="text-muted-foreground text-center py-4"> | |
No categories selected. Please select categories from the filters above. | |
</p> | |
)} | |
</div> | |
</CardContent> | |
</Card> | |
<Card> | |
<CardHeader> | |
<CardTitle>Areas for Improvement</CardTitle> | |
<CardDescription>Categories with lowest average completeness from selected categories</CardDescription> | |
</CardHeader> | |
<CardContent> | |
<div className="space-y-3"> | |
{categoryData | |
.sort((a, b) => a.averageScore - b.averageScore) | |
.slice(0, 5) | |
.map((category, index) => ( | |
<div key={category.id} className="flex items-center justify-between"> | |
<div className="flex items-center space-x-3"> | |
<Badge variant="outline" className="w-8 h-8 rounded-full flex items-center justify-center p-0"> | |
{index + 1} | |
</Badge> | |
<div> | |
<p className="font-medium text-sm">{category.name}</p> | |
<Badge variant={category.type === "capability" ? "secondary" : "destructive"} className="text-xs"> | |
{category.type} | |
</Badge> | |
</div> | |
</div> | |
<div className="text-right"> | |
<p className={`font-bold ${getScoreColor(category.averageScore)}`}> | |
{Math.round(category.averageScore)}% | |
</p> | |
</div> | |
</div> | |
))} | |
{categoryData.length === 0 && ( | |
<p className="text-muted-foreground text-center py-4"> | |
No categories selected. Please select categories from the filters above. | |
</p> | |
)} | |
</div> | |
</CardContent> | |
</Card> | |
</div> | |
{/* Category Performance Breakdown */} | |
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> | |
{/* Capabilities */} | |
<Card> | |
<CardHeader> | |
<CardTitle className="flex items-center gap-2"> | |
<Brain className="h-5 w-5 text-blue-500" /> | |
Capability Categories | |
</CardTitle> | |
<CardDescription>Average completeness by capability category</CardDescription> | |
</CardHeader> | |
<CardContent> | |
<div className="space-y-4"> | |
{categoryData | |
.filter(cat => cat.type === "capability") | |
.sort((a, b) => b.averageScore - a.averageScore) | |
.map(category => ( | |
<div key={category.id} className="space-y-2"> | |
<div className="flex items-center justify-between text-sm"> | |
<span className="font-medium truncate pr-2">{category.name}</span> | |
<Badge variant={getScoreBadgeVariant(category.averageScore)}> | |
{Math.round(category.averageScore)}% | |
</Badge> | |
</div> | |
<Progress value={category.averageScore} className="h-2" /> | |
<div className="text-xs text-muted-foreground"> | |
{category.evaluationCount} evaluation{category.evaluationCount !== 1 ? 's' : ''} | |
</div> | |
</div> | |
))} | |
{categoryData.filter(cat => cat.type === "capability").length === 0 && ( | |
<p className="text-muted-foreground text-center py-4"> | |
No capability categories selected. | |
</p> | |
)} | |
</div> | |
</CardContent> | |
</Card> | |
{/* Risks */} | |
<Card> | |
<CardHeader> | |
<CardTitle className="flex items-center gap-2"> | |
<Shield className="h-5 w-5 text-red-500" /> | |
Risk Categories | |
</CardTitle> | |
<CardDescription>Average completeness by risk category</CardDescription> | |
</CardHeader> | |
<CardContent> | |
<div className="space-y-4"> | |
{categoryData | |
.filter(cat => cat.type === "risk") | |
.sort((a, b) => b.averageScore - a.averageScore) | |
.map(category => ( | |
<div key={category.id} className="space-y-2"> | |
<div className="flex items-center justify-between text-sm"> | |
<span className="font-medium truncate pr-2">{category.name}</span> | |
<Badge variant={getScoreBadgeVariant(category.averageScore)}> | |
{Math.round(category.averageScore)}% | |
</Badge> | |
</div> | |
<Progress value={category.averageScore} className="h-2" /> | |
<div className="text-xs text-muted-foreground"> | |
{category.evaluationCount} evaluation{category.evaluationCount !== 1 ? 's' : ''} | |
</div> | |
</div> | |
))} | |
{categoryData.filter(cat => cat.type === "risk").length === 0 && ( | |
<p className="text-muted-foreground text-center py-4"> | |
No risk categories selected. | |
</p> | |
)} | |
</div> | |
</CardContent> | |
</Card> | |
</div> | |
{/* Distribution Analysis */} | |
<Card> | |
<CardHeader> | |
<CardTitle>Score Distribution</CardTitle> | |
<CardDescription>Breakdown of evaluation completeness scores based on current filters and selected categories</CardDescription> | |
</CardHeader> | |
<CardContent> | |
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> | |
{[ | |
{ range: "80-100%", color: "bg-green-500", count: filteredEvaluations.filter(e => e.overallScore >= 80).length }, | |
{ range: "60-79%", color: "bg-blue-500", count: filteredEvaluations.filter(e => e.overallScore >= 60 && e.overallScore < 80).length }, | |
{ range: "40-59%", color: "bg-yellow-500", count: filteredEvaluations.filter(e => e.overallScore >= 40 && e.overallScore < 60).length }, | |
{ range: "0-39%", color: "bg-red-500", count: filteredEvaluations.filter(e => e.overallScore < 40).length } | |
].map(item => ( | |
<div key={item.range} className="text-center space-y-2"> | |
<div className={`${item.color} rounded-full w-16 h-16 flex items-center justify-center text-white font-bold text-xl mx-auto`}> | |
{item.count} | |
</div> | |
<p className="text-sm font-medium">{item.range}</p> | |
<p className="text-xs text-muted-foreground"> | |
{filteredEvaluations.length > 0 ? Math.round((item.count / filteredEvaluations.length) * 100) : 0}% | |
</p> | |
</div> | |
))} | |
</div> | |
</CardContent> | |
</Card> | |
{/* Additional Insights */} | |
{evaluations.length > 0 && ( | |
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> | |
<Card> | |
<CardHeader> | |
<CardTitle>Most Common Modalities</CardTitle> | |
<CardDescription>Frequently evaluated modality combinations</CardDescription> | |
</CardHeader> | |
<CardContent> | |
<div className="space-y-2"> | |
{Array.from(new Set(evaluations.flatMap(e => e.modality))) | |
.map(modality => ({ | |
modality, | |
count: evaluations.filter(e => e.modality.includes(modality)).length | |
})) | |
.sort((a, b) => b.count - a.count) | |
.slice(0, 5) | |
.map(item => ( | |
<div key={item.modality} className="flex justify-between items-center"> | |
<Badge variant="outline">{item.modality}</Badge> | |
<span className="text-sm font-medium">{item.count} evaluation{item.count !== 1 ? 's' : ''}</span> | |
</div> | |
))} | |
</div> | |
</CardContent> | |
</Card> | |
<Card> | |
<CardHeader> | |
<CardTitle>Organizations</CardTitle> | |
<CardDescription>Number of evaluations per organization</CardDescription> | |
</CardHeader> | |
<CardContent> | |
<div className="space-y-2"> | |
{ORGANIZATIONS | |
.map(org => ({ | |
organization: org, | |
count: evaluations.filter(e => e.organization === org).length | |
})) | |
.sort((a, b) => b.count - a.count) | |
.map(item => ( | |
<div key={item.organization} className="flex justify-between items-center"> | |
<span className="text-sm font-medium">{item.organization}</span> | |
<Badge variant="secondary">{item.count}</Badge> | |
</div> | |
))} | |
</div> | |
</CardContent> | |
</Card> | |
<Card> | |
<CardHeader> | |
<CardTitle>Evaluation Timeline</CardTitle> | |
<CardDescription>Recent evaluation activity</CardDescription> | |
</CardHeader> | |
<CardContent> | |
<div className="space-y-2"> | |
{evaluations | |
.sort((a, b) => new Date(b.submittedDate).getTime() - new Date(a.submittedDate).getTime()) | |
.slice(0, 5) | |
.map(evaluation => ( | |
<div key={evaluation.id} className="flex justify-between items-center text-sm"> | |
<span className="font-medium">{evaluation.name}</span> | |
<span className="text-muted-foreground"> | |
{new Date(evaluation.submittedDate).toLocaleDateString()} | |
</span> | |
</div> | |
))} | |
</div> | |
</CardContent> | |
</Card> | |
</div> | |
)} | |
</TabsContent> | |
</Tabs> | |
</div> | |
) | |
} | |