### 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
| interface UserGroupButtonProps extends Partial<IRemoveMessageById> { | interface UserGroupButtonProps extends Partial<IRemoveMessageById> { | ||||
| messageId: string; | messageId: string; | ||||
| content: string; | content: string; | ||||
| regenerateMessage(): void; | |||||
| regenerateMessage?: () => void; | |||||
| sendLoading: boolean; | sendLoading: boolean; | ||||
| } | } | ||||
| <Radio.Button value="a"> | <Radio.Button value="a"> | ||||
| <CopyToClipboard text={content}></CopyToClipboard> | <CopyToClipboard text={content}></CopyToClipboard> | ||||
| </Radio.Button> | </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 && ( | {removeMessageById && ( | ||||
| <Radio.Button value="c" onClick={onRemoveMessage} disabled={loading}> | <Radio.Button value="c" onClick={onRemoveMessage} disabled={loading}> | ||||
| <Tooltip title={t('common.delete')}> | <Tooltip title={t('common.delete')}> |
| ); | ); | ||||
| const handleRegenerateMessage = useCallback(() => { | const handleRegenerateMessage = useCallback(() => { | ||||
| regenerateMessage(item); | |||||
| regenerateMessage?.(item); | |||||
| }, [regenerateMessage, item]); | }, [regenerateMessage, item]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| content={item.content} | content={item.content} | ||||
| messageId={item.id} | messageId={item.id} | ||||
| removeMessageById={removeMessageById} | removeMessageById={removeMessageById} | ||||
| regenerateMessage={handleRegenerateMessage} | |||||
| regenerateMessage={ | |||||
| regenerateMessage && handleRegenerateMessage | |||||
| } | |||||
| sendLoading={sendLoading} | sendLoading={sendLoading} | ||||
| ></UserGroupButton> | ></UserGroupButton> | ||||
| )} | )} |
| IDialog, | IDialog, | ||||
| IStats, | IStats, | ||||
| IToken, | IToken, | ||||
| Message, | |||||
| } from '@/interfaces/database/chat'; | } from '@/interfaces/database/chat'; | ||||
| import { IFeedbackRequestBody } from '@/interfaces/request/chat'; | import { IFeedbackRequestBody } from '@/interfaces/request/chat'; | ||||
| import i18n from '@/locales/config'; | 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 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 { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | ||||
| import { message } from 'antd'; | import { message } from 'antd'; | ||||
| import dayjs, { Dayjs } from 'dayjs'; | import dayjs, { Dayjs } from 'dayjs'; | ||||
| import { useCallback, useMemo, useState } from 'react'; | import { useCallback, useMemo, useState } from 'react'; | ||||
| import { useSearchParams } from 'umi'; | import { useSearchParams } from 'umi'; | ||||
| const buildMessageListWithUuid = (messages?: Message[]) => { | |||||
| return ( | |||||
| messages?.map((x: Message | IMessage) => ({ | |||||
| ...x, | |||||
| id: buildMessageUuid(x), | |||||
| })) ?? [] | |||||
| ); | |||||
| }; | |||||
| //#region logic | //#region logic | ||||
| export const useClickDialogCard = () => { | export const useClickDialogCard = () => { | ||||
| return { data, loading, createSharedConversation: mutateAsync }; | 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( | const { data } = await chatService.getExternalConversation( | ||||
| null, | null, | ||||
| conversationId, | conversationId, | ||||
| }, | }, | ||||
| }); | }); | ||||
| return { data, loading, fetchConversation: mutateAsync }; | |||||
| return { data, loading }; | |||||
| }; | }; | ||||
| //#endregion | //#endregion |
| import { DSL, IFlow, IFlowTemplate } from '@/interfaces/database/flow'; | import { DSL, IFlow, IFlowTemplate } from '@/interfaces/database/flow'; | ||||
| import i18n from '@/locales/config'; | import i18n from '@/locales/config'; | ||||
| import flowService from '@/services/flow-service'; | import flowService from '@/services/flow-service'; | ||||
| import { buildMessageListWithUuid } from '@/utils/chat'; | |||||
| import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; | ||||
| import { message } from 'antd'; | import { message } from 'antd'; | ||||
| import { set } from 'lodash'; | |||||
| import get from 'lodash/get'; | |||||
| import { useParams } from 'umi'; | import { useParams } from 'umi'; | ||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||
| queryFn: async () => { | queryFn: async () => { | ||||
| const { data } = await flowService.getCanvas({}, id); | const { data } = await flowService.getCanvas({}, id); | ||||
| const messageList = buildMessageListWithUuid( | |||||
| get(data, 'data.dsl.messages', []), | |||||
| ); | |||||
| set(data, 'data.dsl.messages', messageList); | |||||
| return data?.data ?? {}; | return data?.data ?? {}; | ||||
| }, | }, | ||||
| }); | }); |
| import { Authorization } from '@/constants/authorization'; | import { Authorization } from '@/constants/authorization'; | ||||
| import { MessageType } from '@/constants/chat'; | |||||
| import { LanguageTranslationMap } from '@/constants/common'; | import { LanguageTranslationMap } from '@/constants/common'; | ||||
| import { Pagination } from '@/interfaces/common'; | import { Pagination } from '@/interfaces/common'; | ||||
| import { ResponseType } from '@/interfaces/database/base'; | import { ResponseType } from '@/interfaces/database/base'; | ||||
| import { IAnswer, Message } from '@/interfaces/database/chat'; | import { IAnswer, Message } from '@/interfaces/database/chat'; | ||||
| import { IKnowledgeFile } from '@/interfaces/database/knowledge'; | import { IKnowledgeFile } from '@/interfaces/database/knowledge'; | ||||
| import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; | 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 api from '@/utils/api'; | ||||
| import { getAuthorization } from '@/utils/authorization-util'; | import { getAuthorization } from '@/utils/authorization-util'; | ||||
| import { getMessagePureId } from '@/utils/chat'; | |||||
| import { buildMessageUuid, getMessagePureId } from '@/utils/chat'; | |||||
| import { PaginationProps } from 'antd'; | import { PaginationProps } from 'antd'; | ||||
| import { FormInstance } from 'antd/lib'; | import { FormInstance } from 'antd/lib'; | ||||
| import axios from 'axios'; | import axios from 'axios'; | ||||
| }; | }; | ||||
| }; | }; | ||||
| 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 { | export interface IRemoveMessageById { | ||||
| removeMessageById(messageId: string): void; | removeMessageById(messageId: string): void; | ||||
| } | } | ||||
| }; | }; | ||||
| export interface IRegenerateMessage { | export interface IRegenerateMessage { | ||||
| regenerateMessage(message: Message): void; | |||||
| regenerateMessage?: (message: Message) => void; | |||||
| } | } | ||||
| export const useRegenerateMessage = ({ | export const useRegenerateMessage = ({ | ||||
| messages, | messages, | ||||
| }: { | }: { | ||||
| removeMessagesAfterCurrentMessage(messageId: string): void; | removeMessagesAfterCurrentMessage(messageId: string): void; | ||||
| sendMessage({ message }: { message: Message; messages?: Message[] }): void; | |||||
| sendMessage({ | |||||
| message, | |||||
| }: { | |||||
| message: Message; | |||||
| messages?: Message[]; | |||||
| }): void | Promise<any>; | |||||
| messages: Message[]; | messages: Message[]; | ||||
| }) => { | }) => { | ||||
| const regenerateMessage = useCallback( | const regenerateMessage = useCallback( |
| import { | import { | ||||
| useClickDrawer, | useClickDrawer, | ||||
| useCreateConversationBeforeUploadDocument, | useCreateConversationBeforeUploadDocument, | ||||
| useFetchConversationOnMount, | |||||
| useGetFileIcon, | useGetFileIcon, | ||||
| useGetSendButtonDisabled, | useGetSendButtonDisabled, | ||||
| useSendButtonDisabled, | useSendButtonDisabled, | ||||
| useSendMessage, | |||||
| useSendNextMessage, | |||||
| } from '../hooks'; | } from '../hooks'; | ||||
| import { buildMessageItemReference } from '../utils'; | import { buildMessageItemReference } from '../utils'; | ||||
| import MessageInput from '@/components/message-input'; | import MessageInput from '@/components/message-input'; | ||||
| import { | |||||
| useFetchNextConversation, | |||||
| useGetChatSearchParams, | |||||
| } from '@/hooks/chat-hooks'; | |||||
| import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | ||||
| import { memo } from 'react'; | import { memo } from 'react'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const ChatContainer = () => { | const ChatContainer = () => { | ||||
| const { conversationId } = useGetChatSearchParams(); | |||||
| const { data: conversation } = useFetchNextConversation(); | |||||
| const { | const { | ||||
| ref, | ref, | ||||
| currentConversation: conversation, | |||||
| addNewestConversation, | |||||
| removeLatestMessage, | |||||
| addNewestAnswer, | |||||
| conversationId, | |||||
| loading, | loading, | ||||
| removeMessageById, | |||||
| removeMessagesAfterCurrentMessage, | |||||
| } = useFetchConversationOnMount(); | |||||
| const { | |||||
| sendLoading, | |||||
| derivedMessages, | |||||
| handleInputChange, | handleInputChange, | ||||
| handlePressEnter, | handlePressEnter, | ||||
| value, | value, | ||||
| loading: sendLoading, | |||||
| regenerateMessage, | regenerateMessage, | ||||
| } = useSendMessage( | |||||
| conversation, | |||||
| addNewestConversation, | |||||
| removeLatestMessage, | |||||
| addNewestAnswer, | |||||
| removeMessagesAfterCurrentMessage, | |||||
| ); | |||||
| removeMessageById, | |||||
| } = useSendNextMessage(); | |||||
| const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | ||||
| useClickDrawer(); | useClickDrawer(); | ||||
| const disabled = useGetSendButtonDisabled(); | const disabled = useGetSendButtonDisabled(); | ||||
| <Flex flex={1} vertical className={styles.messageContainer}> | <Flex flex={1} vertical className={styles.messageContainer}> | ||||
| <div> | <div> | ||||
| <Spin spinning={loading}> | <Spin spinning={loading}> | ||||
| {conversation?.message?.map((message, i) => { | |||||
| {derivedMessages?.map((message, i) => { | |||||
| return ( | return ( | ||||
| <MessageItem | <MessageItem | ||||
| loading={ | loading={ | ||||
| message.role === MessageType.Assistant && | message.role === MessageType.Assistant && | ||||
| sendLoading && | sendLoading && | ||||
| conversation?.message.length - 1 === i | |||||
| derivedMessages.length - 1 === i | |||||
| } | } | ||||
| key={message.id} | key={message.id} | ||||
| item={message} | item={message} | ||||
| nickname={userInfo.nickname} | nickname={userInfo.nickname} | ||||
| avatar={userInfo.avatar} | avatar={userInfo.avatar} | ||||
| reference={buildMessageItemReference(conversation, message)} | |||||
| reference={buildMessageItemReference( | |||||
| { | |||||
| message: derivedMessages, | |||||
| reference: conversation.reference, | |||||
| }, | |||||
| message, | |||||
| )} | |||||
| clickDocumentButton={clickDocumentButton} | clickDocumentButton={clickDocumentButton} | ||||
| index={i} | index={i} | ||||
| removeMessageById={removeMessageById} | removeMessageById={removeMessageById} |
| useRegenerateMessage, | useRegenerateMessage, | ||||
| useRemoveMessageById, | useRemoveMessageById, | ||||
| useRemoveMessagesAfterCurrentMessage, | useRemoveMessagesAfterCurrentMessage, | ||||
| useScrollToBottom, | |||||
| useSelectDerivedMessages, | |||||
| useSendMessageWithSse, | useSendMessageWithSse, | ||||
| } from '@/hooks/logic-hooks'; | } from '@/hooks/logic-hooks'; | ||||
| import { | import { | ||||
| useCallback, | useCallback, | ||||
| useEffect, | useEffect, | ||||
| useMemo, | useMemo, | ||||
| useRef, | |||||
| useState, | useState, | ||||
| } from 'react'; | } from 'react'; | ||||
| import { useSearchParams } from 'umi'; | import { useSearchParams } from 'umi'; | ||||
| }; | }; | ||||
| }; | }; | ||||
| 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(() => { | 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 = () => { | export const useFetchConversationOnMount = () => { | ||||
| }; | }; | ||||
| }; | }; | ||||
| 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 = () => { | export const useGetFileIcon = () => { | ||||
| const getFileIcon = (filename: string) => { | const getFileIcon = (filename: string) => { | ||||
| const ext: string = getFileExtension(filename); | const ext: string = getFileExtension(filename); |
| import MessageInput from '@/components/message-input'; | import MessageInput from '@/components/message-input'; | ||||
| import MessageItem from '@/components/message-item'; | import MessageItem from '@/components/message-item'; | ||||
| import { MessageType, SharedFrom } from '@/constants/chat'; | import { MessageType, SharedFrom } from '@/constants/chat'; | ||||
| import { useFetchNextSharedConversation } from '@/hooks/chat-hooks'; | |||||
| import { useSendButtonDisabled } from '@/pages/chat/hooks'; | import { useSendButtonDisabled } from '@/pages/chat/hooks'; | ||||
| import { Flex, Spin } from 'antd'; | import { Flex, Spin } from 'antd'; | ||||
| import { forwardRef } from 'react'; | import { forwardRef } from 'react'; | ||||
| import { | import { | ||||
| useCreateSharedConversationOnMount, | useCreateSharedConversationOnMount, | ||||
| useGetSharedChatSearchParams, | useGetSharedChatSearchParams, | ||||
| useSelectCurrentSharedConversation, | |||||
| useSendSharedMessage, | useSendSharedMessage, | ||||
| } from '../shared-hooks'; | } from '../shared-hooks'; | ||||
| import { buildMessageItemReference } from '../utils'; | import { buildMessageItemReference } from '../utils'; | ||||
| const ChatContainer = () => { | const ChatContainer = () => { | ||||
| const { conversationId } = useCreateSharedConversationOnMount(); | const { conversationId } = useCreateSharedConversationOnMount(); | ||||
| const { | |||||
| currentConversation: conversation, | |||||
| addNewestConversation, | |||||
| removeLatestMessage, | |||||
| ref, | |||||
| loading, | |||||
| setCurrentConversation, | |||||
| addNewestAnswer, | |||||
| } = useSelectCurrentSharedConversation(conversationId); | |||||
| const { data } = useFetchNextSharedConversation(conversationId); | |||||
| const { | const { | ||||
| handlePressEnter, | handlePressEnter, | ||||
| handleInputChange, | handleInputChange, | ||||
| value, | value, | ||||
| loading: sendLoading, | |||||
| } = useSendSharedMessage( | |||||
| conversation, | |||||
| addNewestConversation, | |||||
| removeLatestMessage, | |||||
| setCurrentConversation, | |||||
| addNewestAnswer, | |||||
| ); | |||||
| sendLoading, | |||||
| loading, | |||||
| ref, | |||||
| derivedMessages, | |||||
| } = useSendSharedMessage(conversationId); | |||||
| const sendDisabled = useSendButtonDisabled(value); | const sendDisabled = useSendButtonDisabled(value); | ||||
| const { from } = useGetSharedChatSearchParams(); | const { from } = useGetSharedChatSearchParams(); | ||||
| <Flex flex={1} vertical className={styles.messageContainer}> | <Flex flex={1} vertical className={styles.messageContainer}> | ||||
| <div> | <div> | ||||
| <Spin spinning={loading}> | <Spin spinning={loading}> | ||||
| {conversation?.message?.map((message, i) => { | |||||
| {derivedMessages?.map((message, i) => { | |||||
| return ( | return ( | ||||
| <MessageItem | <MessageItem | ||||
| key={message.id} | key={message.id} | ||||
| item={message} | item={message} | ||||
| nickname="You" | nickname="You" | ||||
| reference={buildMessageItemReference(conversation, message)} | |||||
| reference={buildMessageItemReference( | |||||
| { | |||||
| message: derivedMessages, | |||||
| reference: data?.data?.reference, | |||||
| }, | |||||
| message, | |||||
| )} | |||||
| loading={ | loading={ | ||||
| message.role === MessageType.Assistant && | message.role === MessageType.Assistant && | ||||
| sendLoading && | sendLoading && | ||||
| conversation?.message.length - 1 === i | |||||
| derivedMessages?.length - 1 === i | |||||
| } | } | ||||
| index={i} | index={i} | ||||
| ></MessageItem> | ></MessageItem> |
| useCreateNextSharedConversation, | useCreateNextSharedConversation, | ||||
| useFetchNextSharedConversation, | useFetchNextSharedConversation, | ||||
| } from '@/hooks/chat-hooks'; | } 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 api from '@/utils/api'; | ||||
| import { buildMessageUuid } from '@/utils/chat'; | |||||
| import trim from 'lodash/trim'; | import trim from 'lodash/trim'; | ||||
| import { | |||||
| Dispatch, | |||||
| SetStateAction, | |||||
| useCallback, | |||||
| useEffect, | |||||
| useState, | |||||
| } from 'react'; | |||||
| import { useCallback, useEffect, useState } from 'react'; | |||||
| import { useSearchParams } from 'umi'; | import { useSearchParams } from 'umi'; | ||||
| import { v4 as uuid } from 'uuid'; | import { v4 as uuid } from 'uuid'; | ||||
| import { useHandleMessageInputChange, useScrollToBottom } from './hooks'; | |||||
| import { IClientConversation, IMessage } from './interface'; | |||||
| import { useHandleMessageInputChange } from './hooks'; | |||||
| export const useCreateSharedConversationOnMount = () => { | export const useCreateSharedConversationOnMount = () => { | ||||
| const [currentQueryParameters] = useSearchParams(); | const [currentQueryParameters] = useSearchParams(); | ||||
| return { conversationId }; | 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(() => { | useEffect(() => { | ||||
| fetchConversationOnMount(); | |||||
| }, [fetchConversationOnMount]); | |||||
| setDerivedMessages(data?.data?.message); | |||||
| }, [setDerivedMessages, data]); | |||||
| return { | return { | ||||
| currentConversation, | |||||
| addNewestConversation, | |||||
| derivedMessages, | |||||
| addNewestAnswer, | |||||
| addNewestQuestion, | |||||
| removeLatestMessage, | removeLatestMessage, | ||||
| loading, | loading, | ||||
| ref, | ref, | ||||
| setCurrentConversation, | |||||
| addNewestAnswer, | |||||
| setDerivedMessages, | |||||
| }; | }; | ||||
| }; | }; | ||||
| return trim(value) === ''; | 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 } = | const { createSharedConversation: setConversation } = | ||||
| useCreateNextSharedConversation(); | useCreateNextSharedConversation(); | ||||
| const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | ||||
| const { send, answer, done } = useSendMessageWithSse( | const { send, answer, done } = useSendMessageWithSse( | ||||
| api.completeExternalConversation, | api.completeExternalConversation, | ||||
| ); | ); | ||||
| const { | |||||
| derivedMessages, | |||||
| ref, | |||||
| removeLatestMessage, | |||||
| addNewestAnswer, | |||||
| addNewestQuestion, | |||||
| loading, | |||||
| } = useSelectNextSharedMessages(conversationId); | |||||
| const sendMessage = useCallback( | const sendMessage = useCallback( | ||||
| async (message: Message, id?: string) => { | async (message: Message, id?: string) => { | ||||
| const res = await send({ | const res = await send({ | ||||
| conversation_id: id ?? conversationId, | conversation_id: id ?? conversationId, | ||||
| quote: false, | quote: false, | ||||
| messages: [...(conversation?.message ?? []), message], | |||||
| messages: [...(derivedMessages ?? []), message], | |||||
| }); | }); | ||||
| if (res && (res?.response.status !== 200 || res?.data?.retcode !== 0)) { | if (res && (res?.response.status !== 200 || res?.data?.retcode !== 0)) { | ||||
| removeLatestMessage(); | removeLatestMessage(); | ||||
| } | } | ||||
| }, | }, | ||||
| [ | |||||
| conversationId, | |||||
| conversation?.message, | |||||
| // fetchConversation, | |||||
| removeLatestMessage, | |||||
| setValue, | |||||
| send, | |||||
| // setCurrentConversation, | |||||
| ], | |||||
| [conversationId, derivedMessages, removeLatestMessage, setValue, send], | |||||
| ); | ); | ||||
| const handleSendMessage = useCallback( | const handleSendMessage = useCallback( | ||||
| const id = uuid(); | const id = uuid(); | ||||
| if (done) { | if (done) { | ||||
| setValue(''); | setValue(''); | ||||
| addNewestConversation({ | |||||
| addNewestQuestion({ | |||||
| content: value, | content: value, | ||||
| doc_ids: documentIds, | doc_ids: documentIds, | ||||
| id, | id, | ||||
| }); | }); | ||||
| } | } | ||||
| }, | }, | ||||
| [addNewestConversation, done, handleSendMessage, setValue, value], | |||||
| [addNewestQuestion, done, handleSendMessage, setValue, value], | |||||
| ); | ); | ||||
| return { | return { | ||||
| handlePressEnter, | handlePressEnter, | ||||
| handleInputChange, | handleInputChange, | ||||
| value, | value, | ||||
| loading: !done, | |||||
| sendLoading: !done, | |||||
| ref, | |||||
| loading, | |||||
| derivedMessages, | |||||
| }; | }; | ||||
| }; | }; | ||||
| ); | ); | ||||
| const reference = message?.reference | const reference = message?.reference | ||||
| ? message?.reference | ? message?.reference | ||||
| : conversation.reference[referenceIndex]; | |||||
| : (conversation?.reference ?? {})[referenceIndex]; | |||||
| return reference; | return reference; | ||||
| }; | }; |
| import { buildMessageItemReference } from '@/pages/chat/utils'; | import { buildMessageItemReference } from '@/pages/chat/utils'; | ||||
| import { Button, Drawer, Flex, Input, Spin } from 'antd'; | 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 { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| const FlowChatBox = () => { | const FlowChatBox = () => { | ||||
| const { | const { | ||||
| ref, | |||||
| currentMessages, | |||||
| reference, | |||||
| addNewestAnswer, | |||||
| addNewestQuestion, | |||||
| removeLatestMessage, | |||||
| loading, | |||||
| } = useSelectCurrentMessages(); | |||||
| const { | |||||
| sendLoading, | |||||
| handleInputChange, | handleInputChange, | ||||
| handlePressEnter, | handlePressEnter, | ||||
| value, | value, | ||||
| loading: sendLoading, | |||||
| } = useSendMessage(addNewestQuestion, removeLatestMessage, addNewestAnswer); | |||||
| loading, | |||||
| ref, | |||||
| derivedMessages, | |||||
| reference, | |||||
| } = useSendNextMessage(); | |||||
| const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | ||||
| useClickDrawer(); | useClickDrawer(); | ||||
| useGetFileIcon(); | useGetFileIcon(); | ||||
| <Flex flex={1} vertical className={styles.messageContainer}> | <Flex flex={1} vertical className={styles.messageContainer}> | ||||
| <div> | <div> | ||||
| <Spin spinning={loading}> | <Spin spinning={loading}> | ||||
| {currentMessages?.map((message, i) => { | |||||
| {derivedMessages?.map((message, i) => { | |||||
| return ( | return ( | ||||
| <MessageItem | <MessageItem | ||||
| loading={ | loading={ | ||||
| message.role === MessageType.Assistant && | message.role === MessageType.Assistant && | ||||
| sendLoading && | sendLoading && | ||||
| currentMessages.length - 1 === i | |||||
| derivedMessages.length - 1 === i | |||||
| } | } | ||||
| key={message.id} | key={message.id} | ||||
| nickname={userInfo.nickname} | nickname={userInfo.nickname} | ||||
| avatar={userInfo.avatar} | avatar={userInfo.avatar} | ||||
| item={message} | item={message} | ||||
| reference={buildMessageItemReference( | reference={buildMessageItemReference( | ||||
| { message: currentMessages, reference }, | |||||
| { message: derivedMessages, reference }, | |||||
| message, | message, | ||||
| )} | )} | ||||
| clickDocumentButton={clickDocumentButton} | clickDocumentButton={clickDocumentButton} | ||||
| index={i} | index={i} | ||||
| regenerateMessage={() => {}} | |||||
| showLikeButton={false} | showLikeButton={false} | ||||
| sendLoading={sendLoading} | |||||
| ></MessageItem> | ></MessageItem> | ||||
| ); | ); | ||||
| })} | })} |
| import { | import { | ||||
| useHandleMessageInputChange, | useHandleMessageInputChange, | ||||
| useScrollToBottom, | useScrollToBottom, | ||||
| useSelectDerivedMessages, | |||||
| useSendMessageWithSse, | useSendMessageWithSse, | ||||
| } from '@/hooks/logic-hooks'; | } from '@/hooks/logic-hooks'; | ||||
| import { IAnswer, Message } from '@/interfaces/database/chat'; | import { IAnswer, Message } from '@/interfaces/database/chat'; | ||||
| }; | }; | ||||
| }; | }; | ||||
| 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 = ( | export const useSendMessage = ( | ||||
| addNewestQuestion: (message: Message, answer?: string) => void, | addNewestQuestion: (message: Message, answer?: string) => void, | ||||
| removeLatestMessage: () => void, | removeLatestMessage: () => void, | ||||
| loading: !done, | 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, | |||||
| }; | |||||
| }; |
| } | } | ||||
| return id; | return id; | ||||
| }; | }; | ||||
| export const buildMessageListWithUuid = (messages?: Message[]) => { | |||||
| return ( | |||||
| messages?.map((x: Message | IMessage) => ({ | |||||
| ...x, | |||||
| id: buildMessageUuid(x), | |||||
| })) ?? [] | |||||
| ); | |||||
| }; |