gzdaniel commited on
Commit
31e8dae
·
1 Parent(s): d07e391

Feat: Add document deletion for WebUI

Browse files
lightrag_webui/src/api/lightrag.ts CHANGED
@@ -114,6 +114,12 @@ export type DocActionResponse = {
114
  message: string
115
  }
116
 
 
 
 
 
 
 
117
  export type DocStatus = 'pending' | 'processing' | 'processed' | 'failed'
118
 
119
  export type DocStatusResponse = {
@@ -515,6 +521,13 @@ export const clearCache = async (modes?: string[]): Promise<{
515
  return response.data
516
  }
517
 
 
 
 
 
 
 
 
518
  export const getAuthStatus = async (): Promise<AuthStatusResponse> => {
519
  try {
520
  // Add a timeout to the request to prevent hanging
 
114
  message: string
115
  }
116
 
117
+ export type DeleteDocResponse = {
118
+ status: 'deletion_started' | 'busy' | 'not_allowed'
119
+ message: string
120
+ doc_id: string
121
+ }
122
+
123
  export type DocStatus = 'pending' | 'processing' | 'processed' | 'failed'
124
 
125
  export type DocStatusResponse = {
 
521
  return response.data
522
  }
523
 
524
+ export const deleteDocuments = async (docIds: string[]): Promise<DeleteDocResponse> => {
525
+ const response = await axiosInstance.delete('/documents/delete_document', {
526
+ data: { doc_ids: docIds }
527
+ })
528
+ return response.data
529
+ }
530
+
531
  export const getAuthStatus = async (): Promise<AuthStatusResponse> => {
532
  try {
533
  // Add a timeout to the request to prevent hanging
lightrag_webui/src/components/documents/DeleteDocumentsDialog.tsx ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback, useEffect } from 'react'
2
+ import Button from '@/components/ui/Button'
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ DialogTrigger,
10
+ DialogFooter
11
+ } from '@/components/ui/Dialog'
12
+ import Input from '@/components/ui/Input'
13
+ import { toast } from 'sonner'
14
+ import { errorMessage } from '@/lib/utils'
15
+ import { deleteDocuments } from '@/api/lightrag'
16
+
17
+ import { TrashIcon, AlertTriangleIcon } from 'lucide-react'
18
+ import { useTranslation } from 'react-i18next'
19
+
20
+ // Simple Label component
21
+ const Label = ({
22
+ htmlFor,
23
+ className,
24
+ children,
25
+ ...props
26
+ }: React.LabelHTMLAttributes<HTMLLabelElement>) => (
27
+ <label
28
+ htmlFor={htmlFor}
29
+ className={className}
30
+ {...props}
31
+ >
32
+ {children}
33
+ </label>
34
+ )
35
+
36
+ interface DeleteDocumentsDialogProps {
37
+ selectedDocIds: string[]
38
+ totalCompletedCount: number
39
+ onDocumentsDeleted?: () => Promise<void>
40
+ }
41
+
42
+ export default function DeleteDocumentsDialog({ selectedDocIds, totalCompletedCount, onDocumentsDeleted }: DeleteDocumentsDialogProps) {
43
+ const { t } = useTranslation()
44
+ const [open, setOpen] = useState(false)
45
+ const [confirmText, setConfirmText] = useState('')
46
+ const [isDeleting, setIsDeleting] = useState(false)
47
+ const isConfirmEnabled = confirmText.toLowerCase() === 'yes' && !isDeleting
48
+
49
+ // Reset state when dialog closes
50
+ useEffect(() => {
51
+ if (!open) {
52
+ setConfirmText('')
53
+ setIsDeleting(false)
54
+ }
55
+ }, [open])
56
+
57
+ const handleDelete = useCallback(async () => {
58
+ if (!isConfirmEnabled || selectedDocIds.length === 0) return
59
+
60
+ // Check if user is trying to delete all completed documents
61
+ if (selectedDocIds.length === totalCompletedCount && totalCompletedCount > 0) {
62
+ toast.error(t('documentPanel.deleteDocuments.cannotDeleteAll'))
63
+ return
64
+ }
65
+
66
+ setIsDeleting(true)
67
+ try {
68
+ const result = await deleteDocuments(selectedDocIds)
69
+
70
+ if (result.status === 'deletion_started') {
71
+ toast.success(t('documentPanel.deleteDocuments.success', { count: selectedDocIds.length }))
72
+ } else if (result.status === 'busy') {
73
+ toast.error(t('documentPanel.deleteDocuments.busy'))
74
+ setConfirmText('')
75
+ setIsDeleting(false)
76
+ return
77
+ } else if (result.status === 'not_allowed') {
78
+ toast.error(t('documentPanel.deleteDocuments.notAllowed'))
79
+ setConfirmText('')
80
+ setIsDeleting(false)
81
+ return
82
+ } else {
83
+ toast.error(t('documentPanel.deleteDocuments.failed', { message: result.message }))
84
+ setConfirmText('')
85
+ setIsDeleting(false)
86
+ return
87
+ }
88
+
89
+ // Refresh document list if provided
90
+ if (onDocumentsDeleted) {
91
+ onDocumentsDeleted().catch(console.error)
92
+ }
93
+
94
+ // Close dialog after successful operation
95
+ setOpen(false)
96
+ } catch (err) {
97
+ toast.error(t('documentPanel.deleteDocuments.error', { error: errorMessage(err) }))
98
+ setConfirmText('')
99
+ } finally {
100
+ setIsDeleting(false)
101
+ }
102
+ }, [isConfirmEnabled, selectedDocIds, totalCompletedCount, setOpen, t, onDocumentsDeleted])
103
+
104
+ return (
105
+ <Dialog open={open} onOpenChange={setOpen}>
106
+ <DialogTrigger asChild>
107
+ <Button
108
+ variant="destructive"
109
+ side="bottom"
110
+ tooltip={t('documentPanel.deleteDocuments.tooltip', { count: selectedDocIds.length })}
111
+ size="sm"
112
+ >
113
+ <TrashIcon/> {t('documentPanel.deleteDocuments.button')}
114
+ </Button>
115
+ </DialogTrigger>
116
+ <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
117
+ <DialogHeader>
118
+ <DialogTitle className="flex items-center gap-2 text-red-500 dark:text-red-400 font-bold">
119
+ <AlertTriangleIcon className="h-5 w-5" />
120
+ {t('documentPanel.deleteDocuments.title')}
121
+ </DialogTitle>
122
+ <DialogDescription className="pt-2">
123
+ {t('documentPanel.deleteDocuments.description', { count: selectedDocIds.length })}
124
+ </DialogDescription>
125
+ </DialogHeader>
126
+
127
+ <div className="text-red-500 dark:text-red-400 font-semibold mb-4">
128
+ {t('documentPanel.deleteDocuments.warning')}
129
+ </div>
130
+
131
+ <div className="mb-4">
132
+ {t('documentPanel.deleteDocuments.confirm', { count: selectedDocIds.length })}
133
+ </div>
134
+
135
+ <div className="space-y-4">
136
+ <div className="space-y-2">
137
+ <Label htmlFor="confirm-text" className="text-sm font-medium">
138
+ {t('documentPanel.deleteDocuments.confirmPrompt')}
139
+ </Label>
140
+ <Input
141
+ id="confirm-text"
142
+ value={confirmText}
143
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setConfirmText(e.target.value)}
144
+ placeholder={t('documentPanel.deleteDocuments.confirmPlaceholder')}
145
+ className="w-full"
146
+ disabled={isDeleting}
147
+ />
148
+ </div>
149
+ </div>
150
+
151
+ <DialogFooter>
152
+ <Button variant="outline" onClick={() => setOpen(false)} disabled={isDeleting}>
153
+ {t('common.cancel')}
154
+ </Button>
155
+ <Button
156
+ variant="destructive"
157
+ onClick={handleDelete}
158
+ disabled={!isConfirmEnabled}
159
+ >
160
+ {isDeleting ? t('documentPanel.deleteDocuments.deleting') : t('documentPanel.deleteDocuments.confirmButton')}
161
+ </Button>
162
+ </DialogFooter>
163
+ </DialogContent>
164
+ </Dialog>
165
+ )
166
+ }
lightrag_webui/src/components/documents/DeselectDocumentsDialog.tsx ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback, useEffect } from 'react'
2
+ import Button from '@/components/ui/Button'
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ DialogTrigger,
10
+ DialogFooter
11
+ } from '@/components/ui/Dialog'
12
+
13
+ import { XIcon, AlertCircleIcon } from 'lucide-react'
14
+ import { useTranslation } from 'react-i18next'
15
+
16
+ interface DeselectDocumentsDialogProps {
17
+ selectedCount: number
18
+ onDeselect: () => void
19
+ }
20
+
21
+ export default function DeselectDocumentsDialog({ selectedCount, onDeselect }: DeselectDocumentsDialogProps) {
22
+ const { t } = useTranslation()
23
+ const [open, setOpen] = useState(false)
24
+
25
+ // Reset state when dialog closes
26
+ useEffect(() => {
27
+ if (!open) {
28
+ // No state to reset for this simple dialog
29
+ }
30
+ }, [open])
31
+
32
+ const handleDeselect = useCallback(() => {
33
+ onDeselect()
34
+ setOpen(false)
35
+ }, [onDeselect, setOpen])
36
+
37
+ return (
38
+ <Dialog open={open} onOpenChange={setOpen}>
39
+ <DialogTrigger asChild>
40
+ <Button
41
+ variant="outline"
42
+ side="bottom"
43
+ tooltip={t('documentPanel.deselectDocuments.tooltip')}
44
+ size="sm"
45
+ >
46
+ <XIcon/> {t('documentPanel.deselectDocuments.button')}
47
+ </Button>
48
+ </DialogTrigger>
49
+ <DialogContent className="sm:max-w-md" onCloseAutoFocus={(e) => e.preventDefault()}>
50
+ <DialogHeader>
51
+ <DialogTitle className="flex items-center gap-2">
52
+ <AlertCircleIcon className="h-5 w-5" />
53
+ {t('documentPanel.deselectDocuments.title')}
54
+ </DialogTitle>
55
+ <DialogDescription className="pt-2">
56
+ {t('documentPanel.deselectDocuments.description', { count: selectedCount })}
57
+ </DialogDescription>
58
+ </DialogHeader>
59
+
60
+ <DialogFooter>
61
+ <Button variant="outline" onClick={() => setOpen(false)}>
62
+ {t('common.cancel')}
63
+ </Button>
64
+ <Button
65
+ variant="default"
66
+ onClick={handleDeselect}
67
+ >
68
+ {t('documentPanel.deselectDocuments.confirmButton')}
69
+ </Button>
70
+ </DialogFooter>
71
+ </DialogContent>
72
+ </Dialog>
73
+ )
74
+ }
lightrag_webui/src/features/DocumentManager.tsx CHANGED
@@ -13,8 +13,11 @@ import {
13
  } from '@/components/ui/Table'
14
  import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card'
15
  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, DocStatus, DocStatusResponse } from '@/api/lightrag'
20
  import { errorMessage } from '@/lib/utils'
@@ -173,6 +176,25 @@ export default function DocumentManager() {
173
  // State for document status filter
174
  const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
  // Handle sort column click
178
  const handleSort = (field: SortField) => {
@@ -463,6 +485,12 @@ export default function DocumentManager() {
463
  prevStatusCounts.current = newStatusCounts
464
  }, [docs]);
465
 
 
 
 
 
 
 
466
  // Add dependency on sort state to re-render when sort changes
467
  useEffect(() => {
468
  // This effect ensures the component re-renders when sort state changes
@@ -499,7 +527,21 @@ export default function DocumentManager() {
499
  </Button>
500
  </div>
501
  <div className="flex-1" />
502
- <ClearDocumentsDialog onDocumentsCleared={fetchDocuments} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  <UploadDocumentsDialog onDocumentsUploaded={fetchDocuments} />
504
  <PipelineStatusDialog
505
  open={showPipelineStatus}
@@ -652,6 +694,9 @@ export default function DocumentManager() {
652
  )}
653
  </div>
654
  </TableHead>
 
 
 
655
  </TableRow>
656
  </TableHeader>
657
  <TableBody className="text-sm overflow-auto">
@@ -718,6 +763,14 @@ export default function DocumentManager() {
718
  <TableCell className="truncate">
719
  {new Date(doc.updated_at).toLocaleString()}
720
  </TableCell>
 
 
 
 
 
 
 
 
721
  </TableRow>
722
  ))}
723
  </TableBody>
 
13
  } from '@/components/ui/Table'
14
  import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card'
15
  import EmptyCard from '@/components/ui/EmptyCard'
16
+ import Checkbox from '@/components/ui/Checkbox'
17
  import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog'
18
  import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog'
19
+ import DeleteDocumentsDialog from '@/components/documents/DeleteDocumentsDialog'
20
+ import DeselectDocumentsDialog from '@/components/documents/DeselectDocumentsDialog'
21
 
22
  import { getDocuments, scanNewDocuments, DocsStatusesResponse, DocStatus, DocStatusResponse } from '@/api/lightrag'
23
  import { errorMessage } from '@/lib/utils'
 
176
  // State for document status filter
177
  const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
178
 
179
+ // State for document selection
180
+ const [selectedDocIds, setSelectedDocIds] = useState<string[]>([])
181
+ const isSelectionMode = selectedDocIds.length > 0
182
+
183
+ // Handle checkbox change for individual documents
184
+ const handleDocumentSelect = useCallback((docId: string, checked: boolean) => {
185
+ setSelectedDocIds(prev => {
186
+ if (checked) {
187
+ return [...prev, docId]
188
+ } else {
189
+ return prev.filter(id => id !== docId)
190
+ }
191
+ })
192
+ }, [])
193
+
194
+ // Handle deselect all documents
195
+ const handleDeselectAll = useCallback(() => {
196
+ setSelectedDocIds([])
197
+ }, [])
198
 
199
  // Handle sort column click
200
  const handleSort = (field: SortField) => {
 
485
  prevStatusCounts.current = newStatusCounts
486
  }, [docs]);
487
 
488
+ // Handle documents deleted callback
489
+ const handleDocumentsDeleted = useCallback(async () => {
490
+ setSelectedDocIds([])
491
+ await fetchDocuments()
492
+ }, [fetchDocuments])
493
+
494
  // Add dependency on sort state to re-render when sort changes
495
  useEffect(() => {
496
  // This effect ensures the component re-renders when sort state changes
 
527
  </Button>
528
  </div>
529
  <div className="flex-1" />
530
+ {isSelectionMode && (
531
+ <DeleteDocumentsDialog
532
+ selectedDocIds={selectedDocIds}
533
+ totalCompletedCount={documentCounts.processed || 0}
534
+ onDocumentsDeleted={handleDocumentsDeleted}
535
+ />
536
+ )}
537
+ {isSelectionMode ? (
538
+ <DeselectDocumentsDialog
539
+ selectedCount={selectedDocIds.length}
540
+ onDeselect={handleDeselectAll}
541
+ />
542
+ ) : (
543
+ <ClearDocumentsDialog onDocumentsCleared={fetchDocuments} />
544
+ )}
545
  <UploadDocumentsDialog onDocumentsUploaded={fetchDocuments} />
546
  <PipelineStatusDialog
547
  open={showPipelineStatus}
 
694
  )}
695
  </div>
696
  </TableHead>
697
+ <TableHead className="w-16 text-center">
698
+ {t('documentPanel.documentManager.columns.select')}
699
+ </TableHead>
700
  </TableRow>
701
  </TableHeader>
702
  <TableBody className="text-sm overflow-auto">
 
763
  <TableCell className="truncate">
764
  {new Date(doc.updated_at).toLocaleString()}
765
  </TableCell>
766
+ <TableCell className="text-center">
767
+ <Checkbox
768
+ checked={selectedDocIds.includes(doc.id)}
769
+ onCheckedChange={(checked) => handleDocumentSelect(doc.id, checked === true)}
770
+ disabled={doc.status !== 'processed'}
771
+ className="mx-auto"
772
+ />
773
+ </TableCell>
774
  </TableRow>
775
  ))}
776
  </TableBody>
lightrag_webui/src/locales/en.json CHANGED
@@ -56,6 +56,30 @@
56
  "failed": "Clear Documents Failed:\n{{message}}",
57
  "error": "Clear Documents Failed:\n{{error}}"
58
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  "uploadDocuments": {
60
  "button": "Upload",
61
  "tooltip": "Upload documents",
@@ -105,7 +129,8 @@
105
  "chunks": "Chunks",
106
  "created": "Created",
107
  "updated": "Updated",
108
- "metadata": "Metadata"
 
109
  },
110
  "status": {
111
  "all": "All",
 
56
  "failed": "Clear Documents Failed:\n{{message}}",
57
  "error": "Clear Documents Failed:\n{{error}}"
58
  },
59
+ "deleteDocuments": {
60
+ "button": "Delete",
61
+ "tooltip": "Delete selected documents",
62
+ "title": "Delete Documents",
63
+ "description": "This will permanently delete the selected documents from the system",
64
+ "warning": "WARNING: This action will permanently delete the selected documents and cannot be undone!",
65
+ "confirm": "Do you really want to delete {{count}} selected document(s)?",
66
+ "confirmPrompt": "Type 'yes' to confirm this action",
67
+ "confirmPlaceholder": "Type yes to confirm",
68
+ "confirmButton": "YES",
69
+ "success": "Documents deleted successfully",
70
+ "failed": "Delete Documents Failed:\n{{message}}",
71
+ "error": "Delete Documents Failed:\n{{error}}",
72
+ "busy": "Pipeline is busy, please try again later",
73
+ "notAllowed": "No permission to perform this operation",
74
+ "cannotDeleteAll": "Cannot delete all documents. If you need to delete all documents, please use the Clear Documents feature."
75
+ },
76
+ "deselectDocuments": {
77
+ "button": "Deselect",
78
+ "tooltip": "Deselect all selected documents",
79
+ "title": "Deselect Documents",
80
+ "description": "This will clear all selected documents ({{count}} selected)",
81
+ "confirmButton": "Deselect All"
82
+ },
83
  "uploadDocuments": {
84
  "button": "Upload",
85
  "tooltip": "Upload documents",
 
129
  "chunks": "Chunks",
130
  "created": "Created",
131
  "updated": "Updated",
132
+ "metadata": "Metadata",
133
+ "select": "Select"
134
  },
135
  "status": {
136
  "all": "All",
lightrag_webui/src/locales/zh.json CHANGED
@@ -56,6 +56,30 @@
56
  "failed": "清空文档失败:\n{{message}}",
57
  "error": "清空文档失败:\n{{error}}"
58
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  "uploadDocuments": {
60
  "button": "上传",
61
  "tooltip": "上传文档",
@@ -105,7 +129,8 @@
105
  "chunks": "分块",
106
  "created": "创建时间",
107
  "updated": "更新时间",
108
- "metadata": "元数据"
 
109
  },
110
  "status": {
111
  "all": "全部",
 
56
  "failed": "清空文档失败:\n{{message}}",
57
  "error": "清空文档失败:\n{{error}}"
58
  },
59
+ "deleteDocuments": {
60
+ "button": "删除",
61
+ "tooltip": "删除选中的文档",
62
+ "title": "删除文档",
63
+ "description": "此操作将永久删除选中的文档",
64
+ "warning": "警告:此操作将永久删除选中的文档,无法恢复!",
65
+ "confirm": "确定要删除 {{count}} 个选中的文档吗?",
66
+ "confirmPrompt": "请输入 yes 确认操作",
67
+ "confirmPlaceholder": "输入 yes 确认",
68
+ "confirmButton": "确定",
69
+ "success": "文档删除成功",
70
+ "failed": "删除文档失败:\n{{message}}",
71
+ "error": "删除文档失败:\n{{error}}",
72
+ "busy": "流水线被占用,请稍后再试",
73
+ "notAllowed": "没有操作权限",
74
+ "cannotDeleteAll": "无法删除所有文档。如确实需要删除所有文档请使用清空文档功能。"
75
+ },
76
+ "deselectDocuments": {
77
+ "button": "取消选择",
78
+ "tooltip": "取消选择所有文档",
79
+ "title": "取消选择文档",
80
+ "description": "此操作将清除所有选中的文档(已选择 {{count}} 个)",
81
+ "confirmButton": "取消全部选择"
82
+ },
83
  "uploadDocuments": {
84
  "button": "上传",
85
  "tooltip": "上传文档",
 
129
  "chunks": "分块",
130
  "created": "创建时间",
131
  "updated": "更新时间",
132
+ "metadata": "元数据",
133
+ "select": "选择"
134
  },
135
  "status": {
136
  "all": "全部",