diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf36d..b02a1ff77 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +package-lock.json # Editor directories and files .vscode/* diff --git a/frontend/src/components/ChatBox.tsx b/frontend/src/components/ChatBox.tsx index aecde7b7b..2d34fb87c 100644 --- a/frontend/src/components/ChatBox.tsx +++ b/frontend/src/components/ChatBox.tsx @@ -86,6 +86,7 @@ interface SelectedFile { uploadError?: string preview?: string // URL for image preview fileType?: FileType + abortController?: AbortController // For cancelling ongoing uploads } export const getFileIcon = (fileType: FileType | string | undefined) => { @@ -832,24 +833,36 @@ export const ChatBox = React.forwardRef( setUploadingFilesCount((prev) => prev + files.length) const uploadedMetadata: AttachmentMetadata[] = [] - // Set all files to uploading state + // Create AbortControllers for each file before starting uploads + const fileAbortControllers = new Map() + files.forEach(file => { + fileAbortControllers.set(file.id, new AbortController()) + }) + + // Set all files to uploading state and store AbortControllers setSelectedFiles((prev) => - prev.map((f) => - files.some((file) => file.id === f.id) - ? { ...f, uploading: true, uploadError: undefined } - : f, - ), + prev.map((f) => { + const abortController = fileAbortControllers.get(f.id) + if (abortController) { + return { ...f, uploading: true, uploadError: undefined, abortController } + } + return f + }), ) const uploadPromises = files.map(async (selectedFile) => { + const abortController = fileAbortControllers.get(selectedFile.id) + try { const formData = new FormData() formData.append("attachment", selectedFile.file) + const response = await authFetch( "/api/v1/files/upload-attachment", { method: "POST", body: formData, + signal: abortController?.signal, }, ) @@ -865,7 +878,7 @@ export const ChatBox = React.forwardRef( setSelectedFiles((prev) => prev.map((f) => f.id === selectedFile.id - ? { ...f, uploading: false, metadata } + ? { ...f, uploading: false, metadata, abortController: undefined } : f, ), ) @@ -874,12 +887,17 @@ export const ChatBox = React.forwardRef( throw new Error("No document ID returned from upload") } } catch (error) { + // If the upload was aborted, don't show error + if (error instanceof Error && error.name === 'AbortError') { + return null + } + const errorMessage = error instanceof Error ? error.message : "Upload failed" setSelectedFiles((prev) => prev.map((f) => f.id === selectedFile.id - ? { ...f, uploading: false, uploadError: errorMessage } + ? { ...f, uploading: false, uploadError: errorMessage, abortController: undefined } : f, ), ) @@ -980,6 +998,11 @@ export const ChatBox = React.forwardRef( const removeFile = useCallback(async (id: string) => { const fileToRemove = selectedFiles.find((f) => f.id === id) + // If the file is currently uploading, abort the upload + if (fileToRemove?.uploading && fileToRemove?.abortController) { + fileToRemove.abortController.abort() + } + // If the file has metadata with fileId (meaning it's already uploaded), delete it from the server if (fileToRemove?.metadata?.fileId) { try { @@ -1005,7 +1028,12 @@ export const ChatBox = React.forwardRef( if (fileToRemove?.preview) { URL.revokeObjectURL(fileToRemove.preview) } - return prev.filter((f) => f.id !== id) + const filtered = prev.filter((f) => f.id !== id) + // Decrement uploadingFilesCount if the file was uploading + if (fileToRemove?.uploading) { + setUploadingFilesCount((count) => Math.max(0, count - 1)) + } + return filtered }) }, [selectedFiles]) @@ -2862,7 +2890,7 @@ export const ChatBox = React.forwardRef( @@ -2918,7 +2946,7 @@ export const ChatBox = React.forwardRef(