File size: 7,483 Bytes
0e5f52f
22c0f3f
0e5f52f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77ca676
0e5f52f
c911590
 
 
 
 
77ca676
0e5f52f
 
 
70a9b1c
0e5f52f
22c0f3f
 
 
 
 
 
9426437
22c0f3f
 
 
 
9426437
22c0f3f
 
 
 
 
9426437
22c0f3f
 
 
 
 
 
 
 
 
 
0e5f52f
 
 
c911590
9426437
22c0f3f
 
 
 
 
 
 
 
9426437
22c0f3f
 
9426437
0e5f52f
22c0f3f
 
9426437
22c0f3f
 
0e5f52f
22c0f3f
 
 
 
 
9426437
22c0f3f
 
 
 
 
 
 
56be926
ae64c4f
 
 
 
 
 
 
22c0f3f
 
 
 
 
c911590
 
 
22c0f3f
 
 
9426437
22c0f3f
 
9426437
22c0f3f
 
 
 
 
 
 
9426437
22c0f3f
 
 
 
 
 
9426437
22c0f3f
 
 
 
 
 
0e5f52f
22c0f3f
0e5f52f
9426437
22c0f3f
 
9426437
22c0f3f
 
 
 
 
 
c911590
 
 
2e931df
 
 
 
 
c911590
 
0e5f52f
22c0f3f
 
0e5f52f
 
 
 
2e931df
0e5f52f
 
 
 
 
 
70a9b1c
0e5f52f
 
70a9b1c
 
 
 
0e5f52f
 
 
 
77ca676
 
0e5f52f
 
 
 
77ca676
0e5f52f
77ca676
0e5f52f
 
 
 
 
77ca676
0e5f52f
22c0f3f
0e5f52f
70a9b1c
0e5f52f
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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'

interface UploadDocumentsDialogProps {
  onDocumentsUploaded?: () => Promise<void>
}

export default function UploadDocumentsDialog({ onDocumentsUploaded }: UploadDocumentsDialogProps) {
  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)
      let hasSuccessfulUpload = false

      // 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 === 'duplicated') {
                uploadErrors[file.name] = t('documentPanel.uploadDocuments.fileUploader.duplicateFile')
                setFileErrors(prev => ({
                  ...prev,
                  [file.name]: t('documentPanel.uploadDocuments.fileUploader.duplicateFile')
                }))
              } else if (result.status !== 'success') {
                uploadErrors[file.name] = result.message
                setFileErrors(prev => ({
                  ...prev,
                  [file.name]: result.message
                }))
              } else {
                // Mark that we had at least one successful upload
                hasSuccessfulUpload = true
              }
            } 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 })
        }

        // Only update if at least one file was uploaded successfully
        if (hasSuccessfulUpload) {
          // Refresh document list
          if (onDocumentsUploaded) {
            onDocumentsUploaded().catch(err => {
              console.error('Error refreshing documents:', err)
            })
          }
        }
      } 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, onDocumentsUploaded]
  )

  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>
  )
}