feat(upload): support multiple file upload on RAG Database page
This commit is contained in:
parent
fdd5a09c28
commit
aa5f716578
|
|
@ -1,24 +1,21 @@
|
|||
import React from 'react'
|
||||
import { Upload, Loader2, CheckCircle, AlertCircle } from 'lucide-react'
|
||||
import { Upload, Loader2 } from 'lucide-react'
|
||||
|
||||
interface DocumentUploadProps {
|
||||
onUpload: (file: File) => void
|
||||
onUpload: (files: File[]) => void
|
||||
isLoading: boolean
|
||||
success: string | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||
onUpload,
|
||||
isLoading,
|
||||
success,
|
||||
error,
|
||||
}) => {
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (file) {
|
||||
onUpload(file)
|
||||
const files = event.target.files
|
||||
if (files && files.length > 0) {
|
||||
onUpload(Array.from(files))
|
||||
}
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -26,6 +23,7 @@ export const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
|||
<input
|
||||
type="file"
|
||||
accept=".pdf,.docx,.txt"
|
||||
multiple
|
||||
onChange={handleFileChange}
|
||||
disabled={isLoading}
|
||||
className="hidden"
|
||||
|
|
@ -52,20 +50,6 @@ export const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
|||
</>
|
||||
)}
|
||||
</label>
|
||||
|
||||
{success && (
|
||||
<div className="flex items-center space-x-2 text-green-700">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
<span className="text-sm">{success}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center space-x-2 text-red-700">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
<span className="text-sm">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
import React, { useState } from 'react'
|
||||
import { Database, AlertCircle } from 'lucide-react'
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import { Database, AlertCircle, CheckCircle, XCircle, Loader2 } from 'lucide-react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useDocuments, useDocumentChunks, useDeleteDocument, useDeleteChunk, useIngestDocument } from '../lib/queries'
|
||||
import { DocumentList } from '../components/DocumentList'
|
||||
import { ChunkList } from '../components/ChunkList'
|
||||
import { DocumentUpload } from '../components/DocumentUpload'
|
||||
|
||||
interface FileUploadEntry {
|
||||
name: string
|
||||
status: 'uploading' | 'success' | 'error'
|
||||
error?: string
|
||||
}
|
||||
|
||||
export const RAGDatabasePage: React.FC = () => {
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||
const [uploadSuccess, setUploadSuccess] = useState<string | null>(null)
|
||||
const [uploadError, setUploadError] = useState<string | null>(null)
|
||||
const [uploadEntries, setUploadEntries] = useState<FileUploadEntry[]>([])
|
||||
|
||||
const { data: documentsData, isLoading: isLoadingDocuments, error: documentsError } = useDocuments()
|
||||
const { data: chunks, isLoading: isLoadingChunks } = useDocumentChunks(expandedId)
|
||||
|
|
@ -38,20 +43,42 @@ export const RAGDatabasePage: React.FC = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const handleUpload = (file: File) => {
|
||||
setUploadSuccess(null)
|
||||
setUploadError(null)
|
||||
ingestDocumentMutation.mutate(file, {
|
||||
onSuccess: (data) => {
|
||||
setUploadSuccess(`Uploaded ${data.filename}`)
|
||||
queryClient.invalidateQueries({ queryKey: ['documents'] })
|
||||
setTimeout(() => setUploadSuccess(null), 3000)
|
||||
},
|
||||
onError: (error) => {
|
||||
setUploadError(error.message)
|
||||
},
|
||||
})
|
||||
}
|
||||
const handleUpload = useCallback(async (files: File[]) => {
|
||||
const entries: FileUploadEntry[] = files.map((f) => ({
|
||||
name: f.name,
|
||||
status: 'uploading' as const,
|
||||
}))
|
||||
setUploadEntries(entries)
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
files.map(async (file) => {
|
||||
try {
|
||||
await ingestDocumentMutation.mutateAsync(file)
|
||||
setUploadEntries((prev) =>
|
||||
prev.map((e) =>
|
||||
e.name === file.name ? { ...e, status: 'success' as const } : e
|
||||
)
|
||||
)
|
||||
} catch (err: any) {
|
||||
setUploadEntries((prev) =>
|
||||
prev.map((e) =>
|
||||
e.name === file.name
|
||||
? { ...e, status: 'error' as const, error: err?.message ?? 'Upload failed' }
|
||||
: e
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ['documents'] })
|
||||
setTimeout(() => setUploadEntries([]), 5000)
|
||||
}, [ingestDocumentMutation, queryClient])
|
||||
|
||||
const uploadingCount = uploadEntries.filter((e) => e.status === 'uploading').length
|
||||
const successCount = uploadEntries.filter((e) => e.status === 'success').length
|
||||
const errorCount = uploadEntries.filter((e) => e.status === 'error').length
|
||||
const hasEntries = uploadEntries.length > 0
|
||||
|
||||
if (isLoadingDocuments) {
|
||||
return (
|
||||
|
|
@ -90,10 +117,40 @@ export const RAGDatabasePage: React.FC = () => {
|
|||
<DocumentUpload
|
||||
onUpload={handleUpload}
|
||||
isLoading={ingestDocumentMutation.isPending}
|
||||
success={uploadSuccess}
|
||||
error={uploadError}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{hasEntries && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="text-sm font-medium text-gray-600">
|
||||
{uploadingCount > 0
|
||||
? `Uploading ${successCount + errorCount + 1} of ${uploadEntries.length}…`
|
||||
: `${successCount} succeeded${errorCount > 0 ? `, ${errorCount} failed` : ''}`}
|
||||
</div>
|
||||
<div className="space-y-1 max-h-40 overflow-y-auto">
|
||||
{uploadEntries.map((entry) => (
|
||||
<div
|
||||
key={entry.name}
|
||||
className="flex items-center space-x-2 text-sm py-1"
|
||||
>
|
||||
{entry.status === 'uploading' && (
|
||||
<Loader2 className="w-4 h-4 text-blue-500 animate-spin shrink-0" />
|
||||
)}
|
||||
{entry.status === 'success' && (
|
||||
<CheckCircle className="w-4 h-4 text-green-600 shrink-0" />
|
||||
)}
|
||||
{entry.status === 'error' && (
|
||||
<XCircle className="w-4 h-4 text-red-600 shrink-0" />
|
||||
)}
|
||||
<span className="truncate">{entry.name}</span>
|
||||
{entry.status === 'error' && entry.error && (
|
||||
<span className="text-red-600 truncate">— {entry.error}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasDocuments && (
|
||||
|
|
|
|||
Loading…
Reference in New Issue