### What problem does this PR solve? Feat: Upload document #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.18.0
| return ( | return ( | ||||
| <AlertDialog> | <AlertDialog> | ||||
| <AlertDialogTrigger asChild>{children}</AlertDialogTrigger> | <AlertDialogTrigger asChild>{children}</AlertDialogTrigger> | ||||
| <AlertDialogContent> | |||||
| <AlertDialogContent | |||||
| onSelect={(e) => e.preventDefault()} | |||||
| onClick={(e) => e.stopPropagation()} | |||||
| > | |||||
| <AlertDialogHeader> | <AlertDialogHeader> | ||||
| <AlertDialogTitle> | <AlertDialogTitle> | ||||
| {title ?? t('common.deleteModalTitle')} | {title ?? t('common.deleteModalTitle')} |
| } from '@/components/ui/dialog'; | } from '@/components/ui/dialog'; | ||||
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; | import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; | ||||
| import { IModalProps } from '@/interfaces/common'; | import { IModalProps } from '@/interfaces/common'; | ||||
| import { useState } from 'react'; | |||||
| import { Dispatch, SetStateAction, useCallback, useState } from 'react'; | |||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { FileUploader } from '../file-uploader'; | import { FileUploader } from '../file-uploader'; | ||||
| export function UploaderTabs() { | |||||
| const { t } = useTranslation(); | |||||
| type UploaderTabsProps = { | |||||
| setFiles: Dispatch<SetStateAction<File[]>>; | |||||
| }; | |||||
| const [files, setFiles] = useState<File[]>([]); | |||||
| console.log('🚀 ~ TabsDemo ~ files:', files); | |||||
| export function UploaderTabs({ setFiles }: UploaderTabsProps) { | |||||
| const { t } = useTranslation(); | |||||
| return ( | return ( | ||||
| <Tabs defaultValue="account"> | <Tabs defaultValue="account"> | ||||
| ); | ); | ||||
| } | } | ||||
| export function FileUploadDialog({ hideModal }: IModalProps<any>) { | |||||
| export function FileUploadDialog({ hideModal, onOk }: IModalProps<File[]>) { | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const [files, setFiles] = useState<File[]>([]); | |||||
| const handleOk = useCallback(() => { | |||||
| onOk?.(files); | |||||
| }, [files, onOk]); | |||||
| return ( | return ( | ||||
| <Dialog open onOpenChange={hideModal}> | <Dialog open onOpenChange={hideModal}> | ||||
| <DialogHeader> | <DialogHeader> | ||||
| <DialogTitle>{t('fileManager.uploadFile')}</DialogTitle> | <DialogTitle>{t('fileManager.uploadFile')}</DialogTitle> | ||||
| </DialogHeader> | </DialogHeader> | ||||
| <UploaderTabs></UploaderTabs> | |||||
| <UploaderTabs setFiles={setFiles}></UploaderTabs> | |||||
| <DialogFooter> | <DialogFooter> | ||||
| <Button type="submit" variant={'tertiary'} size={'sm'}> | |||||
| <Button | |||||
| type="submit" | |||||
| variant={'tertiary'} | |||||
| size={'sm'} | |||||
| onClick={handleOk} | |||||
| > | |||||
| {t('common.save')} | {t('common.save')} | ||||
| </Button> | </Button> | ||||
| </DialogFooter> | </DialogFooter> |
| searchString?: string; | searchString?: string; | ||||
| onSearchChange?: ChangeEventHandler<HTMLInputElement>; | onSearchChange?: ChangeEventHandler<HTMLInputElement>; | ||||
| count?: number; | count?: number; | ||||
| showFilter?: boolean; | |||||
| } | } | ||||
| const FilterButton = React.forwardRef< | const FilterButton = React.forwardRef< | ||||
| searchString, | searchString, | ||||
| onSearchChange, | onSearchChange, | ||||
| count, | count, | ||||
| showFilter = true, | |||||
| }: PropsWithChildren<IProps>) { | }: PropsWithChildren<IProps>) { | ||||
| return ( | return ( | ||||
| <div className="flex justify-between mb-6"> | <div className="flex justify-between mb-6"> | ||||
| <span className="text-3xl font-bold ">{title}</span> | <span className="text-3xl font-bold ">{title}</span> | ||||
| <div className="flex gap-4 items-center"> | <div className="flex gap-4 items-center"> | ||||
| {FilterPopover ? ( | |||||
| <FilterPopover> | |||||
| <FilterButton count={count}></FilterButton> | |||||
| </FilterPopover> | |||||
| ) : ( | |||||
| <FilterButton></FilterButton> | |||||
| )} | |||||
| {showFilter && | |||||
| (FilterPopover ? ( | |||||
| <FilterPopover> | |||||
| <FilterButton count={count}></FilterButton> | |||||
| </FilterPopover> | |||||
| ) : ( | |||||
| <FilterButton></FilterButton> | |||||
| ))} | |||||
| <SearchInput | <SearchInput | ||||
| value={searchString} | value={searchString} |
| import kbService from '@/services/knowledge-service'; | |||||
| import { useMutation, useQueryClient } from '@tanstack/react-query'; | |||||
| import { get } from 'lodash'; | |||||
| import { useParams } from 'umi'; | |||||
| export const enum DocumentApiAction { | |||||
| UploadDocument = 'uploadDocument', | |||||
| FetchDocumentList = 'fetchDocumentList', | |||||
| } | |||||
| export const useUploadNextDocument = () => { | |||||
| const queryClient = useQueryClient(); | |||||
| const { id } = useParams(); | |||||
| const { | |||||
| data, | |||||
| isPending: loading, | |||||
| mutateAsync, | |||||
| } = useMutation({ | |||||
| mutationKey: [DocumentApiAction.UploadDocument], | |||||
| mutationFn: async (fileList: File[]) => { | |||||
| const formData = new FormData(); | |||||
| formData.append('kb_id', id!); | |||||
| fileList.forEach((file: any) => { | |||||
| formData.append('file', file); | |||||
| }); | |||||
| try { | |||||
| const ret = await kbService.document_upload(formData); | |||||
| const code = get(ret, 'data.code'); | |||||
| if (code === 0 || code === 500) { | |||||
| queryClient.invalidateQueries({ | |||||
| queryKey: [DocumentApiAction.FetchDocumentList], | |||||
| }); | |||||
| } | |||||
| return ret?.data; | |||||
| } catch (error) { | |||||
| console.warn(error); | |||||
| return { | |||||
| code: 500, | |||||
| message: error + '', | |||||
| }; | |||||
| } | |||||
| }, | |||||
| }); | |||||
| return { uploadDocument: mutateAsync, loading, data }; | |||||
| }; |
| import fileManagerService from '@/services/file-manager-service'; | |||||
| import { useMutation, useQueryClient } from '@tanstack/react-query'; | |||||
| import { message } from 'antd'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { useSetPaginationParams } from './route-hook'; | |||||
| export const enum FileApiAction { | |||||
| UploadFile = 'uploadFile', | |||||
| FetchFileList = 'fetchFileList', | |||||
| } | |||||
| export const useUploadFile = () => { | |||||
| const { setPaginationParams } = useSetPaginationParams(); | |||||
| const { t } = useTranslation(); | |||||
| const queryClient = useQueryClient(); | |||||
| const { | |||||
| data, | |||||
| isPending: loading, | |||||
| mutateAsync, | |||||
| } = useMutation({ | |||||
| mutationKey: [FileApiAction.UploadFile], | |||||
| mutationFn: async (params: { fileList: File[]; parentId: string }) => { | |||||
| const fileList = params.fileList; | |||||
| const pathList = params.fileList.map( | |||||
| (file) => (file as any).webkitRelativePath, | |||||
| ); | |||||
| const formData = new FormData(); | |||||
| formData.append('parent_id', params.parentId); | |||||
| fileList.forEach((file: any, index: number) => { | |||||
| formData.append('file', file); | |||||
| formData.append('path', pathList[index]); | |||||
| }); | |||||
| try { | |||||
| const ret = await fileManagerService.uploadFile(formData); | |||||
| if (ret?.data.code === 0) { | |||||
| message.success(t('message.uploaded')); | |||||
| setPaginationParams(1); | |||||
| queryClient.invalidateQueries({ | |||||
| queryKey: [FileApiAction.FetchFileList], | |||||
| }); | |||||
| } | |||||
| return ret?.data?.code; | |||||
| } catch (error) {} | |||||
| }, | |||||
| }); | |||||
| return { data, loading, uploadFile: mutateAsync }; | |||||
| }; |
| useRunNextDocument, | useRunNextDocument, | ||||
| useSaveNextDocumentName, | useSaveNextDocumentName, | ||||
| useSetNextDocumentParser, | useSetNextDocumentParser, | ||||
| useUploadNextDocument, | |||||
| } from '@/hooks/document-hooks'; | } from '@/hooks/document-hooks'; | ||||
| import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; | import { useGetKnowledgeSearchParams } from '@/hooks/route-hook'; | ||||
| import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; | import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; | ||||
| import { getUnSupportedFilesCount } from '@/utils/document-util'; | |||||
| import { UploadFile } from 'antd'; | |||||
| import { useCallback, useState } from 'react'; | import { useCallback, useState } from 'react'; | ||||
| import { useNavigate } from 'umi'; | import { useNavigate } from 'umi'; | ||||
| return rowSelection; | return rowSelection; | ||||
| }; | }; | ||||
| export const useHandleUploadDocument = () => { | |||||
| const { | |||||
| visible: documentUploadVisible, | |||||
| hideModal: hideDocumentUploadModal, | |||||
| showModal: showDocumentUploadModal, | |||||
| } = useSetModalState(); | |||||
| const { uploadDocument, loading } = useUploadNextDocument(); | |||||
| const onDocumentUploadOk = useCallback( | |||||
| async (fileList: UploadFile[]): Promise<number | undefined> => { | |||||
| if (fileList.length > 0) { | |||||
| const ret: any = await uploadDocument(fileList); | |||||
| if (typeof ret?.message !== 'string') { | |||||
| return; | |||||
| } | |||||
| const count = getUnSupportedFilesCount(ret?.message); | |||||
| /// 500 error code indicates that some file types are not supported | |||||
| let code = ret?.code; | |||||
| if ( | |||||
| ret?.code === 0 || | |||||
| (ret?.code === 500 && count !== fileList.length) // Some files were not uploaded successfully, but some were uploaded successfully. | |||||
| ) { | |||||
| code = 0; | |||||
| hideDocumentUploadModal(); | |||||
| } | |||||
| return code; | |||||
| } | |||||
| }, | |||||
| [uploadDocument, hideDocumentUploadModal], | |||||
| ); | |||||
| return { | |||||
| documentUploadLoading: loading, | |||||
| onDocumentUploadOk, | |||||
| documentUploadVisible, | |||||
| hideDocumentUploadModal, | |||||
| showDocumentUploadModal, | |||||
| }; | |||||
| }; | |||||
| export const useHandleWebCrawl = () => { | export const useHandleWebCrawl = () => { | ||||
| const { | const { | ||||
| visible: webCrawlUploadVisible, | visible: webCrawlUploadVisible, |
| import ListFilterBar from '@/components/list-filter-bar'; | import ListFilterBar from '@/components/list-filter-bar'; | ||||
| import { Upload } from 'lucide-react'; | import { Upload } from 'lucide-react'; | ||||
| import { DatasetTable } from './dataset-table'; | import { DatasetTable } from './dataset-table'; | ||||
| import { useHandleUploadDocument } from './hooks'; | |||||
| import { useHandleUploadDocument } from './use-upload-document'; | |||||
| export default function Dataset() { | export default function Dataset() { | ||||
| const { | const { |
| import { useSetModalState } from '@/hooks/common-hooks'; | |||||
| import { useUploadNextDocument } from '@/hooks/use-document-request'; | |||||
| import { getUnSupportedFilesCount } from '@/utils/document-util'; | |||||
| import { useCallback } from 'react'; | |||||
| export const useHandleUploadDocument = () => { | |||||
| const { | |||||
| visible: documentUploadVisible, | |||||
| hideModal: hideDocumentUploadModal, | |||||
| showModal: showDocumentUploadModal, | |||||
| } = useSetModalState(); | |||||
| const { uploadDocument, loading } = useUploadNextDocument(); | |||||
| const onDocumentUploadOk = useCallback( | |||||
| async (fileList: File[]): Promise<number | undefined> => { | |||||
| if (fileList.length > 0) { | |||||
| const ret: any = await uploadDocument(fileList); | |||||
| if (typeof ret?.message !== 'string') { | |||||
| return; | |||||
| } | |||||
| const count = getUnSupportedFilesCount(ret?.message); | |||||
| /// 500 error code indicates that some file types are not supported | |||||
| let code = ret?.code; | |||||
| if ( | |||||
| ret?.code === 0 || | |||||
| (ret?.code === 500 && count !== fileList.length) // Some files were not uploaded successfully, but some were uploaded successfully. | |||||
| ) { | |||||
| code = 0; | |||||
| hideDocumentUploadModal(); | |||||
| } | |||||
| return code; | |||||
| } | |||||
| }, | |||||
| [uploadDocument, hideDocumentUploadModal], | |||||
| ); | |||||
| return { | |||||
| documentUploadLoading: loading, | |||||
| onDocumentUploadOk, | |||||
| documentUploadVisible, | |||||
| hideDocumentUploadModal, | |||||
| showDocumentUploadModal, | |||||
| }; | |||||
| }; |
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | |||||
| import { Badge } from '@/components/ui/badge'; | |||||
| import { Button } from '@/components/ui/button'; | |||||
| import { Card, CardContent } from '@/components/ui/card'; | |||||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | |||||
| import { IKnowledge } from '@/interfaces/database/knowledge'; | |||||
| import { formatDate } from '@/utils/date'; | |||||
| import { Ellipsis } from 'lucide-react'; | |||||
| import { DatasetDropdown } from './dataset-dropdown'; | |||||
| import { useDisplayOwnerName } from './use-display-owner'; | |||||
| import { useRenameDataset } from './use-rename-dataset'; | |||||
| export type DatasetCardProps = { | |||||
| dataset: IKnowledge; | |||||
| } & Pick<ReturnType<typeof useRenameDataset>, 'showDatasetRenameModal'>; | |||||
| export function DatasetCard({ | |||||
| dataset, | |||||
| showDatasetRenameModal, | |||||
| }: DatasetCardProps) { | |||||
| const { navigateToDataset } = useNavigatePage(); | |||||
| const displayOwnerName = useDisplayOwnerName(); | |||||
| const owner = displayOwnerName(dataset.tenant_id, dataset.nickname); | |||||
| return ( | |||||
| <Card | |||||
| key={dataset.id} | |||||
| className="bg-colors-background-inverse-weak flex-1" | |||||
| onClick={navigateToDataset(dataset.id)} | |||||
| > | |||||
| <CardContent className="p-4"> | |||||
| <section className="flex justify-between mb-4"> | |||||
| <div className="flex gap-2"> | |||||
| <Avatar className="w-[70px] h-[70px] rounded-lg"> | |||||
| <AvatarImage src={dataset.avatar} /> | |||||
| <AvatarFallback className="rounded-lg">CN</AvatarFallback> | |||||
| </Avatar> | |||||
| {owner && <Badge className="h-5">{owner}</Badge>} | |||||
| </div> | |||||
| <DatasetDropdown | |||||
| showDatasetRenameModal={showDatasetRenameModal} | |||||
| dataset={dataset} | |||||
| > | |||||
| <Button variant="ghost" size="icon"> | |||||
| <Ellipsis /> | |||||
| </Button> | |||||
| </DatasetDropdown> | |||||
| </section> | |||||
| <div className="flex justify-between items-end"> | |||||
| <div> | |||||
| <h3 className="text-lg font-semibold mb-2 line-clamp-1"> | |||||
| {dataset.name} | |||||
| </h3> | |||||
| <p className="text-sm opacity-80">{dataset.doc_num} files</p> | |||||
| <p className="text-sm opacity-80"> | |||||
| Created {formatDate(dataset.update_time)} | |||||
| </p> | |||||
| </div> | |||||
| </div> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| } |
| import { useDeleteKnowledge } from '@/hooks/use-knowledge-request'; | import { useDeleteKnowledge } from '@/hooks/use-knowledge-request'; | ||||
| import { IKnowledge } from '@/interfaces/database/knowledge'; | import { IKnowledge } from '@/interfaces/database/knowledge'; | ||||
| import { PenLine, Trash2 } from 'lucide-react'; | import { PenLine, Trash2 } from 'lucide-react'; | ||||
| import { PropsWithChildren, useCallback } from 'react'; | |||||
| import { MouseEventHandler, PropsWithChildren, useCallback } from 'react'; | |||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { useRenameDataset } from './use-rename-dataset'; | import { useRenameDataset } from './use-rename-dataset'; | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const { deleteKnowledge } = useDeleteKnowledge(); | const { deleteKnowledge } = useDeleteKnowledge(); | ||||
| const handleShowDatasetRenameModal = useCallback(() => { | |||||
| showDatasetRenameModal(dataset); | |||||
| }, [dataset, showDatasetRenameModal]); | |||||
| const handleShowDatasetRenameModal: MouseEventHandler<HTMLDivElement> = | |||||
| useCallback( | |||||
| (e) => { | |||||
| e.stopPropagation(); | |||||
| showDatasetRenameModal(dataset); | |||||
| }, | |||||
| [dataset, showDatasetRenameModal], | |||||
| ); | |||||
| const handleDelete = useCallback(() => { | |||||
| const handleDelete: MouseEventHandler<HTMLDivElement> = useCallback(() => { | |||||
| deleteKnowledge(dataset.id); | deleteKnowledge(dataset.id); | ||||
| }, [dataset.id, deleteKnowledge]); | }, [dataset.id, deleteKnowledge]); | ||||
| <ConfirmDeleteDialog onOk={handleDelete}> | <ConfirmDeleteDialog onOk={handleDelete}> | ||||
| <DropdownMenuItem | <DropdownMenuItem | ||||
| className="text-text-delete-red" | className="text-text-delete-red" | ||||
| onSelect={(e) => e.preventDefault()} | |||||
| onSelect={(e) => { | |||||
| e.preventDefault(); | |||||
| }} | |||||
| onClick={(e) => { | |||||
| e.stopPropagation(); | |||||
| }} | |||||
| > | > | ||||
| {t('common.delete')} <Trash2 /> | {t('common.delete')} <Trash2 /> | ||||
| </DropdownMenuItem> | </DropdownMenuItem> |
| import ListFilterBar from '@/components/list-filter-bar'; | import ListFilterBar from '@/components/list-filter-bar'; | ||||
| import { RenameDialog } from '@/components/rename-dialog'; | import { RenameDialog } from '@/components/rename-dialog'; | ||||
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | |||||
| import { Badge } from '@/components/ui/badge'; | |||||
| import { Button } from '@/components/ui/button'; | |||||
| import { Card, CardContent } from '@/components/ui/card'; | |||||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | |||||
| import { useFetchNextKnowledgeListByPage } from '@/hooks/use-knowledge-request'; | import { useFetchNextKnowledgeListByPage } from '@/hooks/use-knowledge-request'; | ||||
| import { formatDate } from '@/utils/date'; | |||||
| import { pick } from 'lodash'; | import { pick } from 'lodash'; | ||||
| import { ChevronRight, Ellipsis, Plus } from 'lucide-react'; | |||||
| import { Plus } from 'lucide-react'; | |||||
| import { PropsWithChildren, useCallback } from 'react'; | import { PropsWithChildren, useCallback } from 'react'; | ||||
| import { DatasetCard } from './dataset-card'; | |||||
| import { DatasetCreatingDialog } from './dataset-creating-dialog'; | import { DatasetCreatingDialog } from './dataset-creating-dialog'; | ||||
| import { DatasetDropdown } from './dataset-dropdown'; | |||||
| import { DatasetsFilterPopover } from './datasets-filter-popover'; | import { DatasetsFilterPopover } from './datasets-filter-popover'; | ||||
| import { DatasetsPagination } from './datasets-pagination'; | import { DatasetsPagination } from './datasets-pagination'; | ||||
| import { useSaveKnowledge } from './hooks'; | import { useSaveKnowledge } from './hooks'; | ||||
| import { useDisplayOwnerName } from './use-display-owner'; | |||||
| import { useRenameDataset } from './use-rename-dataset'; | import { useRenameDataset } from './use-rename-dataset'; | ||||
| export default function Datasets() { | export default function Datasets() { | ||||
| onCreateOk, | onCreateOk, | ||||
| loading: creatingLoading, | loading: creatingLoading, | ||||
| } = useSaveKnowledge(); | } = useSaveKnowledge(); | ||||
| const { navigateToDataset } = useNavigatePage(); | |||||
| const { | const { | ||||
| kbs, | kbs, | ||||
| showDatasetRenameModal, | showDatasetRenameModal, | ||||
| } = useRenameDataset(); | } = useRenameDataset(); | ||||
| const displayOwnerName = useDisplayOwnerName(); | |||||
| const handlePageChange = useCallback( | const handlePageChange = useCallback( | ||||
| (page: number, pageSize?: number) => { | (page: number, pageSize?: number) => { | ||||
| setPagination({ page, pageSize }); | setPagination({ page, pageSize }); | ||||
| </ListFilterBar> | </ListFilterBar> | ||||
| <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8"> | <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8"> | ||||
| {kbs.map((dataset) => { | {kbs.map((dataset) => { | ||||
| const owner = displayOwnerName(dataset.tenant_id, dataset.nickname); | |||||
| return ( | return ( | ||||
| <Card | |||||
| <DatasetCard | |||||
| dataset={dataset} | |||||
| key={dataset.id} | key={dataset.id} | ||||
| className="bg-colors-background-inverse-weak flex-1" | |||||
| > | |||||
| <CardContent className="p-4"> | |||||
| <section className="flex justify-between mb-4"> | |||||
| <div className="flex gap-2"> | |||||
| <Avatar className="w-[70px] h-[70px] rounded-lg"> | |||||
| <AvatarImage src={dataset.avatar} /> | |||||
| <AvatarFallback className="rounded-lg">CN</AvatarFallback> | |||||
| </Avatar> | |||||
| {owner && <Badge className="h-5">{owner}</Badge>} | |||||
| </div> | |||||
| <DatasetDropdown | |||||
| showDatasetRenameModal={showDatasetRenameModal} | |||||
| dataset={dataset} | |||||
| > | |||||
| <Button variant="ghost" size="icon"> | |||||
| <Ellipsis /> | |||||
| </Button> | |||||
| </DatasetDropdown> | |||||
| </section> | |||||
| <div className="flex justify-between items-end"> | |||||
| <div> | |||||
| <h3 className="text-lg font-semibold mb-2"> | |||||
| {dataset.name} | |||||
| </h3> | |||||
| <p className="text-sm opacity-80"> | |||||
| {dataset.doc_num} files | |||||
| </p> | |||||
| <p className="text-sm opacity-80"> | |||||
| Created {formatDate(dataset.update_time)} | |||||
| </p> | |||||
| </div> | |||||
| <Button | |||||
| variant="icon" | |||||
| size="icon" | |||||
| onClick={navigateToDataset(dataset.id)} | |||||
| > | |||||
| <ChevronRight className="h-6 w-6" /> | |||||
| </Button> | |||||
| </div> | |||||
| </CardContent> | |||||
| </Card> | |||||
| showDatasetRenameModal={showDatasetRenameModal} | |||||
| ></DatasetCard> | |||||
| ); | ); | ||||
| })} | })} | ||||
| </div> | </div> |
| useFetchParentFolderList, | useFetchParentFolderList, | ||||
| useMoveFile, | useMoveFile, | ||||
| useRenameFile, | useRenameFile, | ||||
| useUploadFile, | |||||
| } from '@/hooks/file-manager-hooks'; | } from '@/hooks/file-manager-hooks'; | ||||
| import { IFile } from '@/interfaces/database/file-manager'; | import { IFile } from '@/interfaces/database/file-manager'; | ||||
| import { TableRowSelection } from 'antd/es/table/interface'; | import { TableRowSelection } from 'antd/es/table/interface'; | ||||
| import { UploadFile } from 'antd/lib'; | |||||
| import { useCallback, useMemo, useState } from 'react'; | import { useCallback, useMemo, useState } from 'react'; | ||||
| import { useNavigate, useSearchParams } from 'umi'; | import { useNavigate, useSearchParams } from 'umi'; | ||||
| return { handleRemoveFile }; | return { handleRemoveFile }; | ||||
| }; | }; | ||||
| export const useHandleUploadFile = () => { | |||||
| const { | |||||
| visible: fileUploadVisible, | |||||
| hideModal: hideFileUploadModal, | |||||
| showModal: showFileUploadModal, | |||||
| } = useSetModalState(); | |||||
| const { uploadFile, loading } = useUploadFile(); | |||||
| const id = useGetFolderId(); | |||||
| const onFileUploadOk = useCallback( | |||||
| async (fileList: UploadFile[]): Promise<number | undefined> => { | |||||
| if (fileList.length > 0) { | |||||
| const ret: number = await uploadFile({ fileList, parentId: id }); | |||||
| if (ret === 0) { | |||||
| hideFileUploadModal(); | |||||
| } | |||||
| return ret; | |||||
| } | |||||
| }, | |||||
| [uploadFile, hideFileUploadModal, id], | |||||
| ); | |||||
| return { | |||||
| fileUploadLoading: loading, | |||||
| onFileUploadOk, | |||||
| fileUploadVisible, | |||||
| hideFileUploadModal, | |||||
| showFileUploadModal, | |||||
| }; | |||||
| }; | |||||
| export const useHandleConnectToKnowledge = () => { | export const useHandleConnectToKnowledge = () => { | ||||
| const { | const { | ||||
| visible: connectToKnowledgeVisible, | visible: connectToKnowledgeVisible, |
| import { FileUploadDialog } from '@/components/file-upload-dialog'; | |||||
| import ListFilterBar from '@/components/list-filter-bar'; | import ListFilterBar from '@/components/list-filter-bar'; | ||||
| import { Upload } from 'lucide-react'; | import { Upload } from 'lucide-react'; | ||||
| import { FilesTable } from './files-table'; | import { FilesTable } from './files-table'; | ||||
| import { useHandleUploadFile } from './use-upload-file'; | |||||
| export default function Files() { | export default function Files() { | ||||
| const { | |||||
| fileUploadVisible, | |||||
| hideFileUploadModal, | |||||
| showFileUploadModal, | |||||
| fileUploadLoading, | |||||
| onFileUploadOk, | |||||
| } = useHandleUploadFile(); | |||||
| return ( | return ( | ||||
| <section className="p-8"> | <section className="p-8"> | ||||
| <ListFilterBar title="Files"> | |||||
| <ListFilterBar title="Files" showDialog={showFileUploadModal}> | |||||
| <Upload /> | <Upload /> | ||||
| Upload file | Upload file | ||||
| </ListFilterBar> | </ListFilterBar> | ||||
| <FilesTable></FilesTable> | <FilesTable></FilesTable> | ||||
| {fileUploadVisible && ( | |||||
| <FileUploadDialog | |||||
| hideModal={hideFileUploadModal} | |||||
| onOk={onFileUploadOk} | |||||
| loading={fileUploadLoading} | |||||
| ></FileUploadDialog> | |||||
| )} | |||||
| </section> | </section> | ||||
| ); | ); | ||||
| } | } |
| import { useSetModalState } from '@/hooks/common-hooks'; | |||||
| import { useUploadFile } from '@/hooks/use-file-request'; | |||||
| import { useCallback } from 'react'; | |||||
| import { useGetFolderId } from './hooks'; | |||||
| export const useHandleUploadFile = () => { | |||||
| const { | |||||
| visible: fileUploadVisible, | |||||
| hideModal: hideFileUploadModal, | |||||
| showModal: showFileUploadModal, | |||||
| } = useSetModalState(); | |||||
| const { uploadFile, loading } = useUploadFile(); | |||||
| const id = useGetFolderId(); | |||||
| const onFileUploadOk = useCallback( | |||||
| async (fileList: File[]): Promise<number | undefined> => { | |||||
| if (fileList.length > 0) { | |||||
| const ret: number = await uploadFile({ fileList, parentId: id }); | |||||
| if (ret === 0) { | |||||
| hideFileUploadModal(); | |||||
| } | |||||
| return ret; | |||||
| } | |||||
| }, | |||||
| [uploadFile, hideFileUploadModal, id], | |||||
| ); | |||||
| return { | |||||
| fileUploadLoading: loading, | |||||
| onFileUploadOk, | |||||
| fileUploadVisible, | |||||
| hideFileUploadModal, | |||||
| showFileUploadModal, | |||||
| }; | |||||
| }; |
| import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; | |||||
| import { RenameDialog } from '@/components/rename-dialog'; | |||||
| import { Button } from '@/components/ui/button'; | import { Button } from '@/components/ui/button'; | ||||
| import { Card, CardContent } from '@/components/ui/card'; | |||||
| import { CardSkeleton } from '@/components/ui/skeleton'; | import { CardSkeleton } from '@/components/ui/skeleton'; | ||||
| import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks'; | |||||
| import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; | ||||
| import { formatDate } from '@/utils/date'; | |||||
| import { ChevronRight, Trash2 } from 'lucide-react'; | |||||
| import { useFetchNextKnowledgeListByPage } from '@/hooks/use-knowledge-request'; | |||||
| import { DatasetCard } from '../datasets/dataset-card'; | |||||
| import { useRenameDataset } from '../datasets/use-rename-dataset'; | |||||
| export function Datasets() { | export function Datasets() { | ||||
| const { navigateToDatasetList, navigateToDataset } = useNavigatePage(); | |||||
| const { list, loading } = useFetchKnowledgeList(); | |||||
| const { navigateToDatasetList } = useNavigatePage(); | |||||
| const { kbs, loading } = useFetchNextKnowledgeListByPage(); | |||||
| const { | |||||
| datasetRenameLoading, | |||||
| initialDatasetName, | |||||
| onDatasetRenameOk, | |||||
| datasetRenameVisible, | |||||
| hideDatasetRenameModal, | |||||
| showDatasetRenameModal, | |||||
| } = useRenameDataset(); | |||||
| return ( | return ( | ||||
| <section> | <section> | ||||
| </div> | </div> | ||||
| ) : ( | ) : ( | ||||
| <div className="flex gap-4 flex-1"> | <div className="flex gap-4 flex-1"> | ||||
| {list.slice(0, 3).map((dataset) => ( | |||||
| <Card | |||||
| {kbs.slice(0, 4).map((dataset) => ( | |||||
| <DatasetCard | |||||
| key={dataset.id} | key={dataset.id} | ||||
| className="bg-colors-background-inverse-weak flex-1 border-colors-outline-neutral-standard max-w-96" | |||||
| > | |||||
| <CardContent className="p-4"> | |||||
| <div className="flex justify-between mb-4"> | |||||
| {dataset.avatar ? ( | |||||
| <div | |||||
| className="w-[70px] h-[70px] rounded-xl bg-cover" | |||||
| style={{ backgroundImage: `url(${dataset.avatar})` }} | |||||
| /> | |||||
| ) : ( | |||||
| <Avatar> | |||||
| <AvatarImage src="https://github.com/shadcn.png" /> | |||||
| <AvatarFallback>CN</AvatarFallback> | |||||
| </Avatar> | |||||
| )} | |||||
| <Button variant="ghost" size="icon"> | |||||
| <Trash2 /> | |||||
| </Button> | |||||
| </div> | |||||
| <div className="flex justify-between items-end"> | |||||
| <div> | |||||
| <h3 className="text-lg font-semibold mb-2"> | |||||
| {dataset.name} | |||||
| </h3> | |||||
| <div className="text-sm opacity-80"> | |||||
| {dataset.doc_num} files | |||||
| </div> | |||||
| <p className="text-sm opacity-80"> | |||||
| Created {formatDate(dataset.update_time)} | |||||
| </p> | |||||
| </div> | |||||
| <Button | |||||
| variant="icon" | |||||
| size="icon" | |||||
| onClick={navigateToDataset(dataset.id)} | |||||
| > | |||||
| <ChevronRight className="h-6 w-6" /> | |||||
| </Button> | |||||
| </div> | |||||
| </CardContent> | |||||
| </Card> | |||||
| dataset={dataset} | |||||
| showDatasetRenameModal={showDatasetRenameModal} | |||||
| ></DatasetCard> | |||||
| ))} | ))} | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| See all | See all | ||||
| </Button> | </Button> | ||||
| </div> | </div> | ||||
| {datasetRenameVisible && ( | |||||
| <RenameDialog | |||||
| hideModal={hideDatasetRenameModal} | |||||
| onOk={onDatasetRenameOk} | |||||
| initialName={initialDatasetName} | |||||
| loading={datasetRenameLoading} | |||||
| ></RenameDialog> | |||||
| )} | |||||
| </section> | </section> | ||||
| ); | ); | ||||
| } | } |