mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 12:32:30 +08:00
### 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)
This commit is contained in:
@ -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<Record<string, boolean>>({});
|
||||
|
||||
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<IAnswer>({} as IAnswer);
|
||||
const [done, setDone] = useState(true);
|
||||
const { doneRecord, clearDoneRecord, setDoneRecordById, allDone } =
|
||||
useSetDoneRecord();
|
||||
const timer = useRef<any>();
|
||||
const sseRef = useRef<AbortController>();
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -651,6 +651,7 @@ export const initialAgentValues = {
|
||||
exception_default_value: '',
|
||||
tools: [],
|
||||
mcp: [],
|
||||
cite: true,
|
||||
outputs: {
|
||||
// structured_output: {
|
||||
// topic: {
|
||||
|
||||
@ -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) {
|
||||
<Collapse title={<div>Advanced Settings</div>}>
|
||||
<FormContainer>
|
||||
<MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`cite`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel tooltip={t('flow.citeTip')}>
|
||||
{t('flow.cite')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
></Switch>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`max_retries`}
|
||||
|
||||
@ -16,13 +16,16 @@ import {
|
||||
useFetchConversation,
|
||||
useFetchDialog,
|
||||
useGetChatSearchParams,
|
||||
useSetDialog,
|
||||
} from '@/hooks/use-chat-request';
|
||||
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
|
||||
import { buildMessageUuidWithRole } from '@/utils/chat';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { isEmpty, omit } from 'lodash';
|
||||
import { ListCheck, Plus, Trash2 } from 'lucide-react';
|
||||
import { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useParams } from 'umi';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
useGetSendButtonDisabled,
|
||||
@ -47,6 +50,7 @@ type ChatCardProps = {
|
||||
id: string;
|
||||
idx: number;
|
||||
derivedMessages: IMessage[];
|
||||
sendLoading: boolean;
|
||||
} & Pick<
|
||||
MultipleChatBoxProps,
|
||||
'controller' | 'removeChatBox' | 'addChatBox' | 'chatBoxIds'
|
||||
@ -61,11 +65,14 @@ const ChatCard = forwardRef(function ChatCard(
|
||||
addChatBox,
|
||||
chatBoxIds,
|
||||
derivedMessages,
|
||||
sendLoading,
|
||||
}: ChatCardProps,
|
||||
ref,
|
||||
) {
|
||||
const { sendLoading, regenerateMessage, removeMessageById } =
|
||||
useSendMessage(controller);
|
||||
const { id: dialogId } = useParams();
|
||||
const { setDialog } = useSetDialog();
|
||||
|
||||
const { regenerateMessage, removeMessageById } = useSendMessage(controller);
|
||||
|
||||
const messageContainerRef = useRef<HTMLDivElement>(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(
|
||||
<div className="space-x-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button variant={'ghost'}>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
disabled={isEmpty(llmId)}
|
||||
onClick={handleApplyConfig}
|
||||
>
|
||||
<ListCheck />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@ -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}
|
||||
></ChatCard>
|
||||
))}
|
||||
</div>
|
||||
@ -218,6 +243,7 @@ export function MultipleChatBox({
|
||||
createConversationBeforeUploadDocument
|
||||
}
|
||||
stopOutputMessage={stopOutputMessage}
|
||||
onUpload={handleUploadFile}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
|
||||
@ -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() {
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
onClick={switchDebugMode}
|
||||
disabled={hasThreeChatBox}
|
||||
disabled={
|
||||
hasThreeChatBox ||
|
||||
isEmpty(conversationId) ||
|
||||
isNew === 'true'
|
||||
}
|
||||
>
|
||||
<ArrowUpRight /> Multiple Models
|
||||
</Button>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 p-0">
|
||||
<CardContent className="flex-1 p-0 min-h-0">
|
||||
<SingleChatBox controller={controller}></SingleChatBox>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
27
web/src/pages/next-chats/hooks/use-upload-file.ts
Normal file
27
web/src/pages/next-chats/hooks/use-upload-file.ts
Normal file
@ -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<string[]>([]);
|
||||
|
||||
const handleUploadFile: NonNullable<FileUploadProps['onUpload']> =
|
||||
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 };
|
||||
}
|
||||
@ -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<keyof typeof methods>(methods);
|
||||
|
||||
Reference in New Issue
Block a user