feat: add form input node (#2773)

* add node

* dispatch

* extract InputTypeConfig component

* question tip

* fix build

* fix

* fix
This commit is contained in:
heheer
2024-09-26 13:48:03 +08:00
committed by GitHub
parent edebfdf5ef
commit 1cf76ee7df
34 changed files with 1326 additions and 419 deletions

View File

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

View File

@@ -6,7 +6,6 @@ import {
NumberInput,
NumberInputField,
NumberInputStepper,
Select,
Switch,
Textarea
} from '@chakra-ui/react';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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