Spaces:
Running
Running
"use client" | |
import { useState } from "react" | |
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" | |
import { Badge } from "@/components/ui/badge" | |
import { Button } from "@/components/ui/button" | |
import { MoreHorizontal, Eye, Download, Trash2 } from "lucide-react" | |
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" | |
import { useRouter } from "next/navigation" | |
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" | |
import { getAllCategories } from "@/lib/schema" | |
export type EvaluationCardData = { | |
id: string | |
systemName: string | |
provider: string | |
inputModalities: string[] | |
outputModalities: string[] | |
completedDate: string | |
applicableCategories: number | |
completedCategories: number | |
status: "strong" | "adequate" | "weak" | "insufficient" | |
capabilityEval: { | |
strong: number | |
adequate: number | |
weak: number | |
insufficient: number | |
strongCategories: string[] | |
adequateCategories: string[] | |
weakCategories: string[] | |
insufficientCategories: string[] | |
totalApplicable: number | |
} | |
riskEval: { | |
strong: number | |
adequate: number | |
weak: number | |
insufficient: number | |
strongCategories: string[] | |
adequateCategories: string[] | |
weakCategories: string[] | |
insufficientCategories: string[] | |
totalApplicable: number | |
} | |
priorityAreas?: string[] | |
priorityDetails?: Record< | |
string, | |
{ | |
yes: string[] | |
negative: { text: string; status: "no" | "na"; reason?: string }[] | |
} | |
> | |
} | |
interface EvaluationCardProps { | |
evaluation: EvaluationCardData | |
onView: (id: string) => void | |
onDelete: (id: string) => void | |
} | |
const getCompletenessColor = (score: number) => { | |
if (score >= 85) return "bg-emerald-500 text-white" | |
if (score >= 70) return "bg-blue-500 text-white" | |
if (score >= 55) return "bg-amber-500 text-white" | |
return "bg-red-500 text-white" | |
} | |
export function EvaluationCard({ evaluation, onView, onDelete }: EvaluationCardProps) { | |
const [expandedAreas, setExpandedAreas] = useState<Record<string, boolean>>({}) | |
const toggleArea = (area: string) => setExpandedAreas((p) => ({ ...p, [area]: !p[area] })) | |
const router = useRouter() | |
const handleCardClick = () => { | |
router.push(`/evaluation/${evaluation.id}`) | |
} | |
const handleMenuClick = (e: React.MouseEvent) => { | |
e.stopPropagation() // Prevent card click when clicking menu | |
} | |
const handleExport = (e: React.MouseEvent) => { | |
e.stopPropagation() // Prevent card click when clicking export | |
const reportData = { | |
id: evaluation.id, | |
systemName: evaluation.systemName, | |
provider: evaluation.provider, | |
completedDate: evaluation.completedDate, | |
exportDate: new Date().toISOString(), | |
inputModalities: evaluation.inputModalities, | |
outputModalities: evaluation.outputModalities, | |
completenessScore: Math.round((evaluation.completedCategories / evaluation.applicableCategories) * 100), | |
status: evaluation.status, | |
capabilityEvaluation: evaluation.capabilityEval, | |
riskEvaluation: evaluation.riskEval, | |
priorityAreas: evaluation.priorityAreas, | |
priorityDetails: evaluation.priorityDetails | |
} | |
const blob = new Blob([JSON.stringify(reportData, null, 2)], { type: "application/json" }) | |
const url = URL.createObjectURL(blob) | |
const a = document.createElement("a") | |
a.href = url | |
a.download = `evaluation-summary-${evaluation.systemName.replace(/[^a-zA-Z0-9]/g, '-')}-${new Date().toISOString().split("T")[0]}.json` | |
a.click() | |
URL.revokeObjectURL(url) | |
} | |
const modalityMap: Record<string, { label: string; emoji?: string; variant?: string }> = { | |
"Text": { label: "Text", emoji: "π" }, | |
"Image": { label: "Image", emoji: "πΌοΈ" }, | |
"Audio": { label: "Audio", emoji: "π" }, | |
"Video": { label: "Video", emoji: "π₯" }, | |
"Tabular": { label: "Tabular", emoji: "π" }, | |
"Robotics/Action": { label: "Robotics", emoji: "π€" }, | |
"Other": { label: "Other", emoji: "β‘" }, | |
} | |
const getModalityDisplay = (inputModalities: string[], outputModalities: string[]) => { | |
const inputStr = inputModalities.join(", ") | |
const outputStr = outputModalities.join(", ") | |
// Special cases for common patterns | |
if (inputModalities.length === 1 && outputModalities.length === 1) { | |
if (inputModalities[0] === "Text" && outputModalities[0] === "Text") { | |
return { label: "Text β Text", emoji: "οΏ½" } | |
} | |
if (inputModalities[0] === "Text" && outputModalities[0] === "Image") { | |
return { label: "Text β Image", emoji: "οΏ½οΈ" } | |
} | |
if (inputModalities[0] === "Image" && outputModalities[0] === "Text") { | |
return { label: "Image β Text", emoji: "π·" } | |
} | |
if (inputModalities[0] === "Tabular" && outputModalities[0] === "Tabular") { | |
return { label: "Tabular", emoji: "π" } | |
} | |
} | |
// Multimodal cases | |
if (inputModalities.length > 1 || outputModalities.length > 1) { | |
return { label: "Multimodal", emoji: "π€" } | |
} | |
// Fallback | |
return { label: `${inputStr} β ${outputStr}`, emoji: "β‘" } | |
} | |
const getUniqueCount = (lists: string[][]) => { | |
const set = new Set<string>() | |
lists.forEach((list) => (list || []).forEach((item) => set.add(item))) | |
return set.size | |
} | |
const capTotalComputed = getUniqueCount([ | |
evaluation.capabilityEval.strongCategories, | |
evaluation.capabilityEval.adequateCategories, | |
evaluation.capabilityEval.weakCategories, | |
evaluation.capabilityEval.insufficientCategories, | |
]) | |
const riskTotalComputed = getUniqueCount([ | |
evaluation.riskEval.strongCategories, | |
evaluation.riskEval.adequateCategories, | |
evaluation.riskEval.weakCategories, | |
evaluation.riskEval.insufficientCategories, | |
]) | |
const calculateCompletenessScore = () => { | |
const weights = { strong: 4, adequate: 3, weak: 2, insufficient: 1 } | |
const capTotal = capTotalComputed | |
const riskTotal = riskTotalComputed | |
if (capTotal === 0 && riskTotal === 0) { | |
return "0.0" | |
} | |
let capScore = 0 | |
if (capTotal > 0) { | |
capScore = | |
((evaluation.capabilityEval.strong * weights.strong + | |
evaluation.capabilityEval.adequate * weights.adequate + | |
evaluation.capabilityEval.weak * weights.weak + | |
evaluation.capabilityEval.insufficient * weights.insufficient) / | |
(capTotal * 4)) * | |
100 | |
} | |
let riskScore = 0 | |
if (riskTotal > 0) { | |
riskScore = | |
((evaluation.riskEval.strong * weights.strong + | |
evaluation.riskEval.adequate * weights.adequate + | |
evaluation.riskEval.weak * weights.weak + | |
evaluation.riskEval.insufficient * weights.insufficient) / | |
(riskTotal * 4)) * | |
100 | |
} | |
const totalApplicable = capTotal + riskTotal | |
const weightedScore = (capScore * capTotal + riskScore * riskTotal) / totalApplicable | |
// Ensure we return a valid number | |
return isNaN(weightedScore) ? "0.0" : weightedScore.toFixed(1) | |
} | |
const handleViewDetails = () => { | |
router.push(`/evaluation/${evaluation.id}`) | |
} | |
const completenessScore = Number.parseFloat(calculateCompletenessScore()) | |
return ( | |
<TooltipProvider> | |
<Card | |
className="hover:shadow-lg transition-all duration-200 cursor-pointer hover:border-primary/20 hover:shadow-primary/5" | |
onClick={handleCardClick} | |
> | |
<CardHeader className="pb-3"> | |
<div className="flex items-start justify-between gap-3"> | |
<div className="space-y-1 flex-1 min-w-0"> | |
<CardTitle className="text-lg sm:text-xl font-bold leading-tight">{evaluation.systemName}</CardTitle> | |
<p className="text-sm text-muted-foreground font-medium">{evaluation.provider}</p> | |
{/* Enhanced modality badge with emoji and hover detail */} | |
{(() => { | |
const info = getModalityDisplay(evaluation.inputModalities, evaluation.outputModalities) | |
return ( | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<Badge variant="secondary" className="text-xs px-2 py-1 w-fit flex items-center gap-1 cursor-help"> | |
{info.emoji ? <span aria-hidden className="text-sm">{info.emoji}</span> : null} | |
<span className="whitespace-nowrap">{info.label}</span> | |
</Badge> | |
</TooltipTrigger> | |
<TooltipContent side="bottom" className="max-w-xs"> | |
<div className="text-sm"> | |
<div><strong>Input:</strong> {evaluation.inputModalities.join(", ")}</div> | |
<div><strong>Output:</strong> {evaluation.outputModalities.join(", ")}</div> | |
</div> | |
</TooltipContent> | |
</Tooltip> | |
) | |
})()} | |
</div> | |
<div className="flex items-center gap-2 flex-shrink-0"> | |
<div onClick={handleMenuClick}> | |
<DropdownMenu> | |
<DropdownMenuTrigger asChild> | |
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 hover:bg-muted"> | |
<MoreHorizontal className="h-4 w-4" /> | |
</Button> | |
</DropdownMenuTrigger> | |
<DropdownMenuContent align="end"> | |
<DropdownMenuItem onClick={handleViewDetails}> | |
<Eye className="h-4 w-4 mr-2" /> | |
View Details | |
</DropdownMenuItem> | |
<DropdownMenuItem onClick={handleExport}> | |
<Download className="h-4 w-4 mr-2" /> | |
Export Report | |
</DropdownMenuItem> | |
<DropdownMenuItem onClick={() => onDelete(evaluation.id)} className="text-destructive"> | |
<Trash2 className="h-4 w-4 mr-2" /> | |
Delete | |
</DropdownMenuItem> | |
</DropdownMenuContent> | |
</DropdownMenu> | |
</div> | |
</div> | |
</div> | |
</CardHeader> | |
<CardContent className="space-y-4"> | |
{/* Top row: Key metrics */} | |
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> | |
<div className="space-y-1"> | |
<span className="text-sm text-muted-foreground font-medium">Completeness</span> | |
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-6 overflow-hidden"> | |
<div | |
className={`h-full rounded-full transition-all duration-300 flex items-center justify-center text-xs font-bold text-white ${getCompletenessColor(completenessScore)}`} | |
style={{ width: `${Math.max(completenessScore, 8)}%` }} | |
> | |
{completenessScore}% | |
</div> | |
</div> | |
</div> | |
<div className="space-y-1"> | |
<span className="text-sm text-muted-foreground font-medium">Submitted</span> | |
<p className="text-sm font-semibold">{evaluation.completedDate}</p> | |
</div> | |
</div> | |
{/* Quick summary stats */} | |
<div className="space-y-3"> | |
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> | |
<div className="space-y-2"> | |
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1"> | |
<span className="text-sm text-muted-foreground font-medium">Capability Eval</span> | |
<span className="text-xs text-muted-foreground">({evaluation.capabilityEval.totalApplicable} applicable)</span> | |
</div> | |
<div className="flex gap-1"> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<div className="flex-1 h-2 bg-emerald-500 rounded-full cursor-help" style={{ | |
opacity: evaluation.capabilityEval.strong > 0 ? 1 : 0.2, | |
flexGrow: evaluation.capabilityEval.strong | |
}} /> | |
</TooltipTrigger> | |
<TooltipContent> | |
<div className="flex items-center gap-2"> | |
<div className="w-3 h-3 bg-emerald-500 rounded-full"></div> | |
<span className="text-xs"><strong>Strong:</strong> {evaluation.capabilityEval.strong} categories - Most evals reported</span> | |
</div> | |
</TooltipContent> | |
</Tooltip> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<div className="flex-1 h-2 bg-blue-500 rounded-full cursor-help" style={{ | |
opacity: evaluation.capabilityEval.adequate > 0 ? 1 : 0.2, | |
flexGrow: evaluation.capabilityEval.adequate | |
}} /> | |
</TooltipTrigger> | |
<TooltipContent> | |
<div className="flex items-center gap-2"> | |
<div className="w-3 h-3 bg-blue-500 rounded-full"></div> | |
<span className="text-xs"><strong>Adequate:</strong> {evaluation.capabilityEval.adequate} categories - Many evals reported</span> | |
</div> | |
</TooltipContent> | |
</Tooltip> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<div className="flex-1 h-2 bg-yellow-500 rounded-full cursor-help" style={{ | |
opacity: evaluation.capabilityEval.weak > 0 ? 1 : 0.2, | |
flexGrow: evaluation.capabilityEval.weak | |
}} /> | |
</TooltipTrigger> | |
<TooltipContent> | |
<div className="flex items-center gap-2"> | |
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div> | |
<span className="text-xs"><strong>Weak:</strong> {evaluation.capabilityEval.weak} categories - Some evals reported</span> | |
</div> | |
</TooltipContent> | |
</Tooltip> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<div className="flex-1 h-2 bg-red-500 rounded-full cursor-help" style={{ | |
opacity: evaluation.capabilityEval.insufficient > 0 ? 1 : 0.2, | |
flexGrow: evaluation.capabilityEval.insufficient | |
}} /> | |
</TooltipTrigger> | |
<TooltipContent> | |
<div className="flex items-center gap-2"> | |
<div className="w-3 h-3 bg-red-500 rounded-full"></div> | |
<span className="text-xs"><strong>Insufficient:</strong> {evaluation.capabilityEval.insufficient} categories - Few evals reported</span> | |
</div> | |
</TooltipContent> | |
</Tooltip> | |
</div> | |
</div> | |
<div className="space-y-2"> | |
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1"> | |
<span className="text-sm text-muted-foreground font-medium">Risk Eval</span> | |
<span className="text-xs text-muted-foreground">({evaluation.riskEval.totalApplicable} applicable)</span> | |
</div> | |
<div className="flex gap-1"> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<div className="flex-1 h-2 bg-emerald-500 rounded-full cursor-help" style={{ | |
opacity: evaluation.riskEval.strong > 0 ? 1 : 0.2, | |
flexGrow: evaluation.riskEval.strong | |
}} /> | |
</TooltipTrigger> | |
<TooltipContent> | |
<div className="flex items-center gap-2"> | |
<div className="w-3 h-3 bg-emerald-500 rounded-full"></div> | |
<span className="text-xs"><strong>Strong:</strong> {evaluation.riskEval.strong} categories - Most evals reported</span> | |
</div> | |
</TooltipContent> | |
</Tooltip> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<div className="flex-1 h-2 bg-blue-500 rounded-full cursor-help" style={{ | |
opacity: evaluation.riskEval.adequate > 0 ? 1 : 0.2, | |
flexGrow: evaluation.riskEval.adequate | |
}} /> | |
</TooltipTrigger> | |
<TooltipContent> | |
<div className="flex items-center gap-2"> | |
<div className="w-3 h-3 bg-blue-500 rounded-full"></div> | |
<span className="text-xs"><strong>Adequate:</strong> {evaluation.riskEval.adequate} categories - Many evals reported</span> | |
</div> | |
</TooltipContent> | |
</Tooltip> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<div className="flex-1 h-2 bg-yellow-500 rounded-full cursor-help" style={{ | |
opacity: evaluation.riskEval.weak > 0 ? 1 : 0.2, | |
flexGrow: evaluation.riskEval.weak | |
}} /> | |
</TooltipTrigger> | |
<TooltipContent> | |
<div className="flex items-center gap-2"> | |
<div className="w-3 h-3 bg-yellow-500 rounded-full"></div> | |
<span className="text-xs"><strong>Weak:</strong> {evaluation.riskEval.weak} categories - Some evals reported</span> | |
</div> | |
</TooltipContent> | |
</Tooltip> | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<div className="flex-1 h-2 bg-red-500 rounded-full cursor-help" style={{ | |
opacity: evaluation.riskEval.insufficient > 0 ? 1 : 0.2, | |
flexGrow: evaluation.riskEval.insufficient | |
}} /> | |
</TooltipTrigger> | |
<TooltipContent> | |
<div className="flex items-center gap-2"> | |
<div className="w-3 h-3 bg-red-500 rounded-full"></div> | |
<span className="text-xs"><strong>Insufficient:</strong> {evaluation.riskEval.insufficient} categories - Few evals reported</span> | |
</div> | |
</TooltipContent> | |
</Tooltip> | |
</div> | |
</div> | |
</div> | |
</div> | |
</CardContent> | |
</Card> | |
</TooltipProvider> | |
) | |
} | |