### What problem does this PR solve? feat: Search for the answers you want based on the selected knowledge base #2247 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.11.0
| import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg'; | import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg'; | ||||
| import { MessageType } from '@/constants/chat'; | import { MessageType } from '@/constants/chat'; | ||||
| import { useSetModalState, useTranslate } from '@/hooks/common-hooks'; | |||||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||||
| import { useSelectFileThumbnails } from '@/hooks/knowledge-hooks'; | import { useSelectFileThumbnails } from '@/hooks/knowledge-hooks'; | ||||
| import { IReference } from '@/interfaces/database/chat'; | import { IReference } from '@/interfaces/database/chat'; | ||||
| import { IChunk } from '@/interfaces/database/knowledge'; | import { IChunk } from '@/interfaces/database/knowledge'; | ||||
| }: IProps) => { | }: IProps) => { | ||||
| const isAssistant = item.role === MessageType.Assistant; | const isAssistant = item.role === MessageType.Assistant; | ||||
| const isUser = item.role === MessageType.User; | const isUser = item.role === MessageType.User; | ||||
| const { t } = useTranslate('chat'); | |||||
| const fileThumbnails = useSelectFileThumbnails(); | const fileThumbnails = useSelectFileThumbnails(); | ||||
| const { data: documentList, setDocumentIds } = useFetchDocumentInfosByIds(); | const { data: documentList, setDocumentIds } = useFetchDocumentInfosByIds(); | ||||
| const { data: documentThumbnails, setDocumentIds: setIds } = | const { data: documentThumbnails, setDocumentIds: setIds } = | ||||
| return reference?.doc_aggs ?? []; | return reference?.doc_aggs ?? []; | ||||
| }, [reference?.doc_aggs]); | }, [reference?.doc_aggs]); | ||||
| const content = useMemo(() => { | |||||
| let text = item.content; | |||||
| if (text === '') { | |||||
| text = t('searching'); | |||||
| } | |||||
| return loading ? text?.concat('~~2$$') : text; | |||||
| }, [item.content, loading, t]); | |||||
| const handleUserDocumentClick = useCallback( | const handleUserDocumentClick = useCallback( | ||||
| (id: string) => () => { | (id: string) => () => { | ||||
| setClickedDocumentId(id); | setClickedDocumentId(id); | ||||
| } | } | ||||
| > | > | ||||
| <MarkdownContent | <MarkdownContent | ||||
| content={content} | |||||
| loading={loading} | |||||
| content={item.content} | |||||
| reference={reference} | reference={reference} | ||||
| clickDocumentButton={clickDocumentButton} | clickDocumentButton={clickDocumentButton} | ||||
| ></MarkdownContent> | ></MarkdownContent> | 
| mutateAsync, | mutateAsync, | ||||
| } = useMutation({ | } = useMutation({ | ||||
| mutationKey: ['testChunk'], // This method is invalid | mutationKey: ['testChunk'], // This method is invalid | ||||
| gcTime: 0, | |||||
| mutationFn: async (values: any) => { | mutationFn: async (values: any) => { | ||||
| const { data } = await kbService.retrieval_test({ | const { data } = await kbService.retrieval_test({ | ||||
| ...values, | ...values, | 
| return { read }; | return { read }; | ||||
| }; | }; | ||||
| export const useFetchAudioWithSse = (url: string = api.tts) => { | |||||
| // const [answer, setAnswer] = useState<IAnswer>({} as IAnswer); | |||||
| const [done, setDone] = useState(true); | |||||
| const read = useCallback( | |||||
| async ( | |||||
| body: any, | |||||
| ): Promise<{ response: Response; data: ResponseType } | undefined> => { | |||||
| try { | |||||
| setDone(false); | |||||
| const response = await fetch(url, { | |||||
| method: 'POST', | |||||
| headers: { | |||||
| [Authorization]: getAuthorization(), | |||||
| 'Content-Type': 'application/json', | |||||
| }, | |||||
| body: JSON.stringify(body), | |||||
| }); | |||||
| const res = response.clone().json(); | |||||
| const reader = response?.body?.getReader(); | |||||
| while (true) { | |||||
| const x = await reader?.read(); | |||||
| if (x) { | |||||
| const { done, value } = x; | |||||
| try { | |||||
| // const val = JSON.parse(value || ''); | |||||
| const val = value; | |||||
| // const d = val?.data; | |||||
| // if (typeof d !== 'boolean') { | |||||
| // console.info('data:', d); | |||||
| // setAnswer({ | |||||
| // ...d, | |||||
| // conversationId: body?.conversation_id, | |||||
| // }); | |||||
| // } | |||||
| } catch (e) { | |||||
| console.warn(e); | |||||
| } | |||||
| if (done) { | |||||
| console.info('done'); | |||||
| break; | |||||
| } | |||||
| } | |||||
| } | |||||
| console.info('done?'); | |||||
| setDone(true); | |||||
| // setAnswer({} as IAnswer); | |||||
| return { data: await res, response }; | |||||
| } catch (e) { | |||||
| setDone(true); | |||||
| // setAnswer({} as IAnswer); | |||||
| console.warn(e); | |||||
| } | |||||
| }, | |||||
| [url], | |||||
| ); | |||||
| return { read, done, setDone }; | |||||
| }; | |||||
| //#region chat hooks | //#region chat hooks | ||||
| export const useScrollToBottom = (messages?: unknown) => { | export const useScrollToBottom = (messages?: unknown) => { | 
| import Toolbar from '../right-toolbar'; | import Toolbar from '../right-toolbar'; | ||||
| import { useFetchAppConf } from '@/hooks/logic-hooks'; | import { useFetchAppConf } from '@/hooks/logic-hooks'; | ||||
| import { MessageOutlined } from '@ant-design/icons'; | |||||
| import { MessageOutlined, SearchOutlined } from '@ant-design/icons'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const { Header } = Layout; | const { Header } = Layout; | ||||
| () => [ | () => [ | ||||
| { path: '/knowledge', name: t('knowledgeBase'), icon: KnowledgeBaseIcon }, | { path: '/knowledge', name: t('knowledgeBase'), icon: KnowledgeBaseIcon }, | ||||
| { path: '/chat', name: t('chat'), icon: MessageOutlined }, | { path: '/chat', name: t('chat'), icon: MessageOutlined }, | ||||
| // { path: '/search', name: t('search'), icon: SearchOutlined }, | |||||
| { path: '/search', name: t('search'), icon: SearchOutlined }, | |||||
| { path: '/flow', name: t('flow'), icon: GraphIcon }, | { path: '/flow', name: t('flow'), icon: GraphIcon }, | ||||
| { path: '/file', name: t('fileManager'), icon: FileIcon }, | { path: '/file', name: t('fileManager'), icon: FileIcon }, | ||||
| ], | ], | 
| import { InfoCircleOutlined } from '@ant-design/icons'; | import { InfoCircleOutlined } from '@ant-design/icons'; | ||||
| import { Button, Flex, Popover, Space } from 'antd'; | import { Button, Flex, Popover, Space } from 'antd'; | ||||
| import DOMPurify from 'dompurify'; | import DOMPurify from 'dompurify'; | ||||
| import { useCallback } from 'react'; | |||||
| import { useCallback, useMemo } from 'react'; | |||||
| import Markdown from 'react-markdown'; | import Markdown from 'react-markdown'; | ||||
| import reactStringReplace from 'react-string-replace'; | import reactStringReplace from 'react-string-replace'; | ||||
| import SyntaxHighlighter from 'react-syntax-highlighter'; | import SyntaxHighlighter from 'react-syntax-highlighter'; | ||||
| import remarkGfm from 'remark-gfm'; | import remarkGfm from 'remark-gfm'; | ||||
| import { visitParents } from 'unist-util-visit-parents'; | import { visitParents } from 'unist-util-visit-parents'; | ||||
| import { useTranslation } from 'react-i18next'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const reg = /(#{2}\d+\${2})/g; | const reg = /(#{2}\d+\${2})/g; | ||||
| reference, | reference, | ||||
| clickDocumentButton, | clickDocumentButton, | ||||
| content, | content, | ||||
| loading, | |||||
| }: { | }: { | ||||
| content: string; | content: string; | ||||
| loading: boolean; | |||||
| reference: IReference; | reference: IReference; | ||||
| clickDocumentButton?: (documentId: string, chunk: IChunk) => void; | clickDocumentButton?: (documentId: string, chunk: IChunk) => void; | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation(); | |||||
| const contentWithCursor = useMemo(() => { | |||||
| let text = content; | |||||
| if (text === '') { | |||||
| text = t('chat.searching'); | |||||
| } | |||||
| return loading ? text?.concat('~~2$$') : text; | |||||
| }, [content, loading, t]); | |||||
| const fileThumbnails = useSelectFileThumbnails(); | const fileThumbnails = useSelectFileThumbnails(); | ||||
| const handleDocumentButtonClick = useCallback( | const handleDocumentButtonClick = useCallback( | ||||
| } as any | } as any | ||||
| } | } | ||||
| > | > | ||||
| {content} | |||||
| {contentWithCursor} | |||||
| </Markdown> | </Markdown> | ||||
| ); | ); | ||||
| }; | }; | 
| import { MessageType } from '@/constants/chat'; | |||||
| import { useTestChunkRetrieval } from '@/hooks/knowledge-hooks'; | import { useTestChunkRetrieval } from '@/hooks/knowledge-hooks'; | ||||
| import { useSendMessageWithSse } from '@/hooks/logic-hooks'; | import { useSendMessageWithSse } from '@/hooks/logic-hooks'; | ||||
| import { IAnswer } from '@/interfaces/database/chat'; | |||||
| import api from '@/utils/api'; | import api from '@/utils/api'; | ||||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | |||||
| import { IMessage } from '../chat/interface'; | |||||
| import { isEmpty } from 'lodash'; | |||||
| import { useCallback, useEffect, 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 { testChunk, loading } = useTestChunkRetrieval(); | const { testChunk, loading } = useTestChunkRetrieval(); | ||||
| const [sendingLoading, setSendingLoading] = useState(false); | const [sendingLoading, setSendingLoading] = useState(false); | ||||
| const message: IMessage = useMemo(() => { | |||||
| return { | |||||
| id: '', | |||||
| content: answer.answer, | |||||
| role: MessageType.Assistant, | |||||
| reference: answer.reference, | |||||
| }; | |||||
| }, [answer]); | |||||
| const [currentAnswer, setCurrentAnswer] = useState({} as IAnswer); | |||||
| const sendQuestion = useCallback( | const sendQuestion = useCallback( | ||||
| (question: string) => { | (question: string) => { | ||||
| setCurrentAnswer({} as IAnswer); | |||||
| setSendingLoading(true); | setSendingLoading(true); | ||||
| send({ kb_ids: kbIds, question }); | send({ kb_ids: kbIds, question }); | ||||
| testChunk({ kb_id: kbIds, highlight: true, question }); | testChunk({ kb_id: kbIds, highlight: true, question }); | ||||
| [send, testChunk, kbIds], | [send, testChunk, kbIds], | ||||
| ); | ); | ||||
| useEffect(() => { | |||||
| if (!isEmpty(answer)) { | |||||
| setCurrentAnswer(answer); | |||||
| } | |||||
| }, [answer]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (done) { | if (done) { | ||||
| setSendingLoading(false); | setSendingLoading(false); | ||||
| } | } | ||||
| }, [done]); | }, [done]); | ||||
| return { sendQuestion, message, loading, sendingLoading }; | |||||
| return { sendQuestion, loading, sendingLoading, answer: currentAnswer }; | |||||
| }; | }; | 
| .searchPage { | .searchPage { | ||||
| // height: 100%; | |||||
| .card { | |||||
| width: 100%; | |||||
| } | |||||
| } | } | ||||
| .searchSide { | .searchSide { | 
| import HightLightMarkdown from '@/components/highlight-markdown'; | import HightLightMarkdown from '@/components/highlight-markdown'; | ||||
| import { ImageWithPopover } from '@/components/image'; | import { ImageWithPopover } from '@/components/image'; | ||||
| import MessageItem from '@/components/message-item'; | |||||
| import { useSelectTestingResult } from '@/hooks/knowledge-hooks'; | import { useSelectTestingResult } from '@/hooks/knowledge-hooks'; | ||||
| import { IReference } from '@/interfaces/database/chat'; | import { IReference } from '@/interfaces/database/chat'; | ||||
| import { Card, Flex, Input, Layout, List, Space } from 'antd'; | import { Card, Flex, Input, Layout, List, Space } from 'antd'; | ||||
| import { useState } from 'react'; | import { useState } from 'react'; | ||||
| import MarkdownContent from '../chat/markdown-content'; | |||||
| import { useSendQuestion } from './hooks'; | import { useSendQuestion } from './hooks'; | ||||
| import SearchSidebar from './sidebar'; | import SearchSidebar from './sidebar'; | ||||
| const SearchPage = () => { | const SearchPage = () => { | ||||
| const [checkedList, setCheckedList] = useState<string[]>([]); | const [checkedList, setCheckedList] = useState<string[]>([]); | ||||
| const list = useSelectTestingResult(); | const list = useSelectTestingResult(); | ||||
| const { sendQuestion, message, sendingLoading } = | |||||
| useSendQuestion(checkedList); | |||||
| const { sendQuestion, answer, sendingLoading } = useSendQuestion(checkedList); | |||||
| return ( | return ( | ||||
| <Layout className={styles.searchPage}> | <Layout className={styles.searchPage}> | ||||
| placeholder="input search text" | placeholder="input search text" | ||||
| onSearch={sendQuestion} | onSearch={sendQuestion} | ||||
| size="large" | size="large" | ||||
| loading={sendingLoading} | |||||
| disabled={checkedList.length === 0} | |||||
| /> | /> | ||||
| <MessageItem | |||||
| item={message} | |||||
| nickname="You" | |||||
| reference={message.reference ?? ({} as IReference)} | |||||
| <MarkdownContent | |||||
| loading={sendingLoading} | loading={sendingLoading} | ||||
| index={0} | |||||
| ></MessageItem> | |||||
| content={answer.answer} | |||||
| reference={answer.reference ?? ({} as IReference)} | |||||
| clickDocumentButton={() => {}} | |||||
| ></MarkdownContent> | |||||
| <List | <List | ||||
| dataSource={list.chunks} | dataSource={list.chunks} | ||||
| renderItem={(item) => ( | renderItem={(item) => ( | ||||
| <List.Item> | <List.Item> | ||||
| <Card> | |||||
| <Card className={styles.card}> | |||||
| <Space> | <Space> | ||||
| <ImageWithPopover id={item.img_id}></ImageWithPopover> | <ImageWithPopover id={item.img_id}></ImageWithPopover> | ||||
| <HightLightMarkdown> | <HightLightMarkdown> |