Spaces:
Sleeping
Sleeping
File size: 10,123 Bytes
8421ec4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | 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;
|