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 "count": 1
} }
}, },
"src/components/view_logs/CostBreakdownViewer.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/view_logs/EvalViewer/EvalViewer.tsx": { "src/components/view_logs/EvalViewer/EvalViewer.tsx": {
"local/no-complex-jsx-arrow": {
"count": 1
},
"no-nested-ternary": { "no-nested-ternary": {
"count": 1 "count": 1
},
"no-restricted-imports": {
"count": 1
} }
}, },
"src/components/view_logs/GuardrailViewer/CompliancePanel.tsx": { "src/components/view_logs/GuardrailViewer/CompliancePanel.tsx": {
"no-nested-ternary": { "no-nested-ternary": {
"count": 2 "count": 2
}, },
"no-restricted-imports": {
"count": 1
},
"react-hooks/set-state-in-effect": { "react-hooks/set-state-in-effect": {
"count": 1 "count": 1
} }
@ -3541,31 +3527,17 @@
"src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx": { "src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx": {
"no-nested-ternary": { "no-nested-ternary": {
"count": 4 "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": { "src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx": {
"no-nested-ternary": { "no-nested-ternary": {
"count": 3 "count": 3
},
"no-restricted-imports": {
"count": 1
} }
}, },
"src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx": { "src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx": {
"no-nested-ternary": { "no-nested-ternary": {
"count": 2 "count": 2
}, },
"no-restricted-imports": {
"count": 1
},
"react-hooks/set-state-in-effect": { "react-hooks/set-state-in-effect": {
"count": 2 "count": 2
} }
@ -3575,36 +3547,11 @@
"count": 2 "count": 2
} }
}, },
"src/components/view_logs/LogDetailsDrawer/RealtimePrettyView.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/view_logs/LogDetailsDrawer/useKeyboardNavigation.ts": { "src/components/view_logs/LogDetailsDrawer/useKeyboardNavigation.ts": {
"react-hooks/immutability": { "react-hooks/immutability": {
"count": 2 "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": { "src/components/view_logs/columns.tsx": {
"local/filename-pascal-case": { "local/filename-pascal-case": {
"count": 1 "count": 1

View file

@ -1,5 +1,6 @@
import React from "react"; import React, { useState } from "react";
import { Collapse } from "antd"; import { ChevronDown, ChevronRight } from "lucide-react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { formatNumberWithCommas } from "@/utils/dataUtils"; import { formatNumberWithCommas } from "@/utils/dataUtils";
export interface CostBreakdown { export interface CostBreakdown {
@ -49,6 +50,7 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
cacheReadTokens, cacheReadTokens,
cacheCreationTokens, cacheCreationTokens,
}) => { }) => {
const [open, setOpen] = useState(false);
const isCached = cacheHit?.toLowerCase() === "true"; const isCached = cacheHit?.toLowerCase() === "true";
const hasTokenCounts = promptTokens !== undefined || completionTokens !== undefined; const hasTokenCounts = promptTokens !== undefined || completionTokens !== undefined;
@ -90,197 +92,195 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
return ( return (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6"> <div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
<Collapse <Collapsible open={open} onOpenChange={setOpen}>
expandIconPosition="start" <CollapsibleTrigger className="flex w-full items-center gap-3 px-4 py-3 text-left">
items={[ {open ? (
{ <ChevronDown className="size-3.5 shrink-0 text-gray-500" />
key: "1", ) : (
label: ( <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 justify-between w-full">
<div className="flex items-center space-x-2 mr-4"> <h3 className="text-lg font-medium text-gray-900">Cost Breakdown</h3>
<span className="text-sm text-gray-500">Total:</span> <div className="flex items-center space-x-2 mr-4">
<span className="text-sm font-semibold text-gray-900"> <span className="text-sm text-gray-500">Total:</span>
{formatCost(totalSpend)} <span className="text-sm font-semibold text-gray-900">
{isCached && " (Cached)"} {formatCost(totalSpend)}
</span> {isCached && " (Cached)"}
</div> </span>
</div> </div>
), </div>
children: ( </CollapsibleTrigger>
<div className="p-6 space-y-4"> <CollapsibleContent>
{/* Step 1: Base Token Costs */} <div className="p-6 space-y-4">
<div className="space-y-2 max-w-2xl"> {/* 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; const hasCacheBreakdown =
if (hasCacheBreakdown) { costBreakdown?.cache_read_cost !== undefined || costBreakdown?.cache_creation_cost !== undefined;
// Separate line items: Input / Cache Read / Cache Write if (hasCacheBreakdown) {
const rawCost = isCached // Separate line items: Input / Cache Read / Cache Write
? 0 const rawCost = isCached
: (inputCost ?? 0) - ? 0
(costBreakdown?.cache_read_cost ?? 0) - : (inputCost ?? 0) -
(costBreakdown?.cache_creation_cost ?? 0); (costBreakdown?.cache_read_cost ?? 0) -
return ( (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 (
<div className="flex text-sm"> <div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span> <span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
<span className="text-gray-900"> <span className="text-gray-900">
{formatCost(inputCost)} {formatCost(rawCost)}
{promptTokens !== undefined && ( {rawInputTokens !== undefined && rawInputTokens !== null && (
<span className="text-gray-500 font-normal ml-1"> <span className="text-gray-500 font-normal ml-1">
({promptTokens.toLocaleString()} prompt tokens) ({rawInputTokens.toLocaleString()} tokens)
</span> </span>
)} )}
</span> </span>
</div> </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"> <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"> <span className="text-gray-900">
{formatCost(outputCost)} {formatCost(inputCost)}
{completionTokens !== undefined && ( {promptTokens !== undefined && (
<span className="text-gray-500 font-normal ml-1"> <span className="text-gray-500 font-normal ml-1">
({completionTokens.toLocaleString()} completion tokens) ({promptTokens.toLocaleString()} prompt tokens)
</span> </span>
)} )}
</span> </span>
</div> </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> <div className="flex text-sm">
<span className="text-gray-900">{formatCost(costBreakdown.tool_usage_cost)}</span> <span className="text-gray-600 font-medium w-1/3">Output Cost:</span>
</div> <span className="text-gray-900">
)} {formatCost(outputCost)}
{costBreakdown?.additional_costs && {completionTokens !== undefined && (
Object.entries(costBreakdown.additional_costs) <span className="text-gray-500 font-normal ml-1">
.filter(([, value]) => value != null && value !== 0) ({completionTokens.toLocaleString()} completion tokens)
.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> </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>
</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> </div>
); );
}; };

View file

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

View file

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

View file

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

View file

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

View file

@ -177,12 +177,19 @@ describe("LogDetailContent", () => {
expect(screen.getByText("Loading request & response data...")).toBeInTheDocument(); 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()} />); render(<LogDetailContent logEntry={createLogEntry()} />);
expect(screen.getByText("Request & Response")).toBeInTheDocument(); expect(screen.getByText("Request & Response")).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "Pretty" })).toBeInTheDocument(); expect(screen.getByText("Pretty")).toBeInTheDocument();
expect(screen.getByRole("radio", { name: "JSON" })).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 () => { 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" })} />); render(<LogDetailContent logEntry={createLogEntry({ cache_hit: "True" })} />);
expect(screen.getByText("Response Cache")).toBeInTheDocument(); 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", () => { 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("34,462")).toBeInTheDocument();
expect(screen.getByText("Prompt Cache Creation Tokens")).toBeInTheDocument(); expect(screen.getByText("Prompt Cache Creation Tokens")).toBeInTheDocument();
expect(screen.getByText("83")).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(); expect(screen.queryByText("Cache Hit")).not.toBeInTheDocument();
}); });
@ -310,8 +317,12 @@ describe("LogDetailContent", () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<LogDetailContent logEntry={createLogEntry({ cache_hit: "True" })} />); render(<LogDetailContent logEntry={createLogEntry({ cache_hit: "True" })} />);
const label = screen.getByText("Response Cache").closest(".ant-space") as HTMLElement; expect(screen.getByText("Response Cache")).toBeInTheDocument();
await user.hover(within(label).getByRole("img", { name: "info-circle" })); // 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( expect(await screen.findByRole("link", { name: "Docs" })).toHaveAttribute(
"href", "href",
@ -333,8 +344,11 @@ describe("LogDetailContent", () => {
/>, />,
); );
const label = screen.getByText("Prompt Cache Read Tokens").closest(".ant-space") as HTMLElement; expect(screen.getByText("Prompt Cache Read Tokens")).toBeInTheDocument();
await user.hover(within(label).getByRole("img", { name: "info-circle" })); // 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( expect(await screen.findByRole("link", { name: "Docs" })).toHaveAttribute(
"href", "href",
@ -370,7 +384,7 @@ describe("LogDetailContent", () => {
expect(screen.queryByText("LiteLLM Overhead")).not.toBeInTheDocument(); 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", () => { it("should display attempted_retries / max_retries for Retries when attempted_retries > 0", () => {
render( render(
@ -386,7 +400,7 @@ describe("LogDetailContent", () => {
render(<LogDetailContent logEntry={createLogEntry({ metadata: { status: "success", attempted_retries: 0 } })} />); render(<LogDetailContent logEntry={createLogEntry({ metadata: { status: "success", attempted_retries: 0 } })} />);
const noneTag = within(retriesItem()).getByText("None"); 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", () => { 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(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 { useState } from "react";
import { Typography, Descriptions, Card, Tag, Tabs, Alert, Collapse, Radio, Space, Spin, Tooltip } from "antd"; import { Check, ChevronDown, ChevronRight, CircleAlert, Copy, Info } from "lucide-react";
import { InfoCircleOutlined } from "@ant-design/icons";
import moment from "moment"; 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 { LogEntry } from "../columns";
import { formatNumberWithCommas } from "@/utils/dataUtils"; import { formatNumberWithCommas } from "@/utils/dataUtils";
import { PROMPT_CACHE_CREATION_TOOLTIP, PROMPT_CACHE_READ_TOOLTIP } from "@/utils/promptCacheUsage"; import { PROMPT_CACHE_CREATION_TOOLTIP, PROMPT_CACHE_READ_TOOLTIP } from "@/utils/promptCacheUsage";
@ -32,13 +38,10 @@ import {
FONT_SIZE_SMALL, FONT_SIZE_SMALL,
FONT_FAMILY_MONO, FONT_FAMILY_MONO,
SPACING_XLARGE, SPACING_XLARGE,
SPACING_MEDIUM,
} from "./constants"; } from "./constants";
import { ToolsSection } from "../ToolsSection"; import { ToolsSection } from "../ToolsSection";
import { PrettyMessagesView } from "./PrettyMessagesView"; import { PrettyMessagesView } from "./PrettyMessagesView";
const { Text } = Typography;
export interface LogDetailContentProps { export interface LogDetailContentProps {
logEntry: LogEntry; logEntry: LogEntry;
/** When true, log details (messages/response) are still being lazy-loaded. */ /** 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` }}> <div style={{ padding: `${DRAWER_CONTENT_PADDING} ${DRAWER_CONTENT_PADDING} 0` }}>
{/* Error Alert */} {/* Error Alert */}
{hasError && errorInfo && ( {hasError && errorInfo && (
<Alert <div
type="error" role="alert"
showIcon className="mb-6 flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm"
message="Request Failed" >
description={<ErrorDescription errorInfo={errorInfo} />} <CircleAlert className="size-4 shrink-0 text-destructive" />
className="mb-6" <div>
/> <div className="font-medium text-destructive">Request Failed</div>
<ErrorDescription errorInfo={errorInfo} />
</div>
</div>
)} )}
{/* Tags */} {/* Tags */}
@ -116,26 +122,31 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
{/* Request Details */} {/* Request Details */}
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6"> <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 }}> <Card size="sm" style={{ marginBottom: 0 }}>
<Descriptions column={2} size="small"> <CardHeader>
<Descriptions.Item label="Model">{logEntry.model}</Descriptions.Item> <CardTitle>Request Details</CardTitle>
<Descriptions.Item label="Provider">{logEntry.custom_llm_provider || "-"}</Descriptions.Item> </CardHeader>
<Descriptions.Item label="Call Type">{logEntry.call_type}</Descriptions.Item> <CardContent>
<Descriptions.Item label="Model ID"> <DescriptionList>
<TruncatedValue value={logEntry.model_id} /> <DescriptionItem label="Model">{logEntry.model}</DescriptionItem>
</Descriptions.Item> <DescriptionItem label="Provider">{logEntry.custom_llm_provider || "-"}</DescriptionItem>
<Descriptions.Item label="API Base"> <DescriptionItem label="Call Type">{logEntry.call_type}</DescriptionItem>
<TruncatedValue value={logEntry.api_base} maxWidth={API_BASE_MAX_WIDTH} /> <DescriptionItem label="Model ID">
</Descriptions.Item> <TruncatedValue value={logEntry.model_id} />
{logEntry.requester_ip_address && ( </DescriptionItem>
<Descriptions.Item label="IP Address">{logEntry.requester_ip_address}</Descriptions.Item> <DescriptionItem label="API Base">
)} <TruncatedValue value={logEntry.api_base} maxWidth={API_BASE_MAX_WIDTH} />
{hasGuardrailData && ( </DescriptionItem>
<Descriptions.Item label="Guardrail"> {logEntry.requester_ip_address && (
<GuardrailLabel label={primaryGuardrailLabel} maskedCount={totalMaskedEntities} /> <DescriptionItem label="IP Address">{logEntry.requester_ip_address}</DescriptionItem>
</Descriptions.Item> )}
)} {hasGuardrailData && (
</Descriptions> <DescriptionItem label="Guardrail">
<GuardrailLabel label={primaryGuardrailLabel} maskedCount={totalMaskedEntities} />
</DescriptionItem>
)}
</DescriptionList>
</CardContent>
</Card> </Card>
</div> </div>
@ -170,7 +181,7 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
{/* Request/Response JSON */} {/* Request/Response JSON */}
{isLoadingDetails ? ( {isLoadingDetails ? (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6 p-8 text-center"> <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 style={{ marginTop: 8, color: "#999" }}>Loading request &amp; response data...</div>
</div> </div>
) : ( ) : (
@ -221,17 +232,64 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
// Helper Components // 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 }) { function ErrorDescription({ errorInfo }: { errorInfo: any }) {
return ( return (
<div> <div>
{errorInfo.error_code && ( {errorInfo.error_code && (
<div> <div>
<Text strong>Error Code:</Text> {errorInfo.error_code} <span className="font-semibold">Error Code:</span> {errorInfo.error_code}
</div> </div>
)} )}
{errorInfo.error_message && ( {errorInfo.error_message && (
<div> <div>
<Text strong>Message:</Text> {errorInfo.error_message} <span className="font-semibold">Message:</span> {errorInfo.error_message}
</div> </div>
)} )}
</div> </div>
@ -241,16 +299,16 @@ function ErrorDescription({ errorInfo }: { errorInfo: any }) {
function TagsSection({ tags }: { tags: Record<string, any> }) { function TagsSection({ tags }: { tags: Record<string, any> }) {
return ( return (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden p-4 mb-6"> <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 Tags
</Text> </span>
<Space size={SPACING_MEDIUM} wrap> <div className="flex flex-wrap items-center gap-2">
{Object.entries(tags).map(([key, value]) => ( {Object.entries(tags).map(([key, value]) => (
<Tag key={key}> <Badge key={key} variant="outline">
{key}: {String(value)} {key}: {String(value)}
</Tag> </Badge>
))} ))}
</Space> </div>
</div> </div>
); );
} }
@ -262,12 +320,12 @@ function GuardrailLabel({ label, maskedCount }: { label: string; maskedCount: nu
}; };
return ( return (
<Space size={SPACING_MEDIUM}> <span className="inline-flex items-center gap-2">
<a onClick={handleClick} style={{ cursor: "pointer" }}> <a onClick={handleClick} style={{ cursor: "pointer" }}>
{label} {label}
</a> </a>
{maskedCount > 0 && <Tag color="blue">{maskedCount} masked</Tag>} {maskedCount > 0 && <Badge variant="secondary">{maskedCount} masked</Badge>}
</Space> </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 }) { function MetricLabel({ label, tooltip, docsUrl }: { label: string; tooltip: string; docsUrl: string }) {
return ( return (
<Space size={4}> <span className="inline-flex items-center gap-1">
{label} {label}
<Tooltip <TooltipProvider>
title={ <Tooltip>
<> <TooltipTrigger
render={<span role="img" aria-label={`${label} info`} className="inline-flex text-muted-foreground" />}
>
<Info className="size-3.5" />
</TooltipTrigger>
<TooltipContent>
{tooltip}{" "} {tooltip}{" "}
<a <a href={docsUrl} target="_blank" rel="noreferrer" className="underline">
href={docsUrl}
target="_blank"
rel="noreferrer"
style={{ color: "#91caff", textDecoration: "underline" }}
>
Docs Docs
</a> </a>
</> </TooltipContent>
} </Tooltip>
> </TooltipProvider>
<InfoCircleOutlined style={{ color: "#8c8c8c" }} /> </span>
</Tooltip>
</Space>
); );
} }
@ -333,102 +389,111 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
return ( return (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6"> <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 }}> <Card size="sm" style={{ marginBottom: 0 }}>
<Descriptions column={2} size="small"> <CardHeader>
{showAnthropicMessagesInputOutput ? ( <CardTitle>Metrics</CardTitle>
<> </CardHeader>
<Descriptions.Item label="Input Tokens">{formatNumberWithCommas(uncachedInputTokens)}</Descriptions.Item> <CardContent>
<Descriptions.Item label="Output Tokens"> <DescriptionList>
{formatNumberWithCommas(logEntry.completion_tokens)} {showAnthropicMessagesInputOutput ? (
</Descriptions.Item> <>
</> <DescriptionItem label="Input Tokens">{formatNumberWithCommas(uncachedInputTokens)}</DescriptionItem>
) : ( <DescriptionItem label="Output Tokens">
<Descriptions.Item label="Tokens"> {formatNumberWithCommas(logEntry.completion_tokens)}
<TokenFlow </DescriptionItem>
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>
)
) : ( ) : (
"-" <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"> {showResponseCache && (
{moment(logEntry.startTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")} <DescriptionItem
</Descriptions.Item> label={
<Descriptions.Item label="End Time"> <MetricLabel
{moment(logEntry.endTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")} label="Response Cache"
</Descriptions.Item> tooltip={RESPONSE_CACHE_TOOLTIP}
</Descriptions> 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> </Card>
</div> </div>
); );
@ -449,6 +514,7 @@ function RequestResponseSection({
getFormattedResponse, getFormattedResponse,
logEntry, logEntry,
}: RequestResponseSectionProps) { }: RequestResponseSectionProps) {
const [open, setOpen] = useState(true);
const [activeTab, setActiveTab] = useState<typeof TAB_REQUEST | typeof TAB_RESPONSE>(TAB_REQUEST); const [activeTab, setActiveTab] = useState<typeof TAB_REQUEST | typeof TAB_RESPONSE>(TAB_REQUEST);
const [viewMode, setViewMode] = useState<"pretty" | "json">("pretty"); const [viewMode, setViewMode] = useState<"pretty" | "json">("pretty");
@ -476,90 +542,76 @@ function RequestResponseSection({
return ( return (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6"> <div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
<Collapse <Collapsible open={open} onOpenChange={setOpen}>
defaultActiveKey={["1"]} <Tabs value={viewMode} onValueChange={(value) => setViewMode(value as "pretty" | "json")}>
expandIconPosition="start" <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }}>
items={[ <CollapsibleTrigger className="flex flex-1 items-center gap-3 px-4 py-3 text-left">
{ {open ? (
key: "1", <ChevronDown className="size-3.5 shrink-0 text-gray-500" />
label: ( ) : (
<div <ChevronRight className="size-3.5 shrink-0 text-gray-500" />
style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }} )}
onClick={(e) => { <h3 className="text-lg font-medium text-gray-900" style={{ margin: 0 }}>
const target = e.target as HTMLElement; Request & Response
if (target.closest(".ant-radio-group")) { </h3>
e.stopPropagation(); </CollapsibleTrigger>
} <TabsList className="mr-4">
}} <TabsTrigger value="pretty">Pretty</TabsTrigger>
> <TabsTrigger value="json">JSON</TabsTrigger>
<h3 className="text-lg font-medium text-gray-900" style={{ margin: 0 }}> </TabsList>
Request & Response </div>
</h3> <CollapsibleContent>
<Radio.Group size="small" value={viewMode} onChange={(e) => setViewMode(e.target.value)}> <div>
<Radio.Button value="pretty">Pretty</Radio.Button> <TabsContent value="pretty">
<Radio.Button value="json">JSON</Radio.Button> <PrettyMessagesView
</Radio.Group> request={getRawRequest()}
</div> response={getFormattedResponse()}
), metrics={{
children: ( prompt_tokens: promptTokens,
<div> completion_tokens: completionTokens,
{viewMode === "pretty" ? ( input_cost: inputCost,
<PrettyMessagesView output_cost: outputCost,
request={getRawRequest()} }}
response={getFormattedResponse()} />
metrics={{ </TabsContent>
prompt_tokens: promptTokens, <TabsContent value="json">
completion_tokens: completionTokens, <Tabs
input_cost: inputCost, value={activeTab}
output_cost: outputCost, onValueChange={(key) => setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)}
}} >
/> <div className="flex items-center justify-between">
) : ( <TabsList>
<Tabs <TabsTrigger value={TAB_REQUEST}>Request</TabsTrigger>
activeKey={activeTab} <TabsTrigger value={TAB_RESPONSE}>Response</TabsTrigger>
onChange={(key) => setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)} </TabsList>
tabBarExtraContent={ <CopyButton
<Text getText={getCopyText}
copyable={{ label="Copy JSON"
text: getCopyText(), disabled={activeTab === TAB_RESPONSE && !hasResponse && !hasError}
tooltips: ["Copy JSON", "Copied!"], />
}} </div>
disabled={activeTab === TAB_RESPONSE && !hasResponse && !hasError} <TabsContent value={TAB_REQUEST}>
/> <div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
} <JsonViewer data={getRawRequest()} mode="formatted" />
items={[ </div>
{ </TabsContent>
key: TAB_REQUEST, <TabsContent value={TAB_RESPONSE}>
label: "Request", <div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
children: ( {hasResponse || hasError ? (
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}> <JsonViewer data={getFormattedResponse()} mode="formatted" />
<JsonViewer data={getRawRequest()} mode="formatted" /> ) : (
</div> <div style={{ textAlign: "center", padding: 20, color: "#999", fontStyle: "italic" }}>
), Response data not available
}, </div>
{ )}
key: TAB_RESPONSE, </div>
label: "Response", </TabsContent>
children: ( </Tabs>
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}> </TabsContent>
{hasResponse || hasError ? ( </div>
<JsonViewer data={getFormattedResponse()} mode="formatted" /> </CollapsibleContent>
) : ( </Tabs>
<div style={{ textAlign: "center", padding: 20, color: "#999", fontStyle: "italic" }}> </Collapsible>
Response data not available
</div>
)}
</div>
),
},
]}
/>
)}
</div>
),
},
]}
/>
</div> </div>
); );
} }
@ -602,43 +654,40 @@ export function GuardrailJumpLink({ guardrailEntries }: { guardrailEntries: any[
} }
function MetadataSection({ metadata }: { metadata: Record<string, any> }) { function MetadataSection({ metadata }: { metadata: Record<string, any> }) {
const [open, setOpen] = useState(true);
return ( return (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6"> <div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
<Collapse <Collapsible open={open} onOpenChange={setOpen}>
defaultActiveKey={["1"]} <CollapsibleTrigger className="flex w-full items-center gap-3 px-4 py-3 text-left">
expandIconPosition="start" {open ? (
items={[ <ChevronDown className="size-3.5 shrink-0 text-gray-500" />
{ ) : (
key: "1", <ChevronRight className="size-3.5 shrink-0 text-gray-500" />
label: <h3 className="text-lg font-medium text-gray-900">Metadata</h3>, )}
children: ( <h3 className="text-lg font-medium text-gray-900">Metadata</h3>
<div> </CollapsibleTrigger>
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 8 }}> <CollapsibleContent>
<Text <div>
copyable={{ <div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 8 }}>
text: JSON.stringify(metadata, null, 2), <CopyButton getText={() => JSON.stringify(metadata, null, 2)} label="Copy Metadata" />
tooltips: ["Copy Metadata", "Copied!"], </div>
}} <pre
/> style={{
</div> maxHeight: METADATA_MAX_HEIGHT,
<pre overflowY: "auto",
style={{ fontSize: FONT_SIZE_SMALL,
maxHeight: METADATA_MAX_HEIGHT, fontFamily: FONT_FAMILY_MONO,
overflowY: "auto", whiteSpace: "pre-wrap",
fontSize: FONT_SIZE_SMALL, wordBreak: "break-all",
fontFamily: FONT_FAMILY_MONO, margin: 0,
whiteSpace: "pre-wrap", }}
wordBreak: "break-all", >
margin: 0, {JSON.stringify(metadata, null, 2)}
}} </pre>
> </div>
{JSON.stringify(metadata, null, 2)} </CollapsibleContent>
</pre> </Collapsible>
</div>
),
},
]}
/>
</div> </div>
); );
} }

View file

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

View file

@ -4,16 +4,6 @@ import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
import { RealtimePrettyView, isRealtimeResponse } from "./RealtimePrettyView"; 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 = { const sampleRealtimeResponse = {
usage: { usage: {
total_tokens: 587, total_tokens: 587,

View file

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

View file

@ -2,11 +2,9 @@
* Formatted view of tool definition with parameters table and call data * 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"; import { ParsedTool, ParameterRow } from "./types";
const { Text } = Typography;
interface FormattedToolViewProps { interface FormattedToolViewProps {
tool: ParsedTool; 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 ( return (
<div> <div>
{/* Description */} {/* Description */}
{tool.description && ( {tool.description && (
<div style={{ marginBottom: 16 }}> <div style={{ marginBottom: 16 }}>
<Text <span
style={{ style={{
lineHeight: 1.6, lineHeight: 1.6,
whiteSpace: "pre-wrap", whiteSpace: "pre-wrap",
}} }}
> >
{tool.description} {tool.description}
</Text> </span>
</div> </div>
)} )}
{/* Parameters Table */} {/* Parameters Table */}
{parameterRows.length > 0 && ( {parameterRows.length > 0 && (
<div> <div>
<Text <span
type="secondary" className="text-muted-foreground"
style={{ style={{
fontSize: 12, fontSize: 12,
display: "block", display: "block",
@ -81,16 +49,42 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) {
}} }}
> >
Parameters Parameters
</Text> </span>
<Table dataSource={parameterRows} columns={columns} pagination={false} size="small" bordered /> <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> </div>
)} )}
{/* If tool was called, show the arguments used */} {/* If tool was called, show the arguments used */}
{tool.called && tool.callData && ( {tool.called && tool.callData && (
<div style={{ marginTop: 16 }}> <div style={{ marginTop: 16 }}>
<Text <span
type="secondary" className="text-muted-foreground"
style={{ style={{
fontSize: 12, fontSize: 12,
display: "block", display: "block",
@ -98,7 +92,7 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) {
}} }}
> >
Called With Called With
</Text> </span>
<div <div
style={{ style={{
background: "#f6ffed", background: "#f6ffed",

View file

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

View file

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

View file

@ -1,5 +1,6 @@
import React, { useState } from "react"; 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"; import { getProviderLogoAndName } from "../provider_info_helpers";
interface VectorStoreContent { interface VectorStoreContent {
@ -31,6 +32,7 @@ interface VectorStoreViewerProps {
} }
export function VectorStoreViewer({ data }: VectorStoreViewerProps) { export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
const [open, setOpen] = useState(true);
const [expandedResults, setExpandedResults] = useState<Record<string, boolean>>({}); const [expandedResults, setExpandedResults] = useState<Record<string, boolean>>({});
if (!data || data.length === 0) { if (!data || data.length === 0) {
@ -57,110 +59,110 @@ export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
return ( return (
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6"> <div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
<Collapse <Collapsible open={open} onOpenChange={setOpen}>
defaultActiveKey={["1"]} <CollapsibleTrigger className="flex w-full items-center gap-3 px-4 py-3 text-left">
expandIconPosition="start" {open ? (
items={[ <ChevronDown className="size-3.5 shrink-0 text-gray-500" />
{ ) : (
key: "1", <ChevronRight className="size-3.5 shrink-0 text-gray-500" />
label: <h3 className="text-lg font-medium text-gray-900">Vector Store Requests</h3>, )}
children: ( <h3 className="text-lg font-medium text-gray-900">Vector Store Requests</h3>
<div className="p-4"> </CollapsibleTrigger>
{data.map((request, index) => ( <CollapsibleContent>
<div key={index} className="mb-6 last:mb-0"> <div className="p-4">
<div className="bg-white rounded-lg border p-4 mb-4"> {data.map((request, index) => (
<div className="grid grid-cols-2 gap-4"> <div key={index} className="mb-6 last:mb-0">
<div className="space-y-2"> <div className="bg-white rounded-lg border p-4 mb-4">
<div className="flex"> <div className="grid grid-cols-2 gap-4">
<span className="font-medium w-1/3">Query:</span> <div className="space-y-2">
<span className="font-mono">{request.query}</span> <div className="flex">
</div> <span className="font-medium w-1/3">Query:</span>
<div className="flex"> <span className="font-mono">{request.query}</span>
<span className="font-medium w-1/3">Vector Store ID:</span> </div>
<span className="font-mono">{request.vector_store_id}</span> <div className="flex">
</div> <span className="font-medium w-1/3">Vector Store ID:</span>
<div className="flex"> <span className="font-mono">{request.vector_store_id}</span>
<span className="font-medium w-1/3">Provider:</span> </div>
<span className="flex items-center"> <div className="flex">
{(() => { <span className="font-medium w-1/3">Provider:</span>
const { logo, displayName } = getProviderLogoAndName(request.custom_llm_provider); <span className="flex items-center">
return ( {(() => {
<> const { logo, displayName } = getProviderLogoAndName(request.custom_llm_provider);
{logo && <img src={logo} alt={`${displayName} logo`} className="h-5 w-5 mr-2" />} return (
{displayName} <>
</> {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> </span>
</div> </div>
</div> </div>
<div className="space-y-2">
<div className="flex"> {isExpanded && (
<span className="font-medium w-1/3">Start Time:</span> <div className="p-3 border-t bg-white">
<span>{formatTime(request.start_time)}</span> {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 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>
</div> );
})}
<h4 className="font-medium mb-2">Search Results</h4> </div>
<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> </div>
); );
} }