mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 21:13:50 +00:00
feat: add form input node (#2773)
* add node * dispatch * extract InputTypeConfig component * question tip * fix build * fix * fix
This commit is contained in:
@@ -50,18 +50,26 @@ export const checkIsInteractiveByHistories = (chatHistories: ChatSiteItemType[])
|
||||
lastAIHistory.value.length - 1
|
||||
] as AIChatItemValueItemType;
|
||||
|
||||
return (
|
||||
if (
|
||||
lastMessageValue &&
|
||||
lastMessageValue.type === ChatItemValueTypeEnum.interactive &&
|
||||
!!lastMessageValue?.interactive?.params &&
|
||||
!!lastMessageValue?.interactive?.params
|
||||
) {
|
||||
const params = lastMessageValue.interactive.params;
|
||||
// 如果用户选择了,则不认为是交互模式(可能是上一轮以交互结尾,发起的新的一轮对话)
|
||||
!lastMessageValue?.interactive?.params?.userSelectedVal
|
||||
);
|
||||
if ('userSelectOptions' in params && 'userSelectedVal' in params) {
|
||||
return !params.userSelectedVal;
|
||||
} else if ('inputForm' in params && 'submitted' in params) {
|
||||
return !params.submitted;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const setUserSelectResultToHistories = (
|
||||
histories: ChatSiteItemType[],
|
||||
selectVal: string
|
||||
interactiveVal: string
|
||||
): ChatSiteItemType[] => {
|
||||
if (histories.length === 0) return histories;
|
||||
|
||||
@@ -77,18 +85,33 @@ export const setUserSelectResultToHistories = (
|
||||
)
|
||||
return val;
|
||||
|
||||
return {
|
||||
...val,
|
||||
interactive: {
|
||||
...val.interactive,
|
||||
params: {
|
||||
...val.interactive.params,
|
||||
userSelectedVal: val.interactive.params.userSelectOptions.find(
|
||||
(item) => item.value === selectVal
|
||||
)?.value
|
||||
if (val.interactive.type === 'userSelect') {
|
||||
return {
|
||||
...val,
|
||||
interactive: {
|
||||
...val.interactive,
|
||||
params: {
|
||||
...val.interactive.params,
|
||||
userSelectedVal: val.interactive.params.userSelectOptions.find(
|
||||
(item) => item.value === interactiveVal
|
||||
)?.value
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
if (val.interactive.type === 'userInput') {
|
||||
return {
|
||||
...val,
|
||||
interactive: {
|
||||
...val.interactive,
|
||||
params: {
|
||||
...val.interactive.params,
|
||||
submitted: true
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
|
@@ -6,7 +6,6 @@ import {
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
Select,
|
||||
Switch,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { StreamResponseType } from '@/web/common/api/fetch';
|
||||
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
|
||||
import { ChatSiteItemType, ToolModuleResponseItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { InteractiveNodeResponseItemType } from '@fastgpt/global/core/workflow/template/system/userSelect/type';
|
||||
import { InteractiveNodeResponseItemType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
|
||||
export type generatingMessageProps = {
|
||||
event: SseResponseEventEnum;
|
||||
|
@@ -7,7 +7,14 @@ import {
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Button,
|
||||
Flex
|
||||
Flex,
|
||||
Input,
|
||||
NumberDecrementStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
@@ -15,12 +22,22 @@ import {
|
||||
ToolModuleResponseItemType,
|
||||
UserChatItemValueItemType
|
||||
} from '@fastgpt/global/core/chat/type';
|
||||
import React from 'react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import { InteractiveNodeResponseItemType } from '@fastgpt/global/core/workflow/template/system/userSelect/type';
|
||||
import {
|
||||
InteractiveBasicType,
|
||||
UserInputInteractive,
|
||||
UserSelectInteractive
|
||||
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import { isEqual } from 'lodash';
|
||||
import { onSendPrompt } from '../ChatContainer/useChat';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
|
||||
type props = {
|
||||
value: UserChatItemValueItemType | AIChatItemValueItemType;
|
||||
@@ -123,10 +140,10 @@ ${toolResponse}`}
|
||||
},
|
||||
(prevProps, nextProps) => isEqual(prevProps, nextProps)
|
||||
);
|
||||
const RenderInteractive = React.memo(function RenderInteractive({
|
||||
const RenderUserSelectInteractive = React.memo(function RenderInteractive({
|
||||
interactive
|
||||
}: {
|
||||
interactive: InteractiveNodeResponseItemType;
|
||||
interactive: InteractiveBasicType & UserSelectInteractive;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
@@ -166,6 +183,114 @@ const RenderInteractive = React.memo(function RenderInteractive({
|
||||
</>
|
||||
);
|
||||
});
|
||||
const RenderUserFormInteractive = React.memo(function RenderFormInput({
|
||||
interactive
|
||||
}: {
|
||||
interactive: InteractiveBasicType & UserInputInteractive;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { register, setValue, handleSubmit: handleSubmitChat, control, reset } = useForm();
|
||||
|
||||
const onSubmit = useCallback((data: any) => {
|
||||
onSendPrompt({
|
||||
text: JSON.stringify(data),
|
||||
isInteractivePrompt: true
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (interactive.type === 'userInput') {
|
||||
const defaultValues = interactive.params.inputForm?.reduce(
|
||||
(acc: Record<string, any>, item) => {
|
||||
acc[item.label] = !!item.value ? item.value : item.defaultValue;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
reset(defaultValues);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} gap={2} w={'250px'}>
|
||||
{interactive.params.inputForm?.map((input) => (
|
||||
<Box key={input.label}>
|
||||
<Flex mb={1}>
|
||||
<FormLabel required={input.required}>{input.label}</FormLabel>
|
||||
<QuestionTip ml={1} label={input.description} />
|
||||
</Flex>
|
||||
{input.type === FlowNodeInputTypeEnum.input && (
|
||||
<Input
|
||||
bg={'white'}
|
||||
maxLength={input.maxLength}
|
||||
isDisabled={interactive.params.submitted}
|
||||
{...register(input.label, {
|
||||
required: input.required
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{input.type === FlowNodeInputTypeEnum.textarea && (
|
||||
<Textarea
|
||||
isDisabled={interactive.params.submitted}
|
||||
bg={'white'}
|
||||
{...register(input.label, {
|
||||
required: input.required
|
||||
})}
|
||||
rows={5}
|
||||
maxLength={input.maxLength || 4000}
|
||||
/>
|
||||
)}
|
||||
{input.type === FlowNodeInputTypeEnum.numberInput && (
|
||||
<NumberInput
|
||||
step={1}
|
||||
min={input.min}
|
||||
max={input.max}
|
||||
isDisabled={interactive.params.submitted}
|
||||
bg={'white'}
|
||||
>
|
||||
<NumberInputField
|
||||
bg={'white'}
|
||||
{...register(input.label, {
|
||||
required: input.required
|
||||
})}
|
||||
/>
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
)}
|
||||
{input.type === FlowNodeInputTypeEnum.select && (
|
||||
<Controller
|
||||
key={input.label}
|
||||
control={control}
|
||||
name={input.label}
|
||||
rules={{ required: input.required }}
|
||||
render={({ field: { ref, value } }) => {
|
||||
if (!input.list) return <></>;
|
||||
return (
|
||||
<MySelect
|
||||
ref={ref}
|
||||
width={'100%'}
|
||||
list={input.list}
|
||||
value={value}
|
||||
isDisabled={interactive.params.submitted}
|
||||
onchange={(e) => setValue(input.label, e)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
{!interactive.params.submitted && (
|
||||
<Flex w={'full'} justifyContent={'end'}>
|
||||
<Button onClick={handleSubmitChat(onSubmit)}>{t('common:Submit')}</Button>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
const AIResponseBox = ({ value, isLastResponseValue, isChatting }: props) => {
|
||||
if (value.type === ChatItemValueTypeEnum.text && value.text)
|
||||
@@ -179,7 +304,13 @@ const AIResponseBox = ({ value, isLastResponseValue, isChatting }: props) => {
|
||||
value.interactive &&
|
||||
value.interactive.type === 'userSelect'
|
||||
)
|
||||
return <RenderInteractive interactive={value.interactive} />;
|
||||
return <RenderUserSelectInteractive interactive={value.interactive} />;
|
||||
if (
|
||||
value.type === ChatItemValueTypeEnum.interactive &&
|
||||
value.interactive &&
|
||||
value.interactive?.type === 'userInput'
|
||||
)
|
||||
return <RenderUserFormInteractive interactive={value.interactive} />;
|
||||
};
|
||||
|
||||
export default React.memo(AIResponseBox);
|
||||
|
@@ -352,6 +352,12 @@ export const WholeResponseContent = ({
|
||||
label={t('common:core.chat.response.loop_output_element')}
|
||||
value={activeModule?.loopOutputValue}
|
||||
/>
|
||||
|
||||
{/* form input */}
|
||||
<Row
|
||||
label={t('common:core.chat.response.form_input_result')}
|
||||
value={activeModule?.formInputResult}
|
||||
/>
|
||||
</Box>
|
||||
) : null;
|
||||
};
|
||||
|
@@ -293,7 +293,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
})();
|
||||
|
||||
const isInteractiveRequest = !!getLastInteractiveValue(histories);
|
||||
const { text: userSelectedVal } = chatValue2RuntimePrompt(userQuestion.value);
|
||||
const { text: userInteractiveVal } = chatValue2RuntimePrompt(userQuestion.value);
|
||||
|
||||
const newTitle = isPlugin
|
||||
? variables.cTime ?? getSystemTime(user.timezone)
|
||||
@@ -312,7 +312,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
appId: app._id,
|
||||
teamId,
|
||||
tmbId: tmbId,
|
||||
userSelectedVal,
|
||||
userInteractiveVal,
|
||||
aiResponse,
|
||||
newVariables,
|
||||
newTitle
|
||||
|
@@ -55,7 +55,8 @@ const nodeTypes: Record<FlowNodeTypeEnum, any> = {
|
||||
[FlowNodeTypeEnum.userSelect]: dynamic(() => import('./nodes/NodeUserSelect')),
|
||||
[FlowNodeTypeEnum.loop]: dynamic(() => import('./nodes/Loop/NodeLoop')),
|
||||
[FlowNodeTypeEnum.loopStart]: dynamic(() => import('./nodes/Loop/NodeLoopStart')),
|
||||
[FlowNodeTypeEnum.loopEnd]: dynamic(() => import('./nodes/Loop/NodeLoopEnd'))
|
||||
[FlowNodeTypeEnum.loopEnd]: dynamic(() => import('./nodes/Loop/NodeLoopEnd')),
|
||||
[FlowNodeTypeEnum.formInput]: dynamic(() => import('./nodes/NodeFormInput'))
|
||||
};
|
||||
const edgeTypes = {
|
||||
[EDGE_TYPE]: ButtonEdge
|
||||
|
@@ -0,0 +1,201 @@
|
||||
import { Box, Flex, FormLabel, Stack } from '@chakra-ui/react';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { UserInputFormItemType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import InputTypeConfig from '../NodePluginIO/InputTypeConfig';
|
||||
|
||||
export const defaultFormInput: UserInputFormItemType = {
|
||||
type: FlowNodeInputTypeEnum.input,
|
||||
key: '',
|
||||
label: '',
|
||||
value: '',
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
required: false
|
||||
};
|
||||
|
||||
// Modal for add or edit user input form items
|
||||
const InputFormEditModal = ({
|
||||
defaultValue,
|
||||
onClose,
|
||||
onSubmit,
|
||||
keys
|
||||
}: {
|
||||
defaultValue: UserInputFormItemType;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: UserInputFormItemType) => void;
|
||||
keys: string[];
|
||||
}) => {
|
||||
const isEdit = !!defaultValue.key;
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
...defaultValue,
|
||||
list: defaultValue.list?.length ? defaultValue.list : [{ label: '', value: '' }]
|
||||
}
|
||||
});
|
||||
const { setValue, watch, reset } = form;
|
||||
|
||||
const inputType = watch('type') || FlowNodeInputTypeEnum.input;
|
||||
|
||||
const maxLength = watch('maxLength');
|
||||
const max = watch('max');
|
||||
const min = watch('min');
|
||||
|
||||
const inputTypeList = [
|
||||
{
|
||||
icon: 'core/workflow/inputType/input',
|
||||
label: t('common:core.workflow.inputType.input'),
|
||||
value: FlowNodeInputTypeEnum.input,
|
||||
defaultValueType: WorkflowIOValueTypeEnum.string
|
||||
},
|
||||
{
|
||||
icon: 'core/workflow/inputType/textarea',
|
||||
label: t('common:core.workflow.inputType.textarea'),
|
||||
value: FlowNodeInputTypeEnum.textarea,
|
||||
defaultValueType: WorkflowIOValueTypeEnum.string
|
||||
},
|
||||
{
|
||||
icon: 'core/workflow/inputType/numberInput',
|
||||
label: t('common:core.workflow.inputType.number input'),
|
||||
value: FlowNodeInputTypeEnum.numberInput,
|
||||
defaultValueType: WorkflowIOValueTypeEnum.number
|
||||
},
|
||||
{
|
||||
icon: 'core/workflow/inputType/option',
|
||||
label: t('common:core.workflow.inputType.select'),
|
||||
value: FlowNodeInputTypeEnum.select,
|
||||
defaultValueType: WorkflowIOValueTypeEnum.string
|
||||
}
|
||||
];
|
||||
|
||||
const onSubmitSuccess = useCallback(
|
||||
(data: UserInputFormItemType, action: 'confirm' | 'continue') => {
|
||||
const isChangeKey = defaultValue.key !== data.key;
|
||||
if (keys.includes(data.key)) {
|
||||
if (!isEdit || isChangeKey) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('workflow:field_name_already_exists')
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
data.key = data.label;
|
||||
|
||||
if (action === 'confirm') {
|
||||
onSubmit(data);
|
||||
onClose();
|
||||
} else if (action === 'continue') {
|
||||
onSubmit(data);
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('common:common.Add Success')
|
||||
});
|
||||
reset(defaultFormInput);
|
||||
}
|
||||
},
|
||||
[toast, t, reset, onSubmit, onClose, defaultFormInput]
|
||||
);
|
||||
|
||||
const onSubmitError = useCallback(
|
||||
(e: Object) => {
|
||||
for (const item of Object.values(e)) {
|
||||
if (item.message) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: item.message
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
iconSrc="file/fill/manual"
|
||||
title={isEdit ? t('workflow:edit_input') : t('workflow:add_new_input')}
|
||||
maxW={['90vw', '878px']}
|
||||
w={'100%'}
|
||||
isCentered
|
||||
>
|
||||
<Flex h={'494px'}>
|
||||
<Stack gap={4} p={8}>
|
||||
<FormLabel color={'myGray.600'} fontWeight={'medium'}>
|
||||
{t('common:core.module.Input Type')}
|
||||
</FormLabel>
|
||||
<Flex flexDirection={'column'} gap={4}>
|
||||
<Box display={'grid'} gridTemplateColumns={'repeat(2, 1fr)'} gap={4}>
|
||||
{inputTypeList.map((item) => {
|
||||
const isSelected = inputType === item.value;
|
||||
return (
|
||||
<Box
|
||||
display={'flex'}
|
||||
key={item.label}
|
||||
border={isSelected ? '1px solid #3370FF' : '1px solid #DFE2EA'}
|
||||
p={3}
|
||||
rounded={'6px'}
|
||||
fontWeight={'medium'}
|
||||
fontSize={'14px'}
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
boxShadow={isSelected ? '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)' : 'none'}
|
||||
_hover={{
|
||||
'& > svg': {
|
||||
color: 'primary.600'
|
||||
},
|
||||
'& > span': {
|
||||
color: 'myGray.900'
|
||||
},
|
||||
border: '1px solid #3370FF',
|
||||
boxShadow: '0px 0px 0px 2.4px rgba(51, 112, 255, 0.15)'
|
||||
}}
|
||||
onClick={() => {
|
||||
setValue('type', item.value);
|
||||
}}
|
||||
>
|
||||
<MyIcon
|
||||
name={item.icon as any}
|
||||
w={'20px'}
|
||||
mr={1.5}
|
||||
color={isSelected ? 'primary.600' : 'myGray.400'}
|
||||
/>
|
||||
<Box as="span" color={isSelected ? 'myGray.900' : 'inherit'} pr={4}>
|
||||
{item.label}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Stack>
|
||||
<InputTypeConfig
|
||||
form={form}
|
||||
type={'formInput'}
|
||||
isEdit={isEdit}
|
||||
inputType={inputType}
|
||||
maxLength={maxLength}
|
||||
max={max}
|
||||
min={min}
|
||||
onClose={onClose}
|
||||
onSubmitSuccess={onSubmitSuccess}
|
||||
onSubmitError={onSubmitError}
|
||||
/>
|
||||
</Flex>
|
||||
</MyModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(InputFormEditModal);
|
@@ -0,0 +1,224 @@
|
||||
import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import NodeCard from '../render/NodeCard';
|
||||
import Container from '../../components/Container';
|
||||
import RenderInput from '../render/RenderInput';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
FlowNodeInputItemType,
|
||||
FlowNodeOutputItemType
|
||||
} from '@fastgpt/global/core/workflow/type/io';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
FormLabel,
|
||||
HStack,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr
|
||||
} from '@chakra-ui/react';
|
||||
import { UserInputFormItemType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
FlowNodeInputMap,
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeOutputTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { SmallAddIcon } from '@chakra-ui/icons';
|
||||
import IOTitle from '../../components/IOTitle';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../context';
|
||||
import InputFormEditModal, { defaultFormInput } from './InputFormEditModal';
|
||||
import RenderOutput from '../render/RenderOutput';
|
||||
|
||||
const NodeFormInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
const { nodeId, inputs, outputs } = data;
|
||||
const { t } = useTranslation();
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
const [editField, setEditField] = useState<UserInputFormItemType>();
|
||||
|
||||
const CustomComponent = useMemo(
|
||||
() => ({
|
||||
[NodeInputKeyEnum.userInputForms]: ({ value, key, ...props }: FlowNodeInputItemType) => {
|
||||
const inputs = value as UserInputFormItemType[];
|
||||
|
||||
const onSubmit = (data: UserInputFormItemType) => {
|
||||
if (!editField?.key) {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key,
|
||||
value: {
|
||||
...props,
|
||||
key,
|
||||
value: inputs.concat(data)
|
||||
}
|
||||
});
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'addOutput',
|
||||
value: {
|
||||
id: data.key,
|
||||
valueType: data.valueType,
|
||||
key: data.key,
|
||||
label: data.label,
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const output = outputs.find((output) => output.key === editField.key);
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key,
|
||||
value: {
|
||||
...props,
|
||||
key,
|
||||
value: inputs.map((input) => (input.key === editField.key ? data : input))
|
||||
}
|
||||
});
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'replaceOutput',
|
||||
key: editField.key,
|
||||
value: {
|
||||
...(output as FlowNodeOutputItemType),
|
||||
valueType: data.valueType,
|
||||
key: data.key,
|
||||
label: data.label
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = (valueKey: string) => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'updateInput',
|
||||
key,
|
||||
value: {
|
||||
...props,
|
||||
key,
|
||||
value: inputs.filter((input) => input.key !== valueKey)
|
||||
}
|
||||
});
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'delOutput',
|
||||
key: valueKey
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<HStack className="nodrag" cursor={'default'} mb={3}>
|
||||
<FormLabel>{t('common:core.module.input_form')}</FormLabel>
|
||||
<Box flex={'1 0 0'} />
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
leftIcon={<SmallAddIcon />}
|
||||
iconSpacing={1}
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
setEditField(defaultFormInput);
|
||||
}}
|
||||
>
|
||||
{t('common:common.Add_new_input')}
|
||||
</Button>
|
||||
{!!editField && (
|
||||
<InputFormEditModal
|
||||
defaultValue={editField}
|
||||
keys={inputs.map((item) => item.key)}
|
||||
onClose={() => {
|
||||
setEditField(undefined);
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<TableContainer>
|
||||
<Table bg={'white'}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th borderBottomLeftRadius={'none !important'}>
|
||||
{t('common:core.module.input_name')}
|
||||
</Th>
|
||||
<Th>{t('common:core.module.input_description')}</Th>
|
||||
<Th>{t('common:common.Require Input')}</Th>
|
||||
<Th borderBottomRightRadius={'none !important'}>{t('user:operations')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{inputs.map((item, index) => {
|
||||
const icon = FlowNodeInputMap[item.type as FlowNodeInputTypeEnum]?.icon;
|
||||
return (
|
||||
<Tr key={index}>
|
||||
<Td>
|
||||
<Flex alignItems={'center'}>
|
||||
{!!icon && (
|
||||
<MyIcon name={icon as any} w={'14px'} mr={1} color={'primary.600'} />
|
||||
)}
|
||||
{item.label}
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td>{item.description}</Td>
|
||||
<Td>{item.required ? '✅' : ''}</Td>
|
||||
<Td>
|
||||
<MyIcon
|
||||
mr={3}
|
||||
name={'common/settingLight'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'primary.600' }}
|
||||
onClick={() => setEditField(item)}
|
||||
/>
|
||||
<MyIcon
|
||||
className="delete"
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
color={'myGray.600'}
|
||||
cursor={'pointer'}
|
||||
ml={2}
|
||||
_hover={{ color: 'red.500' }}
|
||||
onClick={() => {
|
||||
onDelete(item.key);
|
||||
}}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}),
|
||||
[nodeId, editField, t, setEditField, onChangeNode]
|
||||
);
|
||||
|
||||
return (
|
||||
<NodeCard minW={'400px'} selected={selected} {...data}>
|
||||
<Container>
|
||||
<IOTitle text={t('common:common.Input')} />
|
||||
<RenderInput nodeId={nodeId} flowInputList={inputs} CustomComponent={CustomComponent} />
|
||||
</Container>
|
||||
<Container>
|
||||
<IOTitle text={t('common:common.Output')} />
|
||||
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodeFormInput);
|
@@ -1,36 +1,18 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Switch,
|
||||
Input,
|
||||
Textarea,
|
||||
Stack,
|
||||
HStack,
|
||||
FormControl
|
||||
} from '@chakra-ui/react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { Box, Flex, Stack } from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
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 MultipleSelect from '@fastgpt/web/components/common/MySelect/MultipleSelect';
|
||||
import { useBoolean } from 'ahooks';
|
||||
|
||||
const MyNumberInput = dynamic(
|
||||
() => import('@fastgpt/web/components/common/Input/NumberInput/index')
|
||||
);
|
||||
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
|
||||
import InputTypeConfig from './InputTypeConfig';
|
||||
|
||||
export const defaultInput: FlowNodeInputItemType = {
|
||||
renderTypeList: [FlowNodeInputTypeEnum.reference], // Can only choose one here
|
||||
@@ -54,7 +36,7 @@ const FieldEditModal = ({
|
||||
keys: string[];
|
||||
hasDynamicInput: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (e: { data: FlowNodeInputItemType; isChangeKey: boolean }) => void;
|
||||
onSubmit: (data: FlowNodeInputItemType) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
@@ -150,20 +132,13 @@ const FieldEditModal = ({
|
||||
);
|
||||
|
||||
const isEdit = !!defaultValue.key;
|
||||
const { register, getValues, setValue, handleSubmit, watch, control, reset } = useForm({
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
...defaultValue,
|
||||
list: defaultValue.list?.length ? defaultValue.list : [{ label: '', value: '' }]
|
||||
}
|
||||
});
|
||||
const {
|
||||
fields: selectEnums,
|
||||
append: appendEnums,
|
||||
remove: removeEnums
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: 'list'
|
||||
});
|
||||
const { getValues, setValue, watch, reset } = form;
|
||||
|
||||
const inputType = watch('renderTypeList.0') || FlowNodeInputTypeEnum.reference;
|
||||
const valueType = watch('valueType');
|
||||
@@ -174,41 +149,8 @@ const FieldEditModal = ({
|
||||
const max = watch('max');
|
||||
const min = watch('min');
|
||||
const selectValueTypeList = watch('customInputConfig.selectValueTypeList');
|
||||
const defaultJsonValue = watch('defaultValue');
|
||||
|
||||
const showValueTypeSelect =
|
||||
inputType === FlowNodeInputTypeEnum.reference ||
|
||||
inputType === FlowNodeInputTypeEnum.customVariable;
|
||||
|
||||
// input type config
|
||||
const showRequired = useMemo(() => {
|
||||
const list = [FlowNodeInputTypeEnum.addInputParam, FlowNodeInputTypeEnum.customVariable];
|
||||
return !list.includes(inputType);
|
||||
}, [inputType]);
|
||||
const showDefaultValue = useMemo(() => {
|
||||
const list = [
|
||||
FlowNodeInputTypeEnum.input,
|
||||
FlowNodeInputTypeEnum.textarea,
|
||||
FlowNodeInputTypeEnum.JSONEditor,
|
||||
FlowNodeInputTypeEnum.numberInput,
|
||||
FlowNodeInputTypeEnum.switch
|
||||
];
|
||||
|
||||
return list.includes(inputType);
|
||||
}, [inputType]);
|
||||
const showMaxLenInput = useMemo(() => {
|
||||
const list = [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.textarea];
|
||||
|
||||
return list.includes(inputType);
|
||||
}, [inputType]);
|
||||
const showMinMaxInput = useMemo(() => {
|
||||
const list = [FlowNodeInputTypeEnum.numberInput];
|
||||
return list.includes(inputType);
|
||||
}, [inputType]);
|
||||
|
||||
const valueTypeSelectList = Object.values(FlowValueTypeMap).map((item) => ({
|
||||
label: t(item.label as any),
|
||||
value: item.value
|
||||
}));
|
||||
const defaultValueType =
|
||||
inputTypeList.flat().find((item) => item.value === inputType)?.defaultValueType ||
|
||||
WorkflowIOValueTypeEnum.string;
|
||||
@@ -260,16 +202,10 @@ const FieldEditModal = ({
|
||||
data.label = data.key;
|
||||
|
||||
if (action === 'confirm') {
|
||||
onSubmit({
|
||||
data,
|
||||
isChangeKey
|
||||
});
|
||||
onSubmit(data);
|
||||
onClose();
|
||||
} else if (action === 'continue') {
|
||||
onSubmit({
|
||||
data,
|
||||
isChangeKey
|
||||
});
|
||||
onSubmit(data);
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('common:common.Add Success')
|
||||
@@ -381,271 +317,24 @@ const FieldEditModal = ({
|
||||
</Box>
|
||||
</Stack>
|
||||
{/* input type config */}
|
||||
<Stack flex={1} borderLeft={'1px solid #F0F1F6'} justifyContent={'space-between'}>
|
||||
<Flex flexDirection={'column'} p={8} gap={4} flex={'1 0 0'} overflow={'auto'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{t('common:core.module.Field Name')}
|
||||
</FormLabel>
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
placeholder="appointment/sql"
|
||||
{...register('key', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'flex-start'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{t('workflow:field_description')}
|
||||
</FormLabel>
|
||||
<Textarea
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('workflow:field_description_placeholder')}
|
||||
rows={4}
|
||||
{...register('description', { required: isToolInput ? true : false })}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{/* value type */}
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{t('workflow:value_type')}
|
||||
</FormLabel>
|
||||
{showValueTypeSelect ? (
|
||||
<Box flex={1}>
|
||||
<MySelect<WorkflowIOValueTypeEnum>
|
||||
list={valueTypeSelectList.filter(
|
||||
(item) => item.value !== WorkflowIOValueTypeEnum.arrayAny
|
||||
)}
|
||||
value={valueType}
|
||||
onchange={(e) => {
|
||||
setValue('valueType', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box fontSize={'14px'}>{defaultValueType}</Box>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{showRequired && (
|
||||
<Flex alignItems={'center'} minH={'40px'}>
|
||||
<FormLabel flex={'1'} fontWeight={'medium'}>
|
||||
{t('workflow:field_required')}
|
||||
</FormLabel>
|
||||
<Switch {...register('required')} />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{/* reference */}
|
||||
{inputType === FlowNodeInputTypeEnum.reference && (
|
||||
<>
|
||||
<Flex alignItems={'center'} minH={'40px'}>
|
||||
<FormLabel flex={'1'} fontWeight={'medium'}>
|
||||
{t('workflow:field_used_as_tool_input')}
|
||||
</FormLabel>
|
||||
<Switch
|
||||
isChecked={isToolInput}
|
||||
onChange={(e) => {
|
||||
setIsToolInput();
|
||||
console.log(isToolInput);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showMaxLenInput && (
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{t('common:core.module.Max Length')}
|
||||
</FormLabel>
|
||||
<MyNumberInput
|
||||
flex={'1 0 0'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('common:core.module.Max Length placeholder')}
|
||||
value={maxLength}
|
||||
onChange={(e) => {
|
||||
// @ts-ignore
|
||||
setValue('maxLength', e || '');
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{showMinMaxInput && (
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{t('common:core.module.Max Value')}
|
||||
</FormLabel>
|
||||
<MyNumberInput
|
||||
flex={'1 0 0'}
|
||||
bg={'myGray.50'}
|
||||
value={watch('max')}
|
||||
onChange={(e) => {
|
||||
// @ts-ignore
|
||||
setValue('max', e || '');
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{t('common:core.module.Min Value')}
|
||||
</FormLabel>
|
||||
<MyNumberInput
|
||||
flex={'1 0 0'}
|
||||
bg={'myGray.50'}
|
||||
value={watch('min')}
|
||||
onChange={(e) => {
|
||||
// @ts-ignore
|
||||
setValue('min', e || '');
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showDefaultValue && (
|
||||
<Flex alignItems={'center'} minH={'40px'}>
|
||||
<FormLabel
|
||||
flex={inputType === FlowNodeInputTypeEnum.switch ? 1 : '0 0 100px'}
|
||||
fontWeight={'medium'}
|
||||
>
|
||||
{t('common:core.module.Default Value')}
|
||||
</FormLabel>
|
||||
{inputType === FlowNodeInputTypeEnum.numberInput && (
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
max={max}
|
||||
min={min}
|
||||
type={'number'}
|
||||
{...register('defaultValue')}
|
||||
/>
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.input && (
|
||||
<Input bg={'myGray.50'} maxLength={maxLength} {...register('defaultValue')} />
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.textarea && (
|
||||
<Textarea bg={'myGray.50'} maxLength={maxLength} {...register('defaultValue')} />
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.JSONEditor && (
|
||||
<JsonEditor
|
||||
bg={'myGray.50'}
|
||||
resize
|
||||
w={'full'}
|
||||
onChange={(e) => {
|
||||
setValue('defaultValue', e);
|
||||
}}
|
||||
defaultValue={String(getValues('defaultValue'))}
|
||||
/>
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.switch && (
|
||||
<Switch {...register('defaultValue')} />
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{inputType === FlowNodeInputTypeEnum.addInputParam && (
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{t('common:core.module.Input Type')}
|
||||
</FormLabel>
|
||||
<Box fontSize={'14px'}>{t('workflow:only_the_reference_type_is_supported')}</Box>
|
||||
</Flex>
|
||||
<Box>
|
||||
<HStack mb={1}>
|
||||
<FormLabel fontWeight={'medium'}>{t('workflow:optional_value_type')}</FormLabel>
|
||||
<QuestionTip label={t('workflow:optional_value_type_tip')} />
|
||||
</HStack>
|
||||
<MultipleSelect<WorkflowIOValueTypeEnum>
|
||||
list={valueTypeSelectList}
|
||||
bg={'myGray.50'}
|
||||
value={selectValueTypeList || []}
|
||||
onSelect={(e) => {
|
||||
setValue('customInputConfig.selectValueTypeList', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{inputType === FlowNodeInputTypeEnum.select && (
|
||||
<>
|
||||
<Flex flexDirection={'column'} gap={4}>
|
||||
{selectEnums.map((item, i) => (
|
||||
<Flex key={item.id} alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{`${t('common:core.module.variable.variable options')} ${i + 1}`}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
fontSize={'12px'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={`${t('common:core.module.variable.variable options')} ${i + 1}`}
|
||||
{...register(`list.${i}.label`, {
|
||||
required: true,
|
||||
onChange: (e) => {
|
||||
setValue(`list.${i}.value`, e.target.value);
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
{selectEnums.length > 1 && (
|
||||
<MyIcon
|
||||
ml={3}
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
p={2}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'red.100' }}
|
||||
onClick={() => removeEnums(i)}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
leftIcon={<MyIcon name={'common/addLight'} w={'16px'} />}
|
||||
onClick={() => appendEnums({ label: '', value: '' })}
|
||||
fontWeight={'medium'}
|
||||
fontSize={'12px'}
|
||||
w={'24'}
|
||||
py={2}
|
||||
>
|
||||
{t('common:core.module.variable add option')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex justify={'flex-end'} gap={3} pb={8} pr={8}>
|
||||
<Button variant={'whiteBase'} fontWeight={'medium'} onClick={onClose} w={20}>
|
||||
{t('common:common.Close')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={'primaryOutline'}
|
||||
fontWeight={'medium'}
|
||||
onClick={handleSubmit((data) => onSubmitSuccess(data, 'confirm'), onSubmitError)}
|
||||
w={20}
|
||||
>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
{!isEdit && (
|
||||
<Button
|
||||
fontWeight={'medium'}
|
||||
onClick={handleSubmit((data) => onSubmitSuccess(data, 'continue'), onSubmitError)}
|
||||
w={20}
|
||||
>
|
||||
{t('common:comon.Continue_Adding')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
<InputTypeConfig
|
||||
form={form}
|
||||
type={'plugin'}
|
||||
isEdit={isEdit}
|
||||
onClose={onClose}
|
||||
inputType={inputType}
|
||||
maxLength={maxLength}
|
||||
max={max}
|
||||
min={min}
|
||||
selectValueTypeList={selectValueTypeList}
|
||||
defaultJsonValue={defaultJsonValue}
|
||||
isToolInput={isToolInput}
|
||||
setIsToolInput={setIsToolInput}
|
||||
valueType={valueType}
|
||||
defaultValueType={defaultValueType}
|
||||
onSubmitSuccess={onSubmitSuccess}
|
||||
onSubmitError={onSubmitError}
|
||||
/>
|
||||
</Flex>
|
||||
</MyModal>
|
||||
);
|
||||
|
@@ -0,0 +1,398 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
HStack,
|
||||
Input,
|
||||
Stack,
|
||||
Switch,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowValueTypeMap
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
|
||||
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';
|
||||
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useFieldArray, UseFormReturn } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
const InputTypeConfig = ({
|
||||
form,
|
||||
isEdit,
|
||||
onClose,
|
||||
type,
|
||||
inputType,
|
||||
maxLength,
|
||||
max,
|
||||
min,
|
||||
selectValueTypeList,
|
||||
defaultJsonValue,
|
||||
isToolInput,
|
||||
setIsToolInput,
|
||||
valueType,
|
||||
defaultValueType,
|
||||
onSubmitSuccess,
|
||||
onSubmitError
|
||||
}: {
|
||||
// Common fields
|
||||
form: UseFormReturn<any>;
|
||||
isEdit: boolean;
|
||||
onClose: () => void;
|
||||
type: 'plugin' | 'formInput';
|
||||
inputType: FlowNodeInputTypeEnum;
|
||||
|
||||
maxLength?: number;
|
||||
max?: number;
|
||||
min?: number;
|
||||
|
||||
selectValueTypeList?: WorkflowIOValueTypeEnum[];
|
||||
defaultJsonValue?: string;
|
||||
|
||||
// Plugin-specific fields
|
||||
isToolInput?: boolean;
|
||||
setIsToolInput?: () => void;
|
||||
valueType?: WorkflowIOValueTypeEnum;
|
||||
defaultValueType?: WorkflowIOValueTypeEnum;
|
||||
|
||||
// Update methods
|
||||
onSubmitSuccess: (data: any, action: 'confirm' | 'continue') => void;
|
||||
onSubmitError: (e: Object) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { register, setValue, handleSubmit, control } = form;
|
||||
|
||||
const {
|
||||
fields: selectEnums,
|
||||
append: appendEnums,
|
||||
remove: removeEnums
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: 'list'
|
||||
});
|
||||
|
||||
const valueTypeSelectList = Object.values(FlowValueTypeMap).map((item) => ({
|
||||
label: t(item.label as any),
|
||||
value: item.value
|
||||
}));
|
||||
|
||||
const showValueTypeSelect =
|
||||
inputType === FlowNodeInputTypeEnum.reference ||
|
||||
inputType === FlowNodeInputTypeEnum.customVariable;
|
||||
|
||||
const showRequired = useMemo(() => {
|
||||
const list = [FlowNodeInputTypeEnum.addInputParam, FlowNodeInputTypeEnum.customVariable];
|
||||
return !list.includes(inputType);
|
||||
}, [inputType]);
|
||||
|
||||
const showMaxLenInput = useMemo(() => {
|
||||
const list = [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.textarea];
|
||||
return list.includes(inputType);
|
||||
}, [inputType]);
|
||||
|
||||
const showMinMaxInput = useMemo(() => {
|
||||
const list = [FlowNodeInputTypeEnum.numberInput];
|
||||
return list.includes(inputType);
|
||||
}, [inputType]);
|
||||
|
||||
const showDefaultValue = useMemo(() => {
|
||||
const list = [
|
||||
FlowNodeInputTypeEnum.input,
|
||||
FlowNodeInputTypeEnum.textarea,
|
||||
FlowNodeInputTypeEnum.JSONEditor,
|
||||
FlowNodeInputTypeEnum.numberInput,
|
||||
FlowNodeInputTypeEnum.switch
|
||||
];
|
||||
|
||||
return list.includes(inputType);
|
||||
}, [inputType]);
|
||||
|
||||
return (
|
||||
<Stack flex={1} borderLeft={'1px solid #F0F1F6'} justifyContent={'space-between'}>
|
||||
<Flex flexDirection={'column'} p={8} gap={4} flex={'1 0 0'} overflow={'auto'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{type === 'formInput'
|
||||
? t('common:core.module.input_name')
|
||||
: t('common:core.module.Field Name')}
|
||||
</FormLabel>
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
placeholder="appointment/sql"
|
||||
{...register(type === 'formInput' ? 'label' : 'key', {
|
||||
required: true
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'flex-start'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{type === 'formInput'
|
||||
? t('common:core.module.input_description')
|
||||
: t('workflow:field_description')}
|
||||
</FormLabel>
|
||||
<Textarea
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('workflow:field_description_placeholder')}
|
||||
rows={3}
|
||||
{...register('description', { required: isToolInput ? true : false })}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{/* value type */}
|
||||
{valueType && (
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{t('common:core.module.Data Type')}
|
||||
</FormLabel>
|
||||
{showValueTypeSelect ? (
|
||||
<Box flex={1}>
|
||||
<MySelect<WorkflowIOValueTypeEnum>
|
||||
list={valueTypeSelectList.filter(
|
||||
(item) => item.value !== WorkflowIOValueTypeEnum.arrayAny
|
||||
)}
|
||||
value={valueType}
|
||||
onchange={(e) => {
|
||||
setValue('valueType', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Box fontSize={'14px'}>{defaultValueType}</Box>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
{showRequired && (
|
||||
<Flex alignItems={'center'} minH={'40px'}>
|
||||
<FormLabel flex={'1'} fontWeight={'medium'}>
|
||||
{t('workflow:field_required')}
|
||||
</FormLabel>
|
||||
<Switch {...register('required')} />
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{/* reference */}
|
||||
{inputType === FlowNodeInputTypeEnum.reference && (
|
||||
<>
|
||||
<Flex alignItems={'center'} minH={'40px'}>
|
||||
<FormLabel flex={'1'} fontWeight={'medium'}>
|
||||
{t('workflow:field_used_as_tool_input')}
|
||||
</FormLabel>
|
||||
<Switch
|
||||
isChecked={isToolInput}
|
||||
onChange={(e) => {
|
||||
setIsToolInput && setIsToolInput();
|
||||
console.log(isToolInput);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showMaxLenInput && (
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{t('common:core.module.Max Length')}
|
||||
</FormLabel>
|
||||
<MyNumberInput
|
||||
flex={'1 0 0'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={t('common:core.module.Max Length placeholder')}
|
||||
value={maxLength}
|
||||
onChange={(e) => {
|
||||
// @ts-ignore
|
||||
setValue('maxLength', e || '');
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{showMinMaxInput && (
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{t('common:core.module.Max Value')}
|
||||
</FormLabel>
|
||||
<MyNumberInput
|
||||
flex={'1 0 0'}
|
||||
bg={'myGray.50'}
|
||||
value={max}
|
||||
onChange={(e) => {
|
||||
// @ts-ignore
|
||||
setValue('max', e || '');
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{t('common:core.module.Min Value')}
|
||||
</FormLabel>
|
||||
<MyNumberInput
|
||||
flex={'1 0 0'}
|
||||
bg={'myGray.50'}
|
||||
value={min}
|
||||
onChange={(e) => {
|
||||
// @ts-ignore
|
||||
setValue('min', e || '');
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showDefaultValue && (
|
||||
<Flex alignItems={'center'} minH={'40px'}>
|
||||
<FormLabel
|
||||
flex={inputType === FlowNodeInputTypeEnum.switch ? 1 : '0 0 100px'}
|
||||
fontWeight={'medium'}
|
||||
>
|
||||
{t('common:core.module.Default Value')}
|
||||
</FormLabel>
|
||||
{inputType === FlowNodeInputTypeEnum.numberInput && (
|
||||
<Input
|
||||
bg={'myGray.50'}
|
||||
max={max}
|
||||
min={min}
|
||||
type={'number'}
|
||||
{...register('defaultValue')}
|
||||
/>
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.input && (
|
||||
<Input bg={'myGray.50'} maxLength={maxLength} {...register('defaultValue')} />
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.textarea && (
|
||||
<Textarea bg={'myGray.50'} maxLength={maxLength} {...register('defaultValue')} />
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.JSONEditor && (
|
||||
<JsonEditor
|
||||
bg={'myGray.50'}
|
||||
resize
|
||||
w={'full'}
|
||||
onChange={(e) => {
|
||||
setValue('defaultValue', e);
|
||||
}}
|
||||
defaultValue={String(defaultJsonValue)}
|
||||
/>
|
||||
)}
|
||||
{inputType === FlowNodeInputTypeEnum.switch && <Switch {...register('defaultValue')} />}
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{inputType === FlowNodeInputTypeEnum.addInputParam && (
|
||||
<>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{t('common:core.module.Input Type')}
|
||||
</FormLabel>
|
||||
<Box fontSize={'14px'}>{t('workflow:only_the_reference_type_is_supported')}</Box>
|
||||
</Flex>
|
||||
<Box>
|
||||
<HStack mb={1}>
|
||||
<FormLabel fontWeight={'medium'}>{t('workflow:optional_value_type')}</FormLabel>
|
||||
<QuestionTip label={t('workflow:optional_value_type_tip')} />
|
||||
</HStack>
|
||||
<MultipleSelect<WorkflowIOValueTypeEnum>
|
||||
list={valueTypeSelectList}
|
||||
bg={'myGray.50'}
|
||||
value={selectValueTypeList || []}
|
||||
onSelect={(e) => {
|
||||
setValue('customInputConfig.selectValueTypeList', e);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{inputType === FlowNodeInputTypeEnum.select && (
|
||||
<>
|
||||
<Flex flexDirection={'column'} gap={4}>
|
||||
{selectEnums.map((item, i) => (
|
||||
<Flex key={item.id} alignItems={'center'}>
|
||||
<FormLabel flex={'0 0 100px'} fontWeight={'medium'}>
|
||||
{`${t('common:core.module.variable.variable options')} ${i + 1}`}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
fontSize={'12px'}
|
||||
bg={'myGray.50'}
|
||||
placeholder={`${t('common:core.module.variable.variable options')} ${i + 1}`}
|
||||
{...register(`list.${i}.label`, {
|
||||
required: true,
|
||||
onChange: (e: any) => {
|
||||
setValue(`list.${i}.value`, e.target.value);
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</FormControl>
|
||||
{selectEnums.length > 1 && (
|
||||
<MyIcon
|
||||
ml={3}
|
||||
name={'delete'}
|
||||
w={'16px'}
|
||||
cursor={'pointer'}
|
||||
p={2}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'red.100' }}
|
||||
onClick={() => removeEnums(i)}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
<Button
|
||||
variant={'whiteBase'}
|
||||
leftIcon={<MyIcon name={'common/addLight'} w={'16px'} />}
|
||||
onClick={() => appendEnums({ label: '', value: '' })}
|
||||
fontWeight={'medium'}
|
||||
fontSize={'12px'}
|
||||
w={'24'}
|
||||
py={2}
|
||||
>
|
||||
{t('common:core.module.variable add option')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex justify={'flex-end'} gap={3} pb={8} pr={8}>
|
||||
<Button variant={'whiteBase'} fontWeight={'medium'} onClick={onClose} w={20}>
|
||||
{t('common:common.Close')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={'primaryOutline'}
|
||||
fontWeight={'medium'}
|
||||
onClick={handleSubmit(
|
||||
(data: FlowNodeInputItemType) => onSubmitSuccess(data, 'confirm'),
|
||||
onSubmitError
|
||||
)}
|
||||
w={20}
|
||||
>
|
||||
{t('common:common.Confirm')}
|
||||
</Button>
|
||||
{!isEdit && (
|
||||
<Button
|
||||
fontWeight={'medium'}
|
||||
onClick={handleSubmit(
|
||||
(data: FlowNodeInputItemType) => onSubmitSuccess(data, 'continue'),
|
||||
onSubmitError
|
||||
)}
|
||||
w={20}
|
||||
>
|
||||
{t('common:common.Continue_Adding')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(InputTypeConfig);
|
@@ -40,7 +40,7 @@ const NodePluginInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
|
||||
const [editField, setEditField] = useState<FlowNodeInputItemType>();
|
||||
|
||||
const onSubmit = ({ data }: { data: FlowNodeInputItemType; isChangeKey: boolean }) => {
|
||||
const onSubmit = (data: FlowNodeInputItemType) => {
|
||||
if (!editField) return;
|
||||
|
||||
if (editField?.key) {
|
||||
|
@@ -15,7 +15,7 @@ import { SourceHandle } from './render/Handle';
|
||||
import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../context';
|
||||
import { UserSelectOptionItemType } from '@fastgpt/global/core/workflow/template/system/userSelect/type';
|
||||
import { UserSelectOptionItemType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import IOTitle from '../components/IOTitle';
|
||||
import RenderOutput from './render/RenderOutput';
|
||||
|
||||
|
Reference in New Issue
Block a user