|
|
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) |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const loadDocuments = async () => { |
|
|
if (!currentAssistant) { |
|
|
|
|
|
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 { |
|
|
|
|
|
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 |
|
|
})) as UploadedFile[] |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
const newFiles = result.results |
|
|
.filter((r: any) => r.success) |
|
|
.map((r: any) => ({ |
|
|
id: r.doc_id, |
|
|
name: r.filename, |
|
|
size: 0, |
|
|
type: 'processed', |
|
|
uploadedAt: new Date().toISOString(), |
|
|
status: 'processed', |
|
|
chunks: r.chunks, |
|
|
assistantId: currentAssistant?.id |
|
|
})) as UploadedFile[] |
|
|
|
|
|
setUploadedFiles((prev: UploadedFile[]) => [...prev, ...newFiles]) |
|
|
|
|
|
|
|
|
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) |
|
|
} |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
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> |
|
|
) |
|
|
} |
|
|
|