import { useState, useEffect, useCallback, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useSettingsStore } from '@/stores/settings' import Button from '@/components/ui/Button' import { cn } from '@/lib/utils' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/Table' import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card' import EmptyCard from '@/components/ui/EmptyCard' import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog' import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog' import { getDocuments, scanNewDocuments, DocsStatusesResponse } from '@/api/lightrag' import { errorMessage } from '@/lib/utils' import { toast } from 'sonner' import { useBackendState } from '@/stores/state' import { RefreshCwIcon, ActivityIcon } from 'lucide-react' import { DocStatusResponse } from '@/api/lightrag' import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog' const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): string => { // Check if file_path exists and is a non-empty string if (!doc.file_path || typeof doc.file_path !== 'string' || doc.file_path.trim() === '') { return doc.id; } // Try to extract filename from path const parts = doc.file_path.split('/'); const fileName = parts[parts.length - 1]; // Ensure extracted filename is valid if (!fileName || fileName.trim() === '') { return doc.id; } // If filename is longer than maxLength, truncate it and add ellipsis return fileName.length > maxLength ? fileName.slice(0, maxLength) + '...' : fileName; }; const pulseStyle = ` /* Custom tooltip styles */ .tooltip-top { bottom: 100% !important; top: auto !important; margin-bottom: 0.25rem !important; margin-top: 0 !important; } /* Fixed tooltip styles for small tables */ .tooltip-fixed { position: fixed !important; z-index: 9999 !important; } /* Parent container for tooltips */ .tooltip-container { position: relative; overflow: visible !important; } @keyframes pulse { 0% { background-color: rgb(255 0 0 / 0.1); border-color: rgb(255 0 0 / 0.2); } 50% { background-color: rgb(255 0 0 / 0.2); border-color: rgb(255 0 0 / 0.4); } 100% { background-color: rgb(255 0 0 / 0.1); border-color: rgb(255 0 0 / 0.2); } } .dark .pipeline-busy { animation: dark-pulse 2s infinite; } @keyframes dark-pulse { 0% { background-color: rgb(255 0 0 / 0.2); border-color: rgb(255 0 0 / 0.4); } 50% { background-color: rgb(255 0 0 / 0.3); border-color: rgb(255 0 0 / 0.6); } 100% { background-color: rgb(255 0 0 / 0.2); border-color: rgb(255 0 0 / 0.4); } } .pipeline-busy { animation: pulse 2s infinite; border: 1px solid; } `; export default function DocumentManager() { const [showPipelineStatus, setShowPipelineStatus] = useState(false) const { t } = useTranslation() const health = useBackendState.use.health() const pipelineBusy = useBackendState.use.pipelineBusy() const [docs, setDocs] = useState(null) const currentTab = useSettingsStore.use.currentTab() const showFileName = useSettingsStore.use.showFileName() const setShowFileName = useSettingsStore.use.setShowFileName() // Store previous status counts const prevStatusCounts = useRef({ processed: 0, processing: 0, pending: 0, failed: 0 }) // Add pulse style to document useEffect(() => { const style = document.createElement('style') style.textContent = pulseStyle document.head.appendChild(style) return () => { document.head.removeChild(style) } }, []) // Reference to the card content element const cardContentRef = useRef(null); // Add tooltip position adjustment based on mouse position useEffect(() => { if (!docs) return; // Function to handle mouse movement const handleMouseMove = (event: MouseEvent) => { const cardContent = cardContentRef.current; if (!cardContent) return; // Get all visible tooltips const visibleTooltips = document.querySelectorAll('.group:hover > div[class*="invisible group-hover:visible absolute"]'); visibleTooltips.forEach(tooltip => { // Get the parent element that triggered the tooltip const triggerElement = tooltip.parentElement; if (!triggerElement) return; const triggerRect = triggerElement.getBoundingClientRect(); // Use fixed positioning for all tooltips tooltip.classList.add('tooltip-fixed'); // Calculate position based on trigger element and mouse const tooltipHeight = tooltip.offsetHeight; const viewportHeight = window.innerHeight; // Check if tooltip would go off the bottom of the viewport const wouldOverflowBottom = event.clientY + tooltipHeight > viewportHeight; if (wouldOverflowBottom) { // Position above the trigger tooltip.style.top = `${triggerRect.top - tooltipHeight - 5}px`; tooltip.style.bottom = 'auto'; } else { // Position below the trigger tooltip.style.top = `${triggerRect.bottom + 5}px`; tooltip.style.bottom = 'auto'; } // Horizontal positioning tooltip.style.left = `${triggerRect.left}px`; tooltip.style.maxWidth = '600px'; }); }; // Add mouse move listener to the document document.addEventListener('mousemove', handleMouseMove); return () => { document.removeEventListener('mousemove', handleMouseMove); }; }, [docs]); const fetchDocuments = useCallback(async () => { try { const docs = await getDocuments() // Get new status counts (treat null as all zeros) const newStatusCounts = { processed: docs?.statuses?.processed?.length || 0, processing: docs?.statuses?.processing?.length || 0, pending: docs?.statuses?.pending?.length || 0, failed: docs?.statuses?.failed?.length || 0 } // Check if any status count has changed const hasStatusCountChange = (Object.keys(newStatusCounts) as Array).some( status => newStatusCounts[status] !== prevStatusCounts.current[status] ) // Trigger health check if changes detected if (hasStatusCountChange) { useBackendState.getState().check() } // Update previous status counts prevStatusCounts.current = newStatusCounts // Update docs state if (docs && docs.statuses) { const numDocuments = Object.values(docs.statuses).reduce( (acc, status) => acc + status.length, 0 ) if (numDocuments > 0) { setDocs(docs) } else { setDocs(null) } } else { setDocs(null) } } catch (err) { toast.error(t('documentPanel.documentManager.errors.loadFailed', { error: errorMessage(err) })) } }, [setDocs, t]) // Fetch documents when the tab becomes visible useEffect(() => { if (currentTab === 'documents') { fetchDocuments() } }, [currentTab, fetchDocuments]) const scanDocuments = useCallback(async () => { try { const { status } = await scanNewDocuments() toast.message(status) } catch (err) { toast.error(t('documentPanel.documentManager.errors.scanFailed', { error: errorMessage(err) })) } }, [t]) // Set up polling when the documents tab is active and health is good useEffect(() => { if (currentTab !== 'documents' || !health) { return } const interval = setInterval(async () => { try { await fetchDocuments() } catch (err) { toast.error(t('documentPanel.documentManager.errors.scanProgressFailed', { error: errorMessage(err) })) } }, 5000) return () => clearInterval(interval) }, [health, fetchDocuments, t, currentTab]) return ( {t('documentPanel.documentManager.title')}
{t('documentPanel.documentManager.uploadedTitle')}
{t('documentPanel.documentManager.fileNameLabel')}
{!docs && (
)} {docs && (
{t('documentPanel.documentManager.columns.id')} {t('documentPanel.documentManager.columns.summary')} {t('documentPanel.documentManager.columns.status')} {t('documentPanel.documentManager.columns.length')} {t('documentPanel.documentManager.columns.chunks')} {t('documentPanel.documentManager.columns.created')} {t('documentPanel.documentManager.columns.updated')} {Object.entries(docs.statuses).map(([status, documents]) => documents.map((doc) => ( {showFileName ? ( <>
{getDisplayFileName(doc, 25)}
{doc.file_path}
{doc.id}
) : (
{doc.id}
{doc.file_path}
)}
{doc.content_summary}
{doc.content_summary}
{status === 'processed' && ( {t('documentPanel.documentManager.status.completed')} )} {status === 'processing' && ( {t('documentPanel.documentManager.status.processing')} )} {status === 'pending' && {t('documentPanel.documentManager.status.pending')}} {status === 'failed' && {t('documentPanel.documentManager.status.failed')}} {doc.error && ( ⚠️ )} {doc.content_length ?? '-'} {doc.chunks_count ?? '-'} {new Date(doc.created_at).toLocaleString()} {new Date(doc.updated_at).toLocaleString()}
)) )}
)}
) }