diff --git a/web/src/pages/next-chats/chat/chat-box/multiple-chat-box.tsx b/web/src/pages/next-chats/chat/chat-box/next-multiple-chat-box.tsx similarity index 73% rename from web/src/pages/next-chats/chat/chat-box/multiple-chat-box.tsx rename to web/src/pages/next-chats/chat/chat-box/next-multiple-chat-box.tsx index 48fd2acb17..b00290efcf 100644 --- a/web/src/pages/next-chats/chat/chat-box/multiple-chat-box.tsx +++ b/web/src/pages/next-chats/chat/chat-box/next-multiple-chat-box.tsx @@ -1,6 +1,9 @@ import { LargeModelFormFieldWithoutFilter } from '@/components/large-model-form-field'; import { LlmSettingSchema } from '@/components/llm-setting-items/next'; -import { NextMessageInput } from '@/components/message-input/next'; +import { + NextMessageInput, + NextMessageInputOnPressEnterParameter, +} from '@/components/message-input/next'; import MessageItem from '@/components/message-item'; import PdfSheet from '@/components/pdf-drawer'; import { useClickDrawer } from '@/components/pdf-drawer/hooks'; @@ -13,20 +16,30 @@ import { TooltipTrigger, } from '@/components/ui/tooltip'; import { MessageType } from '@/constants/chat'; -import { useScrollToBottom } from '@/hooks/logic-hooks'; +import { + useHandleMessageInputChange, + useScrollToBottom, +} from '@/hooks/logic-hooks'; import { useFetchDialog, useGetChatSearchParams, useSetDialog, } from '@/hooks/use-chat-request'; import { useFetchUserInfo } from '@/hooks/use-user-setting-request'; -import { IClientConversation, IMessage } from '@/interfaces/database/chat'; +import { IClientConversation } from '@/interfaces/database/chat'; import { buildMessageUuidWithRole } from '@/utils/chat'; import { zodResolver } from '@hookform/resolvers/zod'; import { t } from 'i18next'; -import { isEmpty, omit } from 'lodash'; +import { isEmpty, omit, trim } from 'lodash'; import { ListCheck, Plus, Trash2 } from 'lucide-react'; -import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; +import { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react'; import { useForm, useWatch } from 'react-hook-form'; import { useParams } from 'react-router'; import { z } from 'zod'; @@ -34,9 +47,15 @@ import { useGetSendButtonDisabled, useSendButtonDisabled, } from '../../hooks/use-button-disabled'; +import { useCreateConversationBeforeSendMessage } from '../../hooks/use-chat-url'; import { useCreateConversationBeforeUploadDocument } from '../../hooks/use-create-conversation'; import { useSendMessage } from '../../hooks/use-send-chat-message'; -import { useSendMultipleChatMessage } from '../../hooks/use-send-multiple-message'; +import { + HandlePressEnterType, + useSendSingleMessage, + UseSendSingleMessageParameter, +} from '../../hooks/use-send-single-message'; +import { useUploadFile } from '../../hooks/use-upload-file'; import { buildMessageItemReference } from '../../utils'; import { useAddChatBox } from '../use-add-box'; import { useShowInternet } from '../use-show-internet'; @@ -55,14 +74,14 @@ type MultipleChatBoxProps = { type ChatCardProps = { id: string; idx: number; - derivedMessages: IMessage[]; - sendLoading: boolean; conversation: IClientConversation; + setLoading(id: string, loading: boolean): void; } & Pick< MultipleChatBoxProps, 'controller' | 'removeChatBox' | 'addChatBox' | 'chatBoxIds' > & - Pick, 'clickDocumentButton'>; + Pick, 'clickDocumentButton'> & + UseSendSingleMessageParameter; const ChatCard = forwardRef(function ChatCard( { @@ -72,17 +91,29 @@ const ChatCard = forwardRef(function ChatCard( idx, addChatBox, chatBoxIds, - derivedMessages, - sendLoading, clickDocumentButton, conversation, + value, + setValue, + files, + clearFiles, + setLoading, }: ChatCardProps, ref, ) { const { id: dialogId } = useParams(); const { setDialog } = useSetDialog(); - const { regenerateMessage, removeMessageById } = useSendMessage(controller); + const { removeMessageById, derivedMessages, handlePressEnter, sendLoading } = + useSendSingleMessage({ + controller, + value, + setValue, + files, + clearFiles, + }); + + const { regenerateMessage } = useSendMessage(controller); const messageContainerRef = useRef(null); @@ -120,9 +151,15 @@ const ChatCard = forwardRef(function ChatCard( }); }, [currentDialog, dialogId, form, setDialog]); - useImperativeHandle(ref, () => ({ - getFormData: () => form.getValues(), - })); + useImperativeHandle( + ref, + (): HandlePressEnterType => (params) => + handlePressEnter({ ...params, ...form.getValues() }), + ); + + useEffect(() => { + setLoading(id, sendLoading); + }, [id, sendLoading, setLoading]); return ( @@ -209,26 +246,67 @@ export function MultipleChatBox({ stopOutputMessage, conversation, }: MultipleChatBoxProps) { - const { - value, - sendLoading, - messageRecord, - handleInputChange, - handlePressEnter, - setFormRef, - handleUploadFile, - } = useSendMultipleChatMessage(controller, chatBoxIds); + const { createConversationBeforeSendMessage } = + useCreateConversationBeforeSendMessage(); const { createConversationBeforeUploadDocument } = useCreateConversationBeforeUploadDocument(); const { conversationId } = useGetChatSearchParams(); const disabled = useGetSendButtonDisabled(); - const sendDisabled = useSendButtonDisabled(value); const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = useClickDrawer(); + const [chatBoxLoading, setChatBoxLoading] = useState>( + new Map(), + ); + + const setLoading = useCallback((id: string, loading: boolean) => { + setChatBoxLoading((prev) => { + const newMap = new Map(prev); + newMap.set(id, loading); + return newMap; + }); + }, []); + + const allChatBoxLoading = [...chatBoxLoading.values()]; + const showInternet = useShowInternet(); + const { handleInputChange, value, setValue } = useHandleMessageInputChange(); + const { handleUploadFile, isUploading, files, clearFiles, removeFile } = + useUploadFile(); + const sendDisabled = useSendButtonDisabled(value); + + const boxesRef = useRef>({}); + + const setFormRef = (id: string) => (ref: HandlePressEnterType) => { + boxesRef.current[id] = ref; + }; + + const handlePressEnter = useCallback( + async ({ + enableInternet, + enableThinking, + }: NextMessageInputOnPressEnterParameter) => { + if (trim(value) === '') return; + + const data = await createConversationBeforeSendMessage(value); + + if (data === undefined) { + return; + } + + Object.values(boxesRef.current).forEach((box) => { + box?.({ + enableInternet, + enableThinking, + ...data, + }); + }); + }, + [createConversationBeforeSendMessage, value], + ); + return (
@@ -241,11 +319,14 @@ export function MultipleChatBox({ chatBoxIds={chatBoxIds} removeChatBox={removeChatBox} addChatBox={addChatBox} - derivedMessages={messageRecord[id]} ref={setFormRef(id)} - sendLoading={sendLoading} clickDocumentButton={clickDocumentButton} conversation={conversation} + value={value} + files={files} + setValue={setValue} + clearFiles={clearFiles} + setLoading={setLoading} > ))}
@@ -253,7 +334,7 @@ export function MultipleChatBox({ loading)} value={value} resize="vertical" onInputChange={handleInputChange} @@ -266,6 +347,8 @@ export function MultipleChatBox({ onUpload={handleUploadFile} showReasoning showInternet={showInternet} + removeFile={removeFile} + isUploading={isUploading} /> {visible && ( diff --git a/web/src/pages/next-chats/chat/index.tsx b/web/src/pages/next-chats/chat/index.tsx index 3280da5133..d769677e3d 100644 --- a/web/src/pages/next-chats/chat/index.tsx +++ b/web/src/pages/next-chats/chat/index.tsx @@ -29,7 +29,7 @@ import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router'; import { useHandleClickConversationCard } from '../hooks/use-click-card'; import { ChatSettings } from './app-settings/chat-settings'; -import { MultipleChatBox } from './chat-box/multiple-chat-box'; +import { MultipleChatBox } from './chat-box/next-multiple-chat-box'; import { SingleChatBox } from './chat-box/single-chat-box'; import { Sessions } from './sessions'; import { useAddChatBox } from './use-add-box'; diff --git a/web/src/pages/next-chats/hooks/use-chat-url.ts b/web/src/pages/next-chats/hooks/use-chat-url.ts index 70cc73ccef..a1c736bb96 100644 --- a/web/src/pages/next-chats/hooks/use-chat-url.ts +++ b/web/src/pages/next-chats/hooks/use-chat-url.ts @@ -95,3 +95,11 @@ export function useCreateConversationBeforeSendMessage() { createConversationBeforeSendMessage, }; } + +export type CreateConversationBeforeSendMessageType = ReturnType< + typeof useCreateConversationBeforeSendMessage +>['createConversationBeforeSendMessage']; + +export type CreateConversationBeforeSendMessageReturnType = Awaited< + ReturnType +>; diff --git a/web/src/pages/next-chats/hooks/use-send-multiple-message.ts b/web/src/pages/next-chats/hooks/use-send-multiple-message.ts deleted file mode 100644 index 25187afed4..0000000000 --- a/web/src/pages/next-chats/hooks/use-send-multiple-message.ts +++ /dev/null @@ -1,271 +0,0 @@ -import { NextMessageInputOnPressEnterParameter } from '@/components/message-input/next'; -import showMessage from '@/components/ui/message'; -import { MessageType } from '@/constants/chat'; -import { - useHandleMessageInputChange, - useSendMessageWithSse, -} from '@/hooks/logic-hooks'; -import { useGetChatSearchParams } from '@/hooks/use-chat-request'; -import { IAnswer, IMessage, Message } from '@/interfaces/database/chat'; -import api from '@/utils/api'; -import { buildMessageUuid } from '@/utils/chat'; -import { trim } from 'lodash'; -import { useCallback, useEffect, useState } from 'react'; -import { v4 as uuid } from 'uuid'; -import { useBuildFormRefs } from './use-build-form-refs'; -import { useCreateConversationBeforeSendMessage } from './use-chat-url'; -import { useUploadFile } from './use-upload-file'; - -export function useSendMultipleChatMessage( - controller: AbortController, - chatBoxIds: string[], -) { - const [messageRecord, setMessageRecord] = useState< - Record - >({}); - - const { conversationId } = useGetChatSearchParams(); - - const { handleInputChange, value, setValue } = useHandleMessageInputChange(); - const { send, answer, allDone } = useSendMessageWithSse( - api.completeConversation, - ); - - const { handleUploadFile, isUploading, files, clearFiles, removeFile } = - useUploadFile(); - - const { createConversationBeforeSendMessage } = - useCreateConversationBeforeSendMessage(); - - const { setFormRef, getLLMConfigById, isLLMConfigEmpty } = - useBuildFormRefs(chatBoxIds); - - const addNewestQuestion = useCallback( - (message: Message, answer: string = '') => { - setMessageRecord((pre) => { - const currentRecord = { ...pre }; - const chatBoxId = message.chatBoxId; - if (typeof chatBoxId === 'string') { - const currentChatMessages = currentRecord[chatBoxId]; - - const nextChatMessages = [ - ...currentChatMessages, - { - ...message, - id: buildMessageUuid(message), // The message id is generated on the front end, - // and the message id returned by the back end is the same as the question id, - // so that the pair of messages can be deleted together when deleting the message - }, - { - role: MessageType.Assistant, - content: answer, - id: buildMessageUuid({ ...message, role: MessageType.Assistant }), - }, - ]; - - currentRecord[chatBoxId] = nextChatMessages; - } - - return currentRecord; - }); - }, - [], - ); - - // Add the streaming message to the last item in the message list - const addNewestAnswer = useCallback((answer: IAnswer) => { - setMessageRecord((pre) => { - const currentRecord = { ...pre }; - const chatBoxId = answer.chatBoxId; - if (typeof chatBoxId === 'string') { - const currentChatMessages = currentRecord[chatBoxId]; - - const nextChatMessages = [ - ...(currentChatMessages?.slice(0, -1) ?? []), - { - role: MessageType.Assistant, - content: answer.answer, - reference: answer.reference, - id: buildMessageUuid({ - id: answer.id, - role: MessageType.Assistant, - }), - prompt: answer.prompt, - audio_binary: answer.audio_binary, - }, - ]; - - currentRecord[chatBoxId] = nextChatMessages; - } - - return currentRecord; - }); - }, []); - - const removeLatestMessage = useCallback((chatBoxId?: string) => { - setMessageRecord((pre) => { - const currentRecord = { ...pre }; - if (chatBoxId) { - const currentChatMessages = currentRecord[chatBoxId]; - if (currentChatMessages) { - currentRecord[chatBoxId] = currentChatMessages.slice(0, -1); - } - } - return currentRecord; - }); - }, []); - - const adjustRecordByChatBoxIds = useCallback(() => { - setMessageRecord((pre) => { - const currentRecord = { ...pre }; - chatBoxIds.forEach((chatBoxId) => { - if (!currentRecord[chatBoxId]) { - currentRecord[chatBoxId] = []; - } - }); - Object.keys(currentRecord).forEach((chatBoxId) => { - if (!chatBoxIds.includes(chatBoxId)) { - delete currentRecord[chatBoxId]; - } - }); - return currentRecord; - }); - }, [chatBoxIds, setMessageRecord]); - - const sendMessage = useCallback( - async ({ - message, - currentConversationId, - messages, - chatBoxId, - enableInternet, - enableThinking, - }: { - message: Message; - currentConversationId?: string; - chatBoxId: string; - messages?: Message[]; - } & NextMessageInputOnPressEnterParameter) => { - let derivedMessages: IMessage[] = []; - - derivedMessages = messageRecord[chatBoxId]; - - const res = await send( - { - chatBoxId, - conversation_id: currentConversationId ?? conversationId, - messages: [...(messages ?? derivedMessages ?? []), message], - reasoning: enableThinking, - internet: enableInternet, - ...getLLMConfigById(chatBoxId), - }, - controller, - ); - - if (res && (res?.response.status !== 200 || res?.data?.code !== 0)) { - // cancel loading - setValue(message.content); - showMessage.error(res.data.message); - removeLatestMessage(chatBoxId); - } - }, - [ - send, - conversationId, - getLLMConfigById, - controller, - messageRecord, - setValue, - removeLatestMessage, - ], - ); - - const handlePressEnter = useCallback( - async ({ - enableThinking, - enableInternet, - }: NextMessageInputOnPressEnterParameter) => { - if (trim(value) === '') return; - const id = uuid(); - - const data = await createConversationBeforeSendMessage(value); - - if (data === undefined) { - return; - } - - const { targetConversationId, currentMessages } = data; - - chatBoxIds.forEach((chatBoxId) => { - if (!isLLMConfigEmpty(chatBoxId)) { - addNewestQuestion({ - content: value, - id, - role: MessageType.User, - chatBoxId, - files, - conversationId: targetConversationId, - }); - } - }); - - if (allDone) { - setValue(''); - chatBoxIds.forEach((chatBoxId) => { - if (!isLLMConfigEmpty(chatBoxId)) { - sendMessage({ - message: { - id, - content: value.trim(), - role: MessageType.User, - files, - conversationId: targetConversationId, - }, - chatBoxId, - currentConversationId: targetConversationId, - messages: currentMessages, - enableThinking, - enableInternet, - }); - } - }); - } - clearFiles(); - }, - [ - value, - createConversationBeforeSendMessage, - chatBoxIds, - allDone, - clearFiles, - isLLMConfigEmpty, - addNewestQuestion, - files, - setValue, - sendMessage, - ], - ); - - useEffect(() => { - if (answer.answer && conversationId) { - addNewestAnswer(answer); - } - }, [answer, addNewestAnswer, conversationId]); - - useEffect(() => { - adjustRecordByChatBoxIds(); - }, [adjustRecordByChatBoxIds]); - - return { - value, - messageRecord, - sendMessage, - handleInputChange, - handlePressEnter, - sendLoading: !allDone, - setFormRef, - handleUploadFile, - isUploading, - removeFile, - }; -} diff --git a/web/src/pages/next-chats/hooks/use-send-single-message.ts b/web/src/pages/next-chats/hooks/use-send-single-message.ts new file mode 100644 index 0000000000..ef624ef603 --- /dev/null +++ b/web/src/pages/next-chats/hooks/use-send-single-message.ts @@ -0,0 +1,159 @@ +import { NextMessageInputOnPressEnterParameter } from '@/components/message-input/next'; +import { MessageType } from '@/constants/chat'; +import { + useHandleMessageInputChange, + useSelectDerivedMessages, + useSendMessageWithSse, +} from '@/hooks/logic-hooks'; +import { useGetChatSearchParams } from '@/hooks/use-chat-request'; +import { IMessage } from '@/interfaces/database/chat'; +import api from '@/utils/api'; +import { useCallback, useEffect } from 'react'; +import { v4 as uuid } from 'uuid'; +import { CreateConversationBeforeSendMessageReturnType } from './use-chat-url'; +import { useUploadFile } from './use-upload-file'; + +export type UseSendSingleMessageParameter = { + controller: AbortController; +} & Pick, 'value' | 'setValue'> & + Pick, 'files' | 'clearFiles'>; + +export function useSendSingleMessage({ + controller, + value, + setValue, + files, + clearFiles, +}: { + controller: AbortController; +} & Pick, 'value' | 'setValue'> & + Pick, 'files' | 'clearFiles'>) { + const { conversationId } = useGetChatSearchParams(); + + const { send, answer, done } = useSendMessageWithSse( + api.completeConversation, + ); + + const { + scrollRef, + messageContainerRef, + setDerivedMessages, + derivedMessages, + addNewestAnswer, + addNewestQuestion, + removeLatestMessage, + removeMessageById, + removeMessagesAfterCurrentMessage, + } = useSelectDerivedMessages(); + + useEffect(() => { + if (answer.answer) { + addNewestAnswer(answer); + } + }, [answer, addNewestAnswer]); + + const sendMessage = useCallback( + async ({ + message, + currentConversationId, + messages, + enableInternet, + enableThinking, + ...params + }: { + message: IMessage; + currentConversationId?: string; + messages?: IMessage[]; + } & NextMessageInputOnPressEnterParameter) => { + const res = await send( + { + conversation_id: currentConversationId ?? conversationId, + messages: [ + ...(Array.isArray(messages) && messages?.length > 0 + ? messages + : (derivedMessages ?? [])), + message, + ], + reasoning: enableThinking, + internet: enableInternet, + ...params, + }, + controller, + ); + + if (res && (res?.response.status !== 200 || res?.data?.code !== 0)) { + // cancel loading + setValue(message.content); + console.info('removeLatestMessage111'); + removeLatestMessage(); + } + }, + [ + derivedMessages, + conversationId, + removeLatestMessage, + setValue, + send, + controller, + ], + ); + + const handlePressEnter = useCallback( + async ({ + enableThinking, + enableInternet, + currentMessages, + targetConversationId, + ...params + }: NextMessageInputOnPressEnterParameter & + CreateConversationBeforeSendMessageReturnType) => { + const id = uuid(); + + addNewestQuestion({ + content: value, + files: files, + id, + role: MessageType.User, + conversationId: targetConversationId, + }); + + if (done) { + setValue(''); + sendMessage({ + currentConversationId: targetConversationId, + messages: currentMessages, + message: { + id, + content: value.trim(), + role: MessageType.User, + files: files, + conversationId: targetConversationId, + }, + enableInternet, + enableThinking, + ...params, + }); + } + clearFiles(); + }, + [addNewestQuestion, value, files, done, clearFiles, setValue, sendMessage], + ); + + return { + scrollRef, + messageContainerRef, + setDerivedMessages, + derivedMessages, + addNewestAnswer, + addNewestQuestion, + removeLatestMessage, + removeMessageById, + removeMessagesAfterCurrentMessage, + handlePressEnter, + sendLoading: !done, + }; +} + +export type HandlePressEnterType = ReturnType< + typeof useSendSingleMessage +>['handlePressEnter'];