/** * PersonaDocumentsPanel — manages documents attached to a persona for * Chat-with-Docs (RAG). * * Features: * - Upload new documents (PDF/TXT/MD) * - List attached docs with indexing status * - Toggle mode: indexed / pinned / excluded * - Detach (safe remove) documents */ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Upload, FileText, Loader2, BookOpen, Pin, EyeOff, Trash2, CheckCircle2, AlertCircle, Clock, RefreshCw, } from 'lucide-react'; import { listPersonaDocuments, uploadProjectItem, attachPersonaDocument, setPersonaDocumentMode, deletePersonaDocumentPermanently, } from '../inventoryApi'; // --------------------------------------------------------------------------- // Mode config // --------------------------------------------------------------------------- const MODE_CONFIG = { indexed: { label: 'Indexed', icon: BookOpen, color: 'text-green-400', desc: 'Included in RAG retrieval' }, pinned: { label: 'Pinned', icon: Pin, color: 'text-amber-400', desc: 'Always included (boosted)' }, excluded: { label: 'Excluded', icon: EyeOff, color: 'text-red-400', desc: 'Excluded from retrieval' }, }; const STATUS_CONFIG = { ready: { label: 'Ready', icon: CheckCircle2, color: 'text-green-400' }, indexing: { label: 'Indexing...', icon: Loader2, color: 'text-amber-400' }, error: { label: 'Error', icon: AlertCircle, color: 'text-red-400' }, not_indexed: { label: 'Not indexed', icon: Clock, color: 'text-white/30' }, }; // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export function PersonaDocumentsPanel({ projectId, backendUrl, apiKey, onChanged }) { const [docs, setDocs] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [uploading, setUploading] = useState(false); const [actionInProgress, setActionInProgress] = useState(null); const fileInputRef = useRef(null); // ----------------------------------------------------------------------- // Load documents // ----------------------------------------------------------------------- const loadDocs = useCallback(async () => { try { const data = await listPersonaDocuments(backendUrl, projectId, { apiKey }); setDocs(data.documents || []); setError(null); } catch (e) { setError(e.message); } finally { setLoading(false); } }, [backendUrl, projectId, apiKey]); useEffect(() => { loadDocs(); }, [loadDocs]); // ----------------------------------------------------------------------- // Upload handler // ----------------------------------------------------------------------- const handleUpload = useCallback(async (files) => { if (!files || files.length === 0) return; setUploading(true); setError(null); for (const file of Array.from(files)) { try { // 1. Upload file as project item const result = await uploadProjectItem(backendUrl, projectId, file, { apiKey, category: 'file', }); if (result.ok && result.item?.id) { // 2. Attach to persona with mode=indexed await attachPersonaDocument(backendUrl, projectId, result.item.id, 'indexed', { apiKey }); } } catch (e) { setError(`Upload failed for ${file.name}: ${e.message}`); } } setUploading(false); loadDocs(); onChanged?.(); }, [backendUrl, projectId, apiKey, loadDocs, onChanged]); // ----------------------------------------------------------------------- // Mode change handler // ----------------------------------------------------------------------- const handleModeChange = useCallback(async (itemId, newMode) => { setActionInProgress(itemId); try { await setPersonaDocumentMode(backendUrl, projectId, itemId, newMode, { apiKey }); loadDocs(); } catch (e) { setError(e.message); } finally { setActionInProgress(null); } }, [backendUrl, projectId, apiKey, loadDocs]); // ----------------------------------------------------------------------- // Delete handler — removes document from persona AND from storage // ----------------------------------------------------------------------- const handleDelete = useCallback(async (doc) => { if (!confirm(`Delete "${doc.original_name || doc.name}"? This will remove the file from storage.`)) return; setActionInProgress(doc.id); try { await deletePersonaDocumentPermanently(backendUrl, projectId, doc.id, { apiKey }); loadDocs(); onChanged?.(); } catch (e) { setError(e.message); } finally { setActionInProgress(null); } }, [backendUrl, projectId, apiKey, loadDocs, onChanged]); // ----------------------------------------------------------------------- // Render: document row // ----------------------------------------------------------------------- const renderDocRow = (doc) => { const props = doc.properties || {}; const status = props.index_status || 'not_indexed'; const statusCfg = STATUS_CONFIG[status] || STATUS_CONFIG.not_indexed; const modeCfg = MODE_CONFIG[doc.mode] || MODE_CONFIG.indexed; const StatusIcon = statusCfg.icon; const isActive = actionInProgress === doc.id; const ext = (doc.original_name || doc.name || '').split('.').pop()?.toUpperCase() || 'FILE'; const sizeStr = doc.size_bytes > 1048576 ? `${(doc.size_bytes / 1048576).toFixed(1)} MB` : doc.size_bytes > 0 ? `${Math.round(doc.size_bytes / 1024)} KB` : ''; return (
{/* File icon */}
{/* Name + meta */}
{doc.original_name || doc.name}
{ext} {sizeStr && {sizeStr}} {/* Index status */} {statusCfg.label} {props.chunk_count != null && props.chunk_count > 0 && ({props.chunk_count} chunks)}
{/* Mode selector */}
{['indexed', 'pinned', 'excluded'].map((m) => { const cfg = MODE_CONFIG[m]; const MIcon = cfg.icon; const active = doc.mode === m; return (); })}
{/* Delete button */}
); }; // ----------------------------------------------------------------------- // Main render // ----------------------------------------------------------------------- return (
{/* Header with upload button */}
Knowledge Base ({docs.length} docs)
handleUpload(e.target.files)}/>
{/* Error */} {error && (
{error}
)} {/* Loading */} {loading && (
)} {/* Document list */} {!loading && docs.length === 0 && (

No documents attached to this persona.

Add PDFs, text, or markdown files to enable Chat-with-Docs.

)} {!loading && docs.length > 0 && (
{docs.map(renderDocRow)}
)} {/* Refresh */} {!loading && docs.length > 0 && (
)}
); }