mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
Merge pull request #20657 from BerriAI/litellm_ui_logs_request
[Fix] UI - Logs: Input and Output Copying
This commit is contained in:
commit
5a084cef41
4 changed files with 394 additions and 2 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue