From 363a4688a67af78f697050e5f66765a1250f55fa Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Thu, 23 Apr 2026 15:18:29 -0700 Subject: [PATCH] =?UTF-8?q?feat(ui):=20update=20EvalViewer=20=E2=80=94=20t?= =?UTF-8?q?itle=20'LLM=20Judge=20Results',=20weighted=20score=20column,=20?= =?UTF-8?q?summary=20row?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../view_logs/EvalViewer/EvalViewer.tsx | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/view_logs/EvalViewer/EvalViewer.tsx diff --git a/ui/litellm-dashboard/src/components/view_logs/EvalViewer/EvalViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/EvalViewer/EvalViewer.tsx new file mode 100644 index 00000000000..e12c2917a00 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/EvalViewer/EvalViewer.tsx @@ -0,0 +1,203 @@ +import React from "react"; +import { Card, Tag, Table, Typography, Space, Tooltip } from "antd"; +import { CheckCircleOutlined, CloseCircleOutlined, ExperimentOutlined } from "@ant-design/icons"; + +const { Text } = Typography; + +interface EvalVerdict { + criterion_name: string; + score: number; + reasoning: string; + passed: boolean; + weight?: number; +} + +interface EvalInformation { + eval_id?: string; + eval_name: string; + overall_score: number; + passed: boolean; + judge_model: string; + iteration?: number; + eval_error?: string | null; + verdicts?: EvalVerdict[]; + threshold?: number; +} + +interface EvalViewerProps { + data: EvalInformation | EvalInformation[]; +} + +export default function EvalViewer({ data }: EvalViewerProps) { + const entries: EvalInformation[] = Array.isArray(data) ? data : [data]; + + if (!entries.length) return null; + + return ( +
+
+ + + LLM Judge Results + +
+ + {entries.map((entry, idx) => ( + + ))} +
+ ); +} + +function EvalEntryCard({ entry }: { entry: EvalInformation }) { + const passed = entry.passed; + const scoreColor = passed ? "#52c41a" : "#ff4d4f"; + + // Filter out synthetic "Overall" row the judge sometimes appends — it's already in the header + const verdicts = (entry.verdicts || []).filter( + (v) => (v.criterion_name || "").toLowerCase() !== "overall" + ); + + const columns = [ + { + title: "Criterion", + dataIndex: "criterion_name", + key: "criterion_name", + width: 160, + render: (v: string) => {v}, + }, + { + title: "Weight", + dataIndex: "weight", + key: "weight", + width: 65, + render: (v: number) => + v != null ? ( + {v}% + ) : null, + }, + { + title: "Score", + dataIndex: "score", + key: "score", + width: 65, + render: (v: number) => ( + = 70 ? "#52c41a" : v >= 50 ? "#faad14" : "#ff4d4f", fontWeight: 600 }}> + {v} + + ), + }, + { + title: ( + + Weighted + + ), + key: "weighted", + width: 75, + render: (_: unknown, row: EvalVerdict) => { + if (row.weight == null) return null; + const contrib = (row.score * row.weight) / 100; + return ( + + {contrib % 1 === 0 ? contrib : contrib.toFixed(1)} + + ); + }, + }, + { + title: "Comment", + dataIndex: "reasoning", + key: "reasoning", + ellipsis: { showTitle: false }, + render: (v: string) => ( + + {v} + + ), + }, + ]; + + return ( + + {passed ? ( + + ) : ( + + )} + {entry.eval_name} + {passed ? "PASSED" : "FAILED"} + + + {entry.overall_score?.toFixed(0)} / 100 + {entry.threshold != null && ` (threshold: ${entry.threshold})`} + + + + } + extra={ + + {entry.judge_model && ( + + Judge: {entry.judge_model} + + )} + {entry.iteration != null && ( + + Iter: {entry.iteration + 1} + + )} + + } + > + {entry.eval_error && ( + + Judge error: {entry.eval_error} + + )} + + {verdicts.length > 0 ? ( + { + const hasWeights = verdicts.some((v) => v.weight != null); + if (!hasWeights) return null; + const total = verdicts.reduce( + (sum, v) => sum + (v.weight != null ? (v.score * v.weight) / 100 : 0), + 0 + ); + return ( + + + Total + + + + + + {total % 1 === 0 ? total : total.toFixed(1)} + + + + + ); + }} + /> + ) : ( + + Score: {entry.overall_score?.toFixed(1)} — no per-criterion breakdown available. + + )} + + ); +}