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