refactor(ui): migrate log details drawer off antd to shadcn

Replaces Ant Design across every source file under src/components/view_logs,
so the request log drawer and its viewers compose @/components/ui primitives.

- Drawer becomes Sheet, Collapse becomes Collapsible, Segmented and Radio.Group
  become Tabs, Tag becomes Badge, Descriptions becomes a local grid helper
- every lucide icon carries an explicit size class, since antd icons render at
  1em while lucide defaults to 24px
- two tests dropped assertions on antd internal class names in favour of
  rendered text and roles, and the Pretty/JSON case now proves the toggle
  actually swaps the body rather than only that both controls render
- drops the eslint suppressions these files no longer need
This commit is contained in:
Yuneng Jiang 2026-08-14 01:51:24 -07:00
parent 423b791ee0
commit 03d2b16bcb
No known key found for this signature in database
15 changed files with 1154 additions and 1109 deletions

View file

@ -3506,29 +3506,15 @@
"count": 1
}
},
"src/components/view_logs/CostBreakdownViewer.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/view_logs/EvalViewer/EvalViewer.tsx": {
"local/no-complex-jsx-arrow": {
"count": 1
},
"no-nested-ternary": {
"count": 1
},
"no-restricted-imports": {
"count": 1
}
},
"src/components/view_logs/GuardrailViewer/CompliancePanel.tsx": {
"no-nested-ternary": {
"count": 2
},
"no-restricted-imports": {
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 1
}
@ -3541,31 +3527,17 @@
"src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx": {
"no-nested-ternary": {
"count": 4
},
"no-restricted-imports": {
"count": 1
}
},
"src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx": {
"no-nested-ternary": {
"count": 3
},
"no-restricted-imports": {
"count": 1
}
},
"src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx": {
"no-nested-ternary": {
"count": 2
},
"no-restricted-imports": {
"count": 1
},
"react-hooks/set-state-in-effect": {
"count": 2
}
@ -3575,36 +3547,11 @@
"count": 2
}
},
"src/components/view_logs/LogDetailsDrawer/RealtimePrettyView.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/view_logs/LogDetailsDrawer/useKeyboardNavigation.ts": {
"react-hooks/immutability": {
"count": 2
}
},
"src/components/view_logs/ToolsSection/FormattedToolView.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/view_logs/ToolsSection/ToolExpandedContent.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/view_logs/ToolsSection/ToolItem.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/view_logs/VectorStoreViewer.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/view_logs/columns.tsx": {
"local/filename-pascal-case": {
"count": 1

View file

@ -1,5 +1,6 @@
import React from "react";
import { Collapse } from "antd";
import React, { useState } from "react";
import { ChevronDown, ChevronRight } from "lucide-react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { formatNumberWithCommas } from "@/utils/dataUtils";
export interface CostBreakdown {
@ -49,6 +50,7 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
cacheReadTokens,
cacheCreationTokens,
}) => {
const [open, setOpen] = useState(false);
const isCached = cacheHit?.toLowerCase() === "true";
const hasTokenCounts = promptTokens !== undefined || completionTokens !== undefined;
@ -90,197 +92,195 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
return (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
<Collapse
expandIconPosition="start"
items={[
{
key: "1",
label: (
<div className="flex items-center justify-between w-full">
<h3 className="text-lg font-medium text-gray-900">Cost Breakdown</h3>
<div className="flex items-center space-x-2 mr-4">
<span className="text-sm text-gray-500">Total:</span>
<span className="text-sm font-semibold text-gray-900">
{formatCost(totalSpend)}
{isCached && " (Cached)"}
</span>
</div>
</div>
),
children: (
<div className="p-6 space-y-4">
{/* Step 1: Base Token Costs */}
<div className="space-y-2 max-w-2xl">
{(() => {
const hasCacheBreakdown =
costBreakdown?.cache_read_cost !== undefined || costBreakdown?.cache_creation_cost !== undefined;
if (hasCacheBreakdown) {
// Separate line items: Input / Cache Read / Cache Write
const rawCost = isCached
? 0
: (inputCost ?? 0) -
(costBreakdown?.cache_read_cost ?? 0) -
(costBreakdown?.cache_creation_cost ?? 0);
return (
<>
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
<span className="text-gray-900">
{formatCost(rawCost)}
{rawInputTokens !== undefined && rawInputTokens !== null && (
<span className="text-gray-500 font-normal ml-1">
({rawInputTokens.toLocaleString()} tokens)
</span>
)}
</span>
</div>
{(costBreakdown?.cache_read_cost ?? 0) > 0 && (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Prompt Cache Read Cost:</span>
<span className="text-gray-900">
{formatCost(isCached ? 0 : costBreakdown?.cache_read_cost)}
{(cacheReadTokens ?? 0) > 0 && (
<span className="text-gray-500 font-normal ml-1">
({(cacheReadTokens ?? 0).toLocaleString()} tokens)
</span>
)}
</span>
</div>
)}
{(costBreakdown?.cache_creation_cost ?? 0) > 0 && (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Prompt Cache Write Cost:</span>
<span className="text-gray-900">
{formatCost(isCached ? 0 : costBreakdown?.cache_creation_cost)}
{(cacheCreationTokens ?? 0) > 0 && (
<span className="text-gray-500 font-normal ml-1">
({(cacheCreationTokens ?? 0).toLocaleString()} tokens)
</span>
)}
</span>
</div>
)}
</>
);
}
return (
<Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger className="flex w-full items-center gap-3 px-4 py-3 text-left">
{open ? (
<ChevronDown className="size-3.5 shrink-0 text-gray-500" />
) : (
<ChevronRight className="size-3.5 shrink-0 text-gray-500" />
)}
<div className="flex items-center justify-between w-full">
<h3 className="text-lg font-medium text-gray-900">Cost Breakdown</h3>
<div className="flex items-center space-x-2 mr-4">
<span className="text-sm text-gray-500">Total:</span>
<span className="text-sm font-semibold text-gray-900">
{formatCost(totalSpend)}
{isCached && " (Cached)"}
</span>
</div>
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="p-6 space-y-4">
{/* Step 1: Base Token Costs */}
<div className="space-y-2 max-w-2xl">
{(() => {
const hasCacheBreakdown =
costBreakdown?.cache_read_cost !== undefined || costBreakdown?.cache_creation_cost !== undefined;
if (hasCacheBreakdown) {
// Separate line items: Input / Cache Read / Cache Write
const rawCost = isCached
? 0
: (inputCost ?? 0) -
(costBreakdown?.cache_read_cost ?? 0) -
(costBreakdown?.cache_creation_cost ?? 0);
return (
<>
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
<span className="text-gray-900">
{formatCost(inputCost)}
{promptTokens !== undefined && (
{formatCost(rawCost)}
{rawInputTokens !== undefined && rawInputTokens !== null && (
<span className="text-gray-500 font-normal ml-1">
({promptTokens.toLocaleString()} prompt tokens)
({rawInputTokens.toLocaleString()} tokens)
</span>
)}
</span>
</div>
);
})()}
{(costBreakdown?.cache_read_cost ?? 0) > 0 && (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Prompt Cache Read Cost:</span>
<span className="text-gray-900">
{formatCost(isCached ? 0 : costBreakdown?.cache_read_cost)}
{(cacheReadTokens ?? 0) > 0 && (
<span className="text-gray-500 font-normal ml-1">
({(cacheReadTokens ?? 0).toLocaleString()} tokens)
</span>
)}
</span>
</div>
)}
{(costBreakdown?.cache_creation_cost ?? 0) > 0 && (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Prompt Cache Write Cost:</span>
<span className="text-gray-900">
{formatCost(isCached ? 0 : costBreakdown?.cache_creation_cost)}
{(cacheCreationTokens ?? 0) > 0 && (
<span className="text-gray-500 font-normal ml-1">
({(cacheCreationTokens ?? 0).toLocaleString()} tokens)
</span>
)}
</span>
</div>
)}
</>
);
}
return (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Output Cost:</span>
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
<span className="text-gray-900">
{formatCost(outputCost)}
{completionTokens !== undefined && (
{formatCost(inputCost)}
{promptTokens !== undefined && (
<span className="text-gray-500 font-normal ml-1">
({completionTokens.toLocaleString()} completion tokens)
({promptTokens.toLocaleString()} prompt tokens)
</span>
)}
</span>
</div>
{costBreakdown?.tool_usage_cost !== undefined && costBreakdown.tool_usage_cost > 0 && (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Tool Usage Cost:</span>
<span className="text-gray-900">{formatCost(costBreakdown.tool_usage_cost)}</span>
</div>
)}
{costBreakdown?.additional_costs &&
Object.entries(costBreakdown.additional_costs)
.filter(([, value]) => value != null && value !== 0)
.map(([key, value]) => (
<div key={key} className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">{key}:</span>
<span className="text-gray-900">{formatCost(value)}</span>
</div>
))}
</div>
{/* Subtotal / Original Cost - hide when cached since it would be $0 */}
{!isCached && (
<div className="pt-2 border-t border-gray-100 max-w-2xl">
<div className="flex text-sm font-semibold">
<span className="text-gray-900 w-1/3">Original LLM Cost:</span>
<span className="text-gray-900">{formatCost(originalCost)}</span>
</div>
</div>
)}
{/* Step 2: Adjustments (Discount & Margin) */}
{(hasDiscount || hasMargin) && (
<div className="pt-2 space-y-2 max-w-2xl">
{/* Discounts */}
{hasDiscount && (
<div className="space-y-2">
{costBreakdown.discount_percent !== undefined && costBreakdown.discount_percent !== 0 && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">
Discount ({formatPercent(costBreakdown.discount_percent)}):
</span>
<span className="text-gray-900">-{formatCost(costBreakdown.discount_amount)}</span>
</div>
)}
{costBreakdown.discount_amount !== undefined &&
costBreakdown.discount_percent === undefined && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">Discount Amount:</span>
<span className="text-gray-900">-{formatCost(costBreakdown.discount_amount)}</span>
</div>
)}
</div>
)}
{/* Margins */}
{hasMargin && (
<div className="space-y-2">
{costBreakdown.margin_percent !== undefined && costBreakdown.margin_percent !== 0 && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">
Margin ({formatPercent(costBreakdown.margin_percent)}):
</span>
<span className="text-gray-900">
+
{formatCost(
(costBreakdown.margin_total_amount || 0) - (costBreakdown.margin_fixed_amount || 0),
)}
</span>
</div>
)}
{costBreakdown.margin_fixed_amount !== undefined && costBreakdown.margin_fixed_amount !== 0 && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">Margin:</span>
<span className="text-gray-900">+{formatCost(costBreakdown.margin_fixed_amount)}</span>
</div>
)}
</div>
)}
</div>
)}
{/* Final Summary */}
<div className="mt-4 pt-4 border-t border-gray-200 max-w-2xl">
<div className="flex items-center">
<span className="font-bold text-sm text-gray-900 w-1/3">Final Calculated Cost:</span>
<span className="text-sm font-bold text-gray-900">
{formatCost(totalCost)}
{isCached && " (Cached)"}
);
})()}
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Output Cost:</span>
<span className="text-gray-900">
{formatCost(outputCost)}
{completionTokens !== undefined && (
<span className="text-gray-500 font-normal ml-1">
({completionTokens.toLocaleString()} completion tokens)
</span>
</div>
)}
</span>
</div>
{costBreakdown?.tool_usage_cost !== undefined && costBreakdown.tool_usage_cost > 0 && (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Tool Usage Cost:</span>
<span className="text-gray-900">{formatCost(costBreakdown.tool_usage_cost)}</span>
</div>
)}
{costBreakdown?.additional_costs &&
Object.entries(costBreakdown.additional_costs)
.filter(([, value]) => value != null && value !== 0)
.map(([key, value]) => (
<div key={key} className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">{key}:</span>
<span className="text-gray-900">{formatCost(value)}</span>
</div>
))}
</div>
{/* Subtotal / Original Cost - hide when cached since it would be $0 */}
{!isCached && (
<div className="pt-2 border-t border-gray-100 max-w-2xl">
<div className="flex text-sm font-semibold">
<span className="text-gray-900 w-1/3">Original LLM Cost:</span>
<span className="text-gray-900">{formatCost(originalCost)}</span>
</div>
</div>
),
},
]}
/>
)}
{/* Step 2: Adjustments (Discount & Margin) */}
{(hasDiscount || hasMargin) && (
<div className="pt-2 space-y-2 max-w-2xl">
{/* Discounts */}
{hasDiscount && (
<div className="space-y-2">
{costBreakdown.discount_percent !== undefined && costBreakdown.discount_percent !== 0 && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">
Discount ({formatPercent(costBreakdown.discount_percent)}):
</span>
<span className="text-gray-900">-{formatCost(costBreakdown.discount_amount)}</span>
</div>
)}
{costBreakdown.discount_amount !== undefined && costBreakdown.discount_percent === undefined && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">Discount Amount:</span>
<span className="text-gray-900">-{formatCost(costBreakdown.discount_amount)}</span>
</div>
)}
</div>
)}
{/* Margins */}
{hasMargin && (
<div className="space-y-2">
{costBreakdown.margin_percent !== undefined && costBreakdown.margin_percent !== 0 && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">
Margin ({formatPercent(costBreakdown.margin_percent)}):
</span>
<span className="text-gray-900">
+
{formatCost(
(costBreakdown.margin_total_amount || 0) - (costBreakdown.margin_fixed_amount || 0),
)}
</span>
</div>
)}
{costBreakdown.margin_fixed_amount !== undefined && costBreakdown.margin_fixed_amount !== 0 && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">Margin:</span>
<span className="text-gray-900">+{formatCost(costBreakdown.margin_fixed_amount)}</span>
</div>
)}
</div>
)}
</div>
)}
{/* Final Summary */}
<div className="mt-4 pt-4 border-t border-gray-200 max-w-2xl">
<div className="flex items-center">
<span className="font-bold text-sm text-gray-900 w-1/3">Final Calculated Cost:</span>
<span className="text-sm font-bold text-gray-900">
{formatCost(totalCost)}
{isCached && " (Cached)"}
</span>
</div>
</div>
</div>
</CollapsibleContent>
</Collapsible>
</div>
);
};

View file

@ -1,8 +1,9 @@
import React from "react";
import { Card, Tag, Table, Typography, Space, Tooltip } from "antd";
import { CheckCircleOutlined, CloseCircleOutlined, ExperimentOutlined } from "@ant-design/icons";
const { Text } = Typography;
import { CircleCheck, CircleX, FlaskConical } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
interface EvalVerdict {
criterion_name: string;
@ -36,10 +37,10 @@ export default function EvalViewer({ data }: EvalViewerProps) {
return (
<div className="mb-6">
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
<ExperimentOutlined style={{ fontSize: 16, color: "#6366f1" }} />
<Text strong style={{ fontSize: 15 }}>
<FlaskConical className="size-4" style={{ color: "#6366f1" }} />
<span className="font-semibold" style={{ fontSize: 15 }}>
LLM Judge Results
</Text>
</span>
</div>
{entries.map((entry, idx) => (
@ -56,151 +57,159 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
// 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) => (
<Text strong style={{ whiteSpace: "nowrap" }}>
{v}
</Text>
),
},
{
title: "Weight",
dataIndex: "weight",
key: "weight",
width: 65,
render: (v: number) =>
v != null ? (
<Text type="secondary" style={{ fontSize: 12 }}>
{v}%
</Text>
) : null,
},
{
title: "Score",
dataIndex: "score",
key: "score",
width: 65,
render: (v: number) => (
<Text style={{ color: v >= 70 ? "#52c41a" : v >= 50 ? "#faad14" : "#ff4d4f", fontWeight: 600 }}>{v}</Text>
),
},
{
title: (
<Tooltip title="Score × Weight — how much each criterion contributes to the final score">
<span style={{ borderBottom: "1px dashed #aaa", cursor: "help" }}>Weighted</span>
</Tooltip>
),
key: "weighted",
width: 75,
render: (_: unknown, row: EvalVerdict) => {
if (row.weight == null) return null;
const contrib = (row.score * row.weight) / 100;
return (
<Text type="secondary" style={{ fontSize: 12 }}>
{contrib % 1 === 0 ? contrib : contrib.toFixed(1)}
</Text>
);
},
},
{
title: "Comment",
dataIndex: "reasoning",
key: "reasoning",
ellipsis: { showTitle: false },
render: (v: string) => (
<Tooltip title={v}>
<span style={{ fontSize: 12 }}>{v}</span>
</Tooltip>
),
},
];
const hasWeights = verdicts.some((v) => v.weight != null);
const weightedTotal = verdicts.reduce((sum, v) => sum + (v.weight != null ? (v.score * v.weight) / 100 : 0), 0);
return (
<Card
size="small"
className="mb-3"
style={{ borderLeft: `3px solid ${scoreColor}` }}
title={
<Space>
{passed ? (
<CheckCircleOutlined style={{ color: "#52c41a" }} />
) : (
<CloseCircleOutlined style={{ color: "#ff4d4f" }} />
)}
<Text strong>{entry.eval_name}</Text>
<Tag color={passed ? "success" : "error"}>{passed ? "PASSED" : "FAILED"}</Tag>
<Tooltip
title={`Weighted average of all criterion scores. Each criterion has a weight (%) set when the eval was created — higher-weight criteria count more toward the final score.`}
>
<Text type="secondary" style={{ fontSize: 12, cursor: "help", borderBottom: "1px dashed #aaa" }}>
{entry.overall_score?.toFixed(0)} / 100
{entry.threshold != null && ` (threshold: ${entry.threshold})`}
</Text>
</Tooltip>
</Space>
}
extra={
<Space size="small">
{entry.judge_model && (
<Text type="secondary" style={{ fontSize: 12 }}>
Judge: {entry.judge_model}
</Text>
)}
{entry.iteration != null && (
<Text type="secondary" style={{ fontSize: 12 }}>
Iter: {entry.iteration + 1}
</Text>
)}
</Space>
}
>
{entry.eval_error && (
<Text type="warning" style={{ display: "block", marginBottom: 8, fontSize: 12 }}>
Judge error: {entry.eval_error}
</Text>
)}
<Card size="sm" className="mb-3" style={{ borderLeft: `3px solid ${scoreColor}` }}>
<CardHeader>
<CardTitle>
<div className="flex flex-wrap items-center gap-2">
{passed ? (
<CircleCheck className="size-4" style={{ color: "#52c41a" }} />
) : (
<CircleX className="size-4" style={{ color: "#ff4d4f" }} />
)}
<span className="font-semibold">{entry.eval_name}</span>
<Badge variant={passed ? "secondary" : "destructive"}>{passed ? "PASSED" : "FAILED"}</Badge>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<span
className="text-muted-foreground"
style={{ fontSize: 12, cursor: "help", borderBottom: "1px dashed #aaa" }}
/>
}
>
{entry.overall_score?.toFixed(0)} / 100
{entry.threshold != null && ` (threshold: ${entry.threshold})`}
</TooltipTrigger>
<TooltipContent>
Weighted average of all criterion scores. Each criterion has a weight (%) set when the eval was
created higher-weight criteria count more toward the final score.
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</CardTitle>
<CardAction>
<div className="flex items-center gap-2">
{entry.judge_model && (
<span className="text-muted-foreground" style={{ fontSize: 12 }}>
Judge: {entry.judge_model}
</span>
)}
{entry.iteration != null && (
<span className="text-muted-foreground" style={{ fontSize: 12 }}>
Iter: {entry.iteration + 1}
</span>
)}
</div>
</CardAction>
</CardHeader>
{verdicts.length > 0 ? (
<Table
dataSource={verdicts}
columns={columns}
pagination={false}
size="small"
rowKey="criterion_name"
scroll={{ x: true }}
summary={() => {
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 (
<Table.Summary.Row>
<Table.Summary.Cell index={0}>
<Text strong style={{ fontSize: 12 }}>
Total
</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={1} />
<Table.Summary.Cell index={2} />
<Table.Summary.Cell index={3}>
<Text strong style={{ fontSize: 12, color: scoreColor }}>
{total % 1 === 0 ? total : total.toFixed(1)}
</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={4} />
</Table.Summary.Row>
);
}}
/>
) : (
<Text type="secondary" style={{ fontSize: 12 }}>
Score: {entry.overall_score?.toFixed(1)} no per-criterion breakdown available.
</Text>
)}
<CardContent>
{entry.eval_error && (
<span className="text-amber-600" style={{ display: "block", marginBottom: 8, fontSize: 12 }}>
Judge error: {entry.eval_error}
</span>
)}
{verdicts.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead style={{ width: 160 }}>Criterion</TableHead>
<TableHead style={{ width: 65 }}>Weight</TableHead>
<TableHead style={{ width: 65 }}>Score</TableHead>
<TableHead style={{ width: 75 }}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<span style={{ borderBottom: "1px dashed #aaa", cursor: "help" }} />}>
Weighted
</TooltipTrigger>
<TooltipContent>
Score × Weight how much each criterion contributes to the final score
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableHead>
<TableHead>Comment</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{verdicts.map((row) => {
const contrib = row.weight != null ? (row.score * row.weight) / 100 : null;
return (
<TableRow key={row.criterion_name}>
<TableCell>
<span className="font-semibold" style={{ whiteSpace: "nowrap" }}>
{row.criterion_name}
</span>
</TableCell>
<TableCell>
{row.weight != null ? (
<span className="text-muted-foreground" style={{ fontSize: 12 }}>
{row.weight}%
</span>
) : null}
</TableCell>
<TableCell>
<span
style={{
color: row.score >= 70 ? "#52c41a" : row.score >= 50 ? "#faad14" : "#ff4d4f",
fontWeight: 600,
}}
>
{row.score}
</span>
</TableCell>
<TableCell>
{contrib != null ? (
<span className="text-muted-foreground" style={{ fontSize: 12 }}>
{contrib % 1 === 0 ? contrib : contrib.toFixed(1)}
</span>
) : null}
</TableCell>
<TableCell>
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<span style={{ fontSize: 12 }} />}>{row.reasoning}</TooltipTrigger>
<TooltipContent>{row.reasoning}</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableCell>
</TableRow>
);
})}
</TableBody>
{hasWeights && (
<TableFooter>
<TableRow>
<TableCell>
<span className="font-semibold" style={{ fontSize: 12 }}>
Total
</span>
</TableCell>
<TableCell />
<TableCell />
<TableCell>
<span className="font-semibold" style={{ fontSize: 12, color: scoreColor }}>
{weightedTotal % 1 === 0 ? weightedTotal : weightedTotal.toFixed(1)}
</span>
</TableCell>
<TableCell />
</TableRow>
</TableFooter>
)}
</Table>
) : (
<span className="text-muted-foreground" style={{ fontSize: 12 }}>
Score: {entry.overall_score?.toFixed(1)} no per-criterion breakdown available.
</span>
)}
</CardContent>
</Card>
);
}

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react";
import { Tooltip } from "antd";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import {
checkEuAiActCompliance,
checkGdprCompliance,
@ -66,9 +66,12 @@ const ComplianceCard = ({
{loading ? (
<SpinnerIcon />
) : error ? (
<Tooltip title={error}>
<span className="text-gray-400 text-sm">--</span>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<span className="text-gray-400 text-sm" />}>--</TooltipTrigger>
<TooltipContent>{error}</TooltipContent>
</Tooltip>
</TooltipProvider>
) : data?.compliant ? (
<CheckIcon />
) : (

View file

@ -1,5 +1,5 @@
import React, { useState, useMemo } from "react";
import { Tooltip } from "antd";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import PresidioDetectedEntities from "./PresidioDetectedEntities";
import BedrockGuardrailDetails, {
BedrockGuardrailResponse,
@ -517,13 +517,20 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
)}
{riskScore != null && success && (
<Tooltip title={`Risk score: ${riskScore}/10`}>
<span
className={`px-2 py-0.5 border rounded-sm text-[11px] font-semibold shrink-0 ${getRiskColor(riskScore)}`}
>
Risk {riskScore}/10
</span>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<span
className={`px-2 py-0.5 border rounded-sm text-[11px] font-semibold shrink-0 ${getRiskColor(riskScore)}`}
/>
}
>
Risk {riskScore}/10
</TooltipTrigger>
<TooltipContent>{`Risk score: ${riskScore}/10`}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>

View file

@ -1,6 +1,9 @@
import { Button, Space, Tag, Tooltip, Typography } from "antd";
import { CloseOutlined, UpOutlined, DownOutlined } from "@ant-design/icons";
import { useState } from "react";
import { Check, ChevronDown, ChevronUp, Copy, X } from "lucide-react";
import moment from "moment";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { LogEntry } from "../columns";
import { AutoRouterTag } from "@/components/shared/table_cells";
import { ClassifyTag } from "./ClassifyTag";
@ -10,15 +13,11 @@ import {
COLOR_BORDER,
COLOR_BACKGROUND,
SPACING_MEDIUM,
SPACING_LARGE,
FONT_SIZE_HEADER,
FONT_SIZE_MEDIUM,
FONT_FAMILY_MONO,
SPACING_SMALL,
} from "./constants";
const { Text } = Typography;
interface DrawerHeaderProps {
log: LogEntry;
onClose: () => void;
@ -96,7 +95,7 @@ function ModelProviderSection({
providerName?: string;
}) {
return (
<Space size={SPACING_MEDIUM} style={{ marginBottom: SPACING_MEDIUM }}>
<div className="flex items-center gap-2" style={{ marginBottom: SPACING_MEDIUM }}>
{providerLogo && (
<img
src={providerLogo}
@ -108,19 +107,19 @@ function ModelProviderSection({
}}
/>
)}
<Space size={SPACING_MEDIUM} direction="horizontal">
<Text strong style={{ fontSize: 14 }}>
<div className="flex items-center gap-2">
<span className="font-semibold" style={{ fontSize: 14 }}>
{model}
</Text>
</span>
{providerName && (
<Text type="secondary" style={{ fontSize: 12 }}>
<span className="text-muted-foreground" style={{ fontSize: 12 }}>
{providerName}
</Text>
</span>
)}
<AutoRouterTag modelGroup={modelGroup} />
<ClassifyTag origin={internalCallOrigin} />
</Space>
</Space>
</div>
</div>
);
}
@ -128,24 +127,50 @@ function ModelProviderSection({
* Request ID display with copy functionality
*/
function RequestIdSection({ requestId }: { requestId: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(requestId);
setCopied(true);
setTimeout(() => setCopied(false), 1200);
} catch {
/* clipboard unavailable in non-secure contexts */
}
};
return (
<div style={{ flex: 1, minWidth: 0 }}>
<Tooltip title={requestId}>
<Text
strong
copyable={{ text: requestId, tooltips: ["Copy Request ID", "Copied!"] }}
style={{
fontSize: FONT_SIZE_HEADER,
fontFamily: FONT_FAMILY_MONO,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
display: "block",
}}
>
{requestId}
</Text>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={
<span
className="font-semibold"
style={{
fontSize: FONT_SIZE_HEADER,
fontFamily: FONT_FAMILY_MONO,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
display: "block",
}}
/>
}
>
{requestId}
<button
type="button"
aria-label={copied ? "Copied!" : "Copy Request ID"}
onClick={handleCopy}
className="ml-1 align-middle text-muted-foreground hover:text-foreground"
>
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
</button>
</TooltipTrigger>
<TooltipContent>{requestId}</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
}
@ -172,21 +197,29 @@ function NavigationSection({
marginLeft: 4,
background: "#fafafa",
};
const splitStyle = { width: 1, height: 20, background: COLOR_BORDER };
return (
<Space size={SPACING_SMALL} split={<div style={{ width: 1, height: 20, background: COLOR_BORDER }} />}>
<Button type="text" size="small" onClick={onPrevious}>
<UpOutlined />
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" onClick={onPrevious}>
<ChevronUp className="size-4" />
<span style={keyboardShortcutStyle}>K</span>
</Button>
<Button type="text" size="small" onClick={onNext}>
<DownOutlined />
<div style={splitStyle} />
<Button variant="ghost" size="sm" onClick={onNext}>
<ChevronDown className="size-4" />
<span style={keyboardShortcutStyle}>J</span>
</Button>
<Tooltip title="ESC to close">
<Button type="text" icon={<CloseOutlined />} onClick={onClose} />
</Tooltip>
</Space>
<div style={splitStyle} />
<TooltipProvider>
<Tooltip>
<TooltipTrigger render={<Button variant="ghost" size="icon-sm" onClick={onClose} />}>
<X className="size-4" />
</TooltipTrigger>
<TooltipContent>ESC to close</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
}
@ -205,17 +238,17 @@ function StatusBar({
environment: string;
}) {
return (
<Space size={SPACING_LARGE}>
<Tag color={statusColor}>{statusLabel}</Tag>
<Tag>Env: {environment}</Tag>
<Space size={SPACING_MEDIUM}>
<Text type="secondary" style={{ fontSize: FONT_SIZE_MEDIUM }}>
<div className="flex items-center gap-3">
<Badge variant={statusColor === "error" ? "destructive" : "secondary"}>{statusLabel}</Badge>
<Badge variant="outline">Env: {environment}</Badge>
<div className="flex items-center gap-2">
<span className="text-muted-foreground" style={{ fontSize: FONT_SIZE_MEDIUM }}>
{moment(log.startTime).format("MMM D, YYYY h:mm:ss A")}
</Text>
<Text type="secondary" style={{ fontSize: FONT_SIZE_MEDIUM }}>
</span>
<span className="text-muted-foreground" style={{ fontSize: FONT_SIZE_MEDIUM }}>
({moment(log.startTime).fromNow()})
</Text>
</Space>
</Space>
</span>
</div>
</div>
);
}

View file

@ -177,12 +177,19 @@ describe("LogDetailContent", () => {
expect(screen.getByText("Loading request & response data...")).toBeInTheDocument();
});
it("should display Request & Response section with Pretty and JSON view modes", () => {
it("should switch the Request & Response body between the Pretty and JSON view modes", async () => {
const user = userEvent.setup();
render(<LogDetailContent logEntry={createLogEntry()} />);
expect(screen.getByText("Request & Response")).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "Pretty" })).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "JSON" })).toBeInTheDocument();
expect(screen.getByText("Pretty")).toBeInTheDocument();
expect(screen.getByText("JSON")).toBeInTheDocument();
await user.click(screen.getByText("JSON"));
expect(screen.getByRole("tab", { name: "Request" })).toBeInTheDocument();
await user.click(screen.getByText("Pretty"));
expect(screen.queryByRole("tab", { name: "Request" })).not.toBeInTheDocument();
});
it("should display Request and Response tabs when JSON view is selected", async () => {
@ -259,7 +266,7 @@ describe("LogDetailContent", () => {
render(<LogDetailContent logEntry={createLogEntry({ cache_hit: "True" })} />);
expect(screen.getByText("Response Cache")).toBeInTheDocument();
expect(screen.getByText("Hit").closest(".ant-tag")).toHaveClass("ant-tag-green");
expect(screen.getByText("Hit").className).toMatch(/green/);
});
it("should show prompt cache tokens without an alarming red tag when only provider prompt caching occurred", () => {
@ -282,7 +289,7 @@ describe("LogDetailContent", () => {
expect(screen.getByText("34,462")).toBeInTheDocument();
expect(screen.getByText("Prompt Cache Creation Tokens")).toBeInTheDocument();
expect(screen.getByText("83")).toBeInTheDocument();
expect(screen.getByText("Miss").closest(".ant-tag")).not.toHaveClass("ant-tag-red");
expect(screen.getByText("Miss").className).not.toMatch(/red|destructive/);
expect(screen.queryByText("Cache Hit")).not.toBeInTheDocument();
});
@ -310,8 +317,12 @@ describe("LogDetailContent", () => {
const user = userEvent.setup();
render(<LogDetailContent logEntry={createLogEntry({ cache_hit: "True" })} />);
const label = screen.getByText("Response Cache").closest(".ant-space") as HTMLElement;
await user.hover(within(label).getByRole("img", { name: "info-circle" }));
expect(screen.getByText("Response Cache")).toBeInTheDocument();
// Response Cache is the only metric with an info tooltip in this fixture, so an
// unscoped lookup still pins the docs link to that label.
const infoIcons = screen.getAllByRole("img", { name: /info/i });
expect(infoIcons).toHaveLength(1);
await user.hover(infoIcons[0]);
expect(await screen.findByRole("link", { name: "Docs" })).toHaveAttribute(
"href",
@ -333,8 +344,11 @@ describe("LogDetailContent", () => {
/>,
);
const label = screen.getByText("Prompt Cache Read Tokens").closest(".ant-space") as HTMLElement;
await user.hover(within(label).getByRole("img", { name: "info-circle" }));
expect(screen.getByText("Prompt Cache Read Tokens")).toBeInTheDocument();
// Prompt Cache Read Tokens is the only metric with an info tooltip in this fixture.
const infoIcons = screen.getAllByRole("img", { name: /info/i });
expect(infoIcons).toHaveLength(1);
await user.hover(infoIcons[0]);
expect(await screen.findByRole("link", { name: "Docs" })).toHaveAttribute(
"href",
@ -370,7 +384,7 @@ describe("LogDetailContent", () => {
expect(screen.queryByText("LiteLLM Overhead")).not.toBeInTheDocument();
});
const retriesItem = () => screen.getByText("Retries").closest(".ant-descriptions-item") as HTMLElement;
const retriesItem = () => screen.getByText("Retries").parentElement as HTMLElement;
it("should display attempted_retries / max_retries for Retries when attempted_retries > 0", () => {
render(
@ -386,7 +400,7 @@ describe("LogDetailContent", () => {
render(<LogDetailContent logEntry={createLogEntry({ metadata: { status: "success", attempted_retries: 0 } })} />);
const noneTag = within(retriesItem()).getByText("None");
expect(noneTag.closest(".ant-tag")).toHaveClass("ant-tag-green");
expect(noneTag.className).toMatch(/green/);
});
it("should display '-' for Retries when attempted_retries is absent from metadata", () => {
@ -444,8 +458,8 @@ describe("LogDetailContent", () => {
/>,
);
const descriptions = screen.getByText("Provider").closest(".ant-descriptions-item");
const descriptions = screen.getByText("Provider").parentElement as HTMLElement;
expect(descriptions).toBeInTheDocument();
expect(within(descriptions as HTMLElement).getByText("-")).toBeInTheDocument();
expect(within(descriptions).getByText("-")).toBeInTheDocument();
});
});

View file

@ -1,7 +1,13 @@
import { useState } from "react";
import { Typography, Descriptions, Card, Tag, Tabs, Alert, Collapse, Radio, Space, Spin, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Check, ChevronDown, ChevronRight, CircleAlert, Copy, Info } from "lucide-react";
import moment from "moment";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { LogEntry } from "../columns";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { PROMPT_CACHE_CREATION_TOOLTIP, PROMPT_CACHE_READ_TOOLTIP } from "@/utils/promptCacheUsage";
@ -32,13 +38,10 @@ import {
FONT_SIZE_SMALL,
FONT_FAMILY_MONO,
SPACING_XLARGE,
SPACING_MEDIUM,
} from "./constants";
import { ToolsSection } from "../ToolsSection";
import { PrettyMessagesView } from "./PrettyMessagesView";
const { Text } = Typography;
export interface LogDetailContentProps {
logEntry: LogEntry;
/** When true, log details (messages/response) are still being lazy-loaded. */
@ -100,13 +103,16 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
<div style={{ padding: `${DRAWER_CONTENT_PADDING} ${DRAWER_CONTENT_PADDING} 0` }}>
{/* Error Alert */}
{hasError && errorInfo && (
<Alert
type="error"
showIcon
message="Request Failed"
description={<ErrorDescription errorInfo={errorInfo} />}
className="mb-6"
/>
<div
role="alert"
className="mb-6 flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm"
>
<CircleAlert className="size-4 shrink-0 text-destructive" />
<div>
<div className="font-medium text-destructive">Request Failed</div>
<ErrorDescription errorInfo={errorInfo} />
</div>
</div>
)}
{/* Tags */}
@ -116,26 +122,31 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
{/* Request Details */}
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
<Card title="Request Details" size="small" bordered={false} style={{ marginBottom: 0 }}>
<Descriptions column={2} size="small">
<Descriptions.Item label="Model">{logEntry.model}</Descriptions.Item>
<Descriptions.Item label="Provider">{logEntry.custom_llm_provider || "-"}</Descriptions.Item>
<Descriptions.Item label="Call Type">{logEntry.call_type}</Descriptions.Item>
<Descriptions.Item label="Model ID">
<TruncatedValue value={logEntry.model_id} />
</Descriptions.Item>
<Descriptions.Item label="API Base">
<TruncatedValue value={logEntry.api_base} maxWidth={API_BASE_MAX_WIDTH} />
</Descriptions.Item>
{logEntry.requester_ip_address && (
<Descriptions.Item label="IP Address">{logEntry.requester_ip_address}</Descriptions.Item>
)}
{hasGuardrailData && (
<Descriptions.Item label="Guardrail">
<GuardrailLabel label={primaryGuardrailLabel} maskedCount={totalMaskedEntities} />
</Descriptions.Item>
)}
</Descriptions>
<Card size="sm" style={{ marginBottom: 0 }}>
<CardHeader>
<CardTitle>Request Details</CardTitle>
</CardHeader>
<CardContent>
<DescriptionList>
<DescriptionItem label="Model">{logEntry.model}</DescriptionItem>
<DescriptionItem label="Provider">{logEntry.custom_llm_provider || "-"}</DescriptionItem>
<DescriptionItem label="Call Type">{logEntry.call_type}</DescriptionItem>
<DescriptionItem label="Model ID">
<TruncatedValue value={logEntry.model_id} />
</DescriptionItem>
<DescriptionItem label="API Base">
<TruncatedValue value={logEntry.api_base} maxWidth={API_BASE_MAX_WIDTH} />
</DescriptionItem>
{logEntry.requester_ip_address && (
<DescriptionItem label="IP Address">{logEntry.requester_ip_address}</DescriptionItem>
)}
{hasGuardrailData && (
<DescriptionItem label="Guardrail">
<GuardrailLabel label={primaryGuardrailLabel} maskedCount={totalMaskedEntities} />
</DescriptionItem>
)}
</DescriptionList>
</CardContent>
</Card>
</div>
@ -170,7 +181,7 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
{/* Request/Response JSON */}
{isLoadingDetails ? (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6 p-8 text-center">
<Spin size="default" />
<UiLoadingSpinner className="inline-block size-5" />
<div style={{ marginTop: 8, color: "#999" }}>Loading request &amp; response data...</div>
</div>
) : (
@ -221,17 +232,64 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
// Helper Components
// ============================================================================
function DescriptionList({ children }: { children: React.ReactNode }) {
return <div className="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">{children}</div>;
}
function DescriptionItem({ label, children }: { label: React.ReactNode; children: React.ReactNode }) {
return (
<div className="flex min-w-0 flex-wrap items-start gap-x-2 gap-y-0.5">
<span className="shrink-0 text-muted-foreground after:content-[':']">{label}</span>
<span className="min-w-0 break-words">{children}</span>
</div>
);
}
function CopyButton({
getText,
label,
disabled = false,
}: {
getText: () => string;
label: string;
disabled?: boolean;
}) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(getText());
setCopied(true);
setTimeout(() => setCopied(false), 1200);
} catch {
/* clipboard unavailable in non-secure contexts */
}
};
return (
<Button
variant="ghost"
size="icon-sm"
onClick={handleCopy}
disabled={disabled}
aria-label={copied ? "Copied!" : label}
>
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
</Button>
);
}
function ErrorDescription({ errorInfo }: { errorInfo: any }) {
return (
<div>
{errorInfo.error_code && (
<div>
<Text strong>Error Code:</Text> {errorInfo.error_code}
<span className="font-semibold">Error Code:</span> {errorInfo.error_code}
</div>
)}
{errorInfo.error_message && (
<div>
<Text strong>Message:</Text> {errorInfo.error_message}
<span className="font-semibold">Message:</span> {errorInfo.error_message}
</div>
)}
</div>
@ -241,16 +299,16 @@ function ErrorDescription({ errorInfo }: { errorInfo: any }) {
function TagsSection({ tags }: { tags: Record<string, any> }) {
return (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden p-4 mb-6">
<Text strong style={{ display: "block", marginBottom: 8, fontSize: 16 }}>
<span className="font-semibold" style={{ display: "block", marginBottom: 8, fontSize: 16 }}>
Tags
</Text>
<Space size={SPACING_MEDIUM} wrap>
</span>
<div className="flex flex-wrap items-center gap-2">
{Object.entries(tags).map(([key, value]) => (
<Tag key={key}>
<Badge key={key} variant="outline">
{key}: {String(value)}
</Tag>
</Badge>
))}
</Space>
</div>
</div>
);
}
@ -262,12 +320,12 @@ function GuardrailLabel({ label, maskedCount }: { label: string; maskedCount: nu
};
return (
<Space size={SPACING_MEDIUM}>
<span className="inline-flex items-center gap-2">
<a onClick={handleClick} style={{ cursor: "pointer" }}>
{label}
</a>
{maskedCount > 0 && <Tag color="blue">{maskedCount} masked</Tag>}
</Space>
{maskedCount > 0 && <Badge variant="secondary">{maskedCount} masked</Badge>}
</span>
);
}
@ -291,26 +349,24 @@ const PROMPT_CACHE_DOCS_URL = "https://docs.litellm.ai/docs/completion/prompt_ca
function MetricLabel({ label, tooltip, docsUrl }: { label: string; tooltip: string; docsUrl: string }) {
return (
<Space size={4}>
<span className="inline-flex items-center gap-1">
{label}
<Tooltip
title={
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={<span role="img" aria-label={`${label} info`} className="inline-flex text-muted-foreground" />}
>
<Info className="size-3.5" />
</TooltipTrigger>
<TooltipContent>
{tooltip}{" "}
<a
href={docsUrl}
target="_blank"
rel="noreferrer"
style={{ color: "#91caff", textDecoration: "underline" }}
>
<a href={docsUrl} target="_blank" rel="noreferrer" className="underline">
Docs
</a>
</>
}
>
<InfoCircleOutlined style={{ color: "#8c8c8c" }} />
</Tooltip>
</Space>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</span>
);
}
@ -333,102 +389,111 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
return (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
<Card title="Metrics" size="small" style={{ marginBottom: 0 }}>
<Descriptions column={2} size="small">
{showAnthropicMessagesInputOutput ? (
<>
<Descriptions.Item label="Input Tokens">{formatNumberWithCommas(uncachedInputTokens)}</Descriptions.Item>
<Descriptions.Item label="Output Tokens">
{formatNumberWithCommas(logEntry.completion_tokens)}
</Descriptions.Item>
</>
) : (
<Descriptions.Item label="Tokens">
<TokenFlow
prompt={logEntry.prompt_tokens}
completion={logEntry.completion_tokens}
total={logEntry.total_tokens}
/>
</Descriptions.Item>
)}
<Descriptions.Item label="Cost">${formatNumberWithCommas(logEntry.spend || 0, 8)}</Descriptions.Item>
<Descriptions.Item label="Duration">
{logEntry.request_duration_ms != null ? (logEntry.request_duration_ms / 1000).toFixed(3) : "-"} s
</Descriptions.Item>
{ttftMs != null && ttftMs > 0 && (
<Descriptions.Item label="Time to First Token">{(ttftMs / 1000).toFixed(3)} s</Descriptions.Item>
)}
{showResponseCache && (
<Descriptions.Item
label={
<MetricLabel
label="Response Cache"
tooltip={RESPONSE_CACHE_TOOLTIP}
docsUrl={RESPONSE_CACHE_DOCS_URL}
/>
}
>
<Tag color={isResponseCacheHit ? "green" : "default"}>{isResponseCacheHit ? "Hit" : "Miss"}</Tag>
</Descriptions.Item>
)}
{promptCacheReadTokens > 0 && (
<Descriptions.Item
label={
<MetricLabel
label="Prompt Cache Read Tokens"
tooltip={PROMPT_CACHE_READ_TOOLTIP}
docsUrl={PROMPT_CACHE_DOCS_URL}
/>
}
>
{formatNumberWithCommas(promptCacheReadTokens)}
</Descriptions.Item>
)}
{promptCacheCreationTokens > 0 && (
<Descriptions.Item
label={
<MetricLabel
label="Prompt Cache Creation Tokens"
tooltip={PROMPT_CACHE_CREATION_TOOLTIP}
docsUrl={PROMPT_CACHE_DOCS_URL}
/>
}
>
{formatNumberWithCommas(promptCacheCreationTokens)}
</Descriptions.Item>
)}
{metadata?.litellm_overhead_time_ms !== undefined && metadata.litellm_overhead_time_ms !== null && (
<Descriptions.Item label="LiteLLM Overhead">
{metadata.litellm_overhead_time_ms.toFixed(2)} ms
</Descriptions.Item>
)}
<Descriptions.Item label="Retries">
{metadata?.attempted_retries !== undefined && metadata?.attempted_retries !== null ? (
metadata.attempted_retries > 0 ? (
<>
{metadata.attempted_retries}
{metadata.max_retries !== undefined && metadata.max_retries !== null
? ` / ${metadata.max_retries}`
: ""}
</>
) : (
<Tag color="green">None</Tag>
)
<Card size="sm" style={{ marginBottom: 0 }}>
<CardHeader>
<CardTitle>Metrics</CardTitle>
</CardHeader>
<CardContent>
<DescriptionList>
{showAnthropicMessagesInputOutput ? (
<>
<DescriptionItem label="Input Tokens">{formatNumberWithCommas(uncachedInputTokens)}</DescriptionItem>
<DescriptionItem label="Output Tokens">
{formatNumberWithCommas(logEntry.completion_tokens)}
</DescriptionItem>
</>
) : (
"-"
<DescriptionItem label="Tokens">
<TokenFlow
prompt={logEntry.prompt_tokens}
completion={logEntry.completion_tokens}
total={logEntry.total_tokens}
/>
</DescriptionItem>
)}
<DescriptionItem label="Cost">${formatNumberWithCommas(logEntry.spend || 0, 8)}</DescriptionItem>
<DescriptionItem label="Duration">
{logEntry.request_duration_ms != null ? (logEntry.request_duration_ms / 1000).toFixed(3) : "-"} s
</DescriptionItem>
{ttftMs != null && ttftMs > 0 && (
<DescriptionItem label="Time to First Token">{(ttftMs / 1000).toFixed(3)} s</DescriptionItem>
)}
</Descriptions.Item>
<Descriptions.Item label="Start Time">
{moment(logEntry.startTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")}
</Descriptions.Item>
<Descriptions.Item label="End Time">
{moment(logEntry.endTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")}
</Descriptions.Item>
</Descriptions>
{showResponseCache && (
<DescriptionItem
label={
<MetricLabel
label="Response Cache"
tooltip={RESPONSE_CACHE_TOOLTIP}
docsUrl={RESPONSE_CACHE_DOCS_URL}
/>
}
>
<Badge variant="secondary" className={isResponseCacheHit ? "bg-green-100 text-green-700" : undefined}>
{isResponseCacheHit ? "Hit" : "Miss"}
</Badge>
</DescriptionItem>
)}
{promptCacheReadTokens > 0 && (
<DescriptionItem
label={
<MetricLabel
label="Prompt Cache Read Tokens"
tooltip={PROMPT_CACHE_READ_TOOLTIP}
docsUrl={PROMPT_CACHE_DOCS_URL}
/>
}
>
{formatNumberWithCommas(promptCacheReadTokens)}
</DescriptionItem>
)}
{promptCacheCreationTokens > 0 && (
<DescriptionItem
label={
<MetricLabel
label="Prompt Cache Creation Tokens"
tooltip={PROMPT_CACHE_CREATION_TOOLTIP}
docsUrl={PROMPT_CACHE_DOCS_URL}
/>
}
>
{formatNumberWithCommas(promptCacheCreationTokens)}
</DescriptionItem>
)}
{metadata?.litellm_overhead_time_ms !== undefined && metadata.litellm_overhead_time_ms !== null && (
<DescriptionItem label="LiteLLM Overhead">
{metadata.litellm_overhead_time_ms.toFixed(2)} ms
</DescriptionItem>
)}
<DescriptionItem label="Retries">
{metadata?.attempted_retries !== undefined && metadata?.attempted_retries !== null ? (
metadata.attempted_retries > 0 ? (
<>
{metadata.attempted_retries}
{metadata.max_retries !== undefined && metadata.max_retries !== null
? ` / ${metadata.max_retries}`
: ""}
</>
) : (
<Badge variant="secondary" className="bg-green-100 text-green-700">
None
</Badge>
)
) : (
"-"
)}
</DescriptionItem>
<DescriptionItem label="Start Time">
{moment(logEntry.startTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")}
</DescriptionItem>
<DescriptionItem label="End Time">
{moment(logEntry.endTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")}
</DescriptionItem>
</DescriptionList>
</CardContent>
</Card>
</div>
);
@ -449,6 +514,7 @@ function RequestResponseSection({
getFormattedResponse,
logEntry,
}: RequestResponseSectionProps) {
const [open, setOpen] = useState(true);
const [activeTab, setActiveTab] = useState<typeof TAB_REQUEST | typeof TAB_RESPONSE>(TAB_REQUEST);
const [viewMode, setViewMode] = useState<"pretty" | "json">("pretty");
@ -476,90 +542,76 @@ function RequestResponseSection({
return (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
<Collapse
defaultActiveKey={["1"]}
expandIconPosition="start"
items={[
{
key: "1",
label: (
<div
style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }}
onClick={(e) => {
const target = e.target as HTMLElement;
if (target.closest(".ant-radio-group")) {
e.stopPropagation();
}
}}
>
<h3 className="text-lg font-medium text-gray-900" style={{ margin: 0 }}>
Request & Response
</h3>
<Radio.Group size="small" value={viewMode} onChange={(e) => setViewMode(e.target.value)}>
<Radio.Button value="pretty">Pretty</Radio.Button>
<Radio.Button value="json">JSON</Radio.Button>
</Radio.Group>
</div>
),
children: (
<div>
{viewMode === "pretty" ? (
<PrettyMessagesView
request={getRawRequest()}
response={getFormattedResponse()}
metrics={{
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
input_cost: inputCost,
output_cost: outputCost,
}}
/>
) : (
<Tabs
activeKey={activeTab}
onChange={(key) => setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)}
tabBarExtraContent={
<Text
copyable={{
text: getCopyText(),
tooltips: ["Copy JSON", "Copied!"],
}}
disabled={activeTab === TAB_RESPONSE && !hasResponse && !hasError}
/>
}
items={[
{
key: TAB_REQUEST,
label: "Request",
children: (
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
<JsonViewer data={getRawRequest()} mode="formatted" />
</div>
),
},
{
key: TAB_RESPONSE,
label: "Response",
children: (
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
{hasResponse || hasError ? (
<JsonViewer data={getFormattedResponse()} mode="formatted" />
) : (
<div style={{ textAlign: "center", padding: 20, color: "#999", fontStyle: "italic" }}>
Response data not available
</div>
)}
</div>
),
},
]}
/>
)}
</div>
),
},
]}
/>
<Collapsible open={open} onOpenChange={setOpen}>
<Tabs value={viewMode} onValueChange={(value) => setViewMode(value as "pretty" | "json")}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }}>
<CollapsibleTrigger className="flex flex-1 items-center gap-3 px-4 py-3 text-left">
{open ? (
<ChevronDown className="size-3.5 shrink-0 text-gray-500" />
) : (
<ChevronRight className="size-3.5 shrink-0 text-gray-500" />
)}
<h3 className="text-lg font-medium text-gray-900" style={{ margin: 0 }}>
Request & Response
</h3>
</CollapsibleTrigger>
<TabsList className="mr-4">
<TabsTrigger value="pretty">Pretty</TabsTrigger>
<TabsTrigger value="json">JSON</TabsTrigger>
</TabsList>
</div>
<CollapsibleContent>
<div>
<TabsContent value="pretty">
<PrettyMessagesView
request={getRawRequest()}
response={getFormattedResponse()}
metrics={{
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
input_cost: inputCost,
output_cost: outputCost,
}}
/>
</TabsContent>
<TabsContent value="json">
<Tabs
value={activeTab}
onValueChange={(key) => setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)}
>
<div className="flex items-center justify-between">
<TabsList>
<TabsTrigger value={TAB_REQUEST}>Request</TabsTrigger>
<TabsTrigger value={TAB_RESPONSE}>Response</TabsTrigger>
</TabsList>
<CopyButton
getText={getCopyText}
label="Copy JSON"
disabled={activeTab === TAB_RESPONSE && !hasResponse && !hasError}
/>
</div>
<TabsContent value={TAB_REQUEST}>
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
<JsonViewer data={getRawRequest()} mode="formatted" />
</div>
</TabsContent>
<TabsContent value={TAB_RESPONSE}>
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
{hasResponse || hasError ? (
<JsonViewer data={getFormattedResponse()} mode="formatted" />
) : (
<div style={{ textAlign: "center", padding: 20, color: "#999", fontStyle: "italic" }}>
Response data not available
</div>
)}
</div>
</TabsContent>
</Tabs>
</TabsContent>
</div>
</CollapsibleContent>
</Tabs>
</Collapsible>
</div>
);
}
@ -602,43 +654,40 @@ export function GuardrailJumpLink({ guardrailEntries }: { guardrailEntries: any[
}
function MetadataSection({ metadata }: { metadata: Record<string, any> }) {
const [open, setOpen] = useState(true);
return (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
<Collapse
defaultActiveKey={["1"]}
expandIconPosition="start"
items={[
{
key: "1",
label: <h3 className="text-lg font-medium text-gray-900">Metadata</h3>,
children: (
<div>
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 8 }}>
<Text
copyable={{
text: JSON.stringify(metadata, null, 2),
tooltips: ["Copy Metadata", "Copied!"],
}}
/>
</div>
<pre
style={{
maxHeight: METADATA_MAX_HEIGHT,
overflowY: "auto",
fontSize: FONT_SIZE_SMALL,
fontFamily: FONT_FAMILY_MONO,
whiteSpace: "pre-wrap",
wordBreak: "break-all",
margin: 0,
}}
>
{JSON.stringify(metadata, null, 2)}
</pre>
</div>
),
},
]}
/>
<Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger className="flex w-full items-center gap-3 px-4 py-3 text-left">
{open ? (
<ChevronDown className="size-3.5 shrink-0 text-gray-500" />
) : (
<ChevronRight className="size-3.5 shrink-0 text-gray-500" />
)}
<h3 className="text-lg font-medium text-gray-900">Metadata</h3>
</CollapsibleTrigger>
<CollapsibleContent>
<div>
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 8 }}>
<CopyButton getText={() => JSON.stringify(metadata, null, 2)} label="Copy Metadata" />
</div>
<pre
style={{
maxHeight: METADATA_MAX_HEIGHT,
overflowY: "auto",
fontSize: FONT_SIZE_SMALL,
fontFamily: FONT_FAMILY_MONO,
whiteSpace: "pre-wrap",
wordBreak: "break-all",
margin: 0,
}}
>
{JSON.stringify(metadata, null, 2)}
</pre>
</div>
</CollapsibleContent>
</Collapsible>
</div>
);
}

View file

@ -1,7 +1,8 @@
import { useEffect, useMemo, useState } from "react";
import { Button, Drawer, Segmented } from "antd";
import { CheckOutlined, CopyOutlined, LeftOutlined, RightOutlined } from "@ant-design/icons";
import { Bot, Sparkles, Wrench } from "lucide-react";
import { Bot, Check, ChevronLeft, ChevronRight, Copy, Sparkles, Wrench } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent } from "@/components/ui/sheet";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { LogEntry } from "../columns";
import { AutoRouterIcon, useIsAutoRoutedModelGroup } from "@/components/shared/table_cells";
import { AGENT_CALL_TYPES, MCP_CALL_TYPES } from "../constants";
@ -297,181 +298,183 @@ export function LogDetailsDrawer({
if (!currentLog || !enrichedLog) return null;
return (
<Drawer
title={null}
placement="right"
onClose={onClose}
<Sheet
open={open}
width={DRAWER_WIDTH}
closable={false}
mask={true}
maskClosable={true}
styles={{
body: { padding: 0, overflow: "hidden" },
header: { display: "none" },
onOpenChange={(nextOpen) => {
if (!nextOpen) onClose();
}}
>
<div style={{ height: "100%" }} className="flex relative">
{!isSidebarCollapsed ? (
<Button
type="text"
size="small"
icon={<LeftOutlined />}
onClick={() => setIsSidebarCollapsed(true)}
className="absolute top-2 left-2 z-20 bg-white! border! border-slate-200! rounded-md!"
aria-label="Collapse trace sidebar"
/>
) : (
<Button
type="text"
size="small"
icon={<RightOutlined />}
onClick={() => setIsSidebarCollapsed(false)}
className="absolute top-2 left-2 z-20 bg-white! border! border-slate-200! rounded-md!"
aria-label="Expand trace sidebar"
/>
)}
{!isSidebarCollapsed && (
<div className="border-r border-slate-200 bg-slate-50 flex flex-col" style={{ width: SIDEBAR_WIDTH_PX }}>
<div className="pl-12 pr-3 py-2 border-b border-slate-200 bg-white">
<div className="flex items-start justify-between gap-2">
<div>
<div className="text-[10px] uppercase tracking-wide text-slate-500">
{isSessionMode ? "Session" : "Trace"}
</div>
<div className="font-mono text-[12px] text-slate-900 leading-tight flex items-center gap-1">
<span className="truncate">{leftPanelDisplayId}</span>
<button
type="button"
onClick={handleCopyLeftPanelId}
className="text-slate-400 hover:text-slate-600"
aria-label="Copy trace id"
>
{copiedLeftPanelId ? (
<CheckOutlined className="text-[11px]" />
) : (
<CopyOutlined className="text-[11px]" />
)}
</button>
<SheetContent
side="right"
showCloseButton={false}
className="gap-0 overflow-hidden p-0 data-[side=right]:sm:max-w-none"
style={{ width: DRAWER_WIDTH }}
>
<div style={{ height: "100%" }} className="flex relative">
{!isSidebarCollapsed ? (
<Button
variant="ghost"
size="icon-sm"
onClick={() => setIsSidebarCollapsed(true)}
className="absolute top-2 left-2 z-20 bg-white! border! border-slate-200! rounded-md!"
aria-label="Collapse trace sidebar"
>
<ChevronLeft className="size-4" />
</Button>
) : (
<Button
variant="ghost"
size="icon-sm"
onClick={() => setIsSidebarCollapsed(false)}
className="absolute top-2 left-2 z-20 bg-white! border! border-slate-200! rounded-md!"
aria-label="Expand trace sidebar"
>
<ChevronRight className="size-4" />
</Button>
)}
{!isSidebarCollapsed && (
<div className="border-r border-slate-200 bg-slate-50 flex flex-col" style={{ width: SIDEBAR_WIDTH_PX }}>
<div className="pl-12 pr-3 py-2 border-b border-slate-200 bg-white">
<div className="flex items-start justify-between gap-2">
<div>
<div className="text-[10px] uppercase tracking-wide text-slate-500">
{isSessionMode ? "Session" : "Trace"}
</div>
<div className="font-mono text-[12px] text-slate-900 leading-tight flex items-center gap-1">
<span className="truncate">{leftPanelDisplayId}</span>
<button
type="button"
onClick={handleCopyLeftPanelId}
className="text-slate-400 hover:text-slate-600"
aria-label="Copy trace id"
>
{copiedLeftPanelId ? <Check className="size-3" /> : <Copy className="size-3" />}
</button>
</div>
</div>
</div>
</div>
<div className="mt-1 text-[11px] text-slate-500 font-mono">
{logsForList.length} req
{[
isSessionMode
? llmCount
: logsForList.filter(
(row) => !MCP_CALL_TYPES.includes(row.call_type) && !AGENT_CALL_TYPES.includes(row.call_type),
).length,
isSessionMode
? agentCount
: logsForList.filter((row) => AGENT_CALL_TYPES.includes(row.call_type)).length,
isSessionMode ? mcpCount : logsForList.filter((row) => MCP_CALL_TYPES.includes(row.call_type)).length,
].map((count, i) => {
const label = [" LLM", " Agent", " MCP"][i];
return count > 0 ? (
<span key={label}>
<div className="mt-1 text-[11px] text-slate-500 font-mono">
{logsForList.length} req
{[
isSessionMode
? llmCount
: logsForList.filter(
(row) => !MCP_CALL_TYPES.includes(row.call_type) && !AGENT_CALL_TYPES.includes(row.call_type),
).length,
isSessionMode
? agentCount
: logsForList.filter((row) => AGENT_CALL_TYPES.includes(row.call_type)).length,
isSessionMode
? mcpCount
: logsForList.filter((row) => MCP_CALL_TYPES.includes(row.call_type)).length,
].map((count, i) => {
const label = [" LLM", " Agent", " MCP"][i];
return count > 0 ? (
<span key={label}>
<span className="mx-1.5">·</span>
{count}
{label}
</span>
) : null;
})}
<span className="mx-1.5">·</span>
{isSessionMode ? getSpendString(totalSessionCost) : getSpendString(currentLog.spend || 0)}
{isSessionMode && (
<>
<span className="mx-1.5">·</span>
{count}
{label}
</span>
) : null;
})}
<span className="mx-1.5">·</span>
{isSessionMode ? getSpendString(totalSessionCost) : getSpendString(currentLog.spend || 0)}
{sessionDurationSeconds}s
</>
)}
</div>
{isSessionMode && sessionTruncated && (
<div className="mt-1 text-[11px] text-amber-600 font-mono">
Showing most recent {logsForList.length} of {sessionTotalCount}
</div>
)}
{isSessionMode && (
<>
<span className="mx-1.5">·</span>
{sessionDurationSeconds}s
</>
<Tabs
className="mt-1.5"
value={sessionSortMode}
onValueChange={(value) => setSessionSortMode(value as SessionLogSortMode)}
>
<TabsList className="w-full">
<TabsTrigger value="duration" className="text-[11px]">
Duration
</TabsTrigger>
<TabsTrigger value="start_time" className="text-[11px]">
Start time
</TabsTrigger>
</TabsList>
</Tabs>
)}
</div>
{isSessionMode && sessionTruncated && (
<div className="mt-1 text-[11px] text-amber-600 font-mono">
Showing most recent {logsForList.length} of {sessionTotalCount}
</div>
)}
{isSessionMode && (
<Segmented
block
size="small"
className="mt-1.5 [&_.ant-segmented-item-label]:text-[11px]"
options={[
{ label: "Duration", value: "duration" },
{ label: "Start time", value: "start_time" },
]}
value={sessionSortMode}
onChange={(value) => setSessionSortMode(value as SessionLogSortMode)}
/>
)}
</div>
<div className="flex-1 overflow-y-auto">
{normalizeGuardrailEntries(metadata?.guardrail_information).length > 0 && (
<div className="px-3 pt-2">
<GuardrailJumpLink guardrailEntries={normalizeGuardrailEntries(metadata?.guardrail_information)} />
</div>
)}
{isSessionMode ? (
<div className="py-1">
{/* Child events — vertical tree line with horizontal connectors */}
<div className="relative pl-2">
<div className="absolute left-4 top-1 bottom-1 border-l border-slate-300" />
{logsForList.map((row, idx) => {
const isLast = idx === logsForList.length - 1;
return (
<div key={row.request_id} className="relative">
<div className="absolute left-4 top-3 w-3 border-t border-slate-300" />
{isLast && <div className="absolute left-4 top-3 bottom-0 w-px bg-slate-50" />}
<TraceEventRow
row={row}
isSelected={row.request_id === currentLog.request_id}
onClick={() => {
setSelectedSessionRequestId(row.request_id);
onSelectLog?.(row);
}}
/>
</div>
);
})}
<div className="flex-1 overflow-y-auto">
{normalizeGuardrailEntries(metadata?.guardrail_information).length > 0 && (
<div className="px-3 pt-2">
<GuardrailJumpLink guardrailEntries={normalizeGuardrailEntries(metadata?.guardrail_information)} />
</div>
</div>
) : (
<div className="py-1">
{logsForList.map((row) => (
<TraceEventRow
key={row.request_id}
row={row}
isSelected={row.request_id === currentLog.request_id}
onClick={() => onSelectLog?.(row)}
/>
))}
</div>
)}
)}
{isSessionMode ? (
<div className="py-1">
{/* Child events — vertical tree line with horizontal connectors */}
<div className="relative pl-2">
<div className="absolute left-4 top-1 bottom-1 border-l border-slate-300" />
{logsForList.map((row, idx) => {
const isLast = idx === logsForList.length - 1;
return (
<div key={row.request_id} className="relative">
<div className="absolute left-4 top-3 w-3 border-t border-slate-300" />
{isLast && <div className="absolute left-4 top-3 bottom-0 w-px bg-slate-50" />}
<TraceEventRow
row={row}
isSelected={row.request_id === currentLog.request_id}
onClick={() => {
setSelectedSessionRequestId(row.request_id);
onSelectLog?.(row);
}}
/>
</div>
);
})}
</div>
</div>
) : (
<div className="py-1">
{logsForList.map((row) => (
<TraceEventRow
key={row.request_id}
row={row}
isSelected={row.request_id === currentLog.request_id}
onClick={() => onSelectLog?.(row)}
/>
))}
</div>
)}
</div>
</div>
</div>
)}
)}
<div className="flex-1 flex flex-col overflow-hidden">
<DrawerHeader
log={currentLog}
onClose={onClose}
onPrevious={selectPreviousLog}
onNext={selectNextLog}
statusLabel={statusLabel}
statusColor={statusColor}
environment={environment}
/>
<div className="flex-1 overflow-y-auto">
<LogDetailContent
logEntry={enrichedLog}
isLoadingDetails={isLoadingDetails}
accessToken={accessToken ?? null}
<div className="flex-1 flex flex-col overflow-hidden">
<DrawerHeader
log={currentLog}
onClose={onClose}
onPrevious={selectPreviousLog}
onNext={selectNextLog}
statusLabel={statusLabel}
statusColor={statusColor}
environment={environment}
/>
<div className="flex-1 overflow-y-auto">
<LogDetailContent
logEntry={enrichedLog}
isLoadingDetails={isLoadingDetails}
accessToken={accessToken ?? null}
/>
</div>
</div>
</div>
</div>
</Drawer>
</SheetContent>
</Sheet>
);
}

View file

@ -4,16 +4,6 @@ import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { RealtimePrettyView, isRealtimeResponse } from "./RealtimePrettyView";
vi.mock("antd", async () => {
const actual = await vi.importActual<typeof import("antd")>("antd");
return {
...actual,
message: {
success: vi.fn(),
},
};
});
const sampleRealtimeResponse = {
usage: {
total_tokens: 587,

View file

@ -5,19 +5,11 @@
*/
import { useState } from "react";
import { Typography, Tag, Tooltip } from "antd";
import {
SoundOutlined,
MessageOutlined,
SettingOutlined,
AudioOutlined,
DownOutlined,
UpOutlined,
} from "@ant-design/icons";
import { ChevronDown, ChevronUp, MessageSquare, Mic, Settings, Volume2 } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { SectionHeader } from "./SectionHeader";
const { Text } = Typography;
interface RealtimeEvent {
type: string;
event_id?: string;
@ -163,34 +155,34 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<div style={{ display: "flex", alignItems: "center" }}>
{isCollapsed ? (
<DownOutlined style={{ fontSize: 10, color: "#8c8c8c" }} />
<ChevronDown className="size-2.5 text-muted-foreground" />
) : (
<UpOutlined style={{ fontSize: 10, color: "#8c8c8c" }} />
<ChevronUp className="size-2.5 text-muted-foreground" />
)}
</div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<SettingOutlined style={{ color: "#8c8c8c", fontSize: 14 }} />
<Text style={{ fontWeight: 500, fontSize: 14 }}>Session</Text>
<Settings className="size-3.5 text-muted-foreground" />
<span style={{ fontWeight: 500, fontSize: 14 }}>Session</span>
</div>
<Text type="secondary" style={{ fontSize: 12 }}>
<span className="text-muted-foreground" style={{ fontSize: 12 }}>
{session.model}
</Text>
</span>
{turnCount > 0 && (
<Tag color="purple" style={{ margin: 0, fontWeight: 500 }}>
<Badge variant="secondary" style={{ margin: 0, fontWeight: 500 }}>
{turnCount} {turnCount === 1 ? "turn" : "turns"}
</Tag>
</Badge>
)}
{session.voice && (
<Tag color="blue" style={{ margin: 0 }}>
<SoundOutlined /> {session.voice}
</Tag>
<Badge variant="secondary" style={{ margin: 0 }}>
<Volume2 className="size-3" /> {session.voice}
</Badge>
)}
{session.modalities && (
<div style={{ display: "flex", gap: 4 }}>
{session.modalities.map((m) => (
<Tag key={m} style={{ margin: 0 }}>
{m === "audio" ? <AudioOutlined /> : <MessageOutlined />} {m}
</Tag>
<Badge key={m} variant="outline" style={{ margin: 0 }}>
{m === "audio" ? <Mic className="size-3" /> : <MessageSquare className="size-3" />} {m}
</Badge>
))}
</div>
)}
@ -228,8 +220,8 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
{session.instructions && (
<div style={{ marginTop: 12 }}>
<Text
type="secondary"
<span
className="text-muted-foreground"
style={{
fontSize: 10,
letterSpacing: "0.5px",
@ -239,7 +231,7 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
}}
>
Instructions
</Text>
</span>
<div
style={{
fontSize: 12,
@ -344,20 +336,25 @@ function ResponseTurn({ response, index }: { response: RealtimeResponse; index:
marginBottom: 8,
}}
>
<Tag color={response.status === "completed" ? "green" : "orange"} style={{ margin: 0 }}>
<Badge variant={response.status === "completed" ? "secondary" : "outline"} style={{ margin: 0 }}>
{response.status || "unknown"}
</Tag>
</Badge>
{usage && (
<Text type="secondary" style={{ fontSize: 11 }}>
<span className="text-muted-foreground" style={{ fontSize: 11 }}>
{usage.input_tokens ?? 0} in / {usage.output_tokens ?? 0} out tokens
</Text>
</span>
)}
{response.conversation_id && (
<Tooltip title={response.conversation_id}>
<Text type="secondary" style={{ fontSize: 11, cursor: "help" }}>
conv: {response.conversation_id.slice(0, 12)}...
</Text>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger
render={<span className="text-muted-foreground" style={{ fontSize: 11, cursor: "help" }} />}
>
conv: {response.conversation_id.slice(0, 12)}...
</TooltipTrigger>
<TooltipContent>{response.conversation_id}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
@ -381,8 +378,8 @@ function OutputMessage({ output }: { output: RealtimeOutputItem }) {
return (
<div style={{ marginBottom: 8 }}>
<Text
type="secondary"
<span
className="text-muted-foreground"
style={{
fontSize: 10,
letterSpacing: "0.5px",
@ -392,7 +389,7 @@ function OutputMessage({ output }: { output: RealtimeOutputItem }) {
}}
>
{output.role?.toUpperCase() || "ASSISTANT"}
</Text>
</span>
{contents.map((c, cIdx) => {
const text = c.transcript || c.text;
if (!text) return null;
@ -407,20 +404,18 @@ function OutputMessage({ output }: { output: RealtimeOutputItem }) {
}}
>
{c.type === "audio" && (
<AudioOutlined
<Mic
className="size-3 text-muted-foreground"
style={{
color: "#8c8c8c",
fontSize: 12,
marginTop: 3,
flexShrink: 0,
}}
/>
)}
{c.type === "text" && (
<MessageOutlined
<MessageSquare
className="size-3 text-muted-foreground"
style={{
color: "#8c8c8c",
fontSize: 12,
marginTop: 3,
flexShrink: 0,
}}
@ -453,9 +448,12 @@ function TokenBreakdown({ label, details }: { label: string; details: Record<str
return (
<div style={{ marginTop: 4 }}>
<Text type="secondary" style={{ fontSize: 10, letterSpacing: "0.5px", textTransform: "uppercase" }}>
<span
className="text-muted-foreground"
style={{ fontSize: 10, letterSpacing: "0.5px", textTransform: "uppercase" }}
>
{label} Token Breakdown
</Text>
</span>
<div
style={{
display: "flex",
@ -467,9 +465,9 @@ function TokenBreakdown({ label, details }: { label: string; details: Record<str
{entries.map(([key, value]) => {
if (typeof value === "number") {
return (
<Tag key={key} style={{ margin: 0 }}>
<Badge key={key} variant="outline" style={{ margin: 0 }}>
{formatTokenLabel(key)}: {value.toLocaleString()}
</Tag>
</Badge>
);
}
return null;
@ -483,9 +481,9 @@ function ConfigRow({ label, value }: { label: string; value: any }) {
if (value === undefined || value === null) return null;
return (
<div>
<Text type="secondary" style={{ fontSize: 11 }}>
<span className="text-muted-foreground" style={{ fontSize: 11 }}>
{label}
</Text>
</span>
<div style={{ fontSize: 13, color: "#262626" }}>{String(value)}</div>
</div>
);

View file

@ -2,11 +2,9 @@
* Formatted view of tool definition with parameters table and call data
*/
import { Typography, Table } from "antd";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { ParsedTool, ParameterRow } from "./types";
const { Text } = Typography;
interface FormattedToolViewProps {
tool: ParsedTool;
}
@ -23,57 +21,27 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) {
}),
);
const columns = [
{
title: "Parameter",
dataIndex: "name",
key: "name",
render: (name: string, record: ParameterRow) => (
<Text code>
{name}
{record.required && <Text type="danger">*</Text>}
</Text>
),
},
{
title: "Type",
dataIndex: "type",
key: "type",
render: (type: string) => (
<Text code style={{ color: "#1890ff" }}>
{type}
</Text>
),
},
{
title: "Description",
dataIndex: "description",
key: "description",
render: (desc: string) => <Text type="secondary">{desc}</Text>,
},
];
return (
<div>
{/* Description */}
{tool.description && (
<div style={{ marginBottom: 16 }}>
<Text
<span
style={{
lineHeight: 1.6,
whiteSpace: "pre-wrap",
}}
>
{tool.description}
</Text>
</span>
</div>
)}
{/* Parameters Table */}
{parameterRows.length > 0 && (
<div>
<Text
type="secondary"
<span
className="text-muted-foreground"
style={{
fontSize: 12,
display: "block",
@ -81,16 +49,42 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) {
}}
>
Parameters
</Text>
<Table dataSource={parameterRows} columns={columns} pagination={false} size="small" bordered />
</span>
<Table>
<TableHeader>
<TableRow>
<TableHead>Parameter</TableHead>
<TableHead>Type</TableHead>
<TableHead>Description</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{parameterRows.map((row) => (
<TableRow key={row.key}>
<TableCell>
<code>
{row.name}
{row.required && <span className="text-destructive">*</span>}
</code>
</TableCell>
<TableCell>
<code className="text-blue-600">{row.type}</code>
</TableCell>
<TableCell>
<span className="text-muted-foreground">{row.description}</span>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{/* If tool was called, show the arguments used */}
{tool.called && tool.callData && (
<div style={{ marginTop: 16 }}>
<Text
type="secondary"
<span
className="text-muted-foreground"
style={{
fontSize: 12,
display: "block",
@ -98,7 +92,7 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) {
}}
>
Called With
</Text>
</span>
<div
style={{
background: "#f6ffed",

View file

@ -3,13 +3,11 @@
*/
import { useState } from "react";
import { Typography, Radio } from "antd";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ParsedTool } from "./types";
import { FormattedToolView } from "./FormattedToolView";
import { JsonToolView } from "./JsonToolView";
const { Text } = Typography;
type ViewMode = "formatted" | "json";
interface ToolExpandedContentProps {
@ -29,13 +27,13 @@ export function ToolExpandedContent({ tool }: ToolExpandedContentProps) {
marginBottom: 12,
}}
>
<Text type="secondary" style={{ fontSize: 12 }}>
Description
</Text>
<Radio.Group size="small" value={viewMode} onChange={(e) => setViewMode(e.target.value)}>
<Radio.Button value="formatted">Formatted</Radio.Button>
<Radio.Button value="json">JSON</Radio.Button>
</Radio.Group>
<span className="text-xs text-muted-foreground">Description</span>
<Tabs value={viewMode} onValueChange={(value) => setViewMode(value as ViewMode)}>
<TabsList>
<TabsTrigger value="formatted">Formatted</TabsTrigger>
<TabsTrigger value="json">JSON</TabsTrigger>
</TabsList>
</Tabs>
</div>
{viewMode === "formatted" ? <FormattedToolView tool={tool} /> : <JsonToolView tool={tool} />}

View file

@ -3,13 +3,11 @@
*/
import { useState } from "react";
import { Typography, Tag } from "antd";
import { ToolOutlined, RightOutlined, DownOutlined } from "@ant-design/icons";
import { ChevronDown, ChevronRight, Wrench } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { ParsedTool } from "./types";
import { ToolExpandedContent } from "./ToolExpandedContent";
const { Text } = Typography;
interface ToolItemProps {
tool: ParsedTool;
}
@ -39,18 +37,18 @@ export function ToolItem({ tool }: ToolItemProps) {
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<ToolOutlined style={{ color: "#8c8c8c", fontSize: 14 }} />
<Text style={{ fontSize: 14 }}>
<Wrench className="size-3.5 text-muted-foreground" />
<span style={{ fontSize: 14 }}>
{tool.index}. {tool.name}
</Text>
</span>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Tag color={tool.called ? "blue" : "default"}>{tool.called ? "called" : "not called"}</Tag>
<Badge variant={tool.called ? "default" : "secondary"}>{tool.called ? "called" : "not called"}</Badge>
{expanded ? (
<DownOutlined style={{ fontSize: 12, color: "#8c8c8c" }} />
<ChevronDown className="size-3 text-muted-foreground" />
) : (
<RightOutlined style={{ fontSize: 12, color: "#8c8c8c" }} />
<ChevronRight className="size-3 text-muted-foreground" />
)}
</div>
</div>

View file

@ -1,5 +1,6 @@
import React, { useState } from "react";
import { Collapse } from "antd";
import { ChevronDown, ChevronRight } from "lucide-react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { getProviderLogoAndName } from "../provider_info_helpers";
interface VectorStoreContent {
@ -31,6 +32,7 @@ interface VectorStoreViewerProps {
}
export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
const [open, setOpen] = useState(true);
const [expandedResults, setExpandedResults] = useState<Record<string, boolean>>({});
if (!data || data.length === 0) {
@ -57,110 +59,110 @@ export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
return (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
<Collapse
defaultActiveKey={["1"]}
expandIconPosition="start"
items={[
{
key: "1",
label: <h3 className="text-lg font-medium text-gray-900">Vector Store Requests</h3>,
children: (
<div className="p-4">
{data.map((request, index) => (
<div key={index} className="mb-6 last:mb-0">
<div className="bg-white rounded-lg border p-4 mb-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex">
<span className="font-medium w-1/3">Query:</span>
<span className="font-mono">{request.query}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Vector Store ID:</span>
<span className="font-mono">{request.vector_store_id}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Provider:</span>
<span className="flex items-center">
{(() => {
const { logo, displayName } = getProviderLogoAndName(request.custom_llm_provider);
return (
<>
{logo && <img src={logo} alt={`${displayName} logo`} className="h-5 w-5 mr-2" />}
{displayName}
</>
);
})()}
<Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger className="flex w-full items-center gap-3 px-4 py-3 text-left">
{open ? (
<ChevronDown className="size-3.5 shrink-0 text-gray-500" />
) : (
<ChevronRight className="size-3.5 shrink-0 text-gray-500" />
)}
<h3 className="text-lg font-medium text-gray-900">Vector Store Requests</h3>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="p-4">
{data.map((request, index) => (
<div key={index} className="mb-6 last:mb-0">
<div className="bg-white rounded-lg border p-4 mb-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex">
<span className="font-medium w-1/3">Query:</span>
<span className="font-mono">{request.query}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Vector Store ID:</span>
<span className="font-mono">{request.vector_store_id}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Provider:</span>
<span className="flex items-center">
{(() => {
const { logo, displayName } = getProviderLogoAndName(request.custom_llm_provider);
return (
<>
{logo && <img src={logo} alt={`${displayName} logo`} className="h-5 w-5 mr-2" />}
{displayName}
</>
);
})()}
</span>
</div>
</div>
<div className="space-y-2">
<div className="flex">
<span className="font-medium w-1/3">Start Time:</span>
<span>{formatTime(request.start_time)}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">End Time:</span>
<span>{formatTime(request.end_time)}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Duration:</span>
<span>{calculateDuration(request.start_time, request.end_time)}</span>
</div>
</div>
</div>
</div>
<h4 className="font-medium mb-2">Search Results</h4>
<div className="space-y-2">
{request.vector_store_search_response.data.map((result, resultIndex) => {
const isExpanded = expandedResults[`${index}-${resultIndex}`] || false;
return (
<div key={resultIndex} className="border rounded-lg overflow-hidden">
<div
className="flex items-center p-3 bg-gray-50 cursor-pointer"
onClick={() => toggleResult(index, resultIndex)}
>
<svg
className={`w-5 h-5 mr-2 transition-transform ${isExpanded ? "transform rotate-90" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<div className="flex items-center">
<span className="font-medium mr-2">Result {resultIndex + 1}</span>
<span className="text-gray-500 text-sm">
Score: <span className="font-mono">{result.score.toFixed(4)}</span>
</span>
</div>
</div>
<div className="space-y-2">
<div className="flex">
<span className="font-medium w-1/3">Start Time:</span>
<span>{formatTime(request.start_time)}</span>
{isExpanded && (
<div className="p-3 border-t bg-white">
{result.content.map((content, contentIndex) => (
<div key={contentIndex} className="mb-2 last:mb-0">
<div className="text-xs text-gray-500 mb-1">{content.type}</div>
<pre className="text-xs font-mono whitespace-pre-wrap break-all bg-gray-50 p-2 rounded-sm">
{content.text}
</pre>
</div>
))}
</div>
<div className="flex">
<span className="font-medium w-1/3">End Time:</span>
<span>{formatTime(request.end_time)}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Duration:</span>
<span>{calculateDuration(request.start_time, request.end_time)}</span>
</div>
</div>
)}
</div>
</div>
<h4 className="font-medium mb-2">Search Results</h4>
<div className="space-y-2">
{request.vector_store_search_response.data.map((result, resultIndex) => {
const isExpanded = expandedResults[`${index}-${resultIndex}`] || false;
return (
<div key={resultIndex} className="border rounded-lg overflow-hidden">
<div
className="flex items-center p-3 bg-gray-50 cursor-pointer"
onClick={() => toggleResult(index, resultIndex)}
>
<svg
className={`w-5 h-5 mr-2 transition-transform ${isExpanded ? "transform rotate-90" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<div className="flex items-center">
<span className="font-medium mr-2">Result {resultIndex + 1}</span>
<span className="text-gray-500 text-sm">
Score: <span className="font-mono">{result.score.toFixed(4)}</span>
</span>
</div>
</div>
{isExpanded && (
<div className="p-3 border-t bg-white">
{result.content.map((content, contentIndex) => (
<div key={contentIndex} className="mb-2 last:mb-0">
<div className="text-xs text-gray-500 mb-1">{content.type}</div>
<pre className="text-xs font-mono whitespace-pre-wrap break-all bg-gray-50 p-2 rounded-sm">
{content.text}
</pre>
</div>
))}
</div>
)}
</div>
);
})}
</div>
</div>
))}
);
})}
</div>
</div>
),
},
]}
/>
))}
</div>
</CollapsibleContent>
</Collapsible>
</div>
);
}