wu981526092's picture
� ENHANCE FILE UPLOAD: Allow document upload without assistant
e507e4c
raw
history blame
14.5 kB
import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Slider } from '@/components/ui/slider'
import { Label } from '@/components/ui/label'
import { User, Upload, File, X, BookOpen } from 'lucide-react'
interface UploadedFile {
id: string
name: string
size: number
type: string
uploadedAt: string
status: string
chunks?: number
assistantId?: string | null
}
interface SelectedAssistant {
id: string
name: string
type: 'user'|'template'|'new'
originalTemplate?: string
}
interface DocumentsTabProps {
isLoading: boolean
ragEnabled: boolean
setRagEnabled: (enabled: boolean) => void
retrievalCount: number
setRetrievalCount: (count: number) => void
currentAssistant: SelectedAssistant | null
}
export function DocumentsTab({
isLoading,
ragEnabled,
setRagEnabled,
retrievalCount,
setRetrievalCount,
currentAssistant
}: DocumentsTabProps) {
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
const [isUploading, setIsUploading] = useState(false)
// Load assistant-specific documents when assistant changes
useEffect(() => {
const loadDocuments = async () => {
if (!currentAssistant) {
// Load all documents when no assistant is selected
try {
const response = await fetch('/rag/documents')
if (response.ok) {
const data = await response.json()
const allFiles: UploadedFile[] = data.documents.map((doc: any) => ({
id: doc.id,
name: doc.filename,
size: doc.size || 0,
type: doc.content_type || 'application/octet-stream',
uploadedAt: doc.upload_date,
status: 'processed',
chunks: doc.chunk_count,
assistantId: doc.assistant_id
}))
setUploadedFiles(allFiles)
}
} catch (error) {
console.error('Error loading documents:', error)
}
return
}
try {
// For now, load all documents but filter by assistant in the future
const response = await fetch('/rag/documents')
if (response.ok) {
const data = await response.json()
if (data.documents) {
const documentList = Object.entries(data.documents).map(([docId, docInfo]: [string, any]) => ({
id: docId,
name: docInfo.filename,
size: 0,
type: docInfo.file_type,
uploadedAt: new Date().toISOString(),
status: docInfo.status,
chunks: docInfo.chunks,
assistantId: docInfo.assistant_id || null // Future: filter by assistant
})) as UploadedFile[]
// TODO: Filter documents by currentAssistant.id when backend supports it
setUploadedFiles(documentList)
}
}
} catch (error) {
console.error('Error loading documents:', error)
}
}
loadDocuments()
}, [currentAssistant])
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files
if (!files) return
setIsUploading(true)
try {
const formData = new FormData()
// Add assistant ID to the upload (if available)
if (currentAssistant) {
formData.append('assistant_id', currentAssistant.id)
}
for (const file of Array.from(files)) {
formData.append('files', file)
}
const response = await fetch('/rag/upload', {
method: 'POST',
body: formData,
})
if (response.ok) {
const result = await response.json()
// Add successfully processed files to the list
const newFiles = result.results
.filter((r: any) => r.success)
.map((r: any) => ({
id: r.doc_id,
name: r.filename,
size: 0, // Server doesn't return size currently
type: 'processed',
uploadedAt: new Date().toISOString(),
status: 'processed',
chunks: r.chunks,
assistantId: currentAssistant?.id
})) as UploadedFile[]
setUploadedFiles((prev: UploadedFile[]) => [...prev, ...newFiles])
// Show errors for failed uploads
const failedUploads = result.results.filter((r: any) => !r.success)
if (failedUploads.length > 0) {
console.error('Some files failed to upload:', failedUploads)
}
} else {
console.error('Upload failed:', response.statusText)
}
// Reset input
event.target.value = ''
} catch (error) {
console.error('File upload error:', error)
} finally {
setIsUploading(false)
}
}
const removeFile = async (fileId: string) => {
try {
const response = await fetch(`/rag/documents/${fileId}`, {
method: 'DELETE',
})
if (response.ok) {
setUploadedFiles((prev: UploadedFile[]) => prev.filter((f: UploadedFile) => f.id !== fileId))
} else {
console.error('Failed to delete document:', response.statusText)
}
} catch (error) {
console.error('Error deleting document:', error)
// Remove from UI anyway
setUploadedFiles((prev: UploadedFile[]) => prev.filter((f: UploadedFile) => f.id !== fileId))
}
}
return (
<div className="space-y-4 pb-6">
{!currentAssistant ? (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BookOpen className="h-4 w-4 text-blue-600" />
<span>Documents & Knowledge Base</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Upload Area */}
<div className="border-2 border-dashed border-gray-200 rounded-lg p-6 text-center hover:border-gray-300 transition-colors">
<input
type="file"
id="file-upload"
multiple
accept=".pdf,.txt,.docx,.md"
onChange={handleFileUpload}
className="hidden"
disabled={isUploading || isLoading}
/>
<Label
htmlFor="file-upload"
className="cursor-pointer flex flex-col items-center space-y-2"
>
<Upload className="h-8 w-8 text-gray-400" />
<span className="text-sm font-medium">
{isUploading ? 'Uploading...' : 'Drag or drop a file'}
</span>
<span className="text-xs text-muted-foreground">
PDF, TXT, DOCX, MD supported
</span>
</Label>
</div>
{/* Documents List */}
{uploadedFiles.length > 0 && (
<div className="space-y-2">
<Label className="text-sm font-medium">Documents ({uploadedFiles.length})</Label>
<div className="space-y-1">
{uploadedFiles.map((file) => (
<div
key={file.id}
className="flex items-center justify-between p-2 border rounded bg-gray-50/50 hover:bg-gray-50"
>
<div className="flex items-center space-x-2 flex-1 min-w-0">
<File className="h-3 w-3 text-blue-600 flex-shrink-0" />
<span className="text-sm truncate">{file.name}</span>
{file.chunks && (
<span className="text-xs text-muted-foreground flex-shrink-0">
{file.chunks} chunks
</span>
)}
</div>
<Button
size="sm"
variant="ghost"
onClick={() => removeFile(file.id)}
disabled={isLoading}
className="h-6 w-6 p-0 text-red-500 hover:text-red-600 hover:bg-red-50"
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
</div>
)}
{/* Empty State */}
{uploadedFiles.length === 0 && (
<div className="text-center py-3">
<p className="text-xs text-muted-foreground">
No documents uploaded yet
</p>
</div>
)}
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">
<User className="h-4 w-4 text-blue-600" />
<span>{currentAssistant.name}</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${
currentAssistant.type === 'user' ? 'bg-blue-100 text-blue-700' :
currentAssistant.type === 'template' ? 'bg-gray-100 text-gray-700' :
'bg-green-100 text-green-700'
}`}>
{currentAssistant.type === 'user' ? 'My Assistant' :
currentAssistant.type === 'template' ? 'Template' : 'New Assistant'}
</span>
</div>
<div className="flex items-center gap-2">
<Label className="text-xs text-muted-foreground">RAG</Label>
<input
type="checkbox"
checked={ragEnabled}
onChange={(e) => setRagEnabled(e.target.checked)}
className="rounded"
disabled={isLoading || uploadedFiles.length === 0}
/>
{ragEnabled && uploadedFiles.length > 0 && (
<div className="flex items-center gap-1 text-green-600">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-xs font-medium">Active</span>
</div>
)}
</div>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* RAG Settings - Compact */}
{ragEnabled && (
<div className="border-l-2 border-blue-200 pl-3 space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs font-medium">Retrieval Depth</Label>
<span className="text-xs text-muted-foreground">{retrievalCount} chunks</span>
</div>
<Slider
value={[retrievalCount]}
onValueChange={(value) => setRetrievalCount(value[0])}
max={10}
min={1}
step={1}
className="w-full"
/>
</div>
)}
{/* Upload Area */}
<div className="border-2 border-dashed border-gray-200 rounded-lg p-4 text-center hover:border-gray-300 transition-colors">
<input
type="file"
id="file-upload"
multiple
accept=".pdf,.txt,.docx,.md"
onChange={handleFileUpload}
className="hidden"
disabled={isUploading || isLoading}
/>
<Label
htmlFor="file-upload"
className="cursor-pointer flex flex-col items-center space-y-2"
>
<Upload className="h-6 w-6 text-gray-400" />
<span className="text-sm">
{isUploading ? 'Uploading...' : 'Drop files or click to upload'}
</span>
<span className="text-xs text-muted-foreground">
PDF, TXT, DOCX, MD supported
</span>
</Label>
</div>
{/* Documents List - Compact */}
{uploadedFiles.length > 0 && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium">Documents ({uploadedFiles.length})</Label>
{ragEnabled && (
<span className="text-xs text-green-600">
Using {Math.min(retrievalCount, uploadedFiles.length)} for context
</span>
)}
</div>
<div className="space-y-1">
{uploadedFiles.map((file) => (
<div
key={file.id}
className="flex items-center justify-between p-2 border rounded bg-gray-50/50 hover:bg-gray-50"
>
<div className="flex items-center space-x-2 flex-1 min-w-0">
<File className="h-3 w-3 text-blue-600 flex-shrink-0" />
<span className="text-sm truncate">{file.name}</span>
{file.chunks && (
<span className="text-xs text-muted-foreground flex-shrink-0">
{file.chunks} chunks
</span>
)}
</div>
<Button
size="sm"
variant="ghost"
onClick={() => removeFile(file.id)}
disabled={isLoading}
className="h-6 w-6 p-0 text-red-500 hover:text-red-600 hover:bg-red-50"
>
<X className="h-3 w-3" />
</Button>
</div>
))}
</div>
</div>
)}
{/* Empty State */}
{uploadedFiles.length === 0 && (
<div className="text-center py-3">
<p className="text-xs text-muted-foreground">
No documents uploaded yet
</p>
</div>
)}
</CardContent>
</Card>
)}
</div>
)
}