mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-02-06 10:35:06 +08:00
### What problem does this PR solve? Feat: Add loop operator node. #10427 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
229
web/src/pages/agent/form/loop-form/dynamic-variables.tsx
Normal file
229
web/src/pages/agent/form/loop-form/dynamic-variables.tsx
Normal file
@ -0,0 +1,229 @@
|
||||
import { BoolSegmented } from '@/components/bool-segmented';
|
||||
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 { Separator } from '@/components/ui/separator';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Editor, loader } from '@monaco-editor/react';
|
||||
import { X } from 'lucide-react';
|
||||
import { ReactNode, useCallback } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { InputMode, TypesWithArray } from '../../constant';
|
||||
import {
|
||||
InputModeOptions,
|
||||
buildConversationVariableSelectOptions,
|
||||
} from '../../utils';
|
||||
import { DynamicFormHeader } from '../components/dynamic-fom-header';
|
||||
import { QueryVariable } from '../components/query-variable';
|
||||
import { useInitializeConditions } from './use-watch-form-change';
|
||||
|
||||
loader.config({ paths: { vs: '/vs' } });
|
||||
|
||||
type SelectKeysProps = {
|
||||
name: string;
|
||||
label: ReactNode;
|
||||
tooltip?: string;
|
||||
keyField?: string;
|
||||
valueField?: string;
|
||||
operatorField?: string;
|
||||
nodeId?: string;
|
||||
};
|
||||
|
||||
const VariableTypeOptions = buildConversationVariableSelectOptions();
|
||||
|
||||
const modeField = 'input_mode';
|
||||
|
||||
const ConstantValueMap = {
|
||||
[TypesWithArray.Boolean]: true,
|
||||
[TypesWithArray.Number]: 0,
|
||||
[TypesWithArray.String]: '',
|
||||
[TypesWithArray.ArrayBoolean]: '[]',
|
||||
[TypesWithArray.ArrayNumber]: '[]',
|
||||
[TypesWithArray.ArrayString]: '[]',
|
||||
[TypesWithArray.ArrayObject]: '[]',
|
||||
[TypesWithArray.Object]: '{}',
|
||||
};
|
||||
|
||||
export function DynamicVariables({
|
||||
name,
|
||||
label,
|
||||
tooltip,
|
||||
keyField = 'variable',
|
||||
valueField = 'value',
|
||||
operatorField = 'type',
|
||||
nodeId,
|
||||
}: SelectKeysProps) {
|
||||
const form = useFormContext();
|
||||
const isDarkTheme = useIsDarkTheme();
|
||||
|
||||
const { fields, remove, append } = useFieldArray({
|
||||
name: name,
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const { initializeVariableRelatedConditions } =
|
||||
useInitializeConditions(nodeId);
|
||||
|
||||
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);
|
||||
},
|
||||
[form, initializeValue],
|
||||
);
|
||||
|
||||
const handleVariableTypeChange = useCallback(
|
||||
(
|
||||
variableType: string,
|
||||
valueFieldAlias: string,
|
||||
modeFieldAlias: string,
|
||||
keyFieldAlias: string,
|
||||
) => {
|
||||
const mode = form.getValues(modeFieldAlias);
|
||||
|
||||
initializeVariableRelatedConditions(
|
||||
form.getValues(keyFieldAlias),
|
||||
variableType,
|
||||
);
|
||||
|
||||
initializeValue(mode, variableType, valueFieldAlias);
|
||||
},
|
||||
[form, initializeValue, initializeVariableRelatedConditions],
|
||||
);
|
||||
|
||||
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 <BoolSegmented></BoolSegmented>;
|
||||
}
|
||||
|
||||
if (logicalOperator === TypesWithArray.Number) {
|
||||
return <Input className="w-full" type="number"></Input>;
|
||||
}
|
||||
|
||||
if (logicalOperator === TypesWithArray.String) {
|
||||
return <Textarea></Textarea>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Editor
|
||||
height={300}
|
||||
theme={isDarkTheme ? 'vs-dark' : 'vs'}
|
||||
language={'json'}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
automaticLayout: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryVariable
|
||||
types={[logicalOperator]}
|
||||
hideLabel
|
||||
pureQuery
|
||||
></QueryVariable>
|
||||
);
|
||||
},
|
||||
[form, isDarkTheme],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="space-y-2">
|
||||
<DynamicFormHeader
|
||||
label={label}
|
||||
tooltip={tooltip}
|
||||
onClick={() =>
|
||||
append({
|
||||
[keyField]: '',
|
||||
[valueField]: '',
|
||||
[modeField]: InputMode.Constant,
|
||||
[operatorField]: TypesWithArray.String,
|
||||
})
|
||||
}
|
||||
></DynamicFormHeader>
|
||||
<div className="space-y-5">
|
||||
{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 (
|
||||
<section key={field.id} className="flex gap-2">
|
||||
<div className="flex-1 space-y-3 min-w-0">
|
||||
<div className="flex items-center">
|
||||
<RAGFlowFormItem name={keyFieldAlias} className="flex-1 ">
|
||||
<KeyInput></KeyInput>
|
||||
</RAGFlowFormItem>
|
||||
<Separator className="w-2" />
|
||||
<RAGFlowFormItem name={operatorFieldAlias} className="flex-1">
|
||||
{(field) => (
|
||||
<SelectWithSearch
|
||||
value={field.value}
|
||||
onChange={(val) => {
|
||||
handleVariableTypeChange(
|
||||
val,
|
||||
valueFieldAlias,
|
||||
modeFieldAlias,
|
||||
keyFieldAlias,
|
||||
);
|
||||
field.onChange(val);
|
||||
}}
|
||||
options={VariableTypeOptions}
|
||||
></SelectWithSearch>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
<Separator className="w-2" />
|
||||
<RAGFlowFormItem name={modeFieldAlias} className="flex-1">
|
||||
{(field) => (
|
||||
<SelectWithSearch
|
||||
value={field.value}
|
||||
onChange={(val) => {
|
||||
handleModeChange(
|
||||
val,
|
||||
valueFieldAlias,
|
||||
operatorFieldAlias,
|
||||
);
|
||||
field.onChange(val);
|
||||
}}
|
||||
options={InputModeOptions}
|
||||
></SelectWithSearch>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
</div>
|
||||
<RAGFlowFormItem name={valueFieldAlias} className="w-full">
|
||||
{renderParameter(operatorFieldAlias, modeFieldAlias)}
|
||||
</RAGFlowFormItem>
|
||||
</div>
|
||||
|
||||
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||
<X />
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
52
web/src/pages/agent/form/loop-form/index.tsx
Normal file
52
web/src/pages/agent/form/loop-form/index.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { SliderInputFormField } from '@/components/slider-input-form-field';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { FormLayout } from '@/constants/form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { memo } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { initialLoopValues } from '../../constant';
|
||||
import { INextOperatorForm } from '../../interface';
|
||||
import { FormWrapper } from '../components/form-wrapper';
|
||||
import { DynamicVariables } from './dynamic-variables';
|
||||
import { LoopTerminationCondition } from './loop-termination-condition';
|
||||
import { FormSchema, LoopFormSchemaType } from './schema';
|
||||
import { useFormValues } from './use-values';
|
||||
import { useWatchFormChange } from './use-watch-form-change';
|
||||
|
||||
function LoopForm({ node }: INextOperatorForm) {
|
||||
const defaultValues = useFormValues(initialLoopValues, node);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<LoopFormSchemaType>({
|
||||
defaultValues: defaultValues,
|
||||
resolver: zodResolver(FormSchema),
|
||||
});
|
||||
|
||||
useWatchFormChange(node?.id, form);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<FormWrapper>
|
||||
<DynamicVariables
|
||||
name="loop_variables"
|
||||
label={t('flow.loopVariables')}
|
||||
nodeId={node?.id}
|
||||
></DynamicVariables>
|
||||
<LoopTerminationCondition
|
||||
label={t('flow.loopTerminationCondition')}
|
||||
nodeId={node?.id}
|
||||
></LoopTerminationCondition>
|
||||
<SliderInputFormField
|
||||
min={1}
|
||||
max={100}
|
||||
name="maximum_loop_count"
|
||||
label={t('flow.maximumLoopCount')}
|
||||
layout={FormLayout.Vertical}
|
||||
></SliderInputFormField>
|
||||
</FormWrapper>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LoopForm);
|
||||
@ -0,0 +1,316 @@
|
||||
import { BoolSegmented } from '@/components/bool-segmented';
|
||||
import { LogicalOperator } from '@/components/logical-operator';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { ComparisonOperator, SwitchLogicOperator } from '@/constants/agent';
|
||||
import { loader } from '@monaco-editor/react';
|
||||
import { toLower } from 'lodash';
|
||||
import { X } from 'lucide-react';
|
||||
import { ReactNode, useCallback, useMemo } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import {
|
||||
AgentVariableType,
|
||||
InputMode,
|
||||
JsonSchemaDataType,
|
||||
} from '../../constant';
|
||||
import { useFilterChildNodeIds } from '../../hooks/use-filter-child-node-ids';
|
||||
import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query';
|
||||
import { InputModeOptions } from '../../utils';
|
||||
import { DynamicFormHeader } from '../components/dynamic-fom-header';
|
||||
import { QueryVariable } from '../components/query-variable';
|
||||
import { LoopFormSchemaType } from './schema';
|
||||
import { useBuildLogicalOptions } from './use-build-logical-options';
|
||||
import {
|
||||
ConditionKeyType,
|
||||
ConditionModeType,
|
||||
ConditionOperatorType,
|
||||
ConditionValueType,
|
||||
useInitializeConditions,
|
||||
} from './use-watch-form-change';
|
||||
|
||||
loader.config({ paths: { vs: '/vs' } });
|
||||
|
||||
const VariablesExceptOperatorOutputs = [AgentVariableType.Conversation];
|
||||
|
||||
type LoopTerminationConditionProps = {
|
||||
label: ReactNode;
|
||||
tooltip?: string;
|
||||
keyField?: string;
|
||||
valueField?: string;
|
||||
operatorField?: string;
|
||||
modeField?: string;
|
||||
nodeId?: string;
|
||||
};
|
||||
|
||||
const EmptyFields = [ComparisonOperator.Empty, ComparisonOperator.NotEmpty];
|
||||
|
||||
const LogicalOperatorFieldName = 'logical_operator';
|
||||
|
||||
const name = 'loop_termination_condition';
|
||||
|
||||
export function LoopTerminationCondition({
|
||||
label,
|
||||
tooltip,
|
||||
keyField = 'variable',
|
||||
valueField = 'value',
|
||||
operatorField = 'operator',
|
||||
modeField = 'input_mode',
|
||||
nodeId,
|
||||
}: LoopTerminationConditionProps) {
|
||||
const form = useFormContext<LoopFormSchemaType>();
|
||||
const childNodeIds = useFilterChildNodeIds(nodeId);
|
||||
|
||||
const nodeIds = useMemo(() => {
|
||||
if (!nodeId) return [];
|
||||
return [nodeId, ...childNodeIds];
|
||||
}, [childNodeIds, nodeId]);
|
||||
|
||||
const { getType } = useGetVariableLabelOrTypeByValue({
|
||||
nodeIds: nodeIds,
|
||||
variablesExceptOperatorOutputs: VariablesExceptOperatorOutputs,
|
||||
});
|
||||
|
||||
const {
|
||||
initializeConditionMode,
|
||||
initializeConditionOperator,
|
||||
initializeConditionValue,
|
||||
} = useInitializeConditions(nodeId);
|
||||
|
||||
const { fields, remove, append } = useFieldArray({
|
||||
name: name,
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const { buildLogicalOptions } = useBuildLogicalOptions();
|
||||
|
||||
const getVariableType = useCallback(
|
||||
(keyFieldName: ConditionKeyType) => {
|
||||
const key = form.getValues(keyFieldName);
|
||||
return toLower(getType(key));
|
||||
},
|
||||
[form, getType],
|
||||
);
|
||||
|
||||
const initializeMode = useCallback(
|
||||
(modeFieldAlias: ConditionModeType, keyFieldAlias: ConditionKeyType) => {
|
||||
const keyType = getVariableType(keyFieldAlias);
|
||||
|
||||
initializeConditionMode(modeFieldAlias, keyType);
|
||||
},
|
||||
[getVariableType, initializeConditionMode],
|
||||
);
|
||||
|
||||
const initializeValue = useCallback(
|
||||
(valueFieldAlias: ConditionValueType, keyFieldAlias: ConditionKeyType) => {
|
||||
const keyType = getVariableType(keyFieldAlias);
|
||||
|
||||
initializeConditionValue(valueFieldAlias, keyType);
|
||||
},
|
||||
[getVariableType, initializeConditionValue],
|
||||
);
|
||||
|
||||
const handleVariableChange = useCallback(
|
||||
(
|
||||
operatorFieldAlias: ConditionOperatorType,
|
||||
valueFieldAlias: ConditionValueType,
|
||||
keyFieldAlias: ConditionKeyType,
|
||||
modeFieldAlias: ConditionModeType,
|
||||
) => {
|
||||
return () => {
|
||||
initializeConditionOperator(
|
||||
operatorFieldAlias,
|
||||
getVariableType(keyFieldAlias),
|
||||
);
|
||||
|
||||
initializeMode(modeFieldAlias, keyFieldAlias);
|
||||
|
||||
initializeValue(valueFieldAlias, keyFieldAlias);
|
||||
};
|
||||
},
|
||||
[
|
||||
getVariableType,
|
||||
initializeConditionOperator,
|
||||
initializeMode,
|
||||
initializeValue,
|
||||
],
|
||||
);
|
||||
|
||||
const handleOperatorChange = useCallback(
|
||||
(
|
||||
valueFieldAlias: ConditionValueType,
|
||||
keyFieldAlias: ConditionKeyType,
|
||||
modeFieldAlias: ConditionModeType,
|
||||
) => {
|
||||
initializeMode(modeFieldAlias, keyFieldAlias);
|
||||
initializeValue(valueFieldAlias, keyFieldAlias);
|
||||
},
|
||||
[initializeMode, initializeValue],
|
||||
);
|
||||
|
||||
const handleModeChange = useCallback(
|
||||
(mode: string, valueFieldAlias: ConditionValueType) => {
|
||||
form.setValue(valueFieldAlias, mode === InputMode.Constant ? 0 : '', {
|
||||
shouldDirty: true,
|
||||
});
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const renderParameterPanel = useCallback(
|
||||
(
|
||||
keyFieldName: ConditionKeyType,
|
||||
valueFieldAlias: ConditionValueType,
|
||||
modeFieldAlias: ConditionModeType,
|
||||
operatorFieldAlias: ConditionOperatorType,
|
||||
) => {
|
||||
const type = getVariableType(keyFieldName);
|
||||
const mode = form.getValues(modeFieldAlias);
|
||||
const operator = form.getValues(operatorFieldAlias);
|
||||
|
||||
if (EmptyFields.includes(operator as ComparisonOperator)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type === JsonSchemaDataType.Number) {
|
||||
return (
|
||||
<section className="flex items-center gap-1">
|
||||
<RAGFlowFormItem name={modeFieldAlias}>
|
||||
{(field) => (
|
||||
<SelectWithSearch
|
||||
value={field.value}
|
||||
onChange={(val) => {
|
||||
handleModeChange(val, valueFieldAlias);
|
||||
field.onChange(val);
|
||||
}}
|
||||
options={InputModeOptions}
|
||||
></SelectWithSearch>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
<Separator className="w-2" />
|
||||
{mode === InputMode.Constant ? (
|
||||
<RAGFlowFormItem name={valueFieldAlias}>
|
||||
<Input type="number" />
|
||||
</RAGFlowFormItem>
|
||||
) : (
|
||||
<QueryVariable
|
||||
types={[JsonSchemaDataType.Number]}
|
||||
hideLabel
|
||||
pureQuery
|
||||
name={valueFieldAlias}
|
||||
className="flex-1 min-w-0"
|
||||
></QueryVariable>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === JsonSchemaDataType.Boolean) {
|
||||
return (
|
||||
<RAGFlowFormItem name={valueFieldAlias} className="w-full">
|
||||
<BoolSegmented></BoolSegmented>
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RAGFlowFormItem name={valueFieldAlias} className="w-full">
|
||||
<Input />
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
},
|
||||
[form, getVariableType, handleModeChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="space-y-2">
|
||||
<DynamicFormHeader
|
||||
label={label}
|
||||
tooltip={tooltip}
|
||||
onClick={() => {
|
||||
if (fields.length === 1) {
|
||||
form.setValue(LogicalOperatorFieldName, SwitchLogicOperator.And);
|
||||
}
|
||||
append({ [keyField]: '', [valueField]: '' });
|
||||
}}
|
||||
></DynamicFormHeader>
|
||||
<section className="flex">
|
||||
{fields.length > 1 && (
|
||||
<LogicalOperator name={LogicalOperatorFieldName}></LogicalOperator>
|
||||
)}
|
||||
<div className="space-y-5 flex-1 min-w-0">
|
||||
{fields.map((field, index) => {
|
||||
const keyFieldAlias =
|
||||
`${name}.${index}.${keyField}` as ConditionKeyType;
|
||||
const valueFieldAlias =
|
||||
`${name}.${index}.${valueField}` as ConditionValueType;
|
||||
const operatorFieldAlias =
|
||||
`${name}.${index}.${operatorField}` as ConditionOperatorType;
|
||||
const modeFieldAlias =
|
||||
`${name}.${index}.${modeField}` as ConditionModeType;
|
||||
|
||||
return (
|
||||
<section key={field.id} className="flex gap-2">
|
||||
<div className="flex-1 space-y-3 min-w-0">
|
||||
<div className="flex items-center">
|
||||
<QueryVariable
|
||||
name={keyFieldAlias}
|
||||
hideLabel
|
||||
className="flex-1 min-w-0"
|
||||
onChange={handleVariableChange(
|
||||
operatorFieldAlias,
|
||||
valueFieldAlias,
|
||||
keyFieldAlias,
|
||||
modeFieldAlias,
|
||||
)}
|
||||
nodeIds={nodeIds}
|
||||
variablesExceptOperatorOutputs={
|
||||
VariablesExceptOperatorOutputs
|
||||
}
|
||||
></QueryVariable>
|
||||
|
||||
<Separator className="w-2" />
|
||||
|
||||
<RAGFlowFormItem
|
||||
name={operatorFieldAlias}
|
||||
className="w-1/3"
|
||||
>
|
||||
{({ onChange, value }) => (
|
||||
<SelectWithSearch
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
handleOperatorChange(
|
||||
valueFieldAlias,
|
||||
keyFieldAlias,
|
||||
modeFieldAlias,
|
||||
);
|
||||
onChange(val);
|
||||
}}
|
||||
options={buildLogicalOptions(
|
||||
getVariableType(keyFieldAlias),
|
||||
)}
|
||||
></SelectWithSearch>
|
||||
)}
|
||||
</RAGFlowFormItem>
|
||||
</div>
|
||||
{renderParameterPanel(
|
||||
keyFieldAlias,
|
||||
valueFieldAlias,
|
||||
modeFieldAlias,
|
||||
operatorFieldAlias,
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||
<X />
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
24
web/src/pages/agent/form/loop-form/schema.ts
Normal file
24
web/src/pages/agent/form/loop-form/schema.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const FormSchema = z.object({
|
||||
loop_variables: z.array(
|
||||
z.object({
|
||||
variable: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
value: z.string().or(z.number()).or(z.boolean()).optional(),
|
||||
input_mode: z.string(),
|
||||
}),
|
||||
),
|
||||
logical_operator: z.string(),
|
||||
loop_termination_condition: z.array(
|
||||
z.object({
|
||||
variable: z.string().optional(),
|
||||
operator: z.string().optional(),
|
||||
value: z.string().or(z.number()).or(z.boolean()).optional(),
|
||||
input_mode: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
maximum_loop_count: z.number(),
|
||||
});
|
||||
|
||||
export type LoopFormSchemaType = z.infer<typeof FormSchema>;
|
||||
@ -0,0 +1,27 @@
|
||||
import { SwitchOperatorOptions } from '@/constants/agent';
|
||||
import { camelCase, toLower } from 'lodash';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoopTerminationStringComparisonOperatorMap } from '../../constant';
|
||||
|
||||
export function useBuildLogicalOptions() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const buildLogicalOptions = useCallback(
|
||||
(type: string) => {
|
||||
return LoopTerminationStringComparisonOperatorMap[
|
||||
toLower(type) as keyof typeof LoopTerminationStringComparisonOperatorMap
|
||||
]?.map((x) => ({
|
||||
label: t(
|
||||
`flow.switchOperatorOptions.${camelCase(SwitchOperatorOptions.find((y) => y.value === x)?.label || x)}`,
|
||||
),
|
||||
value: x,
|
||||
}));
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
return {
|
||||
buildLogicalOptions,
|
||||
};
|
||||
}
|
||||
20
web/src/pages/agent/form/loop-form/use-values.ts
Normal file
20
web/src/pages/agent/form/loop-form/use-values.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { isEmpty, omit } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useFormValues(
|
||||
defaultValues: Record<string, any>,
|
||||
node?: RAGFlowNodeType,
|
||||
) {
|
||||
const values = useMemo(() => {
|
||||
const formData = node?.data?.form;
|
||||
|
||||
if (isEmpty(formData)) {
|
||||
return omit(defaultValues, 'outputs');
|
||||
}
|
||||
|
||||
return omit(formData, 'outputs');
|
||||
}, [defaultValues, node?.data?.form]);
|
||||
|
||||
return values;
|
||||
}
|
||||
116
web/src/pages/agent/form/loop-form/use-watch-form-change.ts
Normal file
116
web/src/pages/agent/form/loop-form/use-watch-form-change.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import { JsonSchemaDataType } from '@/constants/agent';
|
||||
import { buildVariableValue } from '@/utils/canvas-util';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { UseFormReturn, useFormContext, useWatch } from 'react-hook-form';
|
||||
import { InputMode } from '../../constant';
|
||||
import { IOutputs } from '../../interface';
|
||||
import useGraphStore from '../../store';
|
||||
import { LoopFormSchemaType } from './schema';
|
||||
import { useBuildLogicalOptions } from './use-build-logical-options';
|
||||
|
||||
export function useWatchFormChange(
|
||||
id?: string,
|
||||
form?: UseFormReturn<LoopFormSchemaType>,
|
||||
) {
|
||||
let values = useWatch({ control: form?.control });
|
||||
const { replaceNodeForm } = useGraphStore((state) => state);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
let nextValues = {
|
||||
...values,
|
||||
outputs: values.loop_variables?.reduce((pre, cur) => {
|
||||
const variable = cur.variable;
|
||||
if (variable) {
|
||||
pre[variable] = {
|
||||
type: cur.type,
|
||||
value: '',
|
||||
};
|
||||
}
|
||||
return pre;
|
||||
}, {} as IOutputs),
|
||||
};
|
||||
|
||||
replaceNodeForm(id, nextValues);
|
||||
}
|
||||
}, [form?.formState.isDirty, id, replaceNodeForm, values]);
|
||||
}
|
||||
|
||||
type ConditionPrefixType = `loop_termination_condition.${number}.`;
|
||||
export type ConditionKeyType = `${ConditionPrefixType}variable`;
|
||||
export type ConditionModeType = `${ConditionPrefixType}input_mode`;
|
||||
export type ConditionValueType = `${ConditionPrefixType}value`;
|
||||
export type ConditionOperatorType = `${ConditionPrefixType}operator`;
|
||||
export function useInitializeConditions(id?: string) {
|
||||
const form = useFormContext<LoopFormSchemaType>();
|
||||
const { buildLogicalOptions } = useBuildLogicalOptions();
|
||||
|
||||
const initializeConditionMode = useCallback(
|
||||
(modeFieldAlias: ConditionModeType, keyType: string) => {
|
||||
if (keyType === JsonSchemaDataType.Number) {
|
||||
form.setValue(modeFieldAlias, InputMode.Constant, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const initializeConditionValue = useCallback(
|
||||
(valueFieldAlias: ConditionValueType, keyType: string) => {
|
||||
let initialValue: string | boolean | number = '';
|
||||
|
||||
if (keyType === JsonSchemaDataType.Number) {
|
||||
initialValue = 0;
|
||||
} else if (keyType === JsonSchemaDataType.Boolean) {
|
||||
initialValue = true;
|
||||
}
|
||||
|
||||
form.setValue(valueFieldAlias, initialValue, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const initializeConditionOperator = useCallback(
|
||||
(operatorFieldAlias: ConditionOperatorType, keyType: string) => {
|
||||
const logicalOptions = buildLogicalOptions(keyType);
|
||||
|
||||
form.setValue(operatorFieldAlias, logicalOptions?.at(0)?.value, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
},
|
||||
[buildLogicalOptions, form],
|
||||
);
|
||||
|
||||
const initializeVariableRelatedConditions = useCallback(
|
||||
(variable: string, variableType: string) => {
|
||||
form?.getValues('loop_termination_condition').forEach((x, idx) => {
|
||||
if (variable && x.variable === buildVariableValue(variable, id)) {
|
||||
const prefix: ConditionPrefixType = `loop_termination_condition.${idx}.`;
|
||||
initializeConditionMode(`${prefix}input_mode`, variableType);
|
||||
initializeConditionValue(`${prefix}value`, variableType);
|
||||
initializeConditionOperator(`${prefix}operator`, variableType);
|
||||
}
|
||||
});
|
||||
},
|
||||
[
|
||||
form,
|
||||
id,
|
||||
initializeConditionMode,
|
||||
initializeConditionOperator,
|
||||
initializeConditionValue,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
initializeVariableRelatedConditions,
|
||||
initializeConditionMode,
|
||||
initializeConditionValue,
|
||||
initializeConditionOperator,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user