### What problem does this PR solve? feat: Display mindmap in drawer #2247 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.11.0
| @@ -15,9 +15,6 @@ server { | |||
| include proxy.conf; | |||
| } | |||
| location /HPImageArchive { | |||
| proxy_pass https://cn.bing.com; | |||
| } | |||
| location / { | |||
| index index.html; | |||
| @@ -1,4 +1,5 @@ | |||
| import { useFetchMindMap, useFetchRelatedQuestions } from '@/hooks/chat-hooks'; | |||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { useTestChunkRetrieval } from '@/hooks/knowledge-hooks'; | |||
| import { | |||
| useGetPaginationWithRouter, | |||
| @@ -7,7 +8,13 @@ import { | |||
| import { IAnswer } from '@/interfaces/database/chat'; | |||
| import api from '@/utils/api'; | |||
| import { get, isEmpty, trim } from 'lodash'; | |||
| import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; | |||
| import { | |||
| ChangeEventHandler, | |||
| useCallback, | |||
| useEffect, | |||
| useRef, | |||
| useState, | |||
| } from 'react'; | |||
| export const useSendQuestion = (kbIds: string[]) => { | |||
| const { send, answer, done } = useSendMessageWithSse(api.ask); | |||
| @@ -16,11 +23,6 @@ export const useSendQuestion = (kbIds: string[]) => { | |||
| const [currentAnswer, setCurrentAnswer] = useState({} as IAnswer); | |||
| const { fetchRelatedQuestions, data: relatedQuestions } = | |||
| useFetchRelatedQuestions(); | |||
| const { | |||
| fetchMindMap, | |||
| data: mindMap, | |||
| loading: mindMapLoading, | |||
| } = useFetchMindMap(); | |||
| const [searchStr, setSearchStr] = useState<string>(''); | |||
| const [isFirstRender, setIsFirstRender] = useState(true); | |||
| const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]); | |||
| @@ -43,10 +45,7 @@ export const useSendQuestion = (kbIds: string[]) => { | |||
| page: 1, | |||
| size: pagination.pageSize, | |||
| }); | |||
| fetchMindMap({ | |||
| question: q, | |||
| kb_ids: kbIds, | |||
| }); | |||
| fetchRelatedQuestions(q); | |||
| }, | |||
| [ | |||
| @@ -54,7 +53,6 @@ export const useSendQuestion = (kbIds: string[]) => { | |||
| testChunk, | |||
| kbIds, | |||
| fetchRelatedQuestions, | |||
| fetchMindMap, | |||
| setPagination, | |||
| pagination.pageSize, | |||
| ], | |||
| @@ -117,11 +115,10 @@ export const useSendQuestion = (kbIds: string[]) => { | |||
| sendingLoading, | |||
| answer: currentAnswer, | |||
| relatedQuestions: relatedQuestions?.slice(0, 5) ?? [], | |||
| mindMap, | |||
| mindMapLoading, | |||
| searchStr, | |||
| isFirstRender, | |||
| selectedDocumentIds, | |||
| isSearchStrEmpty: isEmpty(trim(searchStr)), | |||
| }; | |||
| }; | |||
| @@ -191,3 +188,51 @@ export const useTestRetrieval = ( | |||
| setSelectedDocumentIds, | |||
| }; | |||
| }; | |||
| export const useShowMindMapDrawer = (kbIds: string[], question: string) => { | |||
| const { visible, showModal, hideModal } = useSetModalState(); | |||
| const { | |||
| fetchMindMap, | |||
| data: mindMap, | |||
| loading: mindMapLoading, | |||
| } = useFetchMindMap(); | |||
| const handleShowModal = useCallback(() => { | |||
| fetchMindMap({ question: trim(question), kb_ids: kbIds }); | |||
| showModal(); | |||
| }, [fetchMindMap, showModal, question, kbIds]); | |||
| return { | |||
| mindMap, | |||
| mindMapVisible: visible, | |||
| mindMapLoading, | |||
| showMindMapModal: handleShowModal, | |||
| hideMindMapModal: hideModal, | |||
| }; | |||
| }; | |||
| export const usePendingMindMap = () => { | |||
| const [count, setCount] = useState<number>(0); | |||
| const ref = useRef<NodeJS.Timeout>(); | |||
| const setCountInterval = useCallback(() => { | |||
| ref.current = setInterval(() => { | |||
| setCount((pre) => { | |||
| if (pre > 40) { | |||
| clearInterval(ref?.current); | |||
| } | |||
| return pre + 1; | |||
| }); | |||
| }, 1000); | |||
| }, []); | |||
| useEffect(() => { | |||
| setCountInterval(); | |||
| return () => { | |||
| clearInterval(ref?.current); | |||
| }; | |||
| }, [setCountInterval]); | |||
| return Number(((count / 43) * 100).toFixed(0)); | |||
| }; | |||
| @@ -16,17 +16,19 @@ | |||
| cursor: pointer; | |||
| } | |||
| .mainLayout { | |||
| background: transparent; | |||
| } | |||
| // .mainLayout { | |||
| // background: transparent; | |||
| // } | |||
| } | |||
| .transparentSearchSide { | |||
| background-color: rgb(251 251 251 / 88%) !important; | |||
| } | |||
| // .transparentSearchSide { | |||
| // background-color: rgb(251 251 251 / 88%) !important; | |||
| // } | |||
| .searchSide { | |||
| position: relative; | |||
| max-width: 400px !important; | |||
| min-width: auto !important; | |||
| :global(.ant-layout-sider-children) { | |||
| height: auto; | |||
| @@ -45,19 +47,19 @@ | |||
| .list { | |||
| padding-top: 10px; | |||
| width: 100%; | |||
| // height: 100%; | |||
| height: calc(100vh - 76px); | |||
| overflow: auto; | |||
| background-color: transparent; | |||
| &::-webkit-scrollbar-track { | |||
| background: transparent; | |||
| } | |||
| // background-color: transparent; | |||
| // &::-webkit-scrollbar-track { | |||
| // background: transparent; | |||
| // } | |||
| } | |||
| .checkbox { | |||
| width: 100%; | |||
| } | |||
| .knowledgeName { | |||
| width: 116px; | |||
| max-width: 270px; | |||
| } | |||
| .embeddingId { | |||
| width: 170px; | |||
| @@ -70,27 +72,17 @@ | |||
| .content { | |||
| height: 100%; | |||
| overflow: auto; | |||
| width: 100%; | |||
| padding: 20px 16% 10px; | |||
| .hide { | |||
| display: none; | |||
| } | |||
| .mainMixin() { | |||
| overflow: auto; | |||
| padding: 20px 10px 10px; | |||
| } | |||
| .largeMain { | |||
| width: 100%; | |||
| .mainMixin(); | |||
| } | |||
| .main { | |||
| width: 60%; | |||
| .mainMixin(); | |||
| } | |||
| .graph { | |||
| width: 40%; | |||
| padding: 20px 10px 10px; | |||
| margin: 0 auto; | |||
| width: 100%; | |||
| max-width: 1200px; | |||
| } | |||
| .highlightContent { | |||
| @@ -103,6 +95,9 @@ | |||
| .documentReference { | |||
| cursor: pointer; | |||
| } | |||
| .pagination { | |||
| padding-bottom: 16px; | |||
| } | |||
| } | |||
| .answerWrapper { | |||
| margin-top: 16px; | |||
| @@ -122,9 +117,9 @@ | |||
| border-start-start-radius: 30px !important; | |||
| border-end-start-radius: 30px !important; | |||
| } | |||
| :global(.ant-input-group-addon) { | |||
| background-color: transparent; | |||
| } | |||
| // :global(.ant-input-group-addon) { | |||
| // background-color: transparent; | |||
| // } | |||
| input { | |||
| height: 40px; | |||
| } | |||
| @@ -138,7 +133,7 @@ | |||
| .globalInput { | |||
| width: 600px; | |||
| position: sticky; | |||
| top: 0; | |||
| top: 30%; | |||
| z-index: 1; | |||
| .input(); | |||
| } | |||
| @@ -187,3 +182,12 @@ | |||
| max-height: 40vh; | |||
| overflow: auto; | |||
| } | |||
| .mindMapFloatButton { | |||
| top: 20%; | |||
| width: 60px; | |||
| height: 60px; | |||
| :global(.ant-float-btn-content, .ant-float-btn-icon) { | |||
| width: auto !important; | |||
| } | |||
| } | |||
| @@ -1,10 +1,10 @@ | |||
| import FileIcon from '@/components/file-icon'; | |||
| import HightLightMarkdown from '@/components/highlight-markdown'; | |||
| import { ImageWithPopover } from '@/components/image'; | |||
| import IndentedTree from '@/components/indented-tree/indented-tree'; | |||
| import PdfDrawer from '@/components/pdf-drawer'; | |||
| import { useClickDrawer } from '@/components/pdf-drawer/hooks'; | |||
| import RetrievalDocuments from '@/components/retrieval-documents'; | |||
| import SvgIcon from '@/components/svg-icon'; | |||
| import { | |||
| useNextFetchKnowledgeList, | |||
| useSelectTestingResult, | |||
| @@ -15,6 +15,7 @@ import { | |||
| Card, | |||
| Divider, | |||
| Flex, | |||
| FloatButton, | |||
| Input, | |||
| Layout, | |||
| List, | |||
| @@ -25,14 +26,16 @@ import { | |||
| Space, | |||
| Spin, | |||
| Tag, | |||
| Tooltip, | |||
| } from 'antd'; | |||
| import DOMPurify from 'dompurify'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { useMemo, useState } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import MarkdownContent from '../chat/markdown-content'; | |||
| import { useFetchBackgroundImage, useSendQuestion } from './hooks'; | |||
| import { useSendQuestion, useShowMindMapDrawer } from './hooks'; | |||
| import styles from './index.less'; | |||
| import MindMapDrawer from './mindmap-drawer'; | |||
| import SearchSidebar from './sidebar'; | |||
| const { Content } = Layout; | |||
| @@ -56,29 +59,28 @@ const SearchPage = () => { | |||
| answer, | |||
| sendingLoading, | |||
| relatedQuestions, | |||
| mindMap, | |||
| searchStr, | |||
| loading, | |||
| isFirstRender, | |||
| selectedDocumentIds, | |||
| isSearchStrEmpty, | |||
| } = useSendQuestion(checkedWithoutEmbeddingIdList); | |||
| const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | |||
| useClickDrawer(); | |||
| const imgUrl = useFetchBackgroundImage(); | |||
| const { pagination } = useGetPaginationWithRouter(); | |||
| const { | |||
| mindMapVisible, | |||
| hideMindMapModal, | |||
| showMindMapModal, | |||
| mindMapLoading, | |||
| mindMap, | |||
| } = useShowMindMapDrawer(checkedWithoutEmbeddingIdList, searchStr); | |||
| const onChange: PaginationProps['onChange'] = (pageNumber, pageSize) => { | |||
| pagination.onChange?.(pageNumber, pageSize); | |||
| handleTestChunk(selectedDocumentIds, pageNumber, pageSize); | |||
| }; | |||
| const isMindMapEmpty = useMemo(() => { | |||
| return ( | |||
| (Array.isArray(mindMap?.children) && mindMap.children.length === 0) || | |||
| !Array.isArray(mindMap?.children) | |||
| ); | |||
| }, [mindMap]); | |||
| const InputSearch = ( | |||
| <Search | |||
| value={searchStr} | |||
| @@ -96,10 +98,7 @@ const SearchPage = () => { | |||
| return ( | |||
| <> | |||
| <Layout | |||
| className={styles.searchPage} | |||
| style={{ backgroundImage: `url(${imgUrl})` }} | |||
| > | |||
| <Layout className={styles.searchPage}> | |||
| <SearchSidebar | |||
| isFirstRender={isFirstRender} | |||
| checkedList={checkedWithoutEmbeddingIdList} | |||
| @@ -108,20 +107,14 @@ const SearchPage = () => { | |||
| <Layout className={isFirstRender ? styles.mainLayout : ''}> | |||
| <Content> | |||
| {isFirstRender ? ( | |||
| <Flex | |||
| justify="center" | |||
| align="center" | |||
| className={styles.firstRenderContent} | |||
| > | |||
| <Flex justify="center" className={styles.firstRenderContent}> | |||
| <Flex vertical align="center" gap={'large'}> | |||
| {InputSearch} | |||
| </Flex> | |||
| </Flex> | |||
| ) : ( | |||
| <Flex className={styles.content}> | |||
| <section | |||
| className={isMindMapEmpty ? styles.largeMain : styles.main} | |||
| > | |||
| <section className={styles.main}> | |||
| {InputSearch} | |||
| <Card | |||
| title={ | |||
| @@ -226,28 +219,43 @@ const SearchPage = () => { | |||
| {...pagination} | |||
| total={total} | |||
| onChange={onChange} | |||
| className={styles.pagination} | |||
| /> | |||
| </section> | |||
| <section | |||
| className={isMindMapEmpty ? styles.hide : styles.graph} | |||
| > | |||
| <IndentedTree | |||
| data={mindMap} | |||
| show | |||
| style={{ width: '100%', height: '100%' }} | |||
| ></IndentedTree> | |||
| </section> | |||
| </Flex> | |||
| )} | |||
| </Content> | |||
| </Layout> | |||
| </Layout> | |||
| <PdfDrawer | |||
| visible={visible} | |||
| hideModal={hideModal} | |||
| documentId={documentId} | |||
| chunk={selectedChunk} | |||
| ></PdfDrawer> | |||
| {!isFirstRender && | |||
| !isSearchStrEmpty && | |||
| !isEmpty(checkedWithoutEmbeddingIdList) && ( | |||
| <Tooltip title={t('chunk.mind')} zIndex={1}> | |||
| <FloatButton | |||
| className={styles.mindMapFloatButton} | |||
| onClick={showMindMapModal} | |||
| icon={ | |||
| <SvgIcon name="paper-clip" width={24} height={30}></SvgIcon> | |||
| } | |||
| /> | |||
| </Tooltip> | |||
| )} | |||
| {visible && ( | |||
| <PdfDrawer | |||
| visible={visible} | |||
| hideModal={hideModal} | |||
| documentId={documentId} | |||
| chunk={selectedChunk} | |||
| ></PdfDrawer> | |||
| )} | |||
| {mindMapVisible && ( | |||
| <MindMapDrawer | |||
| visible={mindMapVisible} | |||
| hideModal={hideMindMapModal} | |||
| data={mindMap} | |||
| loading={mindMapLoading} | |||
| ></MindMapDrawer> | |||
| )} | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,36 @@ | |||
| import IndentedTree from '@/components/indented-tree/indented-tree'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { Drawer, Flex, Progress } from 'antd'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { usePendingMindMap } from './hooks'; | |||
| interface IProps extends IModalProps<any> { | |||
| data: any; | |||
| } | |||
| const MindMapDrawer = ({ data, hideModal, visible, loading }: IProps) => { | |||
| const { t } = useTranslation(); | |||
| const percent = usePendingMindMap(); | |||
| return ( | |||
| <Drawer | |||
| title={t('chunk.mind')} | |||
| onClose={hideModal} | |||
| open={visible} | |||
| width={'40vw'} | |||
| > | |||
| {loading ? ( | |||
| <Flex justify="center"> | |||
| <Progress type="circle" percent={percent} size={200} /> | |||
| </Flex> | |||
| ) : ( | |||
| <IndentedTree | |||
| data={data} | |||
| show | |||
| style={{ width: '100%', height: '100%' }} | |||
| ></IndentedTree> | |||
| )} | |||
| </Drawer> | |||
| ); | |||
| }; | |||
| export default MindMapDrawer; | |||
| @@ -138,7 +138,7 @@ const SearchSidebar = ({ | |||
| [styles.transparentSearchSide]: isFirstRender, | |||
| })} | |||
| theme={'light'} | |||
| width={240} | |||
| width={'20%'} | |||
| > | |||
| <Spin spinning={loading}> | |||
| <Tree | |||