diff --git a/web/src/hooks/logic-hooks.ts b/web/src/hooks/logic-hooks.ts index c8decba80..a6bd18436 100644 --- a/web/src/hooks/logic-hooks.ts +++ b/web/src/hooks/logic-hooks.ts @@ -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) { diff --git a/web/src/interfaces/database/chat.ts b/web/src/interfaces/database/chat.ts index 7c46c02f5..08e7c27a7 100644 --- a/web/src/interfaces/database/chat.ts +++ b/web/src/interfaces/database/chat.ts @@ -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 { diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 833d5d6b0..90cc053f3 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -38,6 +38,7 @@ export default { previousPage: '上一页', nextPage: '下一页', add: '添加', + promptPlaceholder: '请输入或使用 / 快速插入变量。', }, login: { login: '登录', diff --git a/web/src/pages/agent/form/components/prompt-editor/index.tsx b/web/src/pages/agent/form/components/prompt-editor/index.tsx index af1823025..9659f02c1 100644 --- a/web/src/pages/agent/form/components/prompt-editor/index.tsx +++ b/web/src/pages/agent/form/components/prompt-editor/index.tsx @@ -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')} diff --git a/web/src/pages/agent/form/retrieval-form/next.tsx b/web/src/pages/agent/form/retrieval-form/next.tsx index 876547df5..03ae60a25 100644 --- a/web/src/pages/agent/form/retrieval-form/next.tsx +++ b/web/src/pages/agent/form/retrieval-form/next.tsx @@ -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) {
- + + + Advanced Settings}> diff --git a/web/src/pages/next-chats/chat/chat-box/multiple-chat-box.tsx b/web/src/pages/next-chats/chat/chat-box/multiple-chat-box.tsx index 4d7b4b411..f73b9c320 100644 --- a/web/src/pages/next-chats/chat/chat-box/multiple-chat-box.tsx +++ b/web/src/pages/next-chats/chat/chat-box/multiple-chat-box.tsx @@ -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(null); + + const { scrollRef } = useScrollToBottom(derivedMessages, messageContainerRef); + + const FormSchema = z.object(LlmSettingSchema); + + const form = useForm>({ + 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 ( - +
{idx + 1} - + + +
@@ -102,8 +127,8 @@ function ChatCard({
- -
+ +
{derivedMessages?.map((message, i) => { return ( @@ -134,12 +159,12 @@ function ChatCard({ ); })}
- {/*
*/} +
); -} +}); 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 (
-
+
{chatBoxIds.map((id, idx) => ( ))}
diff --git a/web/src/pages/next-chats/hooks/use-build-form-refs.ts b/web/src/pages/next-chats/hooks/use-build-form-refs.ts new file mode 100644 index 000000000..c983c5d95 --- /dev/null +++ b/web/src/pages/next-chats/hooks/use-build-form-refs.ts @@ -0,0 +1,48 @@ +import { isEmpty } from 'lodash'; +import { useCallback, useEffect, useRef } from 'react'; + +export function useBuildFormRefs(chatBoxIds: string[]) { + const formRefs = useRef 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, + }; +} 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 new file mode 100644 index 000000000..73d786c9b --- /dev/null +++ b/web/src/pages/next-chats/hooks/use-send-multiple-message.ts @@ -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 + >({}); + + 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, + }; +}