### What problem does this PR solve? fix: Fetch chunk list by @tanstack/react-query #1306 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)tags/v0.9.0
| @@ -1,12 +1,17 @@ | |||
| import { useCallback } from 'react'; | |||
| import { ResponseGetType } from '@/interfaces/database/base'; | |||
| import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge'; | |||
| import kbService from '@/services/knowledge-service'; | |||
| import { useQuery, useQueryClient } from '@tanstack/react-query'; | |||
| import { useDebounce } from 'ahooks'; | |||
| import { PaginationProps } from 'antd'; | |||
| import { useCallback, useState } from 'react'; | |||
| import { useDispatch } from 'umi'; | |||
| import { | |||
| useGetPaginationWithRouter, | |||
| useHandleSearchChange, | |||
| } from './logic-hooks'; | |||
| import { useGetKnowledgeSearchParams } from './route-hook'; | |||
| interface PayloadType { | |||
| doc_id: string; | |||
| keywords?: string; | |||
| } | |||
| export const useFetchChunkList = () => { | |||
| const dispatch = useDispatch(); | |||
| const { documentId } = useGetKnowledgeSearchParams(); | |||
| @@ -22,3 +27,104 @@ export const useFetchChunkList = () => { | |||
| return fetchChunkList; | |||
| }; | |||
| export interface IChunkListResult { | |||
| searchString?: string; | |||
| handleInputChange?: React.ChangeEventHandler<HTMLInputElement>; | |||
| pagination: PaginationProps; | |||
| setPagination?: (pagination: { page: number; pageSize: number }) => void; | |||
| available: number | undefined; | |||
| handleSetAvailable: (available: number | undefined) => void; | |||
| } | |||
| export const useFetchNextChunkList = (): ResponseGetType<{ | |||
| data: IChunk[]; | |||
| total: number; | |||
| documentInfo: IKnowledgeFile; | |||
| }> & | |||
| IChunkListResult => { | |||
| const { pagination, setPagination } = useGetPaginationWithRouter(); | |||
| const { documentId } = useGetKnowledgeSearchParams(); | |||
| const { searchString, handleInputChange } = useHandleSearchChange(); | |||
| const [available, setAvailable] = useState<number | undefined>(); | |||
| const debouncedSearchString = useDebounce(searchString, { wait: 500 }); | |||
| const { data, isFetching: loading } = useQuery({ | |||
| queryKey: [ | |||
| 'fetchChunkList', | |||
| documentId, | |||
| pagination.current, | |||
| pagination.pageSize, | |||
| debouncedSearchString, | |||
| available, | |||
| ], | |||
| initialData: { data: [], total: 0, documentInfo: {} }, | |||
| // placeholderData: keepPreviousData, | |||
| gcTime: 0, | |||
| queryFn: async () => { | |||
| const { data } = await kbService.chunk_list({ | |||
| doc_id: documentId, | |||
| page: pagination.current, | |||
| size: pagination.pageSize, | |||
| available_int: available, | |||
| keywords: searchString, | |||
| }); | |||
| if (data.retcode === 0) { | |||
| const res = data.data; | |||
| return { | |||
| data: res.chunks, | |||
| total: res.total, | |||
| documentInfo: res.doc, | |||
| }; | |||
| } | |||
| return ( | |||
| data?.data ?? { | |||
| data: [], | |||
| total: 0, | |||
| documentInfo: {}, | |||
| } | |||
| ); | |||
| }, | |||
| }); | |||
| const onInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback( | |||
| (e) => { | |||
| setPagination({ page: 1 }); | |||
| handleInputChange(e); | |||
| }, | |||
| [handleInputChange, setPagination], | |||
| ); | |||
| const handleSetAvailable = useCallback( | |||
| (a: number | undefined) => { | |||
| setPagination({ page: 1 }); | |||
| setAvailable(a); | |||
| }, | |||
| [setAvailable, setPagination], | |||
| ); | |||
| return { | |||
| data, | |||
| loading, | |||
| pagination, | |||
| setPagination, | |||
| searchString, | |||
| handleInputChange: onInputChange, | |||
| available, | |||
| handleSetAvailable, | |||
| }; | |||
| }; | |||
| export const useSelectChunkList = () => { | |||
| const queryClient = useQueryClient(); | |||
| const data = queryClient.getQueriesData<{ | |||
| data: IChunk[]; | |||
| total: number; | |||
| documentInfo: IKnowledgeFile; | |||
| }>({ queryKey: ['fetchChunkList'] }); | |||
| // console.log('🚀 ~ useSelectChunkList ~ data:', data); | |||
| return data?.at(-1)?.[1]; | |||
| }; | |||
| @@ -17,8 +17,9 @@ | |||
| .contentEllipsis { | |||
| .multipleLineEllipsis(3); | |||
| } | |||
| .contentText { | |||
| word-break: break-all; | |||
| word-break: break-all !important; | |||
| } | |||
| .chunkCard { | |||
| @@ -1,5 +1,6 @@ | |||
| import { ReactComponent as FilterIcon } from '@/assets/filter.svg'; | |||
| import { KnowledgeRouteKey } from '@/constants/knowledge'; | |||
| import { IChunkListResult, useSelectChunkList } from '@/hooks/chunk-hooks'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { useKnowledgeBaseId } from '@/hooks/knowledge-hooks'; | |||
| import { | |||
| @@ -27,16 +28,18 @@ import { | |||
| Space, | |||
| Typography, | |||
| } from 'antd'; | |||
| import { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; | |||
| import { Link, useDispatch, useSelector } from 'umi'; | |||
| import { useCallback, useMemo, useState } from 'react'; | |||
| import { Link } from 'umi'; | |||
| import { ChunkTextMode } from '../../constant'; | |||
| import { ChunkModelState } from '../../model'; | |||
| const { Text } = Typography; | |||
| interface IProps { | |||
| interface IProps | |||
| extends Pick< | |||
| IChunkListResult, | |||
| 'searchString' | 'handleInputChange' | 'available' | 'handleSetAvailable' | |||
| > { | |||
| checked: boolean; | |||
| getChunkList: () => void; | |||
| selectAllChunk: (checked: boolean) => void; | |||
| createChunk: () => void; | |||
| removeChunk: () => void; | |||
| @@ -45,17 +48,19 @@ interface IProps { | |||
| } | |||
| const ChunkToolBar = ({ | |||
| getChunkList, | |||
| selectAllChunk, | |||
| checked, | |||
| createChunk, | |||
| removeChunk, | |||
| switchChunk, | |||
| changeChunkTextMode, | |||
| available, | |||
| handleSetAvailable, | |||
| searchString, | |||
| handleInputChange, | |||
| }: IProps) => { | |||
| const { documentInfo, available, searchString }: ChunkModelState = | |||
| useSelector((state: any) => state.chunkModel); | |||
| const dispatch = useDispatch(); | |||
| const data = useSelectChunkList(); | |||
| const documentInfo = data?.documentInfo; | |||
| const knowledgeBaseId = useKnowledgeBaseId(); | |||
| const [isShowSearchBox, setIsShowSearchBox] = useState(false); | |||
| const { t } = useTranslate('chunk'); | |||
| @@ -71,17 +76,17 @@ const ChunkToolBar = ({ | |||
| setIsShowSearchBox(true); | |||
| }; | |||
| const handleSearchChange: ChangeEventHandler<HTMLInputElement> = (e) => { | |||
| const val = e.target.value; | |||
| dispatch({ type: 'chunkModel/setSearchString', payload: val }); | |||
| dispatch({ | |||
| type: 'chunkModel/throttledGetChunkList', | |||
| payload: documentInfo.id, | |||
| }); | |||
| }; | |||
| // const handleSearchChange: ChangeEventHandler<HTMLInputElement> = (e) => { | |||
| // const val = e.target.value; | |||
| // dispatch({ type: 'chunkModel/setSearchString', payload: val }); | |||
| // dispatch({ | |||
| // type: 'chunkModel/throttledGetChunkList', | |||
| // payload: documentInfo.id, | |||
| // }); | |||
| // }; | |||
| const handleSearchBlur = () => { | |||
| if (!searchString.trim()) { | |||
| if (!searchString?.trim()) { | |||
| setIsShowSearchBox(false); | |||
| } | |||
| }; | |||
| @@ -155,8 +160,7 @@ const ChunkToolBar = ({ | |||
| const handleFilterChange = (e: RadioChangeEvent) => { | |||
| selectAllChunk(false); | |||
| dispatch({ type: 'chunkModel/setAvailable', payload: e.target.value }); | |||
| getChunkList(); | |||
| handleSetAvailable(e.target.value); | |||
| }; | |||
| const filterContent = ( | |||
| @@ -178,8 +182,8 @@ const ChunkToolBar = ({ | |||
| <ArrowLeftOutlined /> | |||
| </Link> | |||
| <FilePdfOutlined /> | |||
| <Text ellipsis={{ tooltip: documentInfo.name }} style={{ width: 150 }}> | |||
| {documentInfo.name} | |||
| <Text ellipsis={{ tooltip: documentInfo?.name }} style={{ width: 150 }}> | |||
| {documentInfo?.name} | |||
| </Text> | |||
| </Space> | |||
| <Space> | |||
| @@ -202,7 +206,7 @@ const ChunkToolBar = ({ | |||
| placeholder={t('search')} | |||
| prefix={<SearchOutlined />} | |||
| allowClear | |||
| onChange={handleSearchChange} | |||
| onChange={handleInputChange} | |||
| onBlur={handleSearchBlur} | |||
| value={searchString} | |||
| /> | |||
| @@ -1,5 +1,5 @@ | |||
| import { Skeleton } from 'antd'; | |||
| import { useEffect, useRef } from 'react'; | |||
| import { memo, useEffect, useRef } from 'react'; | |||
| import { | |||
| AreaHighlight, | |||
| Highlight, | |||
| @@ -8,7 +8,6 @@ import { | |||
| PdfLoader, | |||
| Popup, | |||
| } from 'react-pdf-highlighter'; | |||
| import { useGetChunkHighlights } from '../../hooks'; | |||
| import { useGetDocumentUrl } from './hooks'; | |||
| import { useCatchDocumentError } from '@/components/pdf-previewer/hooks'; | |||
| @@ -16,7 +15,8 @@ import FileError from '@/pages/document-viewer/file-error'; | |||
| import styles from './index.less'; | |||
| interface IProps { | |||
| selectedChunkId: string; | |||
| highlights: IHighlight[]; | |||
| setWidthAndHeight: (width: number, height: number) => void; | |||
| } | |||
| const HighlightPopup = ({ | |||
| comment, | |||
| @@ -30,11 +30,10 @@ const HighlightPopup = ({ | |||
| ) : null; | |||
| // TODO: merge with DocumentPreviewer | |||
| const Preview = ({ selectedChunkId }: IProps) => { | |||
| const Preview = ({ highlights: state, setWidthAndHeight }: IProps) => { | |||
| const url = useGetDocumentUrl(); | |||
| useCatchDocumentError(url); | |||
| const { highlights: state, setWidthAndHeight } = | |||
| useGetChunkHighlights(selectedChunkId); | |||
| const ref = useRef<(highlight: IHighlight) => void>(() => {}); | |||
| const error = useCatchDocumentError(url); | |||
| @@ -120,4 +119,12 @@ const Preview = ({ selectedChunkId }: IProps) => { | |||
| ); | |||
| }; | |||
| export default Preview; | |||
| const compare = (oldProps: IProps, newProps: IProps) => { | |||
| const arePropsEqual = | |||
| oldProps.highlights === newProps.highlights || | |||
| (oldProps.highlights.length === 0 && newProps.highlights.length === 0); | |||
| return arePropsEqual; | |||
| }; | |||
| export default memo(Preview); | |||
| @@ -1,24 +1,17 @@ | |||
| import { useSelectChunkList } from '@/hooks/chunk-hooks'; | |||
| import { useOneNamespaceEffectsLoading } from '@/hooks/store-hooks'; | |||
| import { IChunk, IKnowledgeFile } from '@/interfaces/database/knowledge'; | |||
| import { IChunk } from '@/interfaces/database/knowledge'; | |||
| import { buildChunkHighlights } from '@/utils/document-util'; | |||
| import { useCallback, useMemo, useState } from 'react'; | |||
| import { IHighlight } from 'react-pdf-highlighter'; | |||
| import { useSelector } from 'umi'; | |||
| import { ChunkTextMode } from './constant'; | |||
| export const useSelectDocumentInfo = () => { | |||
| const documentInfo: IKnowledgeFile = useSelector( | |||
| (state: any) => state.chunkModel.documentInfo, | |||
| ); | |||
| return documentInfo; | |||
| }; | |||
| export const useSelectChunkList = () => { | |||
| const chunkList: IChunk[] = useSelector( | |||
| (state: any) => state.chunkModel.data, | |||
| ); | |||
| return chunkList; | |||
| }; | |||
| // export const useSelectChunkList = () => { | |||
| // const chunkList: IChunk[] = useSelector( | |||
| // (state: any) => state.chunkModel.data, | |||
| // ); | |||
| // return chunkList; | |||
| // }; | |||
| export const useHandleChunkCardClick = () => { | |||
| const [selectedChunkId, setSelectedChunkId] = useState<string>(''); | |||
| @@ -31,9 +24,9 @@ export const useHandleChunkCardClick = () => { | |||
| }; | |||
| export const useGetSelectedChunk = (selectedChunkId: string) => { | |||
| const chunkList: IChunk[] = useSelectChunkList(); | |||
| const data = useSelectChunkList(); | |||
| return ( | |||
| chunkList.find((x) => x.chunk_id === selectedChunkId) ?? ({} as IChunk) | |||
| data?.data?.find((x) => x.chunk_id === selectedChunkId) ?? ({} as IChunk) | |||
| ); | |||
| }; | |||
| @@ -45,14 +38,14 @@ export const useGetChunkHighlights = (selectedChunkId: string) => { | |||
| return buildChunkHighlights(selectedChunk, size); | |||
| }, [selectedChunk, size]); | |||
| const setWidthAndHeight = (width: number, height: number) => { | |||
| const setWidthAndHeight = useCallback((width: number, height: number) => { | |||
| setSize((pre) => { | |||
| if (pre.height !== height || pre.width !== width) { | |||
| return { height, width }; | |||
| } | |||
| return pre; | |||
| }); | |||
| }; | |||
| }, []); | |||
| return { highlights, setWidthAndHeight }; | |||
| }; | |||
| @@ -1,45 +1,46 @@ | |||
| import { useFetchChunkList } from '@/hooks/chunk-hooks'; | |||
| import { useFetchNextChunkList } from '@/hooks/chunk-hooks'; | |||
| import { useDeleteChunkByIds } from '@/hooks/knowledge-hooks'; | |||
| import type { PaginationProps } from 'antd'; | |||
| import { Divider, Flex, Pagination, Space, Spin, message } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| import { useDispatch, useSearchParams, useSelector } from 'umi'; | |||
| import { useDispatch, useSearchParams } from 'umi'; | |||
| import ChunkCard from './components/chunk-card'; | |||
| import CreatingModal from './components/chunk-creating-modal'; | |||
| import ChunkToolBar from './components/chunk-toolbar'; | |||
| import DocumentPreview from './components/document-preview/preview'; | |||
| import { | |||
| useChangeChunkTextMode, | |||
| useGetChunkHighlights, | |||
| useHandleChunkCardClick, | |||
| useSelectChunkListLoading, | |||
| useSelectDocumentInfo, | |||
| } from './hooks'; | |||
| import { ChunkModelState } from './model'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import styles from './index.less'; | |||
| const Chunk = () => { | |||
| const dispatch = useDispatch(); | |||
| const chunkModel: ChunkModelState = useSelector( | |||
| (state: any) => state.chunkModel, | |||
| ); | |||
| const [selectedChunkIds, setSelectedChunkIds] = useState<string[]>([]); | |||
| const [searchParams] = useSearchParams(); | |||
| const { data = [], total, pagination } = chunkModel; | |||
| const loading = useSelectChunkListLoading(); | |||
| // const loading = useSelectChunkListLoading(); | |||
| const documentId: string = searchParams.get('doc_id') || ''; | |||
| const [chunkId, setChunkId] = useState<string | undefined>(); | |||
| const { removeChunk } = useDeleteChunkByIds(); | |||
| const documentInfo = useSelectDocumentInfo(); | |||
| const { | |||
| data: { documentInfo, data = [], total }, | |||
| pagination, | |||
| loading, | |||
| searchString, | |||
| handleInputChange, | |||
| available, | |||
| handleSetAvailable, | |||
| } = useFetchNextChunkList(); | |||
| const { handleChunkCardClick, selectedChunkId } = useHandleChunkCardClick(); | |||
| const isPdf = documentInfo.type === 'pdf'; | |||
| const isPdf = documentInfo?.type === 'pdf'; | |||
| const { t } = useTranslation(); | |||
| const { changeChunkTextMode, textMode } = useChangeChunkTextMode(); | |||
| const getChunkList = useFetchChunkList(); | |||
| const handleEditChunk = useCallback( | |||
| (chunk_id?: string) => { | |||
| setChunkId(chunk_id); | |||
| @@ -57,14 +58,8 @@ const Chunk = () => { | |||
| size, | |||
| ) => { | |||
| setSelectedChunkIds([]); | |||
| dispatch({ | |||
| type: 'chunkModel/setPagination', | |||
| payload: { | |||
| current: page, | |||
| pageSize: size, | |||
| }, | |||
| }); | |||
| getChunkList(); | |||
| pagination.onChange?.(page, size); | |||
| // getChunkList(); | |||
| }; | |||
| const selectAllChunk = useCallback( | |||
| @@ -125,38 +120,44 @@ const Chunk = () => { | |||
| }, | |||
| }); | |||
| if (!chunkIds && resCode === 0) { | |||
| getChunkList(); | |||
| // getChunkList(); | |||
| } | |||
| }, | |||
| [ | |||
| dispatch, | |||
| documentId, | |||
| getChunkList, | |||
| // getChunkList, | |||
| selectedChunkIds, | |||
| showSelectedChunkWarning, | |||
| ], | |||
| ); | |||
| const { highlights, setWidthAndHeight } = | |||
| useGetChunkHighlights(selectedChunkId); | |||
| useEffect(() => { | |||
| getChunkList(); | |||
| // getChunkList(); | |||
| return () => { | |||
| dispatch({ | |||
| type: 'chunkModel/resetFilter', // TODO: need to reset state uniformly | |||
| }); | |||
| }; | |||
| }, [dispatch, getChunkList]); | |||
| }, [dispatch]); | |||
| return ( | |||
| <> | |||
| <div className={styles.chunkPage}> | |||
| <ChunkToolBar | |||
| getChunkList={getChunkList} | |||
| selectAllChunk={selectAllChunk} | |||
| createChunk={handleEditChunk} | |||
| removeChunk={handleRemoveChunk} | |||
| checked={selectedChunkIds.length === data.length} | |||
| switchChunk={switchChunk} | |||
| changeChunkTextMode={changeChunkTextMode} | |||
| searchString={searchString} | |||
| handleInputChange={handleInputChange} | |||
| available={available} | |||
| handleSetAvailable={handleSetAvailable} | |||
| ></ChunkToolBar> | |||
| <Divider></Divider> | |||
| <Flex flex={1} gap={'middle'}> | |||
| @@ -193,33 +194,22 @@ const Chunk = () => { | |||
| </Spin> | |||
| <div className={styles.pageFooter}> | |||
| <Pagination | |||
| responsive | |||
| showLessItems | |||
| showQuickJumper | |||
| showSizeChanger | |||
| onChange={onPaginationChange} | |||
| pageSize={pagination.pageSize} | |||
| pageSizeOptions={[10, 30, 60, 90]} | |||
| current={pagination.current} | |||
| size={'small'} | |||
| {...pagination} | |||
| total={total} | |||
| showTotal={(total) => ( | |||
| <Space> | |||
| {t('total', { keyPrefix: 'common' })} | |||
| {total} | |||
| </Space> | |||
| )} | |||
| size={'small'} | |||
| onChange={onPaginationChange} | |||
| /> | |||
| </div> | |||
| </Flex> | |||
| {isPdf && ( | |||
| { | |||
| <section className={styles.documentPreview}> | |||
| <DocumentPreview | |||
| selectedChunkId={selectedChunkId} | |||
| highlights={highlights} | |||
| setWidthAndHeight={setWidthAndHeight} | |||
| ></DocumentPreview> | |||
| </section> | |||
| )} | |||
| } | |||
| </Flex> | |||
| </div> | |||
| <CreatingModal doc_id={documentId} chunkId={chunkId} /> | |||
| @@ -0,0 +1,33 @@ | |||
| import { memo, useState } from 'react'; | |||
| function compare(oldProps, newProps) { | |||
| return true; | |||
| } | |||
| const Greeting = memo(function Greeting({ name }) { | |||
| console.log('Greeting was rendered at', new Date().toLocaleTimeString()); | |||
| return ( | |||
| <h3> | |||
| Hello{name && ', '} | |||
| {name}! | |||
| </h3> | |||
| ); | |||
| }, compare); | |||
| export default function MyApp() { | |||
| const [name, setName] = useState(''); | |||
| const [address, setAddress] = useState(''); | |||
| return ( | |||
| <> | |||
| <label> | |||
| Name{': '} | |||
| <input value={name} onChange={(e) => setName(e.target.value)} /> | |||
| </label> | |||
| <label> | |||
| Address{': '} | |||
| <input value={address} onChange={(e) => setAddress(e.target.value)} /> | |||
| </label> | |||
| <Greeting name={name} /> | |||
| </> | |||
| ); | |||
| } | |||
| @@ -98,6 +98,11 @@ const routes = [ | |||
| component: '@/pages/document-viewer', | |||
| layout: false, | |||
| }, | |||
| { | |||
| path: 'force', | |||
| component: '@/pages/force-graph', | |||
| layout: false, | |||
| }, | |||
| { | |||
| path: '/*', | |||
| component: '@/pages/404', | |||