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");