yangdx
commited on
Commit
·
23f8583
1
Parent(s):
7ff0d05
Merge branch 'feat-document-filter'
Browse files- lightrag/api/webui/assets/{index-CldiPPHk.js → index-B0dMyyzE.js} +0 -0
- lightrag/api/webui/index.html +0 -0
- lightrag_webui/src/features/DocumentManager.tsx +101 -11
- lightrag_webui/src/locales/ar.json +1 -0
- lightrag_webui/src/locales/en.json +1 -0
- lightrag_webui/src/locales/fr.json +1 -0
- lightrag_webui/src/locales/zh.json +1 -0
lightrag/api/webui/assets/{index-CldiPPHk.js → index-B0dMyyzE.js}
RENAMED
Binary files a/lightrag/api/webui/assets/index-CldiPPHk.js and b/lightrag/api/webui/assets/index-B0dMyyzE.js differ
|
|
lightrag/api/webui/index.html
CHANGED
Binary files a/lightrag/api/webui/index.html and b/lightrag/api/webui/index.html differ
|
|
lightrag_webui/src/features/DocumentManager.tsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { useState, useEffect, useCallback, useRef } from 'react'
|
2 |
import { useTranslation } from 'react-i18next'
|
3 |
import { useSettingsStore } from '@/stores/settings'
|
4 |
import Button from '@/components/ui/Button'
|
@@ -16,15 +16,17 @@ import EmptyCard from '@/components/ui/EmptyCard'
|
|
16 |
import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog'
|
17 |
import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog'
|
18 |
|
19 |
-
import { getDocuments, scanNewDocuments, DocsStatusesResponse } from '@/api/lightrag'
|
20 |
import { errorMessage } from '@/lib/utils'
|
21 |
import { toast } from 'sonner'
|
22 |
import { useBackendState } from '@/stores/state'
|
23 |
|
24 |
-
import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon } from 'lucide-react'
|
25 |
-
import { DocStatusResponse } from '@/api/lightrag'
|
26 |
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
|
27 |
|
|
|
|
|
|
|
28 |
const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): string => {
|
29 |
// Check if file_path exists and is a non-empty string
|
30 |
if (!doc.file_path || typeof doc.file_path !== 'string' || doc.file_path.trim() === '') {
|
@@ -148,6 +150,10 @@ export default function DocumentManager() {
|
|
148 |
const [sortField, setSortField] = useState<SortField>('updated_at')
|
149 |
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
|
150 |
|
|
|
|
|
|
|
|
|
151 |
// Handle sort column click
|
152 |
const handleSort = (field: SortField) => {
|
153 |
if (sortField === field) {
|
@@ -190,6 +196,49 @@ export default function DocumentManager() {
|
|
190 |
});
|
191 |
}
|
192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
193 |
// Store previous status counts
|
194 |
const prevStatusCounts = useRef({
|
195 |
processed: 0,
|
@@ -398,6 +447,50 @@ export default function DocumentManager() {
|
|
398 |
<CardHeader className="flex-none py-2 px-4">
|
399 |
<div className="flex justify-between items-center">
|
400 |
<CardTitle>{t('documentPanel.documentManager.uploadedTitle')}</CardTitle>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
401 |
<div className="flex items-center gap-2">
|
402 |
<span className="text-sm text-gray-500">{t('documentPanel.documentManager.fileNameLabel')}</span>
|
403 |
<Button
|
@@ -477,11 +570,8 @@ export default function DocumentManager() {
|
|
477 |
</TableRow>
|
478 |
</TableHeader>
|
479 |
<TableBody className="text-sm overflow-auto">
|
480 |
-
{Object.entries(
|
481 |
-
|
482 |
-
const sortedDocuments = sortDocuments(documents);
|
483 |
-
|
484 |
-
return sortedDocuments.map(doc => (
|
485 |
<TableRow key={doc.id}>
|
486 |
<TableCell className="truncate font-mono overflow-visible max-w-[250px]">
|
487 |
{showFileName ? (
|
@@ -541,8 +631,8 @@ export default function DocumentManager() {
|
|
541 |
{new Date(doc.updated_at).toLocaleString()}
|
542 |
</TableCell>
|
543 |
</TableRow>
|
544 |
-
))
|
545 |
-
}
|
546 |
</TableBody>
|
547 |
</Table>
|
548 |
</div>
|
|
|
1 |
+
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
2 |
import { useTranslation } from 'react-i18next'
|
3 |
import { useSettingsStore } from '@/stores/settings'
|
4 |
import Button from '@/components/ui/Button'
|
|
|
16 |
import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog'
|
17 |
import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog'
|
18 |
|
19 |
+
import { getDocuments, scanNewDocuments, DocsStatusesResponse, DocStatus, DocStatusResponse } from '@/api/lightrag'
|
20 |
import { errorMessage } from '@/lib/utils'
|
21 |
import { toast } from 'sonner'
|
22 |
import { useBackendState } from '@/stores/state'
|
23 |
|
24 |
+
import { RefreshCwIcon, ActivityIcon, ArrowUpIcon, ArrowDownIcon, FilterIcon } from 'lucide-react'
|
|
|
25 |
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
|
26 |
|
27 |
+
type StatusFilter = DocStatus | 'all';
|
28 |
+
|
29 |
+
|
30 |
const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): string => {
|
31 |
// Check if file_path exists and is a non-empty string
|
32 |
if (!doc.file_path || typeof doc.file_path !== 'string' || doc.file_path.trim() === '') {
|
|
|
150 |
const [sortField, setSortField] = useState<SortField>('updated_at')
|
151 |
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
|
152 |
|
153 |
+
// State for document status filter
|
154 |
+
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
|
155 |
+
|
156 |
+
|
157 |
// Handle sort column click
|
158 |
const handleSort = (field: SortField) => {
|
159 |
if (sortField === field) {
|
|
|
196 |
});
|
197 |
}
|
198 |
|
199 |
+
const filteredAndSortedDocs = useMemo(() => {
|
200 |
+
if (!docs) return null;
|
201 |
+
|
202 |
+
let filteredDocs = { ...docs };
|
203 |
+
|
204 |
+
if (statusFilter !== 'all') {
|
205 |
+
filteredDocs = {
|
206 |
+
...docs,
|
207 |
+
statuses: {
|
208 |
+
pending: [],
|
209 |
+
processing: [],
|
210 |
+
processed: [],
|
211 |
+
failed: [],
|
212 |
+
[statusFilter]: docs.statuses[statusFilter] || []
|
213 |
+
}
|
214 |
+
};
|
215 |
+
}
|
216 |
+
|
217 |
+
if (!sortField || !sortDirection) return filteredDocs;
|
218 |
+
|
219 |
+
const sortedStatuses = Object.entries(filteredDocs.statuses).reduce((acc, [status, documents]) => {
|
220 |
+
const sortedDocuments = sortDocuments(documents);
|
221 |
+
acc[status as DocStatus] = sortedDocuments;
|
222 |
+
return acc;
|
223 |
+
}, {} as DocsStatusesResponse['statuses']);
|
224 |
+
|
225 |
+
return { ...filteredDocs, statuses: sortedStatuses };
|
226 |
+
}, [docs, sortField, sortDirection, statusFilter]);
|
227 |
+
|
228 |
+
// Calculate document counts for each status
|
229 |
+
const documentCounts = useMemo(() => {
|
230 |
+
if (!docs) return { all: 0 } as Record<string, number>;
|
231 |
+
|
232 |
+
const counts: Record<string, number> = { all: 0 };
|
233 |
+
|
234 |
+
Object.entries(docs.statuses).forEach(([status, documents]) => {
|
235 |
+
counts[status as DocStatus] = documents.length;
|
236 |
+
counts.all += documents.length;
|
237 |
+
});
|
238 |
+
|
239 |
+
return counts;
|
240 |
+
}, [docs]);
|
241 |
+
|
242 |
// Store previous status counts
|
243 |
const prevStatusCounts = useRef({
|
244 |
processed: 0,
|
|
|
447 |
<CardHeader className="flex-none py-2 px-4">
|
448 |
<div className="flex justify-between items-center">
|
449 |
<CardTitle>{t('documentPanel.documentManager.uploadedTitle')}</CardTitle>
|
450 |
+
<div className="flex items-center gap-2">
|
451 |
+
<FilterIcon className="h-4 w-4" />
|
452 |
+
<div className="flex gap-1">
|
453 |
+
<Button
|
454 |
+
size="sm"
|
455 |
+
variant={statusFilter === 'all' ? 'secondary' : 'outline'}
|
456 |
+
onClick={() => setStatusFilter('all')}
|
457 |
+
>
|
458 |
+
{t('documentPanel.documentManager.status.all')} ({documentCounts.all})
|
459 |
+
</Button>
|
460 |
+
<Button
|
461 |
+
size="sm"
|
462 |
+
variant={statusFilter === 'processed' ? 'secondary' : 'outline'}
|
463 |
+
onClick={() => setStatusFilter('processed')}
|
464 |
+
className={documentCounts.processed > 0 ? 'text-green-600' : 'text-gray-500'}
|
465 |
+
>
|
466 |
+
{t('documentPanel.documentManager.status.completed')} ({documentCounts.processed || 0})
|
467 |
+
</Button>
|
468 |
+
<Button
|
469 |
+
size="sm"
|
470 |
+
variant={statusFilter === 'processing' ? 'secondary' : 'outline'}
|
471 |
+
onClick={() => setStatusFilter('processing')}
|
472 |
+
className={documentCounts.processing > 0 ? 'text-blue-600' : 'text-gray-500'}
|
473 |
+
>
|
474 |
+
{t('documentPanel.documentManager.status.processing')} ({documentCounts.processing || 0})
|
475 |
+
</Button>
|
476 |
+
<Button
|
477 |
+
size="sm"
|
478 |
+
variant={statusFilter === 'pending' ? 'secondary' : 'outline'}
|
479 |
+
onClick={() => setStatusFilter('pending')}
|
480 |
+
className={documentCounts.pending > 0 ? 'text-yellow-600' : 'text-gray-500'}
|
481 |
+
>
|
482 |
+
{t('documentPanel.documentManager.status.pending')} ({documentCounts.pending || 0})
|
483 |
+
</Button>
|
484 |
+
<Button
|
485 |
+
size="sm"
|
486 |
+
variant={statusFilter === 'failed' ? 'secondary' : 'outline'}
|
487 |
+
onClick={() => setStatusFilter('failed')}
|
488 |
+
className={documentCounts.failed > 0 ? 'text-red-600' : 'text-gray-500'}
|
489 |
+
>
|
490 |
+
{t('documentPanel.documentManager.status.failed')} ({documentCounts.failed || 0})
|
491 |
+
</Button>
|
492 |
+
</div>
|
493 |
+
</div>
|
494 |
<div className="flex items-center gap-2">
|
495 |
<span className="text-sm text-gray-500">{t('documentPanel.documentManager.fileNameLabel')}</span>
|
496 |
<Button
|
|
|
570 |
</TableRow>
|
571 |
</TableHeader>
|
572 |
<TableBody className="text-sm overflow-auto">
|
573 |
+
{filteredAndSortedDocs?.statuses && Object.entries(filteredAndSortedDocs.statuses).flatMap(([status, documents]) =>
|
574 |
+
documents.map((doc) => (
|
|
|
|
|
|
|
575 |
<TableRow key={doc.id}>
|
576 |
<TableCell className="truncate font-mono overflow-visible max-w-[250px]">
|
577 |
{showFileName ? (
|
|
|
631 |
{new Date(doc.updated_at).toLocaleString()}
|
632 |
</TableCell>
|
633 |
</TableRow>
|
634 |
+
)))
|
635 |
+
}
|
636 |
</TableBody>
|
637 |
</Table>
|
638 |
</div>
|
lightrag_webui/src/locales/ar.json
CHANGED
@@ -104,6 +104,7 @@
|
|
104 |
"metadata": "البيانات الوصفية"
|
105 |
},
|
106 |
"status": {
|
|
|
107 |
"completed": "مكتمل",
|
108 |
"processing": "قيد المعالجة",
|
109 |
"pending": "معلق",
|
|
|
104 |
"metadata": "البيانات الوصفية"
|
105 |
},
|
106 |
"status": {
|
107 |
+
"all": "الكل",
|
108 |
"completed": "مكتمل",
|
109 |
"processing": "قيد المعالجة",
|
110 |
"pending": "معلق",
|
lightrag_webui/src/locales/en.json
CHANGED
@@ -104,6 +104,7 @@
|
|
104 |
"metadata": "Metadata"
|
105 |
},
|
106 |
"status": {
|
|
|
107 |
"completed": "Completed",
|
108 |
"processing": "Processing",
|
109 |
"pending": "Pending",
|
|
|
104 |
"metadata": "Metadata"
|
105 |
},
|
106 |
"status": {
|
107 |
+
"all": "All",
|
108 |
"completed": "Completed",
|
109 |
"processing": "Processing",
|
110 |
"pending": "Pending",
|
lightrag_webui/src/locales/fr.json
CHANGED
@@ -104,6 +104,7 @@
|
|
104 |
"metadata": "Métadonnées"
|
105 |
},
|
106 |
"status": {
|
|
|
107 |
"completed": "Terminé",
|
108 |
"processing": "En traitement",
|
109 |
"pending": "En attente",
|
|
|
104 |
"metadata": "Métadonnées"
|
105 |
},
|
106 |
"status": {
|
107 |
+
"all": "Tous",
|
108 |
"completed": "Terminé",
|
109 |
"processing": "En traitement",
|
110 |
"pending": "En attente",
|
lightrag_webui/src/locales/zh.json
CHANGED
@@ -104,6 +104,7 @@
|
|
104 |
"metadata": "元数据"
|
105 |
},
|
106 |
"status": {
|
|
|
107 |
"completed": "已完成",
|
108 |
"processing": "处理中",
|
109 |
"pending": "等待中",
|
|
|
104 |
"metadata": "元数据"
|
105 |
},
|
106 |
"status": {
|
107 |
+
"all": "全部",
|
108 |
"completed": "已完成",
|
109 |
"processing": "处理中",
|
110 |
"pending": "等待中",
|