### 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
| SoundOutlined, | SoundOutlined, | ||||
| SyncOutlined, | SyncOutlined, | ||||
| } from '@ant-design/icons'; | } from '@ant-design/icons'; | ||||
| import { Radio } from 'antd'; | |||||
| import { Radio, Tooltip } from 'antd'; | |||||
| import { useCallback } from 'react'; | import { useCallback } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | |||||
| import SvgIcon from '../svg-icon'; | import SvgIcon from '../svg-icon'; | ||||
| import FeedbackModal from './feedback-modal'; | import FeedbackModal from './feedback-modal'; | ||||
| import { useRemoveMessage, useSendFeedback } from './hooks'; | import { useRemoveMessage, useSendFeedback } from './hooks'; | ||||
| hideModal: hidePromptModal, | hideModal: hidePromptModal, | ||||
| showModal: showPromptModal, | showModal: showPromptModal, | ||||
| } = useSetModalState(); | } = useSetModalState(); | ||||
| const { t } = useTranslation(); | |||||
| const handleLike = useCallback(() => { | const handleLike = useCallback(() => { | ||||
| onFeedbackOk({ thumbup: true }); | onFeedbackOk({ thumbup: true }); | ||||
| <CopyToClipboard text={content}></CopyToClipboard> | <CopyToClipboard text={content}></CopyToClipboard> | ||||
| </Radio.Button> | </Radio.Button> | ||||
| <Radio.Button value="b"> | <Radio.Button value="b"> | ||||
| <SoundOutlined /> | |||||
| <Tooltip title={t('chat.read')}> | |||||
| <SoundOutlined /> | |||||
| </Tooltip> | |||||
| </Radio.Button> | </Radio.Button> | ||||
| <Radio.Button value="c" onClick={handleLike}> | <Radio.Button value="c" onClick={handleLike}> | ||||
| <LikeOutlined /> | <LikeOutlined /> | ||||
| interface UserGroupButtonProps extends IRemoveMessageById { | interface UserGroupButtonProps extends IRemoveMessageById { | ||||
| messageId: string; | messageId: string; | ||||
| content: string; | content: string; | ||||
| regenerateMessage(): void; | |||||
| sendLoading: boolean; | |||||
| } | } | ||||
| export const UserGroupButton = ({ | export const UserGroupButton = ({ | ||||
| content, | content, | ||||
| messageId, | messageId, | ||||
| sendLoading, | |||||
| removeMessageById, | removeMessageById, | ||||
| regenerateMessage, | |||||
| }: UserGroupButtonProps) => { | }: UserGroupButtonProps) => { | ||||
| const { onRemoveMessage, loading } = useRemoveMessage( | const { onRemoveMessage, loading } = useRemoveMessage( | ||||
| messageId, | messageId, | ||||
| removeMessageById, | removeMessageById, | ||||
| ); | ); | ||||
| const { t } = useTranslation(); | |||||
| return ( | return ( | ||||
| <Radio.Group size="small"> | <Radio.Group size="small"> | ||||
| <Radio.Button value="a"> | <Radio.Button value="a"> | ||||
| <CopyToClipboard text={content}></CopyToClipboard> | <CopyToClipboard text={content}></CopyToClipboard> | ||||
| </Radio.Button> | </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> | ||||
| <Radio.Button value="c" onClick={onRemoveMessage} disabled={loading}> | <Radio.Button value="c" onClick={onRemoveMessage} disabled={loading}> | ||||
| <DeleteOutlined spin={loading} /> | |||||
| <Tooltip title={t('common.delete')}> | |||||
| <DeleteOutlined spin={loading} /> | |||||
| </Tooltip> | |||||
| </Radio.Button> | </Radio.Button> | ||||
| </Radio.Group> | </Radio.Group> | ||||
| ); | ); |
| useFetchDocumentInfosByIds, | useFetchDocumentInfosByIds, | ||||
| useFetchDocumentThumbnailsByIds, | useFetchDocumentThumbnailsByIds, | ||||
| } from '@/hooks/document-hooks'; | } from '@/hooks/document-hooks'; | ||||
| import { IRemoveMessageById } from '@/hooks/logic-hooks'; | |||||
| import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks'; | |||||
| import { IMessage } from '@/pages/chat/interface'; | import { IMessage } from '@/pages/chat/interface'; | ||||
| import MarkdownContent from '@/pages/chat/markdown-content'; | import MarkdownContent from '@/pages/chat/markdown-content'; | ||||
| import { getExtension, isImage } from '@/utils/document-util'; | import { getExtension, isImage } from '@/utils/document-util'; | ||||
| const { Text } = Typography; | const { Text } = Typography; | ||||
| interface IProps extends IRemoveMessageById { | |||||
| interface IProps extends IRemoveMessageById, IRegenerateMessage { | |||||
| item: IMessage; | item: IMessage; | ||||
| reference: IReference; | reference: IReference; | ||||
| loading?: boolean; | loading?: boolean; | ||||
| sendLoading?: boolean; | |||||
| nickname?: string; | nickname?: string; | ||||
| avatar?: string; | avatar?: string; | ||||
| clickDocumentButton?: (documentId: string, chunk: IChunk) => void; | clickDocumentButton?: (documentId: string, chunk: IChunk) => void; | ||||
| reference, | reference, | ||||
| loading = false, | loading = false, | ||||
| avatar = '', | avatar = '', | ||||
| sendLoading = false, | |||||
| clickDocumentButton, | clickDocumentButton, | ||||
| index, | index, | ||||
| removeMessageById, | removeMessageById, | ||||
| regenerateMessage, | |||||
| }: IProps) => { | }: IProps) => { | ||||
| const isAssistant = item.role === MessageType.Assistant; | const isAssistant = item.role === MessageType.Assistant; | ||||
| const isUser = item.role === MessageType.User; | const isUser = item.role === MessageType.User; | ||||
| [showModal], | [showModal], | ||||
| ); | ); | ||||
| const handleRegenerateMessage = useCallback(() => { | |||||
| regenerateMessage(item); | |||||
| }, [regenerateMessage, item]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const ids = item?.doc_ids ?? []; | const ids = item?.doc_ids ?? []; | ||||
| if (ids.length) { | if (ids.length) { | ||||
| content={item.content} | content={item.content} | ||||
| messageId={item.id} | messageId={item.id} | ||||
| removeMessageById={removeMessageById} | removeMessageById={removeMessageById} | ||||
| regenerateMessage={handleRegenerateMessage} | |||||
| sendLoading={sendLoading} | |||||
| ></UserGroupButton> | ></UserGroupButton> | ||||
| )} | )} | ||||
| 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 } 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 } from '@/pages/chat/interface'; | ||||
| } from 'react'; | } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { useDispatch } from 'umi'; | import { useDispatch } from 'umi'; | ||||
| import { v4 as uuid } from 'uuid'; | |||||
| import { useSetModalState, useTranslate } from './common-hooks'; | import { useSetModalState, useTranslate } from './common-hooks'; | ||||
| import { useSetDocumentParser } from './document-hooks'; | import { useSetDocumentParser } from './document-hooks'; | ||||
| import { useSetPaginationParams } from './route-hook'; | import { useSetPaginationParams } from './route-hook'; | ||||
| return { removeMessageById }; | 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 | // #endregion | ||||
| /** | /** |
| parsing: 'Parsing', | parsing: 'Parsing', | ||||
| uploading: 'Uploading', | uploading: 'Uploading', | ||||
| uploadFailed: 'Upload failed', | uploadFailed: 'Upload failed', | ||||
| regenerate: 'Regenerate', | |||||
| read: 'Read content', | |||||
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: 'Profile', | profile: 'Profile', |
| parsing: '解析中', | parsing: '解析中', | ||||
| uploading: '上傳中', | uploading: '上傳中', | ||||
| uploadFailed: '上傳失敗', | uploadFailed: '上傳失敗', | ||||
| regenerate: '重新生成', | |||||
| read: '朗讀內容', | |||||
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: '概述', | profile: '概述', |
| parsing: '解析中', | parsing: '解析中', | ||||
| uploading: '上传中', | uploading: '上传中', | ||||
| uploadFailed: '上传失败', | uploadFailed: '上传失败', | ||||
| regenerate: '重新生成', | |||||
| read: '朗读内容', | |||||
| }, | }, | ||||
| setting: { | setting: { | ||||
| profile: '概要', | profile: '概要', |
| conversationId, | conversationId, | ||||
| loading, | loading, | ||||
| removeMessageById, | removeMessageById, | ||||
| removeMessagesAfterCurrentMessage, | |||||
| } = useFetchConversationOnMount(); | } = useFetchConversationOnMount(); | ||||
| const { | const { | ||||
| handleInputChange, | handleInputChange, | ||||
| handlePressEnter, | handlePressEnter, | ||||
| value, | value, | ||||
| loading: sendLoading, | loading: sendLoading, | ||||
| regenerateMessage, | |||||
| } = useSendMessage( | } = useSendMessage( | ||||
| conversation, | conversation, | ||||
| addNewestConversation, | addNewestConversation, | ||||
| removeLatestMessage, | removeLatestMessage, | ||||
| addNewestAnswer, | addNewestAnswer, | ||||
| removeMessagesAfterCurrentMessage, | |||||
| ); | ); | ||||
| const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = | ||||
| useClickDrawer(); | useClickDrawer(); | ||||
| clickDocumentButton={clickDocumentButton} | clickDocumentButton={clickDocumentButton} | ||||
| index={i} | index={i} | ||||
| removeMessageById={removeMessageById} | removeMessageById={removeMessageById} | ||||
| regenerateMessage={regenerateMessage} | |||||
| sendLoading={sendLoading} | |||||
| ></MessageItem> | ></MessageItem> | ||||
| ); | ); | ||||
| })} | })} |
| useTranslate, | useTranslate, | ||||
| } from '@/hooks/common-hooks'; | } from '@/hooks/common-hooks'; | ||||
| import { | import { | ||||
| useRegenerateMessage, | |||||
| useRemoveMessageById, | useRemoveMessageById, | ||||
| useRemoveMessagesAfterCurrentMessage, | |||||
| useSendMessageWithSse, | useSendMessageWithSse, | ||||
| } from '@/hooks/logic-hooks'; | } from '@/hooks/logic-hooks'; | ||||
| import { | import { | ||||
| const { data: dialog } = useFetchNextDialog(); | const { data: dialog } = useFetchNextDialog(); | ||||
| const { conversationId, dialogId } = useGetChatSearchParams(); | const { conversationId, dialogId } = useGetChatSearchParams(); | ||||
| const { removeMessageById } = useRemoveMessageById(setCurrentConversation); | const { removeMessageById } = useRemoveMessageById(setCurrentConversation); | ||||
| const { removeMessagesAfterCurrentMessage } = | |||||
| useRemoveMessagesAfterCurrentMessage(setCurrentConversation); | |||||
| // Show the entered message in the conversation immediately after sending the message | // Show the entered message in the conversation immediately after sending the message | ||||
| const addNewestConversation = useCallback( | const addNewestConversation = useCallback( | ||||
| removeLatestMessage, | removeLatestMessage, | ||||
| addNewestAnswer, | addNewestAnswer, | ||||
| removeMessageById, | removeMessageById, | ||||
| removeMessagesAfterCurrentMessage, | |||||
| loading, | loading, | ||||
| }; | }; | ||||
| }; | }; | ||||
| addNewestAnswer, | addNewestAnswer, | ||||
| loading, | loading, | ||||
| removeMessageById, | removeMessageById, | ||||
| removeMessagesAfterCurrentMessage, | |||||
| } = useSelectCurrentConversation(); | } = useSelectCurrentConversation(); | ||||
| const ref = useScrollToBottom(currentConversation); | const ref = useScrollToBottom(currentConversation); | ||||
| conversationId, | conversationId, | ||||
| loading, | loading, | ||||
| removeMessageById, | removeMessageById, | ||||
| removeMessagesAfterCurrentMessage, | |||||
| }; | }; | ||||
| }; | }; | ||||
| addNewestConversation: (message: Message, answer?: string) => void, | addNewestConversation: (message: Message, answer?: string) => void, | ||||
| removeLatestMessage: () => void, | removeLatestMessage: () => void, | ||||
| addNewestAnswer: (answer: IAnswer) => void, | addNewestAnswer: (answer: IAnswer) => void, | ||||
| removeMessagesAfterCurrentMessage: (messageId: string) => void, | |||||
| ) => { | ) => { | ||||
| const { setConversation } = useSetConversation(); | const { setConversation } = useSetConversation(); | ||||
| const { conversationId } = useGetChatSearchParams(); | const { conversationId } = useGetChatSearchParams(); | ||||
| const { send, answer, done, setDone } = useSendMessageWithSse(); | const { send, answer, done, setDone } = useSendMessageWithSse(); | ||||
| const sendMessage = useCallback( | const sendMessage = useCallback( | ||||
| async (message: Message, documentIds: string[], id?: string) => { | |||||
| async ({ | |||||
| message, | |||||
| currentConversationId, | |||||
| messages, | |||||
| }: { | |||||
| message: Message; | |||||
| currentConversationId?: string; | |||||
| messages?: Message[]; | |||||
| }) => { | |||||
| const res = await send({ | 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)) { | if (res && (res?.response.status !== 200 || res?.data?.retcode !== 0)) { | ||||
| console.info('removeLatestMessage111'); | console.info('removeLatestMessage111'); | ||||
| removeLatestMessage(); | removeLatestMessage(); | ||||
| } else { | } else { | ||||
| if (id) { | |||||
| if (currentConversationId) { | |||||
| console.info('111'); | console.info('111'); | ||||
| // new conversation | // new conversation | ||||
| handleClickConversation(id); | |||||
| handleClickConversation(currentConversationId); | |||||
| } else { | } else { | ||||
| console.info('222'); | console.info('222'); | ||||
| // fetchConversation(conversationId); | // fetchConversation(conversationId); | ||||
| ); | ); | ||||
| const handleSendMessage = useCallback( | const handleSendMessage = useCallback( | ||||
| async (message: Message, documentIds: string[]) => { | |||||
| async (message: Message) => { | |||||
| if (conversationId !== '') { | if (conversationId !== '') { | ||||
| sendMessage(message, documentIds); | |||||
| sendMessage({ message }); | |||||
| } else { | } else { | ||||
| const data = await setConversation(message.content); | const data = await setConversation(message.content); | ||||
| if (data.retcode === 0) { | if (data.retcode === 0) { | ||||
| const id = data.data.id; | const id = data.data.id; | ||||
| sendMessage(message, documentIds, id); | |||||
| sendMessage({ message, currentConversationId: id }); | |||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| [conversationId, setConversation, sendMessage], | [conversationId, setConversation, sendMessage], | ||||
| ); | ); | ||||
| const { regenerateMessage } = useRegenerateMessage({ | |||||
| removeMessagesAfterCurrentMessage, | |||||
| sendMessage, | |||||
| messages: conversation.message, | |||||
| }); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| // #1289 | // #1289 | ||||
| if (answer.answer && answer?.conversationId === conversationId) { | if (answer.answer && answer?.conversationId === conversationId) { | ||||
| }); | }); | ||||
| if (done) { | if (done) { | ||||
| setValue(''); | 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], | [addNewestConversation, handleSendMessage, done, setValue, value], | ||||
| handleInputChange, | handleInputChange, | ||||
| value, | value, | ||||
| setValue, | setValue, | ||||
| regenerateMessage, | |||||
| loading: !done, | loading: !done, | ||||
| }; | }; | ||||
| }; | }; |
| return uuid(); | return uuid(); | ||||
| }; | }; | ||||
| export const getMessagePureId = (id: string) => { | |||||
| const strings = id.split('_'); | |||||
| export const getMessagePureId = (id?: string) => { | |||||
| const strings = id?.split('_') ?? []; | |||||
| if (strings.length > 0) { | if (strings.length > 0) { | ||||
| return strings.at(-1); | return strings.at(-1); | ||||
| } | } |