Browse Source

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)
tags/v0.11.0
balibabu 1 year ago
parent
commit
6a0702f55f
No account linked to committer's email address

+ 0
- 3
docker/nginx/ragflow.conf View File

include proxy.conf; include proxy.conf;
} }


location /HPImageArchive {
proxy_pass https://cn.bing.com;
}


location / { location / {
index index.html; index index.html;

+ 58
- 13
web/src/pages/search/hooks.ts View File

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));
};

+ 35
- 31
web/src/pages/search/index.less View File

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

+ 46
- 38
web/src/pages/search/index.tsx View File

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>
)}
</> </>
); );
}; };

+ 36
- 0
web/src/pages/search/mindmap-drawer.tsx View File

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;

+ 1
- 1
web/src/pages/search/sidebar.tsx View File

[styles.transparentSearchSide]: isFirstRender, [styles.transparentSearchSide]: isFirstRender,
})} })}
theme={'light'} theme={'light'}
width={240}
width={'20%'}
> >
<Spin spinning={loading}> <Spin spinning={loading}>
<Tree <Tree

Loading…
Cancel
Save