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 React from 'react'
|
||||||
import { Upload, Loader2, CheckCircle, AlertCircle } from 'lucide-react'
|
import { Upload, Loader2 } from 'lucide-react'
|
||||||
|
|
||||||
interface DocumentUploadProps {
|
interface DocumentUploadProps {
|
||||||
onUpload: (file: File) => void
|
onUpload: (files: File[]) => void
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
success: string | null
|
|
||||||
error: string | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
export const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||||
onUpload,
|
onUpload,
|
||||||
isLoading,
|
isLoading,
|
||||||
success,
|
|
||||||
error,
|
|
||||||
}) => {
|
}) => {
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0]
|
const files = event.target.files
|
||||||
if (file) {
|
if (files && files.length > 0) {
|
||||||
onUpload(file)
|
onUpload(Array.from(files))
|
||||||
}
|
}
|
||||||
|
event.target.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -26,6 +23,7 @@ export const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".pdf,.docx,.txt"
|
accept=".pdf,.docx,.txt"
|
||||||
|
multiple
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
|
|
@ -52,20 +50,6 @@ export const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</label>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState, useCallback } from 'react'
|
||||||
import { Database, AlertCircle } from 'lucide-react'
|
import { Database, AlertCircle, CheckCircle, XCircle, Loader2 } from 'lucide-react'
|
||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { useDocuments, useDocumentChunks, useDeleteDocument, useDeleteChunk, useIngestDocument } from '../lib/queries'
|
import { useDocuments, useDocumentChunks, useDeleteDocument, useDeleteChunk, useIngestDocument } from '../lib/queries'
|
||||||
import { DocumentList } from '../components/DocumentList'
|
import { DocumentList } from '../components/DocumentList'
|
||||||
import { ChunkList } from '../components/ChunkList'
|
import { ChunkList } from '../components/ChunkList'
|
||||||
import { DocumentUpload } from '../components/DocumentUpload'
|
import { DocumentUpload } from '../components/DocumentUpload'
|
||||||
|
|
||||||
|
interface FileUploadEntry {
|
||||||
|
name: string
|
||||||
|
status: 'uploading' | 'success' | 'error'
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
export const RAGDatabasePage: React.FC = () => {
|
export const RAGDatabasePage: React.FC = () => {
|
||||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||||
const [uploadSuccess, setUploadSuccess] = useState<string | null>(null)
|
const [uploadEntries, setUploadEntries] = useState<FileUploadEntry[]>([])
|
||||||
const [uploadError, setUploadError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const { data: documentsData, isLoading: isLoadingDocuments, error: documentsError } = useDocuments()
|
const { data: documentsData, isLoading: isLoadingDocuments, error: documentsError } = useDocuments()
|
||||||
const { data: chunks, isLoading: isLoadingChunks } = useDocumentChunks(expandedId)
|
const { data: chunks, isLoading: isLoadingChunks } = useDocumentChunks(expandedId)
|
||||||
|
|
@ -38,20 +43,42 @@ export const RAGDatabasePage: React.FC = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpload = (file: File) => {
|
const handleUpload = useCallback(async (files: File[]) => {
|
||||||
setUploadSuccess(null)
|
const entries: FileUploadEntry[] = files.map((f) => ({
|
||||||
setUploadError(null)
|
name: f.name,
|
||||||
ingestDocumentMutation.mutate(file, {
|
status: 'uploading' as const,
|
||||||
onSuccess: (data) => {
|
}))
|
||||||
setUploadSuccess(`Uploaded ${data.filename}`)
|
setUploadEntries(entries)
|
||||||
queryClient.invalidateQueries({ queryKey: ['documents'] })
|
|
||||||
setTimeout(() => setUploadSuccess(null), 3000)
|
const results = await Promise.allSettled(
|
||||||
},
|
files.map(async (file) => {
|
||||||
onError: (error) => {
|
try {
|
||||||
setUploadError(error.message)
|
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) {
|
if (isLoadingDocuments) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -90,10 +117,40 @@ export const RAGDatabasePage: React.FC = () => {
|
||||||
<DocumentUpload
|
<DocumentUpload
|
||||||
onUpload={handleUpload}
|
onUpload={handleUpload}
|
||||||
isLoading={ingestDocumentMutation.isPending}
|
isLoading={ingestDocumentMutation.isPending}
|
||||||
success={uploadSuccess}
|
|
||||||
error={uploadError}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{hasDocuments && (
|
{hasDocuments && (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue