Merge pull request #20657 from BerriAI/litellm_ui_logs_request

[Fix] UI - Logs: Input and Output Copying
This commit is contained in:
yuneng-jiang 2026-02-07 16:19:32 -08:00 • committed by GitHub
commit 5a084cef41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 394 additions and 2 deletions

View file

@ -0,0 +1,219 @@
import React from "react";
import { render, screen, waitFor, fireEvent, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { InputCard } from "./InputCard";
import { ParsedMessage } from "./prettyMessagesTypes";
vi.mock("antd", async () => {
const actual = await vi.importActual<typeof import("antd")>("antd");
return {
...actual,
message: {
success: vi.fn(),
},
};
});
describe("InputCard", () => {
const mockWriteText = vi.fn().mockResolvedValue(undefined);
const mockMessages: ParsedMessage[] = [
{
role: "user",
content: "Hello, how are you?",
},
];
beforeEach(() => {
vi.clearAllMocks();
Object.defineProperty(navigator, "clipboard", {
value: {
writeText: mockWriteText,
},
writable: true,
configurable: true,
});
});
it("should render the InputCard component", () => {
render(<InputCard messages={mockMessages} />);
expect(screen.getByText("Input")).toBeInTheDocument();
});
it("should return null when messages array is empty", () => {
const { container } = render(<InputCard messages={[]} />);
expect(container.firstChild).toBeNull();
});
it("should display system message when present", () => {
const messagesWithSystem: ParsedMessage[] = [
{
role: "system",
content: "You are a helpful assistant",
},
{
role: "user",
content: "Hello",
},
];
render(<InputCard messages={messagesWithSystem} />);
expect(screen.getByText("SYSTEM")).toBeInTheDocument();
expect(screen.getByText("You are a helpful assistant")).toBeInTheDocument();
});
it("should display history messages when present", () => {
const messagesWithHistory: ParsedMessage[] = [
{
role: "user",
content: "First message",
},
{
role: "assistant",
content: "Response",
},
{
role: "user",
content: "Last message",
},
];
render(<InputCard messages={messagesWithHistory} />);
expect(screen.getByText("Last message")).toBeInTheDocument();
});
it("should display last message content", () => {
const messages: ParsedMessage[] = [
{
role: "user",
content: "What is the weather?",
},
];
render(<InputCard messages={messages} />);
expect(screen.getByText("What is the weather?")).toBeInTheDocument();
});
it("should display token count when provided", () => {
render(<InputCard messages={mockMessages} promptTokens={150} />);
expect(screen.getByText(/Tokens: 150/)).toBeInTheDocument();
});
it("should display cost when provided", () => {
render(<InputCard messages={mockMessages} inputCost={0.0015} />);
expect(screen.getByText(/Cost: \$0\.001500/)).toBeInTheDocument();
});
it("should copy last message content when copy button is clicked", async () => {
const messages: ParsedMessage[] = [
{
role: "user",
content: "Copy this text",
},
];
render(<InputCard messages={messages} />);
const copyButtons = screen.getAllByRole("button");
const copyButton = copyButtons.find((button) => {
const icon = button.querySelector('[aria-label="copy"]');
return icon !== null;
});
expect(copyButton).toBeInTheDocument();
await act(async () => {
fireEvent.click(copyButton!);
});
await waitFor(() => {
expect(mockWriteText).toHaveBeenCalledWith("Copy this text");
});
});
it("should toggle collapse state when header is clicked", async () => {
const user = userEvent.setup();
render(<InputCard messages={mockMessages} />);
const header = screen.getByText("Input").closest("div");
expect(header).toBeInTheDocument();
const content = screen.getByText("Hello, how are you?");
expect(content).toBeInTheDocument();
expect(content).toBeVisible();
if (header) {
await user.click(header);
await waitFor(() => {
expect(content).not.toBeVisible();
});
}
});
it("should handle messages without system message", () => {
const messages: ParsedMessage[] = [
{
role: "user",
content: "User message only",
},
];
render(<InputCard messages={messages} />);
expect(screen.queryByText("SYSTEM")).not.toBeInTheDocument();
expect(screen.getByText("User message only")).toBeInTheDocument();
});
it("should handle messages with only system message", () => {
const messages: ParsedMessage[] = [
{
role: "system",
content: "System only",
},
];
render(<InputCard messages={messages} />);
expect(screen.getByText("SYSTEM")).toBeInTheDocument();
expect(screen.getByText("System only")).toBeInTheDocument();
});
it("should display last message role label correctly", () => {
const messages: ParsedMessage[] = [
{
role: "assistant",
content: "Assistant response",
},
];
render(<InputCard messages={messages} />);
expect(screen.getByText("ASSISTANT")).toBeInTheDocument();
expect(screen.getByText("Assistant response")).toBeInTheDocument();
});
it("should handle tool calls in last message", () => {
const messages: ParsedMessage[] = [
{
role: "user",
content: "Call a function",
toolCalls: [
{
id: "call-1",
name: "get_weather",
arguments: { location: "NYC" },
},
],
},
];
render(<InputCard messages={messages} />);
expect(screen.getByText("Call a function")).toBeInTheDocument();
});
it("should handle empty content in last message", () => {
const messages: ParsedMessage[] = [
{
role: "user",
content: "",
},
];
render(<InputCard messages={messages} />);
const copyButtons = screen.getAllByRole("button");
const copyButton = copyButtons.find((button) => {
const icon = button.querySelector('[aria-label="copy"]');
return icon !== null;
});
expect(copyButton).toBeInTheDocument();
});
});

View file

@ -31,7 +31,7 @@ export function InputCard({ messages, promptTokens, inputCost }: InputCardProps)
const historyMessages = nonSystemMessages.slice(0, -1);
const handleCopy = () => {
const content = JSON.stringify(messages, null, 2);
const content = lastMessage?.content || '';
navigator.clipboard.writeText(content);
message.success('Input copied');
};

View file

@ -0,0 +1,173 @@
import React from "react";
import { render, screen, waitFor, fireEvent, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { OutputCard } from "./OutputCard";
import { ParsedMessage } from "./prettyMessagesTypes";
vi.mock("antd", async () => {
const actual = await vi.importActual<typeof import("antd")>("antd");
return {
...actual,
message: {
success: vi.fn(),
},
};
});
describe("OutputCard", () => {
const mockWriteText = vi.fn().mockResolvedValue(undefined);
const mockMessage: ParsedMessage = {
role: "assistant",
content: "This is a test response",
};
beforeEach(() => {
vi.clearAllMocks();
Object.defineProperty(navigator, "clipboard", {
value: {
writeText: mockWriteText,
},
writable: true,
configurable: true,
});
});
it("should render the OutputCard component", () => {
render(<OutputCard message={mockMessage} />);
expect(screen.getByText("Output")).toBeInTheDocument();
});
it("should display 'No response data available' when message is null", () => {
render(<OutputCard message={null} />);
expect(screen.getByText("No response data available")).toBeInTheDocument();
});
it("should display message content when message is present", () => {
render(<OutputCard message={mockMessage} />);
expect(screen.getByText("This is a test response")).toBeInTheDocument();
});
it("should display ASSISTANT label when message is present", () => {
render(<OutputCard message={mockMessage} />);
expect(screen.getByText("ASSISTANT")).toBeInTheDocument();
});
it("should display token count when provided", () => {
render(<OutputCard message={mockMessage} completionTokens={250} />);
expect(screen.getByText(/Tokens: 250/)).toBeInTheDocument();
});
it("should display cost when provided", () => {
render(<OutputCard message={mockMessage} outputCost={0.0025} />);
expect(screen.getByText(/Cost: \$0\.002500/)).toBeInTheDocument();
});
it("should copy message content when copy button is clicked", async () => {
render(<OutputCard message={mockMessage} />);
const copyButtons = screen.getAllByRole("button");
const copyButton = copyButtons.find((button) => {
const icon = button.querySelector('[aria-label="copy"]');
return icon !== null;
});
expect(copyButton).toBeInTheDocument();
await act(async () => {
fireEvent.click(copyButton!);
});
await waitFor(() => {
expect(mockWriteText).toHaveBeenCalledWith("This is a test response");
});
});
it("should not copy when message is null and copy button is clicked", async () => {
const user = userEvent.setup();
render(<OutputCard message={null} />);
const copyButtons = screen.getAllByRole("button");
const copyButton = copyButtons.find((button) => {
const icon = button.querySelector('[aria-label="copy"]');
return icon !== null;
});
expect(copyButton).toBeInTheDocument();
await user.click(copyButton!);
await waitFor(() => {
expect(mockWriteText).not.toHaveBeenCalled();
});
});
it("should toggle collapse state when header is clicked", async () => {
const user = userEvent.setup();
render(<OutputCard message={mockMessage} />);
const header = screen.getByText("Output").closest("div");
expect(header).toBeInTheDocument();
const content = screen.getByText("This is a test response");
expect(content).toBeInTheDocument();
expect(content).toBeVisible();
if (header) {
await user.click(header);
await waitFor(() => {
expect(content).not.toBeVisible();
});
}
});
it("should handle empty content in message", () => {
const messageWithEmptyContent: ParsedMessage = {
role: "assistant",
content: "",
};
render(<OutputCard message={messageWithEmptyContent} />);
expect(screen.getByText("Output")).toBeInTheDocument();
});
it("should handle tool calls in message", () => {
const messageWithToolCalls: ParsedMessage = {
role: "assistant",
content: "I'll call a function",
toolCalls: [
{
id: "call-1",
name: "get_weather",
arguments: { location: "San Francisco" },
},
],
};
render(<OutputCard message={messageWithToolCalls} />);
expect(screen.getByText("I'll call a function")).toBeInTheDocument();
});
it("should display both token count and cost when both are provided", () => {
render(<OutputCard message={mockMessage} completionTokens={300} outputCost={0.003} />);
expect(screen.getByText(/Tokens: 300/)).toBeInTheDocument();
expect(screen.getByText(/Cost: \$0\.003000/)).toBeInTheDocument();
});
it("should handle collapse toggle when message is null", async () => {
const user = userEvent.setup();
render(<OutputCard message={null} />);
const header = screen.getByText("Output").closest("div");
expect(header).toBeInTheDocument();
const noDataText = screen.getByText("No response data available");
expect(noDataText).toBeInTheDocument();
expect(noDataText).toBeVisible();
if (header) {
await user.click(header);
await waitFor(() => {
expect(noDataText).not.toBeVisible();
});
}
});
});

View file

@ -23,7 +23,7 @@ export function OutputCard({ message, completionTokens, outputCost }: OutputCard
const handleCopy = () => {
if (!message) return;
const content = JSON.stringify(message, null, 2);
const content = message.content || '';
navigator.clipboard.writeText(content);
antdMessage.success('Output copied');
};