diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.test.tsx index 9da3e3a4a08..dc9cc7ba373 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.test.tsx @@ -413,4 +413,47 @@ describe("ChatUI", () => { } } }); + + it("revokes pending upload preview object URLs on unmount", async () => { + const createSpy = vi.fn(() => "blob:preview-1"); + const revokeSpy = vi.fn(); + const originalCreate = URL.createObjectURL; + const originalRevoke = URL.revokeObjectURL; + URL.createObjectURL = createSpy; + URL.revokeObjectURL = revokeSpy; + + try { + const { container, unmount } = render( + , + ); + + await waitFor(() => { + expect(container.querySelector('input[type="file"]')).toBeInTheDocument(); + }); + + const fileInput = container.querySelector('input[type="file"]') as HTMLInputElement; + const file = new File(["image data"], "photo.png", { type: "image/png" }); + await act(async () => { + fireEvent.change(fileInput, { target: { files: [file] } }); + }); + + await waitFor(() => { + expect(createSpy).toHaveBeenCalledWith(file); + }); + expect(revokeSpy).not.toHaveBeenCalledWith("blob:preview-1"); + + unmount(); + + expect(revokeSpy).toHaveBeenCalledWith("blob:preview-1"); + } finally { + URL.createObjectURL = originalCreate; + URL.revokeObjectURL = originalRevoke; + } + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx index d2cf27e0c8b..1e0cdab45d8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx @@ -254,6 +254,16 @@ const ChatUI: React.FC = ({ const [chatUploadedImage, setChatUploadedImage] = useState(null); const [chatImagePreviewUrl, setChatImagePreviewUrl] = useState(null); const [uploadedAudio, setUploadedAudio] = useState(null); + const pendingPreviewUrlsRef = useRef([]); + + useEffect(() => { + pendingPreviewUrlsRef.current = [...imagePreviewUrls, responsesImagePreviewUrl, chatImagePreviewUrl].filter( + (url): url is string => !!url, + ); + }, [imagePreviewUrls, responsesImagePreviewUrl, chatImagePreviewUrl]); + + useEffect(() => () => pendingPreviewUrlsRef.current.forEach((url) => URL.revokeObjectURL(url)), []); + const [isGetCodeModalVisible, setIsGetCodeModalVisible] = useState(false); const [generatedCode, setGeneratedCode] = useState(""); const [selectedSdk, setSelectedSdk] = useState<"openai" | "azure">("openai");