New file upload (#3058)

* feat: toolNode aiNode readFileNode adapt new version

* update docker-compose

* update tip

* feat: adapt new file version

* perf: file input

* fix: ts
This commit is contained in:
Archer
2024-11-04 10:44:45 +08:00
committed by archer
parent 9e8138e55f
commit dc1119ca90
55 changed files with 1159 additions and 488 deletions

View File

@@ -16,6 +16,9 @@ OPENAI_BASE_URL=https://api.openai.com/v1
# 通用key。可以是 openai 的也可以是 oneapi 的。
# 此处逻辑:优先走 ONEAPI_URL如果填写了 ONEAPI_URLkey 也需要是 ONEAPI 的 key
CHAT_API_KEY=sk-xxxx
# 是否将图片转成 base64 传递给模型,本地开发和内网环境使用共有模型时候需要设置为 true
MULTIPLE_DATA_TO_BASE64=true
# mongo 数据库连接参数,本地开发连接远程数据库时,可能需要增加 directConnection=true 参数,才能连接上。
MONGODB_URI=mongodb://username:password@0.0.0.0:27017/fastgpt?authSource=admin

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -58,8 +58,8 @@ const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
[FnTypeEnum.visionModel]: {
icon: '/imgs/app/question.svg',
title: t('app:vision_model_title'),
desc: t('app:llm_use_vision_tip'),
imgUrl: '/imgs/app/visionModel.png'
desc: t('app:open_vision_function_tip'),
imgUrl: '/imgs/app/visionModel.svg'
},
[FnTypeEnum.instruction]: {
icon: '/imgs/app/help.svg',

View File

@@ -65,10 +65,6 @@ const VariableEdit = ({
const { setValue, reset, watch, getValues } = form;
const value = getValues();
const type = watch('type');
const valueType = watch('valueType');
const max = watch('max');
const min = watch('min');
const defaultValue = watch('defaultValue');
const inputTypeList = useMemo(
() =>
@@ -376,11 +372,7 @@ const VariableEdit = ({
type={'variable'}
isEdit={!!value.key}
inputType={type}
valueType={valueType}
defaultValue={defaultValue}
defaultValueType={defaultValueType}
max={max}
min={min}
onClose={() => reset({})}
onSubmitSuccess={onSubmitSuccess}
onSubmitError={onSubmitError}

View File

@@ -8,7 +8,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { ChatBoxInputFormType, ChatBoxInputType, SendPromptFnType } from '../type';
import { textareaMinH } from '../constants';
import { UseFormReturn } from 'react-hook-form';
import { useFieldArray, UseFormReturn } from 'react-hook-form';
import { ChatBoxContext } from '../Provider';
import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector';
@@ -58,6 +58,10 @@ const ChatInput = ({
fileSelectConfig
} = useContextSelector(ChatBoxContext, (v) => v);
const fileCtrl = useFieldArray({
control,
name: 'files'
});
const {
File,
onOpenSelectFile,
@@ -74,7 +78,7 @@ const ChatInput = ({
outLinkAuthData,
chatId: chatId || '',
fileSelectConfig,
control
fileCtrl
});
const havInput = !!inputValue || fileList.length > 0;
const hasFileUploading = fileList.some((item) => !item.url);
@@ -468,7 +472,7 @@ const ChatInput = ({
{RenderTranslateLoading}
{/* file preview */}
<Box px={[2, 4]}>
<Box px={[1, 3]}>
<FilePreview fileList={fileList} removeFiles={removeFiles} />
</Box>

View File

@@ -64,14 +64,14 @@ export const VariableInputItem = ({
minH={40}
maxH={160}
bg={'myGray.50'}
{...register(item.key, {
{...register(`variables.${item.key}`, {
required: item.required
})}
/>
)}
{item.type === VariableInputEnum.textarea && (
<Textarea
{...register(item.key, {
{...register(`variables.${item.key}`, {
required: item.required
})}
rows={5}
@@ -82,9 +82,9 @@ export const VariableInputItem = ({
{item.type === VariableInputEnum.select && (
<Controller
key={item.key}
key={`variables.${item.key}`}
control={control}
name={item.key}
name={`variables.${item.key}`}
rules={{ required: item.required }}
render={({ field: { ref, value } }) => {
return (
@@ -96,7 +96,7 @@ export const VariableInputItem = ({
value: item.value
}))}
value={value}
onchange={(e) => setValue(item.key, e)}
onchange={(e) => setValue(`variables.${item.key}`, e)}
/>
);
}}
@@ -104,9 +104,9 @@ export const VariableInputItem = ({
)}
{item.type === VariableInputEnum.numberInput && (
<Controller
key={item.key}
key={`variables.${item.key}`}
control={control}
name={item.key}
name={`variables.${item.key}`}
rules={{ required: item.required, min: item.min, max: item.max }}
render={({ field: { ref, value, onChange } }) => (
<NumberInput

View File

@@ -9,21 +9,22 @@ import { getFileIcon } from '@fastgpt/global/common/file/icon';
import { formatFileSize } from '@fastgpt/global/common/file/tools';
import { clone } from 'lodash';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { Control, useFieldArray } from 'react-hook-form';
import { UseFieldArrayReturn } from 'react-hook-form';
import { ChatBoxInputFormType, UserInputFileItemType } from '../type';
import { AppFileSelectConfigType } from '@fastgpt/global/core/app/type';
import { documentFileType } from '@fastgpt/global/common/file/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
interface UseFileUploadOptions {
outLinkAuthData: any;
type UseFileUploadOptions = {
outLinkAuthData: OutLinkChatAuthProps;
chatId: string;
fileSelectConfig: AppFileSelectConfigType;
control: Control<ChatBoxInputFormType, any>;
}
fileCtrl: UseFieldArrayReturn<ChatBoxInputFormType, 'files', 'id'>;
};
export const useFileUpload = (props: UseFileUploadOptions) => {
const { outLinkAuthData, chatId, fileSelectConfig, control } = props;
const { outLinkAuthData, chatId, fileSelectConfig, fileCtrl } = props;
const { toast } = useToast();
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
@@ -33,15 +34,13 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
remove: removeFiles,
fields: fileList,
replace: replaceFiles
} = useFieldArray({
control: control,
name: 'files'
});
} = fileCtrl;
const showSelectFile = fileSelectConfig?.canSelectFile;
const showSelectImg = fileSelectConfig?.canSelectImg;
const maxSelectFiles = fileSelectConfig?.maxFiles ?? 10;
const maxSize = (feConfigs?.uploadFileMaxSize || 1024) * 1024 * 1024; // nkb
const canSelectFileAmount = maxSelectFiles - fileList.length;
const { icon: selectFileIcon, label: selectFileLabel } = useMemo(() => {
if (showSelectFile && showSelectImg) {
@@ -66,7 +65,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: `${showSelectImg ? 'image/*,' : ''} ${showSelectFile ? documentFileType : ''}`,
multiple: true,
maxCount: maxSelectFiles
maxCount: canSelectFileAmount
});
const onSelectFile = useCallback(

View File

@@ -393,7 +393,7 @@ const ChatBox = (
isInteractivePrompt = false
}) => {
variablesForm.handleSubmit(
async (variables) => {
async ({ variables }) => {
if (!onStartChat) return;
if (isChatting) {
toast({

View File

@@ -20,9 +20,9 @@ export type UserInputFileItemType = {
export type ChatBoxInputFormType = {
input: string;
files: UserInputFileItemType[];
files: UserInputFileItemType[]; // global files
chatStarted: boolean;
[key: string]: any;
variables: Record<string, any>;
};
export type ChatBoxInputType = {

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import { Controller } from 'react-hook-form';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Controller, useFieldArray } from 'react-hook-form';
import RenderPluginInput from './renderPluginInput';
import { Box, Button, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
@@ -14,7 +14,8 @@ import { useFileUpload } from '../../ChatBox/hooks/useFileUpload';
import FilePreview from '../../components/FilePreview';
import { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { ChatBoxInputFormType, UserInputFileItemType } from '../../ChatBox/type';
import { ChatBoxInputFormType } from '../../ChatBox/type';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
const RenderInput = () => {
const { t } = useTranslation();
@@ -29,9 +30,7 @@ const RenderInput = () => {
isChatting,
chatConfig,
chatId,
outLinkAuthData,
restartInputStore,
setRestartInputStore
outLinkAuthData
} = useContextSelector(PluginRunContext, (v) => v);
const {
@@ -42,6 +41,11 @@ const RenderInput = () => {
formState: { errors }
} = variablesForm;
/* ===> Global files(abandon) */
const fileCtrl = useFieldArray({
control: variablesForm.control,
name: 'files'
});
const {
File,
onOpenSelectFile,
@@ -57,41 +61,72 @@ const RenderInput = () => {
outLinkAuthData,
chatId: chatId || '',
fileSelectConfig: chatConfig?.fileSelectConfig,
control
fileCtrl
});
const isDisabledInput = histories.length > 0;
const hasFileUploading = useMemo(() => {
return fileList.some((item) => !item.url);
}, [fileList]);
useRequest2(uploadFiles, {
manual: false,
errorToast: t('common:upload_file_error'),
refreshDeps: [fileList, outLinkAuthData, chatId]
});
/* Global files(abandon) <=== */
const [restartData, setRestartData] = useState<ChatBoxInputFormType>();
const onClickNewChat = useCallback(
(e: ChatBoxInputFormType, files: UserInputFileItemType[] = []) => {
setRestartInputStore({
...e,
files
});
(e: ChatBoxInputFormType) => {
setRestartData(e);
onNewChat?.();
},
[onNewChat, setRestartInputStore]
[onNewChat]
);
const formatPluginInputs = useMemo(() => {
if (histories.length === 0) return pluginInputs;
try {
const historyValue = histories[0]?.value as UserChatItemValueItemType[];
const inputValueString = historyValue.find((item) => item.type === 'text')?.text?.content;
if (!inputValueString) return pluginInputs;
return JSON.parse(inputValueString) as FlowNodeInputItemType[];
} catch (error) {
console.error('Failed to parse input value:', error);
return pluginInputs;
}
}, [histories, pluginInputs]);
// Reset input value
useEffect(() => {
// Set last run value
if (!isDisabledInput && restartInputStore) {
reset(restartInputStore);
// Set config default value
if (histories.length === 0) {
// Restart
if (restartData) {
reset(restartData);
setRestartData(undefined);
return;
}
const defaultFormValues = formatPluginInputs.reduce(
(acc, input) => {
acc[input.key] = input.defaultValue;
return acc;
},
{} as Record<string, any>
);
reset({
files: [],
variables: defaultFormValues
});
return;
}
// Set history to default value
const historyVariables = (() => {
const historyValue = histories[0]?.value as UserChatItemValueItemType[];
if (!historyValue) return undefined;
const defaultFormValues = pluginInputs.reduce(
(acc, input) => {
acc[input.key] = input.defaultValue;
return acc;
},
{} as Record<string, any>
);
const historyFormValues = (() => {
if (!isDisabledInput) return undefined;
const historyValue = histories[0].value;
try {
const inputValueString = historyValue.find((item) => item.type === 'text')?.text?.content;
return (
@@ -115,32 +150,24 @@ const RenderInput = () => {
return undefined;
}
})();
// Parse history file
const historyFileList = (() => {
if (!isDisabledInput) return [];
const historyValue = histories[0].value as UserChatItemValueItemType[];
return historyValue.filter((item) => item.type === 'file').map((item) => item.file);
const historyValue = histories[0]?.value as UserChatItemValueItemType[];
return historyValue?.filter((item) => item.type === 'file').map((item) => item.file);
})();
reset({
...(historyFormValues || defaultFormValues),
variables: historyVariables,
files: historyFileList
});
}, [getValues, histories, isDisabledInput, pluginInputs, replaceFiles, reset, restartInputStore]);
}, [histories.length]);
const hasFileUploading = useMemo(() => {
return fileList.some((item) => !item.url);
}, [fileList]);
const [uploading, setUploading] = useState(false);
useRequest2(uploadFiles, {
manual: false,
errorToast: t('common:upload_file_error'),
refreshDeps: [fileList, outLinkAuthData, chatId]
});
const fileUploading = uploading || hasFileUploading;
return (
<>
<Box>
{/* instruction */}
{chatConfig?.instruction && (
<Box
@@ -155,7 +182,7 @@ const RenderInput = () => {
<Markdown source={chatConfig.instruction} />
</Box>
)}
{/* file select */}
{/* file select(Abandoned) */}
{(showSelectFile || showSelectImg) && (
<Box mb={5}>
<Flex alignItems={'center'}>
@@ -184,12 +211,12 @@ const RenderInput = () => {
</Box>
)}
{/* Filed */}
{pluginInputs.map((input) => {
{formatPluginInputs.map((input) => {
return (
<Controller
key={input.key}
key={`variables.${input.key}`}
control={control}
name={input.key}
name={`variables.${input.key}`}
rules={{
validate: (value) => {
if (!input.required) return true;
@@ -207,6 +234,7 @@ const RenderInput = () => {
isDisabled={isDisabledInput}
isInvalid={errors && Object.keys(errors).includes(input.key)}
input={input}
setUploading={setUploading}
/>
);
}}
@@ -217,13 +245,14 @@ const RenderInput = () => {
{onStartChat && onNewChat && (
<Flex justifyContent={'end'} mt={8}>
<Button
isLoading={isChatting || hasFileUploading}
isLoading={isChatting}
isDisabled={fileUploading}
onClick={() => {
handleSubmit((e) => {
if (isDisabledInput) {
onClickNewChat(e, fileList);
onClickNewChat(e);
} else {
onSubmit(e, fileList);
onSubmit(e);
}
})();
}}
@@ -232,7 +261,7 @@ const RenderInput = () => {
</Button>
</Flex>
)}
</>
</Box>
);
};

View File

@@ -1,5 +1,6 @@
import {
Box,
Button,
Flex,
NumberDecrementStepper,
NumberIncrementStepper,
@@ -12,25 +13,130 @@ import {
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MySelect from '@fastgpt/web/components/common/MySelect';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { useFileUpload } from '../../ChatBox/hooks/useFileUpload';
import { useContextSelector } from 'use-context-selector';
import { PluginRunContext } from '../context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import FilePreview from '../../components/FilePreview';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useEffect, useMemo } from 'react';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useFieldArray } from 'react-hook-form';
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
const FileSelector = ({
input,
setUploading,
onChange
}: {
input: FlowNodeInputItemType;
setUploading: React.Dispatch<React.SetStateAction<boolean>>;
onChange: (...event: any[]) => void;
}) => {
const { t } = useTranslation();
const { variablesForm, histories, chatId, outLinkAuthData } = useContextSelector(
PluginRunContext,
(v) => v
);
const fileCtrl = useFieldArray({
control: variablesForm.control,
name: `variables.${input.key}`
});
const {
File,
fileList,
selectFileIcon,
uploadFiles,
onOpenSelectFile,
onSelectFile,
removeFiles
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
fileSelectConfig: {
canSelectFile: input.canSelectFile ?? true,
canSelectImg: input.canSelectImg ?? false,
maxFiles: input.maxFiles ?? 5
},
// @ts-ignore
fileCtrl
});
const isDisabledInput = histories.length > 0;
useRequest2(uploadFiles, {
manual: false,
errorToast: t('common:upload_file_error'),
refreshDeps: [fileList, outLinkAuthData, chatId]
});
const hasFileUploading = useMemo(() => {
return fileList.some((item) => !item.url);
}, [fileList]);
useEffect(() => {
setUploading(hasFileUploading);
onChange(
fileList.map((item) => ({
type: item.type,
name: item.name,
url: item.url,
icon: item.icon
}))
);
}, [fileList, hasFileUploading, onChange, setUploading]);
return (
<>
<Flex alignItems={'center'}>
<Box position={'relative'}>
{input.required && (
<Box position={'absolute'} left={-2} top={'-1px'} color={'red.600'}>
*
</Box>
)}
<FormLabel fontWeight={'500'}>{t(input.label as any)}</FormLabel>
</Box>
{input.description && <QuestionTip ml={2} label={t(input.description as any)} />}
<Box flex={1} />
{/* 有历史记录,说明是已经跑过了,不能再新增了 */}
<Button
isDisabled={histories.length !== 0}
leftIcon={<MyIcon name={selectFileIcon as any} w={'16px'} />}
variant={'whiteBase'}
onClick={() => {
onOpenSelectFile();
}}
>
{t('chat:select')}
</Button>
</Flex>
<FilePreview fileList={fileList} removeFiles={isDisabledInput ? undefined : removeFiles} />
{fileList.length === 0 && <EmptyTip py={0} mt={3} text={t('chat:not_select_file')} />}
<File onSelect={(files) => onSelectFile({ files, fileList })} />
</>
);
};
const RenderPluginInput = ({
value,
onChange,
isDisabled,
isInvalid,
input
input,
setUploading
}: {
value: any;
onChange: () => void;
onChange: (...event: any[]) => void;
isDisabled?: boolean;
isInvalid: boolean;
input: FlowNodeInputItemType;
setUploading: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
const { t } = useTranslation();
const inputType = input.renderTypeList[0];
@@ -44,6 +150,10 @@ const RenderPluginInput = ({
<MySelect list={input.list} value={value} onchange={onChange} isDisabled={isDisabled} />
);
}
if (inputType === FlowNodeInputTypeEnum.fileSelect) {
return <FileSelector onChange={onChange} input={input} setUploading={setUploading} />;
}
if (input.valueType === WorkflowIOValueTypeEnum.string) {
return (
<Textarea
@@ -100,22 +210,26 @@ const RenderPluginInput = ({
);
})();
return !!render ? (
<Box _notLast={{ mb: 4 }} px={1}>
<Flex alignItems={'center'} mb={1}>
<Box position={'relative'}>
{input.required && (
<Box position={'absolute'} left={-2} top={'-1px'} color={'red.600'}>
*
</Box>
)}
{t(input.label as any)}
</Box>
{input.description && <QuestionTip ml={2} label={t(input.description as any)} />}
</Flex>
return (
<Box _notLast={{ mb: 4 }}>
{/* label */}
{inputType !== FlowNodeInputTypeEnum.fileSelect && (
<Flex alignItems={'center'} mb={1}>
<Box position={'relative'}>
{input.required && (
<Box position={'absolute'} left={-2} top={'-1px'} color={'red.600'}>
*
</Box>
)}
<FormLabel fontWeight={'500'}>{t(input.label as any)}</FormLabel>
</Box>
{input.description && <QuestionTip ml={2} label={t(input.description as any)} />}
</Flex>
)}
{render}
</Box>
) : null;
);
};
export default RenderPluginInput;

View File

@@ -1,4 +1,4 @@
import React, { ReactNode, useCallback, useMemo, useRef, useState } from 'react';
import React, { ReactNode, useCallback, useMemo, useRef } from 'react';
import { createContext } from 'use-context-selector';
import { PluginRunBoxProps } from './type';
import {
@@ -8,7 +8,6 @@ import {
} from '@fastgpt/global/core/chat/type';
import { FieldValues, useForm } from 'react-hook-form';
import { PluginRunBoxTabEnum } from './constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
@@ -16,17 +15,15 @@ import { generatingMessageProps } from '../type';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { useTranslation } from 'next-i18next';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { ChatBoxInputFormType, UserInputFileItemType } from '../ChatBox/type';
import { ChatBoxInputFormType } from '../ChatBox/type';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils';
type PluginRunContextType = OutLinkChatAuthProps &
PluginRunBoxProps & {
isChatting: boolean;
onSubmit: (e: ChatBoxInputFormType, files?: UserInputFileItemType[]) => Promise<any>;
onSubmit: (e: ChatBoxInputFormType) => Promise<any>;
outLinkAuthData: OutLinkChatAuthProps;
restartInputStore?: ChatBoxInputFormType;
setRestartInputStore: React.Dispatch<React.SetStateAction<ChatBoxInputFormType | undefined>>;
};
export const PluginRunContext = createContext<PluginRunContextType>({
@@ -59,8 +56,6 @@ const PluginRunContextProvider = ({
}: PluginRunBoxProps & { children: ReactNode }) => {
const { pluginInputs, onStartChat, setHistories, histories, setTab } = props;
const [restartInputStore, setRestartInputStore] = useState<ChatBoxInputFormType>();
const { toast } = useToast();
const chatController = useRef(new AbortController());
const { t } = useTranslation();
@@ -80,9 +75,7 @@ const PluginRunContextProvider = ({
);
const variablesForm = useForm<ChatBoxInputFormType>({
defaultValues: {
files: []
}
defaultValues: {}
});
const generatingMessage = useCallback(
@@ -179,8 +172,8 @@ const PluginRunContextProvider = ({
[histories]
);
const { runAsync: onSubmit } = useRequest2(
async (e: ChatBoxInputFormType, files?: UserInputFileItemType[]) => {
const onSubmit = useCallback(
async ({ variables, files }: ChatBoxInputFormType) => {
if (!onStartChat) return;
if (isChatting) {
toast({
@@ -199,7 +192,7 @@ const PluginRunContextProvider = ({
{
...getPluginRunUserQuery({
pluginInputs,
variables: e,
variables,
files: files as RuntimeUserPromptType['files']
}),
status: 'finish'
@@ -234,10 +227,13 @@ const PluginRunContextProvider = ({
try {
const { responseData } = await onStartChat({
messages: messages,
messages,
controller: chatController.current,
generatingMessage,
variables: e
variables: {
files: files,
...variables
}
});
setHistories((state) =>
@@ -262,7 +258,18 @@ const PluginRunContextProvider = ({
})
);
}
}
},
[
abortRequest,
generatingMessage,
isChatting,
onStartChat,
pluginInputs,
setHistories,
setTab,
t,
toast
]
);
const contextValue: PluginRunContextType = {
@@ -270,9 +277,7 @@ const PluginRunContextProvider = ({
isChatting,
onSubmit,
outLinkAuthData,
variablesForm,
restartInputStore,
setRestartInputStore
variablesForm
};
return <PluginRunContext.Provider value={contextValue}>{children}</PluginRunContext.Provider>;
};

View File

@@ -18,13 +18,12 @@ const RenderFilePreview = ({
return fileList.length > 0 ? (
<Flex
maxH={'250px'}
overflowY={'auto'}
overflow={'visible'}
wrap={'wrap'}
pt={3}
userSelect={'none'}
mb={fileList.length > 0 ? 2 : 0}
pr={0.5}
gap={'6px'}
>
{fileList.map((item, index) => {
const isFile = item.type === ChatFileTypeEnum.file;
@@ -33,11 +32,8 @@ const RenderFilePreview = ({
<MyBox
key={index}
maxW={isFile ? 56 : 14}
w={isFile ? '50%' : '12.5%'}
w={isFile ? 'calc(50% - 3px)' : '12.5%'}
aspectRatio={isFile ? 4 : 1}
pr={1.5}
pb={1.5}
mb={0.5}
>
<Box
border={'sm'}

View File

@@ -28,13 +28,24 @@ export const useChat = (params?: { chatId?: string; appId: string; type?: GetCha
// Reset to empty input
const data = variablesForm.getValues();
for (const key in data) {
data[key] = '';
// Reset the old variables to empty
const resetVariables: Record<string, any> = {};
for (const key in data.variables) {
resetVariables[key] = (() => {
if (Array.isArray(data.variables[key])) {
return [];
}
return '';
})();
}
variablesForm.reset({
...data,
...variables
variables: {
...resetVariables,
...variables
}
});
},
[variablesForm]
@@ -42,8 +53,8 @@ export const useChat = (params?: { chatId?: string; appId: string; type?: GetCha
const clearChatRecords = useCallback(() => {
const data = variablesForm.getValues();
for (const key in data) {
variablesForm.setValue(key, '');
for (const key in data.variables) {
variablesForm.setValue(`variables.${key}`, '');
}
ChatBoxRef.current?.restartChat?.();

View File

@@ -387,6 +387,7 @@ const RenderList = React.memo(function RenderList({
isInvalid={errors && Object.keys(errors).includes(input.key)}
onChange={onChange}
input={input}
setUploading={() => {}}
/>
);
}}

View File

@@ -465,7 +465,8 @@ const RenderList = React.memo(function RenderList({
// Add default values to some inputs
const defaultValueMap: Record<string, any> = {
[NodeInputKeyEnum.userChatInput]: undefined
[NodeInputKeyEnum.userChatInput]: undefined,
[NodeInputKeyEnum.fileUrlList]: undefined
};
nodeList.forEach((node) => {
if (node.flowNodeType === FlowNodeTypeEnum.workflowStart) {
@@ -473,6 +474,10 @@ const RenderList = React.memo(function RenderList({
node.nodeId,
NodeOutputKeyEnum.userChatInput
];
defaultValueMap[NodeInputKeyEnum.fileUrlList] = [
node.nodeId,
NodeOutputKeyEnum.userFiles
];
}
});

View File

@@ -46,11 +46,6 @@ const InputFormEditModal = ({
const inputType = watch('type') || FlowNodeInputTypeEnum.input;
const maxLength = watch('maxLength');
const max = watch('max');
const min = watch('min');
const defaultInputValue = watch('defaultValue');
const inputTypeList = [
{
icon: 'core/workflow/inputType/input',
@@ -187,14 +182,9 @@ const InputFormEditModal = ({
type={'formInput'}
isEdit={isEdit}
inputType={inputType}
maxLength={maxLength}
max={max}
min={min}
defaultValue={defaultInputValue}
onClose={onClose}
onSubmitSuccess={onSubmitSuccess}
onSubmitError={onSubmitError}
valueType={defaultValueType}
/>
</Flex>
</MyModal>

View File

@@ -11,7 +11,6 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useBoolean } from 'ahooks';
import InputTypeConfig from './InputTypeConfig';
export const defaultInput: FlowNodeInputItemType = {
@@ -23,7 +22,10 @@ export const defaultInput: FlowNodeInputItemType = {
label: '',
description: '',
defaultValue: '',
list: [{ label: '', value: '' }]
list: [{ label: '', value: '' }],
maxFiles: 5,
canSelectFile: true,
canSelectImg: true
};
const FieldEditModal = ({
@@ -108,6 +110,13 @@ const FieldEditModal = ({
])
],
[
{
icon: 'core/workflow/inputType/file',
label: t('app:file_upload'),
value: [FlowNodeInputTypeEnum.fileSelect],
defaultValueType: WorkflowIOValueTypeEnum.arrayString,
description: t('app:file_upload_tip')
},
{
icon: 'core/workflow/inputType/customVariable',
label: t('common:core.workflow.inputType.custom'),
@@ -130,19 +139,10 @@ const FieldEditModal = ({
const form = useForm({
defaultValues: defaultValue
});
const { getValues, setValue, watch, reset } = form;
const { setValue, watch, reset } = form;
const renderTypeList = watch('renderTypeList');
const inputType = renderTypeList[0] || FlowNodeInputTypeEnum.reference;
const valueType = watch('valueType');
const [isToolInput, { toggle: setIsToolInput }] = useBoolean(!!getValues('toolDescription'));
const maxLength = watch('maxLength');
const max = watch('max');
const min = watch('min');
const selectValueTypeList = watch('customInputConfig.selectValueTypeList');
const defaultInputValue = watch('defaultValue');
const defaultValueType = useMemo(
() =>
@@ -190,8 +190,8 @@ const FieldEditModal = ({
}
}
// Focus remove toolDescription
if (isToolInput && data.renderTypeList.includes(FlowNodeInputTypeEnum.reference)) {
// Get toolDescription and removes the types of some unusable tools
if (data.toolDescription && data.renderTypeList.includes(FlowNodeInputTypeEnum.reference)) {
data.toolDescription = data.description;
} else {
data.toolDescription = undefined;
@@ -211,18 +211,7 @@ const FieldEditModal = ({
reset(defaultInput);
}
},
[
defaultValue.key,
defaultValueType,
isEdit,
isToolInput,
keys,
onSubmit,
t,
toast,
onClose,
reset
]
[defaultValue.key, defaultValueType, isEdit, keys, onSubmit, t, toast, onClose, reset]
);
const onSubmitError = useCallback(
(e: Object) => {
@@ -241,7 +230,7 @@ const FieldEditModal = ({
return (
<MyModal
isOpen={true}
isOpen
onClose={onClose}
iconSrc="/imgs/workflow/extract.png"
title={isEdit ? t('workflow:edit_input') : t('workflow:add_new_input')}
@@ -321,14 +310,6 @@ const FieldEditModal = ({
isEdit={isEdit}
onClose={onClose}
inputType={inputType}
maxLength={maxLength}
max={max}
min={min}
selectValueTypeList={selectValueTypeList}
defaultValue={defaultInputValue}
isToolInput={isToolInput}
setIsToolInput={setIsToolInput}
valueType={valueType}
defaultValueType={defaultValueType}
onSubmitSuccess={onSubmitSuccess}
onSubmitError={onSubmitError}

View File

@@ -3,7 +3,6 @@ import {
Button,
Flex,
FormControl,
FormLabel,
HStack,
Input,
NumberDecrementStepper,
@@ -23,7 +22,6 @@ import {
FlowNodeInputTypeEnum,
FlowValueTypeMap
} from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MultipleSelect from '@fastgpt/web/components/common/MySelect/MultipleSelect';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
@@ -36,7 +34,10 @@ import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
type ListValueType = { id: string; value: string; label: string }[];
import ChatFunctionTip from '@/components/core/app/Tip';
import MySlider from '@/components/Slider';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const InputTypeConfig = ({
form,
@@ -44,36 +45,18 @@ const InputTypeConfig = ({
onClose,
type,
inputType,
maxLength,
max,
min,
selectValueTypeList,
defaultValue,
isToolInput,
setIsToolInput,
valueType,
defaultValueType,
onSubmitSuccess,
onSubmitError
}: {
// Common fields
form: UseFormReturn<any>;
form: UseFormReturn<any, any>;
isEdit: boolean;
onClose: () => void;
type: 'plugin' | 'formInput' | 'variable';
inputType: FlowNodeInputTypeEnum | VariableInputEnum;
maxLength?: number;
max?: number;
min?: number;
selectValueTypeList?: WorkflowIOValueTypeEnum[];
defaultValue?: string;
// Plugin-specific fields
isToolInput?: boolean;
setIsToolInput?: () => void;
valueType?: WorkflowIOValueTypeEnum;
defaultValueType?: WorkflowIOValueTypeEnum;
// Update methods
@@ -82,9 +65,7 @@ const InputTypeConfig = ({
}) => {
const { t } = useTranslation();
const defaultListValue = { label: t('common:None'), value: '' };
const { register, setValue, handleSubmit, control, watch } = form;
const listValue: ListValueType = watch('list');
const { feConfigs } = useSystemStore();
const typeLabels = {
name: {
@@ -99,6 +80,18 @@ const InputTypeConfig = ({
}
};
const { register, setValue, handleSubmit, control, watch } = form;
const maxLength = watch('maxLength');
const max = watch('max');
const min = watch('min');
const selectValueTypeList = watch('customInputConfig.selectValueTypeList');
const defaultValue = watch('defaultValue');
const valueType = watch('valueType');
const toolDescription = watch('toolDescription');
const isToolInput = !!toolDescription;
const listValue = watch('list') ?? [];
const {
fields: selectEnums,
append: appendEnums,
@@ -166,6 +159,10 @@ const InputTypeConfig = ({
return type === 'plugin' && list.includes(inputType as FlowNodeInputTypeEnum);
}, [inputType, type]);
// File select
const maxFiles = watch('maxFiles');
const maxSelectFiles = Math.min(feConfigs?.uploadFileMaxAmount ?? 20, 50);
return (
<Stack flex={1} borderLeft={'1px solid #F0F1F6'} justifyContent={'space-between'}>
<Flex flexDirection={'column'} p={8} pb={2} gap={4} flex={'1 0 0'} overflow={'auto'}>
@@ -189,7 +186,9 @@ const InputTypeConfig = ({
bg={'myGray.50'}
placeholder={t('workflow:field_description_placeholder')}
rows={3}
{...register('description', { required: isToolInput ? true : false })}
{...register('description', {
required: showIsToolInput && isToolInput ? true : false
})}
/>
</Flex>
@@ -213,7 +212,7 @@ const InputTypeConfig = ({
</Box>
) : (
<Box fontSize={'14px'} mb={2}>
{defaultValueType}
{defaultValueType ? t(FlowValueTypeMap[defaultValueType]?.label as any) : ''}
</Box>
)}
</Flex>
@@ -236,7 +235,7 @@ const InputTypeConfig = ({
<Switch
isChecked={isToolInput}
onChange={(e) => {
setIsToolInput && setIsToolInput();
setValue('toolDescription', e.target.checked ? 'sign' : '');
}}
/>
</Flex>
@@ -341,7 +340,7 @@ const InputTypeConfig = ({
value: item.value
}))}
value={
defaultValue && listValue.map((item) => item.value).includes(defaultValue)
defaultValue && listValue.map((item: any) => item.value).includes(defaultValue)
? defaultValue
: ''
}
@@ -357,12 +356,12 @@ const InputTypeConfig = ({
{inputType === FlowNodeInputTypeEnum.addInputParam && (
<>
<Flex alignItems={'center'}>
{/* <Flex alignItems={'center'}>
<FormLabel flex={'0 0 132px'} fontWeight={'medium'}>
{t('common:core.module.Input Type')}
</FormLabel>
<Box fontSize={'14px'}>{t('workflow:only_the_reference_type_is_supported')}</Box>
</Flex>
</Flex> */}
<Box>
<HStack mb={1}>
<FormLabel fontWeight={'medium'}>{t('workflow:optional_value_type')}</FormLabel>
@@ -389,7 +388,9 @@ const InputTypeConfig = ({
.map((id) => mergedSelectEnums.find((item) => item.id === id))
.filter(Boolean) as { id: string; value: string }[];
removeEnums();
newSelectEnums.forEach((item) => appendEnums(item));
newSelectEnums.forEach((item) =>
appendEnums({ label: item.value, value: item.value })
);
// 防止最后一个元素被focus
setTimeout(() => {
@@ -505,6 +506,60 @@ const InputTypeConfig = ({
</Button>
</>
)}
{inputType === FlowNodeInputTypeEnum.fileSelect && (
<>
<Flex alignItems={'center'} minH={'40px'}>
<FormLabel flex={'0 0 132px'} fontWeight={'medium'}>
{t('app:document_upload')}
</FormLabel>
<Switch
{...register('canSelectFile', {
required: true
})}
/>
</Flex>
<Box w={'full'} minH={'40px'}>
<Flex alignItems={'center'}>
<FormLabel flex={'0 0 132px'} fontWeight={'medium'}>
{t('app:image_upload')}
</FormLabel>
<Switch
{...register('canSelectImg', {
required: true
})}
/>
</Flex>
<Flex color={'myGray.500'}>
<Box fontSize={'xs'}>{t('app:image_upload_tip')}</Box>
<ChatFunctionTip type="visionModel" />
</Flex>
</Box>
<Box>
<HStack>
<FormLabel fontWeight={'medium'}>{t('app:upload_file_max_amount')}</FormLabel>
<QuestionTip label={t('app:upload_file_max_amount_tip')} />
</HStack>
<Box mt={5}>
<MySlider
markList={[
{ label: '1', value: 1 },
{ label: `${maxSelectFiles}`, value: maxSelectFiles }
]}
width={'100%'}
min={1}
max={maxSelectFiles}
step={1}
value={maxFiles ?? 5}
onChange={(e) => {
setValue('maxFiles', e);
}}
/>
</Box>
</Box>
</>
)}
</Flex>
<Flex justify={'flex-end'} gap={3} pb={8} pr={8}>
@@ -514,10 +569,7 @@ const InputTypeConfig = ({
<Button
variant={'primaryOutline'}
fontWeight={'medium'}
onClick={handleSubmit(
(data: FlowNodeInputItemType) => onSubmitSuccess(data, 'confirm'),
onSubmitError
)}
onClick={handleSubmit((data) => onSubmitSuccess(data, 'confirm'), onSubmitError)}
w={20}
>
{t('common:common.Confirm')}
@@ -525,10 +577,7 @@ const InputTypeConfig = ({
{!isEdit && (
<Button
fontWeight={'medium'}
onClick={handleSubmit(
(data: FlowNodeInputItemType) => onSubmitSuccess(data, 'continue'),
onSubmitError
)}
onClick={handleSubmit((data) => onSubmitSuccess(data, 'continue'), onSubmitError)}
w={20}
>
{t('common:common.Continue_Adding')}
@@ -539,4 +588,4 @@ const InputTypeConfig = ({
);
};
export default React.memo(InputTypeConfig);
export default InputTypeConfig;

View File

@@ -114,46 +114,55 @@ function Instruction({ chatConfig: { instruction }, setAppDetail }: ComponentPro
}
function FileSelectConfig({ chatConfig: { fileSelectConfig }, setAppDetail }: ComponentProps) {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodes = useContextSelector(WorkflowContext, (v) => v.nodes);
const pluginInputNode = nodes.find((item) => item.type === FlowNodeTypeEnum.pluginInput)!;
return (
<FileSelect
value={fileSelectConfig}
color={'myGray.600'}
fontWeight={'medium'}
fontSize={'14px'}
onChange={(e) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
fileSelectConfig: e
}
}));
<>
<FileSelect
value={fileSelectConfig}
color={'myGray.600'}
fontWeight={'medium'}
fontSize={'sm'}
onChange={(e) => {
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
fileSelectConfig: e
}
}));
// Dynamic add or delete userFilesInput
const canUploadFiles = e.canSelectFile || e.canSelectImg;
const repeatKey = pluginInputNode?.data.outputs.find(
(item) => item.key === userFilesInput.key
);
if (canUploadFiles) {
!repeatKey &&
onChangeNode({
nodeId: pluginInputNode.id,
type: 'addOutput',
value: userFilesInput
});
} else {
repeatKey &&
onChangeNode({
nodeId: pluginInputNode.id,
type: 'delOutput',
key: userFilesInput.key
});
}
}}
/>
// Dynamic add or delete userFilesInput
const canUploadFiles = e.canSelectFile || e.canSelectImg;
const repeatKey = pluginInputNode?.data.outputs.find(
(item) => item.key === userFilesInput.key
);
if (canUploadFiles) {
!repeatKey &&
onChangeNode({
nodeId: pluginInputNode.id,
type: 'addOutput',
value: {
...userFilesInput,
label: t('workflow:plugin.global_file_input')
}
});
} else {
repeatKey &&
onChangeNode({
nodeId: pluginInputNode.id,
type: 'delOutput',
key: userFilesInput.key
});
}
}}
/>
<Box fontSize={'mini'} color={'myGray.500'}>
{t('workflow:plugin_file_abandon_tip')}
</Box>
</>
);
}

View File

@@ -142,7 +142,7 @@ const NodePluginInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
}}
/>
</Container>
{!!outputs.filter((output) => output.type !== FlowNodeOutputTypeEnum.hidden).length && (
{outputs.length != inputs.length && (
<Container>
<IOTitle text={t('common:common.Output')} />
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />

View File

@@ -55,9 +55,9 @@ const InputLabel = ({ nodeId, input }: Props) => {
{description && <QuestionTip ml={1} label={t(description as any)}></QuestionTip>}
</Flex>
{/* value type */}
{renderType === FlowNodeInputTypeEnum.reference && (
<ValueTypeLabel valueType={valueType} valueDesc={valueDesc} />
)}
{[FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.fileSelect].includes(
renderType
) && <ValueTypeLabel valueType={valueType} valueDesc={valueDesc} />}
{/* input type select */}
{renderTypeList && renderTypeList.length > 1 && (

View File

@@ -16,6 +16,10 @@ const RenderList: {
types: [FlowNodeInputTypeEnum.reference],
Component: dynamic(() => import('./templates/Reference'))
},
{
types: [FlowNodeInputTypeEnum.fileSelect],
Component: dynamic(() => import('./templates/Reference'))
},
{
types: [FlowNodeInputTypeEnum.select],
Component: dynamic(() => import('./templates/Select'))

View File

@@ -34,7 +34,6 @@ import { useChat } from '@/components/core/chat/ChatContainer/useChat';
import ChatBox from '@/components/core/chat/ChatContainer/ChatBox';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { InitChatResponse } from '@/global/core/chat/api';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
const CustomPluginRunBox = dynamic(() => import('./components/CustomPluginRunBox'));

View File

@@ -11,7 +11,11 @@ import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import {
NodeInputKeyEnum,
NodeOutputKeyEnum,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
@@ -30,9 +34,11 @@ import {
AiChatQuoteTemplate
} from '@fastgpt/global/core/workflow/template/system/aiChat/index';
import { DatasetSearchModule } from '@fastgpt/global/core/workflow/template/system/datasetSearch';
import { ReadFilesNode } from '@fastgpt/global/core/workflow/template/system/readFiles';
import { i18nT } from '@fastgpt/web/i18n/utils';
import { Input_Template_UserChatInput } from '@fastgpt/global/core/workflow/template/input';
import {
Input_Template_File_Link_Prompt,
Input_Template_UserChatInput
} from '@fastgpt/global/core/workflow/template/input';
type WorkflowType = {
nodes: StoreNodeItemType[];
@@ -173,6 +179,10 @@ export function form2AppWorkflow(
valueType: WorkflowIOValueTypeEnum.datasetQuote,
value: selectedDatasets?.length > 0 ? [datasetNodeId, 'quoteQA'] : undefined
},
{
...Input_Template_File_Link_Prompt,
value: [workflowStartNodeId, NodeOutputKeyEnum.userFiles]
},
{
key: NodeInputKeyEnum.aiChatVision,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
@@ -321,44 +331,6 @@ export function form2AppWorkflow(
]
}
: null;
// Read file tool config
const readFileTool: WorkflowType | null = data.chatConfig.fileSelectConfig?.canSelectFile
? {
nodes: [
{
nodeId: ReadFilesNode.id,
name: t(ReadFilesNode.name),
intro: t(ReadFilesNode.intro),
avatar: ReadFilesNode.avatar,
flowNodeType: ReadFilesNode.flowNodeType,
showStatus: true,
position: {
x: 974.6209854328943,
y: 587.6378828744465
},
version: ReadFilesNode.version,
inputs: [
{
key: NodeInputKeyEnum.fileUrlList,
renderTypeList: [FlowNodeInputTypeEnum.reference],
valueType: WorkflowIOValueTypeEnum.arrayString,
label: t('app:workflow.file_url'),
value: [workflowStartNodeId, 'userFiles']
}
],
outputs: ReadFilesNode.outputs
}
],
edges: [
{
source: toolNodeId,
target: ReadFilesNode.id,
sourceHandle: 'selectedTools',
targetHandle: 'selectedTools'
}
]
}
: null;
// Computed tools config
const pluginTool: WorkflowType[] = formData.selectedTools.map((tool, i) => {
@@ -477,6 +449,10 @@ export function form2AppWorkflow(
max: 30,
value: formData.aiSettings.maxHistories
},
{
...Input_Template_File_Link_Prompt,
value: [workflowStartNodeId, NodeOutputKeyEnum.userFiles]
},
{
key: 'userChatInput',
renderTypeList: [FlowNodeInputTypeEnum.reference, FlowNodeInputTypeEnum.textarea],
@@ -497,7 +473,6 @@ export function form2AppWorkflow(
},
// tool nodes
...(datasetTool ? datasetTool.nodes : []),
...(readFileTool ? readFileTool.nodes : []),
...pluginTool.map((tool) => tool.nodes).flat()
],
edges: [
@@ -509,7 +484,6 @@ export function form2AppWorkflow(
},
// tool edges
...(datasetTool ? datasetTool.edges : []),
...(readFileTool ? readFileTool.edges : []),
...pluginTool.map((tool) => tool.edges).flat()
]
};
@@ -530,8 +504,7 @@ export function form2AppWorkflow(
}
const workflow = (() => {
if (data.selectedTools.length > 0 || data.chatConfig.fileSelectConfig?.canSelectFile)
return toolTemplates(data);
if (data.selectedTools.length > 0) return toolTemplates(data);
if (selectedDatasets.length > 0) return datasetTemplate(data);
return simpleChatTemplate(data);
})();