diff --git a/projects/app/src/components/core/chat/components/AIResponseBox.tsx b/projects/app/src/components/core/chat/components/AIResponseBox.tsx
index 81248a6d6..0f3b51a7e 100644
--- a/projects/app/src/components/core/chat/components/AIResponseBox.tsx
+++ b/projects/app/src/components/core/chat/components/AIResponseBox.tsx
@@ -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 ;
});
+
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({
);
});
+
const RenderUserSelectInteractive = React.memo(function RenderInteractive({
interactive
}: {
interactive: InteractiveBasicType & UserSelectInteractive;
}) {
return (
- <>
- {interactive?.params?.description && }
-
- {interactive.params.userSelectOptions?.map((option) => {
- const selected = option.value === interactive?.params?.userSelectedVal;
-
- return (
-
- );
- })}
-
- >
+ {
+ 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, item) => {
+ acc[item.label] = !!item.value ? item.value : item.defaultValue;
+ return acc;
+ }, {});
+ }
+ return {};
+ }, [interactive]);
+
+ // 提交表单时的处理
+ const handleFormSubmit = useCallback((data: Record) => {
onSendPrompt({
text: JSON.stringify(data),
isInteractivePrompt: true
});
}, []);
- useEffect(() => {
- if (interactive.type === 'userInput') {
- const defaultValues = interactive.params.inputForm?.reduce(
- (acc: Record, item) => {
- acc[item.label] = !!item.value ? item.value : item.defaultValue;
- return acc;
- },
- {}
- );
- reset(defaultValues);
- }
- }, []);
-
return (
- {interactive.params.description && }
- {interactive.params.inputForm?.map((input) => (
-
-
- {input.label}
- {input.description && }
-
- {input.type === FlowNodeInputTypeEnum.input && (
-
- )}
- {input.type === FlowNodeInputTypeEnum.textarea && (
-
- )}
- {input.type === FlowNodeInputTypeEnum.numberInput && (
-
- )}
- {input.type === FlowNodeInputTypeEnum.select && (
- {
- if (!input.list) return <>>;
- return (
- setValue(input.label, e)}
- />
- );
- }}
- />
- )}
-
- ))}
- {!interactive.params.submitted && (
-
-
-
- )}
+
);
});
@@ -360,6 +278,8 @@ const AIResponseBox = ({ value, isLastResponseValue, isChatting }: props) => {
if (value.interactive?.type === 'userInput')
return ;
}
+
+ return null;
};
export default React.memo(AIResponseBox);
diff --git a/projects/app/src/components/core/chat/components/Form/FormComponents.tsx b/projects/app/src/components/core/chat/components/Form/FormComponents.tsx
new file mode 100644
index 000000000..a897ac1e5
--- /dev/null
+++ b/projects/app/src/components/core/chat/components/Form/FormComponents.tsx
@@ -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 (
+
+ {description && (
+
+
+
+ )}
+
+ {options.map((option: SelectOption) => {
+ const selected = option.value === selectedValue;
+
+ return (
+
+ );
+ })}
+
+
+ );
+});
+
+// 定义表单项接口
+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) => void;
+ isDisabled?: boolean;
+ defaultValues?: Record;
+ 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) => {
+ if (onSubmit) {
+ onSubmit(data);
+ }
+ },
+ [onSubmit]
+ );
+
+ return (
+
+ {description && (
+
+
+
+ )}
+
+
+ {inputForm.map((input: FormItem) => (
+
+
+
+ {input.label}
+
+ {input.description && }
+
+ {input.type === FlowNodeInputTypeEnum.input && (
+
+ )}
+ {input.type === FlowNodeInputTypeEnum.textarea && (
+
+ )}
+ {input.type === FlowNodeInputTypeEnum.numberInput && (
+
+
+
+ )}
+ {input.type === FlowNodeInputTypeEnum.select && (
+ {
+ if (!input.list) return <>>;
+ return (
+ setValue(input.label, e)}
+ />
+ );
+ }}
+ />
+ )}
+
+ ))}
+
+ {showSubmitButton && (
+
+ : undefined
+ }
+ colorScheme="blue"
+ variant="solid"
+ >
+ {t(submitButtonText as any)}
+
+
+ )}
+
+
+
+ );
+});
+
+// 定义FormHandler接口
+export interface UseFormHandlerReturn> {
+ register: UseFormReturn['register'];
+ setValue: UseFormReturn['setValue'];
+ handleSubmit: UseFormReturn['handleSubmit'];
+ onSubmit: (e?: React.BaseSyntheticEvent) => Promise;
+ control: UseFormReturn['control'];
+ reset: UseFormReturn['reset'];
+ getValues: UseFormReturn['getValues'];
+}
+
+/**
+ * 创建共享的表单Hook
+ */
+export const useFormHandler = >(
+ formConfig: UseFormProps = {},
+ onSubmitCallback?: (data: T) => void
+): UseFormHandlerReturn => {
+ const methods = useForm(formConfig);
+ const { handleSubmit } = methods;
+
+ const onSubmit = useCallback(
+ (data: T) => {
+ if (onSubmitCallback) {
+ onSubmitCallback(data);
+ }
+ },
+ [onSubmitCallback]
+ );
+
+ return {
+ ...methods,
+ onSubmit: handleSubmit(onSubmit)
+ };
+};
diff --git a/projects/app/src/components/core/chat/components/InteractiveComponents.tsx b/projects/app/src/components/core/chat/components/InteractiveComponents.tsx
index ea0f538b8..529883351 100644
--- a/projects/app/src/components/core/chat/components/InteractiveComponents.tsx
+++ b/projects/app/src/components/core/chat/components/InteractiveComponents.tsx
@@ -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 (
- {interactive?.params?.description && (
-
-
-
- )}
-
- {interactive.params.userSelectOptions?.map((option) => {
- const selected = option.value === interactive?.params?.userSelectedVal;
-
- return (
-
- );
- })}
-
+
);
});
@@ -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, item) => {
+ acc[item.label] = !!item.value ? item.value : item.defaultValue;
+ return acc;
+ }, {});
+ }, [interactive.params.inputForm]);
+
+ // 提交表单时的处理
+ const handleFormSubmit = useCallback(
+ (formData: Record) => {
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, 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 (
- {interactive.params.description && (
-
-
-
- )}
-
-
- {interactive.params.inputForm?.map((input) => (
-
-
-
- {input.label}
-
- {input.description && }
-
- {input.type === FlowNodeInputTypeEnum.input && (
-
- )}
- {input.type === FlowNodeInputTypeEnum.textarea && (
-
- )}
- {input.type === FlowNodeInputTypeEnum.numberInput && (
-
-
-
- )}
- {input.type === FlowNodeInputTypeEnum.select && (
- {
- if (!input.list) return <>>;
- return (
- setValue(input.label, e)}
- />
- );
- }}
- />
- )}
-
- ))}
-
-
- }
- colorScheme="blue"
- variant="solid"
- >
- {t('common:Submit')}
-
-
-
-
+
);
});