Feat: Delete and rename files in the knowledge base #3221 (#7268)

### What problem does this PR solve?

Feat: Delete and rename files in the knowledge base #3221
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2025-04-24 14:51:20 +08:00 committed by GitHub
parent ff442c48b5
commit 9a8dda8fc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 259 additions and 75 deletions

View File

@ -16,7 +16,7 @@ export function RenameDialog({
initialName,
onOk,
loading,
}: IModalProps<any> & { initialName: string }) {
}: IModalProps<any> & { initialName?: string }) {
const { t } = useTranslation();
return (

View File

@ -22,7 +22,7 @@ export function RenameForm({
initialName,
hideModal,
onOk,
}: IModalProps<any> & { initialName: string }) {
}: IModalProps<any> & { initialName?: string }) {
const { t } = useTranslation();
const FormSchema = z.object({
name: z
@ -46,7 +46,9 @@ export function RenameForm({
}
useEffect(() => {
form.setValue('name', initialName);
if (initialName) {
form.setValue('name', initialName);
}
}, [form, initialName]);
return (

View File

@ -18,6 +18,8 @@ export const enum DocumentApiAction {
FetchDocumentList = 'fetchDocumentList',
UpdateDocumentStatus = 'updateDocumentStatus',
RunDocumentByIds = 'runDocumentByIds',
RemoveDocument = 'removeDocument',
SaveDocumentName = 'saveDocumentName',
}
export const useUploadNextDocument = () => {
@ -189,3 +191,59 @@ export const useRunDocument = () => {
return { runDocumentByIds: mutateAsync, loading, data };
};
export const useRemoveDocument = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [DocumentApiAction.RemoveDocument],
mutationFn: async (documentIds: string | string[]) => {
const { data } = await kbService.document_rm({ doc_id: documentIds });
if (data.code === 0) {
message.success(i18n.t('message.deleted'));
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
}
return data.code;
},
});
return { data, loading, removeDocument: mutateAsync };
};
export const useSaveDocumentName = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [DocumentApiAction.SaveDocumentName],
mutationFn: async ({
name,
documentId,
}: {
name: string;
documentId: string;
}) => {
const { data } = await kbService.document_rename({
doc_id: documentId,
name: name,
});
if (data.code === 0) {
message.success(i18n.t('message.renamed'));
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
}
return data.code;
},
});
return { loading, saveName: mutateAsync, data };
};

View File

@ -0,0 +1,101 @@
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import { Button } from '@/components/ui/button';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card';
import { useRemoveDocument } from '@/hooks/use-document-request';
import { IDocumentInfo } from '@/interfaces/database/document';
import { formatFileSize } from '@/utils/common-util';
import { formatDate } from '@/utils/date';
import { downloadDocument } from '@/utils/file-util';
import { ArrowDownToLine, Pencil, ScrollText, Trash2 } from 'lucide-react';
import { useCallback } from 'react';
import { UseRenameDocumentShowType } from './use-rename-document';
import { isParserRunning } from './utils';
const Fields = ['name', 'size', 'type', 'create_time', 'update_time'];
const FunctionMap = {
size: formatFileSize,
create_time: formatDate,
update_time: formatDate,
};
export function DatasetActionCell({
record,
showRenameModal,
}: { record: IDocumentInfo } & UseRenameDocumentShowType) {
const { id, run } = record;
const isRunning = isParserRunning(run);
const { removeDocument } = useRemoveDocument();
const onDownloadDocument = useCallback(() => {
downloadDocument({
id,
filename: record.name,
});
}, [id, record.name]);
const handleRemove = useCallback(() => {
removeDocument(id);
}, [id, removeDocument]);
const handleRename = useCallback(() => {
showRenameModal(record);
}, [record, showRenameModal]);
return (
<section className="flex gap-4 items-center">
<HoverCard>
<HoverCardTrigger>
<Button variant="ghost" size={'icon'} disabled={isRunning}>
<ScrollText />
</Button>
</HoverCardTrigger>
<HoverCardContent className="w-[40vw] max-h-[40vh] overflow-auto">
<ul className="space-y-2">
{Object.entries(record)
.filter(([key]) => Fields.some((x) => x === key))
.map(([key, value], idx) => {
return (
<li key={idx} className="flex gap-2">
{key}:
<div>
{key in FunctionMap
? FunctionMap[key as keyof typeof FunctionMap](value)
: value}
</div>
</li>
);
})}
</ul>
</HoverCardContent>
</HoverCard>
<Button
variant={'ghost'}
size={'icon'}
disabled={isRunning}
onClick={handleRename}
>
<Pencil />
</Button>
<Button
variant={'ghost'}
size={'icon'}
onClick={onDownloadDocument}
disabled={isRunning}
>
<ArrowDownToLine />
</Button>
<ConfirmDeleteDialog onOk={handleRemove}>
<Button variant={'ghost'} size={'icon'} disabled={isRunning}>
<Trash2 className="text-text-delete-red" />
</Button>
</ConfirmDeleteDialog>
</section>
);
}

View File

@ -14,6 +14,7 @@ import {
import * as React from 'react';
import { ChunkMethodDialog } from '@/components/chunk-method-dialog';
import { RenameDialog } from '@/components/rename-dialog';
import { Button } from '@/components/ui/button';
import {
Table,
@ -30,6 +31,7 @@ import { getExtension } from '@/utils/document-util';
import { useMemo } from 'react';
import { useChangeDocumentParser } from './hooks';
import { useDatasetTableColumns } from './use-dataset-table-columns';
import { useRenameDocument } from './use-rename-document';
export function DatasetTable() {
const {
@ -57,9 +59,19 @@ export function DatasetTable() {
showChangeParserModal,
} = useChangeDocumentParser(currentRecord.id);
const {
renameLoading,
onRenameOk,
renameVisible,
hideRenameModal,
showRenameModal,
initialName,
} = useRenameDocument();
const columns = useDatasetTableColumns({
showChangeParserModal,
setCurrentRecord: setRecord,
showRenameModal,
});
const currentPagination = useMemo(() => {
@ -196,6 +208,16 @@ export function DatasetTable() {
loading={changeParserLoading}
></ChunkMethodDialog>
)}
{renameVisible && (
<RenameDialog
visible={renameVisible}
onOk={onRenameOk}
loading={renameLoading}
hideModal={hideRenameModal}
initialName={initialName}
></RenameDialog>
)}
</div>
);
}

View File

@ -2,7 +2,6 @@ import { useSetModalState } from '@/hooks/common-hooks';
import {
useCreateNextDocument,
useNextWebCrawl,
useSaveNextDocumentName,
useSetNextDocumentParser,
} from '@/hooks/document-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
@ -23,34 +22,6 @@ export const useNavigateToOtherPage = () => {
return { linkToUploadPage, toChunk };
};
export const useRenameDocument = (documentId: string) => {
const { saveName, loading } = useSaveNextDocumentName();
const {
visible: renameVisible,
hideModal: hideRenameModal,
showModal: showRenameModal,
} = useSetModalState();
const onRenameOk = useCallback(
async (name: string) => {
const ret = await saveName({ documentId, name });
if (ret === 0) {
hideRenameModal();
}
},
[hideRenameModal, saveName, documentId],
);
return {
renameLoading: loading,
onRenameOk,
renameVisible,
hideRenameModal,
showRenameModal,
};
};
export const useCreateEmptyDocument = () => {
const { createDocument, loading } = useCreateNextDocument();

View File

@ -4,6 +4,7 @@ import { Progress } from '@/components/ui/progress';
import { Separator } from '@/components/ui/separator';
import { IDocumentInfo } from '@/interfaces/database/document';
import { CircleX, Play, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { RunningStatus } from './constant';
import { ParsingCard } from './parsing-card';
import { useHandleRunDocumentByIds } from './use-run-document';
@ -18,6 +19,7 @@ const IconMap = {
};
export function ParsingStatusCell({ record }: { record: IDocumentInfo }) {
const { t } = useTranslation();
const { run, parser_id, progress, chunk_num, id } = record;
const operationIcon = IconMap[run];
const p = Number((progress * 100).toFixed(2));
@ -40,20 +42,28 @@ export function ParsingStatusCell({ record }: { record: IDocumentInfo }) {
<Separator orientation="vertical" />
</div>
<ConfirmDeleteDialog
hidden={isZeroChunk}
title={t(`knowledgeDetails.redo`, { chunkNum: chunk_num })}
hidden={isZeroChunk || isRunning}
onOk={handleOperationIconClick(true)}
onCancel={handleOperationIconClick(false)}
>
<Button
variant={'ghost'}
size={'sm'}
onClick={isZeroChunk ? handleOperationIconClick(false) : () => {}}
onClick={
isZeroChunk || isRunning
? handleOperationIconClick(false)
: () => {}
}
>
{operationIcon}
</Button>
</ConfirmDeleteDialog>
{isParserRunning(run) ? (
<Progress value={p} className="h-1" />
<div className="flex items-center gap-1">
<Progress value={p} className="h-1 flex-1 min-w-10" />
{p}%
</div>
) : (
<ParsingCard record={record}></ParsingCard>
)}

View File

@ -1,14 +1,6 @@
import SvgIcon from '@/components/svg-icon';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Switch } from '@/components/ui/switch';
import {
Tooltip,
@ -22,20 +14,25 @@ import { cn } from '@/lib/utils';
import { formatDate } from '@/utils/date';
import { getExtension } from '@/utils/document-util';
import { ColumnDef } from '@tanstack/table-core';
import { ArrowUpDown, MoreHorizontal, Pencil, Wrench } from 'lucide-react';
import { ArrowUpDown } from 'lucide-react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { DatasetActionCell } from './dataset-action-cell';
import { useChangeDocumentParser } from './hooks';
import { ParsingStatusCell } from './parsing-status-cell';
import { UseRenameDocumentShowType } from './use-rename-document';
type UseDatasetTableColumnsType = Pick<
ReturnType<typeof useChangeDocumentParser>,
'showChangeParserModal'
> & { setCurrentRecord: (record: IDocumentInfo) => void };
> & {
setCurrentRecord: (record: IDocumentInfo) => void;
} & UseRenameDocumentShowType;
export function useDatasetTableColumns({
showChangeParserModal,
setCurrentRecord,
showRenameModal,
}: UseDatasetTableColumnsType) {
const { t } = useTranslation('translation', {
keyPrefix: 'knowledgeDetails',
@ -182,36 +179,10 @@ export function useDatasetTableColumns({
const record = row.original;
return (
<section className="flex gap-4 items-center">
<Button
variant="icon"
size={'icon'}
onClick={onShowChangeParserModal(record)}
>
<Wrench />
</Button>
<Button variant="icon" size={'icon'}>
<Pencil />
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="icon" size={'icon'}>
<MoreHorizontal />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(record.id)}
>
Copy payment ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>View customer</DropdownMenuItem>
<DropdownMenuItem>View payment details</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</section>
<DatasetActionCell
record={record}
showRenameModal={showRenameModal}
></DatasetActionCell>
);
},
},

View File

@ -0,0 +1,49 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { useSaveDocumentName } from '@/hooks/use-document-request';
import { IDocumentInfo } from '@/interfaces/database/document';
import { useCallback, useState } from 'react';
export const useRenameDocument = () => {
const { saveName, loading } = useSaveDocumentName();
const [record, setRecord] = useState<IDocumentInfo>();
const {
visible: renameVisible,
hideModal: hideRenameModal,
showModal: showRenameModal,
} = useSetModalState();
const onRenameOk = useCallback(
async (name: string) => {
if (record?.id) {
const ret = await saveName({ documentId: record.id, name });
if (ret === 0) {
hideRenameModal();
}
}
},
[record?.id, saveName, hideRenameModal],
);
const handleShow = useCallback(
(row: IDocumentInfo) => {
setRecord(row);
showRenameModal();
},
[showRenameModal],
);
return {
renameLoading: loading,
onRenameOk,
renameVisible,
hideRenameModal,
showRenameModal: handleShow,
initialName: record?.name,
};
};
export type UseRenameDocumentShowType = Pick<
ReturnType<typeof useRenameDocument>,
'showRenameModal'
>;