Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { Document } from '../types'; | |
| import { getDocuments, uploadDocument, deleteDocument } from '../services/api'; | |
| interface DocumentsModalProps { | |
| isOpen: boolean; | |
| onClose: () => void; | |
| } | |
| const DocumentsModal: React.FC<DocumentsModalProps> = ({ isOpen, onClose }) => { | |
| const [documents, setDocuments] = useState<Document[]>([]); | |
| const [isLoading, setIsLoading] = useState(false); | |
| const [isUploading, setIsUploading] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| const fileInputRef = useRef<HTMLInputElement>(null); | |
| useEffect(() => { | |
| if (isOpen) { | |
| loadDocuments(); | |
| } | |
| }, [isOpen]); | |
| const loadDocuments = async () => { | |
| try { | |
| setIsLoading(true); | |
| setError(null); | |
| const docs = await getDocuments(); | |
| setDocuments(docs); | |
| } catch (err) { | |
| setError('Failed to load documents'); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => { | |
| if (e.target.files && e.target.files[0]) { | |
| const file = e.target.files[0]; | |
| if (file.type !== 'application/pdf') { | |
| setError('Only PDF files are allowed'); | |
| return; | |
| } | |
| try { | |
| setIsUploading(true); | |
| setError(null); | |
| await uploadDocument(file); | |
| await loadDocuments(); // Reload list | |
| } catch (err) { | |
| setError(err instanceof Error ? err.message : 'Upload failed'); | |
| } finally { | |
| setIsUploading(false); | |
| // Reset input | |
| if (fileInputRef.current) fileInputRef.current.value = ''; | |
| } | |
| } | |
| }; | |
| const handleDelete = async (docId: string) => { | |
| try { | |
| if (!confirm('Are you sure you want to delete this document?')) return; | |
| setIsLoading(true); | |
| await deleteDocument(docId); | |
| await loadDocuments(); | |
| } catch (err) { | |
| setError(err instanceof Error ? err.message : 'Delete failed'); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| if (!isOpen) return null; | |
| return ( | |
| <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> | |
| <div className="bg-white rounded-2xl w-full max-w-2xl max-h-[80vh] flex flex-col shadow-2xl animate-in fade-in zoom-in-95 duration-200"> | |
| {/* Header */} | |
| <div className="p-6 border-b border-gray-100 flex items-center justify-between"> | |
| <div> | |
| <h2 className="text-xl font-bold text-gray-900">Manage Documents</h2> | |
| <p className="text-sm text-gray-500 mt-1">Upload PDFs to include in the RAG knowledge base</p> | |
| </div> | |
| <button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-full transition-colors text-gray-500"> | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| </div> | |
| {/* Content */} | |
| <div className="flex-1 overflow-y-auto p-6"> | |
| {/* Upload Area */} | |
| <div className="mb-8"> | |
| <input | |
| type="file" | |
| ref={fileInputRef} | |
| accept=".pdf" | |
| onChange={handleFileSelect} | |
| className="hidden" | |
| /> | |
| <button | |
| onClick={() => fileInputRef.current?.click()} | |
| disabled={isUploading} | |
| className={`w-full border-2 border-dashed border-indigo-200 bg-indigo-50/50 rounded-xl p-8 flex flex-col items-center justify-center transition-all ${isUploading ? 'cursor-not-allowed opacity-75' : 'hover:border-indigo-400 hover:bg-indigo-50 cursor-pointer' | |
| }`} | |
| > | |
| {isUploading ? ( | |
| <> | |
| <div className="w-10 h-10 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin mb-3"></div> | |
| <span className="text-indigo-600 font-medium">Processing Document...</span> | |
| <span className="text-xs text-indigo-400 mt-1">Extracting text & generating embeddings</span> | |
| </> | |
| ) : ( | |
| <> | |
| <div className="w-12 h-12 bg-indigo-100 text-indigo-600 rounded-full flex items-center justify-center mb-3"> | |
| <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /> | |
| </svg> | |
| </div> | |
| <span className="text-gray-900 font-medium">Click to upload PDF</span> | |
| <span className="text-xs text-gray-500 mt-1">Maximum file size: 10MB</span> | |
| </> | |
| )} | |
| </button> | |
| </div> | |
| {/* Error Message */} | |
| {error && ( | |
| <div className="mb-6 p-4 bg-red-50 text-red-600 rounded-xl text-sm flex items-center gap-2 border border-red-100"> | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
| </svg> | |
| {error} | |
| </div> | |
| )} | |
| {/* Document List */} | |
| <div> | |
| <h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wider mb-3">Uploaded Documents</h3> | |
| {isLoading && documents.length === 0 ? ( | |
| <div className="text-center py-8 text-gray-400">Loading...</div> | |
| ) : documents.length === 0 ? ( | |
| <div className="text-center py-8 text-gray-400 bg-gray-50 rounded-xl border border-gray-100 border-dashed"> | |
| No documents uploaded yet. | |
| </div> | |
| ) : ( | |
| <div className="space-y-2"> | |
| {documents.map((doc) => ( | |
| <div key={doc.id} className="flex items-center justify-between p-3 bg-white border border-gray-200 rounded-lg hover:border-indigo-200 transition-colors"> | |
| <div className="flex items-center gap-3 overflow-hidden"> | |
| <div className="w-10 h-10 bg-red-100 text-red-600 rounded-lg flex-shrink-0 flex items-center justify-center"> | |
| <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> | |
| <path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6z" /> | |
| </svg> | |
| </div> | |
| <div className="min-w-0"> | |
| <h4 className="text-sm font-medium text-gray-900 truncate">{doc.filename}</h4> | |
| <p className="text-xs text-gray-500"> | |
| {new Date(doc.upload_date).toLocaleDateString()} • {doc.status} | |
| </p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={() => handleDelete(doc.id)} | |
| className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors" | |
| title="Delete Document" | |
| > | |
| <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> | |
| </svg> | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* Footer */} | |
| <div className="p-4 border-t border-gray-100 bg-gray-50 rounded-b-2xl"> | |
| <p className="text-xs text-center text-gray-500"> | |
| Documents are processed locally. Embeddings are stored in ChromaDB vector store. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default DocumentsModal; | |