### What problem does this PR solve? fix: fix uploaded file time error #680 feat: support preview of word and excel #684 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)tags/v0.6.0
| @@ -1,22 +1,24 @@ | |||
| import { api_host } from '@/utils/api'; | |||
| import React from 'react'; | |||
| interface IProps extends React.PropsWithChildren { | |||
| documentId: string; | |||
| link: string; | |||
| preventDefault?: boolean; | |||
| color?: string; | |||
| } | |||
| const NewDocumentLink = ({ | |||
| children, | |||
| documentId, | |||
| link, | |||
| preventDefault = false, | |||
| color = 'rgb(15, 79, 170)', | |||
| }: IProps) => { | |||
| return ( | |||
| <a | |||
| target="_blank" | |||
| onClick={!preventDefault ? undefined : (e) => e.preventDefault()} | |||
| href={`${api_host}/document/get/${documentId}`} | |||
| href={link} | |||
| rel="noreferrer" | |||
| style={{ color }} | |||
| > | |||
| {children} | |||
| </a> | |||
| @@ -68,3 +68,23 @@ export const FileMimeTypeMap = { | |||
| xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | |||
| mp4: 'video/mp4', | |||
| }; | |||
| //#region file preview | |||
| export const Images = [ | |||
| 'jpg', | |||
| 'jpeg', | |||
| 'png', | |||
| 'gif', | |||
| 'bmp', | |||
| 'tif', | |||
| 'tiff', | |||
| 'webp', | |||
| // 'svg', | |||
| 'ico', | |||
| ]; | |||
| // Without FileViewer | |||
| export const ExceptiveType = ['xlsx', 'xls', 'pdf', ...Images]; | |||
| export const SupportedPreviewDocumentTypes = ['docx', 'csv', ...ExceptiveType]; | |||
| //#endregion | |||
| @@ -9,12 +9,15 @@ import { useDispatch, useSelector } from 'umi'; | |||
| import { useGetKnowledgeSearchParams } from './routeHook'; | |||
| import { useOneNamespaceEffectsLoading } from './storeHooks'; | |||
| export const useGetDocumentUrl = (documentId: string) => { | |||
| const url = useMemo(() => { | |||
| return `${api_host}/document/get/${documentId}`; | |||
| }, [documentId]); | |||
| export const useGetDocumentUrl = (documentId?: string) => { | |||
| const getDocumentUrl = useCallback( | |||
| (id?: string) => { | |||
| return `${api_host}/document/get/${documentId || id}`; | |||
| }, | |||
| [documentId], | |||
| ); | |||
| return url; | |||
| return getDocumentUrl; | |||
| }; | |||
| export const useGetChunkHighlights = (selectedChunk: IChunk) => { | |||
| @@ -505,6 +505,7 @@ export default { | |||
| 'Support for a single or bulk upload. Strictly prohibited from uploading company data or other banned files.', | |||
| local: 'Local uploads', | |||
| s3: 'S3 uploads', | |||
| preview: 'Preview', | |||
| }, | |||
| footer: { | |||
| profile: 'All rights reserved @ React', | |||
| @@ -468,6 +468,7 @@ export default { | |||
| directory: '文件夾', | |||
| local: '本地上傳', | |||
| s3: 'S3 上傳', | |||
| preview: '預覽', | |||
| }, | |||
| footer: { | |||
| profile: '“保留所有權利 @ react”', | |||
| @@ -486,6 +486,7 @@ export default { | |||
| directory: '文件夹', | |||
| local: '本地上传', | |||
| s3: 'S3 上传', | |||
| preview: '预览', | |||
| }, | |||
| footer: { | |||
| profile: 'All rights reserved @ React', | |||
| @@ -106,8 +106,8 @@ const KnowledgeFile = () => { | |||
| }, | |||
| { | |||
| title: t('uploadDate'), | |||
| dataIndex: 'create_date', | |||
| key: 'create_date', | |||
| dataIndex: 'create_time', | |||
| key: 'create_time', | |||
| render(value) { | |||
| return formatDate(value); | |||
| }, | |||
| @@ -1,5 +1,6 @@ | |||
| import { ReactComponent as NavigationPointerIcon } from '@/assets/svg/navigation-pointer.svg'; | |||
| import NewDocumentLink from '@/components/new-document-link'; | |||
| import { useGetDocumentUrl } from '@/hooks/documentHooks'; | |||
| import { ITestingDocument } from '@/interfaces/database/knowledge'; | |||
| import { isPdf } from '@/utils/documentUtils'; | |||
| import { Table, TableProps } from 'antd'; | |||
| @@ -15,6 +16,7 @@ const SelectFiles = ({ handleTesting }: IProps) => { | |||
| ); | |||
| const dispatch = useDispatch(); | |||
| const getDocumentUrl = useGetDocumentUrl(); | |||
| const columns: TableProps<ITestingDocument>['columns'] = [ | |||
| { | |||
| @@ -35,7 +37,10 @@ const SelectFiles = ({ handleTesting }: IProps) => { | |||
| key: 'view', | |||
| width: 50, | |||
| render: (_, { doc_id, doc_name }) => ( | |||
| <NewDocumentLink documentId={doc_id} preventDefault={!isPdf(doc_name)}> | |||
| <NewDocumentLink | |||
| link={getDocumentUrl(doc_id)} | |||
| preventDefault={!isPdf(doc_name)} | |||
| > | |||
| <NavigationPointerIcon /> | |||
| </NewDocumentLink> | |||
| ), | |||
| @@ -30,6 +30,7 @@ import MarkdownContent from '../markdown-content'; | |||
| import SvgIcon from '@/components/svg-icon'; | |||
| import { useTranslate } from '@/hooks/commonHooks'; | |||
| import { useGetDocumentUrl } from '@/hooks/documentHooks'; | |||
| import { getExtension, isPdf } from '@/utils/documentUtils'; | |||
| import styles from './index.less'; | |||
| @@ -44,6 +45,7 @@ const MessageItem = ({ | |||
| }) => { | |||
| const userInfo = useSelectUserInfo(); | |||
| const fileThumbnails = useSelectFileThumbnails(); | |||
| const getDocumentUrl = useGetDocumentUrl(); | |||
| const isAssistant = item.role === MessageType.Assistant; | |||
| @@ -113,7 +115,7 @@ const MessageItem = ({ | |||
| )} | |||
| <NewDocumentLink | |||
| documentId={item.doc_id} | |||
| link={getDocumentUrl(item.doc_id)} | |||
| preventDefault={!isPdf(item.doc_name)} | |||
| > | |||
| {item.doc_name} | |||
| @@ -1,8 +1,13 @@ | |||
| .viewerWrapper { | |||
| width: 100%; | |||
| height: 100%; | |||
| :global { | |||
| .pdf-canvas { | |||
| text-align: center; | |||
| } | |||
| } | |||
| .image { | |||
| width: 100%; | |||
| height: 100%; | |||
| } | |||
| } | |||
| @@ -1,10 +1,17 @@ | |||
| import { ExceptiveType, Images } from '@/constants/common'; | |||
| import { api_host } from '@/utils/api'; | |||
| import { Flex, Image } from 'antd'; | |||
| import FileViewer from 'react-file-viewer'; | |||
| import { useParams, useSearchParams } from 'umi'; | |||
| import Excel from './excel'; | |||
| import Pdf from './pdf'; | |||
| import styles from './index.less'; | |||
| // TODO: The interface returns an incorrect content-type for the SVG. | |||
| const isNotExceptiveType = (ext: string) => ExceptiveType.indexOf(ext) === -1; | |||
| const DocumentViewer = () => { | |||
| const { id: documentId } = useParams(); | |||
| const api = `${api_host}/file/get/${documentId}`; | |||
| @@ -17,8 +24,14 @@ const DocumentViewer = () => { | |||
| return ( | |||
| <section className={styles.viewerWrapper}> | |||
| {ext === 'xlsx' && <Excel filePath={api}></Excel>} | |||
| {ext !== 'xlsx' && ( | |||
| {Images.includes(ext!) && ( | |||
| <Flex className={styles.image} align="center" justify="center"> | |||
| <Image src={api} preview={false}></Image> | |||
| </Flex> | |||
| )} | |||
| {ext === 'pdf' && <Pdf url={api}></Pdf>} | |||
| {(ext === 'xlsx' || ext === 'xls') && <Excel filePath={api}></Excel>} | |||
| {isNotExceptiveType(ext!) && ( | |||
| <FileViewer fileType={ext} filePath={api} onError={onError} /> | |||
| )} | |||
| </section> | |||
| @@ -0,0 +1,38 @@ | |||
| import { Skeleton } from 'antd'; | |||
| import { PdfHighlighter, PdfLoader } from 'react-pdf-highlighter'; | |||
| interface IProps { | |||
| url: string; | |||
| } | |||
| const DocumentPreviewer = ({ url }: IProps) => { | |||
| const resetHash = () => {}; | |||
| return ( | |||
| <div style={{ width: '100%' }}> | |||
| <PdfLoader | |||
| url={url} | |||
| beforeLoad={<Skeleton active />} | |||
| workerSrc="/pdfjs-dist/pdf.worker.min.js" | |||
| > | |||
| {(pdfDocument) => { | |||
| return ( | |||
| <PdfHighlighter | |||
| pdfDocument={pdfDocument} | |||
| enableAreaSelection={(event) => event.altKey} | |||
| onScrollChange={resetHash} | |||
| scrollRef={() => {}} | |||
| onSelectionFinished={() => null} | |||
| highlightTransform={() => { | |||
| return <div></div>; | |||
| }} | |||
| highlights={[]} | |||
| /> | |||
| ); | |||
| }} | |||
| </PdfLoader> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default DocumentPreviewer; | |||
| @@ -6,13 +6,21 @@ import { | |||
| DeleteOutlined, | |||
| DownloadOutlined, | |||
| EditOutlined, | |||
| EyeOutlined, | |||
| LinkOutlined, | |||
| } from '@ant-design/icons'; | |||
| import { Button, Space, Tooltip } from 'antd'; | |||
| import { useHandleDeleteFile, useNavigateToDocument } from '../hooks'; | |||
| import { useHandleDeleteFile } from '../hooks'; | |||
| import NewDocumentLink from '@/components/new-document-link'; | |||
| import { SupportedPreviewDocumentTypes } from '@/constants/common'; | |||
| import { getExtension } from '@/utils/documentUtils'; | |||
| import styles from './index.less'; | |||
| const isSupportedPreviewDocumentType = (fileExtension: string) => { | |||
| return SupportedPreviewDocumentTypes.includes(fileExtension); | |||
| }; | |||
| interface IProps { | |||
| record: IFile; | |||
| setCurrentRecord: (record: any) => void; | |||
| @@ -35,7 +43,7 @@ const ActionCell = ({ | |||
| [documentId], | |||
| setSelectedRowKeys, | |||
| ); | |||
| const navigateToDocument = useNavigateToDocument(record.id, record.name); | |||
| const extension = getExtension(record.name); | |||
| const onDownloadDocument = () => { | |||
| downloadFile({ | |||
| @@ -59,15 +67,6 @@ const ActionCell = ({ | |||
| return ( | |||
| <Space size={0}> | |||
| {/* <Tooltip title={t('addToKnowledge')}> | |||
| <Button | |||
| type="text" | |||
| className={styles.iconButton} | |||
| onClick={navigateToDocument} | |||
| > | |||
| <EyeOutlined size={20} /> | |||
| </Button> | |||
| </Tooltip> */} | |||
| <Tooltip title={t('addToKnowledge')}> | |||
| <Button | |||
| type="text" | |||
| @@ -110,6 +109,18 @@ const ActionCell = ({ | |||
| </Button> | |||
| </Tooltip> | |||
| )} | |||
| {isSupportedPreviewDocumentType(extension) && ( | |||
| <NewDocumentLink | |||
| color="black" | |||
| link={`/document/${documentId}?ext=${extension}`} | |||
| > | |||
| <Tooltip title={t('preview')}> | |||
| <Button type="text" className={styles.iconButton}> | |||
| <EyeOutlined size={20} /> | |||
| </Button> | |||
| </Tooltip> | |||
| </NewDocumentLink> | |||
| )} | |||
| </Space> | |||
| ); | |||
| }; | |||
| @@ -13,7 +13,6 @@ import { | |||
| import { useGetPagination, useSetPagination } from '@/hooks/logicHooks'; | |||
| import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; | |||
| import { IFile } from '@/interfaces/database/file-manager'; | |||
| import { getExtension } from '@/utils/documentUtils'; | |||
| import { PaginationProps } from 'antd'; | |||
| import { UploadFile } from 'antd/lib'; | |||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | |||
| @@ -339,12 +338,3 @@ export const useHandleBreadcrumbClick = () => { | |||
| return { handleBreadcrumbClick }; | |||
| }; | |||
| export const useNavigateToDocument = (documentId: string, name: string) => { | |||
| const navigate = useNavigate(); | |||
| const navigateToDocument = () => { | |||
| navigate(`/document/${documentId}?ext=${getExtension(name)}`); | |||
| }; | |||
| return navigateToDocument; | |||
| }; | |||
| @@ -96,8 +96,8 @@ const FileManager = () => { | |||
| }, | |||
| { | |||
| title: t('uploadDate'), | |||
| dataIndex: 'create_date', | |||
| key: 'create_date', | |||
| dataIndex: 'create_time', | |||
| key: 'create_time', | |||
| render(text) { | |||
| return formatDate(text); | |||
| }, | |||
| @@ -100,7 +100,7 @@ const KnowledgeCard = ({ item }: IProps) => { | |||
| <div className={styles.bottomLeft}> | |||
| <CalendarOutlined className={styles.leftIcon} /> | |||
| <span className={styles.rightText}> | |||
| {formatDate(item.update_date)} | |||
| {formatDate(item.update_time)} | |||
| </span> | |||
| </div> | |||
| {/* <Avatar.Group size={25}> | |||
| @@ -88,12 +88,13 @@ const routes = [ | |||
| path: '/flow', | |||
| component: '@/pages/flow', | |||
| }, | |||
| { | |||
| path: 'document/:id', | |||
| component: '@/pages/document-viewer', | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| path: 'document/:id', | |||
| component: '@/pages/document-viewer', | |||
| layout: false, | |||
| }, | |||
| { | |||
| path: '/*', | |||
| component: '@/pages/404', | |||
| @@ -16,5 +16,5 @@ export function formatDate(date: any) { | |||
| if (!date) { | |||
| return ''; | |||
| } | |||
| return dayjs(date).format('DD/MM/YYYY'); | |||
| return dayjs(date).format('DD/MM/YYYY HH:mm:ss'); | |||
| } | |||