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)
This commit is contained in:
balibabu
2025-08-15 10:04:37 +08:00
committed by GitHub
parent 618d6bc924
commit 562349eb02
11 changed files with 233 additions and 37 deletions

View File

@ -651,6 +651,7 @@ export const initialAgentValues = {
exception_default_value: '',
tools: [],
mcp: [],
cite: true,
outputs: {
// structured_output: {
// topic: {

View File

@ -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`}

View File

@ -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>

View File

@ -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>
);

View File

@ -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>

View File

@ -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,
};
};

View File

@ -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,
};
}

View 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 };
}