Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ node_modules
dist
dist-ssr
*.local
package-lock.json

# Editor directories and files
.vscode/*
Expand Down
50 changes: 39 additions & 11 deletions frontend/src/components/ChatBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -832,24 +833,36 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>(
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<string, AbortController>()
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,
},
)

Expand All @@ -865,7 +878,7 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>(
setSelectedFiles((prev) =>
prev.map((f) =>
f.id === selectedFile.id
? { ...f, uploading: false, metadata }
? { ...f, uploading: false, metadata, abortController: undefined }
: f,
),
)
Expand All @@ -874,12 +887,17 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>(
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,
),
)
Expand Down Expand Up @@ -980,6 +998,11 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>(
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 {
Expand All @@ -1005,7 +1028,12 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>(
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])

Expand Down Expand Up @@ -2862,7 +2890,7 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>(
<button
onClick={() => removeFile(selectedFile.id)}
className="absolute top-1 right-1 bg-black bg-opacity-60 text-white rounded-full p-1 hover:bg-opacity-80 transition-opacity"
disabled={selectedFile.uploading}
title={selectedFile.uploading ? "Cancel upload" : "Remove file"}
>
<X size={10} />
</button>
Expand Down Expand Up @@ -2918,7 +2946,7 @@ export const ChatBox = React.forwardRef<ChatBoxRef, ChatBoxProps>(
<button
onClick={() => removeFile(selectedFile.id)}
className="text-gray-400 hover:text-red-500 transition-colors p-1"
disabled={selectedFile.uploading}
title={selectedFile.uploading ? "Cancel upload" : "Remove file"}
>
<X size={12} />
</button>
Expand Down