### What problem does this PR solve? feat: Regenerate chat message #2088 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.11.0
| @@ -8,8 +8,9 @@ import { | |||
| SoundOutlined, | |||
| SyncOutlined, | |||
| } from '@ant-design/icons'; | |||
| import { Radio } from 'antd'; | |||
| import { Radio, Tooltip } from 'antd'; | |||
| import { useCallback } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import SvgIcon from '../svg-icon'; | |||
| import FeedbackModal from './feedback-modal'; | |||
| import { useRemoveMessage, useSendFeedback } from './hooks'; | |||
| @@ -33,6 +34,7 @@ export const AssistantGroupButton = ({ | |||
| hideModal: hidePromptModal, | |||
| showModal: showPromptModal, | |||
| } = useSetModalState(); | |||
| const { t } = useTranslation(); | |||
| const handleLike = useCallback(() => { | |||
| onFeedbackOk({ thumbup: true }); | |||
| @@ -45,7 +47,9 @@ export const AssistantGroupButton = ({ | |||
| <CopyToClipboard text={content}></CopyToClipboard> | |||
| </Radio.Button> | |||
| <Radio.Button value="b"> | |||
| <SoundOutlined /> | |||
| <Tooltip title={t('chat.read')}> | |||
| <SoundOutlined /> | |||
| </Tooltip> | |||
| </Radio.Button> | |||
| <Radio.Button value="c" onClick={handleLike}> | |||
| <LikeOutlined /> | |||
| @@ -81,27 +85,41 @@ export const AssistantGroupButton = ({ | |||
| interface UserGroupButtonProps extends IRemoveMessageById { | |||
| messageId: string; | |||
| content: string; | |||
| regenerateMessage(): void; | |||
| sendLoading: boolean; | |||
| } | |||
| export const UserGroupButton = ({ | |||
| content, | |||
| messageId, | |||
| sendLoading, | |||
| removeMessageById, | |||
| regenerateMessage, | |||
| }: UserGroupButtonProps) => { | |||
| const { onRemoveMessage, loading } = useRemoveMessage( | |||
| messageId, | |||
| removeMessageById, | |||
| ); | |||
| const { t } = useTranslation(); | |||
| return ( | |||
| <Radio.Group size="small"> | |||
| <Radio.Button value="a"> | |||
| <CopyToClipboard text={content}></CopyToClipboard> | |||
| </Radio.Button> | |||
| <Radio.Button value="b"> | |||
| <SyncOutlined /> | |||
| <Radio.Button | |||
| value="b" | |||
| onClick={regenerateMessage} | |||
| disabled={sendLoading} | |||
| > | |||
| <Tooltip title={t('chat.regenerate')}> | |||
| <SyncOutlined spin={sendLoading} /> | |||
| </Tooltip> | |||
| </Radio.Button> | |||
| <Radio.Button value="c" onClick={onRemoveMessage} disabled={loading}> | |||
| <DeleteOutlined spin={loading} /> | |||
| <Tooltip title={t('common.delete')}> | |||
| <DeleteOutlined spin={loading} /> | |||
| </Tooltip> | |||
| </Radio.Button> | |||
| </Radio.Group> | |||
| ); | |||
| @@ -11,7 +11,7 @@ import { | |||
| useFetchDocumentInfosByIds, | |||
| useFetchDocumentThumbnailsByIds, | |||
| } from '@/hooks/document-hooks'; | |||
| import { IRemoveMessageById } from '@/hooks/logic-hooks'; | |||
| import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks'; | |||
| import { IMessage } from '@/pages/chat/interface'; | |||
| import MarkdownContent from '@/pages/chat/markdown-content'; | |||
| import { getExtension, isImage } from '@/utils/document-util'; | |||
| @@ -24,10 +24,11 @@ import styles from './index.less'; | |||
| const { Text } = Typography; | |||
| interface IProps extends IRemoveMessageById { | |||
| interface IProps extends IRemoveMessageById, IRegenerateMessage { | |||
| item: IMessage; | |||
| reference: IReference; | |||
| loading?: boolean; | |||
| sendLoading?: boolean; | |||
| nickname?: string; | |||
| avatar?: string; | |||
| clickDocumentButton?: (documentId: string, chunk: IChunk) => void; | |||
| @@ -39,9 +40,11 @@ const MessageItem = ({ | |||
| reference, | |||
| loading = false, | |||
| avatar = '', | |||
| sendLoading = false, | |||
| clickDocumentButton, | |||
| index, | |||
| removeMessageById, | |||
| regenerateMessage, | |||
| }: IProps) => { | |||
| const isAssistant = item.role === MessageType.Assistant; | |||
| const isUser = item.role === MessageType.User; | |||
| @@ -73,6 +76,10 @@ const MessageItem = ({ | |||
| [showModal], | |||
| ); | |||
| const handleRegenerateMessage = useCallback(() => { | |||
| regenerateMessage(item); | |||
| }, [regenerateMessage, item]); | |||
| useEffect(() => { | |||
| const ids = item?.doc_ids ?? []; | |||
| if (ids.length) { | |||
| @@ -128,6 +135,8 @@ const MessageItem = ({ | |||
| content={item.content} | |||
| messageId={item.id} | |||
| removeMessageById={removeMessageById} | |||
| regenerateMessage={handleRegenerateMessage} | |||
| sendLoading={sendLoading} | |||
| ></UserGroupButton> | |||
| )} | |||
| @@ -2,7 +2,7 @@ import { Authorization } from '@/constants/authorization'; | |||
| import { LanguageTranslationMap } from '@/constants/common'; | |||
| import { Pagination } from '@/interfaces/common'; | |||
| import { ResponseType } from '@/interfaces/database/base'; | |||
| import { IAnswer } from '@/interfaces/database/chat'; | |||
| 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'; | |||
| @@ -23,6 +23,7 @@ import { | |||
| } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { useDispatch } from 'umi'; | |||
| import { v4 as uuid } from 'uuid'; | |||
| import { useSetModalState, useTranslate } from './common-hooks'; | |||
| import { useSetDocumentParser } from './document-hooks'; | |||
| import { useSetPaginationParams } from './route-hook'; | |||
| @@ -336,6 +337,77 @@ export const useRemoveMessageById = ( | |||
| return { removeMessageById }; | |||
| }; | |||
| export const useRemoveMessagesAfterCurrentMessage = ( | |||
| setCurrentConversation: ( | |||
| callback: (state: IClientConversation) => IClientConversation, | |||
| ) => void, | |||
| ) => { | |||
| const removeMessagesAfterCurrentMessage = useCallback( | |||
| (messageId: string) => { | |||
| setCurrentConversation((pre) => { | |||
| const index = pre.message?.findIndex((x) => x.id === messageId); | |||
| if (index !== -1) { | |||
| let nextMessages = pre.message?.slice(0, index + 2) ?? []; | |||
| const latestMessage = nextMessages.at(-1); | |||
| nextMessages = latestMessage | |||
| ? [ | |||
| ...nextMessages.slice(0, -1), | |||
| { | |||
| ...latestMessage, | |||
| content: '', | |||
| reference: undefined, | |||
| prompt: undefined, | |||
| }, | |||
| ] | |||
| : nextMessages; | |||
| return { | |||
| ...pre, | |||
| message: nextMessages, | |||
| }; | |||
| } | |||
| return pre; | |||
| }); | |||
| }, | |||
| [setCurrentConversation], | |||
| ); | |||
| return { removeMessagesAfterCurrentMessage }; | |||
| }; | |||
| export interface IRegenerateMessage { | |||
| regenerateMessage(message: Message): void; | |||
| } | |||
| export const useRegenerateMessage = ({ | |||
| removeMessagesAfterCurrentMessage, | |||
| sendMessage, | |||
| messages, | |||
| }: { | |||
| removeMessagesAfterCurrentMessage(messageId: string): void; | |||
| sendMessage({ message }: { message: Message; messages?: Message[] }): void; | |||
| messages: Message[]; | |||
| }) => { | |||
| const regenerateMessage = useCallback( | |||
| async (message: Message) => { | |||
| if (message.id) { | |||
| removeMessagesAfterCurrentMessage(message.id); | |||
| const index = messages.findIndex((x) => x.id === message.id); | |||
| let nextMessages; | |||
| if (index !== -1) { | |||
| nextMessages = messages.slice(0, index); | |||
| } | |||
| sendMessage({ | |||
| message: { ...message, id: uuid() }, | |||
| messages: nextMessages, | |||
| }); | |||
| } | |||
| }, | |||
| [removeMessagesAfterCurrentMessage, sendMessage, messages], | |||
| ); | |||
| return { regenerateMessage }; | |||
| }; | |||
| // #endregion | |||
| /** | |||
| @@ -425,6 +425,8 @@ The above is the content you need to summarize.`, | |||
| parsing: 'Parsing', | |||
| uploading: 'Uploading', | |||
| uploadFailed: 'Upload failed', | |||
| regenerate: 'Regenerate', | |||
| read: 'Read content', | |||
| }, | |||
| setting: { | |||
| profile: 'Profile', | |||
| @@ -395,6 +395,8 @@ export default { | |||
| parsing: '解析中', | |||
| uploading: '上傳中', | |||
| uploadFailed: '上傳失敗', | |||
| regenerate: '重新生成', | |||
| read: '朗讀內容', | |||
| }, | |||
| setting: { | |||
| profile: '概述', | |||
| @@ -412,6 +412,8 @@ export default { | |||
| parsing: '解析中', | |||
| uploading: '上传中', | |||
| uploadFailed: '上传失败', | |||
| regenerate: '重新生成', | |||
| read: '朗读内容', | |||
| }, | |||
| setting: { | |||
| profile: '概要', | |||
| @@ -28,17 +28,20 @@ const ChatContainer = () => { | |||
| conversationId, | |||
| loading, | |||
| removeMessageById, | |||
| removeMessagesAfterCurrentMessage, | |||
| } = useFetchConversationOnMount(); | |||
| const { | |||
| handleInputChange, | |||
| handlePressEnter, | |||
| value, | |||
| loading: sendLoading, | |||
| regenerateMessage, | |||
| } = useSendMessage( | |||
| conversation, | |||
| addNewestConversation, | |||
| removeLatestMessage, | |||
| addNewestAnswer, | |||
| removeMessagesAfterCurrentMessage, | |||
| ); | |||
| const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | |||
| useClickDrawer(); | |||
| @@ -71,6 +74,8 @@ const ChatContainer = () => { | |||
| clickDocumentButton={clickDocumentButton} | |||
| index={i} | |||
| removeMessageById={removeMessageById} | |||
| regenerateMessage={regenerateMessage} | |||
| sendLoading={sendLoading} | |||
| ></MessageItem> | |||
| ); | |||
| })} | |||
| @@ -18,7 +18,9 @@ import { | |||
| useTranslate, | |||
| } from '@/hooks/common-hooks'; | |||
| import { | |||
| useRegenerateMessage, | |||
| useRemoveMessageById, | |||
| useRemoveMessagesAfterCurrentMessage, | |||
| useSendMessageWithSse, | |||
| } from '@/hooks/logic-hooks'; | |||
| import { | |||
| @@ -255,6 +257,8 @@ export const useSelectCurrentConversation = () => { | |||
| 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( | |||
| @@ -353,6 +357,7 @@ export const useSelectCurrentConversation = () => { | |||
| removeLatestMessage, | |||
| addNewestAnswer, | |||
| removeMessageById, | |||
| removeMessagesAfterCurrentMessage, | |||
| loading, | |||
| }; | |||
| }; | |||
| @@ -382,6 +387,7 @@ export const useFetchConversationOnMount = () => { | |||
| addNewestAnswer, | |||
| loading, | |||
| removeMessageById, | |||
| removeMessagesAfterCurrentMessage, | |||
| } = useSelectCurrentConversation(); | |||
| const ref = useScrollToBottom(currentConversation); | |||
| @@ -394,6 +400,7 @@ export const useFetchConversationOnMount = () => { | |||
| conversationId, | |||
| loading, | |||
| removeMessageById, | |||
| removeMessagesAfterCurrentMessage, | |||
| }; | |||
| }; | |||
| @@ -418,6 +425,7 @@ export const useSendMessage = ( | |||
| addNewestConversation: (message: Message, answer?: string) => void, | |||
| removeLatestMessage: () => void, | |||
| addNewestAnswer: (answer: IAnswer) => void, | |||
| removeMessagesAfterCurrentMessage: (messageId: string) => void, | |||
| ) => { | |||
| const { setConversation } = useSetConversation(); | |||
| const { conversationId } = useGetChatSearchParams(); | |||
| @@ -427,16 +435,18 @@ export const useSendMessage = ( | |||
| const { send, answer, done, setDone } = useSendMessageWithSse(); | |||
| const sendMessage = useCallback( | |||
| async (message: Message, documentIds: string[], id?: string) => { | |||
| async ({ | |||
| message, | |||
| currentConversationId, | |||
| messages, | |||
| }: { | |||
| message: Message; | |||
| currentConversationId?: string; | |||
| messages?: Message[]; | |||
| }) => { | |||
| const res = await send({ | |||
| conversation_id: id ?? conversationId, | |||
| messages: [ | |||
| ...(conversation?.message ?? []), | |||
| { | |||
| ...message, | |||
| doc_ids: documentIds, | |||
| }, | |||
| ], | |||
| conversation_id: currentConversationId ?? conversationId, | |||
| messages: [...(messages ?? conversation?.message ?? []), message], | |||
| }); | |||
| if (res && (res?.response.status !== 200 || res?.data?.retcode !== 0)) { | |||
| @@ -445,10 +455,10 @@ export const useSendMessage = ( | |||
| console.info('removeLatestMessage111'); | |||
| removeLatestMessage(); | |||
| } else { | |||
| if (id) { | |||
| if (currentConversationId) { | |||
| console.info('111'); | |||
| // new conversation | |||
| handleClickConversation(id); | |||
| handleClickConversation(currentConversationId); | |||
| } else { | |||
| console.info('222'); | |||
| // fetchConversation(conversationId); | |||
| @@ -466,20 +476,26 @@ export const useSendMessage = ( | |||
| ); | |||
| const handleSendMessage = useCallback( | |||
| async (message: Message, documentIds: string[]) => { | |||
| async (message: Message) => { | |||
| if (conversationId !== '') { | |||
| sendMessage(message, documentIds); | |||
| sendMessage({ message }); | |||
| } else { | |||
| const data = await setConversation(message.content); | |||
| if (data.retcode === 0) { | |||
| const id = data.data.id; | |||
| sendMessage(message, documentIds, id); | |||
| sendMessage({ message, currentConversationId: id }); | |||
| } | |||
| } | |||
| }, | |||
| [conversationId, setConversation, sendMessage], | |||
| ); | |||
| const { regenerateMessage } = useRegenerateMessage({ | |||
| removeMessagesAfterCurrentMessage, | |||
| sendMessage, | |||
| messages: conversation.message, | |||
| }); | |||
| useEffect(() => { | |||
| // #1289 | |||
| if (answer.answer && answer?.conversationId === conversationId) { | |||
| @@ -507,10 +523,12 @@ export const useSendMessage = ( | |||
| }); | |||
| if (done) { | |||
| setValue(''); | |||
| handleSendMessage( | |||
| { id, content: value.trim(), role: MessageType.User }, | |||
| documentIds, | |||
| ); | |||
| handleSendMessage({ | |||
| id, | |||
| content: value.trim(), | |||
| role: MessageType.User, | |||
| doc_ids: documentIds, | |||
| }); | |||
| } | |||
| }, | |||
| [addNewestConversation, handleSendMessage, done, setValue, value], | |||
| @@ -521,6 +539,7 @@ export const useSendMessage = ( | |||
| handleInputChange, | |||
| value, | |||
| setValue, | |||
| regenerateMessage, | |||
| loading: !done, | |||
| }; | |||
| }; | |||
| @@ -16,8 +16,8 @@ export const buildMessageUuid = (message: Partial<Message | IMessage>) => { | |||
| return uuid(); | |||
| }; | |||
| export const getMessagePureId = (id: string) => { | |||
| const strings = id.split('_'); | |||
| export const getMessagePureId = (id?: string) => { | |||
| const strings = id?.split('_') ?? []; | |||
| if (strings.length > 0) { | |||
| return strings.at(-1); | |||
| } | |||