Files
lightrag/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx

189 lines
6.5 KiB
TypeScript

import { useState, useCallback } from 'react'
import { FileRejection } from 'react-dropzone'
import Button from '@/components/ui/Button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger
} from '@/components/ui/Dialog'
import FileUploader from '@/components/ui/FileUploader'
import { toast } from 'sonner'
import { errorMessage } from '@/lib/utils'
import { uploadDocument } from '@/api/lightrag'
import { UploadIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export default function UploadDocumentsDialog() {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [isUploading, setIsUploading] = useState(false)
const [progresses, setProgresses] = useState<Record<string, number>>({})
const [fileErrors, setFileErrors] = useState<Record<string, string>>({})
const handleRejectedFiles = useCallback(
(rejectedFiles: FileRejection[]) => {
// Process rejected files and add them to fileErrors
rejectedFiles.forEach(({ file, errors }) => {
// Get the first error message
let errorMsg = errors[0]?.message || t('documentPanel.uploadDocuments.fileUploader.fileRejected', { name: file.name })
// Simplify error message for unsupported file types
if (errorMsg.includes('file-invalid-type')) {
errorMsg = t('documentPanel.uploadDocuments.fileUploader.unsupportedType')
}
// Set progress to 100% to display error message
setProgresses((pre) => ({
...pre,
[file.name]: 100
}))
// Add error message to fileErrors
setFileErrors(prev => ({
...prev,
[file.name]: errorMsg
}))
})
},
[setProgresses, setFileErrors, t]
)
const handleDocumentsUpload = useCallback(
async (filesToUpload: File[]) => {
setIsUploading(true)
// Only clear errors for files that are being uploaded, keep errors for rejected files
setFileErrors(prev => {
const newErrors = { ...prev };
filesToUpload.forEach(file => {
delete newErrors[file.name];
});
return newErrors;
});
// Show uploading toast
const toastId = toast.loading(t('documentPanel.uploadDocuments.batch.uploading'))
try {
// Track errors locally to ensure we have the final state
const uploadErrors: Record<string, string> = {}
await Promise.all(
filesToUpload.map(async (file) => {
try {
// Initialize upload progress
setProgresses((pre) => ({
...pre,
[file.name]: 0
}))
const result = await uploadDocument(file, (percentCompleted: number) => {
console.debug(t('documentPanel.uploadDocuments.single.uploading', { name: file.name, percent: percentCompleted }))
setProgresses((pre) => ({
...pre,
[file.name]: percentCompleted
}))
})
if (result.status !== 'success') {
uploadErrors[file.name] = result.message
setFileErrors(prev => ({
...prev,
[file.name]: result.message
}))
}
} catch (err) {
console.error(`Upload failed for ${file.name}:`, err)
// Handle HTTP errors, including 400 errors
let errorMsg = errorMessage(err)
// If it's an axios error with response data, try to extract more detailed error info
if (err && typeof err === 'object' && 'response' in err) {
const axiosError = err as { response?: { status: number, data?: { detail?: string } } }
if (axiosError.response?.status === 400) {
// Extract specific error message from backend response
errorMsg = axiosError.response.data?.detail || errorMsg
}
// Set progress to 100% to display error message
setProgresses((pre) => ({
...pre,
[file.name]: 100
}))
}
// Record error message in both local tracking and state
uploadErrors[file.name] = errorMsg
setFileErrors(prev => ({
...prev,
[file.name]: errorMsg
}))
}
})
)
// Check if any files failed to upload using our local tracking
const hasErrors = Object.keys(uploadErrors).length > 0
// Update toast status
if (hasErrors) {
toast.error(t('documentPanel.uploadDocuments.batch.error'), { id: toastId })
} else {
toast.success(t('documentPanel.uploadDocuments.batch.success'), { id: toastId })
}
} catch (err) {
console.error('Unexpected error during upload:', err)
toast.error(t('documentPanel.uploadDocuments.generalError', { error: errorMessage(err) }), { id: toastId })
} finally {
setIsUploading(false)
}
},
[setIsUploading, setProgresses, setFileErrors, t]
)
return (
<Dialog
open={open}
onOpenChange={(open) => {
if (isUploading) {
return
}
if (!open) {
setProgresses({})
setFileErrors({})
}
setOpen(open)
}}
>
<DialogTrigger asChild>
<Button variant="default" side="bottom" tooltip={t('documentPanel.uploadDocuments.tooltip')} size="sm">
<UploadIcon /> {t('documentPanel.uploadDocuments.button')}
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
<DialogHeader>
<DialogTitle>{t('documentPanel.uploadDocuments.title')}</DialogTitle>
<DialogDescription>
{t('documentPanel.uploadDocuments.description')}
</DialogDescription>
</DialogHeader>
<FileUploader
maxFileCount={Infinity}
maxSize={200 * 1024 * 1024}
description={t('documentPanel.uploadDocuments.fileTypes')}
onUpload={handleDocumentsUpload}
onReject={handleRejectedFiles}
progresses={progresses}
fileErrors={fileErrors}
disabled={isUploading}
/>
</DialogContent>
</Dialog>
)
}