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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user