mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
refactor(ui): migrate log details drawer off antd to shadcn
Replaces Ant Design across every source file under src/components/view_logs, so the request log drawer and its viewers compose @/components/ui primitives. - Drawer becomes Sheet, Collapse becomes Collapsible, Segmented and Radio.Group become Tabs, Tag becomes Badge, Descriptions becomes a local grid helper - every lucide icon carries an explicit size class, since antd icons render at 1em while lucide defaults to 24px - two tests dropped assertions on antd internal class names in favour of rendered text and roles, and the Pretty/JSON case now proves the toggle actually swaps the body rather than only that both controls render - drops the eslint suppressions these files no longer need
This commit is contained in:
parent
423b791ee0
commit
03d2b16bcb
15 changed files with 1154 additions and 1109 deletions
|
|
@ -3506,29 +3506,15 @@
|
||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/view_logs/CostBreakdownViewer.tsx": {
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/view_logs/EvalViewer/EvalViewer.tsx": {
|
"src/components/view_logs/EvalViewer/EvalViewer.tsx": {
|
||||||
"local/no-complex-jsx-arrow": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"no-nested-ternary": {
|
"no-nested-ternary": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/view_logs/GuardrailViewer/CompliancePanel.tsx": {
|
"src/components/view_logs/GuardrailViewer/CompliancePanel.tsx": {
|
||||||
"no-nested-ternary": {
|
"no-nested-ternary": {
|
||||||
"count": 2
|
"count": 2
|
||||||
},
|
},
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"react-hooks/set-state-in-effect": {
|
"react-hooks/set-state-in-effect": {
|
||||||
"count": 1
|
"count": 1
|
||||||
}
|
}
|
||||||
|
|
@ -3541,31 +3527,17 @@
|
||||||
"src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx": {
|
"src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx": {
|
||||||
"no-nested-ternary": {
|
"no-nested-ternary": {
|
||||||
"count": 4
|
"count": 4
|
||||||
},
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx": {
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx": {
|
"src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx": {
|
||||||
"no-nested-ternary": {
|
"no-nested-ternary": {
|
||||||
"count": 3
|
"count": 3
|
||||||
},
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx": {
|
"src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx": {
|
||||||
"no-nested-ternary": {
|
"no-nested-ternary": {
|
||||||
"count": 2
|
"count": 2
|
||||||
},
|
},
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
},
|
|
||||||
"react-hooks/set-state-in-effect": {
|
"react-hooks/set-state-in-effect": {
|
||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
|
|
@ -3575,36 +3547,11 @@
|
||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/view_logs/LogDetailsDrawer/RealtimePrettyView.tsx": {
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/view_logs/LogDetailsDrawer/useKeyboardNavigation.ts": {
|
"src/components/view_logs/LogDetailsDrawer/useKeyboardNavigation.ts": {
|
||||||
"react-hooks/immutability": {
|
"react-hooks/immutability": {
|
||||||
"count": 2
|
"count": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/components/view_logs/ToolsSection/FormattedToolView.tsx": {
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/view_logs/ToolsSection/ToolExpandedContent.tsx": {
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/view_logs/ToolsSection/ToolItem.tsx": {
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/view_logs/VectorStoreViewer.tsx": {
|
|
||||||
"no-restricted-imports": {
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"src/components/view_logs/columns.tsx": {
|
"src/components/view_logs/columns.tsx": {
|
||||||
"local/filename-pascal-case": {
|
"local/filename-pascal-case": {
|
||||||
"count": 1
|
"count": 1
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { Collapse } from "antd";
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||||
|
|
||||||
export interface CostBreakdown {
|
export interface CostBreakdown {
|
||||||
|
|
@ -49,6 +50,7 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
|
||||||
cacheReadTokens,
|
cacheReadTokens,
|
||||||
cacheCreationTokens,
|
cacheCreationTokens,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const isCached = cacheHit?.toLowerCase() === "true";
|
const isCached = cacheHit?.toLowerCase() === "true";
|
||||||
const hasTokenCounts = promptTokens !== undefined || completionTokens !== undefined;
|
const hasTokenCounts = promptTokens !== undefined || completionTokens !== undefined;
|
||||||
|
|
||||||
|
|
@ -90,197 +92,195 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
|
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
|
||||||
<Collapse
|
<Collapsible open={open} onOpenChange={setOpen}>
|
||||||
expandIconPosition="start"
|
<CollapsibleTrigger className="flex w-full items-center gap-3 px-4 py-3 text-left">
|
||||||
items={[
|
{open ? (
|
||||||
{
|
<ChevronDown className="size-3.5 shrink-0 text-gray-500" />
|
||||||
key: "1",
|
) : (
|
||||||
label: (
|
<ChevronRight className="size-3.5 shrink-0 text-gray-500" />
|
||||||
<div className="flex items-center justify-between w-full">
|
)}
|
||||||
<h3 className="text-lg font-medium text-gray-900">Cost Breakdown</h3>
|
<div className="flex items-center justify-between w-full">
|
||||||
<div className="flex items-center space-x-2 mr-4">
|
<h3 className="text-lg font-medium text-gray-900">Cost Breakdown</h3>
|
||||||
<span className="text-sm text-gray-500">Total:</span>
|
<div className="flex items-center space-x-2 mr-4">
|
||||||
<span className="text-sm font-semibold text-gray-900">
|
<span className="text-sm text-gray-500">Total:</span>
|
||||||
{formatCost(totalSpend)}
|
<span className="text-sm font-semibold text-gray-900">
|
||||||
{isCached && " (Cached)"}
|
{formatCost(totalSpend)}
|
||||||
</span>
|
{isCached && " (Cached)"}
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
</div>
|
||||||
children: (
|
</CollapsibleTrigger>
|
||||||
<div className="p-6 space-y-4">
|
<CollapsibleContent>
|
||||||
{/* Step 1: Base Token Costs */}
|
<div className="p-6 space-y-4">
|
||||||
<div className="space-y-2 max-w-2xl">
|
{/* Step 1: Base Token Costs */}
|
||||||
{(() => {
|
<div className="space-y-2 max-w-2xl">
|
||||||
const hasCacheBreakdown =
|
{(() => {
|
||||||
costBreakdown?.cache_read_cost !== undefined || costBreakdown?.cache_creation_cost !== undefined;
|
const hasCacheBreakdown =
|
||||||
if (hasCacheBreakdown) {
|
costBreakdown?.cache_read_cost !== undefined || costBreakdown?.cache_creation_cost !== undefined;
|
||||||
// Separate line items: Input / Cache Read / Cache Write
|
if (hasCacheBreakdown) {
|
||||||
const rawCost = isCached
|
// Separate line items: Input / Cache Read / Cache Write
|
||||||
? 0
|
const rawCost = isCached
|
||||||
: (inputCost ?? 0) -
|
? 0
|
||||||
(costBreakdown?.cache_read_cost ?? 0) -
|
: (inputCost ?? 0) -
|
||||||
(costBreakdown?.cache_creation_cost ?? 0);
|
(costBreakdown?.cache_read_cost ?? 0) -
|
||||||
return (
|
(costBreakdown?.cache_creation_cost ?? 0);
|
||||||
<>
|
return (
|
||||||
<div className="flex text-sm">
|
<>
|
||||||
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
|
|
||||||
<span className="text-gray-900">
|
|
||||||
{formatCost(rawCost)}
|
|
||||||
{rawInputTokens !== undefined && rawInputTokens !== null && (
|
|
||||||
<span className="text-gray-500 font-normal ml-1">
|
|
||||||
({rawInputTokens.toLocaleString()} tokens)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{(costBreakdown?.cache_read_cost ?? 0) > 0 && (
|
|
||||||
<div className="flex text-sm">
|
|
||||||
<span className="text-gray-600 font-medium w-1/3">Prompt Cache Read Cost:</span>
|
|
||||||
<span className="text-gray-900">
|
|
||||||
{formatCost(isCached ? 0 : costBreakdown?.cache_read_cost)}
|
|
||||||
{(cacheReadTokens ?? 0) > 0 && (
|
|
||||||
<span className="text-gray-500 font-normal ml-1">
|
|
||||||
({(cacheReadTokens ?? 0).toLocaleString()} tokens)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{(costBreakdown?.cache_creation_cost ?? 0) > 0 && (
|
|
||||||
<div className="flex text-sm">
|
|
||||||
<span className="text-gray-600 font-medium w-1/3">Prompt Cache Write Cost:</span>
|
|
||||||
<span className="text-gray-900">
|
|
||||||
{formatCost(isCached ? 0 : costBreakdown?.cache_creation_cost)}
|
|
||||||
{(cacheCreationTokens ?? 0) > 0 && (
|
|
||||||
<span className="text-gray-500 font-normal ml-1">
|
|
||||||
({(cacheCreationTokens ?? 0).toLocaleString()} tokens)
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="flex text-sm">
|
<div className="flex text-sm">
|
||||||
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
|
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">
|
||||||
{formatCost(inputCost)}
|
{formatCost(rawCost)}
|
||||||
{promptTokens !== undefined && (
|
{rawInputTokens !== undefined && rawInputTokens !== null && (
|
||||||
<span className="text-gray-500 font-normal ml-1">
|
<span className="text-gray-500 font-normal ml-1">
|
||||||
({promptTokens.toLocaleString()} prompt tokens)
|
({rawInputTokens.toLocaleString()} tokens)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
{(costBreakdown?.cache_read_cost ?? 0) > 0 && (
|
||||||
})()}
|
<div className="flex text-sm">
|
||||||
|
<span className="text-gray-600 font-medium w-1/3">Prompt Cache Read Cost:</span>
|
||||||
|
<span className="text-gray-900">
|
||||||
|
{formatCost(isCached ? 0 : costBreakdown?.cache_read_cost)}
|
||||||
|
{(cacheReadTokens ?? 0) > 0 && (
|
||||||
|
<span className="text-gray-500 font-normal ml-1">
|
||||||
|
({(cacheReadTokens ?? 0).toLocaleString()} tokens)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(costBreakdown?.cache_creation_cost ?? 0) > 0 && (
|
||||||
|
<div className="flex text-sm">
|
||||||
|
<span className="text-gray-600 font-medium w-1/3">Prompt Cache Write Cost:</span>
|
||||||
|
<span className="text-gray-900">
|
||||||
|
{formatCost(isCached ? 0 : costBreakdown?.cache_creation_cost)}
|
||||||
|
{(cacheCreationTokens ?? 0) > 0 && (
|
||||||
|
<span className="text-gray-500 font-normal ml-1">
|
||||||
|
({(cacheCreationTokens ?? 0).toLocaleString()} tokens)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
<div className="flex text-sm">
|
<div className="flex text-sm">
|
||||||
<span className="text-gray-600 font-medium w-1/3">Output Cost:</span>
|
<span className="text-gray-600 font-medium w-1/3">Input Cost:</span>
|
||||||
<span className="text-gray-900">
|
<span className="text-gray-900">
|
||||||
{formatCost(outputCost)}
|
{formatCost(inputCost)}
|
||||||
{completionTokens !== undefined && (
|
{promptTokens !== undefined && (
|
||||||
<span className="text-gray-500 font-normal ml-1">
|
<span className="text-gray-500 font-normal ml-1">
|
||||||
({completionTokens.toLocaleString()} completion tokens)
|
({promptTokens.toLocaleString()} prompt tokens)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{costBreakdown?.tool_usage_cost !== undefined && costBreakdown.tool_usage_cost > 0 && (
|
);
|
||||||
<div className="flex text-sm">
|
})()}
|
||||||
<span className="text-gray-600 font-medium w-1/3">Tool Usage Cost:</span>
|
<div className="flex text-sm">
|
||||||
<span className="text-gray-900">{formatCost(costBreakdown.tool_usage_cost)}</span>
|
<span className="text-gray-600 font-medium w-1/3">Output Cost:</span>
|
||||||
</div>
|
<span className="text-gray-900">
|
||||||
)}
|
{formatCost(outputCost)}
|
||||||
{costBreakdown?.additional_costs &&
|
{completionTokens !== undefined && (
|
||||||
Object.entries(costBreakdown.additional_costs)
|
<span className="text-gray-500 font-normal ml-1">
|
||||||
.filter(([, value]) => value != null && value !== 0)
|
({completionTokens.toLocaleString()} completion tokens)
|
||||||
.map(([key, value]) => (
|
|
||||||
<div key={key} className="flex text-sm">
|
|
||||||
<span className="text-gray-600 font-medium w-1/3">{key}:</span>
|
|
||||||
<span className="text-gray-900">{formatCost(value)}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Subtotal / Original Cost - hide when cached since it would be $0 */}
|
|
||||||
{!isCached && (
|
|
||||||
<div className="pt-2 border-t border-gray-100 max-w-2xl">
|
|
||||||
<div className="flex text-sm font-semibold">
|
|
||||||
<span className="text-gray-900 w-1/3">Original LLM Cost:</span>
|
|
||||||
<span className="text-gray-900">{formatCost(originalCost)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Step 2: Adjustments (Discount & Margin) */}
|
|
||||||
{(hasDiscount || hasMargin) && (
|
|
||||||
<div className="pt-2 space-y-2 max-w-2xl">
|
|
||||||
{/* Discounts */}
|
|
||||||
{hasDiscount && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{costBreakdown.discount_percent !== undefined && costBreakdown.discount_percent !== 0 && (
|
|
||||||
<div className="flex text-sm text-gray-600">
|
|
||||||
<span className="font-medium w-1/3">
|
|
||||||
Discount ({formatPercent(costBreakdown.discount_percent)}):
|
|
||||||
</span>
|
|
||||||
<span className="text-gray-900">-{formatCost(costBreakdown.discount_amount)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{costBreakdown.discount_amount !== undefined &&
|
|
||||||
costBreakdown.discount_percent === undefined && (
|
|
||||||
<div className="flex text-sm text-gray-600">
|
|
||||||
<span className="font-medium w-1/3">Discount Amount:</span>
|
|
||||||
<span className="text-gray-900">-{formatCost(costBreakdown.discount_amount)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Margins */}
|
|
||||||
{hasMargin && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{costBreakdown.margin_percent !== undefined && costBreakdown.margin_percent !== 0 && (
|
|
||||||
<div className="flex text-sm text-gray-600">
|
|
||||||
<span className="font-medium w-1/3">
|
|
||||||
Margin ({formatPercent(costBreakdown.margin_percent)}):
|
|
||||||
</span>
|
|
||||||
<span className="text-gray-900">
|
|
||||||
+
|
|
||||||
{formatCost(
|
|
||||||
(costBreakdown.margin_total_amount || 0) - (costBreakdown.margin_fixed_amount || 0),
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{costBreakdown.margin_fixed_amount !== undefined && costBreakdown.margin_fixed_amount !== 0 && (
|
|
||||||
<div className="flex text-sm text-gray-600">
|
|
||||||
<span className="font-medium w-1/3">Margin:</span>
|
|
||||||
<span className="text-gray-900">+{formatCost(costBreakdown.margin_fixed_amount)}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Final Summary */}
|
|
||||||
<div className="mt-4 pt-4 border-t border-gray-200 max-w-2xl">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="font-bold text-sm text-gray-900 w-1/3">Final Calculated Cost:</span>
|
|
||||||
<span className="text-sm font-bold text-gray-900">
|
|
||||||
{formatCost(totalCost)}
|
|
||||||
{isCached && " (Cached)"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{costBreakdown?.tool_usage_cost !== undefined && costBreakdown.tool_usage_cost > 0 && (
|
||||||
|
<div className="flex text-sm">
|
||||||
|
<span className="text-gray-600 font-medium w-1/3">Tool Usage Cost:</span>
|
||||||
|
<span className="text-gray-900">{formatCost(costBreakdown.tool_usage_cost)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{costBreakdown?.additional_costs &&
|
||||||
|
Object.entries(costBreakdown.additional_costs)
|
||||||
|
.filter(([, value]) => value != null && value !== 0)
|
||||||
|
.map(([key, value]) => (
|
||||||
|
<div key={key} className="flex text-sm">
|
||||||
|
<span className="text-gray-600 font-medium w-1/3">{key}:</span>
|
||||||
|
<span className="text-gray-900">{formatCost(value)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtotal / Original Cost - hide when cached since it would be $0 */}
|
||||||
|
{!isCached && (
|
||||||
|
<div className="pt-2 border-t border-gray-100 max-w-2xl">
|
||||||
|
<div className="flex text-sm font-semibold">
|
||||||
|
<span className="text-gray-900 w-1/3">Original LLM Cost:</span>
|
||||||
|
<span className="text-gray-900">{formatCost(originalCost)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
)}
|
||||||
},
|
|
||||||
]}
|
{/* Step 2: Adjustments (Discount & Margin) */}
|
||||||
/>
|
{(hasDiscount || hasMargin) && (
|
||||||
|
<div className="pt-2 space-y-2 max-w-2xl">
|
||||||
|
{/* Discounts */}
|
||||||
|
{hasDiscount && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{costBreakdown.discount_percent !== undefined && costBreakdown.discount_percent !== 0 && (
|
||||||
|
<div className="flex text-sm text-gray-600">
|
||||||
|
<span className="font-medium w-1/3">
|
||||||
|
Discount ({formatPercent(costBreakdown.discount_percent)}):
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-900">-{formatCost(costBreakdown.discount_amount)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{costBreakdown.discount_amount !== undefined && costBreakdown.discount_percent === undefined && (
|
||||||
|
<div className="flex text-sm text-gray-600">
|
||||||
|
<span className="font-medium w-1/3">Discount Amount:</span>
|
||||||
|
<span className="text-gray-900">-{formatCost(costBreakdown.discount_amount)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Margins */}
|
||||||
|
{hasMargin && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{costBreakdown.margin_percent !== undefined && costBreakdown.margin_percent !== 0 && (
|
||||||
|
<div className="flex text-sm text-gray-600">
|
||||||
|
<span className="font-medium w-1/3">
|
||||||
|
Margin ({formatPercent(costBreakdown.margin_percent)}):
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-900">
|
||||||
|
+
|
||||||
|
{formatCost(
|
||||||
|
(costBreakdown.margin_total_amount || 0) - (costBreakdown.margin_fixed_amount || 0),
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{costBreakdown.margin_fixed_amount !== undefined && costBreakdown.margin_fixed_amount !== 0 && (
|
||||||
|
<div className="flex text-sm text-gray-600">
|
||||||
|
<span className="font-medium w-1/3">Margin:</span>
|
||||||
|
<span className="text-gray-900">+{formatCost(costBreakdown.margin_fixed_amount)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Final Summary */}
|
||||||
|
<div className="mt-4 pt-4 border-t border-gray-200 max-w-2xl">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="font-bold text-sm text-gray-900 w-1/3">Final Calculated Cost:</span>
|
||||||
|
<span className="text-sm font-bold text-gray-900">
|
||||||
|
{formatCost(totalCost)}
|
||||||
|
{isCached && " (Cached)"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Card, Tag, Table, Typography, Space, Tooltip } from "antd";
|
import { CircleCheck, CircleX, FlaskConical } from "lucide-react";
|
||||||
import { CheckCircleOutlined, CloseCircleOutlined, ExperimentOutlined } from "@ant-design/icons";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
const { Text } = Typography;
|
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
interface EvalVerdict {
|
interface EvalVerdict {
|
||||||
criterion_name: string;
|
criterion_name: string;
|
||||||
|
|
@ -36,10 +37,10 @@ export default function EvalViewer({ data }: EvalViewerProps) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
|
||||||
<ExperimentOutlined style={{ fontSize: 16, color: "#6366f1" }} />
|
<FlaskConical className="size-4" style={{ color: "#6366f1" }} />
|
||||||
<Text strong style={{ fontSize: 15 }}>
|
<span className="font-semibold" style={{ fontSize: 15 }}>
|
||||||
LLM Judge Results
|
LLM Judge Results
|
||||||
</Text>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{entries.map((entry, idx) => (
|
{entries.map((entry, idx) => (
|
||||||
|
|
@ -56,151 +57,159 @@ function EvalEntryCard({ entry }: { entry: EvalInformation }) {
|
||||||
// Filter out synthetic "Overall" row the judge sometimes appends — it's already in the header
|
// Filter out synthetic "Overall" row the judge sometimes appends — it's already in the header
|
||||||
const verdicts = (entry.verdicts || []).filter((v) => (v.criterion_name || "").toLowerCase() !== "overall");
|
const verdicts = (entry.verdicts || []).filter((v) => (v.criterion_name || "").toLowerCase() !== "overall");
|
||||||
|
|
||||||
const columns = [
|
const hasWeights = verdicts.some((v) => v.weight != null);
|
||||||
{
|
const weightedTotal = verdicts.reduce((sum, v) => sum + (v.weight != null ? (v.score * v.weight) / 100 : 0), 0);
|
||||||
title: "Criterion",
|
|
||||||
dataIndex: "criterion_name",
|
|
||||||
key: "criterion_name",
|
|
||||||
width: 160,
|
|
||||||
render: (v: string) => (
|
|
||||||
<Text strong style={{ whiteSpace: "nowrap" }}>
|
|
||||||
{v}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Weight",
|
|
||||||
dataIndex: "weight",
|
|
||||||
key: "weight",
|
|
||||||
width: 65,
|
|
||||||
render: (v: number) =>
|
|
||||||
v != null ? (
|
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
||||||
{v}%
|
|
||||||
</Text>
|
|
||||||
) : null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Score",
|
|
||||||
dataIndex: "score",
|
|
||||||
key: "score",
|
|
||||||
width: 65,
|
|
||||||
render: (v: number) => (
|
|
||||||
<Text style={{ color: v >= 70 ? "#52c41a" : v >= 50 ? "#faad14" : "#ff4d4f", fontWeight: 600 }}>{v}</Text>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: (
|
|
||||||
<Tooltip title="Score × Weight — how much each criterion contributes to the final score">
|
|
||||||
<span style={{ borderBottom: "1px dashed #aaa", cursor: "help" }}>Weighted</span>
|
|
||||||
</Tooltip>
|
|
||||||
),
|
|
||||||
key: "weighted",
|
|
||||||
width: 75,
|
|
||||||
render: (_: unknown, row: EvalVerdict) => {
|
|
||||||
if (row.weight == null) return null;
|
|
||||||
const contrib = (row.score * row.weight) / 100;
|
|
||||||
return (
|
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
||||||
{contrib % 1 === 0 ? contrib : contrib.toFixed(1)}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Comment",
|
|
||||||
dataIndex: "reasoning",
|
|
||||||
key: "reasoning",
|
|
||||||
ellipsis: { showTitle: false },
|
|
||||||
render: (v: string) => (
|
|
||||||
<Tooltip title={v}>
|
|
||||||
<span style={{ fontSize: 12 }}>{v}</span>
|
|
||||||
</Tooltip>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card size="sm" className="mb-3" style={{ borderLeft: `3px solid ${scoreColor}` }}>
|
||||||
size="small"
|
<CardHeader>
|
||||||
className="mb-3"
|
<CardTitle>
|
||||||
style={{ borderLeft: `3px solid ${scoreColor}` }}
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
title={
|
{passed ? (
|
||||||
<Space>
|
<CircleCheck className="size-4" style={{ color: "#52c41a" }} />
|
||||||
{passed ? (
|
) : (
|
||||||
<CheckCircleOutlined style={{ color: "#52c41a" }} />
|
<CircleX className="size-4" style={{ color: "#ff4d4f" }} />
|
||||||
) : (
|
)}
|
||||||
<CloseCircleOutlined style={{ color: "#ff4d4f" }} />
|
<span className="font-semibold">{entry.eval_name}</span>
|
||||||
)}
|
<Badge variant={passed ? "secondary" : "destructive"}>{passed ? "PASSED" : "FAILED"}</Badge>
|
||||||
<Text strong>{entry.eval_name}</Text>
|
<TooltipProvider>
|
||||||
<Tag color={passed ? "success" : "error"}>{passed ? "PASSED" : "FAILED"}</Tag>
|
<Tooltip>
|
||||||
<Tooltip
|
<TooltipTrigger
|
||||||
title={`Weighted average of all criterion scores. Each criterion has a weight (%) set when the eval was created — higher-weight criteria count more toward the final score.`}
|
render={
|
||||||
>
|
<span
|
||||||
<Text type="secondary" style={{ fontSize: 12, cursor: "help", borderBottom: "1px dashed #aaa" }}>
|
className="text-muted-foreground"
|
||||||
{entry.overall_score?.toFixed(0)} / 100
|
style={{ fontSize: 12, cursor: "help", borderBottom: "1px dashed #aaa" }}
|
||||||
{entry.threshold != null && ` (threshold: ${entry.threshold})`}
|
/>
|
||||||
</Text>
|
}
|
||||||
</Tooltip>
|
>
|
||||||
</Space>
|
{entry.overall_score?.toFixed(0)} / 100
|
||||||
}
|
{entry.threshold != null && ` (threshold: ${entry.threshold})`}
|
||||||
extra={
|
</TooltipTrigger>
|
||||||
<Space size="small">
|
<TooltipContent>
|
||||||
{entry.judge_model && (
|
Weighted average of all criterion scores. Each criterion has a weight (%) set when the eval was
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
created — higher-weight criteria count more toward the final score.
|
||||||
Judge: {entry.judge_model}
|
</TooltipContent>
|
||||||
</Text>
|
</Tooltip>
|
||||||
)}
|
</TooltipProvider>
|
||||||
{entry.iteration != null && (
|
</div>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
</CardTitle>
|
||||||
Iter: {entry.iteration + 1}
|
<CardAction>
|
||||||
</Text>
|
<div className="flex items-center gap-2">
|
||||||
)}
|
{entry.judge_model && (
|
||||||
</Space>
|
<span className="text-muted-foreground" style={{ fontSize: 12 }}>
|
||||||
}
|
Judge: {entry.judge_model}
|
||||||
>
|
</span>
|
||||||
{entry.eval_error && (
|
)}
|
||||||
<Text type="warning" style={{ display: "block", marginBottom: 8, fontSize: 12 }}>
|
{entry.iteration != null && (
|
||||||
Judge error: {entry.eval_error}
|
<span className="text-muted-foreground" style={{ fontSize: 12 }}>
|
||||||
</Text>
|
Iter: {entry.iteration + 1}
|
||||||
)}
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardAction>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
{verdicts.length > 0 ? (
|
<CardContent>
|
||||||
<Table
|
{entry.eval_error && (
|
||||||
dataSource={verdicts}
|
<span className="text-amber-600" style={{ display: "block", marginBottom: 8, fontSize: 12 }}>
|
||||||
columns={columns}
|
Judge error: {entry.eval_error}
|
||||||
pagination={false}
|
</span>
|
||||||
size="small"
|
)}
|
||||||
rowKey="criterion_name"
|
|
||||||
scroll={{ x: true }}
|
{verdicts.length > 0 ? (
|
||||||
summary={() => {
|
<Table>
|
||||||
const hasWeights = verdicts.some((v) => v.weight != null);
|
<TableHeader>
|
||||||
if (!hasWeights) return null;
|
<TableRow>
|
||||||
const total = verdicts.reduce((sum, v) => sum + (v.weight != null ? (v.score * v.weight) / 100 : 0), 0);
|
<TableHead style={{ width: 160 }}>Criterion</TableHead>
|
||||||
return (
|
<TableHead style={{ width: 65 }}>Weight</TableHead>
|
||||||
<Table.Summary.Row>
|
<TableHead style={{ width: 65 }}>Score</TableHead>
|
||||||
<Table.Summary.Cell index={0}>
|
<TableHead style={{ width: 75 }}>
|
||||||
<Text strong style={{ fontSize: 12 }}>
|
<TooltipProvider>
|
||||||
Total
|
<Tooltip>
|
||||||
</Text>
|
<TooltipTrigger render={<span style={{ borderBottom: "1px dashed #aaa", cursor: "help" }} />}>
|
||||||
</Table.Summary.Cell>
|
Weighted
|
||||||
<Table.Summary.Cell index={1} />
|
</TooltipTrigger>
|
||||||
<Table.Summary.Cell index={2} />
|
<TooltipContent>
|
||||||
<Table.Summary.Cell index={3}>
|
Score × Weight — how much each criterion contributes to the final score
|
||||||
<Text strong style={{ fontSize: 12, color: scoreColor }}>
|
</TooltipContent>
|
||||||
{total % 1 === 0 ? total : total.toFixed(1)}
|
</Tooltip>
|
||||||
</Text>
|
</TooltipProvider>
|
||||||
</Table.Summary.Cell>
|
</TableHead>
|
||||||
<Table.Summary.Cell index={4} />
|
<TableHead>Comment</TableHead>
|
||||||
</Table.Summary.Row>
|
</TableRow>
|
||||||
);
|
</TableHeader>
|
||||||
}}
|
<TableBody>
|
||||||
/>
|
{verdicts.map((row) => {
|
||||||
) : (
|
const contrib = row.weight != null ? (row.score * row.weight) / 100 : null;
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
return (
|
||||||
Score: {entry.overall_score?.toFixed(1)} — no per-criterion breakdown available.
|
<TableRow key={row.criterion_name}>
|
||||||
</Text>
|
<TableCell>
|
||||||
)}
|
<span className="font-semibold" style={{ whiteSpace: "nowrap" }}>
|
||||||
|
{row.criterion_name}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{row.weight != null ? (
|
||||||
|
<span className="text-muted-foreground" style={{ fontSize: 12 }}>
|
||||||
|
{row.weight}%
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: row.score >= 70 ? "#52c41a" : row.score >= 50 ? "#faad14" : "#ff4d4f",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{row.score}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{contrib != null ? (
|
||||||
|
<span className="text-muted-foreground" style={{ fontSize: 12 }}>
|
||||||
|
{contrib % 1 === 0 ? contrib : contrib.toFixed(1)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger render={<span style={{ fontSize: 12 }} />}>{row.reasoning}</TooltipTrigger>
|
||||||
|
<TooltipContent>{row.reasoning}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
{hasWeights && (
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<span className="font-semibold" style={{ fontSize: 12 }}>
|
||||||
|
Total
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell />
|
||||||
|
<TableCell />
|
||||||
|
<TableCell>
|
||||||
|
<span className="font-semibold" style={{ fontSize: 12, color: scoreColor }}>
|
||||||
|
{weightedTotal % 1 === 0 ? weightedTotal : weightedTotal.toFixed(1)}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell />
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground" style={{ fontSize: 12 }}>
|
||||||
|
Score: {entry.overall_score?.toFixed(1)} — no per-criterion breakdown available.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Tooltip } from "antd";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import {
|
import {
|
||||||
checkEuAiActCompliance,
|
checkEuAiActCompliance,
|
||||||
checkGdprCompliance,
|
checkGdprCompliance,
|
||||||
|
|
@ -66,9 +66,12 @@ const ComplianceCard = ({
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<SpinnerIcon />
|
<SpinnerIcon />
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<Tooltip title={error}>
|
<TooltipProvider>
|
||||||
<span className="text-gray-400 text-sm">--</span>
|
<Tooltip>
|
||||||
</Tooltip>
|
<TooltipTrigger render={<span className="text-gray-400 text-sm" />}>--</TooltipTrigger>
|
||||||
|
<TooltipContent>{error}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
) : data?.compliant ? (
|
) : data?.compliant ? (
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useMemo } from "react";
|
import React, { useState, useMemo } from "react";
|
||||||
import { Tooltip } from "antd";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import PresidioDetectedEntities from "./PresidioDetectedEntities";
|
import PresidioDetectedEntities from "./PresidioDetectedEntities";
|
||||||
import BedrockGuardrailDetails, {
|
import BedrockGuardrailDetails, {
|
||||||
BedrockGuardrailResponse,
|
BedrockGuardrailResponse,
|
||||||
|
|
@ -517,13 +517,20 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{riskScore != null && success && (
|
{riskScore != null && success && (
|
||||||
<Tooltip title={`Risk score: ${riskScore}/10`}>
|
<TooltipProvider>
|
||||||
<span
|
<Tooltip>
|
||||||
className={`px-2 py-0.5 border rounded-sm text-[11px] font-semibold shrink-0 ${getRiskColor(riskScore)}`}
|
<TooltipTrigger
|
||||||
>
|
render={
|
||||||
Risk {riskScore}/10
|
<span
|
||||||
</span>
|
className={`px-2 py-0.5 border rounded-sm text-[11px] font-semibold shrink-0 ${getRiskColor(riskScore)}`}
|
||||||
</Tooltip>
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Risk {riskScore}/10
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{`Risk score: ${riskScore}/10`}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { Button, Space, Tag, Tooltip, Typography } from "antd";
|
import { useState } from "react";
|
||||||
import { CloseOutlined, UpOutlined, DownOutlined } from "@ant-design/icons";
|
import { Check, ChevronDown, ChevronUp, Copy, X } from "lucide-react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { LogEntry } from "../columns";
|
import { LogEntry } from "../columns";
|
||||||
import { AutoRouterTag } from "@/components/shared/table_cells";
|
import { AutoRouterTag } from "@/components/shared/table_cells";
|
||||||
import { ClassifyTag } from "./ClassifyTag";
|
import { ClassifyTag } from "./ClassifyTag";
|
||||||
|
|
@ -10,15 +13,11 @@ import {
|
||||||
COLOR_BORDER,
|
COLOR_BORDER,
|
||||||
COLOR_BACKGROUND,
|
COLOR_BACKGROUND,
|
||||||
SPACING_MEDIUM,
|
SPACING_MEDIUM,
|
||||||
SPACING_LARGE,
|
|
||||||
FONT_SIZE_HEADER,
|
FONT_SIZE_HEADER,
|
||||||
FONT_SIZE_MEDIUM,
|
FONT_SIZE_MEDIUM,
|
||||||
FONT_FAMILY_MONO,
|
FONT_FAMILY_MONO,
|
||||||
SPACING_SMALL,
|
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
interface DrawerHeaderProps {
|
interface DrawerHeaderProps {
|
||||||
log: LogEntry;
|
log: LogEntry;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|
@ -96,7 +95,7 @@ function ModelProviderSection({
|
||||||
providerName?: string;
|
providerName?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Space size={SPACING_MEDIUM} style={{ marginBottom: SPACING_MEDIUM }}>
|
<div className="flex items-center gap-2" style={{ marginBottom: SPACING_MEDIUM }}>
|
||||||
{providerLogo && (
|
{providerLogo && (
|
||||||
<img
|
<img
|
||||||
src={providerLogo}
|
src={providerLogo}
|
||||||
|
|
@ -108,19 +107,19 @@ function ModelProviderSection({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Space size={SPACING_MEDIUM} direction="horizontal">
|
<div className="flex items-center gap-2">
|
||||||
<Text strong style={{ fontSize: 14 }}>
|
<span className="font-semibold" style={{ fontSize: 14 }}>
|
||||||
{model}
|
{model}
|
||||||
</Text>
|
</span>
|
||||||
{providerName && (
|
{providerName && (
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
<span className="text-muted-foreground" style={{ fontSize: 12 }}>
|
||||||
{providerName}
|
{providerName}
|
||||||
</Text>
|
</span>
|
||||||
)}
|
)}
|
||||||
<AutoRouterTag modelGroup={modelGroup} />
|
<AutoRouterTag modelGroup={modelGroup} />
|
||||||
<ClassifyTag origin={internalCallOrigin} />
|
<ClassifyTag origin={internalCallOrigin} />
|
||||||
</Space>
|
</div>
|
||||||
</Space>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,24 +127,50 @@ function ModelProviderSection({
|
||||||
* Request ID display with copy functionality
|
* Request ID display with copy functionality
|
||||||
*/
|
*/
|
||||||
function RequestIdSection({ requestId }: { requestId: string }) {
|
function RequestIdSection({ requestId }: { requestId: string }) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(requestId);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1200);
|
||||||
|
} catch {
|
||||||
|
/* clipboard unavailable in non-secure contexts */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Tooltip title={requestId}>
|
<TooltipProvider>
|
||||||
<Text
|
<Tooltip>
|
||||||
strong
|
<TooltipTrigger
|
||||||
copyable={{ text: requestId, tooltips: ["Copy Request ID", "Copied!"] }}
|
render={
|
||||||
style={{
|
<span
|
||||||
fontSize: FONT_SIZE_HEADER,
|
className="font-semibold"
|
||||||
fontFamily: FONT_FAMILY_MONO,
|
style={{
|
||||||
overflow: "hidden",
|
fontSize: FONT_SIZE_HEADER,
|
||||||
textOverflow: "ellipsis",
|
fontFamily: FONT_FAMILY_MONO,
|
||||||
whiteSpace: "nowrap",
|
overflow: "hidden",
|
||||||
display: "block",
|
textOverflow: "ellipsis",
|
||||||
}}
|
whiteSpace: "nowrap",
|
||||||
>
|
display: "block",
|
||||||
{requestId}
|
}}
|
||||||
</Text>
|
/>
|
||||||
</Tooltip>
|
}
|
||||||
|
>
|
||||||
|
{requestId}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={copied ? "Copied!" : "Copy Request ID"}
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="ml-1 align-middle text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{requestId}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -172,21 +197,29 @@ function NavigationSection({
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
background: "#fafafa",
|
background: "#fafafa",
|
||||||
};
|
};
|
||||||
|
const splitStyle = { width: 1, height: 20, background: COLOR_BORDER };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space size={SPACING_SMALL} split={<div style={{ width: 1, height: 20, background: COLOR_BORDER }} />}>
|
<div className="flex items-center gap-1">
|
||||||
<Button type="text" size="small" onClick={onPrevious}>
|
<Button variant="ghost" size="sm" onClick={onPrevious}>
|
||||||
<UpOutlined />
|
<ChevronUp className="size-4" />
|
||||||
<span style={keyboardShortcutStyle}>K</span>
|
<span style={keyboardShortcutStyle}>K</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="text" size="small" onClick={onNext}>
|
<div style={splitStyle} />
|
||||||
<DownOutlined />
|
<Button variant="ghost" size="sm" onClick={onNext}>
|
||||||
|
<ChevronDown className="size-4" />
|
||||||
<span style={keyboardShortcutStyle}>J</span>
|
<span style={keyboardShortcutStyle}>J</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip title="ESC to close">
|
<div style={splitStyle} />
|
||||||
<Button type="text" icon={<CloseOutlined />} onClick={onClose} />
|
<TooltipProvider>
|
||||||
</Tooltip>
|
<Tooltip>
|
||||||
</Space>
|
<TooltipTrigger render={<Button variant="ghost" size="icon-sm" onClick={onClose} />}>
|
||||||
|
<X className="size-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>ESC to close</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,17 +238,17 @@ function StatusBar({
|
||||||
environment: string;
|
environment: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Space size={SPACING_LARGE}>
|
<div className="flex items-center gap-3">
|
||||||
<Tag color={statusColor}>{statusLabel}</Tag>
|
<Badge variant={statusColor === "error" ? "destructive" : "secondary"}>{statusLabel}</Badge>
|
||||||
<Tag>Env: {environment}</Tag>
|
<Badge variant="outline">Env: {environment}</Badge>
|
||||||
<Space size={SPACING_MEDIUM}>
|
<div className="flex items-center gap-2">
|
||||||
<Text type="secondary" style={{ fontSize: FONT_SIZE_MEDIUM }}>
|
<span className="text-muted-foreground" style={{ fontSize: FONT_SIZE_MEDIUM }}>
|
||||||
{moment(log.startTime).format("MMM D, YYYY h:mm:ss A")}
|
{moment(log.startTime).format("MMM D, YYYY h:mm:ss A")}
|
||||||
</Text>
|
</span>
|
||||||
<Text type="secondary" style={{ fontSize: FONT_SIZE_MEDIUM }}>
|
<span className="text-muted-foreground" style={{ fontSize: FONT_SIZE_MEDIUM }}>
|
||||||
({moment(log.startTime).fromNow()})
|
({moment(log.startTime).fromNow()})
|
||||||
</Text>
|
</span>
|
||||||
</Space>
|
</div>
|
||||||
</Space>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -177,12 +177,19 @@ describe("LogDetailContent", () => {
|
||||||
expect(screen.getByText("Loading request & response data...")).toBeInTheDocument();
|
expect(screen.getByText("Loading request & response data...")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display Request & Response section with Pretty and JSON view modes", () => {
|
it("should switch the Request & Response body between the Pretty and JSON view modes", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
render(<LogDetailContent logEntry={createLogEntry()} />);
|
render(<LogDetailContent logEntry={createLogEntry()} />);
|
||||||
|
|
||||||
expect(screen.getByText("Request & Response")).toBeInTheDocument();
|
expect(screen.getByText("Request & Response")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("radio", { name: "Pretty" })).toBeInTheDocument();
|
expect(screen.getByText("Pretty")).toBeInTheDocument();
|
||||||
expect(screen.getByRole("radio", { name: "JSON" })).toBeInTheDocument();
|
expect(screen.getByText("JSON")).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(screen.getByText("JSON"));
|
||||||
|
expect(screen.getByRole("tab", { name: "Request" })).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(screen.getByText("Pretty"));
|
||||||
|
expect(screen.queryByRole("tab", { name: "Request" })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display Request and Response tabs when JSON view is selected", async () => {
|
it("should display Request and Response tabs when JSON view is selected", async () => {
|
||||||
|
|
@ -259,7 +266,7 @@ describe("LogDetailContent", () => {
|
||||||
render(<LogDetailContent logEntry={createLogEntry({ cache_hit: "True" })} />);
|
render(<LogDetailContent logEntry={createLogEntry({ cache_hit: "True" })} />);
|
||||||
|
|
||||||
expect(screen.getByText("Response Cache")).toBeInTheDocument();
|
expect(screen.getByText("Response Cache")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Hit").closest(".ant-tag")).toHaveClass("ant-tag-green");
|
expect(screen.getByText("Hit").className).toMatch(/green/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should show prompt cache tokens without an alarming red tag when only provider prompt caching occurred", () => {
|
it("should show prompt cache tokens without an alarming red tag when only provider prompt caching occurred", () => {
|
||||||
|
|
@ -282,7 +289,7 @@ describe("LogDetailContent", () => {
|
||||||
expect(screen.getByText("34,462")).toBeInTheDocument();
|
expect(screen.getByText("34,462")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Prompt Cache Creation Tokens")).toBeInTheDocument();
|
expect(screen.getByText("Prompt Cache Creation Tokens")).toBeInTheDocument();
|
||||||
expect(screen.getByText("83")).toBeInTheDocument();
|
expect(screen.getByText("83")).toBeInTheDocument();
|
||||||
expect(screen.getByText("Miss").closest(".ant-tag")).not.toHaveClass("ant-tag-red");
|
expect(screen.getByText("Miss").className).not.toMatch(/red|destructive/);
|
||||||
expect(screen.queryByText("Cache Hit")).not.toBeInTheDocument();
|
expect(screen.queryByText("Cache Hit")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -310,8 +317,12 @@ describe("LogDetailContent", () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<LogDetailContent logEntry={createLogEntry({ cache_hit: "True" })} />);
|
render(<LogDetailContent logEntry={createLogEntry({ cache_hit: "True" })} />);
|
||||||
|
|
||||||
const label = screen.getByText("Response Cache").closest(".ant-space") as HTMLElement;
|
expect(screen.getByText("Response Cache")).toBeInTheDocument();
|
||||||
await user.hover(within(label).getByRole("img", { name: "info-circle" }));
|
// Response Cache is the only metric with an info tooltip in this fixture, so an
|
||||||
|
// unscoped lookup still pins the docs link to that label.
|
||||||
|
const infoIcons = screen.getAllByRole("img", { name: /info/i });
|
||||||
|
expect(infoIcons).toHaveLength(1);
|
||||||
|
await user.hover(infoIcons[0]);
|
||||||
|
|
||||||
expect(await screen.findByRole("link", { name: "Docs" })).toHaveAttribute(
|
expect(await screen.findByRole("link", { name: "Docs" })).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
|
|
@ -333,8 +344,11 @@ describe("LogDetailContent", () => {
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const label = screen.getByText("Prompt Cache Read Tokens").closest(".ant-space") as HTMLElement;
|
expect(screen.getByText("Prompt Cache Read Tokens")).toBeInTheDocument();
|
||||||
await user.hover(within(label).getByRole("img", { name: "info-circle" }));
|
// Prompt Cache Read Tokens is the only metric with an info tooltip in this fixture.
|
||||||
|
const infoIcons = screen.getAllByRole("img", { name: /info/i });
|
||||||
|
expect(infoIcons).toHaveLength(1);
|
||||||
|
await user.hover(infoIcons[0]);
|
||||||
|
|
||||||
expect(await screen.findByRole("link", { name: "Docs" })).toHaveAttribute(
|
expect(await screen.findByRole("link", { name: "Docs" })).toHaveAttribute(
|
||||||
"href",
|
"href",
|
||||||
|
|
@ -370,7 +384,7 @@ describe("LogDetailContent", () => {
|
||||||
expect(screen.queryByText("LiteLLM Overhead")).not.toBeInTheDocument();
|
expect(screen.queryByText("LiteLLM Overhead")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
const retriesItem = () => screen.getByText("Retries").closest(".ant-descriptions-item") as HTMLElement;
|
const retriesItem = () => screen.getByText("Retries").parentElement as HTMLElement;
|
||||||
|
|
||||||
it("should display attempted_retries / max_retries for Retries when attempted_retries > 0", () => {
|
it("should display attempted_retries / max_retries for Retries when attempted_retries > 0", () => {
|
||||||
render(
|
render(
|
||||||
|
|
@ -386,7 +400,7 @@ describe("LogDetailContent", () => {
|
||||||
render(<LogDetailContent logEntry={createLogEntry({ metadata: { status: "success", attempted_retries: 0 } })} />);
|
render(<LogDetailContent logEntry={createLogEntry({ metadata: { status: "success", attempted_retries: 0 } })} />);
|
||||||
|
|
||||||
const noneTag = within(retriesItem()).getByText("None");
|
const noneTag = within(retriesItem()).getByText("None");
|
||||||
expect(noneTag.closest(".ant-tag")).toHaveClass("ant-tag-green");
|
expect(noneTag.className).toMatch(/green/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display '-' for Retries when attempted_retries is absent from metadata", () => {
|
it("should display '-' for Retries when attempted_retries is absent from metadata", () => {
|
||||||
|
|
@ -444,8 +458,8 @@ describe("LogDetailContent", () => {
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const descriptions = screen.getByText("Provider").closest(".ant-descriptions-item");
|
const descriptions = screen.getByText("Provider").parentElement as HTMLElement;
|
||||||
expect(descriptions).toBeInTheDocument();
|
expect(descriptions).toBeInTheDocument();
|
||||||
expect(within(descriptions as HTMLElement).getByText("-")).toBeInTheDocument();
|
expect(within(descriptions).getByText("-")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Typography, Descriptions, Card, Tag, Tabs, Alert, Collapse, Radio, Space, Spin, Tooltip } from "antd";
|
import { Check, ChevronDown, ChevronRight, CircleAlert, Copy, Info } from "lucide-react";
|
||||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||||
import { LogEntry } from "../columns";
|
import { LogEntry } from "../columns";
|
||||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||||
import { PROMPT_CACHE_CREATION_TOOLTIP, PROMPT_CACHE_READ_TOOLTIP } from "@/utils/promptCacheUsage";
|
import { PROMPT_CACHE_CREATION_TOOLTIP, PROMPT_CACHE_READ_TOOLTIP } from "@/utils/promptCacheUsage";
|
||||||
|
|
@ -32,13 +38,10 @@ import {
|
||||||
FONT_SIZE_SMALL,
|
FONT_SIZE_SMALL,
|
||||||
FONT_FAMILY_MONO,
|
FONT_FAMILY_MONO,
|
||||||
SPACING_XLARGE,
|
SPACING_XLARGE,
|
||||||
SPACING_MEDIUM,
|
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { ToolsSection } from "../ToolsSection";
|
import { ToolsSection } from "../ToolsSection";
|
||||||
import { PrettyMessagesView } from "./PrettyMessagesView";
|
import { PrettyMessagesView } from "./PrettyMessagesView";
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
export interface LogDetailContentProps {
|
export interface LogDetailContentProps {
|
||||||
logEntry: LogEntry;
|
logEntry: LogEntry;
|
||||||
/** When true, log details (messages/response) are still being lazy-loaded. */
|
/** When true, log details (messages/response) are still being lazy-loaded. */
|
||||||
|
|
@ -100,13 +103,16 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
|
||||||
<div style={{ padding: `${DRAWER_CONTENT_PADDING} ${DRAWER_CONTENT_PADDING} 0` }}>
|
<div style={{ padding: `${DRAWER_CONTENT_PADDING} ${DRAWER_CONTENT_PADDING} 0` }}>
|
||||||
{/* Error Alert */}
|
{/* Error Alert */}
|
||||||
{hasError && errorInfo && (
|
{hasError && errorInfo && (
|
||||||
<Alert
|
<div
|
||||||
type="error"
|
role="alert"
|
||||||
showIcon
|
className="mb-6 flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm"
|
||||||
message="Request Failed"
|
>
|
||||||
description={<ErrorDescription errorInfo={errorInfo} />}
|
<CircleAlert className="size-4 shrink-0 text-destructive" />
|
||||||
className="mb-6"
|
<div>
|
||||||
/>
|
<div className="font-medium text-destructive">Request Failed</div>
|
||||||
|
<ErrorDescription errorInfo={errorInfo} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
|
|
@ -116,26 +122,31 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
|
||||||
|
|
||||||
{/* Request Details */}
|
{/* Request Details */}
|
||||||
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
|
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
|
||||||
<Card title="Request Details" size="small" bordered={false} style={{ marginBottom: 0 }}>
|
<Card size="sm" style={{ marginBottom: 0 }}>
|
||||||
<Descriptions column={2} size="small">
|
<CardHeader>
|
||||||
<Descriptions.Item label="Model">{logEntry.model}</Descriptions.Item>
|
<CardTitle>Request Details</CardTitle>
|
||||||
<Descriptions.Item label="Provider">{logEntry.custom_llm_provider || "-"}</Descriptions.Item>
|
</CardHeader>
|
||||||
<Descriptions.Item label="Call Type">{logEntry.call_type}</Descriptions.Item>
|
<CardContent>
|
||||||
<Descriptions.Item label="Model ID">
|
<DescriptionList>
|
||||||
<TruncatedValue value={logEntry.model_id} />
|
<DescriptionItem label="Model">{logEntry.model}</DescriptionItem>
|
||||||
</Descriptions.Item>
|
<DescriptionItem label="Provider">{logEntry.custom_llm_provider || "-"}</DescriptionItem>
|
||||||
<Descriptions.Item label="API Base">
|
<DescriptionItem label="Call Type">{logEntry.call_type}</DescriptionItem>
|
||||||
<TruncatedValue value={logEntry.api_base} maxWidth={API_BASE_MAX_WIDTH} />
|
<DescriptionItem label="Model ID">
|
||||||
</Descriptions.Item>
|
<TruncatedValue value={logEntry.model_id} />
|
||||||
{logEntry.requester_ip_address && (
|
</DescriptionItem>
|
||||||
<Descriptions.Item label="IP Address">{logEntry.requester_ip_address}</Descriptions.Item>
|
<DescriptionItem label="API Base">
|
||||||
)}
|
<TruncatedValue value={logEntry.api_base} maxWidth={API_BASE_MAX_WIDTH} />
|
||||||
{hasGuardrailData && (
|
</DescriptionItem>
|
||||||
<Descriptions.Item label="Guardrail">
|
{logEntry.requester_ip_address && (
|
||||||
<GuardrailLabel label={primaryGuardrailLabel} maskedCount={totalMaskedEntities} />
|
<DescriptionItem label="IP Address">{logEntry.requester_ip_address}</DescriptionItem>
|
||||||
</Descriptions.Item>
|
)}
|
||||||
)}
|
{hasGuardrailData && (
|
||||||
</Descriptions>
|
<DescriptionItem label="Guardrail">
|
||||||
|
<GuardrailLabel label={primaryGuardrailLabel} maskedCount={totalMaskedEntities} />
|
||||||
|
</DescriptionItem>
|
||||||
|
)}
|
||||||
|
</DescriptionList>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -170,7 +181,7 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
|
||||||
{/* Request/Response JSON */}
|
{/* Request/Response JSON */}
|
||||||
{isLoadingDetails ? (
|
{isLoadingDetails ? (
|
||||||
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6 p-8 text-center">
|
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6 p-8 text-center">
|
||||||
<Spin size="default" />
|
<UiLoadingSpinner className="inline-block size-5" />
|
||||||
<div style={{ marginTop: 8, color: "#999" }}>Loading request & response data...</div>
|
<div style={{ marginTop: 8, color: "#999" }}>Loading request & response data...</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -221,17 +232,64 @@ export function LogDetailContent({ logEntry, isLoadingDetails = false, accessTok
|
||||||
// Helper Components
|
// Helper Components
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
function DescriptionList({ children }: { children: React.ReactNode }) {
|
||||||
|
return <div className="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DescriptionItem({ label, children }: { label: React.ReactNode; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-w-0 flex-wrap items-start gap-x-2 gap-y-0.5">
|
||||||
|
<span className="shrink-0 text-muted-foreground after:content-[':']">{label}</span>
|
||||||
|
<span className="min-w-0 break-words">{children}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CopyButton({
|
||||||
|
getText,
|
||||||
|
label,
|
||||||
|
disabled = false,
|
||||||
|
}: {
|
||||||
|
getText: () => string;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(getText());
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1200);
|
||||||
|
} catch {
|
||||||
|
/* clipboard unavailable in non-secure contexts */
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={handleCopy}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-label={copied ? "Copied!" : label}
|
||||||
|
>
|
||||||
|
{copied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ErrorDescription({ errorInfo }: { errorInfo: any }) {
|
function ErrorDescription({ errorInfo }: { errorInfo: any }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{errorInfo.error_code && (
|
{errorInfo.error_code && (
|
||||||
<div>
|
<div>
|
||||||
<Text strong>Error Code:</Text> {errorInfo.error_code}
|
<span className="font-semibold">Error Code:</span> {errorInfo.error_code}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{errorInfo.error_message && (
|
{errorInfo.error_message && (
|
||||||
<div>
|
<div>
|
||||||
<Text strong>Message:</Text> {errorInfo.error_message}
|
<span className="font-semibold">Message:</span> {errorInfo.error_message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -241,16 +299,16 @@ function ErrorDescription({ errorInfo }: { errorInfo: any }) {
|
||||||
function TagsSection({ tags }: { tags: Record<string, any> }) {
|
function TagsSection({ tags }: { tags: Record<string, any> }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden p-4 mb-6">
|
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden p-4 mb-6">
|
||||||
<Text strong style={{ display: "block", marginBottom: 8, fontSize: 16 }}>
|
<span className="font-semibold" style={{ display: "block", marginBottom: 8, fontSize: 16 }}>
|
||||||
Tags
|
Tags
|
||||||
</Text>
|
</span>
|
||||||
<Space size={SPACING_MEDIUM} wrap>
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{Object.entries(tags).map(([key, value]) => (
|
{Object.entries(tags).map(([key, value]) => (
|
||||||
<Tag key={key}>
|
<Badge key={key} variant="outline">
|
||||||
{key}: {String(value)}
|
{key}: {String(value)}
|
||||||
</Tag>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</Space>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -262,12 +320,12 @@ function GuardrailLabel({ label, maskedCount }: { label: string; maskedCount: nu
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space size={SPACING_MEDIUM}>
|
<span className="inline-flex items-center gap-2">
|
||||||
<a onClick={handleClick} style={{ cursor: "pointer" }}>
|
<a onClick={handleClick} style={{ cursor: "pointer" }}>
|
||||||
{label}
|
{label}
|
||||||
</a>
|
</a>
|
||||||
{maskedCount > 0 && <Tag color="blue">{maskedCount} masked</Tag>}
|
{maskedCount > 0 && <Badge variant="secondary">{maskedCount} masked</Badge>}
|
||||||
</Space>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,26 +349,24 @@ const PROMPT_CACHE_DOCS_URL = "https://docs.litellm.ai/docs/completion/prompt_ca
|
||||||
|
|
||||||
function MetricLabel({ label, tooltip, docsUrl }: { label: string; tooltip: string; docsUrl: string }) {
|
function MetricLabel({ label, tooltip, docsUrl }: { label: string; tooltip: string; docsUrl: string }) {
|
||||||
return (
|
return (
|
||||||
<Space size={4}>
|
<span className="inline-flex items-center gap-1">
|
||||||
{label}
|
{label}
|
||||||
<Tooltip
|
<TooltipProvider>
|
||||||
title={
|
<Tooltip>
|
||||||
<>
|
<TooltipTrigger
|
||||||
|
render={<span role="img" aria-label={`${label} info`} className="inline-flex text-muted-foreground" />}
|
||||||
|
>
|
||||||
|
<Info className="size-3.5" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
{tooltip}{" "}
|
{tooltip}{" "}
|
||||||
<a
|
<a href={docsUrl} target="_blank" rel="noreferrer" className="underline">
|
||||||
href={docsUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
style={{ color: "#91caff", textDecoration: "underline" }}
|
|
||||||
>
|
|
||||||
Docs
|
Docs
|
||||||
</a>
|
</a>
|
||||||
</>
|
</TooltipContent>
|
||||||
}
|
</Tooltip>
|
||||||
>
|
</TooltipProvider>
|
||||||
<InfoCircleOutlined style={{ color: "#8c8c8c" }} />
|
</span>
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,102 +389,111 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
|
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
|
||||||
<Card title="Metrics" size="small" style={{ marginBottom: 0 }}>
|
<Card size="sm" style={{ marginBottom: 0 }}>
|
||||||
<Descriptions column={2} size="small">
|
<CardHeader>
|
||||||
{showAnthropicMessagesInputOutput ? (
|
<CardTitle>Metrics</CardTitle>
|
||||||
<>
|
</CardHeader>
|
||||||
<Descriptions.Item label="Input Tokens">{formatNumberWithCommas(uncachedInputTokens)}</Descriptions.Item>
|
<CardContent>
|
||||||
<Descriptions.Item label="Output Tokens">
|
<DescriptionList>
|
||||||
{formatNumberWithCommas(logEntry.completion_tokens)}
|
{showAnthropicMessagesInputOutput ? (
|
||||||
</Descriptions.Item>
|
<>
|
||||||
</>
|
<DescriptionItem label="Input Tokens">{formatNumberWithCommas(uncachedInputTokens)}</DescriptionItem>
|
||||||
) : (
|
<DescriptionItem label="Output Tokens">
|
||||||
<Descriptions.Item label="Tokens">
|
{formatNumberWithCommas(logEntry.completion_tokens)}
|
||||||
<TokenFlow
|
</DescriptionItem>
|
||||||
prompt={logEntry.prompt_tokens}
|
</>
|
||||||
completion={logEntry.completion_tokens}
|
|
||||||
total={logEntry.total_tokens}
|
|
||||||
/>
|
|
||||||
</Descriptions.Item>
|
|
||||||
)}
|
|
||||||
<Descriptions.Item label="Cost">${formatNumberWithCommas(logEntry.spend || 0, 8)}</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="Duration">
|
|
||||||
{logEntry.request_duration_ms != null ? (logEntry.request_duration_ms / 1000).toFixed(3) : "-"} s
|
|
||||||
</Descriptions.Item>
|
|
||||||
{ttftMs != null && ttftMs > 0 && (
|
|
||||||
<Descriptions.Item label="Time to First Token">{(ttftMs / 1000).toFixed(3)} s</Descriptions.Item>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showResponseCache && (
|
|
||||||
<Descriptions.Item
|
|
||||||
label={
|
|
||||||
<MetricLabel
|
|
||||||
label="Response Cache"
|
|
||||||
tooltip={RESPONSE_CACHE_TOOLTIP}
|
|
||||||
docsUrl={RESPONSE_CACHE_DOCS_URL}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Tag color={isResponseCacheHit ? "green" : "default"}>{isResponseCacheHit ? "Hit" : "Miss"}</Tag>
|
|
||||||
</Descriptions.Item>
|
|
||||||
)}
|
|
||||||
{promptCacheReadTokens > 0 && (
|
|
||||||
<Descriptions.Item
|
|
||||||
label={
|
|
||||||
<MetricLabel
|
|
||||||
label="Prompt Cache Read Tokens"
|
|
||||||
tooltip={PROMPT_CACHE_READ_TOOLTIP}
|
|
||||||
docsUrl={PROMPT_CACHE_DOCS_URL}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{formatNumberWithCommas(promptCacheReadTokens)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
)}
|
|
||||||
{promptCacheCreationTokens > 0 && (
|
|
||||||
<Descriptions.Item
|
|
||||||
label={
|
|
||||||
<MetricLabel
|
|
||||||
label="Prompt Cache Creation Tokens"
|
|
||||||
tooltip={PROMPT_CACHE_CREATION_TOOLTIP}
|
|
||||||
docsUrl={PROMPT_CACHE_DOCS_URL}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{formatNumberWithCommas(promptCacheCreationTokens)}
|
|
||||||
</Descriptions.Item>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{metadata?.litellm_overhead_time_ms !== undefined && metadata.litellm_overhead_time_ms !== null && (
|
|
||||||
<Descriptions.Item label="LiteLLM Overhead">
|
|
||||||
{metadata.litellm_overhead_time_ms.toFixed(2)} ms
|
|
||||||
</Descriptions.Item>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Descriptions.Item label="Retries">
|
|
||||||
{metadata?.attempted_retries !== undefined && metadata?.attempted_retries !== null ? (
|
|
||||||
metadata.attempted_retries > 0 ? (
|
|
||||||
<>
|
|
||||||
{metadata.attempted_retries}
|
|
||||||
{metadata.max_retries !== undefined && metadata.max_retries !== null
|
|
||||||
? ` / ${metadata.max_retries}`
|
|
||||||
: ""}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Tag color="green">None</Tag>
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
"-"
|
<DescriptionItem label="Tokens">
|
||||||
|
<TokenFlow
|
||||||
|
prompt={logEntry.prompt_tokens}
|
||||||
|
completion={logEntry.completion_tokens}
|
||||||
|
total={logEntry.total_tokens}
|
||||||
|
/>
|
||||||
|
</DescriptionItem>
|
||||||
|
)}
|
||||||
|
<DescriptionItem label="Cost">${formatNumberWithCommas(logEntry.spend || 0, 8)}</DescriptionItem>
|
||||||
|
<DescriptionItem label="Duration">
|
||||||
|
{logEntry.request_duration_ms != null ? (logEntry.request_duration_ms / 1000).toFixed(3) : "-"} s
|
||||||
|
</DescriptionItem>
|
||||||
|
{ttftMs != null && ttftMs > 0 && (
|
||||||
|
<DescriptionItem label="Time to First Token">{(ttftMs / 1000).toFixed(3)} s</DescriptionItem>
|
||||||
)}
|
)}
|
||||||
</Descriptions.Item>
|
|
||||||
|
|
||||||
<Descriptions.Item label="Start Time">
|
{showResponseCache && (
|
||||||
{moment(logEntry.startTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")}
|
<DescriptionItem
|
||||||
</Descriptions.Item>
|
label={
|
||||||
<Descriptions.Item label="End Time">
|
<MetricLabel
|
||||||
{moment(logEntry.endTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")}
|
label="Response Cache"
|
||||||
</Descriptions.Item>
|
tooltip={RESPONSE_CACHE_TOOLTIP}
|
||||||
</Descriptions>
|
docsUrl={RESPONSE_CACHE_DOCS_URL}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Badge variant="secondary" className={isResponseCacheHit ? "bg-green-100 text-green-700" : undefined}>
|
||||||
|
{isResponseCacheHit ? "Hit" : "Miss"}
|
||||||
|
</Badge>
|
||||||
|
</DescriptionItem>
|
||||||
|
)}
|
||||||
|
{promptCacheReadTokens > 0 && (
|
||||||
|
<DescriptionItem
|
||||||
|
label={
|
||||||
|
<MetricLabel
|
||||||
|
label="Prompt Cache Read Tokens"
|
||||||
|
tooltip={PROMPT_CACHE_READ_TOOLTIP}
|
||||||
|
docsUrl={PROMPT_CACHE_DOCS_URL}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{formatNumberWithCommas(promptCacheReadTokens)}
|
||||||
|
</DescriptionItem>
|
||||||
|
)}
|
||||||
|
{promptCacheCreationTokens > 0 && (
|
||||||
|
<DescriptionItem
|
||||||
|
label={
|
||||||
|
<MetricLabel
|
||||||
|
label="Prompt Cache Creation Tokens"
|
||||||
|
tooltip={PROMPT_CACHE_CREATION_TOOLTIP}
|
||||||
|
docsUrl={PROMPT_CACHE_DOCS_URL}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{formatNumberWithCommas(promptCacheCreationTokens)}
|
||||||
|
</DescriptionItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{metadata?.litellm_overhead_time_ms !== undefined && metadata.litellm_overhead_time_ms !== null && (
|
||||||
|
<DescriptionItem label="LiteLLM Overhead">
|
||||||
|
{metadata.litellm_overhead_time_ms.toFixed(2)} ms
|
||||||
|
</DescriptionItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DescriptionItem label="Retries">
|
||||||
|
{metadata?.attempted_retries !== undefined && metadata?.attempted_retries !== null ? (
|
||||||
|
metadata.attempted_retries > 0 ? (
|
||||||
|
<>
|
||||||
|
{metadata.attempted_retries}
|
||||||
|
{metadata.max_retries !== undefined && metadata.max_retries !== null
|
||||||
|
? ` / ${metadata.max_retries}`
|
||||||
|
: ""}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Badge variant="secondary" className="bg-green-100 text-green-700">
|
||||||
|
None
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
"-"
|
||||||
|
)}
|
||||||
|
</DescriptionItem>
|
||||||
|
|
||||||
|
<DescriptionItem label="Start Time">
|
||||||
|
{moment(logEntry.startTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")}
|
||||||
|
</DescriptionItem>
|
||||||
|
<DescriptionItem label="End Time">
|
||||||
|
{moment(logEntry.endTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")}
|
||||||
|
</DescriptionItem>
|
||||||
|
</DescriptionList>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -449,6 +514,7 @@ function RequestResponseSection({
|
||||||
getFormattedResponse,
|
getFormattedResponse,
|
||||||
logEntry,
|
logEntry,
|
||||||
}: RequestResponseSectionProps) {
|
}: RequestResponseSectionProps) {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
const [activeTab, setActiveTab] = useState<typeof TAB_REQUEST | typeof TAB_RESPONSE>(TAB_REQUEST);
|
const [activeTab, setActiveTab] = useState<typeof TAB_REQUEST | typeof TAB_RESPONSE>(TAB_REQUEST);
|
||||||
const [viewMode, setViewMode] = useState<"pretty" | "json">("pretty");
|
const [viewMode, setViewMode] = useState<"pretty" | "json">("pretty");
|
||||||
|
|
||||||
|
|
@ -476,90 +542,76 @@ function RequestResponseSection({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
|
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
|
||||||
<Collapse
|
<Collapsible open={open} onOpenChange={setOpen}>
|
||||||
defaultActiveKey={["1"]}
|
<Tabs value={viewMode} onValueChange={(value) => setViewMode(value as "pretty" | "json")}>
|
||||||
expandIconPosition="start"
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }}>
|
||||||
items={[
|
<CollapsibleTrigger className="flex flex-1 items-center gap-3 px-4 py-3 text-left">
|
||||||
{
|
{open ? (
|
||||||
key: "1",
|
<ChevronDown className="size-3.5 shrink-0 text-gray-500" />
|
||||||
label: (
|
) : (
|
||||||
<div
|
<ChevronRight className="size-3.5 shrink-0 text-gray-500" />
|
||||||
style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }}
|
)}
|
||||||
onClick={(e) => {
|
<h3 className="text-lg font-medium text-gray-900" style={{ margin: 0 }}>
|
||||||
const target = e.target as HTMLElement;
|
Request & Response
|
||||||
if (target.closest(".ant-radio-group")) {
|
</h3>
|
||||||
e.stopPropagation();
|
</CollapsibleTrigger>
|
||||||
}
|
<TabsList className="mr-4">
|
||||||
}}
|
<TabsTrigger value="pretty">Pretty</TabsTrigger>
|
||||||
>
|
<TabsTrigger value="json">JSON</TabsTrigger>
|
||||||
<h3 className="text-lg font-medium text-gray-900" style={{ margin: 0 }}>
|
</TabsList>
|
||||||
Request & Response
|
</div>
|
||||||
</h3>
|
<CollapsibleContent>
|
||||||
<Radio.Group size="small" value={viewMode} onChange={(e) => setViewMode(e.target.value)}>
|
<div>
|
||||||
<Radio.Button value="pretty">Pretty</Radio.Button>
|
<TabsContent value="pretty">
|
||||||
<Radio.Button value="json">JSON</Radio.Button>
|
<PrettyMessagesView
|
||||||
</Radio.Group>
|
request={getRawRequest()}
|
||||||
</div>
|
response={getFormattedResponse()}
|
||||||
),
|
metrics={{
|
||||||
children: (
|
prompt_tokens: promptTokens,
|
||||||
<div>
|
completion_tokens: completionTokens,
|
||||||
{viewMode === "pretty" ? (
|
input_cost: inputCost,
|
||||||
<PrettyMessagesView
|
output_cost: outputCost,
|
||||||
request={getRawRequest()}
|
}}
|
||||||
response={getFormattedResponse()}
|
/>
|
||||||
metrics={{
|
</TabsContent>
|
||||||
prompt_tokens: promptTokens,
|
<TabsContent value="json">
|
||||||
completion_tokens: completionTokens,
|
<Tabs
|
||||||
input_cost: inputCost,
|
value={activeTab}
|
||||||
output_cost: outputCost,
|
onValueChange={(key) => setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)}
|
||||||
}}
|
>
|
||||||
/>
|
<div className="flex items-center justify-between">
|
||||||
) : (
|
<TabsList>
|
||||||
<Tabs
|
<TabsTrigger value={TAB_REQUEST}>Request</TabsTrigger>
|
||||||
activeKey={activeTab}
|
<TabsTrigger value={TAB_RESPONSE}>Response</TabsTrigger>
|
||||||
onChange={(key) => setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)}
|
</TabsList>
|
||||||
tabBarExtraContent={
|
<CopyButton
|
||||||
<Text
|
getText={getCopyText}
|
||||||
copyable={{
|
label="Copy JSON"
|
||||||
text: getCopyText(),
|
disabled={activeTab === TAB_RESPONSE && !hasResponse && !hasError}
|
||||||
tooltips: ["Copy JSON", "Copied!"],
|
/>
|
||||||
}}
|
</div>
|
||||||
disabled={activeTab === TAB_RESPONSE && !hasResponse && !hasError}
|
<TabsContent value={TAB_REQUEST}>
|
||||||
/>
|
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
|
||||||
}
|
<JsonViewer data={getRawRequest()} mode="formatted" />
|
||||||
items={[
|
</div>
|
||||||
{
|
</TabsContent>
|
||||||
key: TAB_REQUEST,
|
<TabsContent value={TAB_RESPONSE}>
|
||||||
label: "Request",
|
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
|
||||||
children: (
|
{hasResponse || hasError ? (
|
||||||
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
|
<JsonViewer data={getFormattedResponse()} mode="formatted" />
|
||||||
<JsonViewer data={getRawRequest()} mode="formatted" />
|
) : (
|
||||||
</div>
|
<div style={{ textAlign: "center", padding: 20, color: "#999", fontStyle: "italic" }}>
|
||||||
),
|
Response data not available
|
||||||
},
|
</div>
|
||||||
{
|
)}
|
||||||
key: TAB_RESPONSE,
|
</div>
|
||||||
label: "Response",
|
</TabsContent>
|
||||||
children: (
|
</Tabs>
|
||||||
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
|
</TabsContent>
|
||||||
{hasResponse || hasError ? (
|
</div>
|
||||||
<JsonViewer data={getFormattedResponse()} mode="formatted" />
|
</CollapsibleContent>
|
||||||
) : (
|
</Tabs>
|
||||||
<div style={{ textAlign: "center", padding: 20, color: "#999", fontStyle: "italic" }}>
|
</Collapsible>
|
||||||
Response data not available
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -602,43 +654,40 @@ export function GuardrailJumpLink({ guardrailEntries }: { guardrailEntries: any[
|
||||||
}
|
}
|
||||||
|
|
||||||
function MetadataSection({ metadata }: { metadata: Record<string, any> }) {
|
function MetadataSection({ metadata }: { metadata: Record<string, any> }) {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
|
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
|
||||||
<Collapse
|
<Collapsible open={open} onOpenChange={setOpen}>
|
||||||
defaultActiveKey={["1"]}
|
<CollapsibleTrigger className="flex w-full items-center gap-3 px-4 py-3 text-left">
|
||||||
expandIconPosition="start"
|
{open ? (
|
||||||
items={[
|
<ChevronDown className="size-3.5 shrink-0 text-gray-500" />
|
||||||
{
|
) : (
|
||||||
key: "1",
|
<ChevronRight className="size-3.5 shrink-0 text-gray-500" />
|
||||||
label: <h3 className="text-lg font-medium text-gray-900">Metadata</h3>,
|
)}
|
||||||
children: (
|
<h3 className="text-lg font-medium text-gray-900">Metadata</h3>
|
||||||
<div>
|
</CollapsibleTrigger>
|
||||||
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 8 }}>
|
<CollapsibleContent>
|
||||||
<Text
|
<div>
|
||||||
copyable={{
|
<div style={{ display: "flex", justifyContent: "flex-end", marginBottom: 8 }}>
|
||||||
text: JSON.stringify(metadata, null, 2),
|
<CopyButton getText={() => JSON.stringify(metadata, null, 2)} label="Copy Metadata" />
|
||||||
tooltips: ["Copy Metadata", "Copied!"],
|
</div>
|
||||||
}}
|
<pre
|
||||||
/>
|
style={{
|
||||||
</div>
|
maxHeight: METADATA_MAX_HEIGHT,
|
||||||
<pre
|
overflowY: "auto",
|
||||||
style={{
|
fontSize: FONT_SIZE_SMALL,
|
||||||
maxHeight: METADATA_MAX_HEIGHT,
|
fontFamily: FONT_FAMILY_MONO,
|
||||||
overflowY: "auto",
|
whiteSpace: "pre-wrap",
|
||||||
fontSize: FONT_SIZE_SMALL,
|
wordBreak: "break-all",
|
||||||
fontFamily: FONT_FAMILY_MONO,
|
margin: 0,
|
||||||
whiteSpace: "pre-wrap",
|
}}
|
||||||
wordBreak: "break-all",
|
>
|
||||||
margin: 0,
|
{JSON.stringify(metadata, null, 2)}
|
||||||
}}
|
</pre>
|
||||||
>
|
</div>
|
||||||
{JSON.stringify(metadata, null, 2)}
|
</CollapsibleContent>
|
||||||
</pre>
|
</Collapsible>
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Button, Drawer, Segmented } from "antd";
|
import { Bot, Check, ChevronLeft, ChevronRight, Copy, Sparkles, Wrench } from "lucide-react";
|
||||||
import { CheckOutlined, CopyOutlined, LeftOutlined, RightOutlined } from "@ant-design/icons";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Bot, Sparkles, Wrench } from "lucide-react";
|
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { LogEntry } from "../columns";
|
import { LogEntry } from "../columns";
|
||||||
import { AutoRouterIcon, useIsAutoRoutedModelGroup } from "@/components/shared/table_cells";
|
import { AutoRouterIcon, useIsAutoRoutedModelGroup } from "@/components/shared/table_cells";
|
||||||
import { AGENT_CALL_TYPES, MCP_CALL_TYPES } from "../constants";
|
import { AGENT_CALL_TYPES, MCP_CALL_TYPES } from "../constants";
|
||||||
|
|
@ -297,181 +298,183 @@ export function LogDetailsDrawer({
|
||||||
if (!currentLog || !enrichedLog) return null;
|
if (!currentLog || !enrichedLog) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Sheet
|
||||||
title={null}
|
|
||||||
placement="right"
|
|
||||||
onClose={onClose}
|
|
||||||
open={open}
|
open={open}
|
||||||
width={DRAWER_WIDTH}
|
onOpenChange={(nextOpen) => {
|
||||||
closable={false}
|
if (!nextOpen) onClose();
|
||||||
mask={true}
|
|
||||||
maskClosable={true}
|
|
||||||
styles={{
|
|
||||||
body: { padding: 0, overflow: "hidden" },
|
|
||||||
header: { display: "none" },
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ height: "100%" }} className="flex relative">
|
<SheetContent
|
||||||
{!isSidebarCollapsed ? (
|
side="right"
|
||||||
<Button
|
showCloseButton={false}
|
||||||
type="text"
|
className="gap-0 overflow-hidden p-0 data-[side=right]:sm:max-w-none"
|
||||||
size="small"
|
style={{ width: DRAWER_WIDTH }}
|
||||||
icon={<LeftOutlined />}
|
>
|
||||||
onClick={() => setIsSidebarCollapsed(true)}
|
<div style={{ height: "100%" }} className="flex relative">
|
||||||
className="absolute top-2 left-2 z-20 bg-white! border! border-slate-200! rounded-md!"
|
{!isSidebarCollapsed ? (
|
||||||
aria-label="Collapse trace sidebar"
|
<Button
|
||||||
/>
|
variant="ghost"
|
||||||
) : (
|
size="icon-sm"
|
||||||
<Button
|
onClick={() => setIsSidebarCollapsed(true)}
|
||||||
type="text"
|
className="absolute top-2 left-2 z-20 bg-white! border! border-slate-200! rounded-md!"
|
||||||
size="small"
|
aria-label="Collapse trace sidebar"
|
||||||
icon={<RightOutlined />}
|
>
|
||||||
onClick={() => setIsSidebarCollapsed(false)}
|
<ChevronLeft className="size-4" />
|
||||||
className="absolute top-2 left-2 z-20 bg-white! border! border-slate-200! rounded-md!"
|
</Button>
|
||||||
aria-label="Expand trace sidebar"
|
) : (
|
||||||
/>
|
<Button
|
||||||
)}
|
variant="ghost"
|
||||||
{!isSidebarCollapsed && (
|
size="icon-sm"
|
||||||
<div className="border-r border-slate-200 bg-slate-50 flex flex-col" style={{ width: SIDEBAR_WIDTH_PX }}>
|
onClick={() => setIsSidebarCollapsed(false)}
|
||||||
<div className="pl-12 pr-3 py-2 border-b border-slate-200 bg-white">
|
className="absolute top-2 left-2 z-20 bg-white! border! border-slate-200! rounded-md!"
|
||||||
<div className="flex items-start justify-between gap-2">
|
aria-label="Expand trace sidebar"
|
||||||
<div>
|
>
|
||||||
<div className="text-[10px] uppercase tracking-wide text-slate-500">
|
<ChevronRight className="size-4" />
|
||||||
{isSessionMode ? "Session" : "Trace"}
|
</Button>
|
||||||
</div>
|
)}
|
||||||
<div className="font-mono text-[12px] text-slate-900 leading-tight flex items-center gap-1">
|
{!isSidebarCollapsed && (
|
||||||
<span className="truncate">{leftPanelDisplayId}</span>
|
<div className="border-r border-slate-200 bg-slate-50 flex flex-col" style={{ width: SIDEBAR_WIDTH_PX }}>
|
||||||
<button
|
<div className="pl-12 pr-3 py-2 border-b border-slate-200 bg-white">
|
||||||
type="button"
|
<div className="flex items-start justify-between gap-2">
|
||||||
onClick={handleCopyLeftPanelId}
|
<div>
|
||||||
className="text-slate-400 hover:text-slate-600"
|
<div className="text-[10px] uppercase tracking-wide text-slate-500">
|
||||||
aria-label="Copy trace id"
|
{isSessionMode ? "Session" : "Trace"}
|
||||||
>
|
</div>
|
||||||
{copiedLeftPanelId ? (
|
<div className="font-mono text-[12px] text-slate-900 leading-tight flex items-center gap-1">
|
||||||
<CheckOutlined className="text-[11px]" />
|
<span className="truncate">{leftPanelDisplayId}</span>
|
||||||
) : (
|
<button
|
||||||
<CopyOutlined className="text-[11px]" />
|
type="button"
|
||||||
)}
|
onClick={handleCopyLeftPanelId}
|
||||||
</button>
|
className="text-slate-400 hover:text-slate-600"
|
||||||
|
aria-label="Copy trace id"
|
||||||
|
>
|
||||||
|
{copiedLeftPanelId ? <Check className="size-3" /> : <Copy className="size-3" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="mt-1 text-[11px] text-slate-500 font-mono">
|
||||||
<div className="mt-1 text-[11px] text-slate-500 font-mono">
|
{logsForList.length} req
|
||||||
{logsForList.length} req
|
{[
|
||||||
{[
|
isSessionMode
|
||||||
isSessionMode
|
? llmCount
|
||||||
? llmCount
|
: logsForList.filter(
|
||||||
: logsForList.filter(
|
(row) => !MCP_CALL_TYPES.includes(row.call_type) && !AGENT_CALL_TYPES.includes(row.call_type),
|
||||||
(row) => !MCP_CALL_TYPES.includes(row.call_type) && !AGENT_CALL_TYPES.includes(row.call_type),
|
).length,
|
||||||
).length,
|
isSessionMode
|
||||||
isSessionMode
|
? agentCount
|
||||||
? agentCount
|
: logsForList.filter((row) => AGENT_CALL_TYPES.includes(row.call_type)).length,
|
||||||
: logsForList.filter((row) => AGENT_CALL_TYPES.includes(row.call_type)).length,
|
isSessionMode
|
||||||
isSessionMode ? mcpCount : logsForList.filter((row) => MCP_CALL_TYPES.includes(row.call_type)).length,
|
? mcpCount
|
||||||
].map((count, i) => {
|
: logsForList.filter((row) => MCP_CALL_TYPES.includes(row.call_type)).length,
|
||||||
const label = [" LLM", " Agent", " MCP"][i];
|
].map((count, i) => {
|
||||||
return count > 0 ? (
|
const label = [" LLM", " Agent", " MCP"][i];
|
||||||
<span key={label}>
|
return count > 0 ? (
|
||||||
|
<span key={label}>
|
||||||
|
<span className="mx-1.5">·</span>
|
||||||
|
{count}
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
<span className="mx-1.5">·</span>
|
||||||
|
{isSessionMode ? getSpendString(totalSessionCost) : getSpendString(currentLog.spend || 0)}
|
||||||
|
{isSessionMode && (
|
||||||
|
<>
|
||||||
<span className="mx-1.5">·</span>
|
<span className="mx-1.5">·</span>
|
||||||
{count}
|
{sessionDurationSeconds}s
|
||||||
{label}
|
</>
|
||||||
</span>
|
)}
|
||||||
) : null;
|
</div>
|
||||||
})}
|
{isSessionMode && sessionTruncated && (
|
||||||
<span className="mx-1.5">·</span>
|
<div className="mt-1 text-[11px] text-amber-600 font-mono">
|
||||||
{isSessionMode ? getSpendString(totalSessionCost) : getSpendString(currentLog.spend || 0)}
|
Showing most recent {logsForList.length} of {sessionTotalCount}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{isSessionMode && (
|
{isSessionMode && (
|
||||||
<>
|
<Tabs
|
||||||
<span className="mx-1.5">·</span>
|
className="mt-1.5"
|
||||||
{sessionDurationSeconds}s
|
value={sessionSortMode}
|
||||||
</>
|
onValueChange={(value) => setSessionSortMode(value as SessionLogSortMode)}
|
||||||
|
>
|
||||||
|
<TabsList className="w-full">
|
||||||
|
<TabsTrigger value="duration" className="text-[11px]">
|
||||||
|
Duration
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="start_time" className="text-[11px]">
|
||||||
|
Start time
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isSessionMode && sessionTruncated && (
|
|
||||||
<div className="mt-1 text-[11px] text-amber-600 font-mono">
|
|
||||||
Showing most recent {logsForList.length} of {sessionTotalCount}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isSessionMode && (
|
|
||||||
<Segmented
|
|
||||||
block
|
|
||||||
size="small"
|
|
||||||
className="mt-1.5 [&_.ant-segmented-item-label]:text-[11px]"
|
|
||||||
options={[
|
|
||||||
{ label: "Duration", value: "duration" },
|
|
||||||
{ label: "Start time", value: "start_time" },
|
|
||||||
]}
|
|
||||||
value={sessionSortMode}
|
|
||||||
onChange={(value) => setSessionSortMode(value as SessionLogSortMode)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{normalizeGuardrailEntries(metadata?.guardrail_information).length > 0 && (
|
{normalizeGuardrailEntries(metadata?.guardrail_information).length > 0 && (
|
||||||
<div className="px-3 pt-2">
|
<div className="px-3 pt-2">
|
||||||
<GuardrailJumpLink guardrailEntries={normalizeGuardrailEntries(metadata?.guardrail_information)} />
|
<GuardrailJumpLink guardrailEntries={normalizeGuardrailEntries(metadata?.guardrail_information)} />
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isSessionMode ? (
|
|
||||||
<div className="py-1">
|
|
||||||
{/* Child events — vertical tree line with horizontal connectors */}
|
|
||||||
<div className="relative pl-2">
|
|
||||||
<div className="absolute left-4 top-1 bottom-1 border-l border-slate-300" />
|
|
||||||
{logsForList.map((row, idx) => {
|
|
||||||
const isLast = idx === logsForList.length - 1;
|
|
||||||
return (
|
|
||||||
<div key={row.request_id} className="relative">
|
|
||||||
<div className="absolute left-4 top-3 w-3 border-t border-slate-300" />
|
|
||||||
{isLast && <div className="absolute left-4 top-3 bottom-0 w-px bg-slate-50" />}
|
|
||||||
<TraceEventRow
|
|
||||||
row={row}
|
|
||||||
isSelected={row.request_id === currentLog.request_id}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedSessionRequestId(row.request_id);
|
|
||||||
onSelectLog?.(row);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
) : (
|
{isSessionMode ? (
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{logsForList.map((row) => (
|
{/* Child events — vertical tree line with horizontal connectors */}
|
||||||
<TraceEventRow
|
<div className="relative pl-2">
|
||||||
key={row.request_id}
|
<div className="absolute left-4 top-1 bottom-1 border-l border-slate-300" />
|
||||||
row={row}
|
{logsForList.map((row, idx) => {
|
||||||
isSelected={row.request_id === currentLog.request_id}
|
const isLast = idx === logsForList.length - 1;
|
||||||
onClick={() => onSelectLog?.(row)}
|
return (
|
||||||
/>
|
<div key={row.request_id} className="relative">
|
||||||
))}
|
<div className="absolute left-4 top-3 w-3 border-t border-slate-300" />
|
||||||
</div>
|
{isLast && <div className="absolute left-4 top-3 bottom-0 w-px bg-slate-50" />}
|
||||||
)}
|
<TraceEventRow
|
||||||
|
row={row}
|
||||||
|
isSelected={row.request_id === currentLog.request_id}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedSessionRequestId(row.request_id);
|
||||||
|
onSelectLog?.(row);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="py-1">
|
||||||
|
{logsForList.map((row) => (
|
||||||
|
<TraceEventRow
|
||||||
|
key={row.request_id}
|
||||||
|
row={row}
|
||||||
|
isSelected={row.request_id === currentLog.request_id}
|
||||||
|
onClick={() => onSelectLog?.(row)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<DrawerHeader
|
<DrawerHeader
|
||||||
log={currentLog}
|
log={currentLog}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onPrevious={selectPreviousLog}
|
onPrevious={selectPreviousLog}
|
||||||
onNext={selectNextLog}
|
onNext={selectNextLog}
|
||||||
statusLabel={statusLabel}
|
statusLabel={statusLabel}
|
||||||
statusColor={statusColor}
|
statusColor={statusColor}
|
||||||
environment={environment}
|
environment={environment}
|
||||||
/>
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
<LogDetailContent
|
|
||||||
logEntry={enrichedLog}
|
|
||||||
isLoadingDetails={isLoadingDetails}
|
|
||||||
accessToken={accessToken ?? null}
|
|
||||||
/>
|
/>
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<LogDetailContent
|
||||||
|
logEntry={enrichedLog}
|
||||||
|
isLoadingDetails={isLoadingDetails}
|
||||||
|
accessToken={accessToken ?? null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SheetContent>
|
||||||
</Drawer>
|
</Sheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,6 @@ import userEvent from "@testing-library/user-event";
|
||||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
import { RealtimePrettyView, isRealtimeResponse } from "./RealtimePrettyView";
|
import { RealtimePrettyView, isRealtimeResponse } from "./RealtimePrettyView";
|
||||||
|
|
||||||
vi.mock("antd", async () => {
|
|
||||||
const actual = await vi.importActual<typeof import("antd")>("antd");
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
message: {
|
|
||||||
success: vi.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const sampleRealtimeResponse = {
|
const sampleRealtimeResponse = {
|
||||||
usage: {
|
usage: {
|
||||||
total_tokens: 587,
|
total_tokens: 587,
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Typography, Tag, Tooltip } from "antd";
|
import { ChevronDown, ChevronUp, MessageSquare, Mic, Settings, Volume2 } from "lucide-react";
|
||||||
import {
|
import { Badge } from "@/components/ui/badge";
|
||||||
SoundOutlined,
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
MessageOutlined,
|
|
||||||
SettingOutlined,
|
|
||||||
AudioOutlined,
|
|
||||||
DownOutlined,
|
|
||||||
UpOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import { SectionHeader } from "./SectionHeader";
|
import { SectionHeader } from "./SectionHeader";
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
interface RealtimeEvent {
|
interface RealtimeEvent {
|
||||||
type: string;
|
type: string;
|
||||||
event_id?: string;
|
event_id?: string;
|
||||||
|
|
@ -163,34 +155,34 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
{isCollapsed ? (
|
{isCollapsed ? (
|
||||||
<DownOutlined style={{ fontSize: 10, color: "#8c8c8c" }} />
|
<ChevronDown className="size-2.5 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<UpOutlined style={{ fontSize: 10, color: "#8c8c8c" }} />
|
<ChevronUp className="size-2.5 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<SettingOutlined style={{ color: "#8c8c8c", fontSize: 14 }} />
|
<Settings className="size-3.5 text-muted-foreground" />
|
||||||
<Text style={{ fontWeight: 500, fontSize: 14 }}>Session</Text>
|
<span style={{ fontWeight: 500, fontSize: 14 }}>Session</span>
|
||||||
</div>
|
</div>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
<span className="text-muted-foreground" style={{ fontSize: 12 }}>
|
||||||
{session.model}
|
{session.model}
|
||||||
</Text>
|
</span>
|
||||||
{turnCount > 0 && (
|
{turnCount > 0 && (
|
||||||
<Tag color="purple" style={{ margin: 0, fontWeight: 500 }}>
|
<Badge variant="secondary" style={{ margin: 0, fontWeight: 500 }}>
|
||||||
{turnCount} {turnCount === 1 ? "turn" : "turns"}
|
{turnCount} {turnCount === 1 ? "turn" : "turns"}
|
||||||
</Tag>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{session.voice && (
|
{session.voice && (
|
||||||
<Tag color="blue" style={{ margin: 0 }}>
|
<Badge variant="secondary" style={{ margin: 0 }}>
|
||||||
<SoundOutlined /> {session.voice}
|
<Volume2 className="size-3" /> {session.voice}
|
||||||
</Tag>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{session.modalities && (
|
{session.modalities && (
|
||||||
<div style={{ display: "flex", gap: 4 }}>
|
<div style={{ display: "flex", gap: 4 }}>
|
||||||
{session.modalities.map((m) => (
|
{session.modalities.map((m) => (
|
||||||
<Tag key={m} style={{ margin: 0 }}>
|
<Badge key={m} variant="outline" style={{ margin: 0 }}>
|
||||||
{m === "audio" ? <AudioOutlined /> : <MessageOutlined />} {m}
|
{m === "audio" ? <Mic className="size-3" /> : <MessageSquare className="size-3" />} {m}
|
||||||
</Tag>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -228,8 +220,8 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
|
||||||
|
|
||||||
{session.instructions && (
|
{session.instructions && (
|
||||||
<div style={{ marginTop: 12 }}>
|
<div style={{ marginTop: 12 }}>
|
||||||
<Text
|
<span
|
||||||
type="secondary"
|
className="text-muted-foreground"
|
||||||
style={{
|
style={{
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
letterSpacing: "0.5px",
|
letterSpacing: "0.5px",
|
||||||
|
|
@ -239,7 +231,7 @@ function SessionCard({ session, turnCount }: { session: RealtimeSession; turnCou
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Instructions
|
Instructions
|
||||||
</Text>
|
</span>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
@ -344,20 +336,25 @@ function ResponseTurn({ response, index }: { response: RealtimeResponse; index:
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tag color={response.status === "completed" ? "green" : "orange"} style={{ margin: 0 }}>
|
<Badge variant={response.status === "completed" ? "secondary" : "outline"} style={{ margin: 0 }}>
|
||||||
{response.status || "unknown"}
|
{response.status || "unknown"}
|
||||||
</Tag>
|
</Badge>
|
||||||
{usage && (
|
{usage && (
|
||||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
<span className="text-muted-foreground" style={{ fontSize: 11 }}>
|
||||||
{usage.input_tokens ?? 0} in / {usage.output_tokens ?? 0} out tokens
|
{usage.input_tokens ?? 0} in / {usage.output_tokens ?? 0} out tokens
|
||||||
</Text>
|
</span>
|
||||||
)}
|
)}
|
||||||
{response.conversation_id && (
|
{response.conversation_id && (
|
||||||
<Tooltip title={response.conversation_id}>
|
<TooltipProvider>
|
||||||
<Text type="secondary" style={{ fontSize: 11, cursor: "help" }}>
|
<Tooltip>
|
||||||
conv: {response.conversation_id.slice(0, 12)}...
|
<TooltipTrigger
|
||||||
</Text>
|
render={<span className="text-muted-foreground" style={{ fontSize: 11, cursor: "help" }} />}
|
||||||
</Tooltip>
|
>
|
||||||
|
conv: {response.conversation_id.slice(0, 12)}...
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{response.conversation_id}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -381,8 +378,8 @@ function OutputMessage({ output }: { output: RealtimeOutputItem }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<Text
|
<span
|
||||||
type="secondary"
|
className="text-muted-foreground"
|
||||||
style={{
|
style={{
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
letterSpacing: "0.5px",
|
letterSpacing: "0.5px",
|
||||||
|
|
@ -392,7 +389,7 @@ function OutputMessage({ output }: { output: RealtimeOutputItem }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{output.role?.toUpperCase() || "ASSISTANT"}
|
{output.role?.toUpperCase() || "ASSISTANT"}
|
||||||
</Text>
|
</span>
|
||||||
{contents.map((c, cIdx) => {
|
{contents.map((c, cIdx) => {
|
||||||
const text = c.transcript || c.text;
|
const text = c.transcript || c.text;
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
|
|
@ -407,20 +404,18 @@ function OutputMessage({ output }: { output: RealtimeOutputItem }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{c.type === "audio" && (
|
{c.type === "audio" && (
|
||||||
<AudioOutlined
|
<Mic
|
||||||
|
className="size-3 text-muted-foreground"
|
||||||
style={{
|
style={{
|
||||||
color: "#8c8c8c",
|
|
||||||
fontSize: 12,
|
|
||||||
marginTop: 3,
|
marginTop: 3,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{c.type === "text" && (
|
{c.type === "text" && (
|
||||||
<MessageOutlined
|
<MessageSquare
|
||||||
|
className="size-3 text-muted-foreground"
|
||||||
style={{
|
style={{
|
||||||
color: "#8c8c8c",
|
|
||||||
fontSize: 12,
|
|
||||||
marginTop: 3,
|
marginTop: 3,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
}}
|
}}
|
||||||
|
|
@ -453,9 +448,12 @@ function TokenBreakdown({ label, details }: { label: string; details: Record<str
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: 4 }}>
|
<div style={{ marginTop: 4 }}>
|
||||||
<Text type="secondary" style={{ fontSize: 10, letterSpacing: "0.5px", textTransform: "uppercase" }}>
|
<span
|
||||||
|
className="text-muted-foreground"
|
||||||
|
style={{ fontSize: 10, letterSpacing: "0.5px", textTransform: "uppercase" }}
|
||||||
|
>
|
||||||
{label} Token Breakdown
|
{label} Token Breakdown
|
||||||
</Text>
|
</span>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
|
@ -467,9 +465,9 @@ function TokenBreakdown({ label, details }: { label: string; details: Record<str
|
||||||
{entries.map(([key, value]) => {
|
{entries.map(([key, value]) => {
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
return (
|
return (
|
||||||
<Tag key={key} style={{ margin: 0 }}>
|
<Badge key={key} variant="outline" style={{ margin: 0 }}>
|
||||||
{formatTokenLabel(key)}: {value.toLocaleString()}
|
{formatTokenLabel(key)}: {value.toLocaleString()}
|
||||||
</Tag>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -483,9 +481,9 @@ function ConfigRow({ label, value }: { label: string; value: any }) {
|
||||||
if (value === undefined || value === null) return null;
|
if (value === undefined || value === null) return null;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
<span className="text-muted-foreground" style={{ fontSize: 11 }}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</span>
|
||||||
<div style={{ fontSize: 13, color: "#262626" }}>{String(value)}</div>
|
<div style={{ fontSize: 13, color: "#262626" }}>{String(value)}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,9 @@
|
||||||
* Formatted view of tool definition with parameters table and call data
|
* Formatted view of tool definition with parameters table and call data
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Typography, Table } from "antd";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { ParsedTool, ParameterRow } from "./types";
|
import { ParsedTool, ParameterRow } from "./types";
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
interface FormattedToolViewProps {
|
interface FormattedToolViewProps {
|
||||||
tool: ParsedTool;
|
tool: ParsedTool;
|
||||||
}
|
}
|
||||||
|
|
@ -23,57 +21,27 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: "Parameter",
|
|
||||||
dataIndex: "name",
|
|
||||||
key: "name",
|
|
||||||
render: (name: string, record: ParameterRow) => (
|
|
||||||
<Text code>
|
|
||||||
{name}
|
|
||||||
{record.required && <Text type="danger">*</Text>}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Type",
|
|
||||||
dataIndex: "type",
|
|
||||||
key: "type",
|
|
||||||
render: (type: string) => (
|
|
||||||
<Text code style={{ color: "#1890ff" }}>
|
|
||||||
{type}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Description",
|
|
||||||
dataIndex: "description",
|
|
||||||
key: "description",
|
|
||||||
render: (desc: string) => <Text type="secondary">{desc}</Text>,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{tool.description && (
|
{tool.description && (
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<Text
|
<span
|
||||||
style={{
|
style={{
|
||||||
lineHeight: 1.6,
|
lineHeight: 1.6,
|
||||||
whiteSpace: "pre-wrap",
|
whiteSpace: "pre-wrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tool.description}
|
{tool.description}
|
||||||
</Text>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Parameters Table */}
|
{/* Parameters Table */}
|
||||||
{parameterRows.length > 0 && (
|
{parameterRows.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<Text
|
<span
|
||||||
type="secondary"
|
className="text-muted-foreground"
|
||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
display: "block",
|
display: "block",
|
||||||
|
|
@ -81,16 +49,42 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Parameters
|
Parameters
|
||||||
</Text>
|
</span>
|
||||||
<Table dataSource={parameterRows} columns={columns} pagination={false} size="small" bordered />
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Parameter</TableHead>
|
||||||
|
<TableHead>Type</TableHead>
|
||||||
|
<TableHead>Description</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{parameterRows.map((row) => (
|
||||||
|
<TableRow key={row.key}>
|
||||||
|
<TableCell>
|
||||||
|
<code>
|
||||||
|
{row.name}
|
||||||
|
{row.required && <span className="text-destructive">*</span>}
|
||||||
|
</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<code className="text-blue-600">{row.type}</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span className="text-muted-foreground">{row.description}</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* If tool was called, show the arguments used */}
|
{/* If tool was called, show the arguments used */}
|
||||||
{tool.called && tool.callData && (
|
{tool.called && tool.callData && (
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
<Text
|
<span
|
||||||
type="secondary"
|
className="text-muted-foreground"
|
||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
display: "block",
|
display: "block",
|
||||||
|
|
@ -98,7 +92,7 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Called With
|
Called With
|
||||||
</Text>
|
</span>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: "#f6ffed",
|
background: "#f6ffed",
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Typography, Radio } from "antd";
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { ParsedTool } from "./types";
|
import { ParsedTool } from "./types";
|
||||||
import { FormattedToolView } from "./FormattedToolView";
|
import { FormattedToolView } from "./FormattedToolView";
|
||||||
import { JsonToolView } from "./JsonToolView";
|
import { JsonToolView } from "./JsonToolView";
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
type ViewMode = "formatted" | "json";
|
type ViewMode = "formatted" | "json";
|
||||||
|
|
||||||
interface ToolExpandedContentProps {
|
interface ToolExpandedContentProps {
|
||||||
|
|
@ -29,13 +27,13 @@ export function ToolExpandedContent({ tool }: ToolExpandedContentProps) {
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
<span className="text-xs text-muted-foreground">Description</span>
|
||||||
Description
|
<Tabs value={viewMode} onValueChange={(value) => setViewMode(value as ViewMode)}>
|
||||||
</Text>
|
<TabsList>
|
||||||
<Radio.Group size="small" value={viewMode} onChange={(e) => setViewMode(e.target.value)}>
|
<TabsTrigger value="formatted">Formatted</TabsTrigger>
|
||||||
<Radio.Button value="formatted">Formatted</Radio.Button>
|
<TabsTrigger value="json">JSON</TabsTrigger>
|
||||||
<Radio.Button value="json">JSON</Radio.Button>
|
</TabsList>
|
||||||
</Radio.Group>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewMode === "formatted" ? <FormattedToolView tool={tool} /> : <JsonToolView tool={tool} />}
|
{viewMode === "formatted" ? <FormattedToolView tool={tool} /> : <JsonToolView tool={tool} />}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Typography, Tag } from "antd";
|
import { ChevronDown, ChevronRight, Wrench } from "lucide-react";
|
||||||
import { ToolOutlined, RightOutlined, DownOutlined } from "@ant-design/icons";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { ParsedTool } from "./types";
|
import { ParsedTool } from "./types";
|
||||||
import { ToolExpandedContent } from "./ToolExpandedContent";
|
import { ToolExpandedContent } from "./ToolExpandedContent";
|
||||||
|
|
||||||
const { Text } = Typography;
|
|
||||||
|
|
||||||
interface ToolItemProps {
|
interface ToolItemProps {
|
||||||
tool: ParsedTool;
|
tool: ParsedTool;
|
||||||
}
|
}
|
||||||
|
|
@ -39,18 +37,18 @@ export function ToolItem({ tool }: ToolItemProps) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||||
<ToolOutlined style={{ color: "#8c8c8c", fontSize: 14 }} />
|
<Wrench className="size-3.5 text-muted-foreground" />
|
||||||
<Text style={{ fontSize: 14 }}>
|
<span style={{ fontSize: 14 }}>
|
||||||
{tool.index}. {tool.name}
|
{tool.index}. {tool.name}
|
||||||
</Text>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<Tag color={tool.called ? "blue" : "default"}>{tool.called ? "called" : "not called"}</Tag>
|
<Badge variant={tool.called ? "default" : "secondary"}>{tool.called ? "called" : "not called"}</Badge>
|
||||||
{expanded ? (
|
{expanded ? (
|
||||||
<DownOutlined style={{ fontSize: 12, color: "#8c8c8c" }} />
|
<ChevronDown className="size-3 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<RightOutlined style={{ fontSize: 12, color: "#8c8c8c" }} />
|
<ChevronRight className="size-3 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Collapse } from "antd";
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
import { getProviderLogoAndName } from "../provider_info_helpers";
|
import { getProviderLogoAndName } from "../provider_info_helpers";
|
||||||
|
|
||||||
interface VectorStoreContent {
|
interface VectorStoreContent {
|
||||||
|
|
@ -31,6 +32,7 @@ interface VectorStoreViewerProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
|
export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
const [expandedResults, setExpandedResults] = useState<Record<string, boolean>>({});
|
const [expandedResults, setExpandedResults] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
|
|
@ -57,110 +59,110 @@ export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
|
<div className="bg-white rounded-lg shadow-sm w-full max-w-full overflow-hidden mb-6">
|
||||||
<Collapse
|
<Collapsible open={open} onOpenChange={setOpen}>
|
||||||
defaultActiveKey={["1"]}
|
<CollapsibleTrigger className="flex w-full items-center gap-3 px-4 py-3 text-left">
|
||||||
expandIconPosition="start"
|
{open ? (
|
||||||
items={[
|
<ChevronDown className="size-3.5 shrink-0 text-gray-500" />
|
||||||
{
|
) : (
|
||||||
key: "1",
|
<ChevronRight className="size-3.5 shrink-0 text-gray-500" />
|
||||||
label: <h3 className="text-lg font-medium text-gray-900">Vector Store Requests</h3>,
|
)}
|
||||||
children: (
|
<h3 className="text-lg font-medium text-gray-900">Vector Store Requests</h3>
|
||||||
<div className="p-4">
|
</CollapsibleTrigger>
|
||||||
{data.map((request, index) => (
|
<CollapsibleContent>
|
||||||
<div key={index} className="mb-6 last:mb-0">
|
<div className="p-4">
|
||||||
<div className="bg-white rounded-lg border p-4 mb-4">
|
{data.map((request, index) => (
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div key={index} className="mb-6 last:mb-0">
|
||||||
<div className="space-y-2">
|
<div className="bg-white rounded-lg border p-4 mb-4">
|
||||||
<div className="flex">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<span className="font-medium w-1/3">Query:</span>
|
<div className="space-y-2">
|
||||||
<span className="font-mono">{request.query}</span>
|
<div className="flex">
|
||||||
</div>
|
<span className="font-medium w-1/3">Query:</span>
|
||||||
<div className="flex">
|
<span className="font-mono">{request.query}</span>
|
||||||
<span className="font-medium w-1/3">Vector Store ID:</span>
|
</div>
|
||||||
<span className="font-mono">{request.vector_store_id}</span>
|
<div className="flex">
|
||||||
</div>
|
<span className="font-medium w-1/3">Vector Store ID:</span>
|
||||||
<div className="flex">
|
<span className="font-mono">{request.vector_store_id}</span>
|
||||||
<span className="font-medium w-1/3">Provider:</span>
|
</div>
|
||||||
<span className="flex items-center">
|
<div className="flex">
|
||||||
{(() => {
|
<span className="font-medium w-1/3">Provider:</span>
|
||||||
const { logo, displayName } = getProviderLogoAndName(request.custom_llm_provider);
|
<span className="flex items-center">
|
||||||
return (
|
{(() => {
|
||||||
<>
|
const { logo, displayName } = getProviderLogoAndName(request.custom_llm_provider);
|
||||||
{logo && <img src={logo} alt={`${displayName} logo`} className="h-5 w-5 mr-2" />}
|
return (
|
||||||
{displayName}
|
<>
|
||||||
</>
|
{logo && <img src={logo} alt={`${displayName} logo`} className="h-5 w-5 mr-2" />}
|
||||||
);
|
{displayName}
|
||||||
})()}
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Start Time:</span>
|
||||||
|
<span>{formatTime(request.start_time)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">End Time:</span>
|
||||||
|
<span>{formatTime(request.end_time)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="font-medium w-1/3">Duration:</span>
|
||||||
|
<span>{calculateDuration(request.start_time, request.end_time)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 className="font-medium mb-2">Search Results</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{request.vector_store_search_response.data.map((result, resultIndex) => {
|
||||||
|
const isExpanded = expandedResults[`${index}-${resultIndex}`] || false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={resultIndex} className="border rounded-lg overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="flex items-center p-3 bg-gray-50 cursor-pointer"
|
||||||
|
onClick={() => toggleResult(index, resultIndex)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className={`w-5 h-5 mr-2 transition-transform ${isExpanded ? "transform rotate-90" : ""}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="font-medium mr-2">Result {resultIndex + 1}</span>
|
||||||
|
<span className="text-gray-500 text-sm">
|
||||||
|
Score: <span className="font-mono">{result.score.toFixed(4)}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex">
|
{isExpanded && (
|
||||||
<span className="font-medium w-1/3">Start Time:</span>
|
<div className="p-3 border-t bg-white">
|
||||||
<span>{formatTime(request.start_time)}</span>
|
{result.content.map((content, contentIndex) => (
|
||||||
|
<div key={contentIndex} className="mb-2 last:mb-0">
|
||||||
|
<div className="text-xs text-gray-500 mb-1">{content.type}</div>
|
||||||
|
<pre className="text-xs font-mono whitespace-pre-wrap break-all bg-gray-50 p-2 rounded-sm">
|
||||||
|
{content.text}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
)}
|
||||||
<span className="font-medium w-1/3">End Time:</span>
|
|
||||||
<span>{formatTime(request.end_time)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium w-1/3">Duration:</span>
|
|
||||||
<span>{calculateDuration(request.start_time, request.end_time)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
|
})}
|
||||||
<h4 className="font-medium mb-2">Search Results</h4>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
{request.vector_store_search_response.data.map((result, resultIndex) => {
|
|
||||||
const isExpanded = expandedResults[`${index}-${resultIndex}`] || false;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={resultIndex} className="border rounded-lg overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="flex items-center p-3 bg-gray-50 cursor-pointer"
|
|
||||||
onClick={() => toggleResult(index, resultIndex)}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className={`w-5 h-5 mr-2 transition-transform ${isExpanded ? "transform rotate-90" : ""}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="font-medium mr-2">Result {resultIndex + 1}</span>
|
|
||||||
<span className="text-gray-500 text-sm">
|
|
||||||
Score: <span className="font-mono">{result.score.toFixed(4)}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isExpanded && (
|
|
||||||
<div className="p-3 border-t bg-white">
|
|
||||||
{result.content.map((content, contentIndex) => (
|
|
||||||
<div key={contentIndex} className="mb-2 last:mb-0">
|
|
||||||
<div className="text-xs text-gray-500 mb-1">{content.type}</div>
|
|
||||||
<pre className="text-xs font-mono whitespace-pre-wrap break-all bg-gray-50 p-2 rounded-sm">
|
|
||||||
{content.text}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
),
|
))}
|
||||||
},
|
</div>
|
||||||
]}
|
</CollapsibleContent>
|
||||||
/>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue