### 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
| initialName, | initialName, | ||||
| onOk, | onOk, | ||||
| loading, | loading, | ||||
| }: IModalProps<any> & { initialName: string }) { | |||||
| }: IModalProps<any> & { initialName?: string }) { | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| return ( | return ( |
| 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 ( |
| 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 }; | |||||
| }; |
| 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> | |||||
| ); | |||||
| } |
| 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> | ||||
| ); | ); | ||||
| } | } |
| 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(); | ||||
| 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> | ||||
| )} | )} |
| 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> | |||||
| ); | ); | ||||
| }, | }, | ||||
| }, | }, |
| 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' | |||||
| >; |