feat(ui): migrate view_logs drawer/detail chrome to shadcn

Migrates the drawer/detail-chrome side of the view_logs subtree (chrome
only; Monaco, JSON viewers, and other payload renderers preserved):

- columns.tsx: antd Tooltip -> shadcn Tooltip via local HoverTooltip
  helper; Tremor Button -> shadcn Button; Tremor Badge -> shadcn Badge.
  ColumnDef<T>[] structure preserved.
- CostBreakdownViewer.tsx: antd Collapse -> shadcn Accordion; text-gray-*
  -> text-muted-foreground semantic tokens.
- GuardrailViewer/GuardrailViewer.tsx: antd Tooltip -> shadcn Tooltip;
  text-gray-* -> text-muted-foreground. Status palettes (green/red/amber
  for pass/fail/warn) kept raw with eslint override.
- LogDetailsDrawer/LogDetailContent.tsx: antd Typography/Descriptions/
  Card/Tag/Tabs/Alert/Collapse/Radio/Space/Spin -> shadcn Alert/Badge/
  Card/Accordion/Tabs/RadioGroup. JSON viewer (JsonViewer) and
  PrettyMessagesView untouched.
- LogDetailsDrawer/LogDetailsDrawer.tsx: antd Drawer -> shadcn Sheet
  (DRAWER_WIDTH preserved as inline width); antd Button -> shadcn Button.
- LogDetailsDrawer/RealtimePrettyView.tsx: antd Typography.Text/Tag/
  Tooltip -> shadcn Badge/Tooltip + local SecondaryText helper.

Adds the migrated files to the .eslintrc.json override list where they
keep categorical status palettes (blue/green/red/amber for
success/failure/cached/warning states).

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-24 10:33:18 +00:00
parent 7546589e82
commit d3708b967f
No known key found for this signature in database
7 changed files with 639 additions and 543 deletions

View file

@ -95,6 +95,11 @@
"src/components/Navbar/UserDropdown/UserDropdown.tsx",
"src/components/playground/chat_ui/ChatMessageBubble.tsx",
"src/components/view_logs/AuditLogDrawer/AuditLogDrawer.tsx",
"src/components/view_logs/columns.tsx",
"src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx",
"src/components/view_logs/LogDetailsDrawer/RealtimePrettyView.tsx",
"src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx",
"src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx",
"src/components/Settings/RouterSettings/Fallbacks/FallbackGroupConfig.tsx",
"src/components/permissions/VectorStorePermissions.tsx",
"src/components/permissions/AgentPermissions.tsx",

View file

@ -1,6 +1,11 @@
import React from "react";
import { Collapse } from "antd";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
export interface CostBreakdown {
input_cost?: number;
@ -91,179 +96,175 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
const totalCost = isCached ? 0 : (costBreakdown?.total_cost ?? totalSpend);
return (
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Collapse
expandIconPosition="start"
items={[
{
key: "1",
label: (
<div className="flex items-center justify-between w-full">
<h3 className="text-lg font-medium text-gray-900">Cost Breakdown</h3>
<div className="flex items-center space-x-2 mr-4">
<span className="text-sm text-gray-500">Total:</span>
<span className="text-sm font-semibold text-gray-900">
{formatCost(totalSpend)}
{isCached && " (Cached)"}
</span>
</div>
</div>
),
children: (
<div className="p-6 space-y-4">
{/* Step 1: Base Token Costs */}
<div className="space-y-2 max-w-2xl">
{(() => {
const hasCacheBreakdown =
costBreakdown?.cache_read_cost !== undefined ||
costBreakdown?.cache_creation_cost !== undefined;
if (hasCacheBreakdown) {
// Separate line items: Input / Cache Read / Cache Write
const rawCost = isCached ? 0 : (inputCost ?? 0) - (costBreakdown?.cache_read_cost ?? 0) - (costBreakdown?.cache_creation_cost ?? 0);
return (
<>
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
<span className="text-gray-900">
{formatCost(rawCost)}
{rawInputTokens !== undefined && rawInputTokens !== null && (
<span className="text-gray-500 font-normal ml-1">({rawInputTokens.toLocaleString()} tokens)</span>
)}
</span>
</div>
{(costBreakdown?.cache_read_cost ?? 0) > 0 && (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">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">Cache Write Cost:</span>
<span className="text-gray-900">
{formatCost(isCached ? 0 : costBreakdown?.cache_creation_cost)}
{(cacheCreationTokens ?? 0) > 0 && (
<span className="text-gray-500 font-normal ml-1">({(cacheCreationTokens ?? 0).toLocaleString()} tokens)</span>
)}
</span>
</div>
)}
</>
);
}
return (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
<span className="text-gray-900">
{formatCost(inputCost)}
{promptTokens !== undefined && (
<span className="text-gray-500 font-normal ml-1">
({promptTokens.toLocaleString()} prompt tokens)
</span>
)}
</span>
</div>
);
})()}
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Output Cost:</span>
<span className="text-gray-900">
{formatCost(outputCost)}
{completionTokens !== undefined && (
<span className="text-gray-500 font-normal ml-1">
({completionTokens.toLocaleString()} completion tokens)
</span>
)}
</span>
</div>
{costBreakdown?.tool_usage_cost !== undefined && costBreakdown.tool_usage_cost > 0 && (
<div className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">Tool Usage Cost:</span>
<span className="text-gray-900">{formatCost(costBreakdown.tool_usage_cost)}</span>
</div>
)}
{costBreakdown?.additional_costs &&
Object.entries(costBreakdown.additional_costs)
.filter(([, value]) => value != null && value !== 0)
.map(([key, value]) => (
<div key={key} className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">{key}:</span>
<span className="text-gray-900">{formatCost(value)}</span>
</div>
))}
</div>
{/* Subtotal / Original Cost - hide when cached since it would be $0 */}
{!isCached && (
<div className="pt-2 border-t border-gray-100 max-w-2xl">
<div className="flex text-sm font-semibold">
<span className="text-gray-900 w-1/3">Original LLM Cost:</span>
<span className="text-gray-900">{formatCost(originalCost)}</span>
</div>
</div>
)}
{/* Step 2: Adjustments (Discount & Margin) */}
{(hasDiscount || hasMargin) && (
<div className="pt-2 space-y-2 max-w-2xl">
{/* Discounts */}
{hasDiscount && (
<div className="space-y-2">
{costBreakdown.discount_percent !== undefined && costBreakdown.discount_percent !== 0 && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">Discount ({formatPercent(costBreakdown.discount_percent)}):</span>
<span className="text-gray-900">-{formatCost(costBreakdown.discount_amount)}</span>
</div>
)}
{costBreakdown.discount_amount !== undefined && costBreakdown.discount_percent === undefined && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">Discount Amount:</span>
<span className="text-gray-900">-{formatCost(costBreakdown.discount_amount)}</span>
</div>
)}
</div>
)}
{/* Margins */}
{hasMargin && (
<div className="space-y-2">
{costBreakdown.margin_percent !== undefined && costBreakdown.margin_percent !== 0 && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">Margin ({formatPercent(costBreakdown.margin_percent)}):</span>
<span className="text-gray-900">+{formatCost((costBreakdown.margin_total_amount || 0) - (costBreakdown.margin_fixed_amount || 0))}</span>
</div>
)}
{costBreakdown.margin_fixed_amount !== undefined && costBreakdown.margin_fixed_amount !== 0 && (
<div className="flex text-sm text-gray-600">
<span className="font-medium w-1/3">Margin:</span>
<span className="text-gray-900">+{formatCost(costBreakdown.margin_fixed_amount)}</span>
</div>
)}
</div>
)}
</div>
)}
{/* Final Summary */}
<div className="mt-4 pt-4 border-t border-gray-200 max-w-2xl">
<div className="flex items-center">
<span className="font-bold text-sm text-gray-900 w-1/3">Final Calculated Cost:</span>
<span className="text-sm font-bold text-gray-900">
{formatCost(totalCost)}
<div className="bg-background rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="cost-breakdown" className="border-b-0">
<AccordionTrigger className="px-4 py-3 hover:no-underline">
<div className="flex items-center justify-between w-full pr-4">
<h3 className="text-lg font-medium text-foreground">Cost Breakdown</h3>
<div className="flex items-center space-x-2 mr-4">
<span className="text-sm text-muted-foreground">Total:</span>
<span className="text-sm font-semibold text-foreground">
{formatCost(totalSpend)}
{isCached && " (Cached)"}
</span>
</div>
</div>
</div>
),
},
]}
/>
</AccordionTrigger>
<AccordionContent>
<div className="p-6 space-y-4">
{/* Step 1: Base Token Costs */}
<div className="space-y-2 max-w-2xl">
{(() => {
const hasCacheBreakdown =
costBreakdown?.cache_read_cost !== undefined ||
costBreakdown?.cache_creation_cost !== undefined;
if (hasCacheBreakdown) {
// Separate line items: Input / Cache Read / Cache Write
const rawCost = isCached ? 0 : (inputCost ?? 0) - (costBreakdown?.cache_read_cost ?? 0) - (costBreakdown?.cache_creation_cost ?? 0);
return (
<>
<div className="flex text-sm">
<span className="text-muted-foreground font-medium w-1/3">Input Cost:</span>
<span className="text-foreground">
{formatCost(rawCost)}
{rawInputTokens !== undefined && rawInputTokens !== null && (
<span className="text-muted-foreground font-normal ml-1">({rawInputTokens.toLocaleString()} tokens)</span>
)}
</span>
</div>
{(costBreakdown?.cache_read_cost ?? 0) > 0 && (
<div className="flex text-sm">
<span className="text-muted-foreground font-medium w-1/3">Cache Read Cost:</span>
<span className="text-foreground">
{formatCost(isCached ? 0 : costBreakdown?.cache_read_cost)}
{(cacheReadTokens ?? 0) > 0 && (
<span className="text-muted-foreground 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-muted-foreground font-medium w-1/3">Cache Write Cost:</span>
<span className="text-foreground">
{formatCost(isCached ? 0 : costBreakdown?.cache_creation_cost)}
{(cacheCreationTokens ?? 0) > 0 && (
<span className="text-muted-foreground font-normal ml-1">({(cacheCreationTokens ?? 0).toLocaleString()} tokens)</span>
)}
</span>
</div>
)}
</>
);
}
return (
<div className="flex text-sm">
<span className="text-muted-foreground font-medium w-1/3">Input Cost:</span>
<span className="text-foreground">
{formatCost(inputCost)}
{promptTokens !== undefined && (
<span className="text-muted-foreground font-normal ml-1">
({promptTokens.toLocaleString()} prompt tokens)
</span>
)}
</span>
</div>
);
})()}
<div className="flex text-sm">
<span className="text-muted-foreground font-medium w-1/3">Output Cost:</span>
<span className="text-foreground">
{formatCost(outputCost)}
{completionTokens !== undefined && (
<span className="text-muted-foreground font-normal ml-1">
({completionTokens.toLocaleString()} completion tokens)
</span>
)}
</span>
</div>
{costBreakdown?.tool_usage_cost !== undefined && costBreakdown.tool_usage_cost > 0 && (
<div className="flex text-sm">
<span className="text-muted-foreground font-medium w-1/3">Tool Usage Cost:</span>
<span className="text-foreground">{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-muted-foreground font-medium w-1/3">{key}:</span>
<span className="text-foreground">{formatCost(value)}</span>
</div>
))}
</div>
{/* Subtotal / Original Cost - hide when cached since it would be $0 */}
{!isCached && (
<div className="pt-2 border-t border-border max-w-2xl">
<div className="flex text-sm font-semibold">
<span className="text-foreground w-1/3">Original LLM Cost:</span>
<span className="text-foreground">{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-muted-foreground">
<span className="font-medium w-1/3">Discount ({formatPercent(costBreakdown.discount_percent)}):</span>
<span className="text-foreground">-{formatCost(costBreakdown.discount_amount)}</span>
</div>
)}
{costBreakdown.discount_amount !== undefined && costBreakdown.discount_percent === undefined && (
<div className="flex text-sm text-muted-foreground">
<span className="font-medium w-1/3">Discount Amount:</span>
<span className="text-foreground">-{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-muted-foreground">
<span className="font-medium w-1/3">Margin ({formatPercent(costBreakdown.margin_percent)}):</span>
<span className="text-foreground">+{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-muted-foreground">
<span className="font-medium w-1/3">Margin:</span>
<span className="text-foreground">+{formatCost(costBreakdown.margin_fixed_amount)}</span>
</div>
)}
</div>
)}
</div>
)}
{/* Final Summary */}
<div className="mt-4 pt-4 border-t border-border max-w-2xl">
<div className="flex items-center">
<span className="font-bold text-sm text-foreground w-1/3">Final Calculated Cost:</span>
<span className="text-sm font-bold text-foreground">
{formatCost(totalCost)}
{isCached && " (Cached)"}
</span>
</div>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};

View file

@ -1,5 +1,10 @@
import React, { useState, useMemo } from "react";
import { Tooltip } from "antd";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import PresidioDetectedEntities from "./PresidioDetectedEntities";
import BedrockGuardrailDetails, {
BedrockGuardrailResponse,
@ -249,11 +254,11 @@ const MatchDetailsTable = ({ matchDetails }: { matchDetails: MatchDetail[] }) =>
return (
<div className="mt-3">
<h5 className="text-sm font-medium mb-2 text-gray-700">Match Details ({matchDetails.length})</h5>
<h5 className="text-sm font-medium mb-2 text-foreground">Match Details ({matchDetails.length})</h5>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-gray-500">
<tr className="border-b text-left text-muted-foreground">
<th className="pb-2 pr-4 font-medium">Type</th>
<th className="pb-2 pr-4 font-medium">Method</th>
<th className="pb-2 pr-4 font-medium">Action</th>
@ -262,7 +267,7 @@ const MatchDetailsTable = ({ matchDetails }: { matchDetails: MatchDetail[] }) =>
</thead>
<tbody>
{matchDetails.map((match, idx) => (
<tr key={idx} className="border-b border-gray-100">
<tr key={idx} className="border-b border-border">
<td className="py-2 pr-4">{match.type}</td>
<td className="py-2 pr-4">
<span className="px-2 py-0.5 bg-slate-100 text-slate-700 rounded text-xs">
@ -278,7 +283,7 @@ const MatchDetailsTable = ({ matchDetails }: { matchDetails: MatchDetail[] }) =>
{match.action_taken ?? "-"}
</span>
</td>
<td className="py-2 font-mono text-xs text-gray-600 break-all">
<td className="py-2 font-mono text-xs text-muted-foreground break-all">
{match.category ? `[${match.category}] ` : ""}
{match.snippet ?? "-"}
</td>
@ -297,7 +302,7 @@ const GenericGuardrailResponse = ({ response }: { response: any }) => {
<div className="mt-3">
<div className="border rounded-lg overflow-hidden">
<div
className="flex items-center justify-between p-3 bg-gray-50 cursor-pointer hover:bg-gray-100"
className="flex items-center justify-between p-3 bg-muted/40 cursor-pointer hover:bg-muted"
onClick={() => setShowRaw(!showRaw)}
>
<div className="flex items-center">
@ -306,8 +311,8 @@ const GenericGuardrailResponse = ({ response }: { response: any }) => {
</div>
</div>
{showRaw && (
<div className="p-3 border-t bg-white">
<pre className="bg-gray-50 rounded p-3 text-xs overflow-x-auto">
<div className="p-3 border-t bg-background">
<pre className="bg-muted/40 rounded p-3 text-xs overflow-x-auto">
{JSON.stringify(response, null, 2)}
</pre>
</div>
@ -407,7 +412,7 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
return (
<div>
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-4">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-4">
Request Lifecycle
</h4>
<div className="relative">
@ -427,7 +432,7 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
)}
</div>
{idx < timeline.length - 1 && (
<div className="w-0.5 bg-gray-200 flex-grow" style={{ minHeight: "24px" }} />
<div className="w-0.5 bg-border flex-grow" style={{ minHeight: "24px" }} />
)}
</div>
@ -436,7 +441,7 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
<div className="flex items-center gap-2 flex-wrap">
<span
className={`text-sm ${
item.type === "llm" ? "text-blue-600 font-medium" : "text-gray-900"
item.type === "llm" ? "text-blue-600 font-medium" : "text-foreground"
}`}
>
{item.label}
@ -452,7 +457,7 @@ const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
{item.status}
</span>
)}
<span className="text-xs text-gray-400 font-mono ml-auto flex-shrink-0">
<span className="text-xs text-muted-foreground font-mono ml-auto flex-shrink-0">
T+{item.offsetMs}ms
</span>
</div>
@ -495,10 +500,10 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
: null;
return (
<div className="border border-gray-200 rounded-lg bg-white">
<div className="border border-border rounded-lg bg-background">
{/* Collapsed header row */}
<div
className="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-gray-50 transition-colors"
className="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-muted/40 transition-colors"
onClick={() => setExpanded(!expanded)}
>
{/* Status icon */}
@ -508,7 +513,7 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
{/* Name + badges */}
<div className="flex items-center gap-2 flex-wrap flex-1 min-w-0">
<span className="font-semibold text-gray-900 text-sm truncate">{displayName}</span>
<span className="font-semibold text-foreground text-sm truncate">{displayName}</span>
<span className="px-2 py-0.5 border border-blue-200 bg-blue-50 text-blue-700 rounded text-[11px] font-semibold uppercase flex-shrink-0">
{modeStr}
@ -533,25 +538,30 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
)}
{entry.confidence_score != null && (
<span className="px-2 py-0.5 bg-gray-100 text-gray-600 border border-gray-200 rounded text-[11px] font-medium flex-shrink-0">
<span className="px-2 py-0.5 bg-muted text-muted-foreground border border-border rounded text-[11px] font-medium flex-shrink-0">
{(entry.confidence_score * 100).toFixed(0)}% conf
</span>
)}
{riskScore != null && success && (
<Tooltip title={`Risk score: ${riskScore}/10`}>
<span className={`px-2 py-0.5 border rounded text-[11px] font-semibold flex-shrink-0 ${getRiskColor(riskScore)}`}>
Risk {riskScore}/10
</span>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className={`px-2 py-0.5 border rounded text-[11px] font-semibold flex-shrink-0 ${getRiskColor(riskScore)}`}>
Risk {riskScore}/10
</span>
</TooltipTrigger>
<TooltipContent>{`Risk score: ${riskScore}/10`}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
{/* Right side: duration + method + chevron */}
<div className="flex items-center gap-3 flex-shrink-0">
<span className="text-sm text-gray-500 font-mono">{durationStr}</span>
<span className="text-sm text-muted-foreground font-mono">{durationStr}</span>
{entry.detection_method && (
<span className="px-2 py-0.5 bg-gray-100 text-gray-600 border border-gray-200 rounded text-[11px] font-medium">
<span className="px-2 py-0.5 bg-muted text-muted-foreground border border-border rounded text-[11px] font-medium">
{entry.detection_method.split(",")[0].trim()}
</span>
)}
@ -561,33 +571,33 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
{/* Expanded details */}
{expanded && (
<div className="border-t border-gray-100 px-4 py-3">
<div className="border-t border-border px-4 py-3">
{/* Classification details for llm-judge */}
{entry.classification && (
<div className="mb-3 bg-gray-50 rounded-lg p-3 space-y-1">
<h5 className="text-sm font-medium text-gray-700 mb-2">Classification</h5>
<div className="mb-3 bg-muted/40 rounded-lg p-3 space-y-1">
<h5 className="text-sm font-medium text-foreground mb-2">Classification</h5>
{entry.classification.category && (
<div className="flex text-sm">
<span className="font-medium w-1/3 text-gray-500">Category:</span>
<span className="font-medium w-1/3 text-muted-foreground">Category:</span>
<span>{entry.classification.category}</span>
</div>
)}
{entry.classification.article_reference && (
<div className="flex text-sm">
<span className="font-medium w-1/3 text-gray-500">Reference:</span>
<span className="font-medium w-1/3 text-muted-foreground">Reference:</span>
<span className="font-mono">{entry.classification.article_reference}</span>
</div>
)}
{entry.classification.confidence != null && (
<div className="flex text-sm">
<span className="font-medium w-1/3 text-gray-500">Confidence:</span>
<span className="font-medium w-1/3 text-muted-foreground">Confidence:</span>
<span>{(entry.classification.confidence * 100).toFixed(0)}%</span>
</div>
)}
{entry.classification.reason && (
<div className="flex text-sm">
<span className="font-medium w-1/3 text-gray-500">Reason:</span>
<span className="font-medium w-1/3 text-muted-foreground">Reason:</span>
<span>{entry.classification.reason}</span>
</div>
)}
@ -602,7 +612,7 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
{/* Masked entity summary */}
{totalMasked > 0 && (
<div className="mt-3">
<h5 className="text-sm font-medium text-gray-700 mb-2">Masked Entities</h5>
<h5 className="text-sm font-medium text-foreground mb-2">Masked Entities</h5>
<div className="flex flex-wrap gap-2">
{Object.entries(entry.masked_entity_count || {}).map(([entityType, count]) => (
<span key={entityType} className="px-2 py-1 bg-blue-50 text-blue-700 rounded text-xs font-medium">
@ -677,20 +687,20 @@ const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps)
};
return (
<div className="bg-white rounded-xl border border-gray-200 shadow-sm w-full max-w-full overflow-hidden mb-6">
<div className="bg-background rounded-xl border border-border shadow-sm w-full max-w-full overflow-hidden mb-6">
{/* ── Header ─────────────────────────────────────────────── */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-100">
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
<div className="flex items-center gap-4">
<ShieldIcon />
<div>
<h3 className="text-lg font-semibold text-gray-900">
<h3 className="text-lg font-semibold text-foreground">
Guardrails &amp; Policy Compliance
</h3>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-sm text-gray-500">
<span className="text-sm text-muted-foreground">
{guardrailEntries.length} guardrail{guardrailEntries.length !== 1 ? "s" : ""} evaluated
</span>
<span className="text-gray-300">|</span>
<span className="text-muted-foreground/50">|</span>
<span
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold ${
allPassed
@ -711,14 +721,14 @@ const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps)
<div className="flex items-center gap-6">
<div className="text-right">
<div className="text-sm font-medium text-gray-900">
<div className="text-sm font-medium text-foreground">
Total: {totalOverheadMs}ms overhead
</div>
</div>
<button
onClick={handleExport}
className="inline-flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 transition-colors"
className="inline-flex items-center gap-2 px-4 py-2 border border-border rounded-lg text-sm font-medium text-foreground bg-background hover:bg-muted/40 transition-colors"
>
<DownloadIcon />
Export Compliance Log
@ -728,7 +738,7 @@ const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps)
{/* ── Compliance Panel ──────────────────────────────────── */}
{accessToken && logEntry && (
<div className="px-6 py-4 border-b border-gray-100">
<div className="px-6 py-4 border-b border-border">
<CompliancePanel accessToken={accessToken} logEntry={logEntry} />
</div>
)}
@ -736,13 +746,13 @@ const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps)
{/* ── Body: two columns ──────────────────────────────────── */}
<div className="flex">
{/* Left column: Request Lifecycle */}
<div className="w-[340px] flex-shrink-0 border-r border-gray-100 px-6 py-5">
<div className="w-[340px] flex-shrink-0 border-r border-border px-6 py-5">
<RequestLifecycle entries={guardrailEntries} />
</div>
{/* Right column: Evaluation Details */}
<div className="flex-1 px-6 py-5 min-w-0">
<h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-4">
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-4">
Evaluation Details
</h4>
<div className="space-y-3">

View file

@ -1,6 +1,18 @@
import { useState } from "react";
import { Typography, Descriptions, Card, Tag, Tabs, Alert, Collapse, Radio, Space, Spin } from "antd";
import moment from "moment";
import { Copy as CopyIcon, LoaderCircle } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
import { LogEntry } from "../columns";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import GuardrailViewer from "../GuardrailViewer/GuardrailViewer";
@ -28,13 +40,10 @@ import {
FONT_SIZE_SMALL,
FONT_FAMILY_MONO,
SPACING_XLARGE,
SPACING_MEDIUM,
} from "./constants";
import { ToolsSection } from "../ToolsSection";
import { PrettyMessagesView } from "./PrettyMessagesView";
const { Text } = Typography;
export interface LogDetailContentProps {
logEntry: LogEntry;
onOpenSettings?: () => void;
@ -93,13 +102,12 @@ export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails =
<div style={{ padding: `${DRAWER_CONTENT_PADDING} ${DRAWER_CONTENT_PADDING} 0` }}>
{/* Error Alert */}
{hasError && errorInfo && (
<Alert
type="error"
showIcon
message="Request Failed"
description={<ErrorDescription errorInfo={errorInfo} />}
className="mb-6"
/>
<Alert variant="destructive" className="mb-6">
<AlertTitle>Request Failed</AlertTitle>
<AlertDescription>
<ErrorDescription errorInfo={errorInfo} />
</AlertDescription>
</Alert>
)}
{/* Tags */}
@ -108,27 +116,28 @@ export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails =
)}
{/* Request Details */}
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Card title="Request Details" size="small" bordered={false} style={{ marginBottom: 0 }}>
<Descriptions column={2} size="small">
<Descriptions.Item label="Model">{logEntry.model}</Descriptions.Item>
<Descriptions.Item label="Provider">{logEntry.custom_llm_provider || "-"}</Descriptions.Item>
<Descriptions.Item label="Call Type">{logEntry.call_type}</Descriptions.Item>
<Descriptions.Item label="Model ID">
<div className="bg-background rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Card className="p-0 border-0 shadow-none rounded-none">
<SectionTitle>Request Details</SectionTitle>
<DescriptionGrid columns={2}>
<DescriptionItem label="Model">{logEntry.model}</DescriptionItem>
<DescriptionItem label="Provider">{logEntry.custom_llm_provider || "-"}</DescriptionItem>
<DescriptionItem label="Call Type">{logEntry.call_type}</DescriptionItem>
<DescriptionItem label="Model ID">
<TruncatedValue value={logEntry.model_id} />
</Descriptions.Item>
<Descriptions.Item label="API Base">
</DescriptionItem>
<DescriptionItem label="API Base">
<TruncatedValue value={logEntry.api_base} maxWidth={API_BASE_MAX_WIDTH} />
</Descriptions.Item>
</DescriptionItem>
{logEntry.requester_ip_address && (
<Descriptions.Item label="IP Address">{logEntry.requester_ip_address}</Descriptions.Item>
<DescriptionItem label="IP Address">{logEntry.requester_ip_address}</DescriptionItem>
)}
{hasGuardrailData && (
<Descriptions.Item label="Guardrail">
<DescriptionItem label="Guardrail">
<GuardrailLabel label={primaryGuardrailLabel} maskedCount={totalMaskedEntities} />
</Descriptions.Item>
</DescriptionItem>
)}
</Descriptions>
</DescriptionGrid>
</Card>
</div>
@ -159,9 +168,9 @@ export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails =
{/* Request/Response JSON */}
{isLoadingDetails ? (
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6 p-8 text-center">
<Spin size="default" />
<div style={{ marginTop: 8, color: "#999" }}>Loading request &amp; response data...</div>
<div className="bg-background rounded-lg shadow w-full max-w-full overflow-hidden mb-6 p-8 text-center">
<LoaderCircle className="animate-spin mx-auto text-muted-foreground" size={20} />
<div className="mt-2 text-muted-foreground">Loading request &amp; response data...</div>
</div>
) : (
<RequestResponseSection
@ -208,17 +217,51 @@ export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails =
// Helper Components
// ============================================================================
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
<div className="px-4 py-2 border-b text-sm font-semibold">
{children}
</div>
);
}
/** Replacement for antd `Descriptions` with `column={n}` layout — renders a
* semantic `<dl>` as a grid of label/value pairs. */
function DescriptionGrid({
columns = 2,
children,
}: {
columns?: number;
children: React.ReactNode;
}) {
const gridCols = columns === 2 ? "md:grid-cols-2" : columns === 3 ? "md:grid-cols-3" : "md:grid-cols-1";
return (
<dl className={`grid grid-cols-1 ${gridCols} gap-x-6 gap-y-2 p-4 text-sm`}>
{children}
</dl>
);
}
function DescriptionItem({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div className="ant-descriptions-item flex gap-2" role="group" aria-label={label}>
<dt className="text-muted-foreground font-medium min-w-[8rem]">{label}</dt>
<dd className="text-foreground">{children}</dd>
</div>
);
}
function ErrorDescription({ errorInfo }: { errorInfo: any }) {
return (
<div>
{errorInfo.error_code && (
<div>
<Text strong>Error Code:</Text> {errorInfo.error_code}
<strong>Error Code:</strong> {errorInfo.error_code}
</div>
)}
{errorInfo.error_message && (
<div>
<Text strong>Message:</Text> {errorInfo.error_message}
<strong>Message:</strong> {errorInfo.error_message}
</div>
)}
</div>
@ -227,17 +270,15 @@ function ErrorDescription({ errorInfo }: { errorInfo: any }) {
function TagsSection({ tags }: { tags: Record<string, any> }) {
return (
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden p-4 mb-6">
<Text strong style={{ display: "block", marginBottom: 8, fontSize: 16 }}>
Tags
</Text>
<Space size={SPACING_MEDIUM} wrap>
<div className="bg-background rounded-lg shadow w-full max-w-full overflow-hidden p-4 mb-6">
<div className="font-semibold text-base mb-2">Tags</div>
<div className="flex flex-wrap gap-2">
{Object.entries(tags).map(([key, value]) => (
<Tag key={key}>
<Badge key={key} variant="secondary">
{key}: {String(value)}
</Tag>
</Badge>
))}
</Space>
</div>
</div>
);
}
@ -249,14 +290,14 @@ function GuardrailLabel({ label, maskedCount }: { label: string; maskedCount: nu
};
return (
<Space size={SPACING_MEDIUM}>
<a onClick={handleClick} style={{ cursor: "pointer" }}>{label}</a>
<span className="inline-flex items-center gap-2">
<a onClick={handleClick} className="cursor-pointer hover:underline">{label}</a>
{maskedCount > 0 && (
<Tag color="blue">
<Badge className="bg-blue-100 text-blue-700 hover:bg-blue-100">
{maskedCount} masked
</Tag>
</Badge>
)}
</Space>
</span>
);
}
@ -286,84 +327,85 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
metadata.additional_usage_values.cache_read_input_tokens > 0);
const cacheHitValue = String(logEntry.cache_hit ?? "None");
const cacheHitColor =
const cacheHitBadgeClass =
cacheHitValue.toLowerCase() === "true"
? "green"
? "bg-green-100 text-green-700"
: cacheHitValue.toLowerCase() === "false"
? "red"
: "default";
? "bg-red-100 text-red-700"
: "bg-muted text-foreground";
const uncachedInputTokens = getUncachedInputTextTokens(metadata);
const showAnthropicMessagesInputOutput =
logEntry.call_type === "anthropic_messages" && uncachedInputTokens !== undefined;
return (
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Card title="Metrics" size="small" style={{ marginBottom: 0 }}>
<Descriptions column={2} size="small">
<div className="bg-background rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Card className="p-0 border-0 shadow-none rounded-none">
<SectionTitle>Metrics</SectionTitle>
<DescriptionGrid columns={2}>
{showAnthropicMessagesInputOutput ? (
<>
<Descriptions.Item label="Input Tokens">
<DescriptionItem label="Input Tokens">
{formatNumberWithCommas(uncachedInputTokens)}
</Descriptions.Item>
<Descriptions.Item label="Output Tokens">
</DescriptionItem>
<DescriptionItem label="Output Tokens">
{formatNumberWithCommas(logEntry.completion_tokens)}
</Descriptions.Item>
</DescriptionItem>
</>
) : (
<Descriptions.Item label="Tokens">
<DescriptionItem label="Tokens">
<TokenFlow
prompt={logEntry.prompt_tokens}
completion={logEntry.completion_tokens}
total={logEntry.total_tokens}
/>
</Descriptions.Item>
</DescriptionItem>
)}
<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>
<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 && (
<Descriptions.Item label="Time to First Token">{(ttftMs / 1000).toFixed(3)} s</Descriptions.Item>
<DescriptionItem label="Time to First Token">{(ttftMs / 1000).toFixed(3)} s</DescriptionItem>
)}
{hasCacheActivity && (
<>
<Descriptions.Item label="Cache Hit">
<Tag color={cacheHitColor}>{cacheHitValue}</Tag>
</Descriptions.Item>
<DescriptionItem label="Cache Hit">
<Badge className={cacheHitBadgeClass}>{cacheHitValue}</Badge>
</DescriptionItem>
{metadata?.additional_usage_values?.cache_read_input_tokens > 0 && (
<Descriptions.Item label="Cache Read Tokens">
<DescriptionItem label="Cache Read Tokens">
{formatNumberWithCommas(metadata.additional_usage_values.cache_read_input_tokens)}
</Descriptions.Item>
</DescriptionItem>
)}
{metadata?.additional_usage_values?.cache_creation_input_tokens > 0 && (
<Descriptions.Item label="Cache Creation Tokens">
<DescriptionItem label="Cache Creation Tokens">
{formatNumberWithCommas(metadata.additional_usage_values.cache_creation_input_tokens)}
</Descriptions.Item>
</DescriptionItem>
)}
</>
)}
{metadata?.litellm_overhead_time_ms !== undefined && metadata.litellm_overhead_time_ms !== null && (
<Descriptions.Item label="LiteLLM Overhead">
<DescriptionItem label="LiteLLM Overhead">
{metadata.litellm_overhead_time_ms.toFixed(2)} ms
</Descriptions.Item>
</DescriptionItem>
)}
<Descriptions.Item label="Retries">
<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}` : ''}</>
: <Tag color="green">None</Tag>
: <Badge className="bg-green-100 text-green-700 hover:bg-green-100">None</Badge>
: "-"}
</Descriptions.Item>
</DescriptionItem>
<Descriptions.Item label="Start Time">
<DescriptionItem label="Start Time">
{moment(logEntry.startTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")}
</Descriptions.Item>
<Descriptions.Item label="End Time">
</DescriptionItem>
<DescriptionItem label="End Time">
{moment(logEntry.endTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")}
</Descriptions.Item>
</Descriptions>
</DescriptionItem>
</DescriptionGrid>
</Card>
</div>
);
@ -392,6 +434,14 @@ function RequestResponseSection({
return JSON.stringify(data, null, 2);
};
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(getCopyText());
} catch {
/* noop */
}
};
const totalSpend = logEntry.spend ?? 0;
const promptTokens = logEntry.prompt_tokens || 0;
const completionTokens = logEntry.completion_tokens || 0;
@ -412,93 +462,83 @@ function RequestResponseSection({
: 0;
return (
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Collapse
defaultActiveKey={["1"]}
expandIconPosition="start"
items={[
{
key: "1",
label: (
<div
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}
onClick={(e) => {
const target = e.target as HTMLElement;
if (target.closest('.ant-radio-group')) {
e.stopPropagation();
}
}}
<div className="bg-background rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Accordion type="single" collapsible defaultValue="request-response" className="w-full">
<AccordionItem value="request-response" className="border-b-0">
<div className="flex items-center justify-between pr-4">
<AccordionTrigger className="flex-1 px-4 py-3 hover:no-underline">
<h3 className="text-lg font-medium text-foreground m-0">Request &amp; Response</h3>
</AccordionTrigger>
<div onClick={(e) => e.stopPropagation()}>
<RadioGroup
value={viewMode}
onValueChange={(v) => setViewMode(v as 'pretty' | 'json')}
className="flex items-center gap-2"
>
<h3 className="text-lg font-medium text-gray-900" style={{ margin: 0 }}>Request & Response</h3>
<Radio.Group
size="small"
value={viewMode}
onChange={(e) => setViewMode(e.target.value)}
>
<Radio.Button value="pretty">Pretty</Radio.Button>
<Radio.Button value="json">JSON</Radio.Button>
</Radio.Group>
</div>
),
children: (
<div>
{viewMode === 'pretty' ? (
<PrettyMessagesView
request={getRawRequest()}
response={getFormattedResponse()}
metrics={{
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
input_cost: inputCost,
output_cost: outputCost,
}}
/>
) : (
<Tabs
activeKey={activeTab}
onChange={(key) => setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)}
tabBarExtraContent={
<Text
copyable={{
text: getCopyText(),
tooltips: ["Copy JSON", "Copied!"]
}}
disabled={activeTab === TAB_RESPONSE && !hasResponse && !hasError}
/>
}
items={[
{
key: TAB_REQUEST,
label: "Request",
children: (
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
<JsonViewer data={getRawRequest()} mode="formatted" />
</div>
),
},
{
key: TAB_RESPONSE,
label: "Response",
children: (
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
{hasResponse || hasError ? (
<JsonViewer data={getFormattedResponse()} mode="formatted" />
) : (
<div style={{ textAlign: "center", padding: 20, color: "#999", fontStyle: "italic" }}>
Response data not available
</div>
)}
</div>
),
},
]}
/>
)}
</div>
),
},
]}
/>
<div className="flex items-center gap-1">
<RadioGroupItem value="pretty" id="view-mode-pretty" />
<Label htmlFor="view-mode-pretty" className="cursor-pointer text-sm font-normal">Pretty</Label>
</div>
<div className="flex items-center gap-1">
<RadioGroupItem value="json" id="view-mode-json" />
<Label htmlFor="view-mode-json" className="cursor-pointer text-sm font-normal">JSON</Label>
</div>
</RadioGroup>
</div>
</div>
<AccordionContent>
<div className="px-4">
{viewMode === 'pretty' ? (
<PrettyMessagesView
request={getRawRequest()}
response={getFormattedResponse()}
metrics={{
prompt_tokens: promptTokens,
completion_tokens: completionTokens,
input_cost: inputCost,
output_cost: outputCost,
}}
/>
) : (
<Tabs value={activeTab} onValueChange={(k) => setActiveTab(k as typeof TAB_REQUEST | typeof TAB_RESPONSE)}>
<div className="flex items-center justify-between">
<TabsList>
<TabsTrigger value={TAB_REQUEST}>Request</TabsTrigger>
<TabsTrigger value={TAB_RESPONSE}>Response</TabsTrigger>
</TabsList>
<button
type="button"
onClick={handleCopy}
disabled={activeTab === TAB_RESPONSE && !hasResponse && !hasError}
className="p-1 text-muted-foreground hover:text-foreground disabled:opacity-40"
aria-label="Copy JSON"
title="Copy JSON"
>
<CopyIcon size={14} />
</button>
</div>
<TabsContent value={TAB_REQUEST}>
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
<JsonViewer data={getRawRequest()} mode="formatted" />
</div>
</TabsContent>
<TabsContent value={TAB_RESPONSE}>
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
{hasResponse || hasError ? (
<JsonViewer data={getFormattedResponse()} mode="formatted" />
) : (
<div className="text-center p-5 text-muted-foreground italic">
Response data not available
</div>
)}
</div>
</TabsContent>
</Tabs>
)}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
}
@ -515,68 +555,68 @@ export function GuardrailJumpLink({ guardrailEntries }: { guardrailEntries: any[
};
return (
<div style={{ textAlign: "left", marginBottom: 12 }}>
<div className="text-left mb-3">
<div
onClick={handleClick}
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
padding: "4px 12px",
borderRadius: 16,
cursor: "pointer",
fontSize: 13,
fontWeight: 500,
backgroundColor: allPassed ? "#f0fdf4" : "#fef2f2",
color: allPassed ? "#15803d" : "#b91c1c",
border: `1px solid ${allPassed ? "#bbf7d0" : "#fecaca"}`,
}}
className={`inline-flex items-center gap-1.5 py-1 px-3 rounded-2xl cursor-pointer text-xs font-medium border ${
allPassed
? "bg-green-50 text-green-700 border-green-200"
: "bg-red-50 text-red-700 border-red-200"
}`}
>
{allPassed ? "\u2713" : "\u2717"} {guardrailEntries.length} guardrail{guardrailEntries.length !== 1 ? "s" : ""} evaluated
<span style={{ fontSize: 11, opacity: 0.7 }}>{"\u2193"}</span>
<span className="text-[11px] opacity-70">{"\u2193"}</span>
</div>
</div>
);
}
function MetadataSection({ metadata }: { metadata: Record<string, any> }) {
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(JSON.stringify(metadata, null, 2));
} catch {
/* noop */
}
};
return (
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Collapse
defaultActiveKey={["1"]}
expandIconPosition="start"
items={[
{
key: "1",
label: <h3 className="text-lg font-medium text-gray-900">Metadata</h3>,
children: (
<div>
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 8 }}>
<Text
copyable={{
text: JSON.stringify(metadata, null, 2),
tooltips: ["Copy Metadata", "Copied!"]
}}
/>
</div>
<pre
style={{
maxHeight: METADATA_MAX_HEIGHT,
overflowY: "auto",
fontSize: FONT_SIZE_SMALL,
fontFamily: FONT_FAMILY_MONO,
whiteSpace: "pre-wrap",
wordBreak: "break-all",
margin: 0,
}}
<div className="bg-background rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
<Accordion type="single" collapsible defaultValue="metadata" className="w-full">
<AccordionItem value="metadata" className="border-b-0">
<AccordionTrigger className="px-4 py-3 hover:no-underline">
<h3 className="text-lg font-medium text-foreground m-0">Metadata</h3>
</AccordionTrigger>
<AccordionContent>
<div className="px-4 pb-4">
<div className="flex justify-end mb-2">
<button
type="button"
onClick={handleCopy}
className="p-1 text-muted-foreground hover:text-foreground"
aria-label="Copy Metadata"
title="Copy Metadata"
>
{JSON.stringify(metadata, null, 2)}
</pre>
<CopyIcon size={14} />
</button>
</div>
),
},
]}
/>
<pre
style={{
maxHeight: METADATA_MAX_HEIGHT,
overflowY: "auto",
fontSize: FONT_SIZE_SMALL,
fontFamily: FONT_FAMILY_MONO,
whiteSpace: "pre-wrap",
wordBreak: "break-all",
margin: 0,
}}
>
{JSON.stringify(metadata, null, 2)}
</pre>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
}

View file

@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { Button, Drawer } from "antd";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
import { Check as CheckOutlined, Copy as CopyOutlined, ChevronLeft as LeftOutlined, ChevronRight as RightOutlined } from "lucide-react";
import { Bot, Sparkles, Wrench } from "lucide-react";
import { LogEntry } from "../columns";
@ -239,39 +240,34 @@ export function LogDetailsDrawer({
if (!currentLog || !enrichedLog) return null;
return (
<Drawer
title={null}
placement="right"
onClose={onClose}
open={open}
width={DRAWER_WIDTH}
closable={false}
mask={true}
maskClosable={true}
styles={{
body: { padding: 0, overflow: "hidden" },
header: { display: "none" },
}}
>
<div style={{ height: "100%" }} className="flex relative">
<Sheet open={open} onOpenChange={(o) => { if (!o) onClose(); }}>
<SheetContent
side="right"
className="p-0 overflow-hidden sm:max-w-none"
style={{ width: DRAWER_WIDTH, maxWidth: "none" }}
>
<SheetTitle className="sr-only">Log Details</SheetTitle>
<div style={{ height: "100%" }} className="flex relative">
{!isSidebarCollapsed ? (
<Button
type="text"
size="small"
icon={<LeftOutlined />}
variant="ghost"
size="icon"
onClick={() => setIsSidebarCollapsed(true)}
className="absolute top-2 left-2 z-20 !bg-white !border !border-slate-200 !rounded-md"
className="absolute top-2 left-2 z-20 h-7 w-7 bg-background border border-border rounded-md"
aria-label="Collapse trace sidebar"
/>
>
<LeftOutlined className="h-3.5 w-3.5" />
</Button>
) : (
<Button
type="text"
size="small"
icon={<RightOutlined />}
variant="ghost"
size="icon"
onClick={() => setIsSidebarCollapsed(false)}
className="absolute top-2 left-2 z-20 !bg-white !border !border-slate-200 !rounded-md"
className="absolute top-2 left-2 z-20 h-7 w-7 bg-background border border-border rounded-md"
aria-label="Expand trace sidebar"
/>
>
<RightOutlined className="h-3.5 w-3.5" />
</Button>
)}
{!isSidebarCollapsed && (
<div
@ -401,6 +397,7 @@ export function LogDetailsDrawer({
</div>
</div>
</div>
</Drawer>
</SheetContent>
</Sheet>
);
}

View file

@ -5,11 +5,32 @@
*/
import { useState } from 'react';
import { Typography, Tag, Tooltip } from 'antd';
import { Badge } from "@/components/ui/badge";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Volume2 as SoundOutlined, MessageSquare as MessageOutlined, Settings as SettingOutlined, Mic as AudioOutlined, ChevronDown as DownOutlined, ChevronUp as UpOutlined } from "lucide-react";
import { SectionHeader } from './SectionHeader';
const { Text } = Typography;
/** Mimics antd `Typography.Text`'s `type="secondary"` mutted-gray style. */
function SecondaryText({
children,
className = "",
style,
}: {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
}) {
return (
<span className={`text-muted-foreground ${className}`} style={style}>
{children}
</span>
);
}
interface RealtimeEvent {
type: string;
@ -170,30 +191,27 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<SettingOutlined style={{ color: '#8c8c8c', fontSize: 14 }} />
<Text style={{ fontWeight: 500, fontSize: 14 }}>Session</Text>
<span style={{ fontWeight: 500, fontSize: 14 }}>Session</span>
</div>
<Text type="secondary" style={{ fontSize: 12 }}>
<SecondaryText style={{ fontSize: 12 }}>
{session.model}
</Text>
</SecondaryText>
{turnCount > 0 && (
<Tag
color="purple"
style={{ margin: 0, fontWeight: 500 }}
>
<Badge className="bg-purple-100 text-purple-700 hover:bg-purple-100 font-medium">
{turnCount} {turnCount === 1 ? 'turn' : 'turns'}
</Tag>
</Badge>
)}
{session.voice && (
<Tag color="blue" style={{ margin: 0 }}>
<SoundOutlined /> {session.voice}
</Tag>
<Badge className="bg-blue-100 text-blue-700 hover:bg-blue-100 gap-1">
<SoundOutlined size={12} /> {session.voice}
</Badge>
)}
{session.modalities && (
<div style={{ display: 'flex', gap: 4 }}>
{session.modalities.map((m) => (
<Tag key={m} style={{ margin: 0 }}>
{m === 'audio' ? <AudioOutlined /> : <MessageOutlined />} {m}
</Tag>
<Badge key={m} variant="secondary" className="gap-1">
{m === 'audio' ? <AudioOutlined size={12} /> : <MessageOutlined size={12} />} {m}
</Badge>
))}
</div>
)}
@ -248,8 +266,7 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
{session.instructions && (
<div style={{ marginTop: 12 }}>
<Text
type="secondary"
<SecondaryText
style={{
fontSize: 10,
letterSpacing: '0.5px',
@ -259,7 +276,7 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
}}
>
Instructions
</Text>
</SecondaryText>
<div
style={{
fontSize: 12,
@ -374,26 +391,31 @@ function ResponseTurn({
marginBottom: 8,
}}
>
<Tag
color={response.status === 'completed' ? 'green' : 'orange'}
style={{ margin: 0 }}
<Badge
className={
response.status === 'completed'
? "bg-green-100 text-green-700 hover:bg-green-100"
: "bg-orange-100 text-orange-700 hover:bg-orange-100"
}
>
{response.status || 'unknown'}
</Tag>
</Badge>
{usage && (
<Text type="secondary" style={{ fontSize: 11 }}>
<SecondaryText style={{ fontSize: 11 }}>
{usage.input_tokens ?? 0} in / {usage.output_tokens ?? 0} out tokens
</Text>
</SecondaryText>
)}
{response.conversation_id && (
<Tooltip title={response.conversation_id}>
<Text
type="secondary"
style={{ fontSize: 11, cursor: 'help' }}
>
conv: {response.conversation_id.slice(0, 12)}...
</Text>
</Tooltip>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span className="text-muted-foreground cursor-help" style={{ fontSize: 11 }}>
conv: {response.conversation_id.slice(0, 12)}...
</span>
</TooltipTrigger>
<TooltipContent>{response.conversation_id}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
@ -427,8 +449,7 @@ function OutputMessage({ output }: { output: RealtimeOutputItem }) {
return (
<div style={{ marginBottom: 8 }}>
<Text
type="secondary"
<SecondaryText
style={{
fontSize: 10,
letterSpacing: '0.5px',
@ -438,7 +459,7 @@ function OutputMessage({ output }: { output: RealtimeOutputItem }) {
}}
>
{output.role?.toUpperCase() || 'ASSISTANT'}
</Text>
</SecondaryText>
{contents.map((c, cIdx) => {
const text = c.transcript || c.text;
if (!text) return null;
@ -507,12 +528,11 @@ function TokenBreakdown({
return (
<div style={{ marginTop: 4 }}>
<Text
type="secondary"
<SecondaryText
style={{ fontSize: 10, letterSpacing: '0.5px', textTransform: 'uppercase' }}
>
{label} Token Breakdown
</Text>
</SecondaryText>
<div
style={{
display: 'flex',
@ -524,9 +544,9 @@ function TokenBreakdown({
{entries.map(([key, value]) => {
if (typeof value === 'number') {
return (
<Tag key={key} style={{ margin: 0 }}>
<Badge key={key} variant="secondary">
{formatTokenLabel(key)}: {value.toLocaleString()}
</Tag>
</Badge>
);
}
return null;
@ -546,10 +566,10 @@ function ConfigRow({
if (value === undefined || value === null) return null;
return (
<div>
<Text type="secondary" style={{ fontSize: 11 }}>
<SecondaryText style={{ fontSize: 11 }}>
{label}
</Text>
<div style={{ fontSize: 13, color: '#262626' }}>
</SecondaryText>
<div style={{ fontSize: 13 }} className="text-foreground">
{String(value)}
</div>
</div>

View file

@ -1,8 +1,13 @@
import { getSpendString } from "@/utils/dataUtils";
import type { ColumnDef } from "@tanstack/react-table";
// eslint-disable-next-line litellm-ui/no-banned-ui-imports
import { Badge, Button } from "@tremor/react";
import { Tooltip } from "antd";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import React, { useState } from "react";
import { getProviderLogoAndName } from "../provider_info_helpers";
import { TableHeaderSortDropdown } from "../common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
@ -76,6 +81,26 @@ export type LogEntry = {
onSessionClick?: (sessionId: string) => void;
};
/** Small wrapper that renders a shadcn Tooltip with its own provider so it drops
* in where antd's `<Tooltip title=...>` used to go without needing a root-level
* provider in every call site. */
const HoverTooltip = ({
content,
children,
asChild = true,
}: {
content: React.ReactNode;
children: React.ReactNode;
asChild?: boolean;
}) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
<TooltipContent>{content}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
const SortableHeader = ({
label,
field,
@ -162,9 +187,9 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
sessionMcpCount > 0 && `${sessionMcpCount} MCP`,
].filter(Boolean);
return (
<Tooltip title={tooltipParts.join(" • ")}>
<HoverTooltip content={tooltipParts.join(" • ")}>
{sessionTypeBadge}
</Tooltip>
</HoverTooltip>
);
},
},
@ -193,16 +218,16 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
const value = String(info.getValue() || "");
const onSessionClick = info.row.original.onSessionClick;
return (
<Tooltip title={String(info.getValue() || "")}>
<HoverTooltip content={value}>
<Button
size="xs"
variant="light"
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal text-xs max-w-[15ch] truncate block"
size="sm"
variant="ghost"
className="h-auto py-0.5 px-2 font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal max-w-[15ch] truncate block"
onClick={() => onSessionClick?.(value)}
>
{String(info.getValue() || "")}
{value}
</Button>
</Tooltip>
</HoverTooltip>
);
},
},
@ -211,9 +236,9 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
header: "Request ID",
accessorKey: "request_id",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "")}>
<HoverTooltip content={String(info.getValue() || "")}>
<span className="font-mono text-xs max-w-[15ch] truncate block">{String(info.getValue() || "")}</span>
</Tooltip>
</HoverTooltip>
),
},
{
@ -236,9 +261,9 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
return (
<div className="flex flex-col">
<Tooltip title={`$${String(info.getValue() || 0)}`}>
<HoverTooltip content={`$${String(info.getValue() || 0)}`}>
<span>{getSpendString(info.getValue() || 0)}</span>
</Tooltip>
</HoverTooltip>
{mcpCount > 0 && mcpSpend > 0 && (
<span className="text-[10px] text-amber-600">
incl. {getSpendString(mcpSpend)} from {mcpCount} MCP
@ -266,9 +291,9 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
if (ms == null) return <span>-</span>;
const seconds = (ms / 1000).toFixed(2);
return (
<Tooltip title={`${ms}ms`}>
<HoverTooltip content={`${ms}ms`}>
<span className="max-w-[15ch] truncate block">{seconds}</span>
</Tooltip>
</HoverTooltip>
);
},
},
@ -285,9 +310,9 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
if (ttftMs <= 0) return <span>-</span>;
const ttftSeconds = (ttftMs / 1000).toFixed(2);
return (
<Tooltip title={`${ttftMs}ms`}>
<HoverTooltip content={`${ttftMs}ms`}>
<span className="max-w-[15ch] truncate block">{ttftSeconds}</span>
</Tooltip>
</HoverTooltip>
);
},
},
@ -295,9 +320,9 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
header: "Team Name",
accessorKey: "metadata.user_api_key_team_alias",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "-")}>
<HoverTooltip content={String(info.getValue() || "-")}>
<span className="max-w-[15ch] truncate block">{String(info.getValue() || "-")}</span>
</Tooltip>
</HoverTooltip>
),
},
{
@ -308,14 +333,14 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
const onKeyHashClick = info.row.original.onKeyHashClick;
return (
<Tooltip title={value}>
<HoverTooltip content={value}>
<span
className="font-mono max-w-[15ch] truncate block cursor-pointer hover:text-blue-600"
onClick={() => onKeyHashClick?.(value)}
>
{value}
</span>
</Tooltip>
</HoverTooltip>
);
},
},
@ -323,9 +348,9 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
header: "Key Name",
accessorKey: "metadata.user_api_key_alias",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "-")}>
<HoverTooltip content={String(info.getValue() || "-")}>
<span className="max-w-[15ch] truncate block">{String(info.getValue() || "-")}</span>
</Tooltip>
</HoverTooltip>
),
},
{
@ -348,9 +373,9 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
}}
/>
)}
<Tooltip title={modelName}>
<HoverTooltip content={modelName}>
<span className="max-w-[15ch] truncate block">{modelName}</span>
</Tooltip>
</HoverTooltip>
</div>
);
},
@ -373,7 +398,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
return (
<span className="text-sm">
{String(row.total_tokens || "0")}
<span className="text-gray-400 text-xs ml-1">
<span className="text-muted-foreground text-xs ml-1">
({String(row.prompt_tokens || "0")}+{String(row.completion_tokens || "0")})
</span>
</span>
@ -384,18 +409,18 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
header: "Internal User",
accessorKey: "user",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "-")}>
<HoverTooltip content={String(info.getValue() || "-")}>
<span className="max-w-[15ch] truncate block">{String(info.getValue() || "-")}</span>
</Tooltip>
</HoverTooltip>
),
},
{
header: "End User",
accessorKey: "end_user",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "-")}>
<HoverTooltip content={String(info.getValue() || "-")}>
<span className="max-w-[15ch] truncate block">{String(info.getValue() || "-")}</span>
</Tooltip>
</HoverTooltip>
),
},
@ -412,8 +437,8 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
return (
<div className="flex flex-wrap gap-1">
<Tooltip
title={
<HoverTooltip
content={
<div className="flex flex-col gap-1">
{tagEntries.map(([key, value]) => (
<span key={key}>
@ -423,11 +448,11 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
</div>
}
>
<span className="px-2 py-1 bg-gray-100 rounded-full text-xs">
<span className="px-2 py-1 bg-muted rounded-full text-xs">
{firstTag[0]}: {String(firstTag[1])}
{remainingTags.length > 0 && ` +${remainingTags.length}`}
</span>
</Tooltip>
</HoverTooltip>
</div>
);
},
@ -464,12 +489,12 @@ export const RequestResponsePanel = ({ request, response }: { request: any; resp
return (
<div className="grid grid-cols-2 gap-4 mt-4">
<div className="rounded-lg border border-gray-200 bg-gray-50">
<div className="flex justify-between items-center p-3 border-b border-gray-200">
<div className="rounded-lg border border-border bg-muted/40">
<div className="flex justify-between items-center p-3 border-b border-border">
<h3 className="text-sm font-medium">Request</h3>
<button
onClick={() => copyToClipboard(requestStr)}
className="p-1 hover:bg-gray-200 rounded"
className="p-1 hover:bg-muted rounded"
title="Copy request"
>
<svg
@ -491,12 +516,12 @@ export const RequestResponsePanel = ({ request, response }: { request: any; resp
<pre className="p-4 overflow-auto text-xs font-mono h-64 whitespace-pre-wrap break-words">{requestStr}</pre>
</div>
<div className="rounded-lg border border-gray-200 bg-gray-50">
<div className="flex justify-between items-center p-3 border-b border-gray-200">
<div className="rounded-lg border border-border bg-muted/40">
<div className="flex justify-between items-center p-3 border-b border-border">
<h3 className="text-sm font-medium">Response</h3>
<button
onClick={() => copyToClipboard(responseStr)}
className="p-1 hover:bg-gray-200 rounded"
className="p-1 hover:bg-muted rounded"
title="Copy response"
>
<svg
@ -536,7 +561,7 @@ const CollapsibleJsonCell = ({ jsonData }: { jsonData: any }) => {
{isExpanded ? "Hide JSON" : "Show JSON"} ({Object.keys(jsonData).length} fields)
</button>
{isExpanded && (
<pre className="mt-2 p-2 bg-gray-50 border rounded text-xs overflow-auto max-h-60">{jsonString}</pre>
<pre className="mt-2 p-2 bg-muted/40 border rounded text-xs overflow-auto max-h-60">{jsonString}</pre>
)}
</div>
);
@ -556,7 +581,7 @@ export type AuditLogEntry = {
const getActionBadge = (action: string) => {
return (
<Badge color="gray" className="flex items-center gap-1">
<Badge variant="secondary" className="flex items-center gap-1">
<span className="whitespace-nowrap text-xs">{action}</span>
</Badge>
);
@ -647,13 +672,11 @@ export const auditLogColumns: ColumnDef<AuditLogEntry>[] = [
<div className="space-y-1">
<div className="font-medium">{changedBy}</div>
{apiKey && ( // Only show API key if it exists
<Tooltip title={apiKey}>
<HoverTooltip content={apiKey}>
<div className="text-xs text-muted-foreground max-w-[15ch] truncate">
{" "}
{/* Apply max-width and truncate */}
{apiKey}
</div>
</Tooltip>
</HoverTooltip>
)}
</div>
);
@ -680,11 +703,11 @@ export const auditLogColumns: ColumnDef<AuditLogEntry>[] = [
};
return (
<Tooltip title={copied ? "Copied!" : String(objectId)}>
<HoverTooltip content={copied ? "Copied!" : String(objectId)}>
<span className="max-w-[20ch] truncate block cursor-pointer hover:text-blue-600" onClick={handleCopy}>
{String(objectId)}
</span>
</Tooltip>
</HoverTooltip>
);
};
return <ObjectIdDisplay />;