diff --git a/web/src/components/llm-setting-items/use-watch-change.ts b/web/src/components/llm-setting-items/use-watch-change.ts index 67a37c073..a7ae89d90 100644 --- a/web/src/components/llm-setting-items/use-watch-change.ts +++ b/web/src/components/llm-setting-items/use-watch-change.ts @@ -23,17 +23,13 @@ export function useHandleFreedomChange() { updateNodeForm(node?.id, nextValues); } - console.info('xx:', node); + for (const key in values) { + if (Object.prototype.hasOwnProperty.call(values, key)) { + const element = values[key]; - // form.reset({ ...currentValues, ...values }); - - // for (const key in values) { - // if (Object.prototype.hasOwnProperty.call(values, key)) { - // const element = values[key]; - - // form.setValue(key, element); - // } - // } + form.setValue(key, element); + } + } }, [form, node, updateNodeForm], ); diff --git a/web/src/pages/agent/form-sheet/next.tsx b/web/src/pages/agent/form-sheet/next.tsx index 0ebb55ab5..ac1082615 100644 --- a/web/src/pages/agent/form-sheet/next.tsx +++ b/web/src/pages/agent/form-sheet/next.tsx @@ -10,10 +10,9 @@ import { IModalProps } from '@/interfaces/common'; import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { cn } from '@/lib/utils'; import { zodResolver } from '@hookform/resolvers/zod'; -import { get, isPlainObject, lowerFirst } from 'lodash'; -import omit from 'lodash/omit'; +import { lowerFirst } from 'lodash'; import { Play, X } from 'lucide-react'; -import { useEffect, useRef } from 'react'; +import { useRef } from 'react'; import { useForm } from 'react-hook-form'; import { BeginId, Operator, operatorMap } from '../constant'; import { FlowFormContext } from '../context'; @@ -21,13 +20,10 @@ import { RunTooltip } from '../flow-tooltip'; import { useHandleNodeNameChange } from '../hooks'; import { useHandleFormValuesChange } from '../hooks/use-watch-form-change'; import OperatorIcon from '../operator-icon'; -import { - buildCategorizeListFromObject, - convertToObjectArray, - needsSingleStepDebugging, -} from '../utils'; +import { needsSingleStepDebugging } from '../utils'; import SingleDebugDrawer from './single-debug-drawer'; import { useFormConfigMap } from './use-form-config-map'; +import { useValues } from './use-values'; interface IProps { node?: RAGFlowNodeType; @@ -54,8 +50,10 @@ const FormSheet = ({ const OperatorForm = currentFormMap.component ?? EmptyContent; + const values = useValues(node); + const form = useForm({ - values: currentFormMap.defaultValues, + values: values, resolver: zodResolver(currentFormMap.schema), }); @@ -74,43 +72,39 @@ const FormSheet = ({ form, ); - useEffect(() => { - if (visible) { - if (node?.id !== previousId.current) { - form.reset(); - form.clearErrors(); - } + // useEffect(() => { + // if (visible && !form.formState.isDirty) { + // // if (node?.id !== previousId.current) { + // // form.reset(); + // // form.clearErrors(); + // // } - const formData = node?.data?.form; + // const formData = node?.data?.form; - if (operatorName === Operator.Categorize) { - const items = buildCategorizeListFromObject( - get(node, 'data.form.category_description', {}), - ); - if (isPlainObject(formData)) { - // form.setFieldsValue({ ...formData, items }); - console.info('xxx'); - const nextValues = { - ...omit(formData, 'category_description'), - items, - }; - // Object.entries(nextValues).forEach(([key, value]) => { - // form.setValue(key, value, { shouldDirty: false }); - // }); - form.reset(nextValues); - } - } else if (operatorName === Operator.Message) { - form.reset({ - ...formData, - content: convertToObjectArray(formData.content), - }); - } else { - // form.setFieldsValue(node?.data?.form); - form.reset(node?.data?.form); - } - previousId.current = node?.id; - } - }, [visible, form, node?.data?.form, node?.id, node, operatorName]); + // if (operatorName === Operator.Categorize) { + // const items = buildCategorizeListFromObject( + // get(node, 'data.form.category_description', {}), + // ); + // if (isPlainObject(formData)) { + // console.info('xxx'); + // const nextValues = { + // ...omit(formData, 'category_description'), + // items, + // }; + + // form.reset(nextValues); + // } + // } else if (operatorName === Operator.Message) { + // form.reset({ + // ...formData, + // content: convertToObjectArray(formData.content), + // }); + // } else { + // form.reset(node?.data?.form); + // } + // previousId.current = node?.id; + // } + // }, [visible, form, node?.data?.form, node?.id, node, operatorName]); return ( diff --git a/web/src/pages/agent/form-sheet/use-form-config-map.tsx b/web/src/pages/agent/form-sheet/use-form-config-map.tsx index 082140eed..042f9f16d 100644 --- a/web/src/pages/agent/form-sheet/use-form-config-map.tsx +++ b/web/src/pages/agent/form-sheet/use-form-config-map.tsx @@ -1,9 +1,8 @@ import { LlmSettingSchema } from '@/components/llm-setting-items/next'; import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent'; -import { ModelVariableType } from '@/constants/knowledge'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; -import { AgentDialogueMode, Operator } from '../constant'; +import { Operator } from '../constant'; import AkShareForm from '../form/akshare-form'; import AnswerForm from '../form/answer-form'; import ArXivForm from '../form/arxiv-form'; @@ -45,11 +44,7 @@ export function useFormConfigMap() { const FormConfigMap = { [Operator.Begin]: { component: BeginForm, - defaultValues: { - enablePrologue: true, - prologue: t('chat.setAnOpenerInitial'), - mode: AgentDialogueMode.Conversational, - }, + defaultValues: {}, schema: z.object({ enablePrologue: z.boolean().optional(), prologue: z @@ -116,16 +111,7 @@ export function useFormConfigMap() { }, [Operator.Categorize]: { component: CategorizeForm, - defaultValues: { - parameter: ModelVariableType.Precise, - message_history_window_size: 1, - temperatureEnabled: true, - topPEnabled: true, - presencePenaltyEnabled: true, - frequencyPenaltyEnabled: true, - maxTokensEnabled: true, - items: [], - }, + defaultValues: {}, schema: z.object({ parameter: z.string().optional(), ...LlmSettingSchema, @@ -149,9 +135,7 @@ export function useFormConfigMap() { }, [Operator.Message]: { component: MessageForm, - defaultValues: { - content: [], - }, + defaultValues: {}, schema: z.object({ content: z .array( diff --git a/web/src/pages/agent/form-sheet/use-values.ts b/web/src/pages/agent/form-sheet/use-values.ts new file mode 100644 index 000000000..eccee9c77 --- /dev/null +++ b/web/src/pages/agent/form-sheet/use-values.ts @@ -0,0 +1,42 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { get, isEmpty, isPlainObject, omit } from 'lodash'; +import { useMemo, useRef } from 'react'; +import { Operator } from '../constant'; +import { buildCategorizeListFromObject, convertToObjectArray } from '../utils'; +import { useFormConfigMap } from './use-form-config-map'; + +export function useValues(node?: RAGFlowNodeType, isDirty?: boolean) { + const operatorName: Operator = node?.data.label as Operator; + const previousId = useRef(node?.id); + + const FormConfigMap = useFormConfigMap(); + + const currentFormMap = FormConfigMap[operatorName]; + + const values = useMemo(() => { + const formData = node?.data?.form; + if (operatorName === Operator.Categorize) { + const items = buildCategorizeListFromObject( + get(node, 'data.form.category_description', {}), + ); + if (isPlainObject(formData)) { + console.info('xxx'); + const nextValues = { + ...omit(formData, 'category_description'), + items, + }; + + return nextValues; + } + } else if (operatorName === Operator.Message) { + return { + ...formData, + content: convertToObjectArray(formData.content), + }; + } else { + return isEmpty(formData) ? currentFormMap : formData; + } + }, [currentFormMap, node, operatorName]); + + return values; +} diff --git a/web/src/pages/agent/form/begin-form/index.tsx b/web/src/pages/agent/form/begin-form/index.tsx index 60af202f1..e765e11ee 100644 --- a/web/src/pages/agent/form/begin-form/index.tsx +++ b/web/src/pages/agent/form/begin-form/index.tsx @@ -13,24 +13,61 @@ import { Switch } from '@/components/ui/switch'; import { Textarea } from '@/components/ui/textarea'; import { FormTooltip } from '@/components/ui/tooltip'; import { buildSelectOptions } from '@/utils/component-util'; +import { zodResolver } from '@hookform/resolvers/zod'; import { Plus } from 'lucide-react'; import { useCallback } from 'react'; -import { useWatch } from 'react-hook-form'; +import { useForm, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; import { AgentDialogueMode } from '../../constant'; import { INextOperatorForm } from '../../interface'; import { ParameterDialog } from './parameter-dialog'; import { QueryTable } from './query-table'; import { useEditQueryRecord } from './use-edit-query'; +import { useValues } from './use-values'; +import { useWatchFormChange } from './use-watch-change'; const ModeOptions = buildSelectOptions([ AgentDialogueMode.Conversational, AgentDialogueMode.Task, ]); -const BeginForm = ({ form, node }: INextOperatorForm) => { +const BeginForm = ({ node }: INextOperatorForm) => { const { t } = useTranslation(); + const values = useValues(node); + + const FormSchema = z.object({ + enablePrologue: z.boolean().optional(), + prologue: z + .string() + .min(1, { + message: t('common.namePlaceholder'), + }) + .trim() + .optional(), + mode: z.string(), + query: z + .array( + z.object({ + key: z.string(), + type: z.string(), + value: z.string(), + optional: z.boolean(), + name: z.string(), + options: z.array(z.union([z.number(), z.string(), z.boolean()])), + }), + ) + .optional(), + }); + + const form = useForm({ + defaultValues: values, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + const query = useWatch({ control: form.control, name: 'query' }); const mode = useWatch({ control: form.control, name: 'mode' }); diff --git a/web/src/pages/agent/form/begin-form/use-values.ts b/web/src/pages/agent/form/begin-form/use-values.ts new file mode 100644 index 000000000..7e8347bd0 --- /dev/null +++ b/web/src/pages/agent/form/begin-form/use-values.ts @@ -0,0 +1,30 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { AgentDialogueMode } from '../../constant'; + +export function useValues(node?: RAGFlowNodeType) { + const { t } = useTranslation(); + + const defaultValues = useMemo( + () => ({ + enablePrologue: true, + prologue: t('chat.setAnOpenerInitial'), + mode: AgentDialogueMode.Conversational, + }), + [t], + ); + + const values = useMemo(() => { + const formData = node?.data?.form; + + if (isEmpty(formData)) { + return defaultValues; + } + + return formData; + }, [defaultValues, node?.data?.form]); + + return values; +} diff --git a/web/src/pages/agent/form/begin-form/use-watch-change.ts b/web/src/pages/agent/form/begin-form/use-watch-change.ts new file mode 100644 index 000000000..ac31dba4d --- /dev/null +++ b/web/src/pages/agent/form/begin-form/use-watch-change.ts @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import useGraphStore from '../../store'; + +export function useWatchFormChange(id?: string, form?: UseFormReturn) { + let values = useWatch({ control: form?.control }); + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + useEffect(() => { + // Manually triggered form updates are synchronized to the canvas + if (id && form?.formState.isDirty) { + values = form?.getValues(); + let nextValues: any = values; + + updateNodeForm(id, nextValues); + } + }, [form?.formState.isDirty, id, updateNodeForm, values]); +} diff --git a/web/src/pages/agent/form/categorize-form/index.tsx b/web/src/pages/agent/form/categorize-form/index.tsx index bc85ed110..7bd906ceb 100644 --- a/web/src/pages/agent/form/categorize-form/index.tsx +++ b/web/src/pages/agent/form/categorize-form/index.tsx @@ -1,4 +1,5 @@ import { LargeModelFormField } from '@/components/large-model-form-field'; +import { LlmSettingSchema } from '@/components/llm-setting-items/next'; import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; import { SelectWithSearch } from '@/components/originui/select-with-search'; import { @@ -9,13 +10,48 @@ import { FormLabel, FormMessage, } from '@/components/ui/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; import { INextOperatorForm } from '../../interface'; import DynamicCategorize from './dynamic-categorize'; +import { useValues } from './use-values'; +import { useWatchFormChange } from './use-watch-change'; -const CategorizeForm = ({ form, node }: INextOperatorForm) => { +const CategorizeForm = ({ node }: INextOperatorForm) => { const { t } = useTranslation(); + const values = useValues(node); + + const FormSchema = z.object({ + parameter: z.string().optional(), + ...LlmSettingSchema, + message_history_window_size: z.coerce.number(), + items: z.array( + z + .object({ + name: z.string().min(1, t('flow.nameMessage')).trim(), + description: z.string().optional(), + // examples: z + // .array( + // z.object({ + // value: z.string(), + // }), + // ) + // .optional(), + }) + .optional(), + ), + }); + + const form = useForm({ + defaultValues: values, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + return (
{ + const formData = node?.data?.form; + if (isEmpty(formData)) { + return defaultValues; + } + const items = buildCategorizeListFromObject( + get(node, 'data.form.category_description', {}), + ); + if (isPlainObject(formData)) { + const nextValues = { + ...omit(formData, 'category_description'), + items, + }; + + return nextValues; + } + }, [node]); + + return values; +} diff --git a/web/src/pages/agent/form/categorize-form/use-watch-change.ts b/web/src/pages/agent/form/categorize-form/use-watch-change.ts new file mode 100644 index 000000000..6f01dc1a9 --- /dev/null +++ b/web/src/pages/agent/form/categorize-form/use-watch-change.ts @@ -0,0 +1,30 @@ +import { omit } from 'lodash'; +import { useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import useGraphStore from '../../store'; +import { buildCategorizeObjectFromList } from '../../utils'; + +export function useWatchFormChange(id?: string, form?: UseFormReturn) { + let values = useWatch({ control: form?.control }); + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + useEffect(() => { + // Manually triggered form updates are synchronized to the canvas + if (id && form?.formState.isDirty) { + values = form?.getValues(); + let nextValues: any = values; + + const categoryDescription = Array.isArray(values.items) + ? buildCategorizeObjectFromList(values.items) + : {}; + if (categoryDescription) { + nextValues = { + ...omit(values, 'items'), + category_description: categoryDescription, + }; + } + + updateNodeForm(id, nextValues); + } + }, [form?.formState.isDirty, id, updateNodeForm, values]); +} diff --git a/web/src/pages/agent/form/code-form/index.tsx b/web/src/pages/agent/form/code-form/index.tsx index 4db2a96a5..71e0f6f52 100644 --- a/web/src/pages/agent/form/code-form/index.tsx +++ b/web/src/pages/agent/form/code-form/index.tsx @@ -12,15 +12,22 @@ import { } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { RAGFlowSelect } from '@/components/ui/select'; -import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent'; +import { ProgrammingLanguage } from '@/constants/agent'; import { ICodeForm } from '@/interfaces/database/flow'; -import { useEffect } from 'react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; import { DynamicInputVariable, TypeOptions, VariableTitle, } from './next-variable'; +import { useValues } from './use-values'; +import { + useHandleLanguageChange, + useWatchFormChange, +} from './use-watch-change'; loader.config({ paths: { vs: '/vs' } }); @@ -29,17 +36,33 @@ const options = [ ProgrammingLanguage.Javascript, ].map((x) => ({ value: x, label: x })); -const CodeForm = ({ form, node }: INextOperatorForm) => { +const CodeForm = ({ node }: INextOperatorForm) => { const formData = node?.data.form as ICodeForm; const { t } = useTranslation(); + const values = useValues(node); - useEffect(() => { - // TODO: Direct operation zustand is more elegant - form?.setValue( - 'script', - CodeTemplateStrMap[formData.lang as ProgrammingLanguage], - ); - }, [form, formData.lang]); + const FormSchema = z.object({ + lang: z.string(), + script: z.string(), + arguments: z.array( + z.object({ name: z.string(), component_id: z.string() }), + ), + return: z.union([ + z + .array(z.object({ name: z.string(), component_id: z.string() })) + .optional(), + z.object({ name: z.string(), component_id: z.string() }), + ]), + }); + + const form = useForm({ + defaultValues: values, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + const handleLanguageChange = useHandleLanguageChange(node?.id, form); return ( @@ -66,7 +89,14 @@ const CodeForm = ({ form, node }: INextOperatorForm) => { render={({ field }) => ( - + { + field.onChange(val); + handleLanguageChange(val); + }} + options={options} + /> diff --git a/web/src/pages/agent/form/code-form/use-values.ts b/web/src/pages/agent/form/code-form/use-values.ts new file mode 100644 index 000000000..9dc7d68d4 --- /dev/null +++ b/web/src/pages/agent/form/code-form/use-values.ts @@ -0,0 +1,27 @@ +import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent'; +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; + +export function useValues(node?: RAGFlowNodeType) { + const defaultValues = useMemo( + () => ({ + lang: ProgrammingLanguage.Python, + script: CodeTemplateStrMap[ProgrammingLanguage.Python], + arguments: [], + }), + [], + ); + + const values = useMemo(() => { + const formData = node?.data?.form; + + if (isEmpty(formData)) { + return defaultValues; + } + + return formData; + }, [defaultValues, node?.data?.form]); + + return values; +} diff --git a/web/src/pages/agent/form/code-form/use-watch-change.ts b/web/src/pages/agent/form/code-form/use-watch-change.ts new file mode 100644 index 000000000..7c9759560 --- /dev/null +++ b/web/src/pages/agent/form/code-form/use-watch-change.ts @@ -0,0 +1,36 @@ +import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent'; +import { useCallback, useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import useGraphStore from '../../store'; + +export function useWatchFormChange(id?: string, form?: UseFormReturn) { + let values = useWatch({ control: form?.control }); + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + useEffect(() => { + // Manually triggered form updates are synchronized to the canvas + if (id && form?.formState.isDirty) { + values = form?.getValues(); + let nextValues: any = values; + + updateNodeForm(id, nextValues); + } + }, [form?.formState.isDirty, id, updateNodeForm, values]); +} + +export function useHandleLanguageChange(id?: string, form?: UseFormReturn) { + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + const handleLanguageChange = useCallback( + (lang: string) => { + if (id) { + const script = CodeTemplateStrMap[lang as ProgrammingLanguage]; + form?.setValue('script', script); + updateNodeForm(id, script, ['script']); + } + }, + [form, id, updateNodeForm], + ); + + return handleLanguageChange; +} diff --git a/web/src/pages/agent/form/message-form/index.tsx b/web/src/pages/agent/form/message-form/index.tsx index 31a907980..cc910c4b6 100644 --- a/web/src/pages/agent/form/message-form/index.tsx +++ b/web/src/pages/agent/form/message-form/index.tsx @@ -9,14 +9,37 @@ import { FormLabel, FormMessage, } from '@/components/ui/form'; +import { zodResolver } from '@hookform/resolvers/zod'; import { X } from 'lucide-react'; -import { useFieldArray } from 'react-hook-form'; +import { useFieldArray, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; import { INextOperatorForm } from '../../interface'; +import { useValues } from './use-values'; +import { useWatchFormChange } from './use-watch-change'; -const MessageForm = ({ form }: INextOperatorForm) => { +const MessageForm = ({ node }: INextOperatorForm) => { const { t } = useTranslation(); + const values = useValues(node); + + const FormSchema = z.object({ + content: z + .array( + z.object({ + value: z.string(), + }), + ) + .optional(), + }); + + const form = useForm({ + defaultValues: values, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + const { fields, append, remove } = useFieldArray({ name: 'content', control: form.control, diff --git a/web/src/pages/agent/form/message-form/use-values.ts b/web/src/pages/agent/form/message-form/use-values.ts new file mode 100644 index 000000000..415314665 --- /dev/null +++ b/web/src/pages/agent/form/message-form/use-values.ts @@ -0,0 +1,25 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; +import { convertToObjectArray } from '../../utils'; + +const defaultValues = { + content: [], +}; + +export function useValues(node?: RAGFlowNodeType) { + const values = useMemo(() => { + const formData = node?.data?.form; + + if (isEmpty(formData)) { + return defaultValues; + } + + return { + ...formData, + content: convertToObjectArray(formData.content), + }; + }, [node]); + + return values; +} diff --git a/web/src/pages/agent/form/message-form/use-watch-change.ts b/web/src/pages/agent/form/message-form/use-watch-change.ts new file mode 100644 index 000000000..10c35c653 --- /dev/null +++ b/web/src/pages/agent/form/message-form/use-watch-change.ts @@ -0,0 +1,24 @@ +import { useEffect } from 'react'; +import { UseFormReturn, useWatch } from 'react-hook-form'; +import useGraphStore from '../../store'; +import { convertToStringArray } from '../../utils'; + +export function useWatchFormChange(id?: string, form?: UseFormReturn) { + let values = useWatch({ control: form?.control }); + const updateNodeForm = useGraphStore((state) => state.updateNodeForm); + + useEffect(() => { + // Manually triggered form updates are synchronized to the canvas + if (id && form?.formState.isDirty) { + values = form?.getValues(); + let nextValues: any = values; + + nextValues = { + ...values, + content: convertToStringArray(values.content), + }; + + updateNodeForm(id, nextValues); + } + }, [form?.formState.isDirty, id, updateNodeForm, values]); +}