Merge pull request #23973 from BerriAI/litellm_/fervent-hypatia

[Refactor] UI - Playground: Extract FilePreviewCard from ChatUI
This commit is contained in:
yuneng-jiang
2026-03-18 00:44:17 -07:00
committed by GitHub
3 changed files with 126 additions and 58 deletions
@@ -60,6 +60,7 @@ import CodeInterpreterOutput from "./CodeInterpreterOutput";
import CodeInterpreterTool from "./CodeInterpreterTool";
import { generateCodeSnippet } from "./CodeSnippets";
import EndpointSelector from "./EndpointSelector";
import FilePreviewCard from "./FilePreviewCard";
import MCPEventsDisplay from "./MCPEventsDisplay";
import type { MCPEvent } from "../../mcp_tools/types";
import { EndpointType, getEndpointType } from "./mode_endpoint_mapping";
@@ -2231,67 +2232,19 @@ const ChatUI: React.FC<ChatUIProps> = ({
{/* Show file previews above input when files are uploaded */}
{endpointType === EndpointType.RESPONSES && responsesUploadedImage && (
<div className="mb-2">
<div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
<div className="relative inline-block">
{responsesUploadedImage.name.toLowerCase().endsWith(".pdf") ? (
<div className="w-10 h-10 rounded-md bg-red-500 flex items-center justify-center">
<FilePdfOutlined style={{ fontSize: "16px", color: "white" }} />
</div>
) : (
<img
src={responsesImagePreviewUrl || ""}
alt="Upload preview"
className="w-10 h-10 rounded-md border border-gray-200 object-cover"
/>
)}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900 truncate">{responsesUploadedImage.name}</div>
<div className="text-xs text-gray-500">
{responsesUploadedImage.name.toLowerCase().endsWith(".pdf") ? "PDF" : "Image"}
</div>
</div>
<button
className="flex items-center justify-center w-6 h-6 text-gray-400 hover:text-gray-600 hover:bg-gray-200 rounded-full transition-colors"
onClick={handleRemoveResponsesImage}
>
<DeleteOutlined style={{ fontSize: "12px" }} />
</button>
</div>
</div>
<FilePreviewCard
file={responsesUploadedImage}
previewUrl={responsesImagePreviewUrl}
onRemove={handleRemoveResponsesImage}
/>
)}
{endpointType === EndpointType.CHAT && chatUploadedImage && (
<div className="mb-2">
<div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
<div className="relative inline-block">
{chatUploadedImage.name.toLowerCase().endsWith(".pdf") ? (
<div className="w-10 h-10 rounded-md bg-red-500 flex items-center justify-center">
<FilePdfOutlined style={{ fontSize: "16px", color: "white" }} />
</div>
) : (
<img
src={chatImagePreviewUrl || ""}
alt="Upload preview"
className="w-10 h-10 rounded-md border border-gray-200 object-cover"
/>
)}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900 truncate">{chatUploadedImage.name}</div>
<div className="text-xs text-gray-500">
{chatUploadedImage.name.toLowerCase().endsWith(".pdf") ? "PDF" : "Image"}
</div>
</div>
<button
className="flex items-center justify-center w-6 h-6 text-gray-400 hover:text-gray-600 hover:bg-gray-200 rounded-full transition-colors"
onClick={handleRemoveChatImage}
>
<DeleteOutlined style={{ fontSize: "12px" }} />
</button>
</div>
</div>
<FilePreviewCard
file={chatUploadedImage}
previewUrl={chatImagePreviewUrl}
onRemove={handleRemoveChatImage}
/>
)}
{/* Code Interpreter indicator and sample prompts when enabled */}
@@ -0,0 +1,70 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import FilePreviewCard from "./FilePreviewCard";
function makeFile(name: string): File {
return new File(["dummy"], name, { type: "application/octet-stream" });
}
describe("FilePreviewCard", () => {
it("should render", () => {
render(
<FilePreviewCard file={makeFile("photo.png")} previewUrl={null} onRemove={vi.fn()} />
);
expect(screen.getByText("photo.png")).toBeInTheDocument();
});
it("should display the file name", () => {
render(
<FilePreviewCard file={makeFile("my-screenshot.jpg")} previewUrl={null} onRemove={vi.fn()} />
);
expect(screen.getByText("my-screenshot.jpg")).toBeInTheDocument();
});
it("should show 'Image' label for non-PDF files", () => {
render(
<FilePreviewCard file={makeFile("photo.png")} previewUrl="blob:http://localhost/abc" onRemove={vi.fn()} />
);
expect(screen.getByText("Image")).toBeInTheDocument();
});
it("should show 'PDF' label for PDF files", () => {
render(
<FilePreviewCard file={makeFile("report.pdf")} previewUrl={null} onRemove={vi.fn()} />
);
expect(screen.getByText("PDF")).toBeInTheDocument();
});
it("should render an image preview when the file is not a PDF", () => {
render(
<FilePreviewCard file={makeFile("photo.png")} previewUrl="blob:http://localhost/abc" onRemove={vi.fn()} />
);
expect(screen.getByAltText("Upload preview")).toBeInTheDocument();
});
it("should not render an image preview when the file is a PDF", () => {
render(
<FilePreviewCard file={makeFile("doc.PDF")} previewUrl={null} onRemove={vi.fn()} />
);
expect(screen.queryByAltText("Upload preview")).not.toBeInTheDocument();
});
it("should call onRemove when the remove button is clicked", async () => {
const onRemove = vi.fn();
const user = userEvent.setup();
render(
<FilePreviewCard file={makeFile("photo.png")} previewUrl={null} onRemove={onRemove} />
);
await user.click(screen.getByRole("button"));
expect(onRemove).toHaveBeenCalledOnce();
});
it("should treat .PDF (uppercase) as a PDF file", () => {
render(
<FilePreviewCard file={makeFile("REPORT.PDF")} previewUrl={null} onRemove={vi.fn()} />
);
expect(screen.getByText("PDF")).toBeInTheDocument();
expect(screen.queryByAltText("Upload preview")).not.toBeInTheDocument();
});
});
@@ -0,0 +1,45 @@
import { DeleteOutlined, FilePdfOutlined } from "@ant-design/icons";
interface FilePreviewCardProps {
file: File;
previewUrl: string | null;
onRemove: () => void;
}
function FilePreviewCard({ file, previewUrl, onRemove }: FilePreviewCardProps) {
const isPdf = file.name.toLowerCase().endsWith(".pdf");
return (
<div className="mb-2">
<div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
<div className="relative inline-block">
{isPdf ? (
<div className="w-10 h-10 rounded-md bg-red-500 flex items-center justify-center">
<FilePdfOutlined style={{ fontSize: "16px", color: "white" }} />
</div>
) : (
<img
src={previewUrl || ""}
alt="Upload preview"
className="w-10 h-10 rounded-md border border-gray-200 object-cover"
/>
)}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-900 truncate">{file.name}</div>
<div className="text-xs text-gray-500">
{isPdf ? "PDF" : "Image"}
</div>
</div>
<button
className="flex items-center justify-center w-6 h-6 text-gray-400 hover:text-gray-600 hover:bg-gray-200 rounded-full transition-colors"
onClick={onRemove}
>
<DeleteOutlined style={{ fontSize: "12px" }} />
</button>
</div>
</div>
);
}
export default FilePreviewCard;