mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-23 00:41:40 +00:00
clean ui
This commit is contained in:
parent
e1b1263da1
commit
316fff15db
13 changed files with 723 additions and 124 deletions
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* CollapsibleMessage - Collapsible message with arrow and char count
|
||||
* Used for system messages
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import { DownOutlined, RightOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface CollapsibleMessageProps {
|
||||
label: string;
|
||||
content?: string;
|
||||
defaultExpanded?: boolean;
|
||||
}
|
||||
|
||||
export function CollapsibleMessage({
|
||||
label,
|
||||
content,
|
||||
defaultExpanded = false
|
||||
}: CollapsibleMessageProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
const charCount = content?.length || 0;
|
||||
|
||||
if (!content || charCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
{/* Clickable Header */}
|
||||
<div
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
cursor: 'pointer',
|
||||
marginBottom: isExpanded ? 6 : 0,
|
||||
}}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<DownOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />
|
||||
) : (
|
||||
<RightOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />
|
||||
)}
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
{label}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
({charCount.toLocaleString()} chars)
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{isExpanded && (
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 16,
|
||||
fontSize: 13,
|
||||
lineHeight: 1.6,
|
||||
color: '#262626',
|
||||
borderLeft: '1px solid #f0f0f0',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,73 +1,58 @@
|
|||
/**
|
||||
* HistorySection - Collapsible section for displaying message history
|
||||
* Shows a summary when collapsed, full messages when expanded
|
||||
* HistoryDivider - Collapsible divider for message history
|
||||
* Dashed line with expandable content
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import { UpOutlined, DownOutlined } from '@ant-design/icons';
|
||||
import { ParsedMessage } from './prettyMessagesTypes';
|
||||
import { MessageCard } from './MessageCard';
|
||||
import { ROLE_STYLES } from './prettyMessagesUtils';
|
||||
import { MessageBlock } from './MessageBlock';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface HistorySectionProps {
|
||||
interface HistoryDividerProps {
|
||||
messages: ParsedMessage[];
|
||||
}
|
||||
|
||||
export function HistorySection({ messages }: HistorySectionProps) {
|
||||
export function HistoryDivider({ messages }: HistoryDividerProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
if (messages.length === 0) return null;
|
||||
|
||||
// Build summary strip showing message flow
|
||||
const summary = messages
|
||||
.map((m) => {
|
||||
const style = ROLE_STYLES[m.role] || ROLE_STYLES.user;
|
||||
return style.label;
|
||||
})
|
||||
.join(' → ');
|
||||
|
||||
return (
|
||||
<div style={{ margin: '16px 0' }}>
|
||||
{/* Collapsed: Dashed line with label */}
|
||||
<div style={{ margin: '12px 0' }}>
|
||||
{/* Dashed Divider with Label */}
|
||||
<div
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
gap: 12,
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, borderTop: '1px dashed #d9d9d9' }} />
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
History ({messages.length} message{messages.length !== 1 ? 's' : ''})
|
||||
{!isExpanded && ` · ${summary}`}
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
History ({messages.length})
|
||||
</Text>
|
||||
{isExpanded ? (
|
||||
<UpOutlined style={{ color: '#8c8c8c', fontSize: 10 }} />
|
||||
<UpOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />
|
||||
) : (
|
||||
<DownOutlined style={{ color: '#8c8c8c', fontSize: 10 }} />
|
||||
<DownOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />
|
||||
)}
|
||||
<div style={{ flex: 1, borderTop: '1px dashed #d9d9d9' }} />
|
||||
</div>
|
||||
|
||||
{/* Expanded View - Full Messages with subtle indent */}
|
||||
{/* Expanded View - Full Messages */}
|
||||
{isExpanded && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
paddingLeft: 16,
|
||||
borderLeft: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
{messages.map((msg, index) => (
|
||||
<MessageCard
|
||||
<MessageBlock
|
||||
key={index}
|
||||
message={msg}
|
||||
defaultCollapsed={msg.content?.length > 500}
|
||||
role={msg.role.toUpperCase()}
|
||||
content={msg.content}
|
||||
toolCalls={msg.toolCalls}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* HistoryTree - Collapsible tree view for message history
|
||||
* Shows arrow indicator and message count
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import { DownOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { ParsedMessage } from './prettyMessagesTypes';
|
||||
import { SimpleMessageBlock } from './SimpleMessageBlock';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface HistoryTreeProps {
|
||||
messages: ParsedMessage[];
|
||||
}
|
||||
|
||||
export function HistoryTree({ messages }: HistoryTreeProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
if (messages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
{/* Clickable Header */}
|
||||
<div
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
cursor: 'pointer',
|
||||
marginBottom: isExpanded ? 8 : 0,
|
||||
}}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<DownOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />
|
||||
) : (
|
||||
<RightOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />
|
||||
)}
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
HISTORY ({messages.length} message{messages.length !== 1 ? 's' : ''})
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Expanded Tree Content */}
|
||||
{isExpanded && (
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 16,
|
||||
borderLeft: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
{messages.map((msg, index) => (
|
||||
<SimpleMessageBlock
|
||||
key={index}
|
||||
label={msg.role.toUpperCase()}
|
||||
content={msg.content}
|
||||
toolCalls={msg.toolCalls}
|
||||
isCompact={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* InputCard - Displays all input messages with token count and cost
|
||||
* Datadog-style: header with icon/metrics, content below
|
||||
*/
|
||||
|
||||
import { message } from 'antd';
|
||||
import { ParsedMessage } from './prettyMessagesTypes';
|
||||
import { SectionHeader } from './SectionHeader';
|
||||
import { CollapsibleMessage } from './CollapsibleMessage';
|
||||
import { HistoryTree } from './HistoryTree';
|
||||
import { SimpleMessageBlock } from './SimpleMessageBlock';
|
||||
|
||||
interface InputCardProps {
|
||||
messages: ParsedMessage[];
|
||||
promptTokens?: number;
|
||||
inputCost?: number;
|
||||
}
|
||||
|
||||
export function InputCard({ messages, promptTokens, inputCost }: InputCardProps) {
|
||||
if (messages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Separate system, history, and last message
|
||||
const systemMessage = messages.find((m) => m.role === 'system');
|
||||
const nonSystemMessages = messages.filter((m) => m.role !== 'system');
|
||||
const lastMessage = nonSystemMessages.length > 0 ? nonSystemMessages[nonSystemMessages.length - 1] : null;
|
||||
const historyMessages = nonSystemMessages.slice(0, -1);
|
||||
|
||||
const handleCopy = () => {
|
||||
const content = JSON.stringify(messages, null, 2);
|
||||
navigator.clipboard.writeText(content);
|
||||
message.success('Input copied');
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 6,
|
||||
marginBottom: 12,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Datadog-style Header */}
|
||||
<SectionHeader
|
||||
type="input"
|
||||
tokens={promptTokens}
|
||||
cost={inputCost}
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ padding: '12px 14px' }}>
|
||||
{/* System Message - Collapsible with arrow */}
|
||||
{systemMessage && (
|
||||
<CollapsibleMessage
|
||||
label="SYSTEM"
|
||||
content={systemMessage.content}
|
||||
defaultExpanded={!!(systemMessage.content && systemMessage.content.length < 200)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* History - Tree style, collapsed by default */}
|
||||
{historyMessages.length > 0 && <HistoryTree messages={historyMessages} />}
|
||||
|
||||
{/* Last User Message - Always visible */}
|
||||
{lastMessage && (
|
||||
<SimpleMessageBlock
|
||||
label={lastMessage.role.toUpperCase()}
|
||||
content={lastMessage.content}
|
||||
toolCalls={lastMessage.toolCalls}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -208,6 +208,7 @@ export function LogDetailsDrawer({
|
|||
onCopy={(data, label) => copyToClipboard(JSON.stringify(data, null, 2), label)}
|
||||
getRawRequest={getRawRequest}
|
||||
getFormattedResponse={getFormattedResponse}
|
||||
logEntry={logEntry}
|
||||
/>
|
||||
|
||||
{/* Guardrail Data - Show only if present */}
|
||||
|
|
@ -341,6 +342,7 @@ interface RequestResponseSectionProps {
|
|||
onCopy: (data: any, label: string) => void;
|
||||
getRawRequest: () => any;
|
||||
getFormattedResponse: () => any;
|
||||
logEntry: LogEntry;
|
||||
}
|
||||
|
||||
function RequestResponseSection({
|
||||
|
|
@ -348,6 +350,7 @@ function RequestResponseSection({
|
|||
onCopy,
|
||||
getRawRequest,
|
||||
getFormattedResponse,
|
||||
logEntry,
|
||||
}: RequestResponseSectionProps) {
|
||||
const [activeTab, setActiveTab] = useState<typeof TAB_REQUEST | typeof TAB_RESPONSE>(TAB_REQUEST);
|
||||
const [viewMode, setViewMode] = useState<'pretty' | 'json'>('pretty');
|
||||
|
|
@ -358,6 +361,17 @@ function RequestResponseSection({
|
|||
onCopy(data, label);
|
||||
};
|
||||
|
||||
// Calculate input and output costs
|
||||
// Assume average cost if not explicitly provided
|
||||
const totalSpend = logEntry.spend || 0;
|
||||
const promptTokens = logEntry.prompt_tokens || 0;
|
||||
const completionTokens = logEntry.completion_tokens || 0;
|
||||
const totalTokens = promptTokens + completionTokens;
|
||||
|
||||
// Estimate input/output costs proportionally if not available
|
||||
const inputCost = totalTokens > 0 ? (totalSpend * promptTokens) / totalTokens : 0;
|
||||
const outputCost = totalTokens > 0 ? (totalSpend * completionTokens) / totalTokens : 0;
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden mb-6">
|
||||
<Collapse
|
||||
|
|
@ -385,6 +399,12 @@ function RequestResponseSection({
|
|||
<PrettyMessagesView
|
||||
request={getRawRequest()}
|
||||
response={getFormattedResponse()}
|
||||
metrics={{
|
||||
prompt_tokens: promptTokens,
|
||||
completion_tokens: completionTokens,
|
||||
input_cost: inputCost,
|
||||
output_cost: outputCost,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Tabs
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* MessageBlock - Displays a single message with role label
|
||||
* No colors, minimal gray styling
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Typography, Button } from 'antd';
|
||||
import { ToolCall } from './prettyMessagesTypes';
|
||||
import { ToolCallBlock } from './ToolCallBlock';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface MessageBlockProps {
|
||||
role: string;
|
||||
content?: string;
|
||||
toolCalls?: ToolCall[];
|
||||
}
|
||||
|
||||
const TRUNCATE_LENGTH = 500;
|
||||
|
||||
export function MessageBlock({ role, content, toolCalls }: MessageBlockProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const hasContent = content && content.length > 0;
|
||||
const hasToolCalls = toolCalls && toolCalls.length > 0;
|
||||
const isLong = hasContent && content.length > TRUNCATE_LENGTH;
|
||||
const shouldTruncate = isLong && !isExpanded;
|
||||
|
||||
// If no content and no tool calls, don't render anything
|
||||
if (!hasContent && !hasToolCalls) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
{/* Role Label */}
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: 11,
|
||||
display: 'block',
|
||||
marginBottom: 4,
|
||||
color: '#8c8c8c',
|
||||
}}
|
||||
>
|
||||
{role}
|
||||
</Text>
|
||||
|
||||
{/* Content */}
|
||||
{hasContent && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
lineHeight: 1.6,
|
||||
color: '#262626',
|
||||
marginBottom: hasToolCalls ? 8 : 0,
|
||||
}}
|
||||
>
|
||||
{shouldTruncate ? (
|
||||
<>
|
||||
{content.slice(0, TRUNCATE_LENGTH)}...
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => setIsExpanded(true)}
|
||||
style={{ padding: '0 4px', fontSize: 12 }}
|
||||
>
|
||||
Show more
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{content}
|
||||
{isLong && isExpanded && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => setIsExpanded(false)}
|
||||
style={{
|
||||
padding: '0 4px',
|
||||
fontSize: 12,
|
||||
display: 'block',
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
Show less
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool Calls */}
|
||||
{hasToolCalls && (
|
||||
<div>
|
||||
{toolCalls.map((tool, index) => (
|
||||
<ToolCallBlock key={tool.id || index} tool={tool} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* OutputCard - Displays output message with token count and cost
|
||||
* Datadog-style: header with icon/metrics, content below
|
||||
*/
|
||||
|
||||
import { Typography, message as antdMessage } from 'antd';
|
||||
import { ParsedMessage } from './prettyMessagesTypes';
|
||||
import { SectionHeader } from './SectionHeader';
|
||||
import { SimpleMessageBlock } from './SimpleMessageBlock';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface OutputCardProps {
|
||||
message: ParsedMessage | null;
|
||||
completionTokens?: number;
|
||||
outputCost?: number;
|
||||
}
|
||||
|
||||
export function OutputCard({ message, completionTokens, outputCost }: OutputCardProps) {
|
||||
const handleCopy = () => {
|
||||
if (!message) return;
|
||||
|
||||
const content = JSON.stringify(message, null, 2);
|
||||
navigator.clipboard.writeText(content);
|
||||
antdMessage.success('Output copied');
|
||||
};
|
||||
|
||||
if (!message) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<SectionHeader
|
||||
type="output"
|
||||
tokens={completionTokens}
|
||||
cost={outputCost}
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
<div style={{ padding: '12px 14px' }}>
|
||||
<Text type="secondary" style={{ fontSize: 13, fontStyle: 'italic' }}>
|
||||
No response data available
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 6,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Datadog-style Header */}
|
||||
<SectionHeader
|
||||
type="output"
|
||||
tokens={completionTokens}
|
||||
cost={outputCost}
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ padding: '12px 14px' }}>
|
||||
<SimpleMessageBlock
|
||||
label="ASSISTANT"
|
||||
content={message.content}
|
||||
toolCalls={message.toolCalls}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,108 +1,41 @@
|
|||
/**
|
||||
* PrettyMessagesView - Chat-style view of request and response messages
|
||||
* Replaces raw JSON with scannable, readable message cards
|
||||
* PrettyMessagesView - Datadog-style view with Input/Output cards
|
||||
* Two main cards showing request and response with token counts and costs
|
||||
*/
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import { parseMessages } from './prettyMessagesUtils';
|
||||
import { MessageCard } from './MessageCard';
|
||||
import { HistorySection } from './HistorySection';
|
||||
|
||||
const { Text } = Typography;
|
||||
import { InputCard } from './InputCard';
|
||||
import { OutputCard } from './OutputCard';
|
||||
|
||||
interface PrettyMessagesViewProps {
|
||||
request: any;
|
||||
response: any;
|
||||
metrics?: {
|
||||
prompt_tokens?: number;
|
||||
completion_tokens?: number;
|
||||
input_cost?: number;
|
||||
output_cost?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function PrettyMessagesView({ request, response }: PrettyMessagesViewProps) {
|
||||
export function PrettyMessagesView({ request, response, metrics }: PrettyMessagesViewProps) {
|
||||
const { requestMessages, responseMessage } = parseMessages(request, response);
|
||||
|
||||
// Separate system, history, and last user message
|
||||
const systemMessage = requestMessages.find((m) => m.role === 'system');
|
||||
const nonSystemMessages = requestMessages.filter((m) => m.role !== 'system');
|
||||
const lastUserMessage =
|
||||
nonSystemMessages.length > 0 ? nonSystemMessages[nonSystemMessages.length - 1] : null;
|
||||
const historyMessages = nonSystemMessages.slice(0, -1);
|
||||
|
||||
return (
|
||||
<div style={{ paddingTop: 4, paddingBottom: 16 }}>
|
||||
{/* REQUEST SECTION */}
|
||||
<div>
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
marginBottom: 16,
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
Request ({requestMessages.length} message{requestMessages.length !== 1 ? 's' : ''})
|
||||
</Text>
|
||||
{/* Input Card */}
|
||||
<InputCard
|
||||
messages={requestMessages}
|
||||
promptTokens={metrics?.prompt_tokens}
|
||||
inputCost={metrics?.input_cost}
|
||||
/>
|
||||
|
||||
{/* System Message - Collapsed by default */}
|
||||
{systemMessage && <MessageCard message={systemMessage} defaultCollapsed={true} />}
|
||||
|
||||
{/* History - Collapsed if > 0 messages */}
|
||||
{historyMessages.length > 0 && <HistorySection messages={historyMessages} />}
|
||||
|
||||
{/* Last User Message - Always expanded */}
|
||||
{lastUserMessage && lastUserMessage.role === 'user' && (
|
||||
<MessageCard message={lastUserMessage} defaultCollapsed={false} />
|
||||
)}
|
||||
|
||||
{/* Fallback if no messages */}
|
||||
{requestMessages.length === 0 && (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: 20,
|
||||
color: '#8c8c8c',
|
||||
fontStyle: 'italic',
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
No request messages available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Section Divider */}
|
||||
<div
|
||||
style={{
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
margin: '20px 0',
|
||||
paddingTop: 16,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
marginBottom: 16,
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
Response
|
||||
</Text>
|
||||
|
||||
{/* RESPONSE SECTION */}
|
||||
{responseMessage ? (
|
||||
<MessageCard message={responseMessage} defaultCollapsed={false} showToolCalls />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: 20,
|
||||
color: '#8c8c8c',
|
||||
fontStyle: 'italic',
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
Response data not available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Output Card */}
|
||||
<OutputCard
|
||||
message={responseMessage}
|
||||
completionTokens={metrics?.completion_tokens}
|
||||
outputCost={metrics?.output_cost}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* SectionHeader - Datadog-style header with icon, label, metrics, and copy
|
||||
*/
|
||||
|
||||
import { Typography, Button, Tooltip } from 'antd';
|
||||
import {
|
||||
MessageOutlined,
|
||||
ThunderboltOutlined,
|
||||
CopyOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface SectionHeaderProps {
|
||||
type: 'input' | 'output';
|
||||
tokens?: number;
|
||||
cost?: number;
|
||||
onCopy: () => void;
|
||||
}
|
||||
|
||||
export function SectionHeader({ type, tokens, cost, onCopy }: SectionHeaderProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '10px 14px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
background: '#fafafa',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 20 }}>
|
||||
{/* Icon + Label */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
{type === 'input' ? (
|
||||
<MessageOutlined style={{ color: '#8c8c8c', fontSize: 14 }} />
|
||||
) : (
|
||||
<ThunderboltOutlined style={{ color: '#8c8c8c', fontSize: 14 }} />
|
||||
)}
|
||||
<Text strong style={{ fontSize: 13 }}>
|
||||
{type === 'input' ? 'Input' : 'Output'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Tokens */}
|
||||
{tokens !== undefined && (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
Tokens: {tokens.toLocaleString()}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Cost */}
|
||||
{cost !== undefined && (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
Cost: ${cost.toFixed(6)}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Copy Button */}
|
||||
<Tooltip title="Copy">
|
||||
<Button type="text" size="small" icon={<CopyOutlined />} onClick={onCopy} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* SimpleMessageBlock - Simple message display without collapsing
|
||||
* Used for messages in tree view and last user message
|
||||
*/
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import { ToolCall } from './prettyMessagesTypes';
|
||||
import { SimpleToolCallBlock } from './SimpleToolCallBlock';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface SimpleMessageBlockProps {
|
||||
label: string;
|
||||
content?: string;
|
||||
toolCalls?: ToolCall[];
|
||||
isCompact?: boolean;
|
||||
}
|
||||
|
||||
export function SimpleMessageBlock({
|
||||
label,
|
||||
content,
|
||||
toolCalls,
|
||||
isCompact = false
|
||||
}: SimpleMessageBlockProps) {
|
||||
// Don't show "null" for empty content
|
||||
const displayContent = content && content !== 'null' && content.length > 0 ? content : null;
|
||||
const hasToolCalls = toolCalls && toolCalls.length > 0;
|
||||
|
||||
// If no content and no tool calls, don't render
|
||||
if (!displayContent && !hasToolCalls) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: isCompact ? 10 : 0 }}>
|
||||
<Text type="secondary" style={{ fontSize: 11, display: 'block', marginBottom: 4 }}>
|
||||
{label}
|
||||
</Text>
|
||||
|
||||
{displayContent && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
lineHeight: 1.6,
|
||||
color: '#262626',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
marginBottom: hasToolCalls ? 8 : 0,
|
||||
}}
|
||||
>
|
||||
{displayContent}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Inline tool calls for assistant messages in history */}
|
||||
{hasToolCalls && (
|
||||
<div>
|
||||
{toolCalls.map((tc, index) => (
|
||||
<SimpleToolCallBlock key={tc.id || index} tool={tc} compact={isCompact} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* SimpleToolCallBlock - Simple tool call display without copy button
|
||||
* Used in compact/tree views
|
||||
*/
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import { ToolCall } from './prettyMessagesTypes';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface SimpleToolCallBlockProps {
|
||||
tool: ToolCall;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function SimpleToolCallBlock({ tool, compact = false }: SimpleToolCallBlockProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: '#fafafa',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 4,
|
||||
padding: compact ? '6px 10px' : '8px 12px',
|
||||
marginTop: 8,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>
|
||||
{tool.name}
|
||||
</Text>
|
||||
|
||||
{Object.keys(tool.arguments).length > 0 && (
|
||||
<div>
|
||||
{Object.entries(tool.arguments).map(([key, value]) => (
|
||||
<div key={key} style={{ marginBottom: 2 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{key}:{' '}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 12 }}>{JSON.stringify(value)}</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* ToolCallBlock - Displays tool call with white background
|
||||
* Minimal, monochrome styling
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Typography, Button, message } from 'antd';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { ToolCall } from './prettyMessagesTypes';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface ToolCallBlockProps {
|
||||
tool: ToolCall;
|
||||
}
|
||||
|
||||
export function ToolCallBlock({ tool }: ToolCallBlockProps) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(JSON.stringify(tool.arguments, null, 2));
|
||||
message.success('Tool arguments copied');
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: '#fff',
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: 4,
|
||||
padding: '8px 12px',
|
||||
marginTop: 8,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{/* Tool Name Header */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: Object.keys(tool.arguments).length > 0 ? 6 : 0,
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ fontSize: 12, color: '#262626' }}>
|
||||
{tool.name}
|
||||
</Text>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
style={{
|
||||
opacity: isHovered ? 1 : 0,
|
||||
transition: 'opacity 0.2s',
|
||||
}}
|
||||
onClick={handleCopy}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tool Arguments */}
|
||||
{Object.keys(tool.arguments).length > 0 && (
|
||||
<div>
|
||||
{Object.entries(tool.arguments).map(([key, value]) => (
|
||||
<div key={key} style={{ marginBottom: 2 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{key}:{' '}
|
||||
</Text>
|
||||
<Text style={{ fontSize: 12 }}>{JSON.stringify(value)}</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue