feat: Display mindmap in drawer #2247 (#2430)

### What problem does this PR solve?

feat: Display mindmap in drawer #2247

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2024-09-14 14:42:36 +08:00 committed by GitHub
parent 3044cb85fd
commit 6a0702f55f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 176 additions and 86 deletions

View File

@ -15,9 +15,6 @@ server {
include proxy.conf; include proxy.conf;
} }
location /HPImageArchive {
proxy_pass https://cn.bing.com;
}
location / { location / {
index index.html; index index.html;

View File

@ -1,4 +1,5 @@
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,
@ -7,7 +8,13 @@ import {
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);
@ -16,11 +23,6 @@ export const useSendQuestion = (kbIds: string[]) => {
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[]>([]);
@ -43,10 +45,7 @@ export const useSendQuestion = (kbIds: string[]) => {
page: 1, page: 1,
size: pagination.pageSize, size: pagination.pageSize,
}); });
fetchMindMap({
question: q,
kb_ids: kbIds,
});
fetchRelatedQuestions(q); fetchRelatedQuestions(q);
}, },
[ [
@ -54,7 +53,6 @@ export const useSendQuestion = (kbIds: string[]) => {
testChunk, testChunk,
kbIds, kbIds,
fetchRelatedQuestions, fetchRelatedQuestions,
fetchMindMap,
setPagination, setPagination,
pagination.pageSize, pagination.pageSize,
], ],
@ -117,11 +115,10 @@ export const useSendQuestion = (kbIds: string[]) => {
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)),
}; };
}; };
@ -191,3 +188,51 @@ export const useTestRetrieval = (
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));
};

View File

@ -16,17 +16,19 @@
cursor: pointer; cursor: pointer;
} }
.mainLayout { // .mainLayout {
background: transparent; // background: transparent;
} // }
} }
.transparentSearchSide { // .transparentSearchSide {
background-color: rgb(251 251 251 / 88%) !important; // 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;
@ -45,19 +47,19 @@
.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; // background-color: transparent;
&::-webkit-scrollbar-track { // &::-webkit-scrollbar-track {
background: transparent; // background: transparent;
} // }
} }
.checkbox { .checkbox {
width: 100%; width: 100%;
} }
.knowledgeName { .knowledgeName {
width: 116px; width: 116px;
max-width: 270px;
} }
.embeddingId { .embeddingId {
width: 170px; width: 170px;
@ -70,27 +72,17 @@
.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%; margin: 0 auto;
.mainMixin(); width: 100%;
} max-width: 1200px;
.graph {
width: 40%;
padding: 20px 10px 10px;
} }
.highlightContent { .highlightContent {
@ -103,6 +95,9 @@
.documentReference { .documentReference {
cursor: pointer; cursor: pointer;
} }
.pagination {
padding-bottom: 16px;
}
} }
.answerWrapper { .answerWrapper {
margin-top: 16px; margin-top: 16px;
@ -122,9 +117,9 @@
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) { // :global(.ant-input-group-addon) {
background-color: transparent; // background-color: transparent;
} // }
input { input {
height: 40px; height: 40px;
} }
@ -138,7 +133,7 @@
.globalInput { .globalInput {
width: 600px; width: 600px;
position: sticky; position: sticky;
top: 0; top: 30%;
z-index: 1; z-index: 1;
.input(); .input();
} }
@ -187,3 +182,12 @@
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;
}
}

View File

@ -1,10 +1,10 @@
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,
@ -15,6 +15,7 @@ import {
Card, Card,
Divider, Divider,
Flex, Flex,
FloatButton,
Input, Input,
Layout, Layout,
List, List,
@ -25,14 +26,16 @@ import {
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;
@ -56,29 +59,28 @@ const SearchPage = () => {
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}
@ -96,10 +98,7 @@ const SearchPage = () => {
return ( return (
<> <>
<Layout <Layout className={styles.searchPage}>
className={styles.searchPage}
style={{ backgroundImage: `url(${imgUrl})` }}
>
<SearchSidebar <SearchSidebar
isFirstRender={isFirstRender} isFirstRender={isFirstRender}
checkedList={checkedWithoutEmbeddingIdList} checkedList={checkedWithoutEmbeddingIdList}
@ -108,20 +107,14 @@ const SearchPage = () => {
<Layout className={isFirstRender ? styles.mainLayout : ''}> <Layout className={isFirstRender ? styles.mainLayout : ''}>
<Content> <Content>
{isFirstRender ? ( {isFirstRender ? (
<Flex <Flex justify="center" className={styles.firstRenderContent}>
justify="center"
align="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 <section className={styles.main}>
className={isMindMapEmpty ? styles.largeMain : styles.main}
>
{InputSearch} {InputSearch}
<Card <Card
title={ title={
@ -226,28 +219,43 @@ const SearchPage = () => {
{...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 {!isFirstRender &&
visible={visible} !isSearchStrEmpty &&
hideModal={hideModal} !isEmpty(checkedWithoutEmbeddingIdList) && (
documentId={documentId} <Tooltip title={t('chunk.mind')} zIndex={1}>
chunk={selectedChunk} <FloatButton
></PdfDrawer> 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>
)}
</> </>
); );
}; };

View File

@ -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;

View File

@ -138,7 +138,7 @@ const SearchSidebar = ({
[styles.transparentSearchSide]: isFirstRender, [styles.transparentSearchSide]: isFirstRender,
})} })}
theme={'light'} theme={'light'}
width={240} width={'20%'}
> >
<Spin spinning={loading}> <Spin spinning={loading}>
<Tree <Tree