Spaces:
Running
Running
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { UploadCloud, File, Trash2, ShieldAlert, CheckCircle, Loader, Eye, FileText } from 'lucide-react'; | |
| import { getProjectDocuments, uploadProjectDocument, deleteProjectDocument } from '../../api/client'; | |
| interface ResourcePanelProps { | |
| projectId: string; | |
| } | |
| const ProjectResourcesPanel: React.FC<ResourcePanelProps> = ({ projectId }) => { | |
| const [documents, setDocuments] = useState<any[]>([]); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [isUploading, setIsUploading] = useState(false); | |
| const [dragActive, setDragActive] = useState(false); | |
| const [toastMsg, setToastMsg] = useState(""); | |
| const [expandedDoc, setExpandedDoc] = useState<string | null>(null); | |
| const inputRef = useRef<HTMLInputElement>(null); | |
| const fetchDocuments = async () => { | |
| try { | |
| setIsLoading(true); | |
| const data = await getProjectDocuments(projectId); | |
| setDocuments(data || []); | |
| } catch (err) { | |
| console.error("Error fetching documents:", err); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| if (projectId) { | |
| fetchDocuments(); | |
| } | |
| }, [projectId]); | |
| const showToast = (msg: string) => { | |
| setToastMsg(msg); | |
| setTimeout(() => setToastMsg(""), 3000); | |
| }; | |
| const handleUpload = async (file: File) => { | |
| if (file.size > 10 * 1024 * 1024) { | |
| alert("Plik przekracza rozmiar 10MB"); | |
| return; | |
| } | |
| const validTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/msword', 'text/plain']; | |
| // Quick extension check as fallback | |
| const ext = file.name.split('.').pop()?.toLowerCase(); | |
| if (!validTypes.includes(file.type) && !['pdf', 'docx', 'doc', 'txt'].includes(ext || '')) { | |
| alert("Nieobsługiwany format. Użyj PDF, DOCX, DOC lub TXT."); | |
| return; | |
| } | |
| try { | |
| setIsUploading(true); | |
| await uploadProjectDocument(projectId, file); | |
| showToast("Dokument wgrany i dodany do wiedzy projektu"); | |
| await fetchDocuments(); | |
| } catch (err: any) { | |
| console.error(err); | |
| alert("Błąd podczas wgrywania dokumentu: " + (err.response?.data?.detail || err.message)); | |
| } finally { | |
| setIsUploading(false); | |
| if (inputRef.current) inputRef.current.value = ""; | |
| } | |
| }; | |
| const handleDrag = (e: React.DragEvent) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (e.type === "dragenter" || e.type === "dragover") { | |
| setDragActive(true); | |
| } else if (e.type === "dragleave") { | |
| setDragActive(false); | |
| } | |
| }; | |
| const handleDrop = async (e: React.DragEvent) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| setDragActive(false); | |
| if (e.dataTransfer.files && e.dataTransfer.files[0]) { | |
| handleUpload(e.dataTransfer.files[0]); | |
| } | |
| }; | |
| const handleDelete = async (filename: string) => { | |
| if (!window.confirm(`Czy na pewno chcesz usunąć plik ${filename}? Zostanie on usunięty z wiedzy RAG.`)) return; | |
| try { | |
| await deleteProjectDocument(projectId, filename); | |
| showToast("Plik został usunięty"); | |
| await fetchDocuments(); | |
| } catch (err) { | |
| alert("Błąd podczas usuwania pliku."); | |
| } | |
| }; | |
| return ( | |
| <div style={{ maxWidth: '1000px', margin: '0 auto', display: 'flex', flexDirection: 'column', gap: '2rem' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> | |
| <div> | |
| <h2 style={{ fontSize: '1.8rem', fontWeight: 800, margin: '0 0 0.5rem 0' }}>Zasoby</h2> | |
| <p style={{ color: 'var(--text-secondary)', margin: 0 }}> | |
| Tutaj możesz dodać umowy, oferty, biznesplany – pomogą AI lepiej zrozumieć Twój projekt. | |
| </p> | |
| </div> | |
| </div> | |
| {toastMsg && ( | |
| <div style={{ background: 'rgba(16,185,129,0.1)', borderLeft: '3px solid var(--accent-green)', padding: '1rem', borderRadius: '4px', display: 'flex', alignItems: 'center', gap: '0.5rem', color: '#A7F3D0', fontWeight: 600 }}> | |
| <CheckCircle size={18} /> {toastMsg} | |
| </div> | |
| )} | |
| {/* UPLOADER */} | |
| <div | |
| onDragEnter={handleDrag} | |
| onDragOver={handleDrag} | |
| onDragLeave={handleDrag} | |
| onDrop={handleDrop} | |
| style={{ | |
| border: dragActive ? '2px dashed var(--accent-blue)' : '2px dashed rgba(255,255,255,0.2)', | |
| background: dragActive ? 'rgba(59,130,246,0.05)' : 'var(--bg-elevated)', | |
| borderRadius: '12px', | |
| padding: '4rem 2rem', | |
| textAlign: 'center', | |
| transition: 'all 0.2s ease', | |
| cursor: 'pointer', | |
| position: 'relative' | |
| }} | |
| onClick={() => inputRef.current?.click()} | |
| > | |
| <input | |
| type="file" | |
| ref={inputRef} | |
| style={{ display: 'none' }} | |
| accept=".pdf,.docx,.doc,.txt" | |
| onChange={(e) => { | |
| if (e.target.files && e.target.files[0]) handleUpload(e.target.files[0]); | |
| }} | |
| /> | |
| {isUploading ? ( | |
| <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem', color: 'var(--accent-blue)' }}> | |
| <Loader className="spin" size={48} /> | |
| <h3 style={{ margin: 0 }}>Analizowanie dokumentu...</h3> | |
| <p style={{ color: 'var(--text-muted)', fontSize: '0.9rem', margin: 0 }}>Trwa ekstrakcja tekstu i wektoryzacja w Pinecone</p> | |
| </div> | |
| ) : ( | |
| <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}> | |
| <div style={{ width: '64px', height: '64px', borderRadius: '50%', background: 'rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> | |
| <UploadCloud size={32} color={dragActive ? 'var(--accent-blue)' : 'var(--text-secondary)'} /> | |
| </div> | |
| <h3 style={{ margin: 0, fontSize: '1.2rem', color: dragActive ? 'var(--accent-blue)' : '#fff' }}> | |
| {dragActive ? 'Upuść plik tutaj' : 'Kliknij lub przeciągnij plik tutaj'} | |
| </h3> | |
| <p style={{ color: 'var(--text-muted)', fontSize: '0.9rem', margin: 0 }}> | |
| Obsługiwane formaty: PDF, DOCX, TXT. Maksymalny rozmiar: 10 MB. | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| {/* LISTA DOKUMENTÓW */} | |
| <div className="glass-card" style={{ padding: '0' }}> | |
| <div style={{ padding: '1.5rem', borderBottom: '1px solid rgba(255,255,255,0.05)' }}> | |
| <h3 style={{ margin: 0, fontSize: '1.1rem', fontWeight: 600 }}>Wgrane dokumenty ({documents?.length || 0} / 5 dla Free)</h3> | |
| </div> | |
| {isLoading ? ( | |
| <div style={{ padding: '2rem', textAlign: 'center', color: 'var(--text-muted)' }}>Ładowanie...</div> | |
| ) : documents?.length === 0 ? ( | |
| <div style={{ padding: '3rem 2rem', textAlign: 'center', color: 'var(--text-muted)' }}> | |
| <FileText size={48} color="var(--accent-blue)" style={{ opacity: 0.5, marginBottom: '1rem' }} /> | |
| <div style={{ fontSize: '1.05rem', marginBottom: '0.5rem', fontWeight: 600, color: 'var(--text-primary)' }}>Brak dołączonych plików do tego projektu.</div> | |
| <div style={{ maxWidth: '400px', margin: '0 auto', lineHeight: 1.5 }}>Tutaj możesz dodać umowy, oferty, biznesplany – pomogą AI lepiej zrozumieć Twój projekt.</div> | |
| </div> | |
| ) : ( | |
| <div style={{ display: 'flex', flexDirection: 'column' }}> | |
| {documents.map((doc, idx) => ( | |
| <div key={doc.id || idx} style={{ borderBottom: idx !== documents.length - 1 ? '1px solid rgba(255,255,255,0.05)' : 'none' }}> | |
| <div style={{ | |
| display: 'flex', alignItems: 'center', justifyContent: 'space-between', | |
| padding: '1rem 1.5rem', | |
| background: 'transparent', | |
| transition: '0.2s' | |
| }} className="hover-bg-subtle"> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}> | |
| <File size={24} color="var(--accent-blue)" /> | |
| <div> | |
| <div style={{ fontWeight: 600, color: '#fff', fontSize: '1rem', wordBreak: 'break-all' }}>{doc.filename}</div> | |
| <div style={{ fontSize: '0.8rem', color: 'var(--text-muted)', display: 'flex', gap: '1rem' }}> | |
| <span>Wgrano: {new Date(doc.uploaded_at).toLocaleString('pl-PL', { day: 'numeric', month: 'long', year: 'numeric' })}</span> | |
| <span>Rozmiar: {((doc.size_bytes || doc.size || 0) / 1024 / 1024).toFixed(2)} MB</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div style={{ display: 'flex', gap: '0.5rem' }}> | |
| <button | |
| className="btn hover-bg" | |
| style={{ background: expandedDoc === doc.id ? 'var(--accent-blue)' : 'rgba(255,255,255,0.05)', color: expandedDoc === doc.id ? '#fff' : 'var(--text-secondary)', border: 'none', padding: '0.5rem', borderRadius: '8px' }} | |
| onClick={() => setExpandedDoc(expandedDoc === doc.id ? null : doc.id)} | |
| title="Pokaż wyciągnięty tekst" | |
| > | |
| <Eye size={18} /> | |
| </button> | |
| <button | |
| className="btn hover-bg" | |
| style={{ background: 'rgba(239, 68, 68, 0.1)', color: 'var(--accent-red)', border: 'none', padding: '0.5rem', borderRadius: '8px' }} | |
| onClick={() => handleDelete(doc.filename)} | |
| title="Usuń plik i wyczyść wektory" | |
| > | |
| <Trash2 size={18} /> | |
| </button> | |
| </div> | |
| </div> | |
| {expandedDoc === doc.id && ( | |
| <div style={{ padding: '1.5rem', background: 'rgba(0,0,0,0.3)', borderTop: '1px solid rgba(255,255,255,0.02)', fontSize: '0.9rem', color: 'var(--text-secondary)' }}> | |
| <h4 style={{ margin: '0 0 0.5rem 0', color: 'var(--text-primary)' }}>Wyciągnięty tekst (podgląd):</h4> | |
| <div style={{ maxHeight: '200px', overflowY: 'auto', whiteSpace: 'pre-wrap', fontFamily: 'monospace' }} className="custom-scrollbar"> | |
| {doc.extracted_text ? doc.extracted_text : 'Brak wyciągniętego tekstu.'} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| <style>{`.spin { animation: spin 1s linear infinite; } @keyframes spin { 100% { transform: rotate(360deg); } } .hover-bg-subtle:hover { background: rgba(255,255,255,0.02) ; }`}</style> | |
| </div> | |
| ); | |
| }; | |
| export default ProjectResourcesPanel; | |