feat: Add RetrievalDocuments to SearchPage #2247 (#2327)

### What problem does this PR solve?
feat: Add RetrievalDocuments to SearchPage #2247
feat: Click on the link in the reference to display the pdf drawer #2247

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2024-09-09 19:20:16 +08:00 committed by GitHub
parent 7241c73c7a
commit 42eeb38247
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 390 additions and 143 deletions

View File

@ -0,0 +1,27 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { IChunk } from '@/interfaces/database/knowledge';
import { useCallback, useState } from 'react';
export const useClickDrawer = () => {
const { visible, showModal, hideModal } = useSetModalState();
const [selectedChunk, setSelectedChunk] = useState<IChunk>({} as IChunk);
const [documentId, setDocumentId] = useState<string>('');
const clickDocumentButton = useCallback(
(documentId: string, chunk: IChunk) => {
showModal();
setSelectedChunk(chunk);
setDocumentId(documentId);
},
[showModal],
);
return {
clickDocumentButton,
visible,
showModal,
hideModal,
selectedChunk,
documentId,
};
};

View File

@ -0,0 +1,33 @@
import { IModalProps } from '@/interfaces/common';
import { IChunk } from '@/interfaces/database/knowledge';
import { Drawer } from 'antd';
import DocumentPreviewer from '../pdf-previewer';
interface IProps extends IModalProps<any> {
documentId: string;
chunk: IChunk;
}
export const PdfDrawer = ({
visible = false,
hideModal,
documentId,
chunk,
}: IProps) => {
return (
<Drawer
title="Document Previewer"
onClose={hideModal}
open={visible}
width={'50vw'}
>
<DocumentPreviewer
documentId={documentId}
chunk={chunk}
visible={visible}
></DocumentPreviewer>
</Drawer>
);
};
export default PdfDrawer;

View File

@ -0,0 +1,11 @@
.selectFilesCollapse {
:global(.ant-collapse-header) {
padding-left: 22px;
}
margin-bottom: 32px;
overflow-y: auto;
}
.selectFilesTitle {
padding-right: 10px;
}

View File

@ -0,0 +1,55 @@
import { ReactComponent as SelectedFilesCollapseIcon } from '@/assets/svg/selected-files-collapse.svg';
import { Collapse, Flex, Space } from 'antd';
import SelectFiles from './select-files';
import { useSelectTestingResult } from '@/hooks/knowledge-hooks';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import styles from './index.less';
interface IProps {
selectedDocumentIdsLength?: number;
onTesting(documentIds: string[]): void;
}
const RetrievalDocuments = ({ onTesting }: IProps) => {
const { t } = useTranslation();
const { documents } = useSelectTestingResult();
const [selectedDocumentIds, setSelectedDocumentIds] = useState<string[]>([]);
return (
<Collapse
expandIcon={() => <SelectedFilesCollapseIcon></SelectedFilesCollapseIcon>}
className={styles.selectFilesCollapse}
items={[
{
key: '1',
label: (
<Flex
justify={'space-between'}
align="center"
className={styles.selectFilesTitle}
>
<Space>
<span>
{selectedDocumentIds.length ?? 0}/{documents.length}
</span>
{t('knowledgeDetails.filesSelected')}
</Space>
</Flex>
),
children: (
<div>
<SelectFiles
setSelectedDocumentIds={setSelectedDocumentIds}
handleTesting={onTesting}
></SelectFiles>
</div>
),
},
]}
/>
);
};
export default RetrievalDocuments;

View File

@ -0,0 +1,73 @@
import NewDocumentLink from '@/components/new-document-link';
import { useTranslate } from '@/hooks/common-hooks';
import { useSelectTestingResult } from '@/hooks/knowledge-hooks';
import { ITestingDocument } from '@/interfaces/database/knowledge';
import { EyeOutlined } from '@ant-design/icons';
import { Button, Table, TableProps, Tooltip } from 'antd';
interface IProps {
handleTesting: (ids: string[]) => void;
setSelectedDocumentIds: (ids: string[]) => void;
}
const SelectFiles = ({ setSelectedDocumentIds, handleTesting }: IProps) => {
const { documents } = useSelectTestingResult();
const { t } = useTranslate('fileManager');
const columns: TableProps<ITestingDocument>['columns'] = [
{
title: 'Name',
dataIndex: 'doc_name',
key: 'doc_name',
render: (text) => <p>{text}</p>,
},
{
title: 'Hits',
dataIndex: 'count',
key: 'count',
width: 80,
},
{
title: 'View',
key: 'view',
width: 50,
render: (_, { doc_id, doc_name }) => (
<NewDocumentLink
documentName={doc_name}
documentId={doc_id}
prefix="document"
>
<Tooltip title={t('preview')}>
<Button type="text">
<EyeOutlined size={20} />
</Button>
</Tooltip>
</NewDocumentLink>
),
},
];
const rowSelection = {
onChange: (selectedRowKeys: React.Key[]) => {
handleTesting(selectedRowKeys as string[]);
setSelectedDocumentIds(selectedRowKeys as string[]);
},
getCheckboxProps: (record: ITestingDocument) => ({
disabled: record.doc_name === 'Disabled User', // Column configuration not to be checked
name: record.doc_name,
}),
};
return (
<Table
columns={columns}
dataSource={documents}
showHeader={false}
rowSelection={rowSelection}
rowKey={'doc_id'}
/>
);
};
export default SelectFiles;

View File

@ -646,7 +646,7 @@ The above is the content you need to summarize.`,
operation: 'operation', operation: 'operation',
run: 'Run', run: 'Run',
save: 'Save', save: 'Save',
title: 'Title:', title: 'ID:',
beginDescription: 'This is where the flow begins.', beginDescription: 'This is where the flow begins.',
answerDescription: `A component that serves as the interface between human and bot, receiving user inputs and displaying the agent's responses.`, answerDescription: `A component that serves as the interface between human and bot, receiving user inputs and displaying the agent's responses.`,
retrievalDescription: `A component that retrieves information from a specified knowledge base and returns 'Empty response' if no information is found. Ensure the correct knowledge base is selected.`, retrievalDescription: `A component that retrieves information from a specified knowledge base and returns 'Empty response' if no information is found. Ensure the correct knowledge base is selected.`,

View File

@ -602,7 +602,7 @@ export default {
operation: '操作', operation: '操作',
run: '運行', run: '運行',
save: '儲存', save: '儲存',
title: '標題', title: 'ID',
beginDescription: '這是流程開始的地方', beginDescription: '這是流程開始的地方',
answerDescription: `該組件用作機器人與人類之間的介面。它接收使用者的輸入並顯示機器人的計算結果。`, answerDescription: `該組件用作機器人與人類之間的介面。它接收使用者的輸入並顯示機器人的計算結果。`,

View File

@ -621,7 +621,7 @@ export default {
operation: '操作', operation: '操作',
run: '运行', run: '运行',
save: '保存', save: '保存',
title: '标题', title: 'ID',
beginDescription: '这是流程开始的地方', beginDescription: '这是流程开始的地方',
answerDescription: `该组件用作机器人与人类之间的接口。它接收用户的输入并显示机器人的计算结果。`, answerDescription: `该组件用作机器人与人类之间的接口。它接收用户的输入并显示机器人的计算结果。`,
retrievalDescription: `此组件用于从知识库中检索相关信息。选择知识库。如果没有检索到任何内容,将返回“空响应”。`, retrievalDescription: `此组件用于从知识库中检索相关信息。选择知识库。如果没有检索到任何内容,将返回“空响应”。`,

View File

@ -1,9 +1,7 @@
import MessageItem from '@/components/message-item'; import MessageItem from '@/components/message-item';
import DocumentPreviewer from '@/components/pdf-previewer';
import { MessageType } from '@/constants/chat'; import { MessageType } from '@/constants/chat';
import { Drawer, Flex, Spin } from 'antd'; import { Flex, Spin } from 'antd';
import { import {
useClickDrawer,
useCreateConversationBeforeUploadDocument, useCreateConversationBeforeUploadDocument,
useGetFileIcon, useGetFileIcon,
useGetSendButtonDisabled, useGetSendButtonDisabled,
@ -13,6 +11,8 @@ import {
import { buildMessageItemReference } from '../utils'; import { buildMessageItemReference } from '../utils';
import MessageInput from '@/components/message-input'; import MessageInput from '@/components/message-input';
import PdfDrawer from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import { import {
useFetchNextConversation, useFetchNextConversation,
useGetChatSearchParams, useGetChatSearchParams,
@ -96,18 +96,12 @@ const ChatContainer = () => {
} }
></MessageInput> ></MessageInput>
</Flex> </Flex>
<Drawer <PdfDrawer
title="Document Previewer" visible={visible}
onClose={hideModal} hideModal={hideModal}
open={visible} documentId={documentId}
width={'50vw'} chunk={selectedChunk}
> ></PdfDrawer>
<DocumentPreviewer
documentId={documentId}
chunk={selectedChunk}
visible={visible}
></DocumentPreviewer>
</Drawer>
</> </>
); );
}; };

View File

@ -23,7 +23,6 @@ import {
useSendMessageWithSse, useSendMessageWithSse,
} from '@/hooks/logic-hooks'; } from '@/hooks/logic-hooks';
import { IConversation, IDialog, Message } from '@/interfaces/database/chat'; import { IConversation, IDialog, Message } from '@/interfaces/database/chat';
import { IChunk } from '@/interfaces/database/knowledge';
import { getFileExtension } from '@/utils'; import { getFileExtension } from '@/utils';
import { useMutationState } from '@tanstack/react-query'; import { useMutationState } from '@tanstack/react-query';
import { get } from 'lodash'; import { get } from 'lodash';
@ -545,30 +544,6 @@ export const useRenameConversation = () => {
}; };
}; };
export const useClickDrawer = () => {
const { visible, showModal, hideModal } = useSetModalState();
const [selectedChunk, setSelectedChunk] = useState<IChunk>({} as IChunk);
const [documentId, setDocumentId] = useState<string>('');
const clickDocumentButton = useCallback(
(documentId: string, chunk: IChunk) => {
showModal();
setSelectedChunk(chunk);
setDocumentId(documentId);
},
[showModal],
);
return {
clickDocumentButton,
visible,
showModal,
hideModal,
selectedChunk,
documentId,
};
};
export const useGetSendButtonDisabled = () => { export const useGetSendButtonDisabled = () => {
const { dialogId, conversationId } = useGetChatSearchParams(); const { dialogId, conversationId } = useGetChatSearchParams();

View File

@ -1,13 +1,14 @@
import MessageItem from '@/components/message-item'; import MessageItem from '@/components/message-item';
import DocumentPreviewer from '@/components/pdf-previewer';
import { MessageType } from '@/constants/chat'; import { MessageType } from '@/constants/chat';
import { useTranslate } from '@/hooks/common-hooks'; import { useTranslate } from '@/hooks/common-hooks';
import { useClickDrawer, useGetFileIcon } from '@/pages/chat/hooks'; import { useGetFileIcon } from '@/pages/chat/hooks';
import { buildMessageItemReference } from '@/pages/chat/utils'; import { buildMessageItemReference } from '@/pages/chat/utils';
import { Button, Drawer, Flex, Input, Spin } from 'antd'; import { Button, Flex, Input, Spin } from 'antd';
import { useSendNextMessage } from './hooks'; import { useSendNextMessage } from './hooks';
import PdfDrawer from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import styles from './index.less'; import styles from './index.less';
@ -79,19 +80,12 @@ const FlowChatBox = () => {
onChange={handleInputChange} onChange={handleInputChange}
/> />
</Flex> </Flex>
<Drawer <PdfDrawer
title="Document Previewer" visible={visible}
onClose={hideModal} hideModal={hideModal}
open={visible} documentId={documentId}
width={'50vw'} chunk={selectedChunk}
mask={false} ></PdfDrawer>
>
<DocumentPreviewer
documentId={documentId}
chunk={selectedChunk}
visible={visible}
></DocumentPreviewer>
</Drawer>
</> </>
); );
}; };

View File

@ -46,10 +46,27 @@ export const useSendQuestion = (kbIds: string[]) => {
const handleClickRelatedQuestion = useCallback( const handleClickRelatedQuestion = useCallback(
(question: string) => () => { (question: string) => () => {
if (sendingLoading) return;
setSearchStr(question); setSearchStr(question);
sendQuestion(question); sendQuestion(question);
}, },
[sendQuestion], [sendQuestion, sendingLoading],
);
const handleTestChunk = useCallback(
(documentIds: string[]) => {
const q = trim(searchStr);
if (sendingLoading || isEmpty(q)) return;
testChunk({
kb_id: kbIds,
highlight: true,
question: q,
doc_ids: Array.isArray(documentIds) ? documentIds : [],
});
},
[sendingLoading, searchStr, kbIds, testChunk],
); );
useEffect(() => { useEffect(() => {
@ -71,6 +88,7 @@ export const useSendQuestion = (kbIds: string[]) => {
sendQuestion, sendQuestion,
handleSearchStrChange, handleSearchStrChange,
handleClickRelatedQuestion, handleClickRelatedQuestion,
handleTestChunk,
loading, loading,
sendingLoading, sendingLoading,
answer: currentAnswer, answer: currentAnswer,

View File

@ -51,6 +51,9 @@
.firstRenderContent { .firstRenderContent {
height: 100%; height: 100%;
background-image: url(https://www.bing.com/th?id=OHR.IguazuRainbow_ZH-CN6524347982_1920x1080.webp&qlt=50);
background-position: center;
background-size: cover;
} }
.content { .content {
@ -79,10 +82,13 @@
.input() { .input() {
:global(.ant-input-affix-wrapper) { :global(.ant-input-affix-wrapper) {
padding: 4px 8px; padding: 4px 12px;
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;
}
input { input {
height: 40px; height: 40px;
} }
@ -101,3 +107,35 @@
width: 100%; width: 100%;
.input(); .input();
} }
.appIcon {
display: inline-block;
vertical-align: middle;
width: 60px;
}
.appName {
vertical-align: middle;
font-family: Inter;
font-size: 40px;
font-style: normal;
font-weight: 600;
line-height: 20px;
background: linear-gradient(to right, #095fab 10%, #25abe8 50%, #57d75b 60%);
background-size: auto auto;
background-clip: border-box;
background-size: 200% auto;
color: #fff;
background-clip: text;
text-fill-color: transparent;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: textclip 1.5s linear infinite;
}
@keyframes textclip {
to {
background-position: 200% center;
}
}

View File

@ -19,18 +19,26 @@ import MarkdownContent from '../chat/markdown-content';
import { useSendQuestion } from './hooks'; import { useSendQuestion } from './hooks';
import SearchSidebar from './sidebar'; import SearchSidebar from './sidebar';
import PdfDrawer from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import RetrievalDocuments from '@/components/retrieval-documents';
import { useFetchAppConf } from '@/hooks/logic-hooks';
import { useTranslation } from 'react-i18next';
import styles from './index.less'; import styles from './index.less';
const { Content } = Layout; const { Content } = Layout;
const { Search } = Input; const { Search } = Input;
const SearchPage = () => { const SearchPage = () => {
const { t } = useTranslation();
const [checkedList, setCheckedList] = useState<string[]>([]); const [checkedList, setCheckedList] = useState<string[]>([]);
const list = useSelectTestingResult(); const list = useSelectTestingResult();
const appConf = useFetchAppConf();
const { const {
sendQuestion, sendQuestion,
handleClickRelatedQuestion, handleClickRelatedQuestion,
handleSearchStrChange, handleSearchStrChange,
handleTestChunk,
answer, answer,
sendingLoading, sendingLoading,
relatedQuestions, relatedQuestions,
@ -40,12 +48,14 @@ const SearchPage = () => {
loading, loading,
isFirstRender, isFirstRender,
} = useSendQuestion(checkedList); } = useSendQuestion(checkedList);
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();
const InputSearch = ( const InputSearch = (
<Search <Search
value={searchStr} value={searchStr}
onChange={handleSearchStrChange} onChange={handleSearchStrChange}
placeholder="input search text" placeholder={t('header.search')}
allowClear allowClear
enterButton enterButton
onSearch={sendQuestion} onSearch={sendQuestion}
@ -57,88 +67,107 @@ const SearchPage = () => {
); );
return ( return (
<Layout className={styles.searchPage}> <>
<SearchSidebar <Layout className={styles.searchPage}>
checkedList={checkedList} <SearchSidebar
setCheckedList={setCheckedList} checkedList={checkedList}
></SearchSidebar> setCheckedList={setCheckedList}
<Layout> ></SearchSidebar>
<Content> <Layout>
{isFirstRender ? ( <Content>
<Flex {isFirstRender ? (
justify="center" <Flex
align="center" justify="center"
className={styles.firstRenderContent} align="center"
> className={styles.firstRenderContent}
{InputSearch} >
</Flex> <Flex vertical align="center" gap={'large'}>
) : ( <Space size={30}>
<Flex className={styles.content}> <img src="/logo.svg" alt="" className={styles.appIcon} />
<section className={styles.main}> <span className={styles.appName}>{appConf.appName}</span>
{InputSearch} </Space>
{answer.answer && ( {InputSearch}
<div className={styles.answerWrapper}> </Flex>
<MarkdownContent </Flex>
loading={sendingLoading} ) : (
content={answer.answer} <Flex className={styles.content}>
reference={answer.reference ?? ({} as IReference)} <section className={styles.main}>
clickDocumentButton={() => {}} {InputSearch}
></MarkdownContent> {answer.answer && (
</div> <div className={styles.answerWrapper}>
)} <MarkdownContent
<Divider></Divider> loading={sendingLoading}
{list.chunks.length > 0 && ( content={answer.answer}
<List reference={answer.reference ?? ({} as IReference)}
dataSource={list.chunks} clickDocumentButton={clickDocumentButton}
loading={loading} ></MarkdownContent>
renderItem={(item) => ( </div>
<List.Item> )}
<Card className={styles.card}> <Divider></Divider>
<Space> <RetrievalDocuments
<ImageWithPopover selectedDocumentIdsLength={0}
id={item.img_id} onTesting={handleTestChunk}
></ImageWithPopover> ></RetrievalDocuments>
<HightLightMarkdown> <Divider></Divider>
{item.highlight} {list.chunks.length > 0 && (
</HightLightMarkdown> <List
</Space> dataSource={list.chunks}
</Card> loading={loading}
</List.Item> renderItem={(item) => (
)} <List.Item>
/> <Card className={styles.card}>
)} <Space>
{relatedQuestions?.length > 0 && ( <ImageWithPopover
<Card> id={item.img_id}
<Flex wrap="wrap" gap={'10px 0'}> ></ImageWithPopover>
{relatedQuestions?.map((x, idx) => ( <HightLightMarkdown>
<Tag {item.highlight}
key={idx} </HightLightMarkdown>
className={styles.tag} </Space>
onClick={handleClickRelatedQuestion(x)} </Card>
> </List.Item>
{x} )}
</Tag> />
))} )}
</Flex> {relatedQuestions?.length > 0 && (
</Card> <Card>
)} <Flex wrap="wrap" gap={'10px 0'}>
</section> {relatedQuestions?.map((x, idx) => (
<section className={styles.graph}> <Tag
{mindMapLoading ? ( key={idx}
<Skeleton active /> className={styles.tag}
) : ( onClick={handleClickRelatedQuestion(x)}
<IndentedTree >
data={mindMap} {x}
show </Tag>
style={{ width: '100%', height: '100%' }} ))}
></IndentedTree> </Flex>
)} </Card>
</section> )}
</Flex> </section>
)} <section className={styles.graph}>
</Content> {mindMapLoading ? (
<Skeleton active />
) : (
<IndentedTree
data={mindMap}
show
style={{ width: '100%', height: '100%' }}
></IndentedTree>
)}
</section>
</Flex>
)}
</Content>
</Layout>
</Layout> </Layout>
</Layout> <PdfDrawer
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
></PdfDrawer>
</>
); );
}; };