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 |
-
|
31 |
-
|
|
|
32 |
try {
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
}
|
48 |
-
})
|
|
|
|
|
|
|
|
|
|
|
49 |
)
|
50 |
} catch (err) {
|
51 |
-
|
|
|
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 |
-
|
|
|
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 |
-
|
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 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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">
|