"use client"
import { useState, useMemo } from "react"
import { cn } from "@/lib/utils"
import { MultiSelect } from "@/components/multi-select"
function Tooltip({ text, children }: { text: string; children: React.ReactNode }) {
const [show, setShow] = useState(false)
return (
setShow(true)}
onMouseLeave={() => setShow(false)}
>
{children}
{show && (
{text}
)}
)
}
export interface StatCardProps {
label: string
value: string | number
subtext?: string
mono?: boolean
}
export function StatCard({ label, value, subtext, mono }: StatCardProps) {
return (
{label}
{value}
{subtext &&
{subtext}
}
)
}
export interface StatsGridProps {
cards: StatCardProps[]
}
export function StatsGrid({ cards }: StatsGridProps) {
return (
{cards.map((card, idx) => (
))}
)
}
export interface QuestionTypeStats {
accuracy: number
correct: number
total: number
}
export interface AccuracyByTypeProps {
byQuestionType: Record
}
export function AccuracyByType({ byQuestionType }: AccuracyByTypeProps) {
if (!byQuestionType || Object.keys(byQuestionType).length === 0) {
return null
}
return (
Accuracy by Question Type
{Object.entries(byQuestionType).map(([type, stats]) => (
{type.replace(/[-_]/g, " ")}
{(stats.accuracy * 100).toFixed(0)}%
{stats.correct}/{stats.total}
))}
)
}
export interface LatencyStats {
min: number
max: number
mean: number
median: number
p95: number
p99: number
}
export interface RetrievalStats {
hitAtK: number
precisionAtK: number
recallAtK: number
f1AtK: number
mrr: number
ndcg: number
k: number
}
export interface LatencyTableProps {
latency?: {
ingest?: LatencyStats
indexing?: LatencyStats
search?: LatencyStats
answer?: LatencyStats
evaluate?: LatencyStats
total?: LatencyStats
} | null
}
export function LatencyTable({ latency }: LatencyTableProps) {
if (!latency) return null
return (
Latency Stats (ms)
phase
min
max
mean
median
p95
p99
{(["ingest", "indexing", "search", "answer", "evaluate", "total"] as const).map(
(phase) => {
const stats = latency[phase]
if (!stats) return null
return (
{phase}
{stats.min}
{stats.max}
{stats.mean}
{stats.median}
{stats.p95}
{stats.p99}
)
}
)}
)
}
export interface RetrievalMetricsProps {
retrieval?: RetrievalStats | null
byQuestionType?: Record | null
}
export function RetrievalMetrics({ retrieval, byQuestionType }: RetrievalMetricsProps) {
if (!retrieval) return null
const questionTypes = byQuestionType
? Object.entries(byQuestionType).filter(([_, stats]) => stats.retrieval)
: []
return (
Retrieval Quality (K={retrieval.k})
Hit@{retrieval.k}
{(retrieval.hitAtK * 100).toFixed(0)}%
found relevant
MRR
{retrieval.mrr.toFixed(2)}
mean reciprocal rank
NDCG
{retrieval.ndcg.toFixed(2)}
ranking quality
F1@{retrieval.k}
{(retrieval.f1AtK * 100).toFixed(0)}%
precision-recall balance
metric
overall
{questionTypes.map(([type]) => (
{type.replace(/[-_]/g, " ")}
))}
{(["hitAtK", "precisionAtK", "recallAtK", "f1AtK", "mrr", "ndcg"] as const).map(
(metric) => {
const labels: Record = {
hitAtK: `Hit@${retrieval.k}`,
precisionAtK: "Precision",
recallAtK: "Recall",
f1AtK: "F1",
mrr: "MRR",
ndcg: "NDCG",
}
const tooltips: Record = {
hitAtK: "found at least one relevant result",
precisionAtK: "relevant results out of retrieved",
recallAtK: "found relevant content",
f1AtK: "precision-recall balance",
mrr: "mean reciprocal rank",
ndcg: "ranking quality score",
}
const isPercentage = ["hitAtK", "precisionAtK", "recallAtK", "f1AtK"].includes(
metric
)
const format = (v: number) =>
isPercentage ? `${(v * 100).toFixed(1)}%` : v.toFixed(3)
return (
{labels[metric]}
{format(retrieval[metric])}
{questionTypes.map(([type, stats]) => (
{stats.retrieval ? format(stats.retrieval[metric]) : "—"}
))}
)
}
)}
)
}
export interface EvaluationResult {
questionId: string
questionType: string
question?: string
groundTruth: string
hypothesis?: string
score?: number
label?: string
explanation?: string
}
export interface EvaluationListProps {
evaluations: EvaluationResult[]
onViewDetails?: (questionId: string) => void
}
export function EvaluationList({ evaluations, onViewDetails }: EvaluationListProps) {
const [expandedId, setExpandedId] = useState(null)
const [search, setSearch] = useState("")
const [selectedTypes, setSelectedTypes] = useState([])
const [showFailuresOnly, setShowFailuresOnly] = useState(false)
const questionTypes = useMemo(() => {
const counts: Record = {}
evaluations.forEach((e) => {
const type = e.questionType || "unknown"
counts[type] = (counts[type] || 0) + 1
})
return Object.entries(counts).map(([value, count]) => ({
value,
label: value.replace(/[-_]/g, " "),
count,
}))
}, [evaluations])
const failureCount = useMemo(() => {
return evaluations.filter((e) => e.label === "incorrect" || e.score === 0).length
}, [evaluations])
const filtered = useMemo(() => {
return evaluations.filter((e) => {
if (showFailuresOnly && e.label !== "incorrect" && e.score !== 0) {
return false
}
if (search) {
const searchLower = search.toLowerCase()
const matchesSearch =
e.questionId.toLowerCase().includes(searchLower) ||
(e.question?.toLowerCase().includes(searchLower) ?? false) ||
e.groundTruth.toLowerCase().includes(searchLower) ||
(e.hypothesis?.toLowerCase().includes(searchLower) ?? false)
if (!matchesSearch) return false
}
const type = e.questionType || "unknown"
if (selectedTypes.length > 0 && !selectedTypes.includes(type)) {
return false
}
return true
})
}, [evaluations, search, selectedTypes, showFailuresOnly])
const hasActiveFilters = search || selectedTypes.length > 0 || showFailuresOnly
if (evaluations.length === 0) {
return No results available
}
return (
Showing {filtered.length} of {evaluations.length}{" "}
{evaluations.length === 1 ? "result" : "results"}
{
setSearch("")
setSelectedTypes([])
setShowFailuresOnly(false)
}}
>
Clear filters
setShowFailuresOnly(!showFailuresOnly)}
>
Failures
{failureCount}
{filtered.length === 0 ? (
{showFailuresOnly ? "No failures found" : "No results match your filters"}
) : (
{filtered.map((evaluation, idx) => {
const isExpanded = expandedId === evaluation.questionId
const isCorrect = evaluation.score === 1 || evaluation.label === "correct"
const isLast = idx === filtered.length - 1
return (
setExpandedId(isExpanded ? null : evaluation.questionId)}
>
{evaluation.questionId}
{evaluation.questionType?.replace(/[-_]/g, " ")}
{evaluation.question || evaluation.groundTruth}
{evaluation.label}
{onViewDetails && (
{
e.stopPropagation()
onViewDetails(evaluation.questionId)
}}
>
View details
)}
{isExpanded && (
{evaluation.question && (
Question
{evaluation.question}
)}
Ground Truth
{evaluation.groundTruth}
Model Answer
{evaluation.hypothesis || "—"}
{evaluation.explanation && (
Explanation
{evaluation.explanation}
)}
)}
)
})}
)}
)
}