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:
@@ -16,6 +16,9 @@ OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
# 通用key。可以是 openai 的也可以是 oneapi 的。
|
||||
# 此处逻辑:优先走 ONEAPI_URL,如果填写了 ONEAPI_URL,key 也需要是 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 |
207
projects/app/public/imgs/app/visionModel.svg
Normal file
207
projects/app/public/imgs/app/visionModel.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 48 KiB |
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -393,7 +393,7 @@ const ChatBox = (
|
||||
isInteractivePrompt = false
|
||||
}) => {
|
||||
variablesForm.handleSubmit(
|
||||
async (variables) => {
|
||||
async ({ variables }) => {
|
||||
if (!onStartChat) return;
|
||||
if (isChatting) {
|
||||
toast({
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
@@ -387,6 +387,7 @@ const RenderList = React.memo(function RenderList({
|
||||
isInvalid={errors && Object.keys(errors).includes(input.key)}
|
||||
onChange={onChange}
|
||||
input={input}
|
||||
setUploading={() => {}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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'));
|
||||
|
||||
|
||||
@@ -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);
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user