feat(upload): support multiple file upload on RAG Database page

This commit is contained in:
Woody 2026-04-28 13:22:25 +08:00
parent fdd5a09c28
commit aa5f716578
2 changed files with 84 additions and 43 deletions

View File

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

View File

@ -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 && (