### 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
| include proxy.conf; | include proxy.conf; | ||||
| } | } | ||||
| location /HPImageArchive { | |||||
| proxy_pass https://cn.bing.com; | |||||
| } | |||||
| location / { | location / { | ||||
| index index.html; | index index.html; |
| import { useFetchMindMap, useFetchRelatedQuestions } from '@/hooks/chat-hooks'; | import { useFetchMindMap, useFetchRelatedQuestions } from '@/hooks/chat-hooks'; | ||||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||||
| import { useTestChunkRetrieval } from '@/hooks/knowledge-hooks'; | import { useTestChunkRetrieval } from '@/hooks/knowledge-hooks'; | ||||
| import { | import { | ||||
| useGetPaginationWithRouter, | useGetPaginationWithRouter, | ||||
| import { IAnswer } from '@/interfaces/database/chat'; | import { IAnswer } from '@/interfaces/database/chat'; | ||||
| import api from '@/utils/api'; | import api from '@/utils/api'; | ||||
| import { get, isEmpty, trim } from 'lodash'; | 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[]) => { | export const useSendQuestion = (kbIds: string[]) => { | ||||
| const { send, answer, done } = useSendMessageWithSse(api.ask); | const { send, answer, done } = useSendMessageWithSse(api.ask); | ||||
| const [currentAnswer, setCurrentAnswer] = useState({} as IAnswer); | const [currentAnswer, setCurrentAnswer] = useState({} as IAnswer); | ||||
| const { fetchRelatedQuestions, data: relatedQuestions } = | const { fetchRelatedQuestions, data: relatedQuestions } = | ||||
| useFetchRelatedQuestions(); | useFetchRelatedQuestions(); | ||||
| const { | |||||
| fetchMindMap, | |||||
| data: mindMap, | |||||
| loading: mindMapLoading, | |||||
| } = useFetchMindMap(); | |||||
| const [searchStr, setSearchStr] = useState<string>(''); | const [searchStr, setSearchStr] = useState<string>(''); | ||||
| const [isFirstRender, setIsFirstRender] = useState(true); | const [isFirstRender, setIsFirstRender] = useState(true); | ||||
| const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]); | const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]); | ||||
| page: 1, | page: 1, | ||||
| size: pagination.pageSize, | size: pagination.pageSize, | ||||
| }); | }); | ||||
| fetchMindMap({ | |||||
| question: q, | |||||
| kb_ids: kbIds, | |||||
| }); | |||||
| fetchRelatedQuestions(q); | fetchRelatedQuestions(q); | ||||
| }, | }, | ||||
| [ | [ | ||||
| testChunk, | testChunk, | ||||
| kbIds, | kbIds, | ||||
| fetchRelatedQuestions, | fetchRelatedQuestions, | ||||
| fetchMindMap, | |||||
| setPagination, | setPagination, | ||||
| pagination.pageSize, | pagination.pageSize, | ||||
| ], | ], | ||||
| sendingLoading, | sendingLoading, | ||||
| answer: currentAnswer, | answer: currentAnswer, | ||||
| relatedQuestions: relatedQuestions?.slice(0, 5) ?? [], | relatedQuestions: relatedQuestions?.slice(0, 5) ?? [], | ||||
| mindMap, | |||||
| mindMapLoading, | |||||
| searchStr, | searchStr, | ||||
| isFirstRender, | isFirstRender, | ||||
| selectedDocumentIds, | selectedDocumentIds, | ||||
| isSearchStrEmpty: isEmpty(trim(searchStr)), | |||||
| }; | }; | ||||
| }; | }; | ||||
| setSelectedDocumentIds, | 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)); | |||||
| }; |
| cursor: pointer; | 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 { | .searchSide { | ||||
| position: relative; | position: relative; | ||||
| max-width: 400px !important; | |||||
| min-width: auto !important; | |||||
| :global(.ant-layout-sider-children) { | :global(.ant-layout-sider-children) { | ||||
| height: auto; | height: auto; | ||||
| .list { | .list { | ||||
| padding-top: 10px; | padding-top: 10px; | ||||
| width: 100%; | width: 100%; | ||||
| // height: 100%; | |||||
| height: calc(100vh - 76px); | height: calc(100vh - 76px); | ||||
| overflow: auto; | overflow: auto; | ||||
| background-color: transparent; | |||||
| &::-webkit-scrollbar-track { | |||||
| background: transparent; | |||||
| } | |||||
| // background-color: transparent; | |||||
| // &::-webkit-scrollbar-track { | |||||
| // background: transparent; | |||||
| // } | |||||
| } | } | ||||
| .checkbox { | .checkbox { | ||||
| width: 100%; | width: 100%; | ||||
| } | } | ||||
| .knowledgeName { | .knowledgeName { | ||||
| width: 116px; | width: 116px; | ||||
| max-width: 270px; | |||||
| } | } | ||||
| .embeddingId { | .embeddingId { | ||||
| width: 170px; | width: 170px; | ||||
| .content { | .content { | ||||
| height: 100%; | height: 100%; | ||||
| overflow: auto; | |||||
| width: 100%; | |||||
| padding: 20px 16% 10px; | |||||
| .hide { | .hide { | ||||
| display: none; | display: none; | ||||
| } | } | ||||
| .mainMixin() { | |||||
| overflow: auto; | |||||
| padding: 20px 10px 10px; | |||||
| } | |||||
| .largeMain { | |||||
| width: 100%; | |||||
| .mainMixin(); | |||||
| } | |||||
| .main { | .main { | ||||
| width: 60%; | |||||
| .mainMixin(); | |||||
| } | |||||
| .graph { | |||||
| width: 40%; | |||||
| padding: 20px 10px 10px; | |||||
| margin: 0 auto; | |||||
| width: 100%; | |||||
| max-width: 1200px; | |||||
| } | } | ||||
| .highlightContent { | .highlightContent { | ||||
| .documentReference { | .documentReference { | ||||
| cursor: pointer; | cursor: pointer; | ||||
| } | } | ||||
| .pagination { | |||||
| padding-bottom: 16px; | |||||
| } | |||||
| } | } | ||||
| .answerWrapper { | .answerWrapper { | ||||
| margin-top: 16px; | margin-top: 16px; | ||||
| border-start-start-radius: 30px !important; | border-start-start-radius: 30px !important; | ||||
| border-end-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 { | input { | ||||
| height: 40px; | height: 40px; | ||||
| } | } | ||||
| .globalInput { | .globalInput { | ||||
| width: 600px; | width: 600px; | ||||
| position: sticky; | position: sticky; | ||||
| top: 0; | |||||
| top: 30%; | |||||
| z-index: 1; | z-index: 1; | ||||
| .input(); | .input(); | ||||
| } | } | ||||
| max-height: 40vh; | max-height: 40vh; | ||||
| overflow: auto; | overflow: auto; | ||||
| } | } | ||||
| .mindMapFloatButton { | |||||
| top: 20%; | |||||
| width: 60px; | |||||
| height: 60px; | |||||
| :global(.ant-float-btn-content, .ant-float-btn-icon) { | |||||
| width: auto !important; | |||||
| } | |||||
| } |
| import FileIcon from '@/components/file-icon'; | import FileIcon from '@/components/file-icon'; | ||||
| import HightLightMarkdown from '@/components/highlight-markdown'; | import HightLightMarkdown from '@/components/highlight-markdown'; | ||||
| import { ImageWithPopover } from '@/components/image'; | import { ImageWithPopover } from '@/components/image'; | ||||
| import IndentedTree from '@/components/indented-tree/indented-tree'; | |||||
| import PdfDrawer from '@/components/pdf-drawer'; | import PdfDrawer from '@/components/pdf-drawer'; | ||||
| import { useClickDrawer } from '@/components/pdf-drawer/hooks'; | import { useClickDrawer } from '@/components/pdf-drawer/hooks'; | ||||
| import RetrievalDocuments from '@/components/retrieval-documents'; | import RetrievalDocuments from '@/components/retrieval-documents'; | ||||
| import SvgIcon from '@/components/svg-icon'; | |||||
| import { | import { | ||||
| useNextFetchKnowledgeList, | useNextFetchKnowledgeList, | ||||
| useSelectTestingResult, | useSelectTestingResult, | ||||
| Card, | Card, | ||||
| Divider, | Divider, | ||||
| Flex, | Flex, | ||||
| FloatButton, | |||||
| Input, | Input, | ||||
| Layout, | Layout, | ||||
| List, | List, | ||||
| Space, | Space, | ||||
| Spin, | Spin, | ||||
| Tag, | Tag, | ||||
| Tooltip, | |||||
| } from 'antd'; | } from 'antd'; | ||||
| import DOMPurify from 'dompurify'; | import DOMPurify from 'dompurify'; | ||||
| import { isEmpty } from 'lodash'; | import { isEmpty } from 'lodash'; | ||||
| import { useMemo, useState } from 'react'; | import { useMemo, useState } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import MarkdownContent from '../chat/markdown-content'; | import MarkdownContent from '../chat/markdown-content'; | ||||
| import { useFetchBackgroundImage, useSendQuestion } from './hooks'; | |||||
| import { useSendQuestion, useShowMindMapDrawer } from './hooks'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import MindMapDrawer from './mindmap-drawer'; | |||||
| import SearchSidebar from './sidebar'; | import SearchSidebar from './sidebar'; | ||||
| const { Content } = Layout; | const { Content } = Layout; | ||||
| answer, | answer, | ||||
| sendingLoading, | sendingLoading, | ||||
| relatedQuestions, | relatedQuestions, | ||||
| mindMap, | |||||
| searchStr, | searchStr, | ||||
| loading, | loading, | ||||
| isFirstRender, | isFirstRender, | ||||
| selectedDocumentIds, | selectedDocumentIds, | ||||
| isSearchStrEmpty, | |||||
| } = useSendQuestion(checkedWithoutEmbeddingIdList); | } = useSendQuestion(checkedWithoutEmbeddingIdList); | ||||
| const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | ||||
| useClickDrawer(); | useClickDrawer(); | ||||
| const imgUrl = useFetchBackgroundImage(); | |||||
| const { pagination } = useGetPaginationWithRouter(); | const { pagination } = useGetPaginationWithRouter(); | ||||
| const { | |||||
| mindMapVisible, | |||||
| hideMindMapModal, | |||||
| showMindMapModal, | |||||
| mindMapLoading, | |||||
| mindMap, | |||||
| } = useShowMindMapDrawer(checkedWithoutEmbeddingIdList, searchStr); | |||||
| const onChange: PaginationProps['onChange'] = (pageNumber, pageSize) => { | const onChange: PaginationProps['onChange'] = (pageNumber, pageSize) => { | ||||
| pagination.onChange?.(pageNumber, pageSize); | pagination.onChange?.(pageNumber, pageSize); | ||||
| handleTestChunk(selectedDocumentIds, 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 = ( | const InputSearch = ( | ||||
| <Search | <Search | ||||
| value={searchStr} | value={searchStr} | ||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Layout | |||||
| className={styles.searchPage} | |||||
| style={{ backgroundImage: `url(${imgUrl})` }} | |||||
| > | |||||
| <Layout className={styles.searchPage}> | |||||
| <SearchSidebar | <SearchSidebar | ||||
| isFirstRender={isFirstRender} | isFirstRender={isFirstRender} | ||||
| checkedList={checkedWithoutEmbeddingIdList} | checkedList={checkedWithoutEmbeddingIdList} | ||||
| <Layout className={isFirstRender ? styles.mainLayout : ''}> | <Layout className={isFirstRender ? styles.mainLayout : ''}> | ||||
| <Content> | <Content> | ||||
| {isFirstRender ? ( | {isFirstRender ? ( | ||||
| <Flex | |||||
| justify="center" | |||||
| align="center" | |||||
| className={styles.firstRenderContent} | |||||
| > | |||||
| <Flex justify="center" className={styles.firstRenderContent}> | |||||
| <Flex vertical align="center" gap={'large'}> | <Flex vertical align="center" gap={'large'}> | ||||
| {InputSearch} | {InputSearch} | ||||
| </Flex> | </Flex> | ||||
| </Flex> | </Flex> | ||||
| ) : ( | ) : ( | ||||
| <Flex className={styles.content}> | <Flex className={styles.content}> | ||||
| <section | |||||
| className={isMindMapEmpty ? styles.largeMain : styles.main} | |||||
| > | |||||
| <section className={styles.main}> | |||||
| {InputSearch} | {InputSearch} | ||||
| <Card | <Card | ||||
| title={ | title={ | ||||
| {...pagination} | {...pagination} | ||||
| total={total} | total={total} | ||||
| onChange={onChange} | onChange={onChange} | ||||
| className={styles.pagination} | |||||
| /> | /> | ||||
| </section> | </section> | ||||
| <section | |||||
| className={isMindMapEmpty ? styles.hide : styles.graph} | |||||
| > | |||||
| <IndentedTree | |||||
| data={mindMap} | |||||
| show | |||||
| style={{ width: '100%', height: '100%' }} | |||||
| ></IndentedTree> | |||||
| </section> | |||||
| </Flex> | </Flex> | ||||
| )} | )} | ||||
| </Content> | </Content> | ||||
| </Layout> | </Layout> | ||||
| </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> | |||||
| )} | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; |
| 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; |
| [styles.transparentSearchSide]: isFirstRender, | [styles.transparentSearchSide]: isFirstRender, | ||||
| })} | })} | ||||
| theme={'light'} | theme={'light'} | ||||
| width={240} | |||||
| width={'20%'} | |||||
| > | > | ||||
| <Spin spinning={loading}> | <Spin spinning={loading}> | ||||
| <Tree | <Tree |