From 562349eb02e7c0404624205e0994667f056c3a93 Mon Sep 17 00:00:00 2001 From: balibabu Date: Fri, 15 Aug 2025 10:04:37 +0800 Subject: [PATCH] Feat: Upload files in the chat box #3221 (#9483) ### What problem does this PR solve? Feat: Upload files in the chat box #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/hooks/logic-hooks.ts | 66 +++++++++++++++++-- web/src/hooks/use-chat-request.ts | 33 ++++++++++ web/src/pages/agent/constant.tsx | 1 + web/src/pages/agent/form/agent-form/index.tsx | 19 ++++++ .../chat/chat-box/multiple-chat-box.tsx | 34 ++++++++-- .../chat/chat-box/single-chat-box.tsx | 2 + web/src/pages/next-chats/chat/index.tsx | 17 ++++- .../next-chats/hooks/use-send-chat-message.ts | 48 ++++++++------ .../hooks/use-send-multiple-message.ts | 18 +++-- .../pages/next-chats/hooks/use-upload-file.ts | 27 ++++++++ web/src/services/next-chat-service.ts | 5 ++ 11 files changed, 233 insertions(+), 37 deletions(-) create mode 100644 web/src/pages/next-chats/hooks/use-upload-file.ts diff --git a/web/src/hooks/logic-hooks.ts b/web/src/hooks/logic-hooks.ts index a6bd18436..fd9600193 100644 --- a/web/src/hooks/logic-hooks.ts +++ b/web/src/hooks/logic-hooks.ts @@ -12,7 +12,7 @@ import { PaginationProps, message } from 'antd'; import { FormInstance } from 'antd/lib'; import axios from 'axios'; import { EventSourceParserStream } from 'eventsource-parser/stream'; -import { omit } from 'lodash'; +import { has, isEmpty, omit } from 'lodash'; import { ChangeEventHandler, useCallback, @@ -166,11 +166,43 @@ export const useFetchAppConf = () => { return appConf; }; +function useSetDoneRecord() { + const [doneRecord, setDoneRecord] = useState>({}); + + const clearDoneRecord = useCallback(() => { + setDoneRecord({}); + }, []); + + const setDoneRecordById = useCallback((id: string, val: boolean) => { + setDoneRecord((prev) => ({ ...prev, [id]: val })); + }, []); + + const allDone = useMemo(() => { + return Object.values(doneRecord).every((val) => val); + }, [doneRecord]); + + useEffect(() => { + if (!isEmpty(doneRecord) && allDone) { + clearDoneRecord(); + } + }, [allDone, clearDoneRecord, doneRecord]); + + return { + doneRecord, + setDoneRecord, + setDoneRecordById, + clearDoneRecord, + allDone, + }; +} + export const useSendMessageWithSse = ( url: string = api.completeConversation, ) => { const [answer, setAnswer] = useState({} as IAnswer); const [done, setDone] = useState(true); + const { doneRecord, clearDoneRecord, setDoneRecordById, allDone } = + useSetDoneRecord(); const timer = useRef(); const sseRef = useRef(); @@ -188,6 +220,17 @@ export const useSendMessageWithSse = ( }, 1000); }, []); + const setDoneValue = useCallback( + (body: any, value: boolean) => { + if (has(body, 'chatBoxId')) { + setDoneRecordById(body.chatBoxId, value); + } else { + setDone(value); + } + }, + [setDoneRecordById], + ); + const send = useCallback( async ( body: any, @@ -195,7 +238,7 @@ export const useSendMessageWithSse = ( ): Promise<{ response: Response; data: ResponseType } | undefined> => { initializeSseRef(); try { - setDone(false); + setDoneValue(body, false); const response = await fetch(url, { method: 'POST', headers: { @@ -236,23 +279,34 @@ export const useSendMessageWithSse = ( } } } - setDone(true); + setDoneValue(body, true); resetAnswer(); return { data: await res, response }; } catch (e) { - setDone(true); + setDoneValue(body, true); + resetAnswer(); // Swallow fetch errors silently } }, - [initializeSseRef, url, resetAnswer], + [initializeSseRef, setDoneValue, url, resetAnswer], ); const stopOutputMessage = useCallback(() => { sseRef.current?.abort(); }, []); - return { send, answer, done, setDone, resetAnswer, stopOutputMessage }; + return { + send, + answer, + done, + doneRecord, + allDone, + setDone, + resetAnswer, + stopOutputMessage, + clearDoneRecord, + }; }; export const useSpeechWithSse = (url: string = api.tts) => { diff --git a/web/src/hooks/use-chat-request.ts b/web/src/hooks/use-chat-request.ts index 157eb7607..8fb01ba78 100644 --- a/web/src/hooks/use-chat-request.ts +++ b/web/src/hooks/use-chat-request.ts @@ -30,6 +30,7 @@ export const enum ChatApiAction { DeleteMessage = 'deleteMessage', FetchMindMap = 'fetchMindMap', FetchRelatedQuestions = 'fetchRelatedQuestions', + UploadAndParse = 'upload_and_parse', } export const useGetChatSearchParams = () => { @@ -163,6 +164,10 @@ export const useSetDialog = () => { queryKey: [ChatApiAction.FetchDialogList], }); + queryClient.invalidateQueries({ + queryKey: [ChatApiAction.FetchDialog], + }); + message.success( t(`message.${params.dialog_id ? 'modified' : 'created'}`), ); @@ -376,6 +381,34 @@ export const useDeleteMessage = () => { return { data, loading, deleteMessage: mutateAsync }; }; +export function useUploadAndParseFile() { + const { conversationId } = useGetChatSearchParams(); + const { t } = useTranslation(); + + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [ChatApiAction.UploadAndParse], + mutationFn: async (file: File) => { + const formData = new FormData(); + formData.append('file', file); + formData.append('conversation_id', conversationId); + + const { data } = await chatService.uploadAndParse(formData); + + if (data.code === 0) { + message.success(t(`message.uploaded`)); + } + + return data; + }, + }); + + return { data, loading, uploadAndParseFile: mutateAsync }; +} + //#endregion //#region search page diff --git a/web/src/pages/agent/constant.tsx b/web/src/pages/agent/constant.tsx index f10298449..46edc870e 100644 --- a/web/src/pages/agent/constant.tsx +++ b/web/src/pages/agent/constant.tsx @@ -651,6 +651,7 @@ export const initialAgentValues = { exception_default_value: '', tools: [], mcp: [], + cite: true, outputs: { // structured_output: { // topic: { diff --git a/web/src/pages/agent/form/agent-form/index.tsx b/web/src/pages/agent/form/agent-form/index.tsx index 72cb6200b..c6afef755 100644 --- a/web/src/pages/agent/form/agent-form/index.tsx +++ b/web/src/pages/agent/form/agent-form/index.tsx @@ -15,6 +15,7 @@ import { FormLabel, } from '@/components/ui/form'; import { Input, NumberInput } from '@/components/ui/input'; +import { Switch } from '@/components/ui/switch'; import { LlmModelType } from '@/constants/knowledge'; import { useFindLlmByUuid } from '@/hooks/use-llm-request'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -71,6 +72,7 @@ const FormSchema = z.object({ exception_goto: z.array(z.string()).optional(), exception_default_value: z.string().optional(), ...LargeModelFilterFormSchema, + cite: z.boolean().optional(), }); const outputList = buildOutputList(initialAgentValues.outputs); @@ -184,6 +186,23 @@ function AgentForm({ node }: INextOperatorForm) { Advanced Settings}> + ( + + + {t('flow.cite')} + + + + + + )} + /> (null); @@ -80,6 +87,8 @@ const ChatCard = forwardRef(function ChatCard( }, }); + const llmId = useWatch({ control: form.control, name: 'llm_id' }); + const { data: userInfo } = useFetchUserInfo(); const { data: currentDialog } = useFetchDialog(); const { data: conversation } = useFetchConversation(); @@ -90,6 +99,16 @@ const ChatCard = forwardRef(function ChatCard( removeChatBox(id); }, [id, removeChatBox]); + const handleApplyConfig = useCallback(() => { + const values = form.getValues(); + setDialog({ + ...currentDialog, + llm_id: values.llm_id, + llm_setting: omit(values, 'llm_id'), + dialog_id: dialogId, + }); + }, [currentDialog, dialogId, form, setDialog]); + useImperativeHandle(ref, () => ({ getFormData: () => form.getValues(), })); @@ -107,7 +126,11 @@ const ChatCard = forwardRef(function ChatCard(
- @@ -180,6 +203,7 @@ export function MultipleChatBox({ handlePressEnter, stopOutputMessage, setFormRef, + handleUploadFile, } = useSendMultipleChatMessage(controller, chatBoxIds); const { createConversationBeforeUploadDocument } = @@ -202,6 +226,7 @@ export function MultipleChatBox({ addChatBox={addChatBox} derivedMessages={messageRecord[id]} ref={setFormRef(id)} + sendLoading={sendLoading} > ))}
@@ -218,6 +243,7 @@ export function MultipleChatBox({ createConversationBeforeUploadDocument } stopOutputMessage={stopOutputMessage} + onUpload={handleUploadFile} /> diff --git a/web/src/pages/next-chats/chat/chat-box/single-chat-box.tsx b/web/src/pages/next-chats/chat/chat-box/single-chat-box.tsx index 4db3183e5..6a8cc01ea 100644 --- a/web/src/pages/next-chats/chat/chat-box/single-chat-box.tsx +++ b/web/src/pages/next-chats/chat/chat-box/single-chat-box.tsx @@ -32,6 +32,7 @@ export function SingleChatBox({ controller }: IProps) { regenerateMessage, removeMessageById, stopOutputMessage, + handleUploadFile, } = useSendMessage(controller); const { data: userInfo } = useFetchUserInfo(); const { data: currentDialog } = useFetchDialog(); @@ -89,6 +90,7 @@ export function SingleChatBox({ controller }: IProps) { createConversationBeforeUploadDocument } stopOutputMessage={stopOutputMessage} + onUpload={handleUploadFile} /> ); diff --git a/web/src/pages/next-chats/chat/index.tsx b/web/src/pages/next-chats/chat/index.tsx index 195736698..19947f65c 100644 --- a/web/src/pages/next-chats/chat/index.tsx +++ b/web/src/pages/next-chats/chat/index.tsx @@ -11,8 +11,13 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { useSetModalState } from '@/hooks/common-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; -import { useFetchConversation, useFetchDialog } from '@/hooks/use-chat-request'; +import { + useFetchConversation, + useFetchDialog, + useGetChatSearchParams, +} from '@/hooks/use-chat-request'; import { cn } from '@/lib/utils'; +import { isEmpty } from 'lodash'; import { ArrowUpRight, LogOut } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useHandleClickConversationCard } from '../hooks/use-click-card'; @@ -42,6 +47,8 @@ export default function Chat() { hasThreeChatBox, } = useAddChatBox(); + const { conversationId, isNew } = useGetChatSearchParams(); + const { isDebugMode, switchDebugMode } = useSwitchDebugMode(); if (isDebugMode) { @@ -104,13 +111,17 @@ export default function Chat() { - + diff --git a/web/src/pages/next-chats/hooks/use-send-chat-message.ts b/web/src/pages/next-chats/hooks/use-send-chat-message.ts index 534120b6d..390743049 100644 --- a/web/src/pages/next-chats/hooks/use-send-chat-message.ts +++ b/web/src/pages/next-chats/hooks/use-send-chat-message.ts @@ -18,6 +18,7 @@ import { useParams, useSearchParams } from 'umi'; import { v4 as uuid } from 'uuid'; import { IMessage } from '../chat/interface'; import { useFindPrologueFromDialogList } from './use-select-conversation-list'; +import { useUploadFile } from './use-upload-file'; export const useSetChatRouteParams = () => { const [currentQueryParameters, setSearchParams] = useSearchParams(); @@ -137,6 +138,8 @@ export const useSendMessage = (controller: AbortController) => { const { conversationId, isNew } = useGetChatSearchParams(); const { handleInputChange, value, setValue } = useHandleMessageInputChange(); + const { handleUploadFile, fileIds, clearFileIds } = useUploadFile(); + const { send, answer, done } = useSendMessageWithSse( api.completeConversation, ); @@ -238,29 +241,35 @@ export const useSendMessage = (controller: AbortController) => { } }, [answer, addNewestAnswer, conversationId, isNew]); - const handlePressEnter = useCallback( - (documentIds: string[]) => { - if (trim(value) === '') return; - const id = uuid(); + const handlePressEnter = useCallback(() => { + if (trim(value) === '') return; + const id = uuid(); - addNewestQuestion({ - content: value, - doc_ids: documentIds, + addNewestQuestion({ + content: value, + doc_ids: fileIds, + id, + role: MessageType.User, + }); + if (done) { + setValue(''); + handleSendMessage({ id, + content: value.trim(), role: MessageType.User, + doc_ids: fileIds, }); - if (done) { - setValue(''); - handleSendMessage({ - id, - content: value.trim(), - role: MessageType.User, - doc_ids: documentIds, - }); - } - }, - [addNewestQuestion, handleSendMessage, done, setValue, value], - ); + } + clearFileIds(); + }, [ + value, + addNewestQuestion, + fileIds, + done, + clearFileIds, + setValue, + handleSendMessage, + ]); return { handlePressEnter, @@ -275,5 +284,6 @@ export const useSendMessage = (controller: AbortController) => { derivedMessages, removeMessageById, stopOutputMessage, + handleUploadFile, }; }; 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 index 73d786c9b..5e8e1ee71 100644 --- a/web/src/pages/next-chats/hooks/use-send-multiple-message.ts +++ b/web/src/pages/next-chats/hooks/use-send-multiple-message.ts @@ -12,6 +12,7 @@ import { useCallback, useEffect, useState } from 'react'; import { v4 as uuid } from 'uuid'; import { IMessage } from '../chat/interface'; import { useBuildFormRefs } from './use-build-form-refs'; +import { useUploadFile } from './use-upload-file'; export function useSendMultipleChatMessage( controller: AbortController, @@ -24,10 +25,12 @@ export function useSendMultipleChatMessage( const { conversationId } = useGetChatSearchParams(); const { handleInputChange, value, setValue } = useHandleMessageInputChange(); - const { send, answer, done } = useSendMessageWithSse( + const { send, answer, allDone } = useSendMessageWithSse( api.completeConversation, ); + const { handleUploadFile, fileIds, clearFileIds } = useUploadFile(); + const { setFormRef, getLLMConfigById, isLLMConfigEmpty } = useBuildFormRefs(chatBoxIds); @@ -182,12 +185,12 @@ export function useSendMultipleChatMessage( id, role: MessageType.User, chatBoxId, + doc_ids: fileIds, }); } }); - if (done) { - // TODO: + if (allDone) { setValue(''); chatBoxIds.forEach((chatBoxId) => { if (!isLLMConfigEmpty(chatBoxId)) { @@ -196,18 +199,22 @@ export function useSendMultipleChatMessage( id, content: value.trim(), role: MessageType.User, + doc_ids: fileIds, }, chatBoxId, }); } }); } + clearFileIds(); }, [ value, chatBoxIds, - done, + allDone, + clearFileIds, isLLMConfigEmpty, addNewestQuestion, + fileIds, setValue, sendMessage, ]); @@ -229,7 +236,8 @@ export function useSendMultipleChatMessage( handleInputChange, handlePressEnter, stopOutputMessage, - sendLoading: false, + sendLoading: !allDone, setFormRef, + handleUploadFile, }; } diff --git a/web/src/pages/next-chats/hooks/use-upload-file.ts b/web/src/pages/next-chats/hooks/use-upload-file.ts new file mode 100644 index 000000000..0bcc8d737 --- /dev/null +++ b/web/src/pages/next-chats/hooks/use-upload-file.ts @@ -0,0 +1,27 @@ +import { FileUploadProps } from '@/components/file-upload'; +import { useUploadAndParseFile } from '@/hooks/use-chat-request'; +import { useCallback, useState } from 'react'; + +export function useUploadFile() { + const { uploadAndParseFile } = useUploadAndParseFile(); + const [fileIds, setFileIds] = useState([]); + + const handleUploadFile: NonNullable = + useCallback( + async (files) => { + if (Array.isArray(files) && files.length) { + const ret = await uploadAndParseFile(files[0]); + if (ret.code === 0 && Array.isArray(ret.data)) { + setFileIds((list) => [...list, ...ret.data]); + } + } + }, + [uploadAndParseFile], + ); + + const clearFileIds = useCallback(() => { + setFileIds([]); + }, []); + + return { handleUploadFile, clearFileIds, fileIds }; +} diff --git a/web/src/services/next-chat-service.ts b/web/src/services/next-chat-service.ts index 4e353d06f..80a6e42a0 100644 --- a/web/src/services/next-chat-service.ts +++ b/web/src/services/next-chat-service.ts @@ -27,6 +27,7 @@ const { mindmap, getRelatedQuestions, listNextDialog, + upload_and_parse, } = api; const methods = { @@ -126,6 +127,10 @@ const methods = { url: getRelatedQuestions, method: 'post', }, + uploadAndParse: { + method: 'post', + url: upload_and_parse, + }, } as const; const chatService = registerNextServer(methods);