diff --git a/web/src/pages/agent/canvas/index.less b/web/src/pages/agent/canvas/index.less index 0183d41b5..eebec6d0a 100644 --- a/web/src/pages/agent/canvas/index.less +++ b/web/src/pages/agent/canvas/index.less @@ -8,4 +8,7 @@ border: 0; background-color: transparent; } + :global(.react-flow__node-group.selectable.selected) { + box-shadow: none; + } } diff --git a/web/src/pages/agent/canvas/node/iteration-node.tsx b/web/src/pages/agent/canvas/node/iteration-node.tsx index 99177c272..a11da33dc 100644 --- a/web/src/pages/agent/canvas/node/iteration-node.tsx +++ b/web/src/pages/agent/canvas/node/iteration-node.tsx @@ -8,7 +8,6 @@ import { memo } from 'react'; import { NodeHandleId, Operator } from '../../constant'; import OperatorIcon from '../../operator-icon'; import { CommonHandle, LeftEndHandle } from './handle'; -import styles from './index.less'; import NodeHeader from './node-header'; import { NodeWrapper } from './node-wrapper'; import { ResizeIcon, controlStyle } from './resize-icon'; @@ -23,9 +22,12 @@ export function InnerIterationNode({ return (
@@ -43,9 +45,9 @@ export function InnerIterationNode({ name={data.name} label={data.label} wrapperClassName={cn( - 'bg-background-header-bar p-2 rounded-t-[10px] absolute w-full top-[-44px] left-[-0.3px]', + 'bg-background-header-bar p-2 rounded-t-[10px] absolute w-full top-[-38px] left-[-0.3px] border-x border-t border-border-button', { - [styles.selectedHeader]: selected, + ['border-x border-t border-accent-primary']: selected, }, )} > diff --git a/web/src/pages/agent/form/iteration-form/dynamic-variables.tsx b/web/src/pages/agent/form/iteration-form/dynamic-variables.tsx new file mode 100644 index 000000000..62840d5a7 --- /dev/null +++ b/web/src/pages/agent/form/iteration-form/dynamic-variables.tsx @@ -0,0 +1,253 @@ +import { KeyInput } from '@/components/key-input'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { useIsDarkTheme } from '@/components/theme-provider'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Separator } from '@/components/ui/separator'; +import { Textarea } from '@/components/ui/textarea'; +import { buildOptions } from '@/utils/form'; +import { Editor, loader } from '@monaco-editor/react'; +import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; +import { X } from 'lucide-react'; +import { ReactNode, useCallback } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { TypesWithArray } from '../../constant'; +import { buildConversationVariableSelectOptions } from '../../utils'; +import { DynamicFormHeader } from '../components/dynamic-fom-header'; +import { QueryVariable } from '../components/query-variable'; + +loader.config({ paths: { vs: '/vs' } }); + +enum InputMode { + Constant = 'constant', + Variable = 'variable', +} + +const InputModeOptions = buildOptions(InputMode); + +type SelectKeysProps = { + name: string; + label: ReactNode; + tooltip?: string; + keyField?: string; + valueField?: string; + operatorField?: string; +}; + +type RadioGroupProps = React.ComponentProps; + +type RadioButtonProps = Partial< + Omit & { + onChange: RadioGroupProps['onValueChange']; + } +>; + +function RadioButton({ value, onChange }: RadioButtonProps) { + return ( + +
+ + +
+
+ + +
+
+ ); +} + +const VariableTypeOptions = buildConversationVariableSelectOptions(); + +const modeField = 'mode'; + +const ConstantValueMap = { + [TypesWithArray.Boolean]: 'yes', + [TypesWithArray.Number]: 0, + [TypesWithArray.String]: '', + [TypesWithArray.ArrayBoolean]: '[]', + [TypesWithArray.ArrayNumber]: '[]', + [TypesWithArray.ArrayString]: '[]', + [TypesWithArray.ArrayObject]: '[]', + [TypesWithArray.Object]: '{}', +}; + +export function DynamicVariables({ + name, + label, + tooltip, + keyField = 'variable', + valueField = 'parameter', + operatorField = 'operator', +}: SelectKeysProps) { + const form = useFormContext(); + const isDarkTheme = useIsDarkTheme(); + + const { fields, remove, append } = useFieldArray({ + name: name, + control: form.control, + }); + + const initializeValue = useCallback( + (mode: string, variableType: string, valueFieldAlias: string) => { + if (mode === InputMode.Variable) { + form.setValue(valueFieldAlias, '', { shouldDirty: true }); + } else { + const val = ConstantValueMap[variableType as TypesWithArray]; + form.setValue(valueFieldAlias, val, { shouldDirty: true }); + } + }, + [form], + ); + + const handleModeChange = useCallback( + (mode: string, valueFieldAlias: string, operatorFieldAlias: string) => { + const variableType = form.getValues(operatorFieldAlias); + initializeValue(mode, variableType, valueFieldAlias); + // if (mode === InputMode.Variable) { + // form.setValue(valueFieldAlias, ''); + // } else { + // const val = ConstantValueMap[variableType as TypesWithArray]; + // form.setValue(valueFieldAlias, val); + // } + }, + [form, initializeValue], + ); + + const handleVariableTypeChange = useCallback( + (variableType: string, valueFieldAlias: string, modeFieldAlias: string) => { + const mode = form.getValues(modeFieldAlias); + + initializeValue(mode, variableType, valueFieldAlias); + }, + [form, initializeValue], + ); + + const renderParameter = useCallback( + (operatorFieldName: string, modeFieldName: string) => { + const mode = form.getValues(modeFieldName); + const logicalOperator = form.getValues(operatorFieldName); + + if (mode === InputMode.Constant) { + if (logicalOperator === TypesWithArray.Boolean) { + return ; + } + + if (logicalOperator === TypesWithArray.Number) { + return ; + } + + if (logicalOperator === TypesWithArray.String) { + return ; + } + + return ( + + ); + } + + return ( + + ); + }, + [form, isDarkTheme], + ); + + return ( +
+ + append({ + [keyField]: '', + [valueField]: '', + [modeField]: InputMode.Constant, + [operatorField]: TypesWithArray.String, + }) + } + > +
+ {fields.map((field, index) => { + const keyFieldAlias = `${name}.${index}.${keyField}`; + const valueFieldAlias = `${name}.${index}.${valueField}`; + const operatorFieldAlias = `${name}.${index}.${operatorField}`; + const modeFieldAlias = `${name}.${index}.${modeField}`; + + return ( +
+
+
+ + + + + + {(field) => ( + { + handleVariableTypeChange( + val, + valueFieldAlias, + modeFieldAlias, + ); + field.onChange(val); + }} + options={VariableTypeOptions} + > + )} + + + + {(field) => ( + { + handleModeChange( + val, + valueFieldAlias, + operatorFieldAlias, + ); + field.onChange(val); + }} + options={InputModeOptions} + > + )} + +
+ + {renderParameter(operatorFieldAlias, modeFieldAlias)} + +
+ + +
+ ); + })} +
+
+ ); +} diff --git a/web/src/pages/agent/form/iteration-form/index.tsx b/web/src/pages/agent/form/iteration-form/index.tsx index 6132b7f5e..c528fa720 100644 --- a/web/src/pages/agent/form/iteration-form/index.tsx +++ b/web/src/pages/agent/form/iteration-form/index.tsx @@ -1,4 +1,3 @@ -import { FormContainer } from '@/components/form-container'; import { Form } from '@/components/ui/form'; import { zodResolver } from '@hookform/resolvers/zod'; import { memo, useMemo } from 'react'; @@ -10,12 +9,21 @@ import { FormWrapper } from '../components/form-wrapper'; import { Output } from '../components/output'; import { QueryVariable } from '../components/query-variable'; import { DynamicOutput } from './dynamic-output'; +import { DynamicVariables } from './dynamic-variables'; import { OutputArray } from './interface'; import { useValues } from './use-values'; import { useWatchFormChange } from './use-watch-form-change'; const FormSchema = z.object({ query: z.string().optional(), + variables: z.array( + z.object({ + variable: z.string().optional(), + operator: z.string().optional(), + parameter: z.string().or(z.number()).or(z.boolean()).optional(), + mode: z.string(), + }), + ), outputs: z.array(z.object({ name: z.string(), value: z.any() })).optional(), }); @@ -41,12 +49,11 @@ function IterationForm({ node }: INextOperatorForm) { return (
- - - + + diff --git a/web/src/pages/agent/form/iteration-form/use-build-logical-options.ts b/web/src/pages/agent/form/iteration-form/use-build-logical-options.ts new file mode 100644 index 000000000..a7f960e98 --- /dev/null +++ b/web/src/pages/agent/form/iteration-form/use-build-logical-options.ts @@ -0,0 +1,59 @@ +import { buildOptions } from '@/utils/form'; +import { camelCase } from 'lodash'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + JsonSchemaDataType, + VariableAssignerLogicalArrayOperator, + VariableAssignerLogicalNumberOperator, + VariableAssignerLogicalNumberOperatorLabelMap, + VariableAssignerLogicalOperator, +} from '../../constant'; + +export function useBuildLogicalOptions() { + const { t } = useTranslation(); + + const buildVariableAssignerLogicalOptions = useCallback( + (record: Record) => { + return buildOptions( + record, + t, + 'flow.variableAssignerLogicalOperatorOptions', + true, + ); + }, + [t], + ); + + const buildLogicalOptions = useCallback( + (type: string) => { + if ( + type?.toLowerCase().startsWith(JsonSchemaDataType.Array.toLowerCase()) + ) { + return buildVariableAssignerLogicalOptions( + VariableAssignerLogicalArrayOperator, + ); + } + + if (type === JsonSchemaDataType.Number) { + return Object.values(VariableAssignerLogicalNumberOperator).map( + (val) => ({ + label: t( + `flow.variableAssignerLogicalOperatorOptions.${camelCase(VariableAssignerLogicalNumberOperatorLabelMap[val as keyof typeof VariableAssignerLogicalNumberOperatorLabelMap] || val)}`, + ), + value: val, + }), + ); + } + + return buildVariableAssignerLogicalOptions( + VariableAssignerLogicalOperator, + ); + }, + [buildVariableAssignerLogicalOptions, t], + ); + + return { + buildLogicalOptions, + }; +} diff --git a/web/src/pages/agent/gobal-variable-sheet/constant.ts b/web/src/pages/agent/gobal-variable-sheet/constant.ts index 72a7d6342..935540c15 100644 --- a/web/src/pages/agent/gobal-variable-sheet/constant.ts +++ b/web/src/pages/agent/gobal-variable-sheet/constant.ts @@ -1,7 +1,7 @@ import { FormFieldConfig, FormFieldType } from '@/components/dynamic-form'; -import { buildSelectOptions } from '@/utils/component-util'; import { t } from 'i18next'; import { TypesWithArray } from '../constant'; +import { buildConversationVariableSelectOptions } from '../utils'; export { TypesWithArray } from '../constant'; // const TypesWithoutArray = Object.values(JsonSchemaDataType).filter( // (item) => item !== JsonSchemaDataType.Array, @@ -29,7 +29,7 @@ export const GlobalFormFields = [ placeholder: '', required: true, type: FormFieldType.Select, - options: buildSelectOptions(Object.values(TypesWithArray)), + options: buildConversationVariableSelectOptions(), }, { label: t('flow.defaultValue'), diff --git a/web/src/pages/agent/utils.ts b/web/src/pages/agent/utils.ts index f4e4a4b1d..6ae2935b4 100644 --- a/web/src/pages/agent/utils.ts +++ b/web/src/pages/agent/utils.ts @@ -7,6 +7,7 @@ import { ICategorizeItemResult, } from '@/interfaces/database/agent'; import { DSLComponents, RAGFlowNodeType } from '@/interfaces/database/flow'; +import { buildSelectOptions } from '@/utils/component-util'; import { removeUselessFieldsFromValues } from '@/utils/form'; import { Edge, Node, XYPosition } from '@xyflow/react'; import { FormInstance, FormListFieldData } from 'antd'; @@ -30,6 +31,7 @@ import { NoDebugOperatorsList, NodeHandleId, Operator, + TypesWithArray, } from './constant'; import { DataOperationsFormSchemaType } from './form/data-operations-form'; import { ExtractorFormSchemaType } from './form/extractor-form'; @@ -766,3 +768,7 @@ export function buildBeginQueryWithObject( export function getArrayElementType(type: string) { return typeof type === 'string' ? type.match(/<([^>]+)>/)?.at(1) ?? '' : ''; } + +export function buildConversationVariableSelectOptions() { + return buildSelectOptions(Object.values(TypesWithArray)); +}