mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix ui
This commit is contained in:
parent
f07ef8af00
commit
5f07635310
5 changed files with 190 additions and 173 deletions
|
|
@ -2,6 +2,7 @@ import { Button, Tag, Tooltip, Typography } from "antd";
|
||||||
import { CloseOutlined, CopyOutlined, UpOutlined, DownOutlined } from "@ant-design/icons";
|
import { CloseOutlined, CopyOutlined, UpOutlined, DownOutlined } from "@ant-design/icons";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { LogEntry } from "../columns";
|
import { LogEntry } from "../columns";
|
||||||
|
import { getProviderLogoAndName } from "../../provider_info_helpers";
|
||||||
import {
|
import {
|
||||||
DRAWER_HEADER_PADDING,
|
DRAWER_HEADER_PADDING,
|
||||||
COLOR_BORDER,
|
COLOR_BORDER,
|
||||||
|
|
@ -29,7 +30,7 @@ interface DrawerHeaderProps {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Header component for the log details drawer.
|
* Header component for the log details drawer.
|
||||||
* Displays request ID, navigation controls, status, environment, and timestamp.
|
* Displays model/provider, request ID, navigation controls, status, environment, and timestamp.
|
||||||
*/
|
*/
|
||||||
export function DrawerHeader({
|
export function DrawerHeader({
|
||||||
log,
|
log,
|
||||||
|
|
@ -41,6 +42,9 @@ export function DrawerHeader({
|
||||||
statusColor,
|
statusColor,
|
||||||
environment,
|
environment,
|
||||||
}: DrawerHeaderProps) {
|
}: DrawerHeaderProps) {
|
||||||
|
const provider = log.custom_llm_provider || "";
|
||||||
|
const providerInfo = provider ? getProviderLogoAndName(provider) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -52,6 +56,9 @@ export function DrawerHeader({
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* Row 0: Model + Provider with Logo */}
|
||||||
|
<ModelProviderSection model={log.model} providerLogo={providerInfo?.logo} providerName={providerInfo?.displayName} />
|
||||||
|
|
||||||
{/* Row 1: Request ID + Actions */}
|
{/* Row 1: Request ID + Actions */}
|
||||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: SPACING_MEDIUM }}>
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: SPACING_MEDIUM }}>
|
||||||
<RequestIdSection requestId={log.request_id} onCopy={onCopyRequestId} />
|
<RequestIdSection requestId={log.request_id} onCopy={onCopyRequestId} />
|
||||||
|
|
@ -64,6 +71,45 @@ export function DrawerHeader({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model and Provider display with logo
|
||||||
|
*/
|
||||||
|
function ModelProviderSection({
|
||||||
|
model,
|
||||||
|
providerLogo,
|
||||||
|
providerName,
|
||||||
|
}: {
|
||||||
|
model: string;
|
||||||
|
providerLogo?: string;
|
||||||
|
providerName?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: SPACING_MEDIUM, marginBottom: SPACING_MEDIUM }}>
|
||||||
|
{providerLogo && (
|
||||||
|
<img
|
||||||
|
src={providerLogo}
|
||||||
|
alt={providerName || "Provider"}
|
||||||
|
style={{ width: 24, height: 24 }}
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.style.display = "none";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<Text strong style={{ fontSize: 14 }}>
|
||||||
|
{model}
|
||||||
|
</Text>
|
||||||
|
{providerName && (
|
||||||
|
<Text type="secondary" style={{ fontSize: 12, marginLeft: SPACING_MEDIUM }}>
|
||||||
|
{providerName}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request ID display with copy button
|
* Request ID display with copy button
|
||||||
*/
|
*/
|
||||||
|
|
@ -93,6 +139,7 @@ function RequestIdSection({ requestId, onCopy }: { requestId: string; onCopy: ()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigation controls (previous, next, close)
|
* Navigation controls (previous, next, close)
|
||||||
|
* Shows keyboard shortcuts with bounding boxes for visibility
|
||||||
*/
|
*/
|
||||||
function NavigationSection({
|
function NavigationSection({
|
||||||
onPrevious,
|
onPrevious,
|
||||||
|
|
@ -103,18 +150,32 @@ function NavigationSection({
|
||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const keyboardShortcutStyle = {
|
||||||
|
border: "1px solid #d9d9d9",
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: "0 4px",
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "monospace",
|
||||||
|
marginLeft: 4,
|
||||||
|
background: "#fafafa",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: SPACING_SMALL }}>
|
<div style={{ display: "flex", alignItems: "center", gap: SPACING_SMALL }}>
|
||||||
<Tooltip title="Previous (K)">
|
<Button type="text" size="small" onClick={onPrevious}>
|
||||||
<Button type="text" size="small" icon={<UpOutlined />} onClick={onPrevious} />
|
<UpOutlined />
|
||||||
</Tooltip>
|
<span style={keyboardShortcutStyle}>K</span>
|
||||||
<Tooltip title="Next (J)">
|
</Button>
|
||||||
<Button type="text" size="small" icon={<DownOutlined />} onClick={onNext} />
|
<Button type="text" size="small" onClick={onNext}>
|
||||||
</Tooltip>
|
<DownOutlined />
|
||||||
|
<span style={keyboardShortcutStyle}>J</span>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<div style={{ width: 1, height: 20, background: COLOR_BORDER, margin: `0 ${SPACING_MEDIUM}px` }} />
|
<div style={{ width: 1, height: 20, background: COLOR_BORDER, margin: `0 ${SPACING_MEDIUM}px` }} />
|
||||||
|
|
||||||
<Button type="text" icon={<CloseOutlined />} onClick={onClose} />
|
<Tooltip title="ESC to close">
|
||||||
|
<Button type="text" icon={<CloseOutlined />} onClick={onClose} />
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,22 @@
|
||||||
import { Typography } from "antd";
|
import { Typography } from "antd";
|
||||||
import { JsonView, defaultStyles } from "react-json-view-lite";
|
import { JsonView, defaultStyles } from "react-json-view-lite";
|
||||||
import "react-json-view-lite/dist/index.css";
|
import "react-json-view-lite/dist/index.css";
|
||||||
import {
|
import { JSON_MAX_HEIGHT, COLOR_BG_LIGHT, SPACING_LARGE } from "./constants";
|
||||||
JSON_MAX_HEIGHT,
|
|
||||||
FONT_SIZE_SMALL,
|
|
||||||
COLOR_BG_LIGHT,
|
|
||||||
SPACING_LARGE,
|
|
||||||
FONT_FAMILY_MONO,
|
|
||||||
VIEW_MODE_JSON,
|
|
||||||
} from "./constants";
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
export type ViewMode = "formatted" | "json";
|
|
||||||
|
|
||||||
interface JsonViewerProps {
|
interface JsonViewerProps {
|
||||||
data: any;
|
data: any;
|
||||||
mode: ViewMode;
|
mode: "formatted";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays JSON data in either formatted tree view or raw JSON format.
|
* Displays JSON data in formatted tree view.
|
||||||
* Formatted view uses an interactive tree, JSON view shows raw stringified output.
|
* Uses an interactive tree component for easy navigation.
|
||||||
*/
|
*/
|
||||||
export function JsonViewer({ data, mode }: JsonViewerProps) {
|
export function JsonViewer({ data }: JsonViewerProps) {
|
||||||
if (!data) return <Text type="secondary">No data</Text>;
|
if (!data) return <Text type="secondary">No data</Text>;
|
||||||
|
|
||||||
if (mode === VIEW_MODE_JSON) {
|
|
||||||
return (
|
|
||||||
<pre
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
wordBreak: "break-word",
|
|
||||||
maxHeight: JSON_MAX_HEIGHT,
|
|
||||||
overflow: "auto",
|
|
||||||
fontSize: FONT_SIZE_SMALL,
|
|
||||||
background: COLOR_BG_LIGHT,
|
|
||||||
padding: SPACING_LARGE,
|
|
||||||
borderRadius: 4,
|
|
||||||
fontFamily: FONT_FAMILY_MONO,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{JSON.stringify(data, null, 2)}
|
|
||||||
</pre>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Formatted tree view
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Drawer, Typography, Button, Descriptions, Card, Tag, Tabs, Radio, Alert, message } from "antd";
|
import { Drawer, Typography, Button, Descriptions, Card, Tag, Tabs, Alert, message, Collapse } from "antd";
|
||||||
import { CopyOutlined } from "@ant-design/icons";
|
import { CopyOutlined, DownOutlined } from "@ant-design/icons";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { LogEntry } from "../columns";
|
import { LogEntry } from "../columns";
|
||||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||||
|
|
@ -10,7 +10,7 @@ import { ConfigInfoMessage } from "../ConfigInfoMessage";
|
||||||
import { VectorStoreViewer } from "../VectorStoreViewer";
|
import { VectorStoreViewer } from "../VectorStoreViewer";
|
||||||
import { TruncatedValue } from "./TruncatedValue";
|
import { TruncatedValue } from "./TruncatedValue";
|
||||||
import { TokenFlow } from "./TokenFlow";
|
import { TokenFlow } from "./TokenFlow";
|
||||||
import { JsonViewer, ViewMode } from "./JsonViewer";
|
import { JsonViewer } from "./JsonViewer";
|
||||||
import { DrawerHeader } from "./DrawerHeader";
|
import { DrawerHeader } from "./DrawerHeader";
|
||||||
import { copyToClipboard } from "./clipboardUtils";
|
import { copyToClipboard } from "./clipboardUtils";
|
||||||
import { useKeyboardNavigation } from "./useKeyboardNavigation";
|
import { useKeyboardNavigation } from "./useKeyboardNavigation";
|
||||||
|
|
@ -21,7 +21,6 @@ import {
|
||||||
METADATA_MAX_HEIGHT,
|
METADATA_MAX_HEIGHT,
|
||||||
TAB_REQUEST,
|
TAB_REQUEST,
|
||||||
TAB_RESPONSE,
|
TAB_RESPONSE,
|
||||||
VIEW_MODE_FORMATTED,
|
|
||||||
FONT_SIZE_SMALL,
|
FONT_SIZE_SMALL,
|
||||||
FONT_FAMILY_MONO,
|
FONT_FAMILY_MONO,
|
||||||
SPACING_XLARGE,
|
SPACING_XLARGE,
|
||||||
|
|
@ -58,7 +57,6 @@ export function LogDetailsDrawer({
|
||||||
onSelectLog,
|
onSelectLog,
|
||||||
}: LogDetailsDrawerProps) {
|
}: LogDetailsDrawerProps) {
|
||||||
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 [jsonViewMode, setJsonViewMode] = useState<ViewMode>(VIEW_MODE_FORMATTED);
|
|
||||||
|
|
||||||
// Keyboard navigation
|
// Keyboard navigation
|
||||||
const { selectNextLog, selectPreviousLog } = useKeyboardNavigation({
|
const { selectNextLog, selectPreviousLog } = useKeyboardNavigation({
|
||||||
|
|
@ -162,8 +160,9 @@ export function LogDetailsDrawer({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Request Details Section */}
|
{/* Request Details Section */}
|
||||||
<Card title="Request Details" size="small" style={{ marginBottom: SPACING_XLARGE }}>
|
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden" style={{ marginBottom: SPACING_XLARGE }}>
|
||||||
<Descriptions column={2} size="small">
|
<Card title="Request Details" size="small" bordered={false} style={{ marginBottom: 0 }}>
|
||||||
|
<Descriptions column={2} size="small">
|
||||||
<Descriptions.Item label="Model">{logEntry.model}</Descriptions.Item>
|
<Descriptions.Item label="Model">{logEntry.model}</Descriptions.Item>
|
||||||
<Descriptions.Item label="Provider">{logEntry.custom_llm_provider || "-"}</Descriptions.Item>
|
<Descriptions.Item label="Provider">{logEntry.custom_llm_provider || "-"}</Descriptions.Item>
|
||||||
<Descriptions.Item label="Call Type">{logEntry.call_type}</Descriptions.Item>
|
<Descriptions.Item label="Call Type">{logEntry.call_type}</Descriptions.Item>
|
||||||
|
|
@ -183,6 +182,7 @@ export function LogDetailsDrawer({
|
||||||
)}
|
)}
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Metrics Section */}
|
{/* Metrics Section */}
|
||||||
<MetricsSection logEntry={logEntry} metadata={metadata} />
|
<MetricsSection logEntry={logEntry} metadata={metadata} />
|
||||||
|
|
@ -193,31 +193,19 @@ export function LogDetailsDrawer({
|
||||||
{/* Configuration Info Message - Show when data is missing */}
|
{/* Configuration Info Message - Show when data is missing */}
|
||||||
<ConfigInfoMessage show={missingData} onOpenSettings={onOpenSettings} />
|
<ConfigInfoMessage show={missingData} onOpenSettings={onOpenSettings} />
|
||||||
|
|
||||||
{/* Request/Response JSON - Using Tabs with View Toggle */}
|
{/* Request/Response JSON - Collapsible */}
|
||||||
<RequestResponseSection
|
<RequestResponseSection
|
||||||
activeTab={activeTab}
|
|
||||||
jsonViewMode={jsonViewMode}
|
|
||||||
hasResponse={hasResponse}
|
hasResponse={hasResponse}
|
||||||
onTabChange={setActiveTab}
|
|
||||||
onViewModeChange={setJsonViewMode}
|
|
||||||
onCopy={(data, label) => copyToClipboard(JSON.stringify(data, null, 2), label)}
|
onCopy={(data, label) => copyToClipboard(JSON.stringify(data, null, 2), label)}
|
||||||
getRawRequest={getRawRequest}
|
getRawRequest={getRawRequest}
|
||||||
getFormattedResponse={getFormattedResponse}
|
getFormattedResponse={getFormattedResponse}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Guardrail Data - Show only if present */}
|
{/* Guardrail Data - Show only if present */}
|
||||||
{hasGuardrailData && (
|
{hasGuardrailData && <GuardrailViewer data={guardrailInfo} />}
|
||||||
<div style={{ marginBottom: SPACING_XLARGE }}>
|
|
||||||
<GuardrailViewer data={guardrailInfo} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Vector Store Request Data - Show only if present */}
|
{/* Vector Store Request Data - Show only if present */}
|
||||||
{hasVectorStoreData && (
|
{hasVectorStoreData && <VectorStoreViewer data={metadata.vector_store_request_metadata} />}
|
||||||
<div style={{ marginBottom: SPACING_XLARGE }}>
|
|
||||||
<VectorStoreViewer data={metadata.vector_store_request_metadata} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Metadata Card - Only show if there's metadata */}
|
{/* Metadata Card - Only show if there's metadata */}
|
||||||
{logEntry.metadata && Object.keys(logEntry.metadata).length > 0 && (
|
{logEntry.metadata && Object.keys(logEntry.metadata).length > 0 && (
|
||||||
|
|
@ -251,8 +239,8 @@ function ErrorDescription({ errorInfo }: { errorInfo: any }) {
|
||||||
|
|
||||||
function TagsSection({ tags }: { tags: Record<string, any> }) {
|
function TagsSection({ tags }: { tags: Record<string, any> }) {
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: SPACING_XLARGE }}>
|
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden p-4" style={{ marginBottom: SPACING_XLARGE }}>
|
||||||
<Text strong style={{ display: "block", marginBottom: 8 }}>
|
<Text strong style={{ display: "block", marginBottom: 8, fontSize: 16 }}>
|
||||||
Tags
|
Tags
|
||||||
</Text>
|
</Text>
|
||||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||||
|
|
@ -286,8 +274,9 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
|
||||||
metadata.additional_usage_values.cache_read_input_tokens > 0);
|
metadata.additional_usage_values.cache_read_input_tokens > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="Metrics" size="small" style={{ marginBottom: SPACING_XLARGE }}>
|
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden" style={{ marginBottom: SPACING_XLARGE }}>
|
||||||
<Descriptions column={2} size="small">
|
<Card title="Metrics" size="small" bordered={false} style={{ marginBottom: 0 }}>
|
||||||
|
<Descriptions column={2} size="small">
|
||||||
<Descriptions.Item label="Tokens">
|
<Descriptions.Item label="Tokens">
|
||||||
<TokenFlow
|
<TokenFlow
|
||||||
prompt={logEntry.prompt_tokens}
|
prompt={logEntry.prompt_tokens}
|
||||||
|
|
@ -317,7 +306,7 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{metadata?.litellm_overhead_time_ms !== undefined && (
|
{metadata?.litellm_overhead_time_ms !== undefined && metadata.litellm_overhead_time_ms !== null && (
|
||||||
<Descriptions.Item label="LiteLLM Overhead">
|
<Descriptions.Item label="LiteLLM Overhead">
|
||||||
{metadata.litellm_overhead_time_ms.toFixed(2)} ms
|
{metadata.litellm_overhead_time_ms.toFixed(2)} ms
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
|
@ -331,30 +320,26 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata:
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequestResponseSectionProps {
|
interface RequestResponseSectionProps {
|
||||||
activeTab: typeof TAB_REQUEST | typeof TAB_RESPONSE;
|
|
||||||
jsonViewMode: ViewMode;
|
|
||||||
hasResponse: boolean;
|
hasResponse: boolean;
|
||||||
onTabChange: (key: typeof TAB_REQUEST | typeof TAB_RESPONSE) => void;
|
|
||||||
onViewModeChange: (mode: ViewMode) => void;
|
|
||||||
onCopy: (data: any, label: string) => void;
|
onCopy: (data: any, label: string) => void;
|
||||||
getRawRequest: () => any;
|
getRawRequest: () => any;
|
||||||
getFormattedResponse: () => any;
|
getFormattedResponse: () => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RequestResponseSection({
|
function RequestResponseSection({
|
||||||
activeTab,
|
|
||||||
jsonViewMode,
|
|
||||||
hasResponse,
|
hasResponse,
|
||||||
onTabChange,
|
|
||||||
onViewModeChange,
|
|
||||||
onCopy,
|
onCopy,
|
||||||
getRawRequest,
|
getRawRequest,
|
||||||
getFormattedResponse,
|
getFormattedResponse,
|
||||||
}: RequestResponseSectionProps) {
|
}: RequestResponseSectionProps) {
|
||||||
|
const [activeTab, setActiveTab] = useState<typeof TAB_REQUEST | typeof TAB_RESPONSE>(TAB_REQUEST);
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(true);
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
const data = activeTab === TAB_REQUEST ? getRawRequest() : getFormattedResponse();
|
const data = activeTab === TAB_REQUEST ? getRawRequest() : getFormattedResponse();
|
||||||
const label = activeTab === TAB_REQUEST ? "Request" : "Response";
|
const label = activeTab === TAB_REQUEST ? "Request" : "Response";
|
||||||
|
|
@ -362,98 +347,109 @@ function RequestResponseSection({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden" style={{ marginBottom: SPACING_XLARGE }}>
|
||||||
title="Request & Response"
|
<Collapse
|
||||||
size="small"
|
activeKey={isOpen ? ["request-response"] : []}
|
||||||
style={{ marginBottom: SPACING_XLARGE }}
|
onChange={(keys) => setIsOpen(keys.includes("request-response"))}
|
||||||
styles={{ body: { padding: 0 } }}
|
expandIcon={({ isActive }) => <DownOutlined rotate={isActive ? 180 : 0} />}
|
||||||
>
|
bordered={false}
|
||||||
<Tabs
|
|
||||||
activeKey={activeTab}
|
|
||||||
onChange={(key) => onTabChange(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)}
|
|
||||||
tabBarExtraContent={
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 8, paddingRight: SPACING_XLARGE }}>
|
|
||||||
{/* View Mode Toggle */}
|
|
||||||
<Radio.Group size="small" value={jsonViewMode} onChange={(e) => onViewModeChange(e.target.value)}>
|
|
||||||
<Radio.Button value={VIEW_MODE_FORMATTED}>Formatted</Radio.Button>
|
|
||||||
<Radio.Button value="json">JSON</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
|
|
||||||
{/* Copy Button */}
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<CopyOutlined />}
|
|
||||||
onClick={handleCopy}
|
|
||||||
disabled={activeTab === TAB_RESPONSE && !hasResponse}
|
|
||||||
>
|
|
||||||
Copy
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: TAB_REQUEST,
|
key: "request-response",
|
||||||
label: "Request",
|
label: <span style={{ fontSize: 16, fontWeight: 500 }}>Request & Response</span>,
|
||||||
children: (
|
children: (
|
||||||
<div style={{ padding: SPACING_XLARGE }}>
|
<Tabs
|
||||||
<JsonViewer data={getRawRequest()} mode={jsonViewMode} />
|
activeKey={activeTab}
|
||||||
</div>
|
onChange={(key) => setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)}
|
||||||
),
|
tabBarExtraContent={
|
||||||
},
|
<Button
|
||||||
{
|
type="text"
|
||||||
key: TAB_RESPONSE,
|
size="small"
|
||||||
label: "Response",
|
icon={<CopyOutlined />}
|
||||||
children: (
|
onClick={handleCopy}
|
||||||
<div style={{ padding: SPACING_XLARGE }}>
|
disabled={activeTab === TAB_RESPONSE && !hasResponse}
|
||||||
{hasResponse ? (
|
>
|
||||||
<JsonViewer data={getFormattedResponse()} mode={jsonViewMode} />
|
Copy
|
||||||
) : (
|
</Button>
|
||||||
<div style={{ textAlign: "center", padding: 20, color: "#999", fontStyle: "italic" }}>
|
}
|
||||||
Response data not available
|
items={[
|
||||||
</div>
|
{
|
||||||
)}
|
key: TAB_REQUEST,
|
||||||
</div>
|
label: "Request",
|
||||||
|
children: (
|
||||||
|
<div style={{ paddingTop: SPACING_XLARGE }}>
|
||||||
|
<JsonViewer data={getRawRequest()} mode="formatted" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: TAB_RESPONSE,
|
||||||
|
label: "Response",
|
||||||
|
children: (
|
||||||
|
<div style={{ paddingTop: SPACING_XLARGE }}>
|
||||||
|
{hasResponse ? (
|
||||||
|
<JsonViewer data={getFormattedResponse()} mode="formatted" />
|
||||||
|
) : (
|
||||||
|
<div style={{ textAlign: "center", padding: 20, color: "#999", fontStyle: "italic" }}>
|
||||||
|
Response data not available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
style={{ padding: `0 ${SPACING_XLARGE}px` }}
|
styles={{
|
||||||
|
header: {
|
||||||
|
padding: "16px",
|
||||||
|
borderBottom: "1px solid #f0f0f0",
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MetadataSection({ metadata, onCopy }: { metadata: Record<string, any>; onCopy: (data: string) => void }) {
|
function MetadataSection({ metadata, onCopy }: { metadata: Record<string, any>; onCopy: (data: string) => void }) {
|
||||||
return (
|
return (
|
||||||
<Card
|
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden" style={{ marginBottom: SPACING_XLARGE }}>
|
||||||
title="Metadata"
|
<Card
|
||||||
size="small"
|
title="Metadata"
|
||||||
style={{ marginBottom: SPACING_XLARGE }}
|
size="small"
|
||||||
extra={
|
bordered={false}
|
||||||
<Button
|
style={{ marginBottom: 0 }}
|
||||||
type="text"
|
extra={
|
||||||
size="small"
|
<Button
|
||||||
icon={<CopyOutlined />}
|
type="text"
|
||||||
onClick={() => onCopy(JSON.stringify(metadata, null, 2))}
|
size="small"
|
||||||
>
|
icon={<CopyOutlined />}
|
||||||
Copy
|
onClick={() => onCopy(JSON.stringify(metadata, null, 2))}
|
||||||
</Button>
|
>
|
||||||
}
|
Copy
|
||||||
>
|
</Button>
|
||||||
<pre
|
}
|
||||||
style={{
|
|
||||||
maxHeight: METADATA_MAX_HEIGHT,
|
|
||||||
overflowY: "auto",
|
|
||||||
fontSize: FONT_SIZE_SMALL,
|
|
||||||
fontFamily: FONT_FAMILY_MONO,
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
wordBreak: "break-all",
|
|
||||||
margin: 0,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{JSON.stringify(metadata, null, 2)}
|
<pre
|
||||||
</pre>
|
style={{
|
||||||
</Card>
|
maxHeight: METADATA_MAX_HEIGHT,
|
||||||
|
overflowY: "auto",
|
||||||
|
fontSize: FONT_SIZE_SMALL,
|
||||||
|
fontFamily: FONT_FAMILY_MONO,
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
wordBreak: "break-all",
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{JSON.stringify(metadata, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { Typography } from "antd";
|
import { Typography } from "antd";
|
||||||
import { COLOR_SECONDARY, FONT_FAMILY_MONO, FONT_SIZE_MEDIUM, SPACING_SMALL, SPACING_MEDIUM } from "./constants";
|
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
|
@ -10,18 +9,14 @@ interface TokenFlowProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays token usage in a flow format: "prompt → completion (Σ total)"
|
* Displays token usage in LiteLLM format: "12 (9 prompt tokens + 3 completion tokens)"
|
||||||
* Makes it easy to see the relationship between input, output, and total tokens.
|
* Shows total with breakdown of prompt and completion tokens.
|
||||||
*/
|
*/
|
||||||
export function TokenFlow({ prompt = 0, completion = 0, total = 0 }: TokenFlowProps) {
|
export function TokenFlow({ prompt = 0, completion = 0, total = 0 }: TokenFlowProps) {
|
||||||
return (
|
return (
|
||||||
<Text style={{ fontFamily: FONT_FAMILY_MONO, fontSize: FONT_SIZE_MEDIUM }}>
|
<Text>
|
||||||
{prompt.toLocaleString()}
|
{total.toLocaleString()} ({prompt.toLocaleString()} prompt tokens + {completion.toLocaleString()} completion
|
||||||
<span style={{ color: COLOR_SECONDARY, margin: `0 ${SPACING_SMALL}px` }}>→</span>
|
tokens)
|
||||||
{completion.toLocaleString()}
|
|
||||||
<span style={{ color: COLOR_SECONDARY, marginLeft: SPACING_MEDIUM }}>
|
|
||||||
(Σ {total.toLocaleString()})
|
|
||||||
</span>
|
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,10 @@ export const API_BASE_MAX_WIDTH = 200;
|
||||||
export const JSON_MAX_HEIGHT = 400;
|
export const JSON_MAX_HEIGHT = 400;
|
||||||
export const METADATA_MAX_HEIGHT = 300;
|
export const METADATA_MAX_HEIGHT = 300;
|
||||||
|
|
||||||
// Tab keys
|
// Tab keys (kept for backwards compatibility if needed)
|
||||||
export const TAB_REQUEST = "request" as const;
|
export const TAB_REQUEST = "request" as const;
|
||||||
export const TAB_RESPONSE = "response" as const;
|
export const TAB_RESPONSE = "response" as const;
|
||||||
|
|
||||||
// View modes
|
|
||||||
export const VIEW_MODE_FORMATTED = "formatted" as const;
|
|
||||||
export const VIEW_MODE_JSON = "json" as const;
|
|
||||||
|
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
export const KEY_ESCAPE = "Escape";
|
export const KEY_ESCAPE = "Escape";
|
||||||
export const KEY_J_LOWER = "j";
|
export const KEY_J_LOWER = "j";
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue