From 8266dc1dcc6a7c5a28095997cbcdb9adab7b7cc9 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Wed, 13 Aug 2025 10:12:17 +0800 Subject: [PATCH] form --- .../base/form/components/base/base-field.tsx | 118 +++-- web/app/components/base/form/types.ts | 13 +- .../components/array-value-list.tsx | 2 +- .../components/variable-modal.tsx | 446 ++---------------- .../panel/chat-variable-panel/hooks/index.ts | 1 + .../chat-variable-panel/hooks/use-form.ts | 293 ++++++++++++ 6 files changed, 436 insertions(+), 437 deletions(-) create mode 100644 web/app/components/workflow/panel/chat-variable-panel/hooks/index.ts create mode 100644 web/app/components/workflow/panel/chat-variable-panel/hooks/use-form.ts diff --git a/web/app/components/base/form/components/base/base-field.tsx b/web/app/components/base/form/components/base/base-field.tsx index 55a4201cf2..5da10ba78a 100644 --- a/web/app/components/base/form/components/base/base-field.tsx +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -1,11 +1,14 @@ import { isValidElement, memo, + useCallback, useMemo, } from 'react' import { RiArrowDownSFill, + RiDraftLine, RiExternalLinkLine, + RiInputField, } from '@remixicon/react' import type { AnyFieldApi } from '@tanstack/react-form' import { useStore } from '@tanstack/react-form' @@ -19,6 +22,11 @@ import RadioE from '@/app/components/base/radio/ui' import Textarea from '@/app/components/base/textarea' import PromptEditor from '@/app/components/base/prompt-editor' import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector' +import ObjectValueList from '@/app/components/workflow/panel/chat-variable-panel/components/object-value-list' +import ArrayValueList from '@/app/components/workflow/panel/chat-variable-panel/components/array-value-list' +import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' +import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' +import Button from '@/app/components/base/button' export type BaseFieldProps = { fieldClassName?: string @@ -40,6 +48,7 @@ const BaseField = ({ }: BaseFieldProps) => { const renderI18nObject = useRenderI18nObject() const { + type: typeOrFn, label, required, placeholder, @@ -49,7 +58,13 @@ const BaseField = ({ inputContainerClassName: formInputContainerClassName, inputClassName: formInputClassName, show_on = [], + url, + help, + selfFormProps, + onChange, } = formSchema + const type = typeof typeOrFn === 'function' ? typeOrFn(field.form) : typeOrFn + console.log('type', field.name, type) const memorizedLabel = useMemo(() => { if (isValidElement(label)) @@ -86,7 +101,7 @@ const BaseField = ({ return option.show_on.every((condition) => { const conditionValue = optionValues[condition.variable] - return conditionValue === condition.value + return Array.isArray(condition.value) ? condition.value.includes(conditionValue) : conditionValue === condition.value }) }).map((option) => { return { @@ -97,17 +112,22 @@ const BaseField = ({ }, [options, renderI18nObject]) const value = useStore(field.form.store, s => s.values[field.name]) const values = useStore(field.form.store, (s) => { - return show_on.reduce((acc, condition) => { + return (Array.isArray(show_on) ? show_on : show_on(field.form)).reduce((acc, condition) => { acc[condition.variable] = s.values[condition.variable] return acc }, {} as Record) }) const show = useMemo(() => { - return show_on.every((condition) => { + return (Array.isArray(show_on) ? show_on : show_on(field.form)).every((condition) => { const conditionValue = values[condition.variable] - return conditionValue === condition.value + console.log('conditionValue', condition.value, field.name, conditionValue) + return Array.isArray(condition.value) ? condition.value.includes(conditionValue) : conditionValue === condition.value }) - }, [values, show_on]) + }, [values, show_on, field.name]) + const handleChange = useCallback((value: any) => { + field.handleChange(value) + onChange?.(field.form) + }, [field, onChange]) if (!show) return null @@ -122,26 +142,39 @@ const BaseField = ({ ) } { - formSchema.type === FormTypeEnum.collapse && ( + type === FormTypeEnum.collapse && ( field.handleChange(!value)} + onClick={() => handleChange(!value)} /> ) } + { + type === FormTypeEnum.editMode && ( + + ) + }
{ - formSchema.type === FormTypeEnum.textInput && ( + type === FormTypeEnum.textInput && ( field.handleChange(e.target.value)} + onChange={e => handleChange(e.target.value)} onBlur={field.handleBlur} disabled={disabled} placeholder={memorizedPlaceholder} @@ -149,14 +182,14 @@ const BaseField = ({ ) } { - formSchema.type === FormTypeEnum.secretInput && ( + type === FormTypeEnum.secretInput && ( field.handleChange(e.target.value)} + onChange={e => handleChange(e.target.value)} onBlur={field.handleBlur} disabled={disabled} placeholder={memorizedPlaceholder} @@ -164,14 +197,14 @@ const BaseField = ({ ) } { - formSchema.type === FormTypeEnum.textNumber && ( + type === FormTypeEnum.textNumber && ( field.handleChange(e.target.value)} + onChange={e => handleChange(e.target.value)} onBlur={field.handleBlur} disabled={disabled} placeholder={memorizedPlaceholder} @@ -179,10 +212,10 @@ const BaseField = ({ ) } { - formSchema.type === FormTypeEnum.select && ( + type === FormTypeEnum.select && ( field.handleChange(v)} + onChange={handleChange} disabled={disabled} placeholder={memorizedPlaceholder} options={memorizedOptions} @@ -191,7 +224,7 @@ const BaseField = ({ ) } { - formSchema.type === FormTypeEnum.radio && ( + type === FormTypeEnum.radio && (
@@ -205,10 +238,10 @@ const BaseField = ({ inputClassName, formInputClassName, )} - onClick={() => field.handleChange(option.value)} + onClick={() => handleChange(option.value)} > { - formSchema.showRadioUI && ( + selfFormProps?.(field.form)?.showRadioUI && ( field.handleChange(e.target.value)} + onChange={e => handleChange(e.target.value)} onBlur={field.handleBlur} disabled={disabled} /> ) } { - formSchema.type === FormTypeEnum.promptInput && ( + type === FormTypeEnum.promptInput && ( field.handleChange(e)} + onChange={handleChange} onBlur={field.handleBlur} editable={!disabled} placeholder={memorizedPlaceholder} @@ -255,11 +288,42 @@ const BaseField = ({ ) } { - formSchema.type === FormTypeEnum.modelSelector && ( + type === FormTypeEnum.objectList && ( + + ) + } + { + type === FormTypeEnum.arrayList && ( + + ) + } + { + type === FormTypeEnum.jsonInput && ( +
+ {selfFormProps?.(field.form)?.placeholder as string}
} + onChange={handleChange} + /> +
+ ) + } + { + type === FormTypeEnum.modelSelector && ( field.handleChange(p)} + setModel={handleChange} readonly={disabled} scope={formSchema.scope} isAdvancedMode @@ -267,14 +331,14 @@ const BaseField = ({ ) } { - formSchema.url && ( + url && ( - {renderI18nObject(formSchema?.help as any)} + {renderI18nObject(help as any)} { diff --git a/web/app/components/base/form/types.ts b/web/app/components/base/form/types.ts index fa62963263..eda37aa1ae 100644 --- a/web/app/components/base/form/types.ts +++ b/web/app/components/base/form/types.ts @@ -15,7 +15,7 @@ export type TypeWithI18N = { export type FormShowOnObject = { variable: string - value: string + value: string | string[] } export enum FormTypeEnum { @@ -34,7 +34,11 @@ export enum FormTypeEnum { dynamicSelect = 'dynamic-select', textareaInput = 'textarea-input', promptInput = 'prompt-input', + objectList = 'object-list', + arrayList = 'array-list', + jsonInput = 'json-input', collapse = 'collapse', + editMode = 'edit-mode', } export type FormOption = { @@ -47,13 +51,13 @@ export type FormOption = { export type AnyValidators = FieldValidators export type FormSchema = { - type: FormTypeEnum + type: FormTypeEnum | ((form: AnyFormApi) => FormTypeEnum) name: string label: string | ReactNode | TypeWithI18N required: boolean default?: any tooltip?: string | TypeWithI18N - show_on?: FormShowOnObject[] + show_on?: FormShowOnObject[] | ((form: AnyFormApi) => FormShowOnObject[]) url?: string scope?: string help?: string | TypeWithI18N @@ -64,7 +68,8 @@ export type FormSchema = { inputContainerClassName?: string inputClassName?: string validators?: AnyValidators - showRadioUI?: boolean + selfFormProps?: (form: AnyFormApi) => Record + onChange?: (form: AnyFormApi) => void } export type FormValues = Record diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/array-value-list.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/array-value-list.tsx index 302b8ff26e..29855528a8 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/array-value-list.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/array-value-list.tsx @@ -52,7 +52,7 @@ const ArrayValueList: FC = ({
diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx index 1c6a66e2e6..32813c5d8e 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx @@ -1,23 +1,21 @@ -import React, { useCallback, useEffect, useMemo } from 'react' +import React, { useCallback, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' -import { v4 as uuid4 } from 'uuid' -import { RiCloseLine, RiDraftLine, RiInputField } from '@remixicon/react' -import VariableTypeSelector from '@/app/components/workflow/panel/chat-variable-panel/components/variable-type-select' -import ObjectValueList from '@/app/components/workflow/panel/chat-variable-panel/components/object-value-list' -import { DEFAULT_OBJECT_VALUE } from '@/app/components/workflow/panel/chat-variable-panel/components/object-value-item' -import ArrayValueList from '@/app/components/workflow/panel/chat-variable-panel/components/array-value-list' +import { + useForm as useTanstackForm, + useStore as useTanstackStore, +} from '@tanstack/react-form' +import { RiCloseLine } from '@remixicon/react' import Button from '@/app/components/base/button' -import Input from '@/app/components/base/input' -import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import { ToastContext } from '@/app/components/base/toast' import { useStore } from '@/app/components/workflow/store' import type { ConversationVariable } from '@/app/components/workflow/types' -import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type' import cn from '@/utils/classnames' -import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' +import { checkKeys } from '@/utils/var' +import type { FormRefObject, FormSchema } from '@/app/components/base/form/types' import VariableForm from '@/app/components/base/form/form-scenarios/variable' +import { useForm } from '../hooks' export type ModalPropsType = { chatVar?: ConversationVariable @@ -25,48 +23,6 @@ export type ModalPropsType = { onSave: (chatVar: ConversationVariable) => void } -type ObjectValueItem = { - key: string - type: ChatVarType - value: string | number | undefined -} - -const typeList = [ - ChatVarType.String, - ChatVarType.Number, - ChatVarType.Object, - ChatVarType.ArrayString, - ChatVarType.ArrayNumber, - ChatVarType.ArrayObject, -] - -const objectPlaceholder = `# example -# { -# "name": "ray", -# "age": 20 -# }` -const arrayStringPlaceholder = `# example -# [ -# "value1", -# "value2" -# ]` -const arrayNumberPlaceholder = `# example -# [ -# 100, -# 200 -# ]` -const arrayObjectPlaceholder = `# example -# [ -# { -# "name": "ray", -# "age": 20 -# }, -# { -# "name": "lily", -# "age": 18 -# } -# ]` - const ChatVariableModal = ({ chatVar, onClose, @@ -75,133 +31,16 @@ const ChatVariableModal = ({ const { t } = useTranslation() const { notify } = useContext(ToastContext) const varList = useStore(s => s.conversationVariables) - const [name, setName] = React.useState('') - const [type, setType] = React.useState(ChatVarType.String) - const [value, setValue] = React.useState() - const [objectValue, setObjectValue] = React.useState([DEFAULT_OBJECT_VALUE]) - const [editorContent, setEditorContent] = React.useState() - const [editInJSON, setEditInJSON] = React.useState(false) - const [description, setDescription] = React.useState('') - const variableFormSchemas = useMemo(() => { - return [ - { - name: 'name', - label: t('workflow.chatVariable.modal.name'), - type: 'text-input', - placeholder: t('workflow.chatVariable.modal.namePlaceholder'), - }, - { - name: 'type', - label: t('workflow.chatVariable.modal.type'), - type: 'select', - options: typeList.map(type => ({ - label: type, - value: type, - })), - }, - { - name: 'value', - label: t('workflow.chatVariable.modal.value'), - type: 'textarea', - placeholder: t('workflow.chatVariable.modal.valuePlaceholder'), - fieldClassName: 'h-20', - }, - { - name: 'description', - label: t('workflow.chatVariable.modal.description'), - type: 'textarea-input', - placeholder: t('workflow.chatVariable.modal.descriptionPlaceholder'), - }, - { - name: 'memoryTemplate', - label: 'Memory template', - type: 'prompt-input', - }, - { - name: 'updateTrigger', - label: 'Update trigger', - type: 'radio', - required: true, - fieldClassName: 'flex items-center justify-between', - options: [ - { - label: 'Every N turns', - value: 'every_n_turns', - }, - { - label: 'Auto', - value: 'auto', - }, - ], - }, - { - name: 'moreSettings', - label: 'More settings', - type: 'collapse', - }, - { - name: 'memoryModel', - label: 'Memory model', - type: 'model-selector', - show_on: [ - { - variable: 'moreSettings', - value: true, - }, - ], - }, - ] - }, []) - - const editorMinHeight = useMemo(() => { - if (type === ChatVarType.ArrayObject) - return '240px' - return '120px' - }, [type]) - const placeholder = useMemo(() => { - if (type === ChatVarType.ArrayString) - return arrayStringPlaceholder - if (type === ChatVarType.ArrayNumber) - return arrayNumberPlaceholder - if (type === ChatVarType.ArrayObject) - return arrayObjectPlaceholder - return objectPlaceholder - }, [type]) - const getObjectValue = useCallback(() => { - if (!chatVar || Object.keys(chatVar.value).length === 0) - return [DEFAULT_OBJECT_VALUE] - - return Object.keys(chatVar.value).map((key) => { - return { - key, - type: typeof chatVar.value[key] === 'string' ? ChatVarType.String : ChatVarType.Number, - value: chatVar.value[key], - } - }) - }, [chatVar]) - const formatValueFromObject = useCallback((list: ObjectValueItem[]) => { - return list.reduce((acc: any, curr) => { - if (curr.key) - acc[curr.key] = curr.value || null - return acc - }, {}) - }, []) - - const formatValue = (value: any) => { - switch (type) { - case ChatVarType.String: - return value || '' - case ChatVarType.Number: - return value || 0 - case ChatVarType.Object: - return editInJSON ? value : formatValueFromObject(objectValue) - case ChatVarType.ArrayString: - case ChatVarType.ArrayNumber: - case ChatVarType.ArrayObject: - return value?.filter(Boolean) || [] - } - } + const { + formSchemas, + defaultValues, + } = useForm(chatVar) + const formRef = useRef(null) + const form = useTanstackForm({ + defaultValues, + }) + const type = useTanstackStore(form.store, s => s.values.type) const checkVariableName = (value: string) => { const { isValid, errorMessageKey } = checkKeys([value], false) @@ -215,121 +54,31 @@ const ChatVariableModal = ({ return true } - const handleVarNameChange = (e: React.ChangeEvent) => { - replaceSpaceWithUnderscoreInVarNameInput(e.target) - if (!!e.target.value && !checkVariableName(e.target.value)) - return - setName(e.target.value || '') - } - - const handleTypeChange = (v: ChatVarType) => { - setValue(undefined) - setEditorContent(undefined) - if (v === ChatVarType.ArrayObject) - setEditInJSON(true) - if (v === ChatVarType.String || v === ChatVarType.Number || v === ChatVarType.Object) - setEditInJSON(false) - setType(v) - } - - const handleEditorChange = (editInJSON: boolean) => { - if (type === ChatVarType.Object) { - if (editInJSON) { - const newValue = !objectValue[0].key ? undefined : formatValueFromObject(objectValue) - setValue(newValue) - setEditorContent(JSON.stringify(newValue)) - } - else { - if (!editorContent) { - setValue(undefined) - setObjectValue([DEFAULT_OBJECT_VALUE]) - } - else { - try { - const newValue = JSON.parse(editorContent) - setValue(newValue) - const newObjectValue = Object.keys(newValue).map((key) => { - return { - key, - type: typeof newValue[key] === 'string' ? ChatVarType.String : ChatVarType.Number, - value: newValue[key], - } - }) - setObjectValue(newObjectValue) - } - catch { - // ignore JSON.parse errors - } - } - } - } - if (type === ChatVarType.ArrayString || type === ChatVarType.ArrayNumber) { - if (editInJSON) { - const newValue = (value?.length && value.filter(Boolean).length) ? value.filter(Boolean) : undefined - setValue(newValue) - if (!editorContent) - setEditorContent(JSON.stringify(newValue)) - } - else { - setValue(value?.length ? value : [undefined]) - } - } - setEditInJSON(editInJSON) - } - - const handleEditorValueChange = (content: string) => { - if (!content) { - setEditorContent(content) - return setValue(undefined) - } - else { - setEditorContent(content) - try { - const newValue = JSON.parse(content) - setValue(newValue) - } - catch { - // ignore JSON.parse errors - } - } - } - - const handleSave = () => { + const handleConfirm = useCallback(async () => { + const { + values, + } = formRef.current?.getFormValues({}) || { isCheckValidated: false, values: {} } + const { + name, + type, + 'object-list-value': objectValue, + } = values if (!checkVariableName(name)) return if (!chatVar && varList.some(chatVar => chatVar.name === name)) return notify({ type: 'error', message: 'name is existed' }) - // if (type !== ChatVarType.Object && !value) - // return notify({ type: 'error', message: 'value can not be empty' }) - if (type === ChatVarType.Object && objectValue.some(item => !item.key && !!item.value)) + if (type === ChatVarType.Object && objectValue.some((item: any) => !item.key && !!item.value)) return notify({ type: 'error', message: 'object key can not be empty' }) - onSave({ - id: chatVar ? chatVar.id : uuid4(), - name, - value_type: type, - value: formatValue(value), - description, - }) + // onSave({ + // id: chatVar ? chatVar.id : uuid4(), + // name, + // value_type: type, + // value: values, + // description, + // }) onClose() - } - - useEffect(() => { - if (chatVar) { - setName(chatVar.name) - setType(chatVar.value_type) - setValue(chatVar.value) - setDescription(chatVar.description) - setObjectValue(getObjectValue()) - if (chatVar.value_type === ChatVarType.ArrayObject) { - setEditorContent(JSON.stringify(chatVar.value)) - setEditInJSON(true) - } - else { - setEditInJSON(false) - } - } - }, [chatVar, getObjectValue]) + }, [onClose, notify, t, varList, chatVar, checkVariableName]) return (
- {/* name */} -
-
{t('workflow.chatVariable.modal.name')}
-
- checkVariableName(e.target.value)} - type='text' - /> -
-
- {/* type */} -
-
{t('workflow.chatVariable.modal.type')}
-
- -
-
- {/* default value */} -
-
-
{t('workflow.chatVariable.modal.value')}
- {(type === ChatVarType.ArrayString || type === ChatVarType.ArrayNumber) && ( - - )} - {type === ChatVarType.Object && ( - - )} -
-
- {type === ChatVarType.String && ( - // Input will remove \n\r, so use Textarea just like description area -