Browse Source

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)
tags/v0.19.0
balibabu 6 months ago
parent
commit
9a8dda8fc7
No account linked to committer's email address

+ 1
- 1
web/src/components/rename-dialog/index.tsx View File

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

return (

+ 4
- 2
web/src/components/rename-dialog/rename-form.tsx 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 (

+ 58
- 0
web/src/hooks/use-document-request.ts 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 };
};

+ 101
- 0
web/src/pages/dataset/dataset/dataset-action-cell.tsx 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>
);
}

+ 22
- 0
web/src/pages/dataset/dataset/dataset-table.tsx 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>
);
}

+ 0
- 29
web/src/pages/dataset/dataset/hooks.ts 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();


+ 13
- 3
web/src/pages/dataset/dataset/parsing-status-cell.tsx 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>
)}

+ 11
- 40
web/src/pages/dataset/dataset/use-dataset-table-columns.tsx 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>
);
},
},

+ 49
- 0
web/src/pages/dataset/dataset/use-rename-document.ts 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'
>;

Loading…
Cancel
Save