Feat: Add loop operator node. #10427 (#11449)

### 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:
balibabu
2025-11-27 15:55:46 +08:00
committed by GitHub
parent b6314164c5
commit f57f32cf3a
51 changed files with 1246 additions and 1138 deletions

View File

@ -1,22 +0,0 @@
import { TopNFormField } from '@/components/top-n-item';
import { Form } from '@/components/ui/form';
import { INextOperatorForm } from '../../interface';
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
const AkShareForm = ({ form, node }: INextOperatorForm) => {
return (
<Form {...form}>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
}}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNFormField max={99}></TopNFormField>
</form>
</Form>
);
};
export default AkShareForm;

View File

@ -1,127 +0,0 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Collapse, Flex, Form, Input, Select } from 'antd';
import { PropsWithChildren, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useBuildVariableOptions } from '../../hooks/use-get-begin-query';
import styles from './index.less';
interface IProps {
node?: RAGFlowNodeType;
}
enum VariableType {
Reference = 'reference',
Input = 'input',
}
const getVariableName = (type: string) =>
type === VariableType.Reference ? 'component_id' : 'value';
const DynamicVariableForm = ({ node }: IProps) => {
const { t } = useTranslation();
const valueOptions = useBuildVariableOptions(node?.id, node?.parentId);
const form = Form.useFormInstance();
const options = [
{ value: VariableType.Reference, label: t('flow.reference') },
{ value: VariableType.Input, label: t('flow.text') },
];
const handleTypeChange = useCallback(
(name: number) => () => {
setTimeout(() => {
form.setFieldValue(['query', name, 'component_id'], undefined);
form.setFieldValue(['query', name, 'value'], undefined);
}, 0);
},
[form],
);
return (
<Form.List name="query">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Flex key={key} gap={10} align={'baseline'}>
<Form.Item
{...restField}
name={[name, 'type']}
className={styles.variableType}
>
<Select
options={options}
onChange={handleTypeChange(name)}
></Select>
</Form.Item>
<Form.Item noStyle dependencies={[name, 'type']}>
{({ getFieldValue }) => {
const type = getFieldValue(['query', name, 'type']);
return (
<Form.Item
{...restField}
name={[name, getVariableName(type)]}
className={styles.variableValue}
>
{type === VariableType.Reference ? (
<Select
placeholder={t('common.pleaseSelect')}
options={valueOptions}
></Select>
) : (
<Input placeholder={t('common.pleaseInput')} />
)}
</Form.Item>
);
}}
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</Flex>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => add({ type: VariableType.Reference })}
block
icon={<PlusOutlined />}
className={styles.addButton}
>
{t('flow.addVariable')}
</Button>
</Form.Item>
</>
)}
</Form.List>
);
};
export function FormCollapse({
children,
title,
}: PropsWithChildren<{ title: string }>) {
return (
<Collapse
className={styles.dynamicInputVariable}
defaultActiveKey={['1']}
items={[
{
key: '1',
label: <span className={styles.title}>{title}</span>,
children,
},
]}
/>
);
}
const DynamicInputVariable = ({ node }: IProps) => {
const { t } = useTranslation();
return (
<FormCollapse title={t('flow.input')}>
<DynamicVariableForm node={node}></DynamicVariableForm>
</FormCollapse>
);
};
export default DynamicInputVariable;

View File

@ -1,135 +0,0 @@
'use client';
import { SideDown } from '@/assets/icon/next-icon';
import { Button } from '@/components/ui/button';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { Plus, Trash2 } from 'lucide-react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useBuildVariableOptions } from '../../hooks/use-get-begin-query';
interface IProps {
node?: RAGFlowNodeType;
}
enum VariableType {
Reference = 'reference',
Input = 'input',
}
const getVariableName = (type: string) =>
type === VariableType.Reference ? 'component_id' : 'value';
export function DynamicVariableForm({ node }: IProps) {
const { t } = useTranslation();
const form = useFormContext();
const { fields, remove, append } = useFieldArray({
name: 'query',
control: form.control,
});
const valueOptions = useBuildVariableOptions(node?.id, node?.parentId);
const options = [
{ value: VariableType.Reference, label: t('flow.reference') },
{ value: VariableType.Input, label: t('flow.text') },
];
return (
<div>
{fields.map((field, index) => {
const typeField = `query.${index}.type`;
const typeValue = form.watch(typeField);
return (
<div key={field.id} className="flex items-center gap-1">
<FormField
control={form.control}
name={typeField}
render={({ field }) => (
<FormItem className="w-2/5">
<FormDescription />
<FormControl>
<RAGFlowSelect
{...field}
placeholder={t('common.pleaseSelect')}
options={options}
onChange={(val) => {
field.onChange(val);
form.resetField(`query.${index}.value`);
form.resetField(`query.${index}.component_id`);
}}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`query.${index}.${getVariableName(typeValue)}`}
render={({ field }) => (
<FormItem className="flex-1">
<FormDescription />
<FormControl>
{typeValue === VariableType.Reference ? (
<RAGFlowSelect
placeholder={t('common.pleaseSelect')}
{...field}
options={valueOptions}
></RAGFlowSelect>
) : (
<Input placeholder={t('common.pleaseInput')} {...field} />
)}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Trash2
className="cursor-pointer mx-3 size-4 text-colors-text-functional-danger"
onClick={() => remove(index)}
/>
</div>
);
})}
<Button onClick={append} className="mt-4" variant={'outline'} size={'sm'}>
<Plus />
{t('flow.addVariable')}
</Button>
</div>
);
}
export function DynamicInputVariable({ node }: IProps) {
const { t } = useTranslation();
return (
<Collapsible defaultOpen className="group/collapsible">
<CollapsibleTrigger className="flex justify-between w-full pb-2">
<span className="font-bold text-2xl text-colors-text-neutral-strong">
{t('flow.input')}
</span>
<Button variant={'icon'} size={'icon'}>
<SideDown />
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<DynamicVariableForm node={node}></DynamicVariableForm>
</CollapsibleContent>
</Collapsible>
);
}

View File

@ -192,7 +192,7 @@ export default function VariablePickerMenuPlugin({
const [queryString, setQueryString] = React.useState<string | null>('');
let options = useFilterQueryVariableOptionsByTypes(types);
let options = useFilterQueryVariableOptionsByTypes({ types });
if (baseOptions) {
options = baseOptions as typeof options;

View File

@ -20,7 +20,7 @@ export function QueryVariableList({
const form = useFormContext();
const name = 'query';
let options = useFilterQueryVariableOptionsByTypes(types);
let options = useFilterQueryVariableOptionsByTypes({ types });
const secondOptions = flatOptions(options);

View File

@ -9,7 +9,10 @@ import { ReactNode } from 'react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { JsonSchemaDataType } from '../../constant';
import { useFilterQueryVariableOptionsByTypes } from '../../hooks/use-get-begin-query';
import {
BuildQueryVariableOptions,
useFilterQueryVariableOptionsByTypes,
} from '../../hooks/use-get-begin-query';
import { GroupedSelectWithSecondaryMenu } from './select-with-secondary-menu';
type QueryVariableProps = {
@ -21,7 +24,7 @@ type QueryVariableProps = {
onChange?: (value: string) => void;
pureQuery?: boolean;
value?: string;
};
} & BuildQueryVariableOptions;
export function QueryVariable({
name = 'query',
@ -32,11 +35,17 @@ export function QueryVariable({
onChange,
pureQuery = false,
value,
nodeIds = [],
variablesExceptOperatorOutputs,
}: QueryVariableProps) {
const { t } = useTranslation();
const form = useFormContext();
const finalOptions = useFilterQueryVariableOptionsByTypes(types);
const finalOptions = useFilterQueryVariableOptionsByTypes({
types,
nodeIds,
variablesExceptOperatorOutputs,
});
const renderWidget = (
value?: string,

View File

@ -49,7 +49,7 @@ export function VariableTable({
nodeId,
}: IProps) {
const { t } = useTranslation();
const { getLabel } = useGetVariableLabelOrTypeByValue(nodeId!);
const { getLabel } = useGetVariableLabelOrTypeByValue({ nodeId: nodeId! });
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(

View File

@ -2,7 +2,6 @@
import { FormContainer } from '@/components/form-container';
import { KeyInput } from '@/components/key-input';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { BlockButton, Button } from '@/components/ui/button';
import {
FormControl,
@ -11,13 +10,17 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Separator } from '@/components/ui/separator';
import { Operator } from '@/constants/agent';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { t } from 'i18next';
import { isEmpty } from 'lodash';
import { X } from 'lucide-react';
import { ReactNode, useCallback, useMemo } from 'react';
import { ReactNode } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useBuildSubNodeOutputOptions } from './use-build-options';
import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query';
import useGraphStore from '../../store';
import { QueryVariable } from '../components/query-variable';
interface IProps {
node?: RAGFlowNodeType;
@ -26,28 +29,22 @@ interface IProps {
export function DynamicOutputForm({ node }: IProps) {
const { t } = useTranslation();
const form = useFormContext();
const options = useBuildSubNodeOutputOptions(node?.id);
const { nodes } = useGraphStore((state) => state);
const childNodeIds = nodes
.filter(
(x) =>
x.parentId === node?.id &&
x.data.label !== Operator.IterationStart &&
!isEmpty(x.data?.form?.outputs),
)
.map((x) => x.id);
const name = 'outputs';
const flatOptions = useMemo(() => {
return options.reduce<{ label: string; value: string; type: string }[]>(
(pre, cur) => {
pre.push(...cur.options);
return pre;
},
[],
);
}, [options]);
const findType = useCallback(
(val: string) => {
const type = flatOptions.find((x) => x.value === val)?.type;
if (type) {
return `Array<${type}>`;
}
},
[flatOptions],
);
const { getType } = useGetVariableLabelOrTypeByValue({
nodeIds: childNodeIds,
});
const { fields, remove, append } = useFieldArray({
name: name,
@ -77,25 +74,15 @@ export function DynamicOutputForm({ node }: IProps) {
)}
/>
<Separator className="w-3 text-text-secondary" />
<FormField
control={form.control}
<QueryVariable
name={`${name}.${index}.ref`}
render={({ field }) => (
<FormItem className="w-2/5">
<FormControl>
<SelectWithSearch
options={options}
{...field}
onChange={(val) => {
form.setValue(typeField, findType(val));
field.onChange(val);
}}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
hideLabel
className="w-2/5"
onChange={(val) => {
form.setValue(typeField, `Array<${getType(val)}>`);
}}
nodeIds={childNodeIds}
></QueryVariable>
<FormField
control={form.control}
name={typeField}

View File

@ -9,7 +9,6 @@ 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';
@ -53,7 +52,6 @@ function IterationForm({ node }: INextOperatorForm) {
name="items_ref"
types={ArrayFields as any[]}
></QueryVariable>
<DynamicVariables name="variables" label="Variables"></DynamicVariables>
<DynamicOutput node={node}></DynamicOutput>
<Output list={outputList}></Output>
</FormWrapper>

View File

@ -1,31 +0,0 @@
import { buildOutputOptions } from '@/utils/canvas-util';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { Operator } from '../../constant';
import useGraphStore from '../../store';
export function useBuildSubNodeOutputOptions(nodeId?: string) {
const { nodes } = useGraphStore((state) => state);
const nodeOutputOptions = useMemo(() => {
if (!nodeId) {
return [];
}
const subNodeWithOutputList = nodes.filter(
(x) =>
x.parentId === nodeId &&
x.data.label !== Operator.IterationStart &&
!isEmpty(x.data?.form?.outputs),
);
return subNodeWithOutputList.map((x) => ({
label: x.data.name,
value: x.id,
title: x.data.name,
options: buildOutputOptions(x.data.form.outputs, x.id),
}));
}, [nodeId, nodes]);
return nodeOutputOptions;
}

View File

@ -1,145 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input, Select } from 'antd';
import { useMemo } from 'react';
import { IOperatorForm } from '../../interface';
import {
Jin10CalendarDatashapeOptions,
Jin10CalendarTypeOptions,
Jin10FlashTypeOptions,
Jin10SymbolsDatatypeOptions,
Jin10SymbolsTypeOptions,
Jin10TypeOptions,
} from '../../options';
import DynamicInputVariable from '../components/dynamic-input-variable';
const Jin10Form = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const jin10TypeOptions = useMemo(() => {
return Jin10TypeOptions.map((x) => ({
value: x,
label: t(`jin10TypeOptions.${x}`),
}));
}, [t]);
const jin10FlashTypeOptions = useMemo(() => {
return Jin10FlashTypeOptions.map((x) => ({
value: x,
label: t(`jin10FlashTypeOptions.${x}`),
}));
}, [t]);
const jin10CalendarTypeOptions = useMemo(() => {
return Jin10CalendarTypeOptions.map((x) => ({
value: x,
label: t(`jin10CalendarTypeOptions.${x}`),
}));
}, [t]);
const jin10CalendarDatashapeOptions = useMemo(() => {
return Jin10CalendarDatashapeOptions.map((x) => ({
value: x,
label: t(`jin10CalendarDatashapeOptions.${x}`),
}));
}, [t]);
const jin10SymbolsTypeOptions = useMemo(() => {
return Jin10SymbolsTypeOptions.map((x) => ({
value: x,
label: t(`jin10SymbolsTypeOptions.${x}`),
}));
}, [t]);
const jin10SymbolsDatatypeOptions = useMemo(() => {
return Jin10SymbolsDatatypeOptions.map((x) => ({
value: x,
label: t(`jin10SymbolsDatatypeOptions.${x}`),
}));
}, [t]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item label={t('type')} name={'type'} initialValue={'flash'}>
<Select options={jin10TypeOptions}></Select>
</Form.Item>
<Form.Item label={t('secretKey')} name={'secret_key'}>
<Input></Input>
</Form.Item>
<Form.Item noStyle dependencies={['type']}>
{({ getFieldValue }) => {
const type = getFieldValue('type');
switch (type) {
case 'flash':
return (
<>
<Form.Item label={t('flashType')} name={'flash_type'}>
<Select options={jin10FlashTypeOptions}></Select>
</Form.Item>
<Form.Item label={t('contain')} name={'contain'}>
<Input></Input>
</Form.Item>
<Form.Item label={t('filter')} name={'filter'}>
<Input></Input>
</Form.Item>
</>
);
case 'calendar':
return (
<>
<Form.Item label={t('calendarType')} name={'calendar_type'}>
<Select options={jin10CalendarTypeOptions}></Select>
</Form.Item>
<Form.Item
label={t('calendarDatashape')}
name={'calendar_datashape'}
>
<Select options={jin10CalendarDatashapeOptions}></Select>
</Form.Item>
</>
);
case 'symbols':
return (
<>
<Form.Item label={t('symbolsType')} name={'symbols_type'}>
<Select options={jin10SymbolsTypeOptions}></Select>
</Form.Item>
<Form.Item
label={t('symbolsDatatype')}
name={'symbols_datatype'}
>
<Select options={jin10SymbolsDatatypeOptions}></Select>
</Form.Item>
</>
);
case 'news':
return (
<>
<Form.Item label={t('contain')} name={'contain'}>
<Input></Input>
</Form.Item>
<Form.Item label={t('filter')} name={'filter'}>
<Input></Input>
</Form.Item>
</>
);
default:
return <></>;
}
}}
</Form.Item>
</Form>
);
};
export default Jin10Form;

View File

@ -1,48 +0,0 @@
import { NextLLMSelect } from '@/components/llm-select/next';
import { TopNFormField } from '@/components/top-n-item';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { useTranslation } from 'react-i18next';
import { INextOperatorForm } from '../../interface';
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
const KeywordExtractForm = ({ form, node }: INextOperatorForm) => {
const { t } = useTranslation();
return (
<Form {...form}>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
}}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<FormField
control={form.control}
name="llm_id"
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('chat.modelTip')}>
{t('chat.model')}
</FormLabel>
<FormControl>
<NextLLMSelect {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<TopNFormField></TopNFormField>
</form>
</Form>
);
};
export default KeywordExtractForm;

View File

@ -1,33 +1,27 @@
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 { 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 { 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' } });
enum InputMode {
Constant = 'constant',
Variable = 'variable',
}
const InputModeOptions = buildOptions(InputMode);
type SelectKeysProps = {
name: string;
label: ReactNode;
@ -35,42 +29,15 @@ type SelectKeysProps = {
keyField?: string;
valueField?: string;
operatorField?: string;
nodeId?: string;
};
type RadioGroupProps = React.ComponentProps<typeof RadioGroupPrimitive.Root>;
type RadioButtonProps = Partial<
Omit<RadioGroupProps, 'onValueChange'> & {
onChange: RadioGroupProps['onValueChange'];
}
>;
function RadioButton({ value, onChange }: RadioButtonProps) {
return (
<RadioGroup
defaultValue="yes"
className="flex"
value={value}
onValueChange={onChange}
>
<div className="flex items-center gap-3">
<RadioGroupItem value="yes" id="r1" />
<Label htmlFor="r1">Yes</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem value="no" id="r2" />
<Label htmlFor="r2">No</Label>
</div>
</RadioGroup>
);
}
const VariableTypeOptions = buildConversationVariableSelectOptions();
const modeField = 'mode';
const modeField = 'input_mode';
const ConstantValueMap = {
[TypesWithArray.Boolean]: 'yes',
[TypesWithArray.Boolean]: true,
[TypesWithArray.Number]: 0,
[TypesWithArray.String]: '',
[TypesWithArray.ArrayBoolean]: '[]',
@ -85,8 +52,9 @@ export function DynamicVariables({
label,
tooltip,
keyField = 'variable',
valueField = 'parameter',
operatorField = 'operator',
valueField = 'value',
operatorField = 'type',
nodeId,
}: SelectKeysProps) {
const form = useFormContext();
const isDarkTheme = useIsDarkTheme();
@ -96,6 +64,9 @@ export function DynamicVariables({
control: form.control,
});
const { initializeVariableRelatedConditions } =
useInitializeConditions(nodeId);
const initializeValue = useCallback(
(mode: string, variableType: string, valueFieldAlias: string) => {
if (mode === InputMode.Variable) {
@ -112,23 +83,27 @@ export function DynamicVariables({
(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) => {
(
variableType: string,
valueFieldAlias: string,
modeFieldAlias: string,
keyFieldAlias: string,
) => {
const mode = form.getValues(modeFieldAlias);
initializeVariableRelatedConditions(
form.getValues(keyFieldAlias),
variableType,
);
initializeValue(mode, variableType, valueFieldAlias);
},
[form, initializeValue],
[form, initializeValue, initializeVariableRelatedConditions],
);
const renderParameter = useCallback(
@ -138,7 +113,7 @@ export function DynamicVariables({
if (mode === InputMode.Constant) {
if (logicalOperator === TypesWithArray.Boolean) {
return <RadioButton></RadioButton>;
return <BoolSegmented></BoolSegmented>;
}
if (logicalOperator === TypesWithArray.Number) {
@ -211,6 +186,7 @@ export function DynamicVariables({
val,
valueFieldAlias,
modeFieldAlias,
keyFieldAlias,
);
field.onChange(val);
}}

View 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);

View File

@ -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>
);
}

View 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>;

View File

@ -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,
};
}

View 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;
}

View 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,
};
}

View File

@ -1,157 +0,0 @@
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { INextOperatorForm } from '../../interface';
import {
QWeatherLangOptions,
QWeatherTimePeriodOptions,
QWeatherTypeOptions,
QWeatherUserTypeOptions,
} from '../../options';
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
enum FormFieldName {
Type = 'type',
UserType = 'user_type',
}
const QWeatherForm = ({ form, node }: INextOperatorForm) => {
const { t } = useTranslation();
const typeValue = form.watch(FormFieldName.Type);
const qWeatherLangOptions = useMemo(() => {
return QWeatherLangOptions.map((x) => ({
value: x,
label: t(`flow.qWeatherLangOptions.${x}`),
}));
}, [t]);
const qWeatherTypeOptions = useMemo(() => {
return QWeatherTypeOptions.map((x) => ({
value: x,
label: t(`flow.qWeatherTypeOptions.${x}`),
}));
}, [t]);
const qWeatherUserTypeOptions = useMemo(() => {
return QWeatherUserTypeOptions.map((x) => ({
value: x,
label: t(`flow.qWeatherUserTypeOptions.${x}`),
}));
}, [t]);
const getQWeatherTimePeriodOptions = useCallback(() => {
let options = QWeatherTimePeriodOptions;
const userType = form.getValues(FormFieldName.UserType);
if (userType === 'free') {
options = options.slice(0, 3);
}
return options.map((x) => ({
value: x,
label: t(`flow.qWeatherTimePeriodOptions.${x}`),
}));
}, [form, t]);
return (
<Form {...form}>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
}}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<FormField
control={form.control}
name="web_apikey"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.webApiKey')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lang"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.lang')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={qWeatherLangOptions}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={FormFieldName.Type}
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.type')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={qWeatherTypeOptions}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={FormFieldName.UserType}
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.userType')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={qWeatherUserTypeOptions}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{typeValue === 'weather' && (
<FormField
control={form.control}
name={'time_period'}
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.timePeriod')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={getQWeatherTimePeriodOptions()}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</form>
</Form>
);
};
export default QWeatherForm;

View File

@ -1,5 +1,4 @@
import { Operator } from '../../constant';
import AkShareForm from '../akshare-form';
import ArXivForm from './arxiv-form';
import BingForm from './bing-form';
import CrawlerForm from './crawler-form';
@ -29,7 +28,6 @@ export const ToolFormConfigMap = {
[Operator.GoogleScholar]: GoogleScholarForm,
[Operator.GitHub]: GithubForm,
[Operator.ExeSQL]: ExeSQLForm,
[Operator.AkShare]: AkShareForm,
[Operator.YahooFinance]: YahooFinanceForm,
[Operator.Crawler]: CrawlerForm,
[Operator.Email]: EmailForm,

View File

@ -1,83 +0,0 @@
import { useTranslate } from '@/hooks/common-hooks';
import { DatePicker, DatePickerProps, Form, Input, Select } from 'antd';
import dayjs from 'dayjs';
import { useCallback, useMemo } from 'react';
import { IOperatorForm } from '../../interface';
import { TuShareSrcOptions } from '../../options';
import DynamicInputVariable from '../components/dynamic-input-variable';
const DateTimePicker = ({
onChange,
value,
}: {
onChange?: (val: number | undefined) => void;
value?: number | undefined;
}) => {
const handleChange: DatePickerProps['onChange'] = useCallback(
(val: any) => {
const nextVal = val?.format('YYYY-MM-DD HH:mm:ss');
onChange?.(nextVal ? nextVal : undefined);
},
[onChange],
);
// The value needs to be converted into a string and saved to the backend
const nextValue = useMemo(() => {
if (value) {
return dayjs(value);
}
return undefined;
}, [value]);
return (
<DatePicker
showTime
format="YYYY-MM-DD HH:mm:ss"
onChange={handleChange}
value={nextValue}
/>
);
};
const TuShareForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const tuShareSrcOptions = useMemo(() => {
return TuShareSrcOptions.map((x) => ({
value: x,
label: t(`tuShareSrcOptions.${x}`),
}));
}, [t]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
label={t('token')}
name={'token'}
tooltip={'Get from https://tushare.pro/'}
>
<Input></Input>
</Form.Item>
<Form.Item label={t('src')} name={'src'}>
<Select options={tuShareSrcOptions}></Select>
</Form.Item>
<Form.Item label={t('startDate')} name={'start_date'}>
<DateTimePicker />
</Form.Item>
<Form.Item label={t('endDate')} name={'end_date'}>
<DateTimePicker />
</Form.Item>
<Form.Item label={t('keyword')} name={'keyword'}>
<Input></Input>
</Form.Item>
</Form>
);
};
export default TuShareForm;