Fix: The output content of the multi-model comparison will disappear. #13227 (#13241)

### What problem does this PR solve?

Fix: The output content of the multi-model comparison will disappear.
#13227
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
balibabu
2026-02-27 19:18:40 +08:00
committed by GitHub
parent 861ebfc6e1
commit 6d0100ca67
5 changed files with 279 additions and 300 deletions

View File

@@ -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<ReturnType<typeof useClickDrawer>, 'clickDocumentButton'>;
Pick<ReturnType<typeof useClickDrawer>, '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<HTMLDivElement>(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 (
<Card className="bg-transparent border flex-1 flex flex-col">
@@ -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<Map<string, boolean>>(
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<Record<string, HandlePressEnterType>>({});
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 (
<section className="h-full flex flex-col px-5">
<div className="flex gap-4 flex-1 px-5 pb-14 min-h-0">
@@ -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}
></ChatCard>
))}
</div>
@@ -253,7 +334,7 @@ export function MultipleChatBox({
<NextMessageInput
disabled={disabled}
sendDisabled={sendDisabled}
sendLoading={sendLoading}
sendLoading={allChatBoxLoading.some((loading) => loading)}
value={value}
resize="vertical"
onInputChange={handleInputChange}
@@ -266,6 +347,8 @@ export function MultipleChatBox({
onUpload={handleUploadFile}
showReasoning
showInternet={showInternet}
removeFile={removeFile}
isUploading={isUploading}
/>
</div>
{visible && (

View File

@@ -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';

View File

@@ -95,3 +95,11 @@ export function useCreateConversationBeforeSendMessage() {
createConversationBeforeSendMessage,
};
}
export type CreateConversationBeforeSendMessageType = ReturnType<
typeof useCreateConversationBeforeSendMessage
>['createConversationBeforeSendMessage'];
export type CreateConversationBeforeSendMessageReturnType = Awaited<
ReturnType<CreateConversationBeforeSendMessageType>
>;

View File

@@ -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<string, IMessage[]>
>({});
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,
};
}

View File

@@ -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<ReturnType<typeof useHandleMessageInputChange>, 'value' | 'setValue'> &
Pick<ReturnType<typeof useUploadFile>, 'files' | 'clearFiles'>;
export function useSendSingleMessage({
controller,
value,
setValue,
files,
clearFiles,
}: {
controller: AbortController;
} & Pick<ReturnType<typeof useHandleMessageInputChange>, 'value' | 'setValue'> &
Pick<ReturnType<typeof useUploadFile>, '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'];