### 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
| [Authorization]: getAuthorization(), | [Authorization]: getAuthorization(), | ||||
| 'Content-Type': 'application/json', | 'Content-Type': 'application/json', | ||||
| }, | }, | ||||
| body: JSON.stringify(body), | |||||
| body: JSON.stringify(omit(body, 'chatBoxId')), | |||||
| signal: controller?.signal || sseRef.current?.signal, | signal: controller?.signal || sseRef.current?.signal, | ||||
| }); | }); | ||||
| setAnswer({ | setAnswer({ | ||||
| ...d, | ...d, | ||||
| conversationId: body?.conversation_id, | conversationId: body?.conversation_id, | ||||
| chatBoxId: body.chatBoxId, | |||||
| }); | }); | ||||
| } | } | ||||
| } catch (e) { | } catch (e) { |
| audio_binary?: string; | audio_binary?: string; | ||||
| data?: any; | data?: any; | ||||
| files?: File[]; | files?: File[]; | ||||
| chatBoxId?: string; | |||||
| } | } | ||||
| export interface IReferenceChunk { | export interface IReferenceChunk { | ||||
| id?: string; | id?: string; | ||||
| audio_binary?: string; | audio_binary?: string; | ||||
| data?: any; | data?: any; | ||||
| chatBoxId?: string; | |||||
| } | } | ||||
| export interface Docagg { | export interface Docagg { |
| previousPage: '上一页', | previousPage: '上一页', | ||||
| nextPage: '下一页', | nextPage: '下一页', | ||||
| add: '添加', | add: '添加', | ||||
| promptPlaceholder: '请输入或使用 / 快速插入变量。', | |||||
| }, | }, | ||||
| login: { | login: { | ||||
| login: '登录', | login: '登录', |
| 'absolute top-1 left-2 text-text-secondary pointer-events-none', | 'absolute top-1 left-2 text-text-secondary pointer-events-none', | ||||
| { | { | ||||
| 'truncate w-[90%]': !multiLine, | 'truncate w-[90%]': !multiLine, | ||||
| 'translate-y-10': multiLine, | |||||
| }, | }, | ||||
| )} | )} | ||||
| data-xxx | |||||
| > | > | ||||
| {placeholder || t('common.promptPlaceholder')} | {placeholder || t('common.promptPlaceholder')} | ||||
| </div> | </div> |
| import { CrossLanguageFormField } from '@/components/cross-language-form-field'; | import { CrossLanguageFormField } from '@/components/cross-language-form-field'; | ||||
| import { FormContainer } from '@/components/form-container'; | import { FormContainer } from '@/components/form-container'; | ||||
| import { KnowledgeBaseFormField } from '@/components/knowledge-base-item'; | import { KnowledgeBaseFormField } from '@/components/knowledge-base-item'; | ||||
| import { RAGFlowFormItem } from '@/components/ragflow-form'; | |||||
| import { RerankFormFields } from '@/components/rerank'; | import { RerankFormFields } from '@/components/rerank'; | ||||
| import { SimilaritySliderFormField } from '@/components/similarity-slider'; | import { SimilaritySliderFormField } from '@/components/similarity-slider'; | ||||
| import { TopNFormField } from '@/components/top-n-item'; | import { TopNFormField } from '@/components/top-n-item'; | ||||
| import { INextOperatorForm } from '../../interface'; | import { INextOperatorForm } from '../../interface'; | ||||
| import { FormWrapper } from '../components/form-wrapper'; | import { FormWrapper } from '../components/form-wrapper'; | ||||
| import { Output } from '../components/output'; | import { Output } from '../components/output'; | ||||
| import { QueryVariable } from '../components/query-variable'; | |||||
| import { PromptEditor } from '../components/prompt-editor'; | |||||
| import { useValues } from './use-values'; | import { useValues } from './use-values'; | ||||
| export const RetrievalPartialSchema = { | export const RetrievalPartialSchema = { | ||||
| } | } | ||||
| function RetrievalForm({ node }: INextOperatorForm) { | function RetrievalForm({ node }: INextOperatorForm) { | ||||
| const { t } = useTranslation(); | |||||
| const outputList = useMemo(() => { | const outputList = useMemo(() => { | ||||
| return [ | return [ | ||||
| { | { | ||||
| <Form {...form}> | <Form {...form}> | ||||
| <FormWrapper> | <FormWrapper> | ||||
| <FormContainer> | <FormContainer> | ||||
| <QueryVariable></QueryVariable> | |||||
| <RAGFlowFormItem name="query" label={t('flow.query')}> | |||||
| <PromptEditor></PromptEditor> | |||||
| </RAGFlowFormItem> | |||||
| <KnowledgeBaseFormField showVariable></KnowledgeBaseFormField> | <KnowledgeBaseFormField showVariable></KnowledgeBaseFormField> | ||||
| </FormContainer> | </FormContainer> | ||||
| <Collapse title={<div>Advanced Settings</div>}> | <Collapse title={<div>Advanced Settings</div>}> |
| 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 } from '@/components/message-input/next'; | ||||
| import MessageItem from '@/components/message-item'; | import MessageItem from '@/components/message-item'; | ||||
| import { Button } from '@/components/ui/button'; | import { Button } from '@/components/ui/button'; | ||||
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; | ||||
| import { Form } from '@/components/ui/form'; | |||||
| import { | import { | ||||
| Tooltip, | Tooltip, | ||||
| TooltipContent, | TooltipContent, | ||||
| TooltipTrigger, | TooltipTrigger, | ||||
| } from '@/components/ui/tooltip'; | } from '@/components/ui/tooltip'; | ||||
| import { MessageType } from '@/constants/chat'; | import { MessageType } from '@/constants/chat'; | ||||
| import { useScrollToBottom } from '@/hooks/logic-hooks'; | |||||
| import { | import { | ||||
| useFetchConversation, | useFetchConversation, | ||||
| useFetchDialog, | useFetchDialog, | ||||
| } from '@/hooks/use-chat-request'; | } from '@/hooks/use-chat-request'; | ||||
| import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | import { useFetchUserInfo } from '@/hooks/user-setting-hooks'; | ||||
| import { buildMessageUuidWithRole } from '@/utils/chat'; | import { buildMessageUuidWithRole } from '@/utils/chat'; | ||||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||||
| import { ListCheck, Plus, Trash2 } from 'lucide-react'; | 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 { | import { | ||||
| useGetSendButtonDisabled, | useGetSendButtonDisabled, | ||||
| useSendButtonDisabled, | useSendButtonDisabled, | ||||
| } from '../../hooks/use-button-disabled'; | } from '../../hooks/use-button-disabled'; | ||||
| import { useCreateConversationBeforeUploadDocument } from '../../hooks/use-create-conversation'; | import { useCreateConversationBeforeUploadDocument } from '../../hooks/use-create-conversation'; | ||||
| import { useSendMessage } from '../../hooks/use-send-chat-message'; | import { useSendMessage } from '../../hooks/use-send-chat-message'; | ||||
| import { useSendMultipleChatMessage } from '../../hooks/use-send-multiple-message'; | |||||
| import { buildMessageItemReference } from '../../utils'; | import { buildMessageItemReference } from '../../utils'; | ||||
| import { LLMSelectForm } from '../llm-select-form'; | |||||
| import { IMessage } from '../interface'; | |||||
| import { useAddChatBox } from '../use-add-box'; | import { useAddChatBox } from '../use-add-box'; | ||||
| type MultipleChatBoxProps = { | type MultipleChatBoxProps = { | ||||
| 'removeChatBox' | 'addChatBox' | 'chatBoxIds' | 'removeChatBox' | 'addChatBox' | 'chatBoxIds' | ||||
| >; | >; | ||||
| type ChatCardProps = { id: string; idx: number } & Pick< | |||||
| type ChatCardProps = { | |||||
| id: string; | |||||
| idx: number; | |||||
| derivedMessages: IMessage[]; | |||||
| } & Pick< | |||||
| MultipleChatBoxProps, | MultipleChatBoxProps, | ||||
| 'controller' | 'removeChatBox' | 'addChatBox' | 'chatBoxIds' | '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, | 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: userInfo } = useFetchUserInfo(); | ||||
| const { data: currentDialog } = useFetchDialog(); | const { data: currentDialog } = useFetchDialog(); | ||||
| removeChatBox(id); | removeChatBox(id); | ||||
| }, [id, removeChatBox]); | }, [id, removeChatBox]); | ||||
| useImperativeHandle(ref, () => ({ | |||||
| getFormData: () => form.getValues(), | |||||
| })); | |||||
| return ( | 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"> | <CardHeader className="border-b px-5 py-3"> | ||||
| <CardTitle className="flex justify-between items-center"> | <CardTitle className="flex justify-between items-center"> | ||||
| <div className="flex items-center gap-3"> | <div className="flex items-center gap-3"> | ||||
| <span className="text-base">{idx + 1}</span> | <span className="text-base">{idx + 1}</span> | ||||
| <LLMSelectForm></LLMSelectForm> | |||||
| <Form {...form}> | |||||
| <LargeModelFormFieldWithoutFilter></LargeModelFormFieldWithoutFilter> | |||||
| </Form> | |||||
| </div> | </div> | ||||
| <div className="space-x-2"> | <div className="space-x-2"> | ||||
| <Tooltip> | <Tooltip> | ||||
| </div> | </div> | ||||
| </CardTitle> | </CardTitle> | ||||
| </CardHeader> | </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"> | <div className="w-full"> | ||||
| {derivedMessages?.map((message, i) => { | {derivedMessages?.map((message, i) => { | ||||
| return ( | return ( | ||||
| ); | ); | ||||
| })} | })} | ||||
| </div> | </div> | ||||
| {/* <div ref={scrollRef} /> */} | |||||
| <div ref={scrollRef} /> | |||||
| </div> | </div> | ||||
| </CardContent> | </CardContent> | ||||
| </Card> | </Card> | ||||
| ); | ); | ||||
| } | |||||
| }); | |||||
| export function MultipleChatBox({ | export function MultipleChatBox({ | ||||
| controller, | controller, | ||||
| const { | const { | ||||
| value, | value, | ||||
| sendLoading, | sendLoading, | ||||
| messageRecord, | |||||
| handleInputChange, | handleInputChange, | ||||
| handlePressEnter, | handlePressEnter, | ||||
| stopOutputMessage, | stopOutputMessage, | ||||
| } = useSendMessage(controller); | |||||
| setFormRef, | |||||
| } = useSendMultipleChatMessage(controller, chatBoxIds); | |||||
| const { createConversationBeforeUploadDocument } = | const { createConversationBeforeUploadDocument } = | ||||
| useCreateConversationBeforeUploadDocument(); | useCreateConversationBeforeUploadDocument(); | ||||
| return ( | return ( | ||||
| <section className="h-full flex flex-col px-5"> | <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) => ( | {chatBoxIds.map((id, idx) => ( | ||||
| <ChatCard | <ChatCard | ||||
| key={id} | key={id} | ||||
| chatBoxIds={chatBoxIds} | chatBoxIds={chatBoxIds} | ||||
| removeChatBox={removeChatBox} | removeChatBox={removeChatBox} | ||||
| addChatBox={addChatBox} | addChatBox={addChatBox} | ||||
| derivedMessages={messageRecord[id]} | |||||
| ref={setFormRef(id)} | |||||
| ></ChatCard> | ></ChatCard> | ||||
| ))} | ))} | ||||
| </div> | </div> |
| 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, | |||||
| }; | |||||
| } |
| 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, | |||||
| }; | |||||
| } |