yangdx commited on
Commit
70a9b1c
·
1 Parent(s): 9bf45d2

feat(upload): improve file upload progress and error handling

Browse files

- 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

lightrag_webui/src/components/documents/UploadDocumentsDialog.tsx CHANGED
@@ -21,37 +21,65 @@ export default function UploadDocumentsDialog() {
21
  const [open, setOpen] = useState(false)
22
  const [isUploading, setIsUploading] = useState(false)
23
  const [progresses, setProgresses] = useState<Record<string, number>>({})
 
 
24
 
25
  const handleDocumentsUpload = useCallback(
26
  async (filesToUpload: File[]) => {
27
  setIsUploading(true)
28
 
 
 
 
29
  try {
30
- await Promise.all(
31
- filesToUpload.map(async (file) => {
 
32
  try {
33
- const result = await uploadDocument(file, (percentCompleted: number) => {
34
- console.debug(t('documentPanel.uploadDocuments.uploading', { name: file.name, percent: percentCompleted }))
35
- setProgresses((pre) => ({
36
- ...pre,
37
- [file.name]: percentCompleted
38
- }))
39
- })
40
- if (result.status === 'success') {
41
- toast.success(t('documentPanel.uploadDocuments.success', { name: file.name }))
42
- } else {
43
- toast.error(t('documentPanel.uploadDocuments.failed', { name: file.name, message: result.message }))
44
- }
45
- } catch (err) {
46
- toast.error(t('documentPanel.uploadDocuments.error', { name: file.name, error: errorMessage(err) }))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  }
48
- })
 
 
 
 
 
49
  )
50
  } catch (err) {
51
- toast.error(t('documentPanel.uploadDocuments.generalError', { error: errorMessage(err) }))
 
52
  } finally {
53
  setIsUploading(false)
54
- // setOpen(false)
55
  }
56
  },
57
  [setIsUploading, setProgresses, t]
@@ -61,9 +89,15 @@ export default function UploadDocumentsDialog() {
61
  <Dialog
62
  open={open}
63
  onOpenChange={(open) => {
64
- if (isUploading && !open) {
 
65
  return
66
  }
 
 
 
 
 
67
  setOpen(open)
68
  }}
69
  >
@@ -85,6 +119,7 @@ export default function UploadDocumentsDialog() {
85
  description={t('documentPanel.uploadDocuments.fileTypes')}
86
  onUpload={handleDocumentsUpload}
87
  progresses={progresses}
 
88
  disabled={isUploading}
89
  />
90
  </DialogContent>
 
21
  const [open, setOpen] = useState(false)
22
  const [isUploading, setIsUploading] = useState(false)
23
  const [progresses, setProgresses] = useState<Record<string, number>>({})
24
+ // Track upload errors for each file
25
+ const [fileErrors, setFileErrors] = useState<Record<string, string>>({})
26
 
27
  const handleDocumentsUpload = useCallback(
28
  async (filesToUpload: File[]) => {
29
  setIsUploading(true)
30
 
31
+ // Reset error states before new upload
32
+ setFileErrors({})
33
+
34
  try {
35
+ // Use a single toast for the entire batch upload process
36
+ toast.promise(
37
+ (async () => {
38
  try {
39
+ await Promise.all(
40
+ filesToUpload.map(async (file) => {
41
+ try {
42
+ const result = await uploadDocument(file, (percentCompleted: number) => {
43
+ console.debug(t('documentPanel.uploadDocuments.uploading', { name: file.name, percent: percentCompleted }))
44
+ setProgresses((pre) => ({
45
+ ...pre,
46
+ [file.name]: percentCompleted
47
+ }))
48
+ })
49
+
50
+ // Store error message if upload failed
51
+ if (result.status !== 'success') {
52
+ setFileErrors(prev => ({
53
+ ...prev,
54
+ [file.name]: result.message
55
+ }))
56
+ }
57
+ } catch (err) {
58
+ // Store error message from exception
59
+ setFileErrors(prev => ({
60
+ ...prev,
61
+ [file.name]: errorMessage(err)
62
+ }))
63
+ }
64
+ })
65
+ )
66
+ // Keep dialog open to show final status
67
+ // User needs to close dialog manually
68
+ } catch (error) {
69
+ console.error('Upload failed:', error)
70
  }
71
+ })(),
72
+ {
73
+ loading: t('documentPanel.uploadDocuments.uploading.batch'),
74
+ success: t('documentPanel.uploadDocuments.success.batch'),
75
+ error: t('documentPanel.uploadDocuments.error.batch')
76
+ }
77
  )
78
  } catch (err) {
79
+ // Handle general upload errors
80
+ toast.error(`Upload error: ${errorMessage(err)}`)
81
  } finally {
82
  setIsUploading(false)
 
83
  }
84
  },
85
  [setIsUploading, setProgresses, t]
 
89
  <Dialog
90
  open={open}
91
  onOpenChange={(open) => {
92
+ // Prevent closing dialog during upload
93
+ if (isUploading) {
94
  return
95
  }
96
+ if (!open) {
97
+ // Reset states when dialog is closed
98
+ setProgresses({})
99
+ setFileErrors({})
100
+ }
101
  setOpen(open)
102
  }}
103
  >
 
119
  description={t('documentPanel.uploadDocuments.fileTypes')}
120
  onUpload={handleDocumentsUpload}
121
  progresses={progresses}
122
+ fileErrors={fileErrors}
123
  disabled={isUploading}
124
  />
125
  </DialogContent>
lightrag_webui/src/components/ui/FileUploader.tsx CHANGED
@@ -10,7 +10,6 @@ import { toast } from 'sonner'
10
  import { cn } from '@/lib/utils'
11
  import { useControllableState } from '@radix-ui/react-use-controllable-state'
12
  import Button from '@/components/ui/Button'
13
- import Progress from '@/components/ui/Progress'
14
  import { ScrollArea } from '@/components/ui/ScrollArea'
15
  import { supportedFileTypes } from '@/lib/constants'
16
 
@@ -47,6 +46,14 @@ interface FileUploaderProps extends React.HTMLAttributes<HTMLDivElement> {
47
  */
48
  progresses?: Record<string, number>
49
 
 
 
 
 
 
 
 
 
50
  /**
51
  * Accepted file types for the uploader.
52
  * @type { [key: string]: string[]}
@@ -117,6 +124,7 @@ function FileUploader(props: FileUploaderProps) {
117
  onValueChange,
118
  onUpload,
119
  progresses,
 
120
  accept = supportedFileTypes,
121
  maxSize = 1024 * 1024 * 200,
122
  maxFileCount = 1,
@@ -161,16 +169,7 @@ function FileUploader(props: FileUploaderProps) {
161
  }
162
 
163
  if (onUpload && updatedFiles.length > 0 && updatedFiles.length <= maxFileCount) {
164
- const target = updatedFiles.length > 0 ? `${updatedFiles.length} files` : 'file'
165
-
166
- toast.promise(onUpload(updatedFiles), {
167
- loading: `Uploading ${target}...`,
168
- success: () => {
169
- setFiles([])
170
- return `${target} uploaded`
171
- },
172
- error: `Failed to upload ${target}`
173
- })
174
  }
175
  },
176
 
@@ -265,6 +264,7 @@ function FileUploader(props: FileUploaderProps) {
265
  file={file}
266
  onRemove={() => onRemove(index)}
267
  progress={progresses?.[file.name]}
 
268
  />
269
  ))}
270
  </div>
@@ -274,13 +274,33 @@ function FileUploader(props: FileUploaderProps) {
274
  )
275
  }
276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  interface FileCardProps {
278
  file: File
279
  onRemove: () => void
280
  progress?: number
 
281
  }
282
 
283
- function FileCard({ file, progress, onRemove }: FileCardProps) {
284
  return (
285
  <div className="relative flex items-center gap-2.5">
286
  <div className="flex flex-1 gap-2.5">
@@ -290,7 +310,14 @@ function FileCard({ file, progress, onRemove }: FileCardProps) {
290
  <p className="text-foreground/80 line-clamp-1 text-sm font-medium">{file.name}</p>
291
  <p className="text-muted-foreground text-xs">{formatBytes(file.size)}</p>
292
  </div>
293
- {progress ? <Progress value={progress} /> : null}
 
 
 
 
 
 
 
294
  </div>
295
  </div>
296
  <div className="flex items-center gap-2">
 
10
  import { cn } from '@/lib/utils'
11
  import { useControllableState } from '@radix-ui/react-use-controllable-state'
12
  import Button from '@/components/ui/Button'
 
13
  import { ScrollArea } from '@/components/ui/ScrollArea'
14
  import { supportedFileTypes } from '@/lib/constants'
15
 
 
46
  */
47
  progresses?: Record<string, number>
48
 
49
+ /**
50
+ * Error messages for failed uploads.
51
+ * @type Record<string, string> | undefined
52
+ * @default undefined
53
+ * @example fileErrors={{ "file1.png": "Upload failed" }}
54
+ */
55
+ fileErrors?: Record<string, string>
56
+
57
  /**
58
  * Accepted file types for the uploader.
59
  * @type { [key: string]: string[]}
 
124
  onValueChange,
125
  onUpload,
126
  progresses,
127
+ fileErrors,
128
  accept = supportedFileTypes,
129
  maxSize = 1024 * 1024 * 200,
130
  maxFileCount = 1,
 
169
  }
170
 
171
  if (onUpload && updatedFiles.length > 0 && updatedFiles.length <= maxFileCount) {
172
+ onUpload(updatedFiles)
 
 
 
 
 
 
 
 
 
173
  }
174
  },
175
 
 
264
  file={file}
265
  onRemove={() => onRemove(index)}
266
  progress={progresses?.[file.name]}
267
+ error={fileErrors?.[file.name]}
268
  />
269
  ))}
270
  </div>
 
274
  )
275
  }
276
 
277
+ interface ProgressProps {
278
+ value: number
279
+ error?: boolean
280
+ }
281
+
282
+ function Progress({ value, error }: ProgressProps) {
283
+ return (
284
+ <div className="h-2 w-full overflow-hidden rounded-full bg-secondary">
285
+ <div
286
+ className={cn(
287
+ 'h-full transition-all',
288
+ error ? 'bg-destructive' : 'bg-primary'
289
+ )}
290
+ style={{ width: `${value}%` }}
291
+ />
292
+ </div>
293
+ )
294
+ }
295
+
296
  interface FileCardProps {
297
  file: File
298
  onRemove: () => void
299
  progress?: number
300
+ error?: string
301
  }
302
 
303
+ function FileCard({ file, progress, error, onRemove }: FileCardProps) {
304
  return (
305
  <div className="relative flex items-center gap-2.5">
306
  <div className="flex flex-1 gap-2.5">
 
310
  <p className="text-foreground/80 line-clamp-1 text-sm font-medium">{file.name}</p>
311
  <p className="text-muted-foreground text-xs">{formatBytes(file.size)}</p>
312
  </div>
313
+ {error ? (
314
+ <div className="text-destructive text-sm">
315
+ <Progress value={100} error={true} />
316
+ <p className="mt-1">{error}</p>
317
+ </div>
318
+ ) : (
319
+ progress ? <Progress value={progress} /> : null
320
+ )}
321
  </div>
322
  </div>
323
  <div className="flex items-center gap-2">