### What problem does this PR solve? Feat: Use one-way data flow to synchronize the form data to the canvas #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.1
| @@ -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], | |||
| ); | |||
| @@ -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(); | |||
| } | |||
| 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]); | |||
| // useEffect(() => { | |||
| // if (visible && !form.formState.isDirty) { | |||
| // // if (node?.id !== previousId.current) { | |||
| // // form.reset(); | |||
| // // form.clearErrors(); | |||
| // // } | |||
| // 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, | |||
| // }; | |||
| // 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 ( | |||
| <Sheet open={visible} modal={false}> | |||
| @@ -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( | |||
| @@ -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<string | undefined>(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; | |||
| } | |||
| @@ -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' }); | |||
| @@ -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; | |||
| } | |||
| @@ -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]); | |||
| } | |||
| @@ -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 ( | |||
| <Form {...form}> | |||
| <form | |||
| @@ -0,0 +1,38 @@ | |||
| import { ModelVariableType } from '@/constants/knowledge'; | |||
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | |||
| import { get, isEmpty, isPlainObject, omit } from 'lodash'; | |||
| import { useMemo } from 'react'; | |||
| import { buildCategorizeListFromObject } from '../../utils'; | |||
| const defaultValues = { | |||
| parameter: ModelVariableType.Precise, | |||
| message_history_window_size: 1, | |||
| temperatureEnabled: true, | |||
| topPEnabled: true, | |||
| presencePenaltyEnabled: true, | |||
| frequencyPenaltyEnabled: true, | |||
| maxTokensEnabled: true, | |||
| items: [], | |||
| }; | |||
| export function useValues(node?: RAGFlowNodeType) { | |||
| const values = useMemo(() => { | |||
| 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; | |||
| } | |||
| @@ -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]); | |||
| } | |||
| @@ -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); | |||
| 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); | |||
| useEffect(() => { | |||
| // TODO: Direct operation zustand is more elegant | |||
| form?.setValue( | |||
| 'script', | |||
| CodeTemplateStrMap[formData.lang as ProgrammingLanguage], | |||
| ); | |||
| }, [form, formData.lang]); | |||
| const handleLanguageChange = useHandleLanguageChange(node?.id, form); | |||
| return ( | |||
| <Form {...form}> | |||
| @@ -66,7 +89,14 @@ const CodeForm = ({ form, node }: INextOperatorForm) => { | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormControl> | |||
| <RAGFlowSelect {...field} options={options} /> | |||
| <RAGFlowSelect | |||
| {...field} | |||
| onChange={(val) => { | |||
| field.onChange(val); | |||
| handleLanguageChange(val); | |||
| }} | |||
| options={options} | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| @@ -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; | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -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, | |||
| @@ -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; | |||
| } | |||
| @@ -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]); | |||
| } | |||