From b804d74d34a833e2422ce03d178804e37f4cbbfb Mon Sep 17 00:00:00 2001 From: yangdx Date: Wed, 26 Mar 2025 20:06:14 +0800 Subject: [PATCH] feat(upload): improve file upload progress and error handling - Add persistent progress bars and error states - Remove individual file toasts in favor of batch status - Keep dialog open until manual close - Move Progress component inline to reduce dependencies --- .../documents/UploadDocumentsDialog.tsx | 75 ++++++++++++++----- .../src/components/ui/FileUploader.tsx | 53 +++++++++---- 2 files changed, 95 insertions(+), 33 deletions(-) diff --git a/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx b/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx index 22eef22c..c5015b42 100644 --- a/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx +++ b/lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx @@ -21,37 +21,65 @@ export default function UploadDocumentsDialog() { const [open, setOpen] = useState(false) const [isUploading, setIsUploading] = useState(false) const [progresses, setProgresses] = useState>({}) + // Track upload errors for each file + const [fileErrors, setFileErrors] = useState>({}) const handleDocumentsUpload = useCallback( async (filesToUpload: File[]) => { setIsUploading(true) + // Reset error states before new upload + setFileErrors({}) + try { - await Promise.all( - filesToUpload.map(async (file) => { + // Use a single toast for the entire batch upload process + toast.promise( + (async () => { try { - const result = await uploadDocument(file, (percentCompleted: number) => { - console.debug(t('documentPanel.uploadDocuments.uploading', { name: file.name, percent: percentCompleted })) - setProgresses((pre) => ({ - ...pre, - [file.name]: percentCompleted - })) - }) - if (result.status === 'success') { - toast.success(t('documentPanel.uploadDocuments.success', { name: file.name })) - } else { - toast.error(t('documentPanel.uploadDocuments.failed', { name: file.name, message: result.message })) - } - } catch (err) { - toast.error(t('documentPanel.uploadDocuments.error', { name: file.name, error: errorMessage(err) })) + await Promise.all( + filesToUpload.map(async (file) => { + try { + const result = await uploadDocument(file, (percentCompleted: number) => { + console.debug(t('documentPanel.uploadDocuments.uploading', { name: file.name, percent: percentCompleted })) + setProgresses((pre) => ({ + ...pre, + [file.name]: percentCompleted + })) + }) + + // Store error message if upload failed + if (result.status !== 'success') { + setFileErrors(prev => ({ + ...prev, + [file.name]: result.message + })) + } + } catch (err) { + // Store error message from exception + setFileErrors(prev => ({ + ...prev, + [file.name]: errorMessage(err) + })) + } + }) + ) + // Keep dialog open to show final status + // User needs to close dialog manually + } catch (error) { + console.error('Upload failed:', error) } - }) + })(), + { + loading: t('documentPanel.uploadDocuments.uploading.batch'), + success: t('documentPanel.uploadDocuments.success.batch'), + error: t('documentPanel.uploadDocuments.error.batch') + } ) } catch (err) { - toast.error(t('documentPanel.uploadDocuments.generalError', { error: errorMessage(err) })) + // Handle general upload errors + toast.error(`Upload error: ${errorMessage(err)}`) } finally { setIsUploading(false) - // setOpen(false) } }, [setIsUploading, setProgresses, t] @@ -61,9 +89,15 @@ export default function UploadDocumentsDialog() { { - if (isUploading && !open) { + // Prevent closing dialog during upload + if (isUploading) { return } + if (!open) { + // Reset states when dialog is closed + setProgresses({}) + setFileErrors({}) + } setOpen(open) }} > @@ -85,6 +119,7 @@ export default function UploadDocumentsDialog() { description={t('documentPanel.uploadDocuments.fileTypes')} onUpload={handleDocumentsUpload} progresses={progresses} + fileErrors={fileErrors} disabled={isUploading} /> diff --git a/lightrag_webui/src/components/ui/FileUploader.tsx b/lightrag_webui/src/components/ui/FileUploader.tsx index da9e6521..d34981ae 100644 --- a/lightrag_webui/src/components/ui/FileUploader.tsx +++ b/lightrag_webui/src/components/ui/FileUploader.tsx @@ -10,7 +10,6 @@ import { toast } from 'sonner' import { cn } from '@/lib/utils' import { useControllableState } from '@radix-ui/react-use-controllable-state' import Button from '@/components/ui/Button' -import Progress from '@/components/ui/Progress' import { ScrollArea } from '@/components/ui/ScrollArea' import { supportedFileTypes } from '@/lib/constants' @@ -47,6 +46,14 @@ interface FileUploaderProps extends React.HTMLAttributes { */ progresses?: Record + /** + * Error messages for failed uploads. + * @type Record | undefined + * @default undefined + * @example fileErrors={{ "file1.png": "Upload failed" }} + */ + fileErrors?: Record + /** * Accepted file types for the uploader. * @type { [key: string]: string[]} @@ -117,6 +124,7 @@ function FileUploader(props: FileUploaderProps) { onValueChange, onUpload, progresses, + fileErrors, accept = supportedFileTypes, maxSize = 1024 * 1024 * 200, maxFileCount = 1, @@ -161,16 +169,7 @@ function FileUploader(props: FileUploaderProps) { } if (onUpload && updatedFiles.length > 0 && updatedFiles.length <= maxFileCount) { - const target = updatedFiles.length > 0 ? `${updatedFiles.length} files` : 'file' - - toast.promise(onUpload(updatedFiles), { - loading: `Uploading ${target}...`, - success: () => { - setFiles([]) - return `${target} uploaded` - }, - error: `Failed to upload ${target}` - }) + onUpload(updatedFiles) } }, @@ -265,6 +264,7 @@ function FileUploader(props: FileUploaderProps) { file={file} onRemove={() => onRemove(index)} progress={progresses?.[file.name]} + error={fileErrors?.[file.name]} /> ))} @@ -274,13 +274,33 @@ function FileUploader(props: FileUploaderProps) { ) } +interface ProgressProps { + value: number + error?: boolean +} + +function Progress({ value, error }: ProgressProps) { + return ( +
+
+
+ ) +} + interface FileCardProps { file: File onRemove: () => void progress?: number + error?: string } -function FileCard({ file, progress, onRemove }: FileCardProps) { +function FileCard({ file, progress, error, onRemove }: FileCardProps) { return (
@@ -290,7 +310,14 @@ function FileCard({ file, progress, onRemove }: FileCardProps) {

{file.name}

{formatBytes(file.size)}

- {progress ? : null} + {error ? ( +
+ +

{error}

+
+ ) : ( + progress ? : null + )}