Просмотр исходного кода

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 месяцев назад
Родитель
Сommit
9a8dda8fc7
Аккаунт пользователя с таким Email не найден

+ 1
- 1
web/src/components/rename-dialog/index.tsx Просмотреть файл

initialName, initialName,
onOk, onOk,
loading, loading,
}: IModalProps<any> & { initialName: string }) {
}: IModalProps<any> & { initialName?: string }) {
const { t } = useTranslation(); const { t } = useTranslation();


return ( return (

+ 4
- 2
web/src/components/rename-dialog/rename-form.tsx Просмотреть файл

initialName, initialName,
hideModal, hideModal,
onOk, onOk,
}: IModalProps<any> & { initialName: string }) {
}: IModalProps<any> & { initialName?: string }) {
const { t } = useTranslation(); const { t } = useTranslation();
const FormSchema = z.object({ const FormSchema = z.object({
name: z name: z
} }


useEffect(() => { useEffect(() => {
form.setValue('name', initialName);
if (initialName) {
form.setValue('name', initialName);
}
}, [form, initialName]); }, [form, initialName]);


return ( return (

+ 58
- 0
web/src/hooks/use-document-request.ts Просмотреть файл

FetchDocumentList = 'fetchDocumentList', FetchDocumentList = 'fetchDocumentList',
UpdateDocumentStatus = 'updateDocumentStatus', UpdateDocumentStatus = 'updateDocumentStatus',
RunDocumentByIds = 'runDocumentByIds', RunDocumentByIds = 'runDocumentByIds',
RemoveDocument = 'removeDocument',
SaveDocumentName = 'saveDocumentName',
} }


export const useUploadNextDocument = () => { export const useUploadNextDocument = () => {


return { runDocumentByIds: mutateAsync, loading, data }; 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 Просмотреть файл

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 Просмотреть файл

import * as React from 'react'; import * as React from 'react';


import { ChunkMethodDialog } from '@/components/chunk-method-dialog'; import { ChunkMethodDialog } from '@/components/chunk-method-dialog';
import { RenameDialog } from '@/components/rename-dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Table, Table,
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useChangeDocumentParser } from './hooks'; import { useChangeDocumentParser } from './hooks';
import { useDatasetTableColumns } from './use-dataset-table-columns'; import { useDatasetTableColumns } from './use-dataset-table-columns';
import { useRenameDocument } from './use-rename-document';


export function DatasetTable() { export function DatasetTable() {
const { const {
showChangeParserModal, showChangeParserModal,
} = useChangeDocumentParser(currentRecord.id); } = useChangeDocumentParser(currentRecord.id);


const {
renameLoading,
onRenameOk,
renameVisible,
hideRenameModal,
showRenameModal,
initialName,
} = useRenameDocument();

const columns = useDatasetTableColumns({ const columns = useDatasetTableColumns({
showChangeParserModal, showChangeParserModal,
setCurrentRecord: setRecord, setCurrentRecord: setRecord,
showRenameModal,
}); });


const currentPagination = useMemo(() => { const currentPagination = useMemo(() => {
loading={changeParserLoading} loading={changeParserLoading}
></ChunkMethodDialog> ></ChunkMethodDialog>
)} )}

{renameVisible && (
<RenameDialog
visible={renameVisible}
onOk={onRenameOk}
loading={renameLoading}
hideModal={hideRenameModal}
initialName={initialName}
></RenameDialog>
)}
</div> </div>
); );
} }

+ 0
- 29
web/src/pages/dataset/dataset/hooks.ts Просмотреть файл

import { import {
useCreateNextDocument, useCreateNextDocument,
useNextWebCrawl, useNextWebCrawl,
useSaveNextDocumentName,
useSetNextDocumentParser, useSetNextDocumentParser,
} from '@/hooks/document-hooks'; } from '@/hooks/document-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
return { linkToUploadPage, toChunk }; 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 = () => { export const useCreateEmptyDocument = () => {
const { createDocument, loading } = useCreateNextDocument(); const { createDocument, loading } = useCreateNextDocument();



+ 13
- 3
web/src/pages/dataset/dataset/parsing-status-cell.tsx Просмотреть файл

import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { IDocumentInfo } from '@/interfaces/database/document'; import { IDocumentInfo } from '@/interfaces/database/document';
import { CircleX, Play, RefreshCw } from 'lucide-react'; import { CircleX, Play, RefreshCw } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { RunningStatus } from './constant'; import { RunningStatus } from './constant';
import { ParsingCard } from './parsing-card'; import { ParsingCard } from './parsing-card';
import { useHandleRunDocumentByIds } from './use-run-document'; import { useHandleRunDocumentByIds } from './use-run-document';
}; };


export function ParsingStatusCell({ record }: { record: IDocumentInfo }) { export function ParsingStatusCell({ record }: { record: IDocumentInfo }) {
const { t } = useTranslation();
const { run, parser_id, progress, chunk_num, id } = record; const { run, parser_id, progress, chunk_num, id } = record;
const operationIcon = IconMap[run]; const operationIcon = IconMap[run];
const p = Number((progress * 100).toFixed(2)); const p = Number((progress * 100).toFixed(2));
<Separator orientation="vertical" /> <Separator orientation="vertical" />
</div> </div>
<ConfirmDeleteDialog <ConfirmDeleteDialog
hidden={isZeroChunk}
title={t(`knowledgeDetails.redo`, { chunkNum: chunk_num })}
hidden={isZeroChunk || isRunning}
onOk={handleOperationIconClick(true)} onOk={handleOperationIconClick(true)}
onCancel={handleOperationIconClick(false)} onCancel={handleOperationIconClick(false)}
> >
<Button <Button
variant={'ghost'} variant={'ghost'}
size={'sm'} size={'sm'}
onClick={isZeroChunk ? handleOperationIconClick(false) : () => {}}
onClick={
isZeroChunk || isRunning
? handleOperationIconClick(false)
: () => {}
}
> >
{operationIcon} {operationIcon}
</Button> </Button>
</ConfirmDeleteDialog> </ConfirmDeleteDialog>
{isParserRunning(run) ? ( {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> <ParsingCard record={record}></ParsingCard>
)} )}

+ 11
- 40
web/src/pages/dataset/dataset/use-dataset-table-columns.tsx Просмотреть файл

import SvgIcon from '@/components/svg-icon'; import SvgIcon from '@/components/svg-icon';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox'; 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 { Switch } from '@/components/ui/switch';
import { import {
Tooltip, Tooltip,
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { getExtension } from '@/utils/document-util'; import { getExtension } from '@/utils/document-util';
import { ColumnDef } from '@tanstack/table-core'; import { ColumnDef } from '@tanstack/table-core';
import { ArrowUpDown, MoreHorizontal, Pencil, Wrench } from 'lucide-react';
import { ArrowUpDown } from 'lucide-react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DatasetActionCell } from './dataset-action-cell';
import { useChangeDocumentParser } from './hooks'; import { useChangeDocumentParser } from './hooks';
import { ParsingStatusCell } from './parsing-status-cell'; import { ParsingStatusCell } from './parsing-status-cell';
import { UseRenameDocumentShowType } from './use-rename-document';


type UseDatasetTableColumnsType = Pick< type UseDatasetTableColumnsType = Pick<
ReturnType<typeof useChangeDocumentParser>, ReturnType<typeof useChangeDocumentParser>,
'showChangeParserModal' 'showChangeParserModal'
> & { setCurrentRecord: (record: IDocumentInfo) => void };
> & {
setCurrentRecord: (record: IDocumentInfo) => void;
} & UseRenameDocumentShowType;


export function useDatasetTableColumns({ export function useDatasetTableColumns({
showChangeParserModal, showChangeParserModal,
setCurrentRecord, setCurrentRecord,
showRenameModal,
}: UseDatasetTableColumnsType) { }: UseDatasetTableColumnsType) {
const { t } = useTranslation('translation', { const { t } = useTranslation('translation', {
keyPrefix: 'knowledgeDetails', keyPrefix: 'knowledgeDetails',
const record = row.original; const record = row.original;


return ( 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 Просмотреть файл

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'
>;

Загрузка…
Отмена
Сохранить