feat: 重构 AIResponseBox 组件,简化用户交互逻辑并引入共用表单组件

This commit is contained in:
sd0ric4
2025-03-27 12:32:02 +08:00
parent ce4c85b1c4
commit c8a9c8157e
3 changed files with 452 additions and 374 deletions

View File

@@ -8,8 +8,7 @@ import {
Box, Box,
Button, Button,
Flex, Flex,
HStack, HStack
Textarea
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants'; import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
import { import {
@@ -17,7 +16,7 @@ import {
ToolModuleResponseItemType, ToolModuleResponseItemType,
UserChatItemValueItemType UserChatItemValueItemType
} from '@fastgpt/global/core/chat/type'; } from '@fastgpt/global/core/chat/type';
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect, useMemo } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar'; import Avatar from '@fastgpt/web/components/common/Avatar';
import { import {
@@ -26,16 +25,14 @@ import {
UserSelectInteractive UserSelectInteractive
} from '@fastgpt/global/core/workflow/template/system/interactive/type'; } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
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 'next-i18next'; import { useTranslation } from 'next-i18next';
import { Controller, useForm } from 'react-hook-form';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
import { SendPromptFnType } from '../ChatContainer/ChatBox/type';
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus'; import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
import {
SelectOptionsComponent,
SelectOption,
FormInputComponent,
FormItem
} from './Form/FormComponents';
type props = { type props = {
value: UserChatItemValueItemType | AIChatItemValueItemType; value: UserChatItemValueItemType | AIChatItemValueItemType;
@@ -43,7 +40,12 @@ type props = {
isChatting: boolean; isChatting: boolean;
}; };
const onSendPrompt: SendPromptFnType = (e) => eventBus.emit(EventNameEnum.sendQuestion, e); interface SendPromptParams {
text: string;
isInteractivePrompt: boolean;
}
const onSendPrompt = (e: SendPromptParams) => eventBus.emit(EventNameEnum.sendQuestion, e);
const RenderText = React.memo(function RenderText({ const RenderText = React.memo(function RenderText({
showAnimation, showAnimation,
@@ -53,11 +55,9 @@ const RenderText = React.memo(function RenderText({
text?: string; text?: string;
}) { }) {
let source = text || ''; let source = text || '';
// First empty line
// if (!source && !isLastChild) return null;
return <Markdown source={source} showAnimation={showAnimation} />; return <Markdown source={source} showAnimation={showAnimation} />;
}); });
const RenderTool = React.memo( const RenderTool = React.memo(
function RenderTool({ function RenderTool({
showAnimation, showAnimation,
@@ -140,6 +140,7 @@ ${toolResponse}`}
}, },
(prevProps, nextProps) => isEqual(prevProps, nextProps) (prevProps, nextProps) => isEqual(prevProps, nextProps)
); );
const RenderResoningContent = React.memo(function RenderResoningContent({ const RenderResoningContent = React.memo(function RenderResoningContent({
content, content,
isChatting, isChatting,
@@ -192,149 +193,66 @@ const RenderResoningContent = React.memo(function RenderResoningContent({
</Accordion> </Accordion>
); );
}); });
const RenderUserSelectInteractive = React.memo(function RenderInteractive({ const RenderUserSelectInteractive = React.memo(function RenderInteractive({
interactive interactive
}: { }: {
interactive: InteractiveBasicType & UserSelectInteractive; interactive: InteractiveBasicType & UserSelectInteractive;
}) { }) {
return ( return (
<> <SelectOptionsComponent
{interactive?.params?.description && <Markdown source={interactive.params.description} />} options={(interactive.params.userSelectOptions || []) as SelectOption[]}
<Flex flexDirection={'column'} gap={2} w={'250px'}> description={interactive.params.description}
{interactive.params.userSelectOptions?.map((option) => { selectedValue={interactive.params.userSelectedVal}
const selected = option.value === interactive?.params?.userSelectedVal; onSelectOption={(value: string) => {
return (
<Button
key={option.key}
variant={'whitePrimary'}
whiteSpace={'pre-wrap'}
isDisabled={interactive?.params?.userSelectedVal !== undefined}
{...(selected
? {
_disabled: {
cursor: 'default',
borderColor: 'primary.300',
bg: 'primary.50 !important',
color: 'primary.600'
}
}
: {})}
onClick={() => {
onSendPrompt({ onSendPrompt({
text: option.value, text: value,
isInteractivePrompt: true isInteractivePrompt: true
}); });
}} }}
> isDisabled={interactive.params.userSelectedVal !== undefined}
{option.value} variant="whitePrimary"
</Button> />
);
})}
</Flex>
</>
); );
}); });
const RenderUserFormInteractive = React.memo(function RenderFormInput({ const RenderUserFormInteractive = React.memo(function RenderFormInput({
interactive interactive
}: { }: {
interactive: InteractiveBasicType & UserInputInteractive; interactive: InteractiveBasicType & UserInputInteractive;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { register, setValue, handleSubmit: handleSubmitChat, control, reset } = useForm();
const onSubmit = useCallback((data: any) => { // 处理默认值
const defaultValues = useMemo(() => {
if (interactive.type === 'userInput') {
return interactive.params.inputForm?.reduce((acc: Record<string, any>, item) => {
acc[item.label] = !!item.value ? item.value : item.defaultValue;
return acc;
}, {});
}
return {};
}, [interactive]);
// 提交表单时的处理
const handleFormSubmit = useCallback((data: Record<string, any>) => {
onSendPrompt({ onSendPrompt({
text: JSON.stringify(data), text: JSON.stringify(data),
isInteractivePrompt: true 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 ( return (
<Flex flexDirection={'column'} gap={2} w={'250px'}> <Flex flexDirection={'column'} gap={2} w={'250px'}>
{interactive.params.description && <Markdown source={interactive.params.description} />} <FormInputComponent
{interactive.params.inputForm?.map((input) => ( inputForm={(interactive.params.inputForm || []) as FormItem[]}
<Box key={input.label}> description={interactive.params.description}
<Flex mb={1} alignItems={'center'}> onSubmit={handleFormSubmit}
<FormLabel required={input.required}>{input.label}</FormLabel>
{input.description && <QuestionTip ml={1} label={input.description} />}
</Flex>
{input.type === FlowNodeInputTypeEnum.input && (
<MyTextarea
isDisabled={interactive.params.submitted} isDisabled={interactive.params.submitted}
{...register(input.label, { defaultValues={defaultValues}
required: input.required submitButtonText="common:Submit"
})} isCompact={true}
bg={'white'}
autoHeight
minH={40}
maxH={100}
/> />
)}
{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 && (
<MyNumberInput
min={input.min}
max={input.max}
defaultValue={input.defaultValue}
isDisabled={interactive.params.submitted}
bg={'white'}
register={register}
name={input.label}
isRequired={input.required}
/>
)}
{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> </Flex>
); );
}); });
@@ -360,6 +278,8 @@ const AIResponseBox = ({ value, isLastResponseValue, isChatting }: props) => {
if (value.interactive?.type === 'userInput') if (value.interactive?.type === 'userInput')
return <RenderUserFormInteractive interactive={value.interactive} />; return <RenderUserFormInteractive interactive={value.interactive} />;
} }
return null;
}; };
export default React.memo(AIResponseBox); export default React.memo(AIResponseBox);

View File

@@ -0,0 +1,356 @@
import React, { useCallback } from 'react';
import { Box, Button, Flex, Textarea } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { Controller, useForm, UseFormProps, UseFormReturn, FieldValues } from 'react-hook-form';
import Markdown from '@/components/Markdown';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import MyIcon from '@fastgpt/web/components/common/Icon';
// 定义IconName类型 - 这应该与MyIcon组件要求的类型匹配
type IconName = 'core/workflow/debugNext' | 'common/loading' | 'core/chat/think';
// 定义选项接口
export interface SelectOption {
key: string;
value: string;
}
// 定义SelectOptionsComponent接口
export interface SelectOptionsComponentProps {
options: SelectOption[];
description?: string;
selectedValue?: string;
onSelectOption: (value: string) => void;
isDisabled?: boolean;
variant?: string;
}
/**
* 共享的选项按钮组件
*/
export const SelectOptionsComponent = React.memo(function SelectOptionsComponent({
options = [],
description,
selectedValue,
onSelectOption,
isDisabled = false,
variant = 'outline'
}: SelectOptionsComponentProps) {
return (
<Box>
{description && (
<Box
mb={4}
p={4}
border="1px solid"
borderColor="blue.200"
bg="blue.50"
borderRadius="md"
boxShadow="sm"
textAlign="center"
>
<Markdown source={description} />
</Box>
)}
<Flex flexDirection={'column'} gap={3} maxW={'400px'} mx="auto">
{options.map((option: SelectOption) => {
const selected = option.value === selectedValue;
return (
<Button
key={option.key}
variant={variant}
height="auto"
py={3}
px={4}
fontWeight="medium"
borderWidth="1.5px"
whiteSpace={'pre-wrap'}
_hover={{
bg: 'primary.50',
borderColor: 'primary.300'
}}
isDisabled={isDisabled}
{...(selected
? {
borderColor: 'primary.500',
bg: 'primary.50',
color: 'primary.700',
_disabled: {
cursor: 'default',
borderColor: 'primary.500',
bg: 'primary.50 !important',
color: 'primary.700',
opacity: 1
}
}
: {})}
onClick={() => onSelectOption(option.value)}
>
{option.value}
</Button>
);
})}
</Flex>
</Box>
);
});
// 定义表单项接口
export interface FormItem {
label: string;
key?: string;
type: FlowNodeInputTypeEnum;
required?: boolean;
description?: string;
defaultValue?: any;
value?: any;
maxLength?: number;
min?: number;
max?: number;
list?: Array<{
label: string;
value: string;
}>;
}
// 定义FormInputComponent接口
export interface FormInputComponentProps {
inputForm: FormItem[];
description?: string;
onSubmit?: (data: Record<string, any>) => void;
isDisabled?: boolean;
defaultValues?: Record<string, any>;
submitButtonText?: 'common:Submit' | string; // 使用联合类型指定特定的i18n键名
showSubmitButton?: boolean;
submitButtonIcon?: IconName;
isCompact?: boolean;
}
/**
* 共享的表单呈现组件
*/
export const FormInputComponent = React.memo(function FormInputComponent({
inputForm = [],
description,
onSubmit,
isDisabled = false,
defaultValues = {},
submitButtonText = 'common:Submit',
showSubmitButton = true,
submitButtonIcon,
isCompact = false
}: FormInputComponentProps) {
const { t } = useTranslation();
const { register, setValue, handleSubmit, control, reset, getValues } = useForm({
defaultValues
});
const handleFormSubmit = useCallback(
(data: Record<string, any>) => {
if (onSubmit) {
onSubmit(data);
}
},
[onSubmit]
);
return (
<Box>
{description && (
<Box
mb={4}
p={4}
border="1px solid"
borderColor="blue.200"
bg="blue.50"
borderRadius="md"
boxShadow="sm"
textAlign="center"
>
<Markdown source={description} />
</Box>
)}
<Box
as="form"
onSubmit={handleSubmit(handleFormSubmit)}
maxW={isCompact ? 'auto' : '560px'}
mx="auto"
p={isCompact ? 0 : 4}
borderRadius="md"
>
<Flex flexDirection={'column'} gap={5} w={'100%'}>
{inputForm.map((input: FormItem) => (
<Box key={input.label} mb={2}>
<Flex mb={2} alignItems={'center'}>
<FormLabel required={input.required} mb={0} fontWeight="medium" color="gray.700">
{input.label}
</FormLabel>
{input.description && <QuestionTip ml={1} label={input.description} />}
</Flex>
{input.type === FlowNodeInputTypeEnum.input && (
<MyTextarea
isDisabled={isDisabled}
{...register(input.label, {
required: input.required
})}
bg={'white'}
borderWidth="1px"
borderColor="gray.300"
_hover={{ borderColor: 'gray.400' }}
_focus={{
borderColor: 'primary.500',
boxShadow: '0 0 0 1px var(--chakra-colors-primary-500)'
}}
autoHeight
minH={40}
maxH={100}
borderRadius="md"
p={3}
/>
)}
{input.type === FlowNodeInputTypeEnum.textarea && (
<Textarea
isDisabled={isDisabled}
bg={'white'}
borderWidth="1px"
borderColor="gray.300"
_hover={{ borderColor: 'gray.400' }}
_focus={{
borderColor: 'primary.500',
boxShadow: '0 0 0 1px var(--chakra-colors-primary-500)'
}}
{...register(input.label, {
required: input.required
})}
rows={5}
maxLength={input.maxLength || 4000}
borderRadius="md"
p={3}
/>
)}
{input.type === FlowNodeInputTypeEnum.numberInput && (
<Box position="relative">
<MyNumberInput
min={input.min}
max={input.max}
defaultValue={input.defaultValue}
isDisabled={isDisabled}
bg={'white'}
borderWidth="1px"
borderRadius="md"
borderColor="gray.300"
_hover={{ borderColor: 'gray.400' }}
_focus={{ borderColor: 'primary.500' }}
register={register}
name={input.label}
isRequired={input.required}
sx={{
'& input': {
width: '100%',
height: '40px',
px: 3,
borderRadius: 'md',
border: 'none',
_focus: { outline: 'none' }
},
'& button': {
border: 'none',
bg: 'transparent',
color: 'gray.500'
}
}}
/>
</Box>
)}
{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%'}
variant="outline"
borderColor="gray.300"
borderRadius="md"
height="40px"
bg="white"
_hover={{ borderColor: 'gray.400' }}
list={input.list}
value={value}
isDisabled={isDisabled}
onChange={(e) => setValue(input.label, e)}
/>
);
}}
/>
)}
</Box>
))}
{showSubmitButton && (
<Flex w={'full'} justifyContent={'flex-end'} mt={3} gap={2}>
<Button
type="submit"
size="sm"
leftIcon={
submitButtonIcon ? <MyIcon name={submitButtonIcon} w={'16px'} /> : undefined
}
colorScheme="blue"
variant="solid"
>
{t(submitButtonText as any)}
</Button>
</Flex>
)}
</Flex>
</Box>
</Box>
);
});
// 定义FormHandler接口
export interface UseFormHandlerReturn<T extends FieldValues = Record<string, any>> {
register: UseFormReturn<T>['register'];
setValue: UseFormReturn<T>['setValue'];
handleSubmit: UseFormReturn<T>['handleSubmit'];
onSubmit: (e?: React.BaseSyntheticEvent) => Promise<void>;
control: UseFormReturn<T>['control'];
reset: UseFormReturn<T>['reset'];
getValues: UseFormReturn<T>['getValues'];
}
/**
* 创建共享的表单Hook
*/
export const useFormHandler = <T extends FieldValues = Record<string, any>>(
formConfig: UseFormProps<T> = {},
onSubmitCallback?: (data: T) => void
): UseFormHandlerReturn<T> => {
const methods = useForm<T>(formConfig);
const { handleSubmit } = methods;
const onSubmit = useCallback(
(data: T) => {
if (onSubmitCallback) {
onSubmitCallback(data);
}
},
[onSubmitCallback]
);
return {
...methods,
onSubmit: handleSubmit(onSubmit)
};
};

View File

@@ -1,24 +1,21 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Box, Button, Flex, Textarea, FormLabel as ChakraFormLabel } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { Controller, useForm } from 'react-hook-form';
import Markdown from '@/components/Markdown';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import {
UserInputInteractive,
UserSelectInteractive
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '@/pageComponents/app/detail/WorkflowComponents/context'; import { WorkflowContext } from '@/pageComponents/app/detail/WorkflowComponents/context';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants'; import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { ChatItemType, UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { initWorkflowEdgeStatus } from '@fastgpt/global/core/workflow/runtime/utils'; import { initWorkflowEdgeStatus } from '@fastgpt/global/core/workflow/runtime/utils';
import {
UserInputInteractive,
UserSelectInteractive
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
import {
FormInputComponent,
FormItem,
SelectOption,
SelectOptionsComponent
} from './Form/FormComponents';
// 创建共用的交互式调试 Hook // 创建共用的交互式调试 Hook
const useInteractiveDebug = ( const useInteractiveDebug = (
@@ -120,58 +117,13 @@ export const RenderUserSelectInteractive = React.memo(function RenderInteractive
return ( return (
<Box px={4} py={3}> <Box px={4} py={3}>
{interactive?.params?.description && ( <SelectOptionsComponent
<Box options={(interactive.params.userSelectOptions || []) as SelectOption[]}
mb={4} description={interactive.params.description}
p={3} selectedValue={interactive.params.userSelectedVal}
borderLeft="4px solid" onSelectOption={handleSelectAndNext}
borderColor="primary.100" isDisabled={interactive.params.userSelectedVal !== undefined}
bg="primary.50" />
borderRadius="md"
>
<Markdown source={interactive.params.description} />
</Box>
)}
<Flex flexDirection={'column'} gap={3} maxW={'400px'} mx="auto">
{interactive.params.userSelectOptions?.map((option) => {
const selected = option.value === interactive?.params?.userSelectedVal;
return (
<Button
key={option.key}
variant={'outline'}
height="auto"
py={3}
px={4}
fontWeight="medium"
borderWidth="1.5px"
whiteSpace={'pre-wrap'}
_hover={{
bg: 'primary.50',
borderColor: 'primary.300'
}}
isDisabled={interactive?.params?.userSelectedVal !== undefined}
{...(selected
? {
borderColor: 'primary.500',
bg: 'primary.50',
color: 'primary.700',
_disabled: {
cursor: 'default',
borderColor: 'primary.500',
bg: 'primary.50 !important',
color: 'primary.700',
opacity: 1
}
}
: {})}
onClick={() => handleSelectAndNext(option.value)}
>
{option.value}
</Button>
);
})}
</Flex>
</Box> </Box>
); );
}); });
@@ -184,23 +136,23 @@ export const RenderUserFormInteractive = React.memo(function RenderFormInput({
nodeId?: string; nodeId?: string;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const {
register,
setValue,
handleSubmit: handleSubmitChat,
control,
reset,
getValues
} = useForm();
const [isSubmitted, setIsSubmitted] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false);
const { startDebug } = useInteractiveDebug(interactive, nodeId); const { startDebug } = useInteractiveDebug(interactive, nodeId);
const onSubmit = useCallback( // 处理默认值
(data: any) => { const defaultValues = useMemo(() => {
return interactive.params.inputForm?.reduce((acc: Record<string, any>, item) => {
acc[item.label] = !!item.value ? item.value : item.defaultValue;
return acc;
}, {});
}, [interactive.params.inputForm]);
// 提交表单时的处理
const handleFormSubmit = useCallback(
(formData: Record<string, any>) => {
if (!nodeId) return; if (!nodeId) return;
setIsSubmitted(true); setIsSubmitted(true);
const formData = getValues();
startDebug(JSON.stringify(formData), (node) => ({ startDebug(JSON.stringify(formData), (node) => ({
...node, ...node,
inputs: node.inputs.map((input: { key: string }) => { inputs: node.inputs.map((input: { key: string }) => {
@@ -215,177 +167,27 @@ export const RenderUserFormInteractive = React.memo(function RenderFormInput({
formSubmitted: true formSubmitted: true
})); }));
}, },
[nodeId, getValues, startDebug, interactive.params.inputForm] [nodeId, startDebug, interactive.params.inputForm]
); );
// 设置已提交状态
useEffect(() => { 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);
}
// 如果已经有表单结果,标记为已提交
if (interactive.params.submitted) { if (interactive.params.submitted) {
setIsSubmitted(true); setIsSubmitted(true);
} }
}, [interactive, reset]); }, [interactive.params.submitted]);
return ( return (
<Box px={4} py={4} bg="white" borderRadius="md"> <Box px={4} py={4} bg="white" borderRadius="md">
{interactive.params.description && ( <FormInputComponent
<Box inputForm={(interactive.params.inputForm || []) as FormItem[]}
mb={4} description={interactive.params.description}
p={3} onSubmit={handleFormSubmit}
borderLeft="4px solid"
borderColor="blue.100"
bg="blue.50"
borderRadius="md"
>
<Markdown source={interactive.params.description} />
</Box>
)}
<Box
as="form"
onSubmit={handleSubmitChat(onSubmit)}
maxW="560px"
mx="auto"
bg="white"
p={4}
borderRadius="md"
>
<Flex flexDirection={'column'} gap={5} w={'100%'}>
{interactive.params.inputForm?.map((input) => (
<Box key={input.label} mb={2}>
<Flex mb={2} alignItems={'center'}>
<FormLabel required={input.required} mb={0} fontWeight="medium" color="gray.700">
{input.label}
</FormLabel>
{input.description && <QuestionTip ml={1} label={input.description} />}
</Flex>
{input.type === FlowNodeInputTypeEnum.input && (
<MyTextarea
isDisabled={isSubmitted || interactive.params.submitted} isDisabled={isSubmitted || interactive.params.submitted}
{...register(input.label, { defaultValues={defaultValues}
required: input.required submitButtonText="common:Submit"
})} submitButtonIcon="core/workflow/debugNext"
bg={'white'}
borderWidth="1px"
borderColor="gray.300"
_hover={{ borderColor: 'gray.400' }}
_focus={{
borderColor: 'primary.500',
boxShadow: '0 0 0 1px var(--chakra-colors-primary-500)'
}}
autoHeight
minH={40}
maxH={100}
borderRadius="md"
p={3}
/> />
)}
{input.type === FlowNodeInputTypeEnum.textarea && (
<Textarea
isDisabled={isSubmitted || interactive.params.submitted}
bg={'white'}
borderWidth="1px"
borderColor="gray.300"
_hover={{ borderColor: 'gray.400' }}
_focus={{
borderColor: 'primary.500',
boxShadow: '0 0 0 1px var(--chakra-colors-primary-500)'
}}
{...register(input.label, {
required: input.required
})}
rows={5}
maxLength={input.maxLength || 4000}
borderRadius="md"
p={3}
/>
)}
{input.type === FlowNodeInputTypeEnum.numberInput && (
<Box position="relative">
<MyNumberInput
min={input.min}
max={input.max}
defaultValue={input.defaultValue}
isDisabled={isSubmitted || interactive.params.submitted}
bg={'white'}
borderWidth="1px"
borderRadius="md"
borderColor="gray.300"
_hover={{ borderColor: 'gray.400' }}
_focus={{ borderColor: 'primary.500' }}
register={register}
name={input.label}
isRequired={input.required}
sx={{
'& input': {
width: '100%',
height: '40px',
px: 3,
borderRadius: 'md',
border: 'none',
_focus: { outline: 'none' }
},
'& button': {
border: 'none',
bg: 'transparent',
color: 'gray.500'
}
}}
/>
</Box>
)}
{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%'}
variant="outline"
borderColor="gray.300"
borderRadius="md"
height="40px"
bg="white"
_hover={{ borderColor: 'gray.400' }}
list={input.list}
value={value}
isDisabled={isSubmitted || interactive.params.submitted}
onChange={(e) => setValue(input.label, e)}
/>
);
}}
/>
)}
</Box>
))}
<Flex w={'full'} justifyContent={'flex-end'} mt={3} gap={2}>
<Button
type="submit"
size="sm"
leftIcon={<MyIcon name={'core/workflow/debugNext'} w={'16px'} />}
colorScheme="blue"
variant="solid"
>
{t('common:Submit')}
</Button>
</Flex>
</Flex>
</Box>
</Box> </Box>
); );
}); });