Merge pull request #1202 from danielaskdd/sort-file
Browse files
lightrag/api/webui/assets/{index-CZkfsko8.js → index-BoPw3HVA.js}
RENAMED
Binary files a/lightrag/api/webui/assets/index-CZkfsko8.js and b/lightrag/api/webui/assets/index-BoPw3HVA.js differ
|
|
lightrag/api/webui/assets/index-Bwboeqcm.css
ADDED
Binary file (55 kB). View file
|
|
lightrag/api/webui/assets/index-CP4Boz-Y.css
DELETED
Binary file (55.4 kB)
|
|
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
@@ -21,7 +21,7 @@ import { errorMessage } from '@/lib/utils'
|
|
21 |
import { toast } from 'sonner'
|
22 |
import { useBackendState } from '@/stores/state'
|
23 |
|
24 |
-
import { RefreshCwIcon, ActivityIcon } from 'lucide-react'
|
25 |
import { DocStatusResponse } from '@/api/lightrag'
|
26 |
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
|
27 |
|
@@ -47,18 +47,41 @@ const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): str
|
|
47 |
};
|
48 |
|
49 |
const pulseStyle = `
|
50 |
-
/*
|
51 |
-
.tooltip-fixed {
|
52 |
-
position: fixed !important;
|
53 |
-
z-index: 9999 !important;
|
54 |
-
}
|
55 |
-
|
56 |
-
/* Parent container for tooltips */
|
57 |
.tooltip-container {
|
58 |
position: relative;
|
59 |
overflow: visible !important;
|
60 |
}
|
61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
@keyframes pulse {
|
63 |
0% {
|
64 |
background-color: rgb(255 0 0 / 0.1);
|
@@ -99,6 +122,10 @@ const pulseStyle = `
|
|
99 |
}
|
100 |
`;
|
101 |
|
|
|
|
|
|
|
|
|
102 |
export default function DocumentManager() {
|
103 |
const [showPipelineStatus, setShowPipelineStatus] = useState(false)
|
104 |
const { t } = useTranslation()
|
@@ -109,6 +136,52 @@ export default function DocumentManager() {
|
|
109 |
const showFileName = useSettingsStore.use.showFileName()
|
110 |
const setShowFileName = useSettingsStore.use.setShowFileName()
|
111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
// Store previous status counts
|
113 |
const prevStatusCounts = useRef({
|
114 |
processed: 0,
|
@@ -130,64 +203,47 @@ export default function DocumentManager() {
|
|
130 |
// Reference to the card content element
|
131 |
const cardContentRef = useRef<HTMLDivElement>(null);
|
132 |
|
133 |
-
// Add tooltip position adjustment
|
134 |
useEffect(() => {
|
135 |
if (!docs) return;
|
136 |
|
137 |
-
// Function to
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
const handleMouseMove = () => {
|
142 |
-
const now = Date.now();
|
143 |
-
if (now - lastExecution < throttleInterval) return;
|
144 |
-
lastExecution = now;
|
145 |
-
|
146 |
-
const cardContent = cardContentRef.current;
|
147 |
-
if (!cardContent) return;
|
148 |
-
|
149 |
-
// Get all visible tooltips
|
150 |
-
const visibleTooltips = document.querySelectorAll<HTMLElement>('.group:hover > div[class*="invisible group-hover:visible absolute"]');
|
151 |
-
if (visibleTooltips.length === 0) return;
|
152 |
-
|
153 |
-
visibleTooltips.forEach(tooltip => {
|
154 |
-
// Get the parent element that triggered the tooltip
|
155 |
-
const triggerElement = tooltip.parentElement;
|
156 |
-
if (!triggerElement) return;
|
157 |
|
158 |
-
|
|
|
|
|
159 |
|
160 |
-
//
|
161 |
-
tooltip.
|
162 |
|
163 |
-
//
|
164 |
-
const
|
165 |
-
const viewportHeight = window.innerHeight;
|
166 |
|
167 |
-
//
|
168 |
-
|
|
|
|
|
|
|
|
|
169 |
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
tooltip.style.top = `${triggerRect.bottom + 5}px`;
|
177 |
-
tooltip.style.bottom = 'auto';
|
178 |
-
}
|
179 |
|
180 |
-
|
181 |
-
|
182 |
-
tooltip.style.maxWidth = '600px';
|
183 |
-
});
|
184 |
};
|
185 |
|
186 |
-
|
187 |
-
document.addEventListener('mousemove', handleMouseMove);
|
188 |
|
189 |
return () => {
|
190 |
-
document.removeEventListener('
|
191 |
};
|
192 |
}, [docs]);
|
193 |
|
@@ -268,6 +324,11 @@ export default function DocumentManager() {
|
|
268 |
return () => clearInterval(interval)
|
269 |
}, [health, fetchDocuments, t, currentTab])
|
270 |
|
|
|
|
|
|
|
|
|
|
|
271 |
return (
|
272 |
<Card className="!rounded-none !overflow-hidden flex flex-col h-full min-h-0">
|
273 |
<CardHeader className="py-2 px-6">
|
@@ -340,22 +401,61 @@ export default function DocumentManager() {
|
|
340 |
)}
|
341 |
{docs && (
|
342 |
<div className="absolute inset-0 flex flex-col p-0">
|
343 |
-
<div className="w-full h-full flex flex-col
|
344 |
<Table className="w-full">
|
345 |
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
|
346 |
<TableRow className="border-b bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75 shadow-[inset_0_-1px_0_rgba(0,0,0,0.1)]">
|
347 |
-
<TableHead
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
348 |
<TableHead>{t('documentPanel.documentManager.columns.summary')}</TableHead>
|
349 |
<TableHead>{t('documentPanel.documentManager.columns.status')}</TableHead>
|
350 |
<TableHead>{t('documentPanel.documentManager.columns.length')}</TableHead>
|
351 |
<TableHead>{t('documentPanel.documentManager.columns.chunks')}</TableHead>
|
352 |
-
<TableHead
|
353 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
354 |
</TableRow>
|
355 |
</TableHeader>
|
356 |
<TableBody className="text-sm overflow-auto">
|
357 |
-
{Object.entries(docs.statuses).
|
358 |
-
|
|
|
|
|
|
|
359 |
<TableRow key={doc.id}>
|
360 |
<TableCell className="truncate font-mono overflow-visible max-w-[250px]">
|
361 |
{showFileName ? (
|
@@ -364,7 +464,7 @@ export default function DocumentManager() {
|
|
364 |
<div className="truncate">
|
365 |
{getDisplayFileName(doc, 30)}
|
366 |
</div>
|
367 |
-
<div className="invisible group-hover:visible
|
368 |
{doc.file_path}
|
369 |
</div>
|
370 |
</div>
|
@@ -375,7 +475,7 @@ export default function DocumentManager() {
|
|
375 |
<div className="truncate">
|
376 |
{doc.id}
|
377 |
</div>
|
378 |
-
<div className="invisible group-hover:visible
|
379 |
{doc.file_path}
|
380 |
</div>
|
381 |
</div>
|
@@ -386,7 +486,7 @@ export default function DocumentManager() {
|
|
386 |
<div className="truncate">
|
387 |
{doc.content_summary}
|
388 |
</div>
|
389 |
-
<div className="invisible group-hover:visible
|
390 |
{doc.content_summary}
|
391 |
</div>
|
392 |
</div>
|
@@ -415,8 +515,8 @@ export default function DocumentManager() {
|
|
415 |
{new Date(doc.updated_at).toLocaleString()}
|
416 |
</TableCell>
|
417 |
</TableRow>
|
418 |
-
))
|
419 |
-
)}
|
420 |
</TableBody>
|
421 |
</Table>
|
422 |
</div>
|
|
|
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 |
|
|
|
47 |
};
|
48 |
|
49 |
const pulseStyle = `
|
50 |
+
/* Tooltip styles */
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
.tooltip-container {
|
52 |
position: relative;
|
53 |
overflow: visible !important;
|
54 |
}
|
55 |
|
56 |
+
.tooltip {
|
57 |
+
position: fixed; /* Use fixed positioning to escape overflow constraints */
|
58 |
+
z-index: 9999; /* Ensure tooltip appears above all other elements */
|
59 |
+
max-width: 600px;
|
60 |
+
white-space: normal;
|
61 |
+
border-radius: 0.375rem;
|
62 |
+
padding: 0.5rem 0.75rem;
|
63 |
+
background-color: rgba(0, 0, 0, 0.95);
|
64 |
+
color: white;
|
65 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
66 |
+
pointer-events: none; /* Prevent tooltip from interfering with mouse events */
|
67 |
+
}
|
68 |
+
|
69 |
+
.dark .tooltip {
|
70 |
+
background-color: rgba(255, 255, 255, 0.95);
|
71 |
+
color: black;
|
72 |
+
}
|
73 |
+
|
74 |
+
/* Position tooltip helper class */
|
75 |
+
.tooltip-helper {
|
76 |
+
position: absolute;
|
77 |
+
visibility: hidden;
|
78 |
+
pointer-events: none;
|
79 |
+
top: 0;
|
80 |
+
left: 0;
|
81 |
+
width: 100%;
|
82 |
+
height: 0;
|
83 |
+
}
|
84 |
+
|
85 |
@keyframes pulse {
|
86 |
0% {
|
87 |
background-color: rgb(255 0 0 / 0.1);
|
|
|
122 |
}
|
123 |
`;
|
124 |
|
125 |
+
// Type definitions for sort field and direction
|
126 |
+
type SortField = 'created_at' | 'updated_at' | 'id';
|
127 |
+
type SortDirection = 'asc' | 'desc';
|
128 |
+
|
129 |
export default function DocumentManager() {
|
130 |
const [showPipelineStatus, setShowPipelineStatus] = useState(false)
|
131 |
const { t } = useTranslation()
|
|
|
136 |
const showFileName = useSettingsStore.use.showFileName()
|
137 |
const setShowFileName = useSettingsStore.use.setShowFileName()
|
138 |
|
139 |
+
// Sort state
|
140 |
+
const [sortField, setSortField] = useState<SortField>('updated_at')
|
141 |
+
const [sortDirection, setSortDirection] = useState<SortDirection>('desc')
|
142 |
+
|
143 |
+
// Handle sort column click
|
144 |
+
const handleSort = (field: SortField) => {
|
145 |
+
if (sortField === field) {
|
146 |
+
// Toggle sort direction if clicking the same field
|
147 |
+
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc')
|
148 |
+
} else {
|
149 |
+
// Set new sort field with default desc direction
|
150 |
+
setSortField(field)
|
151 |
+
setSortDirection('desc')
|
152 |
+
}
|
153 |
+
}
|
154 |
+
|
155 |
+
// Sort documents based on current sort field and direction
|
156 |
+
const sortDocuments = (documents: DocStatusResponse[]) => {
|
157 |
+
return [...documents].sort((a, b) => {
|
158 |
+
let valueA, valueB;
|
159 |
+
|
160 |
+
// Special handling for ID field based on showFileName setting
|
161 |
+
if (sortField === 'id' && showFileName) {
|
162 |
+
valueA = getDisplayFileName(a);
|
163 |
+
valueB = getDisplayFileName(b);
|
164 |
+
} else if (sortField === 'id') {
|
165 |
+
valueA = a.id;
|
166 |
+
valueB = b.id;
|
167 |
+
} else {
|
168 |
+
// Date fields
|
169 |
+
valueA = new Date(a[sortField]).getTime();
|
170 |
+
valueB = new Date(b[sortField]).getTime();
|
171 |
+
}
|
172 |
+
|
173 |
+
// Apply sort direction
|
174 |
+
const sortMultiplier = sortDirection === 'asc' ? 1 : -1;
|
175 |
+
|
176 |
+
// Compare values
|
177 |
+
if (typeof valueA === 'string' && typeof valueB === 'string') {
|
178 |
+
return sortMultiplier * valueA.localeCompare(valueB);
|
179 |
+
} else {
|
180 |
+
return sortMultiplier * (valueA > valueB ? 1 : valueA < valueB ? -1 : 0);
|
181 |
+
}
|
182 |
+
});
|
183 |
+
}
|
184 |
+
|
185 |
// Store previous status counts
|
186 |
const prevStatusCounts = useRef({
|
187 |
processed: 0,
|
|
|
203 |
// Reference to the card content element
|
204 |
const cardContentRef = useRef<HTMLDivElement>(null);
|
205 |
|
206 |
+
// Add tooltip position adjustment for fixed positioning
|
207 |
useEffect(() => {
|
208 |
if (!docs) return;
|
209 |
|
210 |
+
// Function to position tooltips
|
211 |
+
const positionTooltips = () => {
|
212 |
+
// Get all tooltip containers
|
213 |
+
const containers = document.querySelectorAll<HTMLElement>('.tooltip-container');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
214 |
|
215 |
+
containers.forEach(container => {
|
216 |
+
const tooltip = container.querySelector<HTMLElement>('.tooltip');
|
217 |
+
if (!tooltip) return;
|
218 |
|
219 |
+
// Only position visible tooltips
|
220 |
+
if (getComputedStyle(tooltip).visibility === 'hidden') return;
|
221 |
|
222 |
+
// Get container position
|
223 |
+
const rect = container.getBoundingClientRect();
|
|
|
224 |
|
225 |
+
// Position tooltip above the container
|
226 |
+
tooltip.style.left = `${rect.left}px`;
|
227 |
+
tooltip.style.top = `${rect.top - 5}px`;
|
228 |
+
tooltip.style.transform = 'translateY(-100%)';
|
229 |
+
});
|
230 |
+
};
|
231 |
|
232 |
+
// Set up event listeners
|
233 |
+
const handleMouseOver = (e: MouseEvent) => {
|
234 |
+
// Check if target or its parent is a tooltip container
|
235 |
+
const target = e.target as HTMLElement;
|
236 |
+
const container = target.closest('.tooltip-container');
|
237 |
+
if (!container) return;
|
|
|
|
|
|
|
238 |
|
239 |
+
// Small delay to ensure tooltip is visible before positioning
|
240 |
+
setTimeout(positionTooltips, 10);
|
|
|
|
|
241 |
};
|
242 |
|
243 |
+
document.addEventListener('mouseover', handleMouseOver);
|
|
|
244 |
|
245 |
return () => {
|
246 |
+
document.removeEventListener('mouseover', handleMouseOver);
|
247 |
};
|
248 |
}, [docs]);
|
249 |
|
|
|
324 |
return () => clearInterval(interval)
|
325 |
}, [health, fetchDocuments, t, currentTab])
|
326 |
|
327 |
+
// Add dependency on sort state to re-render when sort changes
|
328 |
+
useEffect(() => {
|
329 |
+
// This effect ensures the component re-renders when sort state changes
|
330 |
+
}, [sortField, sortDirection]);
|
331 |
+
|
332 |
return (
|
333 |
<Card className="!rounded-none !overflow-hidden flex flex-col h-full min-h-0">
|
334 |
<CardHeader className="py-2 px-6">
|
|
|
401 |
)}
|
402 |
{docs && (
|
403 |
<div className="absolute inset-0 flex flex-col p-0">
|
404 |
+
<div className="w-full h-full flex flex-col border border-gray-200 dark:border-gray-700 overflow-hidden">
|
405 |
<Table className="w-full">
|
406 |
<TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
|
407 |
<TableRow className="border-b bg-card/95 backdrop-blur supports-[backdrop-filter]:bg-card/75 shadow-[inset_0_-1px_0_rgba(0,0,0,0.1)]">
|
408 |
+
<TableHead
|
409 |
+
onClick={() => handleSort('id')}
|
410 |
+
className="cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-800 select-none"
|
411 |
+
>
|
412 |
+
<div className="flex items-center">
|
413 |
+
{t('documentPanel.documentManager.columns.id')}
|
414 |
+
{sortField === 'id' && (
|
415 |
+
<span className="ml-1">
|
416 |
+
{sortDirection === 'asc' ? <ArrowUpIcon size={14} /> : <ArrowDownIcon size={14} />}
|
417 |
+
</span>
|
418 |
+
)}
|
419 |
+
</div>
|
420 |
+
</TableHead>
|
421 |
<TableHead>{t('documentPanel.documentManager.columns.summary')}</TableHead>
|
422 |
<TableHead>{t('documentPanel.documentManager.columns.status')}</TableHead>
|
423 |
<TableHead>{t('documentPanel.documentManager.columns.length')}</TableHead>
|
424 |
<TableHead>{t('documentPanel.documentManager.columns.chunks')}</TableHead>
|
425 |
+
<TableHead
|
426 |
+
onClick={() => handleSort('created_at')}
|
427 |
+
className="cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-800 select-none"
|
428 |
+
>
|
429 |
+
<div className="flex items-center">
|
430 |
+
{t('documentPanel.documentManager.columns.created')}
|
431 |
+
{sortField === 'created_at' && (
|
432 |
+
<span className="ml-1">
|
433 |
+
{sortDirection === 'asc' ? <ArrowUpIcon size={14} /> : <ArrowDownIcon size={14} />}
|
434 |
+
</span>
|
435 |
+
)}
|
436 |
+
</div>
|
437 |
+
</TableHead>
|
438 |
+
<TableHead
|
439 |
+
onClick={() => handleSort('updated_at')}
|
440 |
+
className="cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-800 select-none"
|
441 |
+
>
|
442 |
+
<div className="flex items-center">
|
443 |
+
{t('documentPanel.documentManager.columns.updated')}
|
444 |
+
{sortField === 'updated_at' && (
|
445 |
+
<span className="ml-1">
|
446 |
+
{sortDirection === 'asc' ? <ArrowUpIcon size={14} /> : <ArrowDownIcon size={14} />}
|
447 |
+
</span>
|
448 |
+
)}
|
449 |
+
</div>
|
450 |
+
</TableHead>
|
451 |
</TableRow>
|
452 |
</TableHeader>
|
453 |
<TableBody className="text-sm overflow-auto">
|
454 |
+
{Object.entries(docs.statuses).flatMap(([status, documents]) => {
|
455 |
+
// Apply sorting to documents
|
456 |
+
const sortedDocuments = sortDocuments(documents);
|
457 |
+
|
458 |
+
return sortedDocuments.map(doc => (
|
459 |
<TableRow key={doc.id}>
|
460 |
<TableCell className="truncate font-mono overflow-visible max-w-[250px]">
|
461 |
{showFileName ? (
|
|
|
464 |
<div className="truncate">
|
465 |
{getDisplayFileName(doc, 30)}
|
466 |
</div>
|
467 |
+
<div className="invisible group-hover:visible tooltip">
|
468 |
{doc.file_path}
|
469 |
</div>
|
470 |
</div>
|
|
|
475 |
<div className="truncate">
|
476 |
{doc.id}
|
477 |
</div>
|
478 |
+
<div className="invisible group-hover:visible tooltip">
|
479 |
{doc.file_path}
|
480 |
</div>
|
481 |
</div>
|
|
|
486 |
<div className="truncate">
|
487 |
{doc.content_summary}
|
488 |
</div>
|
489 |
+
<div className="invisible group-hover:visible tooltip">
|
490 |
{doc.content_summary}
|
491 |
</div>
|
492 |
</div>
|
|
|
515 |
{new Date(doc.updated_at).toLocaleString()}
|
516 |
</TableCell>
|
517 |
</TableRow>
|
518 |
+
));
|
519 |
+
})}
|
520 |
</TableBody>
|
521 |
</Table>
|
522 |
</div>
|