mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-03 02:22:24 +00:00
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:
parent
7546589e82
commit
d3708b967f
7 changed files with 639 additions and 543 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 & 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">
|
||||
|
|
|
|||
|
|
@ -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 & 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 & 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 & 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 />;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue