feat: 重构 AIResponseBox 组件,简化用户交互逻辑并引入共用表单组件
This commit is contained in:
@@ -8,8 +8,7 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
HStack,
|
||||
Textarea
|
||||
HStack
|
||||
} from '@chakra-ui/react';
|
||||
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
@@ -17,7 +16,7 @@ import {
|
||||
ToolModuleResponseItemType,
|
||||
UserChatItemValueItemType
|
||||
} 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 Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import {
|
||||
@@ -26,16 +25,14 @@ import {
|
||||
UserSelectInteractive
|
||||
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
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 { 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 {
|
||||
SelectOptionsComponent,
|
||||
SelectOption,
|
||||
FormInputComponent,
|
||||
FormItem
|
||||
} from './Form/FormComponents';
|
||||
|
||||
type props = {
|
||||
value: UserChatItemValueItemType | AIChatItemValueItemType;
|
||||
@@ -43,7 +40,12 @@ type props = {
|
||||
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({
|
||||
showAnimation,
|
||||
@@ -53,11 +55,9 @@ const RenderText = React.memo(function RenderText({
|
||||
text?: string;
|
||||
}) {
|
||||
let source = text || '';
|
||||
// First empty line
|
||||
// if (!source && !isLastChild) return null;
|
||||
|
||||
return <Markdown source={source} showAnimation={showAnimation} />;
|
||||
});
|
||||
|
||||
const RenderTool = React.memo(
|
||||
function RenderTool({
|
||||
showAnimation,
|
||||
@@ -140,6 +140,7 @@ ${toolResponse}`}
|
||||
},
|
||||
(prevProps, nextProps) => isEqual(prevProps, nextProps)
|
||||
);
|
||||
|
||||
const RenderResoningContent = React.memo(function RenderResoningContent({
|
||||
content,
|
||||
isChatting,
|
||||
@@ -192,149 +193,66 @@ const RenderResoningContent = React.memo(function RenderResoningContent({
|
||||
</Accordion>
|
||||
);
|
||||
});
|
||||
|
||||
const RenderUserSelectInteractive = React.memo(function RenderInteractive({
|
||||
interactive
|
||||
}: {
|
||||
interactive: InteractiveBasicType & UserSelectInteractive;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{interactive?.params?.description && <Markdown source={interactive.params.description} />}
|
||||
<Flex flexDirection={'column'} gap={2} w={'250px'}>
|
||||
{interactive.params.userSelectOptions?.map((option) => {
|
||||
const selected = option.value === interactive?.params?.userSelectedVal;
|
||||
|
||||
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({
|
||||
text: option.value,
|
||||
isInteractivePrompt: true
|
||||
});
|
||||
}}
|
||||
>
|
||||
{option.value}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</>
|
||||
<SelectOptionsComponent
|
||||
options={(interactive.params.userSelectOptions || []) as SelectOption[]}
|
||||
description={interactive.params.description}
|
||||
selectedValue={interactive.params.userSelectedVal}
|
||||
onSelectOption={(value: string) => {
|
||||
onSendPrompt({
|
||||
text: value,
|
||||
isInteractivePrompt: true
|
||||
});
|
||||
}}
|
||||
isDisabled={interactive.params.userSelectedVal !== undefined}
|
||||
variant="whitePrimary"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
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) => {
|
||||
// 处理默认值
|
||||
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({
|
||||
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.description && <Markdown source={interactive.params.description} />}
|
||||
{interactive.params.inputForm?.map((input) => (
|
||||
<Box key={input.label}>
|
||||
<Flex mb={1} alignItems={'center'}>
|
||||
<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}
|
||||
{...register(input.label, {
|
||||
required: input.required
|
||||
})}
|
||||
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>
|
||||
)}
|
||||
<FormInputComponent
|
||||
inputForm={(interactive.params.inputForm || []) as FormItem[]}
|
||||
description={interactive.params.description}
|
||||
onSubmit={handleFormSubmit}
|
||||
isDisabled={interactive.params.submitted}
|
||||
defaultValues={defaultValues}
|
||||
submitButtonText="common:Submit"
|
||||
isCompact={true}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
@@ -360,6 +278,8 @@ const AIResponseBox = ({ value, isLastResponseValue, isChatting }: props) => {
|
||||
if (value.interactive?.type === 'userInput')
|
||||
return <RenderUserFormInteractive interactive={value.interactive} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default React.memo(AIResponseBox);
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
};
|
||||
@@ -1,24 +1,21 @@
|
||||
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 { 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 { WorkflowContext } from '@/pageComponents/app/detail/WorkflowComponents/context';
|
||||
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
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 {
|
||||
UserInputInteractive,
|
||||
UserSelectInteractive
|
||||
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||
import {
|
||||
FormInputComponent,
|
||||
FormItem,
|
||||
SelectOption,
|
||||
SelectOptionsComponent
|
||||
} from './Form/FormComponents';
|
||||
|
||||
// 创建共用的交互式调试 Hook
|
||||
const useInteractiveDebug = (
|
||||
@@ -120,58 +117,13 @@ export const RenderUserSelectInteractive = React.memo(function RenderInteractive
|
||||
|
||||
return (
|
||||
<Box px={4} py={3}>
|
||||
{interactive?.params?.description && (
|
||||
<Box
|
||||
mb={4}
|
||||
p={3}
|
||||
borderLeft="4px solid"
|
||||
borderColor="primary.100"
|
||||
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>
|
||||
<SelectOptionsComponent
|
||||
options={(interactive.params.userSelectOptions || []) as SelectOption[]}
|
||||
description={interactive.params.description}
|
||||
selectedValue={interactive.params.userSelectedVal}
|
||||
onSelectOption={handleSelectAndNext}
|
||||
isDisabled={interactive.params.userSelectedVal !== undefined}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
@@ -184,23 +136,23 @@ export const RenderUserFormInteractive = React.memo(function RenderFormInput({
|
||||
nodeId?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
handleSubmit: handleSubmitChat,
|
||||
control,
|
||||
reset,
|
||||
getValues
|
||||
} = useForm();
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
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;
|
||||
setIsSubmitted(true);
|
||||
|
||||
const formData = getValues();
|
||||
startDebug(JSON.stringify(formData), (node) => ({
|
||||
...node,
|
||||
inputs: node.inputs.map((input: { key: string }) => {
|
||||
@@ -215,177 +167,27 @@ export const RenderUserFormInteractive = React.memo(function RenderFormInput({
|
||||
formSubmitted: true
|
||||
}));
|
||||
},
|
||||
[nodeId, getValues, startDebug, interactive.params.inputForm]
|
||||
[nodeId, startDebug, interactive.params.inputForm]
|
||||
);
|
||||
|
||||
// 设置已提交状态
|
||||
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) {
|
||||
setIsSubmitted(true);
|
||||
}
|
||||
}, [interactive, reset]);
|
||||
}, [interactive.params.submitted]);
|
||||
|
||||
return (
|
||||
<Box px={4} py={4} bg="white" borderRadius="md">
|
||||
{interactive.params.description && (
|
||||
<Box
|
||||
mb={4}
|
||||
p={3}
|
||||
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}
|
||||
{...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={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>
|
||||
<FormInputComponent
|
||||
inputForm={(interactive.params.inputForm || []) as FormItem[]}
|
||||
description={interactive.params.description}
|
||||
onSubmit={handleFormSubmit}
|
||||
isDisabled={isSubmitted || interactive.params.submitted}
|
||||
defaultValues={defaultValues}
|
||||
submitButtonText="common:Submit"
|
||||
submitButtonIcon="core/workflow/debugNext"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user