diff --git a/lightrag_webui/src/api/lightrag.ts b/lightrag_webui/src/api/lightrag.ts index 55046e15..675ed4e4 100644 --- a/lightrag_webui/src/api/lightrag.ts +++ b/lightrag_webui/src/api/lightrag.ts @@ -114,6 +114,12 @@ export type DocActionResponse = { message: string } +export type DeleteDocResponse = { + status: 'deletion_started' | 'busy' | 'not_allowed' + message: string + doc_id: string +} + export type DocStatus = 'pending' | 'processing' | 'processed' | 'failed' export type DocStatusResponse = { @@ -515,6 +521,13 @@ export const clearCache = async (modes?: string[]): Promise<{ return response.data } +export const deleteDocuments = async (docIds: string[]): Promise => { + const response = await axiosInstance.delete('/documents/delete_document', { + data: { doc_ids: docIds } + }) + return response.data +} + export const getAuthStatus = async (): Promise => { try { // Add a timeout to the request to prevent hanging diff --git a/lightrag_webui/src/components/documents/DeleteDocumentsDialog.tsx b/lightrag_webui/src/components/documents/DeleteDocumentsDialog.tsx new file mode 100644 index 00000000..0d4924ca --- /dev/null +++ b/lightrag_webui/src/components/documents/DeleteDocumentsDialog.tsx @@ -0,0 +1,166 @@ +import { useState, useCallback, useEffect } from 'react' +import Button from '@/components/ui/Button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogFooter +} from '@/components/ui/Dialog' +import Input from '@/components/ui/Input' +import { toast } from 'sonner' +import { errorMessage } from '@/lib/utils' +import { deleteDocuments } from '@/api/lightrag' + +import { TrashIcon, AlertTriangleIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' + +// Simple Label component +const Label = ({ + htmlFor, + className, + children, + ...props +}: React.LabelHTMLAttributes) => ( + +) + +interface DeleteDocumentsDialogProps { + selectedDocIds: string[] + totalCompletedCount: number + onDocumentsDeleted?: () => Promise +} + +export default function DeleteDocumentsDialog({ selectedDocIds, totalCompletedCount, onDocumentsDeleted }: DeleteDocumentsDialogProps) { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + const [confirmText, setConfirmText] = useState('') + const [isDeleting, setIsDeleting] = useState(false) + const isConfirmEnabled = confirmText.toLowerCase() === 'yes' && !isDeleting + + // Reset state when dialog closes + useEffect(() => { + if (!open) { + setConfirmText('') + setIsDeleting(false) + } + }, [open]) + + const handleDelete = useCallback(async () => { + if (!isConfirmEnabled || selectedDocIds.length === 0) return + + // Check if user is trying to delete all completed documents + if (selectedDocIds.length === totalCompletedCount && totalCompletedCount > 0) { + toast.error(t('documentPanel.deleteDocuments.cannotDeleteAll')) + return + } + + setIsDeleting(true) + try { + const result = await deleteDocuments(selectedDocIds) + + if (result.status === 'deletion_started') { + toast.success(t('documentPanel.deleteDocuments.success', { count: selectedDocIds.length })) + } else if (result.status === 'busy') { + toast.error(t('documentPanel.deleteDocuments.busy')) + setConfirmText('') + setIsDeleting(false) + return + } else if (result.status === 'not_allowed') { + toast.error(t('documentPanel.deleteDocuments.notAllowed')) + setConfirmText('') + setIsDeleting(false) + return + } else { + toast.error(t('documentPanel.deleteDocuments.failed', { message: result.message })) + setConfirmText('') + setIsDeleting(false) + return + } + + // Refresh document list if provided + if (onDocumentsDeleted) { + onDocumentsDeleted().catch(console.error) + } + + // Close dialog after successful operation + setOpen(false) + } catch (err) { + toast.error(t('documentPanel.deleteDocuments.error', { error: errorMessage(err) })) + setConfirmText('') + } finally { + setIsDeleting(false) + } + }, [isConfirmEnabled, selectedDocIds, totalCompletedCount, setOpen, t, onDocumentsDeleted]) + + return ( + + + + + e.preventDefault()}> + + + + {t('documentPanel.deleteDocuments.title')} + + + {t('documentPanel.deleteDocuments.description', { count: selectedDocIds.length })} + + + +
+ {t('documentPanel.deleteDocuments.warning')} +
+ +
+ {t('documentPanel.deleteDocuments.confirm', { count: selectedDocIds.length })} +
+ +
+
+ + ) => setConfirmText(e.target.value)} + placeholder={t('documentPanel.deleteDocuments.confirmPlaceholder')} + className="w-full" + disabled={isDeleting} + /> +
+
+ + + + + +
+
+ ) +} diff --git a/lightrag_webui/src/components/documents/DeselectDocumentsDialog.tsx b/lightrag_webui/src/components/documents/DeselectDocumentsDialog.tsx new file mode 100644 index 00000000..10e112a6 --- /dev/null +++ b/lightrag_webui/src/components/documents/DeselectDocumentsDialog.tsx @@ -0,0 +1,74 @@ +import { useState, useCallback, useEffect } from 'react' +import Button from '@/components/ui/Button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogFooter +} from '@/components/ui/Dialog' + +import { XIcon, AlertCircleIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' + +interface DeselectDocumentsDialogProps { + selectedCount: number + onDeselect: () => void +} + +export default function DeselectDocumentsDialog({ selectedCount, onDeselect }: DeselectDocumentsDialogProps) { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + + // Reset state when dialog closes + useEffect(() => { + if (!open) { + // No state to reset for this simple dialog + } + }, [open]) + + const handleDeselect = useCallback(() => { + onDeselect() + setOpen(false) + }, [onDeselect, setOpen]) + + return ( + + + + + e.preventDefault()}> + + + + {t('documentPanel.deselectDocuments.title')} + + + {t('documentPanel.deselectDocuments.description', { count: selectedCount })} + + + + + + + + + + ) +} diff --git a/lightrag_webui/src/features/DocumentManager.tsx b/lightrag_webui/src/features/DocumentManager.tsx index f1b25472..8128fc07 100644 --- a/lightrag_webui/src/features/DocumentManager.tsx +++ b/lightrag_webui/src/features/DocumentManager.tsx @@ -13,8 +13,11 @@ import { } from '@/components/ui/Table' import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card' import EmptyCard from '@/components/ui/EmptyCard' +import Checkbox from '@/components/ui/Checkbox' import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog' import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog' +import DeleteDocumentsDialog from '@/components/documents/DeleteDocumentsDialog' +import DeselectDocumentsDialog from '@/components/documents/DeselectDocumentsDialog' import { getDocuments, scanNewDocuments, DocsStatusesResponse, DocStatus, DocStatusResponse } from '@/api/lightrag' import { errorMessage } from '@/lib/utils' @@ -173,6 +176,25 @@ export default function DocumentManager() { // State for document status filter const [statusFilter, setStatusFilter] = useState('all'); + // State for document selection + const [selectedDocIds, setSelectedDocIds] = useState([]) + const isSelectionMode = selectedDocIds.length > 0 + + // Handle checkbox change for individual documents + const handleDocumentSelect = useCallback((docId: string, checked: boolean) => { + setSelectedDocIds(prev => { + if (checked) { + return [...prev, docId] + } else { + return prev.filter(id => id !== docId) + } + }) + }, []) + + // Handle deselect all documents + const handleDeselectAll = useCallback(() => { + setSelectedDocIds([]) + }, []) // Handle sort column click const handleSort = (field: SortField) => { @@ -463,6 +485,12 @@ export default function DocumentManager() { prevStatusCounts.current = newStatusCounts }, [docs]); + // Handle documents deleted callback + const handleDocumentsDeleted = useCallback(async () => { + setSelectedDocIds([]) + await fetchDocuments() + }, [fetchDocuments]) + // Add dependency on sort state to re-render when sort changes useEffect(() => { // This effect ensures the component re-renders when sort state changes @@ -499,7 +527,21 @@ export default function DocumentManager() {
- + {isSelectionMode && ( + + )} + {isSelectionMode ? ( + + ) : ( + + )} + + {t('documentPanel.documentManager.columns.select')} + @@ -718,6 +763,14 @@ export default function DocumentManager() { {new Date(doc.updated_at).toLocaleString()} + + handleDocumentSelect(doc.id, checked === true)} + disabled={doc.status !== 'processed'} + className="mx-auto" + /> + ))} diff --git a/lightrag_webui/src/locales/en.json b/lightrag_webui/src/locales/en.json index 643b6f2e..ca63a6e8 100644 --- a/lightrag_webui/src/locales/en.json +++ b/lightrag_webui/src/locales/en.json @@ -56,6 +56,30 @@ "failed": "Clear Documents Failed:\n{{message}}", "error": "Clear Documents Failed:\n{{error}}" }, + "deleteDocuments": { + "button": "Delete", + "tooltip": "Delete selected documents", + "title": "Delete Documents", + "description": "This will permanently delete the selected documents from the system", + "warning": "WARNING: This action will permanently delete the selected documents and cannot be undone!", + "confirm": "Do you really want to delete {{count}} selected document(s)?", + "confirmPrompt": "Type 'yes' to confirm this action", + "confirmPlaceholder": "Type yes to confirm", + "confirmButton": "YES", + "success": "Documents deleted successfully", + "failed": "Delete Documents Failed:\n{{message}}", + "error": "Delete Documents Failed:\n{{error}}", + "busy": "Pipeline is busy, please try again later", + "notAllowed": "No permission to perform this operation", + "cannotDeleteAll": "Cannot delete all documents. If you need to delete all documents, please use the Clear Documents feature." + }, + "deselectDocuments": { + "button": "Deselect", + "tooltip": "Deselect all selected documents", + "title": "Deselect Documents", + "description": "This will clear all selected documents ({{count}} selected)", + "confirmButton": "Deselect All" + }, "uploadDocuments": { "button": "Upload", "tooltip": "Upload documents", @@ -105,7 +129,8 @@ "chunks": "Chunks", "created": "Created", "updated": "Updated", - "metadata": "Metadata" + "metadata": "Metadata", + "select": "Select" }, "status": { "all": "All", diff --git a/lightrag_webui/src/locales/zh.json b/lightrag_webui/src/locales/zh.json index 8f776de8..0e35d0d7 100644 --- a/lightrag_webui/src/locales/zh.json +++ b/lightrag_webui/src/locales/zh.json @@ -56,6 +56,30 @@ "failed": "清空文档失败:\n{{message}}", "error": "清空文档失败:\n{{error}}" }, + "deleteDocuments": { + "button": "删除", + "tooltip": "删除选中的文档", + "title": "删除文档", + "description": "此操作将永久删除选中的文档", + "warning": "警告:此操作将永久删除选中的文档,无法恢复!", + "confirm": "确定要删除 {{count}} 个选中的文档吗?", + "confirmPrompt": "请输入 yes 确认操作", + "confirmPlaceholder": "输入 yes 确认", + "confirmButton": "确定", + "success": "文档删除成功", + "failed": "删除文档失败:\n{{message}}", + "error": "删除文档失败:\n{{error}}", + "busy": "流水线被占用,请稍后再试", + "notAllowed": "没有操作权限", + "cannotDeleteAll": "无法删除所有文档。如确实需要删除所有文档请使用清空文档功能。" + }, + "deselectDocuments": { + "button": "取消选择", + "tooltip": "取消选择所有文档", + "title": "取消选择文档", + "description": "此操作将清除所有选中的文档(已选择 {{count}} 个)", + "confirmButton": "取消全部选择" + }, "uploadDocuments": { "button": "上传", "tooltip": "上传文档", @@ -105,7 +129,8 @@ "chunks": "分块", "created": "创建时间", "updated": "更新时间", - "metadata": "元数据" + "metadata": "元数据", + "select": "选择" }, "status": { "all": "全部",