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) => { showDeleteConfirm({ onOk: () => removeDialog(dialogIds) }); }; return { onRemoveDialog }; }; export const useHandleItemHover = () => { const [activated, setActivated] = useState(''); const handleItemEnter = (id: string) => { setActivated(id); }; const handleItemLeave = () => { setActivated(''); }; return { activated, handleItemEnter, handleItemLeave, }; }; export const useEditDialog = () => { const [dialog, setDialog] = useState({} 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>([]); 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(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 = (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(''); 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) => async () => { const ret = await removeConversation(conversationIds); if (ret === 0) { handleClickConversation(''); } return ret; }; const onRemoveConversation = (conversationIds: Array) => { showDeleteConfirm({ onOk: deleteConversation(conversationIds) }); }; return { onRemoveConversation }; }; export const useRenameConversation = () => { const [conversation, setConversation] = useState( {} 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