### What problem does this PR solve? feat: Select derived messages from backend #2088 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.11.0
| @@ -91,7 +91,7 @@ export const AssistantGroupButton = ({ | |||
| interface UserGroupButtonProps extends Partial<IRemoveMessageById> { | |||
| messageId: string; | |||
| content: string; | |||
| regenerateMessage(): void; | |||
| regenerateMessage?: () => void; | |||
| sendLoading: boolean; | |||
| } | |||
| @@ -113,15 +113,17 @@ export const UserGroupButton = ({ | |||
| <Radio.Button value="a"> | |||
| <CopyToClipboard text={content}></CopyToClipboard> | |||
| </Radio.Button> | |||
| <Radio.Button | |||
| value="b" | |||
| onClick={regenerateMessage} | |||
| disabled={sendLoading} | |||
| > | |||
| <Tooltip title={t('chat.regenerate')}> | |||
| <SyncOutlined spin={sendLoading} /> | |||
| </Tooltip> | |||
| </Radio.Button> | |||
| {regenerateMessage && ( | |||
| <Radio.Button | |||
| value="b" | |||
| onClick={regenerateMessage} | |||
| disabled={sendLoading} | |||
| > | |||
| <Tooltip title={t('chat.regenerate')}> | |||
| <SyncOutlined spin={sendLoading} /> | |||
| </Tooltip> | |||
| </Radio.Button> | |||
| )} | |||
| {removeMessageById && ( | |||
| <Radio.Button value="c" onClick={onRemoveMessage} disabled={loading}> | |||
| <Tooltip title={t('common.delete')}> | |||
| @@ -79,7 +79,7 @@ const MessageItem = ({ | |||
| ); | |||
| const handleRegenerateMessage = useCallback(() => { | |||
| regenerateMessage(item); | |||
| regenerateMessage?.(item); | |||
| }, [regenerateMessage, item]); | |||
| useEffect(() => { | |||
| @@ -138,7 +138,9 @@ const MessageItem = ({ | |||
| content={item.content} | |||
| messageId={item.id} | |||
| removeMessageById={removeMessageById} | |||
| regenerateMessage={handleRegenerateMessage} | |||
| regenerateMessage={ | |||
| regenerateMessage && handleRegenerateMessage | |||
| } | |||
| sendLoading={sendLoading} | |||
| ></UserGroupButton> | |||
| )} | |||
| @@ -4,13 +4,12 @@ import { | |||
| IDialog, | |||
| IStats, | |||
| IToken, | |||
| Message, | |||
| } from '@/interfaces/database/chat'; | |||
| import { IFeedbackRequestBody } from '@/interfaces/request/chat'; | |||
| import i18n from '@/locales/config'; | |||
| import { IClientConversation, IMessage } from '@/pages/chat/interface'; | |||
| import { IClientConversation } from '@/pages/chat/interface'; | |||
| import chatService from '@/services/chat-service'; | |||
| import { buildMessageUuid, isConversationIdExist } from '@/utils/chat'; | |||
| import { buildMessageListWithUuid, isConversationIdExist } from '@/utils/chat'; | |||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | |||
| import { message } from 'antd'; | |||
| import dayjs, { Dayjs } from 'dayjs'; | |||
| @@ -18,15 +17,6 @@ import { set } from 'lodash'; | |||
| import { useCallback, useMemo, useState } from 'react'; | |||
| import { useSearchParams } from 'umi'; | |||
| const buildMessageListWithUuid = (messages?: Message[]) => { | |||
| return ( | |||
| messages?.map((x: Message | IMessage) => ({ | |||
| ...x, | |||
| id: buildMessageUuid(x), | |||
| })) ?? [] | |||
| ); | |||
| }; | |||
| //#region logic | |||
| export const useClickDialogCard = () => { | |||
| @@ -465,14 +455,11 @@ export const useCreateNextSharedConversation = () => { | |||
| return { data, loading, createSharedConversation: mutateAsync }; | |||
| }; | |||
| export const useFetchNextSharedConversation = () => { | |||
| const { | |||
| data, | |||
| isPending: loading, | |||
| mutateAsync, | |||
| } = useMutation({ | |||
| mutationKey: ['fetchSharedConversation'], | |||
| mutationFn: async (conversationId: string) => { | |||
| export const useFetchNextSharedConversation = (conversationId: string) => { | |||
| const { data, isPending: loading } = useQuery({ | |||
| queryKey: ['fetchSharedConversation'], | |||
| enabled: !!conversationId, | |||
| queryFn: async () => { | |||
| const { data } = await chatService.getExternalConversation( | |||
| null, | |||
| conversationId, | |||
| @@ -486,7 +473,7 @@ export const useFetchNextSharedConversation = () => { | |||
| }, | |||
| }); | |||
| return { data, loading, fetchConversation: mutateAsync }; | |||
| return { data, loading }; | |||
| }; | |||
| //#endregion | |||
| @@ -2,8 +2,11 @@ import { ResponseType } from '@/interfaces/database/base'; | |||
| import { DSL, IFlow, IFlowTemplate } from '@/interfaces/database/flow'; | |||
| import i18n from '@/locales/config'; | |||
| import flowService from '@/services/flow-service'; | |||
| import { buildMessageListWithUuid } from '@/utils/chat'; | |||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | |||
| import { message } from 'antd'; | |||
| import { set } from 'lodash'; | |||
| import get from 'lodash/get'; | |||
| import { useParams } from 'umi'; | |||
| import { v4 as uuid } from 'uuid'; | |||
| @@ -101,6 +104,11 @@ export const useFetchFlow = (): { | |||
| queryFn: async () => { | |||
| const { data } = await flowService.getCanvas({}, id); | |||
| const messageList = buildMessageListWithUuid( | |||
| get(data, 'data.dsl.messages', []), | |||
| ); | |||
| set(data, 'data.dsl.messages', messageList); | |||
| return data?.data ?? {}; | |||
| }, | |||
| }); | |||
| @@ -1,14 +1,15 @@ | |||
| import { Authorization } from '@/constants/authorization'; | |||
| import { MessageType } from '@/constants/chat'; | |||
| import { LanguageTranslationMap } from '@/constants/common'; | |||
| import { Pagination } from '@/interfaces/common'; | |||
| import { ResponseType } from '@/interfaces/database/base'; | |||
| import { IAnswer, Message } from '@/interfaces/database/chat'; | |||
| import { IKnowledgeFile } from '@/interfaces/database/knowledge'; | |||
| import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; | |||
| import { IClientConversation } from '@/pages/chat/interface'; | |||
| import { IClientConversation, IMessage } from '@/pages/chat/interface'; | |||
| import api from '@/utils/api'; | |||
| import { getAuthorization } from '@/utils/authorization-util'; | |||
| import { getMessagePureId } from '@/utils/chat'; | |||
| import { buildMessageUuid, getMessagePureId } from '@/utils/chat'; | |||
| import { PaginationProps } from 'antd'; | |||
| import { FormInstance } from 'antd/lib'; | |||
| import axios from 'axios'; | |||
| @@ -309,6 +310,108 @@ export const useHandleMessageInputChange = () => { | |||
| }; | |||
| }; | |||
| export const useSelectDerivedMessages = () => { | |||
| const [derivedMessages, setDerivedMessages] = useState<IMessage[]>([]); | |||
| const ref = useScrollToBottom(derivedMessages); | |||
| const addNewestQuestion = useCallback( | |||
| (message: Message, answer: string = '') => { | |||
| setDerivedMessages((pre) => { | |||
| return [ | |||
| ...pre, | |||
| { | |||
| ...message, | |||
| id: buildMessageUuid(message), | |||
| }, | |||
| { | |||
| role: MessageType.Assistant, | |||
| content: answer, | |||
| id: buildMessageUuid({ ...message, role: MessageType.Assistant }), | |||
| }, | |||
| ]; | |||
| }); | |||
| }, | |||
| [], | |||
| ); | |||
| // Add the streaming message to the last item in the message list | |||
| const addNewestAnswer = useCallback((answer: IAnswer) => { | |||
| setDerivedMessages((pre) => { | |||
| return [ | |||
| ...(pre?.slice(0, -1) ?? []), | |||
| { | |||
| role: MessageType.Assistant, | |||
| content: answer.answer, | |||
| reference: answer.reference, | |||
| id: buildMessageUuid({ | |||
| id: answer.id, | |||
| role: MessageType.Assistant, | |||
| }), | |||
| prompt: answer.prompt, | |||
| }, | |||
| ]; | |||
| }); | |||
| }, []); | |||
| const removeLatestMessage = useCallback(() => { | |||
| setDerivedMessages((pre) => { | |||
| const nextMessages = pre?.slice(0, -2) ?? []; | |||
| return nextMessages; | |||
| }); | |||
| }, []); | |||
| const removeMessageById = useCallback( | |||
| (messageId: string) => { | |||
| setDerivedMessages((pre) => { | |||
| const nextMessages = | |||
| pre?.filter( | |||
| (x) => getMessagePureId(x.id) !== getMessagePureId(messageId), | |||
| ) ?? []; | |||
| return nextMessages; | |||
| }); | |||
| }, | |||
| [setDerivedMessages], | |||
| ); | |||
| const removeMessagesAfterCurrentMessage = useCallback( | |||
| (messageId: string) => { | |||
| setDerivedMessages((pre) => { | |||
| const index = pre.findIndex((x) => x.id === messageId); | |||
| if (index !== -1) { | |||
| let nextMessages = pre.slice(0, index + 2) ?? []; | |||
| const latestMessage = nextMessages.at(-1); | |||
| nextMessages = latestMessage | |||
| ? [ | |||
| ...nextMessages.slice(0, -1), | |||
| { | |||
| ...latestMessage, | |||
| content: '', | |||
| reference: undefined, | |||
| prompt: undefined, | |||
| }, | |||
| ] | |||
| : nextMessages; | |||
| return nextMessages; | |||
| } | |||
| return pre; | |||
| }); | |||
| }, | |||
| [setDerivedMessages], | |||
| ); | |||
| return { | |||
| ref, | |||
| derivedMessages, | |||
| setDerivedMessages, | |||
| addNewestQuestion, | |||
| addNewestAnswer, | |||
| removeLatestMessage, | |||
| removeMessageById, | |||
| removeMessagesAfterCurrentMessage, | |||
| }; | |||
| }; | |||
| export interface IRemoveMessageById { | |||
| removeMessageById(messageId: string): void; | |||
| } | |||
| @@ -375,7 +478,7 @@ export const useRemoveMessagesAfterCurrentMessage = ( | |||
| }; | |||
| export interface IRegenerateMessage { | |||
| regenerateMessage(message: Message): void; | |||
| regenerateMessage?: (message: Message) => void; | |||
| } | |||
| export const useRegenerateMessage = ({ | |||
| @@ -384,7 +487,12 @@ export const useRegenerateMessage = ({ | |||
| messages, | |||
| }: { | |||
| removeMessagesAfterCurrentMessage(messageId: string): void; | |||
| sendMessage({ message }: { message: Message; messages?: Message[] }): void; | |||
| sendMessage({ | |||
| message, | |||
| }: { | |||
| message: Message; | |||
| messages?: Message[]; | |||
| }): void | Promise<any>; | |||
| messages: Message[]; | |||
| }) => { | |||
| const regenerateMessage = useCallback( | |||
| @@ -5,44 +5,38 @@ import { Drawer, Flex, Spin } from 'antd'; | |||
| import { | |||
| useClickDrawer, | |||
| useCreateConversationBeforeUploadDocument, | |||
| useFetchConversationOnMount, | |||
| useGetFileIcon, | |||
| useGetSendButtonDisabled, | |||
| useSendButtonDisabled, | |||
| useSendMessage, | |||
| useSendNextMessage, | |||
| } from '../hooks'; | |||
| import { buildMessageItemReference } from '../utils'; | |||
| import MessageInput from '@/components/message-input'; | |||
| import { | |||
| useFetchNextConversation, | |||
| useGetChatSearchParams, | |||
| } from '@/hooks/chat-hooks'; | |||
| import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | |||
| import { memo } from 'react'; | |||
| import styles from './index.less'; | |||
| const ChatContainer = () => { | |||
| const { conversationId } = useGetChatSearchParams(); | |||
| const { data: conversation } = useFetchNextConversation(); | |||
| const { | |||
| ref, | |||
| currentConversation: conversation, | |||
| addNewestConversation, | |||
| removeLatestMessage, | |||
| addNewestAnswer, | |||
| conversationId, | |||
| loading, | |||
| removeMessageById, | |||
| removeMessagesAfterCurrentMessage, | |||
| } = useFetchConversationOnMount(); | |||
| const { | |||
| sendLoading, | |||
| derivedMessages, | |||
| handleInputChange, | |||
| handlePressEnter, | |||
| value, | |||
| loading: sendLoading, | |||
| regenerateMessage, | |||
| } = useSendMessage( | |||
| conversation, | |||
| addNewestConversation, | |||
| removeLatestMessage, | |||
| addNewestAnswer, | |||
| removeMessagesAfterCurrentMessage, | |||
| ); | |||
| removeMessageById, | |||
| } = useSendNextMessage(); | |||
| const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | |||
| useClickDrawer(); | |||
| const disabled = useGetSendButtonDisabled(); | |||
| @@ -58,19 +52,25 @@ const ChatContainer = () => { | |||
| <Flex flex={1} vertical className={styles.messageContainer}> | |||
| <div> | |||
| <Spin spinning={loading}> | |||
| {conversation?.message?.map((message, i) => { | |||
| {derivedMessages?.map((message, i) => { | |||
| return ( | |||
| <MessageItem | |||
| loading={ | |||
| message.role === MessageType.Assistant && | |||
| sendLoading && | |||
| conversation?.message.length - 1 === i | |||
| derivedMessages.length - 1 === i | |||
| } | |||
| key={message.id} | |||
| item={message} | |||
| nickname={userInfo.nickname} | |||
| avatar={userInfo.avatar} | |||
| reference={buildMessageItemReference(conversation, message)} | |||
| reference={buildMessageItemReference( | |||
| { | |||
| message: derivedMessages, | |||
| reference: conversation.reference, | |||
| }, | |||
| message, | |||
| )} | |||
| clickDocumentButton={clickDocumentButton} | |||
| index={i} | |||
| removeMessageById={removeMessageById} | |||
| @@ -21,6 +21,8 @@ import { | |||
| useRegenerateMessage, | |||
| useRemoveMessageById, | |||
| useRemoveMessagesAfterCurrentMessage, | |||
| useScrollToBottom, | |||
| useSelectDerivedMessages, | |||
| useSendMessageWithSse, | |||
| } from '@/hooks/logic-hooks'; | |||
| import { | |||
| @@ -40,7 +42,6 @@ import { | |||
| useCallback, | |||
| useEffect, | |||
| useMemo, | |||
| useRef, | |||
| useState, | |||
| } from 'react'; | |||
| import { useSearchParams } from 'umi'; | |||
| @@ -362,20 +363,71 @@ export const useSelectCurrentConversation = () => { | |||
| }; | |||
| }; | |||
| export const useScrollToBottom = (currentConversation: IClientConversation) => { | |||
| const ref = useRef<HTMLDivElement>(null); | |||
| // export const useScrollToBottom = (currentConversation: IClientConversation) => { | |||
| // const ref = useRef<HTMLDivElement>(null); | |||
| const scrollToBottom = useCallback(() => { | |||
| if (currentConversation.id) { | |||
| ref.current?.scrollIntoView({ behavior: 'instant' }); | |||
| // const scrollToBottom = useCallback(() => { | |||
| // if (currentConversation.id) { | |||
| // ref.current?.scrollIntoView({ behavior: 'instant' }); | |||
| // } | |||
| // }, [currentConversation]); | |||
| // useEffect(() => { | |||
| // scrollToBottom(); | |||
| // }, [scrollToBottom]); | |||
| // return ref; | |||
| // }; | |||
| export const useSelectNextMessages = () => { | |||
| const { | |||
| ref, | |||
| setDerivedMessages, | |||
| derivedMessages, | |||
| addNewestAnswer, | |||
| addNewestQuestion, | |||
| removeLatestMessage, | |||
| removeMessageById, | |||
| removeMessagesAfterCurrentMessage, | |||
| } = useSelectDerivedMessages(); | |||
| const { data: conversation, loading } = useFetchNextConversation(); | |||
| const { data: dialog } = useFetchNextDialog(); | |||
| const { conversationId, dialogId } = useGetChatSearchParams(); | |||
| const addPrologue = useCallback(() => { | |||
| if (dialogId !== '' && conversationId === '') { | |||
| const prologue = dialog.prompt_config?.prologue; | |||
| const nextMessage = { | |||
| role: MessageType.Assistant, | |||
| content: prologue, | |||
| id: uuid(), | |||
| } as IMessage; | |||
| setDerivedMessages([nextMessage]); | |||
| } | |||
| }, [currentConversation]); | |||
| }, [conversationId, dialog, dialogId, setDerivedMessages]); | |||
| useEffect(() => { | |||
| addPrologue(); | |||
| }, [addPrologue]); | |||
| useEffect(() => { | |||
| scrollToBottom(); | |||
| }, [scrollToBottom]); | |||
| if (conversationId) { | |||
| setDerivedMessages(conversation.message); | |||
| } | |||
| }, [conversation.message, conversationId, setDerivedMessages]); | |||
| return ref; | |||
| return { | |||
| ref, | |||
| derivedMessages, | |||
| loading, | |||
| addNewestAnswer, | |||
| addNewestQuestion, | |||
| removeLatestMessage, | |||
| removeMessageById, | |||
| removeMessagesAfterCurrentMessage, | |||
| }; | |||
| }; | |||
| export const useFetchConversationOnMount = () => { | |||
| @@ -544,6 +596,137 @@ export const useSendMessage = ( | |||
| }; | |||
| }; | |||
| export const useSendNextMessage = () => { | |||
| const { setConversation } = useSetConversation(); | |||
| const { conversationId } = useGetChatSearchParams(); | |||
| const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | |||
| const { handleClickConversation } = useClickConversationCard(); | |||
| const { send, answer, done, setDone } = useSendMessageWithSse(); | |||
| const { | |||
| ref, | |||
| derivedMessages, | |||
| loading, | |||
| addNewestAnswer, | |||
| addNewestQuestion, | |||
| removeLatestMessage, | |||
| removeMessageById, | |||
| removeMessagesAfterCurrentMessage, | |||
| } = useSelectNextMessages(); | |||
| const sendMessage = useCallback( | |||
| async ({ | |||
| message, | |||
| currentConversationId, | |||
| messages, | |||
| }: { | |||
| message: Message; | |||
| currentConversationId?: string; | |||
| messages?: Message[]; | |||
| }) => { | |||
| const res = await send({ | |||
| conversation_id: currentConversationId ?? conversationId, | |||
| messages: [...(messages ?? derivedMessages ?? []), message], | |||
| }); | |||
| if (res && (res?.response.status !== 200 || res?.data?.retcode !== 0)) { | |||
| // cancel loading | |||
| setValue(message.content); | |||
| console.info('removeLatestMessage111'); | |||
| removeLatestMessage(); | |||
| } else { | |||
| if (currentConversationId) { | |||
| console.info('111'); | |||
| // new conversation | |||
| handleClickConversation(currentConversationId); | |||
| } else { | |||
| console.info('222'); | |||
| // fetchConversation(conversationId); | |||
| } | |||
| } | |||
| }, | |||
| [ | |||
| derivedMessages, | |||
| conversationId, | |||
| handleClickConversation, | |||
| removeLatestMessage, | |||
| setValue, | |||
| send, | |||
| ], | |||
| ); | |||
| const handleSendMessage = useCallback( | |||
| async (message: Message) => { | |||
| if (conversationId !== '') { | |||
| sendMessage({ message }); | |||
| } else { | |||
| const data = await setConversation(message.content); | |||
| if (data.retcode === 0) { | |||
| const id = data.data.id; | |||
| sendMessage({ message, currentConversationId: id }); | |||
| } | |||
| } | |||
| }, | |||
| [conversationId, setConversation, sendMessage], | |||
| ); | |||
| const { regenerateMessage } = useRegenerateMessage({ | |||
| removeMessagesAfterCurrentMessage, | |||
| sendMessage, | |||
| messages: derivedMessages, | |||
| }); | |||
| useEffect(() => { | |||
| // #1289 | |||
| if (answer.answer && answer?.conversationId === conversationId) { | |||
| addNewestAnswer(answer); | |||
| } | |||
| }, [answer, addNewestAnswer, conversationId]); | |||
| useEffect(() => { | |||
| // #1289 switch to another conversion window when the last conversion answer doesn't finish. | |||
| if (conversationId) { | |||
| setDone(true); | |||
| } | |||
| }, [setDone, conversationId]); | |||
| const handlePressEnter = useCallback( | |||
| (documentIds: string[]) => { | |||
| if (trim(value) === '') return; | |||
| const id = uuid(); | |||
| addNewestQuestion({ | |||
| content: value, | |||
| doc_ids: documentIds, | |||
| id, | |||
| role: MessageType.User, | |||
| }); | |||
| if (done) { | |||
| setValue(''); | |||
| handleSendMessage({ | |||
| id, | |||
| content: value.trim(), | |||
| role: MessageType.User, | |||
| doc_ids: documentIds, | |||
| }); | |||
| } | |||
| }, | |||
| [addNewestQuestion, handleSendMessage, done, setValue, value], | |||
| ); | |||
| return { | |||
| handlePressEnter, | |||
| handleInputChange, | |||
| value, | |||
| setValue, | |||
| regenerateMessage, | |||
| sendLoading: !done, | |||
| loading, | |||
| ref, | |||
| derivedMessages, | |||
| removeMessageById, | |||
| }; | |||
| }; | |||
| export const useGetFileIcon = () => { | |||
| const getFileIcon = (filename: string) => { | |||
| const ext: string = getFileExtension(filename); | |||
| @@ -1,13 +1,13 @@ | |||
| import MessageInput from '@/components/message-input'; | |||
| import MessageItem from '@/components/message-item'; | |||
| import { MessageType, SharedFrom } from '@/constants/chat'; | |||
| import { useFetchNextSharedConversation } from '@/hooks/chat-hooks'; | |||
| import { useSendButtonDisabled } from '@/pages/chat/hooks'; | |||
| import { Flex, Spin } from 'antd'; | |||
| import { forwardRef } from 'react'; | |||
| import { | |||
| useCreateSharedConversationOnMount, | |||
| useGetSharedChatSearchParams, | |||
| useSelectCurrentSharedConversation, | |||
| useSendSharedMessage, | |||
| } from '../shared-hooks'; | |||
| import { buildMessageItemReference } from '../utils'; | |||
| @@ -15,28 +15,17 @@ import styles from './index.less'; | |||
| const ChatContainer = () => { | |||
| const { conversationId } = useCreateSharedConversationOnMount(); | |||
| const { | |||
| currentConversation: conversation, | |||
| addNewestConversation, | |||
| removeLatestMessage, | |||
| ref, | |||
| loading, | |||
| setCurrentConversation, | |||
| addNewestAnswer, | |||
| } = useSelectCurrentSharedConversation(conversationId); | |||
| const { data } = useFetchNextSharedConversation(conversationId); | |||
| const { | |||
| handlePressEnter, | |||
| handleInputChange, | |||
| value, | |||
| loading: sendLoading, | |||
| } = useSendSharedMessage( | |||
| conversation, | |||
| addNewestConversation, | |||
| removeLatestMessage, | |||
| setCurrentConversation, | |||
| addNewestAnswer, | |||
| ); | |||
| sendLoading, | |||
| loading, | |||
| ref, | |||
| derivedMessages, | |||
| } = useSendSharedMessage(conversationId); | |||
| const sendDisabled = useSendButtonDisabled(value); | |||
| const { from } = useGetSharedChatSearchParams(); | |||
| @@ -46,17 +35,23 @@ const ChatContainer = () => { | |||
| <Flex flex={1} vertical className={styles.messageContainer}> | |||
| <div> | |||
| <Spin spinning={loading}> | |||
| {conversation?.message?.map((message, i) => { | |||
| {derivedMessages?.map((message, i) => { | |||
| return ( | |||
| <MessageItem | |||
| key={message.id} | |||
| item={message} | |||
| nickname="You" | |||
| reference={buildMessageItemReference(conversation, message)} | |||
| reference={buildMessageItemReference( | |||
| { | |||
| message: derivedMessages, | |||
| reference: data?.data?.reference, | |||
| }, | |||
| message, | |||
| )} | |||
| loading={ | |||
| message.role === MessageType.Assistant && | |||
| sendLoading && | |||
| conversation?.message.length - 1 === i | |||
| derivedMessages?.length - 1 === i | |||
| } | |||
| index={i} | |||
| ></MessageItem> | |||
| @@ -3,22 +3,17 @@ import { | |||
| useCreateNextSharedConversation, | |||
| useFetchNextSharedConversation, | |||
| } from '@/hooks/chat-hooks'; | |||
| import { useSendMessageWithSse } from '@/hooks/logic-hooks'; | |||
| import { IAnswer, Message } from '@/interfaces/database/chat'; | |||
| import { | |||
| useSelectDerivedMessages, | |||
| useSendMessageWithSse, | |||
| } from '@/hooks/logic-hooks'; | |||
| import { Message } from '@/interfaces/database/chat'; | |||
| import api from '@/utils/api'; | |||
| import { buildMessageUuid } from '@/utils/chat'; | |||
| import trim from 'lodash/trim'; | |||
| import { | |||
| Dispatch, | |||
| SetStateAction, | |||
| useCallback, | |||
| useEffect, | |||
| useState, | |||
| } from 'react'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| import { useSearchParams } from 'umi'; | |||
| import { v4 as uuid } from 'uuid'; | |||
| import { useHandleMessageInputChange, useScrollToBottom } from './hooks'; | |||
| import { IClientConversation, IMessage } from './interface'; | |||
| import { useHandleMessageInputChange } from './hooks'; | |||
| export const useCreateSharedConversationOnMount = () => { | |||
| const [currentQueryParameters] = useSearchParams(); | |||
| @@ -46,91 +41,30 @@ export const useCreateSharedConversationOnMount = () => { | |||
| return { conversationId }; | |||
| }; | |||
| export const useSelectCurrentSharedConversation = (conversationId: string) => { | |||
| const [currentConversation, setCurrentConversation] = | |||
| useState<IClientConversation>({} as IClientConversation); | |||
| const { fetchConversation, loading } = useFetchNextSharedConversation(); | |||
| const ref = useScrollToBottom(currentConversation); | |||
| const addNewestConversation = useCallback((message: Partial<Message>) => { | |||
| setCurrentConversation((pre) => { | |||
| return { | |||
| ...pre, | |||
| message: [ | |||
| ...(pre.message ?? []), | |||
| { | |||
| ...message, | |||
| id: buildMessageUuid(message), | |||
| } as IMessage, | |||
| { | |||
| role: MessageType.Assistant, | |||
| content: '', | |||
| id: buildMessageUuid({ ...message, role: MessageType.Assistant }), | |||
| reference: {}, | |||
| } as IMessage, | |||
| ], | |||
| }; | |||
| }); | |||
| }, []); | |||
| 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 fetchConversationOnMount = useCallback(async () => { | |||
| if (conversationId) { | |||
| const data = await fetchConversation(conversationId); | |||
| if (data.retcode === 0) { | |||
| setCurrentConversation(data.data); | |||
| } | |||
| } | |||
| }, [conversationId, fetchConversation]); | |||
| export const useSelectNextSharedMessages = (conversationId: string) => { | |||
| const { data, loading } = useFetchNextSharedConversation(conversationId); | |||
| const { | |||
| derivedMessages, | |||
| ref, | |||
| setDerivedMessages, | |||
| addNewestAnswer, | |||
| addNewestQuestion, | |||
| removeLatestMessage, | |||
| } = useSelectDerivedMessages(); | |||
| useEffect(() => { | |||
| fetchConversationOnMount(); | |||
| }, [fetchConversationOnMount]); | |||
| setDerivedMessages(data?.data?.message); | |||
| }, [setDerivedMessages, data]); | |||
| return { | |||
| currentConversation, | |||
| addNewestConversation, | |||
| derivedMessages, | |||
| addNewestAnswer, | |||
| addNewestQuestion, | |||
| removeLatestMessage, | |||
| loading, | |||
| ref, | |||
| setCurrentConversation, | |||
| addNewestAnswer, | |||
| setDerivedMessages, | |||
| }; | |||
| }; | |||
| @@ -138,28 +72,28 @@ export const useSendButtonDisabled = (value: string) => { | |||
| return trim(value) === ''; | |||
| }; | |||
| export const useSendSharedMessage = ( | |||
| conversation: IClientConversation, | |||
| addNewestConversation: (message: Partial<Message>, answer?: string) => void, | |||
| removeLatestMessage: () => void, | |||
| setCurrentConversation: Dispatch<SetStateAction<IClientConversation>>, | |||
| addNewestAnswer: (answer: IAnswer) => void, | |||
| ) => { | |||
| const conversationId = conversation.id; | |||
| export const useSendSharedMessage = (conversationId: string) => { | |||
| const { createSharedConversation: setConversation } = | |||
| useCreateNextSharedConversation(); | |||
| const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | |||
| const { send, answer, done } = useSendMessageWithSse( | |||
| api.completeExternalConversation, | |||
| ); | |||
| const { | |||
| derivedMessages, | |||
| ref, | |||
| removeLatestMessage, | |||
| addNewestAnswer, | |||
| addNewestQuestion, | |||
| loading, | |||
| } = useSelectNextSharedMessages(conversationId); | |||
| const sendMessage = useCallback( | |||
| async (message: Message, id?: string) => { | |||
| const res = await send({ | |||
| conversation_id: id ?? conversationId, | |||
| quote: false, | |||
| messages: [...(conversation?.message ?? []), message], | |||
| messages: [...(derivedMessages ?? []), message], | |||
| }); | |||
| if (res && (res?.response.status !== 200 || res?.data?.retcode !== 0)) { | |||
| @@ -168,15 +102,7 @@ export const useSendSharedMessage = ( | |||
| removeLatestMessage(); | |||
| } | |||
| }, | |||
| [ | |||
| conversationId, | |||
| conversation?.message, | |||
| // fetchConversation, | |||
| removeLatestMessage, | |||
| setValue, | |||
| send, | |||
| // setCurrentConversation, | |||
| ], | |||
| [conversationId, derivedMessages, removeLatestMessage, setValue, send], | |||
| ); | |||
| const handleSendMessage = useCallback( | |||
| @@ -206,7 +132,7 @@ export const useSendSharedMessage = ( | |||
| const id = uuid(); | |||
| if (done) { | |||
| setValue(''); | |||
| addNewestConversation({ | |||
| addNewestQuestion({ | |||
| content: value, | |||
| doc_ids: documentIds, | |||
| id, | |||
| @@ -219,14 +145,17 @@ export const useSendSharedMessage = ( | |||
| }); | |||
| } | |||
| }, | |||
| [addNewestConversation, done, handleSendMessage, setValue, value], | |||
| [addNewestQuestion, done, handleSendMessage, setValue, value], | |||
| ); | |||
| return { | |||
| handlePressEnter, | |||
| handleInputChange, | |||
| value, | |||
| loading: !done, | |||
| sendLoading: !done, | |||
| ref, | |||
| loading, | |||
| derivedMessages, | |||
| }; | |||
| }; | |||
| @@ -36,7 +36,7 @@ export const buildMessageItemReference = ( | |||
| ); | |||
| const reference = message?.reference | |||
| ? message?.reference | |||
| : conversation.reference[referenceIndex]; | |||
| : (conversation?.reference ?? {})[referenceIndex]; | |||
| return reference; | |||
| }; | |||
| @@ -6,28 +6,23 @@ import { useClickDrawer, useGetFileIcon } from '@/pages/chat/hooks'; | |||
| import { buildMessageItemReference } from '@/pages/chat/utils'; | |||
| import { Button, Drawer, Flex, Input, Spin } from 'antd'; | |||
| import { useSelectCurrentMessages, useSendMessage } from './hooks'; | |||
| import { useSendNextMessage } from './hooks'; | |||
| import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | |||
| import styles from './index.less'; | |||
| const FlowChatBox = () => { | |||
| const { | |||
| ref, | |||
| currentMessages, | |||
| reference, | |||
| addNewestAnswer, | |||
| addNewestQuestion, | |||
| removeLatestMessage, | |||
| loading, | |||
| } = useSelectCurrentMessages(); | |||
| const { | |||
| sendLoading, | |||
| handleInputChange, | |||
| handlePressEnter, | |||
| value, | |||
| loading: sendLoading, | |||
| } = useSendMessage(addNewestQuestion, removeLatestMessage, addNewestAnswer); | |||
| loading, | |||
| ref, | |||
| derivedMessages, | |||
| reference, | |||
| } = useSendNextMessage(); | |||
| const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | |||
| useClickDrawer(); | |||
| useGetFileIcon(); | |||
| @@ -40,26 +35,26 @@ const FlowChatBox = () => { | |||
| <Flex flex={1} vertical className={styles.messageContainer}> | |||
| <div> | |||
| <Spin spinning={loading}> | |||
| {currentMessages?.map((message, i) => { | |||
| {derivedMessages?.map((message, i) => { | |||
| return ( | |||
| <MessageItem | |||
| loading={ | |||
| message.role === MessageType.Assistant && | |||
| sendLoading && | |||
| currentMessages.length - 1 === i | |||
| derivedMessages.length - 1 === i | |||
| } | |||
| key={message.id} | |||
| nickname={userInfo.nickname} | |||
| avatar={userInfo.avatar} | |||
| item={message} | |||
| reference={buildMessageItemReference( | |||
| { message: currentMessages, reference }, | |||
| { message: derivedMessages, reference }, | |||
| message, | |||
| )} | |||
| clickDocumentButton={clickDocumentButton} | |||
| index={i} | |||
| regenerateMessage={() => {}} | |||
| showLikeButton={false} | |||
| sendLoading={sendLoading} | |||
| ></MessageItem> | |||
| ); | |||
| })} | |||
| @@ -3,6 +3,7 @@ import { useFetchFlow } from '@/hooks/flow-hooks'; | |||
| import { | |||
| useHandleMessageInputChange, | |||
| useScrollToBottom, | |||
| useSelectDerivedMessages, | |||
| useSendMessageWithSse, | |||
| } from '@/hooks/logic-hooks'; | |||
| import { IAnswer, Message } from '@/interfaces/database/chat'; | |||
| @@ -91,6 +92,42 @@ export const useSelectCurrentMessages = () => { | |||
| }; | |||
| }; | |||
| export const useSelectNextMessages = () => { | |||
| const { id: id } = useParams(); | |||
| const { data: flowDetail, loading } = useFetchFlow(); | |||
| const messages = flowDetail.dsl.messages; | |||
| const reference = flowDetail.dsl.reference; | |||
| const { | |||
| derivedMessages, | |||
| setDerivedMessages, | |||
| ref, | |||
| addNewestQuestion, | |||
| addNewestAnswer, | |||
| removeLatestMessage, | |||
| removeMessageById, | |||
| removeMessagesAfterCurrentMessage, | |||
| } = useSelectDerivedMessages(); | |||
| useEffect(() => { | |||
| if (id) { | |||
| const nextMessages = messages.map((x) => ({ ...x, id: uuid() })); | |||
| setDerivedMessages(nextMessages); | |||
| } | |||
| }, [messages, id, setDerivedMessages]); | |||
| return { | |||
| reference, | |||
| loading, | |||
| derivedMessages, | |||
| ref, | |||
| addNewestQuestion, | |||
| addNewestAnswer, | |||
| removeLatestMessage, | |||
| removeMessageById, | |||
| removeMessagesAfterCurrentMessage, | |||
| }; | |||
| }; | |||
| export const useSendMessage = ( | |||
| addNewestQuestion: (message: Message, answer?: string) => void, | |||
| removeLatestMessage: () => void, | |||
| @@ -160,3 +197,84 @@ export const useSendMessage = ( | |||
| loading: !done, | |||
| }; | |||
| }; | |||
| export const useSendNextMessage = () => { | |||
| const { | |||
| reference, | |||
| loading, | |||
| derivedMessages, | |||
| ref, | |||
| addNewestQuestion, | |||
| addNewestAnswer, | |||
| removeLatestMessage, | |||
| removeMessageById, | |||
| } = useSelectNextMessages(); | |||
| const { id: flowId } = useParams(); | |||
| const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | |||
| const { refetch } = useFetchFlow(); | |||
| const { send, answer, done } = useSendMessageWithSse(api.runCanvas); | |||
| const sendMessage = useCallback( | |||
| async ({ message }: { message: Message; messages?: Message[] }) => { | |||
| const params: Record<string, unknown> = { | |||
| id: flowId, | |||
| }; | |||
| if (message.content) { | |||
| params.message = message.content; | |||
| params.message_id = message.id; | |||
| } | |||
| const res = await send(params); | |||
| if (receiveMessageError(res)) { | |||
| antMessage.error(res?.data?.retmsg); | |||
| // cancel loading | |||
| setValue(message.content); | |||
| removeLatestMessage(); | |||
| } else { | |||
| refetch(); // pull the message list after sending the message successfully | |||
| } | |||
| }, | |||
| [flowId, removeLatestMessage, setValue, send, refetch], | |||
| ); | |||
| const handleSendMessage = useCallback( | |||
| async (message: Message) => { | |||
| sendMessage({ message }); | |||
| }, | |||
| [sendMessage], | |||
| ); | |||
| useEffect(() => { | |||
| if (answer.answer) { | |||
| addNewestAnswer(answer); | |||
| } | |||
| }, [answer, addNewestAnswer]); | |||
| const handlePressEnter = useCallback(() => { | |||
| if (trim(value) === '') return; | |||
| const id = uuid(); | |||
| if (done) { | |||
| setValue(''); | |||
| handleSendMessage({ id, content: value.trim(), role: MessageType.User }); | |||
| } | |||
| addNewestQuestion({ | |||
| content: value, | |||
| id, | |||
| role: MessageType.User, | |||
| }); | |||
| }, [addNewestQuestion, handleSendMessage, done, setValue, value]); | |||
| return { | |||
| handlePressEnter, | |||
| handleInputChange, | |||
| value, | |||
| sendLoading: !done, | |||
| reference, | |||
| loading, | |||
| derivedMessages, | |||
| ref, | |||
| removeMessageById, | |||
| }; | |||
| }; | |||
| @@ -23,3 +23,12 @@ export const getMessagePureId = (id?: string) => { | |||
| } | |||
| return id; | |||
| }; | |||
| export const buildMessageListWithUuid = (messages?: Message[]) => { | |||
| return ( | |||
| messages?.map((x: Message | IMessage) => ({ | |||
| ...x, | |||
| id: buildMessageUuid(x), | |||
| })) ?? [] | |||
| ); | |||
| }; | |||