### What problem does this PR solve? Feat: Send data to compare the performance of different models' answers #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.2
| @@ -202,7 +202,7 @@ export const useSendMessageWithSse = ( | |||
| [Authorization]: getAuthorization(), | |||
| 'Content-Type': 'application/json', | |||
| }, | |||
| body: JSON.stringify(body), | |||
| body: JSON.stringify(omit(body, 'chatBoxId')), | |||
| signal: controller?.signal || sseRef.current?.signal, | |||
| }); | |||
| @@ -228,6 +228,7 @@ export const useSendMessageWithSse = ( | |||
| setAnswer({ | |||
| ...d, | |||
| conversationId: body?.conversation_id, | |||
| chatBoxId: body.chatBoxId, | |||
| }); | |||
| } | |||
| } catch (e) { | |||
| @@ -82,6 +82,7 @@ export interface Message { | |||
| audio_binary?: string; | |||
| data?: any; | |||
| files?: File[]; | |||
| chatBoxId?: string; | |||
| } | |||
| export interface IReferenceChunk { | |||
| @@ -117,6 +118,7 @@ export interface IAnswer { | |||
| id?: string; | |||
| audio_binary?: string; | |||
| data?: any; | |||
| chatBoxId?: string; | |||
| } | |||
| export interface Docagg { | |||
| @@ -38,6 +38,7 @@ export default { | |||
| previousPage: '上一页', | |||
| nextPage: '下一页', | |||
| add: '添加', | |||
| promptPlaceholder: '请输入或使用 / 快速插入变量。', | |||
| }, | |||
| login: { | |||
| login: '登录', | |||
| @@ -163,9 +163,9 @@ export function PromptEditor({ | |||
| 'absolute top-1 left-2 text-text-secondary pointer-events-none', | |||
| { | |||
| 'truncate w-[90%]': !multiLine, | |||
| 'translate-y-10': multiLine, | |||
| }, | |||
| )} | |||
| data-xxx | |||
| > | |||
| {placeholder || t('common.promptPlaceholder')} | |||
| </div> | |||
| @@ -2,6 +2,7 @@ import { Collapse } from '@/components/collapse'; | |||
| import { CrossLanguageFormField } from '@/components/cross-language-form-field'; | |||
| import { FormContainer } from '@/components/form-container'; | |||
| import { KnowledgeBaseFormField } from '@/components/knowledge-base-item'; | |||
| import { RAGFlowFormItem } from '@/components/ragflow-form'; | |||
| import { RerankFormFields } from '@/components/rerank'; | |||
| import { SimilaritySliderFormField } from '@/components/similarity-slider'; | |||
| import { TopNFormField } from '@/components/top-n-item'; | |||
| @@ -25,7 +26,7 @@ import { useWatchFormChange } from '../../hooks/use-watch-form-change'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import { FormWrapper } from '../components/form-wrapper'; | |||
| import { Output } from '../components/output'; | |||
| import { QueryVariable } from '../components/query-variable'; | |||
| import { PromptEditor } from '../components/prompt-editor'; | |||
| import { useValues } from './use-values'; | |||
| export const RetrievalPartialSchema = { | |||
| @@ -74,6 +75,8 @@ export function EmptyResponseField() { | |||
| } | |||
| function RetrievalForm({ node }: INextOperatorForm) { | |||
| const { t } = useTranslation(); | |||
| const outputList = useMemo(() => { | |||
| return [ | |||
| { | |||
| @@ -96,7 +99,9 @@ function RetrievalForm({ node }: INextOperatorForm) { | |||
| <Form {...form}> | |||
| <FormWrapper> | |||
| <FormContainer> | |||
| <QueryVariable></QueryVariable> | |||
| <RAGFlowFormItem name="query" label={t('flow.query')}> | |||
| <PromptEditor></PromptEditor> | |||
| </RAGFlowFormItem> | |||
| <KnowledgeBaseFormField showVariable></KnowledgeBaseFormField> | |||
| </FormContainer> | |||
| <Collapse title={<div>Advanced Settings</div>}> | |||
| @@ -1,13 +1,17 @@ | |||
| import { LargeModelFormFieldWithoutFilter } from '@/components/large-model-form-field'; | |||
| import { LlmSettingSchema } from '@/components/llm-setting-items/next'; | |||
| import { NextMessageInput } from '@/components/message-input/next'; | |||
| import MessageItem from '@/components/message-item'; | |||
| import { Button } from '@/components/ui/button'; | |||
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; | |||
| import { Form } from '@/components/ui/form'; | |||
| import { | |||
| Tooltip, | |||
| TooltipContent, | |||
| TooltipTrigger, | |||
| } from '@/components/ui/tooltip'; | |||
| import { MessageType } from '@/constants/chat'; | |||
| import { useScrollToBottom } from '@/hooks/logic-hooks'; | |||
| import { | |||
| useFetchConversation, | |||
| useFetchDialog, | |||
| @@ -15,16 +19,20 @@ import { | |||
| } from '@/hooks/use-chat-request'; | |||
| import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | |||
| import { buildMessageUuidWithRole } from '@/utils/chat'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { ListCheck, Plus, Trash2 } from 'lucide-react'; | |||
| import { useCallback } from 'react'; | |||
| import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { z } from 'zod'; | |||
| import { | |||
| useGetSendButtonDisabled, | |||
| useSendButtonDisabled, | |||
| } from '../../hooks/use-button-disabled'; | |||
| import { useCreateConversationBeforeUploadDocument } from '../../hooks/use-create-conversation'; | |||
| import { useSendMessage } from '../../hooks/use-send-chat-message'; | |||
| import { useSendMultipleChatMessage } from '../../hooks/use-send-multiple-message'; | |||
| import { buildMessageItemReference } from '../../utils'; | |||
| import { LLMSelectForm } from '../llm-select-form'; | |||
| import { IMessage } from '../interface'; | |||
| import { useAddChatBox } from '../use-add-box'; | |||
| type MultipleChatBoxProps = { | |||
| @@ -35,31 +43,42 @@ type MultipleChatBoxProps = { | |||
| 'removeChatBox' | 'addChatBox' | 'chatBoxIds' | |||
| >; | |||
| type ChatCardProps = { id: string; idx: number } & Pick< | |||
| type ChatCardProps = { | |||
| id: string; | |||
| idx: number; | |||
| derivedMessages: IMessage[]; | |||
| } & Pick< | |||
| MultipleChatBoxProps, | |||
| 'controller' | 'removeChatBox' | 'addChatBox' | 'chatBoxIds' | |||
| >; | |||
| function ChatCard({ | |||
| controller, | |||
| removeChatBox, | |||
| id, | |||
| idx, | |||
| addChatBox, | |||
| chatBoxIds, | |||
| }: ChatCardProps) { | |||
| const { | |||
| value, | |||
| // scrollRef, | |||
| messageContainerRef, | |||
| sendLoading, | |||
| const ChatCard = forwardRef(function ChatCard( | |||
| { | |||
| controller, | |||
| removeChatBox, | |||
| id, | |||
| idx, | |||
| addChatBox, | |||
| chatBoxIds, | |||
| derivedMessages, | |||
| handleInputChange, | |||
| handlePressEnter, | |||
| regenerateMessage, | |||
| removeMessageById, | |||
| stopOutputMessage, | |||
| } = useSendMessage(controller); | |||
| }: ChatCardProps, | |||
| ref, | |||
| ) { | |||
| const { sendLoading, regenerateMessage, removeMessageById } = | |||
| useSendMessage(controller); | |||
| const messageContainerRef = useRef<HTMLDivElement>(null); | |||
| const { scrollRef } = useScrollToBottom(derivedMessages, messageContainerRef); | |||
| const FormSchema = z.object(LlmSettingSchema); | |||
| const form = useForm<z.infer<typeof FormSchema>>({ | |||
| resolver: zodResolver(FormSchema), | |||
| defaultValues: { | |||
| llm_id: '', | |||
| }, | |||
| }); | |||
| const { data: userInfo } = useFetchUserInfo(); | |||
| const { data: currentDialog } = useFetchDialog(); | |||
| @@ -71,13 +90,19 @@ function ChatCard({ | |||
| removeChatBox(id); | |||
| }, [id, removeChatBox]); | |||
| useImperativeHandle(ref, () => ({ | |||
| getFormData: () => form.getValues(), | |||
| })); | |||
| return ( | |||
| <Card className="bg-transparent border flex-1"> | |||
| <Card className="bg-transparent border flex-1 flex flex-col"> | |||
| <CardHeader className="border-b px-5 py-3"> | |||
| <CardTitle className="flex justify-between items-center"> | |||
| <div className="flex items-center gap-3"> | |||
| <span className="text-base">{idx + 1}</span> | |||
| <LLMSelectForm></LLMSelectForm> | |||
| <Form {...form}> | |||
| <LargeModelFormFieldWithoutFilter></LargeModelFormFieldWithoutFilter> | |||
| </Form> | |||
| </div> | |||
| <div className="space-x-2"> | |||
| <Tooltip> | |||
| @@ -102,8 +127,8 @@ function ChatCard({ | |||
| </div> | |||
| </CardTitle> | |||
| </CardHeader> | |||
| <CardContent> | |||
| <div ref={messageContainerRef} className="flex-1 overflow-auto min-h-0"> | |||
| <CardContent className="flex-1 min-h-0"> | |||
| <div ref={messageContainerRef} className="h-full overflow-auto"> | |||
| <div className="w-full"> | |||
| {derivedMessages?.map((message, i) => { | |||
| return ( | |||
| @@ -134,12 +159,12 @@ function ChatCard({ | |||
| ); | |||
| })} | |||
| </div> | |||
| {/* <div ref={scrollRef} /> */} | |||
| <div ref={scrollRef} /> | |||
| </div> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| } | |||
| }); | |||
| export function MultipleChatBox({ | |||
| controller, | |||
| @@ -150,10 +175,12 @@ export function MultipleChatBox({ | |||
| const { | |||
| value, | |||
| sendLoading, | |||
| messageRecord, | |||
| handleInputChange, | |||
| handlePressEnter, | |||
| stopOutputMessage, | |||
| } = useSendMessage(controller); | |||
| setFormRef, | |||
| } = useSendMultipleChatMessage(controller, chatBoxIds); | |||
| const { createConversationBeforeUploadDocument } = | |||
| useCreateConversationBeforeUploadDocument(); | |||
| @@ -163,7 +190,7 @@ export function MultipleChatBox({ | |||
| return ( | |||
| <section className="h-full flex flex-col px-5"> | |||
| <div className="flex gap-4 flex-1 px-5 pb-14"> | |||
| <div className="flex gap-4 flex-1 px-5 pb-14 min-h-0"> | |||
| {chatBoxIds.map((id, idx) => ( | |||
| <ChatCard | |||
| key={id} | |||
| @@ -173,6 +200,8 @@ export function MultipleChatBox({ | |||
| chatBoxIds={chatBoxIds} | |||
| removeChatBox={removeChatBox} | |||
| addChatBox={addChatBox} | |||
| derivedMessages={messageRecord[id]} | |||
| ref={setFormRef(id)} | |||
| ></ChatCard> | |||
| ))} | |||
| </div> | |||
| @@ -0,0 +1,48 @@ | |||
| import { isEmpty } from 'lodash'; | |||
| import { useCallback, useEffect, useRef } from 'react'; | |||
| export function useBuildFormRefs(chatBoxIds: string[]) { | |||
| const formRefs = useRef<Record<string, { getFormData: () => any }>>({}); | |||
| const setFormRef = (id: string) => (ref: { getFormData: () => any }) => { | |||
| formRefs.current[id] = ref; | |||
| }; | |||
| const cleanupFormRefs = useCallback(() => { | |||
| const currentIds = new Set(chatBoxIds); | |||
| Object.keys(formRefs.current).forEach((id) => { | |||
| if (!currentIds.has(id)) { | |||
| delete formRefs.current[id]; | |||
| } | |||
| }); | |||
| }, [chatBoxIds]); | |||
| const getLLMConfigById = useCallback( | |||
| (chatBoxId?: string) => { | |||
| const llmConfig = chatBoxId | |||
| ? formRefs.current[chatBoxId].getFormData() | |||
| : {}; | |||
| return llmConfig; | |||
| }, | |||
| [formRefs], | |||
| ); | |||
| const isLLMConfigEmpty = useCallback( | |||
| (chatBoxId?: string) => { | |||
| return isEmpty(getLLMConfigById(chatBoxId)?.llm_id); | |||
| }, | |||
| [getLLMConfigById], | |||
| ); | |||
| useEffect(() => { | |||
| cleanupFormRefs(); | |||
| }, [cleanupFormRefs]); | |||
| return { | |||
| formRefs, | |||
| setFormRef, | |||
| getLLMConfigById, | |||
| isLLMConfigEmpty, | |||
| }; | |||
| } | |||
| @@ -0,0 +1,235 @@ | |||
| import { MessageType } from '@/constants/chat'; | |||
| import { | |||
| useHandleMessageInputChange, | |||
| useSendMessageWithSse, | |||
| } from '@/hooks/logic-hooks'; | |||
| import { useGetChatSearchParams } from '@/hooks/use-chat-request'; | |||
| import { IAnswer, 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 { IMessage } from '../chat/interface'; | |||
| import { useBuildFormRefs } from './use-build-form-refs'; | |||
| 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, done } = useSendMessageWithSse( | |||
| api.completeConversation, | |||
| ); | |||
| const { setFormRef, getLLMConfigById, isLLMConfigEmpty } = | |||
| useBuildFormRefs(chatBoxIds); | |||
| const stopOutputMessage = useCallback(() => { | |||
| controller.abort(); | |||
| }, [controller]); | |||
| 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, | |||
| }: { | |||
| message: Message; | |||
| currentConversationId?: string; | |||
| chatBoxId: string; | |||
| messages?: Message[]; | |||
| }) => { | |||
| let derivedMessages: IMessage[] = []; | |||
| derivedMessages = messageRecord[chatBoxId]; | |||
| const res = await send( | |||
| { | |||
| chatBoxId, | |||
| conversation_id: currentConversationId ?? conversationId, | |||
| messages: [...(messages ?? derivedMessages ?? []), message], | |||
| ...getLLMConfigById(chatBoxId), | |||
| }, | |||
| controller, | |||
| ); | |||
| if (res && (res?.response.status !== 200 || res?.data?.code !== 0)) { | |||
| // cancel loading | |||
| setValue(message.content); | |||
| console.info('removeLatestMessage111'); | |||
| removeLatestMessage(chatBoxId); | |||
| } | |||
| }, | |||
| [ | |||
| send, | |||
| conversationId, | |||
| getLLMConfigById, | |||
| controller, | |||
| messageRecord, | |||
| setValue, | |||
| removeLatestMessage, | |||
| ], | |||
| ); | |||
| const handlePressEnter = useCallback(() => { | |||
| if (trim(value) === '') return; | |||
| const id = uuid(); | |||
| chatBoxIds.forEach((chatBoxId) => { | |||
| if (!isLLMConfigEmpty(chatBoxId)) { | |||
| addNewestQuestion({ | |||
| content: value, | |||
| id, | |||
| role: MessageType.User, | |||
| chatBoxId, | |||
| }); | |||
| } | |||
| }); | |||
| if (done) { | |||
| // TODO: | |||
| setValue(''); | |||
| chatBoxIds.forEach((chatBoxId) => { | |||
| if (!isLLMConfigEmpty(chatBoxId)) { | |||
| sendMessage({ | |||
| message: { | |||
| id, | |||
| content: value.trim(), | |||
| role: MessageType.User, | |||
| }, | |||
| chatBoxId, | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| }, [ | |||
| value, | |||
| chatBoxIds, | |||
| done, | |||
| isLLMConfigEmpty, | |||
| addNewestQuestion, | |||
| setValue, | |||
| sendMessage, | |||
| ]); | |||
| useEffect(() => { | |||
| if (answer.answer && conversationId) { | |||
| addNewestAnswer(answer); | |||
| } | |||
| }, [answer, addNewestAnswer, conversationId]); | |||
| useEffect(() => { | |||
| adjustRecordByChatBoxIds(); | |||
| }, [adjustRecordByChatBoxIds]); | |||
| return { | |||
| value, | |||
| messageRecord, | |||
| sendMessage, | |||
| handleInputChange, | |||
| handlePressEnter, | |||
| stopOutputMessage, | |||
| sendLoading: false, | |||
| setFormRef, | |||
| }; | |||
| } | |||