### What problem does this PR solve? feat: Click on the relevant question tag to continue searching for answers #2247 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.11.0
| @@ -217,6 +217,10 @@ export const useSendMessageWithSse = ( | |||
| const [answer, setAnswer] = useState<IAnswer>({} as IAnswer); | |||
| const [done, setDone] = useState(true); | |||
| const resetAnswer = useCallback(() => { | |||
| setAnswer({} as IAnswer); | |||
| }, []); | |||
| const send = useCallback( | |||
| async ( | |||
| body: any, | |||
| @@ -251,7 +255,7 @@ export const useSendMessageWithSse = ( | |||
| const val = JSON.parse(value?.data || ''); | |||
| const d = val?.data; | |||
| if (typeof d !== 'boolean') { | |||
| // console.info('data:', d); | |||
| console.info('data:', d); | |||
| setAnswer({ | |||
| ...d, | |||
| conversationId: body?.conversation_id, | |||
| @@ -264,18 +268,16 @@ export const useSendMessageWithSse = ( | |||
| } | |||
| 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 { send, answer, done, setDone }; | |||
| return { send, answer, done, setDone, resetAnswer }; | |||
| }; | |||
| export const useSpeechWithSse = (url: string = api.tts) => { | |||
| @@ -2,14 +2,14 @@ import { ReactComponent as FileIcon } from '@/assets/svg/file-management.svg'; | |||
| import { ReactComponent as GraphIcon } from '@/assets/svg/graph.svg'; | |||
| import { ReactComponent as KnowledgeBaseIcon } from '@/assets/svg/knowledge-base.svg'; | |||
| import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { useFetchAppConf } from '@/hooks/logic-hooks'; | |||
| import { useNavigateWithFromState } from '@/hooks/route-hook'; | |||
| import { MessageOutlined, SearchOutlined } from '@ant-design/icons'; | |||
| import { Flex, Layout, Radio, Space, theme } from 'antd'; | |||
| import { useCallback, useMemo } from 'react'; | |||
| import { useLocation } from 'umi'; | |||
| import Toolbar from '../right-toolbar'; | |||
| import { useFetchAppConf } from '@/hooks/logic-hooks'; | |||
| import { MessageOutlined } from '@ant-design/icons'; | |||
| import styles from './index.less'; | |||
| const { Header } = Layout; | |||
| @@ -27,7 +27,7 @@ const RagHeader = () => { | |||
| () => [ | |||
| { path: '/knowledge', name: t('knowledgeBase'), icon: KnowledgeBaseIcon }, | |||
| { 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: '/file', name: t('fileManager'), icon: FileIcon }, | |||
| ], | |||
| @@ -19,21 +19,12 @@ import { | |||
| } from '@/hooks/common-hooks'; | |||
| import { | |||
| useRegenerateMessage, | |||
| useRemoveMessageById, | |||
| useRemoveMessagesAfterCurrentMessage, | |||
| useScrollToBottom, | |||
| useSelectDerivedMessages, | |||
| useSendMessageWithSse, | |||
| } from '@/hooks/logic-hooks'; | |||
| import { | |||
| IAnswer, | |||
| 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 { buildMessageUuid } from '@/utils/chat'; | |||
| import { useMutationState } from '@tanstack/react-query'; | |||
| import { get } from 'lodash'; | |||
| import trim from 'lodash/trim'; | |||
| @@ -251,118 +242,6 @@ export const useSetConversation = () => { | |||
| return { setConversation }; | |||
| }; | |||
| export const useSelectCurrentConversation = () => { | |||
| const [currentConversation, setCurrentConversation] = | |||
| useState<IClientConversation>({} as IClientConversation); | |||
| const { data: conversation, loading } = useFetchNextConversation(); | |||
| const { data: dialog } = useFetchNextDialog(); | |||
| const { conversationId, dialogId } = useGetChatSearchParams(); | |||
| const { removeMessageById } = useRemoveMessageById(setCurrentConversation); | |||
| const { removeMessagesAfterCurrentMessage } = | |||
| useRemoveMessagesAfterCurrentMessage(setCurrentConversation); | |||
| // Show the entered message in the conversation immediately after sending the message | |||
| const addNewestConversation = useCallback( | |||
| (message: Message, answer: string = '') => { | |||
| setCurrentConversation((pre) => { | |||
| return { | |||
| ...pre, | |||
| message: [ | |||
| ...pre.message, | |||
| { | |||
| ...message, | |||
| id: buildMessageUuid(message), | |||
| } as IMessage, | |||
| { | |||
| role: MessageType.Assistant, | |||
| content: answer, | |||
| id: buildMessageUuid({ ...message, role: MessageType.Assistant }), | |||
| reference: {}, | |||
| } as IMessage, | |||
| ], | |||
| }; | |||
| }); | |||
| }, | |||
| [], | |||
| ); | |||
| // Add the streaming message to the last item in the message list | |||
| const addNewestAnswer = useCallback((answer: IAnswer) => { | |||
| setCurrentConversation((pre) => { | |||
| const latestMessage = pre.message?.at(-1); | |||
| if (latestMessage) { | |||
| return { | |||
| ...pre, | |||
| message: [ | |||
| ...pre.message.slice(0, -1), | |||
| { | |||
| ...latestMessage, | |||
| content: answer.answer, | |||
| reference: answer.reference, | |||
| id: buildMessageUuid({ | |||
| id: answer.id, | |||
| role: MessageType.Assistant, | |||
| }), | |||
| prompt: answer.prompt, | |||
| } as IMessage, | |||
| ], | |||
| }; | |||
| } | |||
| return pre; | |||
| }); | |||
| }, []); | |||
| const removeLatestMessage = useCallback(() => { | |||
| setCurrentConversation((pre) => { | |||
| const nextMessages = pre.message?.slice(0, -2) ?? []; | |||
| return { | |||
| ...pre, | |||
| message: nextMessages, | |||
| }; | |||
| }); | |||
| }, []); | |||
| const addPrologue = useCallback(() => { | |||
| if (dialogId !== '' && conversationId === '') { | |||
| const prologue = dialog.prompt_config?.prologue; | |||
| const nextMessage = { | |||
| role: MessageType.Assistant, | |||
| content: prologue, | |||
| id: uuid(), | |||
| } as IMessage; | |||
| setCurrentConversation({ | |||
| id: '', | |||
| dialog_id: dialogId, | |||
| reference: [], | |||
| message: [nextMessage], | |||
| } as any); | |||
| } | |||
| }, [conversationId, dialog, dialogId]); | |||
| useEffect(() => { | |||
| addPrologue(); | |||
| }, [addPrologue]); | |||
| useEffect(() => { | |||
| if (conversationId) { | |||
| setCurrentConversation(conversation); | |||
| } | |||
| }, [conversation, conversationId]); | |||
| return { | |||
| currentConversation, | |||
| addNewestConversation, | |||
| removeLatestMessage, | |||
| addNewestAnswer, | |||
| removeMessageById, | |||
| removeMessagesAfterCurrentMessage, | |||
| loading, | |||
| }; | |||
| }; | |||
| // export const useScrollToBottom = (currentConversation: IClientConversation) => { | |||
| // const ref = useRef<HTMLDivElement>(null); | |||
| @@ -430,32 +309,6 @@ export const useSelectNextMessages = () => { | |||
| }; | |||
| }; | |||
| export const useFetchConversationOnMount = () => { | |||
| const { conversationId } = useGetChatSearchParams(); | |||
| const { | |||
| currentConversation, | |||
| addNewestConversation, | |||
| removeLatestMessage, | |||
| addNewestAnswer, | |||
| loading, | |||
| removeMessageById, | |||
| removeMessagesAfterCurrentMessage, | |||
| } = useSelectCurrentConversation(); | |||
| const ref = useScrollToBottom(currentConversation); | |||
| return { | |||
| currentConversation, | |||
| addNewestConversation, | |||
| ref, | |||
| removeLatestMessage, | |||
| addNewestAnswer, | |||
| conversationId, | |||
| loading, | |||
| removeMessageById, | |||
| removeMessagesAfterCurrentMessage, | |||
| }; | |||
| }; | |||
| export const useHandleMessageInputChange = () => { | |||
| const [value, setValue] = useState(''); | |||
| @@ -477,7 +330,7 @@ export const useSendNextMessage = () => { | |||
| const { conversationId } = useGetChatSearchParams(); | |||
| const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | |||
| const { handleClickConversation } = useClickConversationCard(); | |||
| const { send, answer, done, setDone } = useSendMessageWithSse(); | |||
| const { send, answer, done, setDone, resetAnswer } = useSendMessageWithSse(); | |||
| const { | |||
| ref, | |||
| derivedMessages, | |||
| @@ -557,10 +410,11 @@ export const useSendNextMessage = () => { | |||
| useEffect(() => { | |||
| // #1289 | |||
| console.log('🚀 ~ useEffect ~ answer:', answer, done); | |||
| if ( | |||
| answer.answer && | |||
| !done && | |||
| (answer?.conversationId === conversationId || conversationId === '') | |||
| (answer?.conversationId === conversationId || | |||
| (!done && conversationId === '')) | |||
| ) { | |||
| addNewestAnswer(answer); | |||
| } | |||
| @@ -570,8 +424,10 @@ export const useSendNextMessage = () => { | |||
| // #1289 switch to another conversion window when the last conversion answer doesn't finish. | |||
| if (conversationId) { | |||
| setDone(true); | |||
| } else { | |||
| resetAnswer(); | |||
| } | |||
| }, [setDone, conversationId]); | |||
| }, [setDone, conversationId, resetAnswer]); | |||
| const handlePressEnter = useCallback( | |||
| (documentIds: string[]) => { | |||
| @@ -4,7 +4,7 @@ import { useSendMessageWithSse } from '@/hooks/logic-hooks'; | |||
| import { IAnswer } from '@/interfaces/database/chat'; | |||
| import api from '@/utils/api'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| import { ChangeEventHandler, useCallback, useEffect, useState } from 'react'; | |||
| export const useSendQuestion = (kbIds: string[]) => { | |||
| const { send, answer, done } = useSendMessageWithSse(api.ask); | |||
| @@ -18,6 +18,7 @@ export const useSendQuestion = (kbIds: string[]) => { | |||
| data: mindMap, | |||
| loading: mindMapLoading, | |||
| } = useFetchMindMap(); | |||
| const [searchStr, setSearchStr] = useState<string>(''); | |||
| const sendQuestion = useCallback( | |||
| (question: string) => { | |||
| @@ -34,10 +35,26 @@ export const useSendQuestion = (kbIds: string[]) => { | |||
| [send, testChunk, kbIds, fetchRelatedQuestions, fetchMindMap], | |||
| ); | |||
| const handleSearchStrChange: ChangeEventHandler<HTMLInputElement> = | |||
| useCallback((e) => { | |||
| setSearchStr(e.target.value); | |||
| }, []); | |||
| const handleClickRelatedQuestion = useCallback( | |||
| (question: string) => () => { | |||
| setSearchStr(question); | |||
| sendQuestion(question); | |||
| }, | |||
| [sendQuestion], | |||
| ); | |||
| useEffect(() => { | |||
| if (!isEmpty(answer)) { | |||
| setCurrentAnswer(answer); | |||
| } | |||
| return () => { | |||
| setCurrentAnswer({} as IAnswer); | |||
| }; | |||
| }, [answer]); | |||
| useEffect(() => { | |||
| @@ -54,5 +71,8 @@ export const useSendQuestion = (kbIds: string[]) => { | |||
| relatedQuestions: relatedQuestions?.slice(0, 5) ?? [], | |||
| mindMap, | |||
| mindMapLoading, | |||
| handleClickRelatedQuestion, | |||
| searchStr, | |||
| handleSearchStrChange, | |||
| }; | |||
| }; | |||
| @@ -1,10 +1,17 @@ | |||
| .searchPage { | |||
| .card { | |||
| width: 100%; | |||
| :global(.ant-card-body) { | |||
| padding: 14px; | |||
| } | |||
| p { | |||
| margin: 0; | |||
| } | |||
| } | |||
| .tag { | |||
| padding: 4px 8px; | |||
| font-size: 14px; | |||
| cursor: pointer; | |||
| } | |||
| } | |||
| @@ -56,3 +63,12 @@ | |||
| padding-right: 10px; | |||
| } | |||
| } | |||
| .answerWrapper { | |||
| background-color: #e6f4ff; | |||
| padding: 14px; | |||
| margin-top: 16px; | |||
| border-radius: 8px; | |||
| & > p { | |||
| margin: 0; | |||
| } | |||
| } | |||
| @@ -19,11 +19,14 @@ const SearchPage = () => { | |||
| const list = useSelectTestingResult(); | |||
| const { | |||
| sendQuestion, | |||
| handleClickRelatedQuestion, | |||
| handleSearchStrChange, | |||
| answer, | |||
| sendingLoading, | |||
| relatedQuestions, | |||
| mindMap, | |||
| mindMapLoading, | |||
| searchStr, | |||
| } = useSendQuestion(checkedList); | |||
| return ( | |||
| @@ -37,18 +40,24 @@ const SearchPage = () => { | |||
| <Flex className={styles.content}> | |||
| <section className={styles.main}> | |||
| <Search | |||
| value={searchStr} | |||
| onChange={handleSearchStrChange} | |||
| placeholder="input search text" | |||
| onSearch={sendQuestion} | |||
| size="large" | |||
| loading={sendingLoading} | |||
| disabled={checkedList.length === 0} | |||
| /> | |||
| <MarkdownContent | |||
| loading={sendingLoading} | |||
| content={answer.answer} | |||
| reference={answer.reference ?? ({} as IReference)} | |||
| clickDocumentButton={() => {}} | |||
| ></MarkdownContent> | |||
| {answer.answer && ( | |||
| <div className={styles.answerWrapper}> | |||
| <MarkdownContent | |||
| loading={sendingLoading} | |||
| content={answer.answer} | |||
| reference={answer.reference ?? ({} as IReference)} | |||
| clickDocumentButton={() => {}} | |||
| ></MarkdownContent> | |||
| </div> | |||
| )} | |||
| <List | |||
| dataSource={list.chunks} | |||
| renderItem={(item) => ( | |||
| @@ -68,7 +77,11 @@ const SearchPage = () => { | |||
| <Card> | |||
| <Flex wrap="wrap" gap={'10px 0'}> | |||
| {relatedQuestions?.map((x, idx) => ( | |||
| <Tag key={idx} className={styles.tag}> | |||
| <Tag | |||
| key={idx} | |||
| className={styles.tag} | |||
| onClick={handleClickRelatedQuestion(x)} | |||
| > | |||
| {x} | |||
| </Tag> | |||
| ))} | |||