feat(DocumentManager): Add document status filter feature, support filtering documents by status

This commit is contained in:
Eric Shao
2025-03-31 12:43:21 +08:00
parent 22a4e08439
commit b79dac9d63
7 changed files with 253 additions and 179 deletions

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,7 @@
<link rel="icon" type="image/svg+xml" href="logo.png" /> <link rel="icon" type="image/svg+xml" href="logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title> <title>Lightrag</title>
<script type="module" crossorigin src="/webui/assets/index-raheqJeu.js"></script> <script type="module" crossorigin src="/webui/assets/index-CfYQAXql.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-CD5HxTy1.css"> <link rel="stylesheet" crossorigin href="/webui/assets/index-CD5HxTy1.css">
</head> </head>
<body> <body>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, useRef } from 'react' import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
@@ -16,15 +16,17 @@ import EmptyCard from '@/components/ui/EmptyCard'
import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog' import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog'
import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog' import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog'
import { getDocuments, scanNewDocuments, DocsStatusesResponse } from '@/api/lightrag' import { getDocuments, scanNewDocuments, DocsStatusesResponse, DocStatus, DocStatusResponse } from '@/api/lightrag'
import { errorMessage } from '@/lib/utils' import { errorMessage } from '@/lib/utils'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useBackendState } from '@/stores/state' import { useBackendState } from '@/stores/state'
import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon } from 'lucide-react' import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon, FilterIcon } from 'lucide-react'
import { DocStatusResponse } from '@/api/lightrag'
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog' import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
type StatusFilter = DocStatus | 'all';
const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): string => { const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): string => {
// Check if file_path exists and is a non-empty 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() === '') { if (!doc.file_path || typeof doc.file_path !== 'string' || doc.file_path.trim() === '') {
@@ -148,6 +150,10 @@ export default function DocumentManager() {
const [sortField, setSortField] = useState<SortField>('updated_at') const [sortField, setSortField] = useState<SortField>('updated_at')
const [sortDirection, setSortDirection] = useState<SortDirection>('desc') const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
// State for document status filter
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
// Handle sort column click // Handle sort column click
const handleSort = (field: SortField) => { const handleSort = (field: SortField) => {
if (sortField === field) { if (sortField === field) {
@@ -159,36 +165,54 @@ export default function DocumentManager() {
setSortDirection('desc') setSortDirection('desc')
} }
} }
const filteredAndSortedDocs = useMemo(() => {
if (!docs) return null;
// Sort documents based on current sort field and direction let filteredDocs = { ...docs };
const sortDocuments = (documents: DocStatusResponse[]) => {
return [...documents].sort((a, b) => {
let valueA, valueB;
// Special handling for ID field based on showFileName setting if (statusFilter !== 'all') {
if (sortField === 'id' && showFileName) { filteredDocs = {
valueA = getDisplayFileName(a); ...docs,
valueB = getDisplayFileName(b); statuses: {
} else if (sortField === 'id') { pending: [],
valueA = a.id; processing: [],
valueB = b.id; processed: [],
} else { failed: [],
// Date fields [statusFilter]: docs.statuses[statusFilter] || []
valueA = new Date(a[sortField]).getTime(); }
valueB = new Date(b[sortField]).getTime(); };
} }
// Apply sort direction if (!sortField || !sortDirection) return filteredDocs;
const sortMultiplier = sortDirection === 'asc' ? 1 : -1;
// Compare values const sortedStatuses = Object.entries(filteredDocs.statuses).reduce((acc, [status, documents]) => {
if (typeof valueA === 'string' && typeof valueB === 'string') { const sortedDocuments = [...documents].sort((a, b) => {
return sortMultiplier * valueA.localeCompare(valueB); if (sortDirection === 'asc') {
return (a[sortField] ?? 0) > (b[sortField] ?? 0) ? 1 : -1;
} else { } else {
return sortMultiplier * (valueA > valueB ? 1 : valueA < valueB ? -1 : 0); return (a[sortField] ?? 0) < (b[sortField] ?? 0) ? 1 : -1;
} }
}); });
} acc[status as DocStatus] = sortedDocuments;
return acc;
}, {} as DocsStatusesResponse['statuses']);
return { ...filteredDocs, statuses: sortedStatuses };
}, [docs, sortField, sortDirection, statusFilter]);
// Calculate document counts for each status
const documentCounts = useMemo(() => {
if (!docs) return { all: 0 } as Record<string, number>;
const counts: Record<string, number> = { all: 0 };
Object.entries(docs.statuses).forEach(([status, documents]) => {
counts[status as DocStatus] = documents.length;
counts.all += documents.length;
});
return counts;
}, [docs]);
// Store previous status counts // Store previous status counts
const prevStatusCounts = useRef({ const prevStatusCounts = useRef({
@@ -398,6 +422,50 @@ export default function DocumentManager() {
<CardHeader className="flex-none py-2 px-4"> <CardHeader className="flex-none py-2 px-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<CardTitle>{t('documentPanel.documentManager.uploadedTitle')}</CardTitle> <CardTitle>{t('documentPanel.documentManager.uploadedTitle')}</CardTitle>
<div className="flex items-center gap-2">
<FilterIcon className="h-4 w-4" />
<div className="flex gap-1">
<Button
size="sm"
variant={statusFilter === 'all' ? 'secondary' : 'outline'}
onClick={() => setStatusFilter('all')}
>
{t('documentPanel.documentManager.status.all')} ({documentCounts.all})
</Button>
<Button
size="sm"
variant={statusFilter === 'processed' ? 'secondary' : 'outline'}
onClick={() => setStatusFilter('processed')}
className={documentCounts.processed > 0 ? 'text-green-600' : 'text-gray-500'}
>
{t('documentPanel.documentManager.status.completed')} ({documentCounts.processed || 0})
</Button>
<Button
size="sm"
variant={statusFilter === 'processing' ? 'secondary' : 'outline'}
onClick={() => setStatusFilter('processing')}
className={documentCounts.processing > 0 ? 'text-blue-600' : 'text-gray-500'}
>
{t('documentPanel.documentManager.status.processing')} ({documentCounts.processing || 0})
</Button>
<Button
size="sm"
variant={statusFilter === 'pending' ? 'secondary' : 'outline'}
onClick={() => setStatusFilter('pending')}
className={documentCounts.pending > 0 ? 'text-yellow-600' : 'text-gray-500'}
>
{t('documentPanel.documentManager.status.pending')} ({documentCounts.pending || 0})
</Button>
<Button
size="sm"
variant={statusFilter === 'failed' ? 'secondary' : 'outline'}
onClick={() => setStatusFilter('failed')}
className={documentCounts.failed > 0 ? 'text-red-600' : 'text-gray-500'}
>
{t('documentPanel.documentManager.status.failed')} ({documentCounts.failed || 0})
</Button>
</div>
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-gray-500">{t('documentPanel.documentManager.fileNameLabel')}</span> <span className="text-sm text-gray-500">{t('documentPanel.documentManager.fileNameLabel')}</span>
<Button <Button
@@ -477,11 +545,8 @@ export default function DocumentManager() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody className="text-sm overflow-auto"> <TableBody className="text-sm overflow-auto">
{Object.entries(docs.statuses).flatMap(([status, documents]) => { {filteredAndSortedDocs?.statuses && Object.entries(filteredAndSortedDocs.statuses).flatMap(([status, documents]) =>
// Apply sorting to documents documents.map((doc) => (
const sortedDocuments = sortDocuments(documents);
return sortedDocuments.map(doc => (
<TableRow key={doc.id}> <TableRow key={doc.id}>
<TableCell className="truncate font-mono overflow-visible max-w-[250px]"> <TableCell className="truncate font-mono overflow-visible max-w-[250px]">
{showFileName ? ( {showFileName ? (
@@ -541,8 +606,8 @@ export default function DocumentManager() {
{new Date(doc.updated_at).toLocaleString()} {new Date(doc.updated_at).toLocaleString()}
</TableCell> </TableCell>
</TableRow> </TableRow>
)); )))
})} }
</TableBody> </TableBody>
</Table> </Table>
</div> </div>

View File

@@ -95,6 +95,7 @@
"metadata": "البيانات الوصفية" "metadata": "البيانات الوصفية"
}, },
"status": { "status": {
"all": "الكل",
"completed": "مكتمل", "completed": "مكتمل",
"processing": "قيد المعالجة", "processing": "قيد المعالجة",
"pending": "معلق", "pending": "معلق",

View File

@@ -95,6 +95,7 @@
"metadata": "Metadata" "metadata": "Metadata"
}, },
"status": { "status": {
"all": "All",
"completed": "Completed", "completed": "Completed",
"processing": "Processing", "processing": "Processing",
"pending": "Pending", "pending": "Pending",

View File

@@ -95,6 +95,7 @@
"metadata": "Métadonnées" "metadata": "Métadonnées"
}, },
"status": { "status": {
"all": "Tous",
"completed": "Terminé", "completed": "Terminé",
"processing": "En traitement", "processing": "En traitement",
"pending": "En attente", "pending": "En attente",

View File

@@ -95,6 +95,7 @@
"metadata": "元数据" "metadata": "元数据"
}, },
"status": { "status": {
"all": "全部",
"completed": "已完成", "completed": "已完成",
"processing": "处理中", "processing": "处理中",
"pending": "等待中", "pending": "等待中",