balibabu
				
			
		feat: When voice is turned on, the page will not display an empty reply message when the answer is empty #1877 (#2447)
		7b68d9e
		
		| import { MessageType } from '@/constants/chat'; | |
| import { fileIconMap } from '@/constants/common'; | |
| import { | |
| useFetchManualConversation, | |
| useFetchManualDialog, | |
| useFetchNextConversation, | |
| useFetchNextConversationList, | |
| useFetchNextDialog, | |
| useGetChatSearchParams, | |
| useRemoveNextConversation, | |
| useRemoveNextDialog, | |
| useSetNextDialog, | |
| useUpdateNextConversation, | |
| } from '@/hooks/chat-hooks'; | |
| import { | |
| useSetModalState, | |
| useShowDeleteConfirm, | |
| useTranslate, | |
| } from '@/hooks/common-hooks'; | |
| import { | |
| useRegenerateMessage, | |
| useSelectDerivedMessages, | |
| useSendMessageWithSse, | |
| } from '@/hooks/logic-hooks'; | |
| import { IConversation, IDialog, Message } from '@/interfaces/database/chat'; | |
| import { getFileExtension } from '@/utils'; | |
| import { useMutationState } from '@tanstack/react-query'; | |
| import { get } from 'lodash'; | |
| import trim from 'lodash/trim'; | |
| import { | |
| ChangeEventHandler, | |
| useCallback, | |
| useEffect, | |
| useMemo, | |
| useRef, | |
| useState, | |
| } from 'react'; | |
| import { useSearchParams } from 'umi'; | |
| import { v4 as uuid } from 'uuid'; | |
| import { ChatSearchParams } from './constants'; | |
| import { | |
| IClientConversation, | |
| IMessage, | |
| VariableTableDataType, | |
| } from './interface'; | |
| export const useSelectCurrentDialog = () => { | |
| const data = useMutationState({ | |
| filters: { mutationKey: ['fetchDialog'] }, | |
| select: (mutation) => { | |
| return get(mutation, 'state.data.data', {}); | |
| }, | |
| }); | |
| return (data.at(-1) ?? {}) as IDialog; | |
| }; | |
| export const useSelectPromptConfigParameters = (): VariableTableDataType[] => { | |
| const { data: currentDialog } = useFetchNextDialog(); | |
| const finalParameters: VariableTableDataType[] = useMemo(() => { | |
| const parameters = currentDialog?.prompt_config?.parameters ?? []; | |
| if (!currentDialog.id) { | |
| // The newly created chat has a default parameter | |
| return [{ key: uuid(), variable: 'knowledge', optional: false }]; | |
| } | |
| return parameters.map((x) => ({ | |
| key: uuid(), | |
| variable: x.key, | |
| optional: x.optional, | |
| })); | |
| }, [currentDialog]); | |
| return finalParameters; | |
| }; | |
| export const useDeleteDialog = () => { | |
| const showDeleteConfirm = useShowDeleteConfirm(); | |
| const { removeDialog } = useRemoveNextDialog(); | |
| const onRemoveDialog = (dialogIds: Array<string>) => { | |
| showDeleteConfirm({ onOk: () => removeDialog(dialogIds) }); | |
| }; | |
| return { onRemoveDialog }; | |
| }; | |
| export const useHandleItemHover = () => { | |
| const [activated, setActivated] = useState<string>(''); | |
| const handleItemEnter = (id: string) => { | |
| setActivated(id); | |
| }; | |
| const handleItemLeave = () => { | |
| setActivated(''); | |
| }; | |
| return { | |
| activated, | |
| handleItemEnter, | |
| handleItemLeave, | |
| }; | |
| }; | |
| export const useEditDialog = () => { | |
| const [dialog, setDialog] = useState<IDialog>({} as IDialog); | |
| const { fetchDialog } = useFetchManualDialog(); | |
| const { setDialog: submitDialog, loading } = useSetNextDialog(); | |
| const { | |
| visible: dialogEditVisible, | |
| hideModal: hideDialogEditModal, | |
| showModal: showDialogEditModal, | |
| } = useSetModalState(); | |
| const hideModal = useCallback(() => { | |
| setDialog({} as IDialog); | |
| hideDialogEditModal(); | |
| }, [hideDialogEditModal]); | |
| const onDialogEditOk = useCallback( | |
| async (dialog: IDialog) => { | |
| const ret = await submitDialog(dialog); | |
| if (ret === 0) { | |
| hideModal(); | |
| } | |
| }, | |
| [submitDialog, hideModal], | |
| ); | |
| const handleShowDialogEditModal = useCallback( | |
| async (dialogId?: string) => { | |
| if (dialogId) { | |
| const ret = await fetchDialog(dialogId); | |
| if (ret.retcode === 0) { | |
| setDialog(ret.data); | |
| } | |
| } | |
| showDialogEditModal(); | |
| }, | |
| [showDialogEditModal, fetchDialog], | |
| ); | |
| const clearDialog = useCallback(() => { | |
| setDialog({} as IDialog); | |
| }, []); | |
| return { | |
| dialogSettingLoading: loading, | |
| initialDialog: dialog, | |
| onDialogEditOk, | |
| dialogEditVisible, | |
| hideDialogEditModal: hideModal, | |
| showDialogEditModal: handleShowDialogEditModal, | |
| clearDialog, | |
| }; | |
| }; | |
| //#region conversation | |
| export const useSelectDerivedConversationList = () => { | |
| const { t } = useTranslate('chat'); | |
| const [list, setList] = useState<Array<IConversation>>([]); | |
| const { data: currentDialog } = useFetchNextDialog(); | |
| const { data: conversationList, loading } = useFetchNextConversationList(); | |
| const { dialogId } = useGetChatSearchParams(); | |
| const prologue = currentDialog?.prompt_config?.prologue ?? ''; | |
| const addTemporaryConversation = useCallback(() => { | |
| setList((pre) => { | |
| if (dialogId) { | |
| const nextList = [ | |
| { | |
| id: '', | |
| name: t('newConversation'), | |
| dialog_id: dialogId, | |
| message: [ | |
| { | |
| content: prologue, | |
| role: MessageType.Assistant, | |
| }, | |
| ], | |
| } as IConversation, | |
| ...conversationList, | |
| ]; | |
| return nextList; | |
| } | |
| return pre; | |
| }); | |
| }, [conversationList, dialogId, prologue, t]); | |
| useEffect(() => { | |
| addTemporaryConversation(); | |
| }, [addTemporaryConversation]); | |
| return { list, addTemporaryConversation, loading }; | |
| }; | |
| export const useClickConversationCard = () => { | |
| const [currentQueryParameters, setSearchParams] = useSearchParams(); | |
| const newQueryParameters: URLSearchParams = useMemo( | |
| () => new URLSearchParams(currentQueryParameters.toString()), | |
| [currentQueryParameters], | |
| ); | |
| const handleClickConversation = useCallback( | |
| (conversationId: string) => { | |
| newQueryParameters.set(ChatSearchParams.ConversationId, conversationId); | |
| setSearchParams(newQueryParameters); | |
| }, | |
| [newQueryParameters, setSearchParams], | |
| ); | |
| return { handleClickConversation }; | |
| }; | |
| export const useSetConversation = () => { | |
| const { dialogId } = useGetChatSearchParams(); | |
| const { updateConversation } = useUpdateNextConversation(); | |
| const setConversation = useCallback( | |
| (message: string) => { | |
| return updateConversation({ | |
| dialog_id: dialogId, | |
| name: message, | |
| message: [ | |
| { | |
| role: MessageType.Assistant, | |
| content: message, | |
| }, | |
| ], | |
| }); | |
| }, | |
| [updateConversation, dialogId], | |
| ); | |
| return { setConversation }; | |
| }; | |
| // export const useScrollToBottom = (currentConversation: IClientConversation) => { | |
| // const ref = useRef<HTMLDivElement>(null); | |
| // 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]); | |
| } | |
| }, [conversationId, dialog, dialogId, setDerivedMessages]); | |
| useEffect(() => { | |
| addPrologue(); | |
| }, [addPrologue]); | |
| useEffect(() => { | |
| if (conversationId) { | |
| setDerivedMessages(conversation.message); | |
| } | |
| }, [conversation.message, conversationId, setDerivedMessages]); | |
| return { | |
| ref, | |
| derivedMessages, | |
| loading, | |
| addNewestAnswer, | |
| addNewestQuestion, | |
| removeLatestMessage, | |
| removeMessageById, | |
| removeMessagesAfterCurrentMessage, | |
| }; | |
| }; | |
| export const useHandleMessageInputChange = () => { | |
| const [value, setValue] = useState(''); | |
| const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => { | |
| const value = e.target.value; | |
| const nextValue = value.replaceAll('\\n', '\n').replaceAll('\\t', '\t'); | |
| setValue(nextValue); | |
| }; | |
| return { | |
| handleInputChange, | |
| value, | |
| setValue, | |
| }; | |
| }; | |
| export const useSendNextMessage = () => { | |
| const { setConversation } = useSetConversation(); | |
| const { conversationId } = useGetChatSearchParams(); | |
| const { handleInputChange, value, setValue } = useHandleMessageInputChange(); | |
| const { handleClickConversation } = useClickConversationCard(); | |
| const { send, answer, done, setDone, resetAnswer } = useSendMessageWithSse(); | |
| const { | |
| ref, | |
| derivedMessages, | |
| loading, | |
| addNewestAnswer, | |
| addNewestQuestion, | |
| removeLatestMessage, | |
| removeMessageById, | |
| removeMessagesAfterCurrentMessage, | |
| } = useSelectNextMessages(); | |
| const { data: dialog } = useFetchNextDialog(); | |
| const currentConversationIdRef = useRef<string>(''); | |
| const redirectToNewConversation = useCallback( | |
| (isPlaying: boolean) => { | |
| if (!conversationId && dialog?.prompt_config?.tts && !isPlaying) { | |
| handleClickConversation(currentConversationIdRef.current); | |
| } | |
| }, | |
| [dialog, handleClickConversation, conversationId], | |
| ); | |
| 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 | |
| if (!dialog?.prompt_config?.tts) { | |
| handleClickConversation(currentConversationId); | |
| } | |
| } else { | |
| console.info('222'); | |
| // fetchConversation(conversationId); | |
| } | |
| } | |
| }, | |
| [ | |
| dialog, | |
| 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; | |
| currentConversationIdRef.current = id; | |
| sendMessage({ | |
| message, | |
| currentConversationId: id, | |
| messages: data.data.message, | |
| }); | |
| } | |
| } | |
| }, | |
| [conversationId, setConversation, sendMessage], | |
| ); | |
| const { regenerateMessage } = useRegenerateMessage({ | |
| removeMessagesAfterCurrentMessage, | |
| sendMessage, | |
| messages: derivedMessages, | |
| }); | |
| useEffect(() => { | |
| // #1289 | |
| console.log('🚀 ~ useEffect ~ answer:', answer, done); | |
| if ( | |
| answer.answer && | |
| (answer?.conversationId === conversationId || | |
| ((!done || (done && answer.audio_binary)) && conversationId === '')) | |
| ) { | |
| addNewestAnswer(answer); | |
| } | |
| }, [answer, addNewestAnswer, conversationId, done]); | |
| useEffect(() => { | |
| // #1289 switch to another conversion window when the last conversion answer doesn't finish. | |
| if (conversationId) { | |
| setDone(true); | |
| } else { | |
| resetAnswer(); | |
| } | |
| }, [setDone, conversationId, resetAnswer]); | |
| 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, | |
| redirectToNewConversation, | |
| }; | |
| }; | |
| export const useGetFileIcon = () => { | |
| const getFileIcon = (filename: string) => { | |
| const ext: string = getFileExtension(filename); | |
| const iconPath = fileIconMap[ext as keyof typeof fileIconMap]; | |
| return `@/assets/svg/file-icon/${iconPath}`; | |
| }; | |
| return getFileIcon; | |
| }; | |
| export const useDeleteConversation = () => { | |
| const { handleClickConversation } = useClickConversationCard(); | |
| const showDeleteConfirm = useShowDeleteConfirm(); | |
| const { removeConversation } = useRemoveNextConversation(); | |
| const deleteConversation = (conversationIds: Array<string>) => async () => { | |
| const ret = await removeConversation(conversationIds); | |
| if (ret === 0) { | |
| handleClickConversation(''); | |
| } | |
| return ret; | |
| }; | |
| const onRemoveConversation = (conversationIds: Array<string>) => { | |
| showDeleteConfirm({ onOk: deleteConversation(conversationIds) }); | |
| }; | |
| return { onRemoveConversation }; | |
| }; | |
| export const useRenameConversation = () => { | |
| const [conversation, setConversation] = useState<IClientConversation>( | |
| {} as IClientConversation, | |
| ); | |
| const { fetchConversation } = useFetchManualConversation(); | |
| const { | |
| visible: conversationRenameVisible, | |
| hideModal: hideConversationRenameModal, | |
| showModal: showConversationRenameModal, | |
| } = useSetModalState(); | |
| const { updateConversation, loading } = useUpdateNextConversation(); | |
| const onConversationRenameOk = useCallback( | |
| async (name: string) => { | |
| const ret = await updateConversation({ | |
| ...conversation, | |
| conversation_id: conversation.id, | |
| name, | |
| }); | |
| if (ret.retcode === 0) { | |
| hideConversationRenameModal(); | |
| } | |
| }, | |
| [updateConversation, conversation, hideConversationRenameModal], | |
| ); | |
| const handleShowConversationRenameModal = useCallback( | |
| async (conversationId: string) => { | |
| const ret = await fetchConversation(conversationId); | |
| if (ret.retcode === 0) { | |
| setConversation(ret.data); | |
| } | |
| showConversationRenameModal(); | |
| }, | |
| [showConversationRenameModal, fetchConversation], | |
| ); | |
| return { | |
| conversationRenameLoading: loading, | |
| initialConversationName: conversation.name, | |
| onConversationRenameOk, | |
| conversationRenameVisible, | |
| hideConversationRenameModal, | |
| showConversationRenameModal: handleShowConversationRenameModal, | |
| }; | |
| }; | |
| export const useGetSendButtonDisabled = () => { | |
| const { dialogId, conversationId } = useGetChatSearchParams(); | |
| return dialogId === '' && conversationId === ''; | |
| }; | |
| export const useSendButtonDisabled = (value: string) => { | |
| return trim(value) === ''; | |
| }; | |
| export const useCreateConversationBeforeUploadDocument = () => { | |
| const { setConversation } = useSetConversation(); | |
| const { dialogId } = useGetChatSearchParams(); | |
| const { handleClickConversation } = useClickConversationCard(); | |
| const createConversationBeforeUploadDocument = useCallback( | |
| async (message: string) => { | |
| const data = await setConversation(message); | |
| if (data.retcode === 0) { | |
| const id = data.data.id; | |
| handleClickConversation(id); | |
| } | |
| return data; | |
| }, | |
| [setConversation, handleClickConversation], | |
| ); | |
| return { | |
| createConversationBeforeUploadDocument, | |
| dialogId, | |
| }; | |
| }; | |
| //#endregion | |