### What problem does this PR solve? Feat: Add SwitchForm component #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.1
| @@ -3,7 +3,7 @@ import { RAGFlowNodeType } from '@/interfaces/database/flow'; | |||
| import { MinusCircleOutlined } from '@ant-design/icons'; | |||
| import { Form, Input, Select } from 'antd'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; | |||
| import { useBuildVariableOptions } from '../../hooks/use-get-begin-query'; | |||
| import { FormCollapse } from '../components/dynamic-input-variable'; | |||
| type DynamicInputVariableProps = { | |||
| @@ -17,10 +17,7 @@ export const DynamicInputVariable = ({ | |||
| }: DynamicInputVariableProps) => { | |||
| const { t } = useTranslation(); | |||
| const valueOptions = useBuildComponentIdSelectOptions( | |||
| node?.id, | |||
| node?.parentId, | |||
| ); | |||
| const valueOptions = useBuildVariableOptions(node?.id, node?.parentId); | |||
| return ( | |||
| <FormCollapse title={t('flow.inputVariables')}> | |||
| @@ -16,7 +16,7 @@ import { X } from 'lucide-react'; | |||
| import { ReactNode } from 'react'; | |||
| import { useFieldArray, useFormContext } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; | |||
| import { useBuildVariableOptions } from '../../hooks/use-get-begin-query'; | |||
| interface IProps { | |||
| node?: RAGFlowNodeType; | |||
| @@ -41,10 +41,7 @@ export function DynamicVariableForm({ node, name = 'arguments' }: IProps) { | |||
| control: form.control, | |||
| }); | |||
| const valueOptions = useBuildComponentIdSelectOptions( | |||
| node?.id, | |||
| node?.parentId, | |||
| ); | |||
| const valueOptions = useBuildVariableOptions(node?.id, node?.parentId); | |||
| return ( | |||
| <div className="space-y-5"> | |||
| @@ -3,7 +3,7 @@ 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 { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; | |||
| import { useBuildVariableOptions } from '../../hooks/use-get-begin-query'; | |||
| import styles from './index.less'; | |||
| @@ -21,10 +21,7 @@ const getVariableName = (type: string) => | |||
| const DynamicVariableForm = ({ node }: IProps) => { | |||
| const { t } = useTranslation(); | |||
| const valueOptions = useBuildComponentIdSelectOptions( | |||
| node?.id, | |||
| node?.parentId, | |||
| ); | |||
| const valueOptions = useBuildVariableOptions(node?.id, node?.parentId); | |||
| const form = Form.useFormInstance(); | |||
| const options = [ | |||
| @@ -20,7 +20,7 @@ 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 { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; | |||
| import { useBuildVariableOptions } from '../../hooks/use-get-begin-query'; | |||
| interface IProps { | |||
| node?: RAGFlowNodeType; | |||
| @@ -42,10 +42,7 @@ export function DynamicVariableForm({ node }: IProps) { | |||
| control: form.control, | |||
| }); | |||
| const valueOptions = useBuildComponentIdSelectOptions( | |||
| node?.id, | |||
| node?.parentId, | |||
| ); | |||
| const valueOptions = useBuildVariableOptions(node?.id, node?.parentId); | |||
| const options = [ | |||
| { value: VariableType.Reference, label: t('flow.reference') }, | |||
| @@ -3,7 +3,7 @@ import { useTranslate } from '@/hooks/common-hooks'; | |||
| import { DeleteOutlined } from '@ant-design/icons'; | |||
| import { Button, Collapse, Flex, Input, Select, Table, TableProps } from 'antd'; | |||
| import { trim } from 'lodash'; | |||
| import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; | |||
| import { useBuildVariableOptions } from '../../hooks/use-get-begin-query'; | |||
| import { IInvokeVariable } from '../../interface'; | |||
| import { useHandleOperateParameters } from './hooks'; | |||
| @@ -25,7 +25,7 @@ const DynamicVariablesForm = ({ node }: IProps) => { | |||
| const nodeId = node?.id; | |||
| const { t } = useTranslate('flow'); | |||
| const options = useBuildComponentIdSelectOptions(nodeId, node?.parentId); | |||
| const options = useBuildVariableOptions(nodeId, node?.parentId); | |||
| const { | |||
| dataSource, | |||
| handleAdd, | |||
| @@ -1,21 +0,0 @@ | |||
| @lightBackgroundColor: rgba(150, 150, 150, 0.07); | |||
| @darkBackgroundColor: rgba(150, 150, 150, 0.12); | |||
| .caseCard { | |||
| background-color: @lightBackgroundColor; | |||
| } | |||
| .conditionCard { | |||
| background-color: @darkBackgroundColor; | |||
| } | |||
| .elseCase { | |||
| background-color: @lightBackgroundColor; | |||
| padding: 12px; | |||
| border-radius: 8px; | |||
| } | |||
| .addButton { | |||
| color: rgb(22, 119, 255); | |||
| font-weight: 600; | |||
| } | |||
| @@ -1,31 +1,179 @@ | |||
| import { CloseOutlined } from '@ant-design/icons'; | |||
| import { Button, Card, Divider, Form, Input, Select } from 'antd'; | |||
| import { FormContainer } from '@/components/form-container'; | |||
| import { BlockButton, Button } from '@/components/ui/button'; | |||
| import { Card, CardContent } from '@/components/ui/card'; | |||
| import { | |||
| Form, | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { RAGFlowSelect } from '@/components/ui/select'; | |||
| import { Textarea } from '@/components/ui/textarea'; | |||
| import { ISwitchForm } from '@/interfaces/database/flow'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { X } from 'lucide-react'; | |||
| import { useMemo } from 'react'; | |||
| import { useFieldArray, useForm, useFormContext } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { z } from 'zod'; | |||
| import { | |||
| Operator, | |||
| SwitchElseTo, | |||
| SwitchLogicOperatorOptions, | |||
| SwitchOperatorOptions, | |||
| } from '../../constant'; | |||
| import { useBuildFormSelectOptions } from '../../form-hooks'; | |||
| import { useBuildComponentIdSelectOptions } from '../../hooks/use-get-begin-query'; | |||
| import { | |||
| useBuildComponentIdAndBeginOptions, | |||
| useBuildVariableOptions, | |||
| } from '../../hooks/use-get-begin-query'; | |||
| import { IOperatorForm } from '../../interface'; | |||
| import { getOtherFieldValues } from '../../utils'; | |||
| import { useValues } from './use-values'; | |||
| import { ISwitchForm } from '@/interfaces/database/flow'; | |||
| import styles from './index.less'; | |||
| const ConditionKey = 'conditions'; | |||
| const ItemKey = 'items'; | |||
| type ConditionCardsProps = { | |||
| name: string; | |||
| } & IOperatorForm; | |||
| function ConditionCards({ name: parentName, node }: ConditionCardsProps) { | |||
| const form = useFormContext(); | |||
| const { t } = useTranslation(); | |||
| const componentIdOptions = useBuildComponentIdAndBeginOptions( | |||
| node?.id, | |||
| node?.parentId, | |||
| ); | |||
| const switchOperatorOptions = useMemo(() => { | |||
| return SwitchOperatorOptions.map((x) => ({ | |||
| value: x.value, | |||
| label: t(`flow.switchOperatorOptions.${x.label}`), | |||
| })); | |||
| }, [t]); | |||
| const name = `${parentName}.${ItemKey}`; | |||
| const { fields, remove, append } = useFieldArray({ | |||
| name: name, | |||
| control: form.control, | |||
| }); | |||
| const SwitchForm = ({ onValuesChange, node, form }: IOperatorForm) => { | |||
| return ( | |||
| <section className="flex-1 space-y-2.5"> | |||
| {fields.map((field, index) => { | |||
| return ( | |||
| <div key={field.id} className="flex"> | |||
| <Card className="bg-transparent border-input-border border flex-1"> | |||
| <section className="p-2 bg-background-card flex justify-between"> | |||
| <FormField | |||
| control={form.control} | |||
| name={`${name}.${index}.cpn_id`} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormControl> | |||
| <RAGFlowSelect | |||
| {...field} | |||
| options={componentIdOptions} | |||
| triggerClassName="w-30 text-background-checked" | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| control={form.control} | |||
| name={`${name}.${index}.operator`} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormControl> | |||
| <RAGFlowSelect | |||
| {...field} | |||
| options={switchOperatorOptions} | |||
| triggerClassName="w-30" | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| </section> | |||
| <CardContent className="p-4 "> | |||
| <FormField | |||
| control={form.control} | |||
| name={`${name}.${index}.value`} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormControl> | |||
| <Textarea {...field} className="bg-transparent" /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| </CardContent> | |||
| </Card> | |||
| <Button variant={'ghost'} onClick={() => remove(index)}> | |||
| <X /> | |||
| </Button> | |||
| </div> | |||
| ); | |||
| })} | |||
| <div className="pr-9"> | |||
| <BlockButton | |||
| className="mt-6" | |||
| onClick={() => append({ operator: switchOperatorOptions[0].value })} | |||
| > | |||
| add | |||
| </BlockButton> | |||
| </div> | |||
| </section> | |||
| ); | |||
| } | |||
| const SwitchForm = ({ node }: IOperatorForm) => { | |||
| const { t } = useTranslation(); | |||
| const values = useValues(); | |||
| const FormSchema = z.object({ | |||
| conditions: z.array( | |||
| z | |||
| .object({ | |||
| logical_operator: z.string(), | |||
| items: z | |||
| .array( | |||
| z.object({ | |||
| cpn_id: z.string(), | |||
| operator: z.string(), | |||
| value: z.string().optional(), | |||
| }), | |||
| ) | |||
| .optional(), | |||
| to: z.array(z.string()).optional(), | |||
| }) | |||
| .optional(), | |||
| ), | |||
| }); | |||
| const form = useForm({ | |||
| defaultValues: values, | |||
| resolver: zodResolver(FormSchema), | |||
| }); | |||
| const { fields, remove, append } = useFieldArray({ | |||
| name: ConditionKey, | |||
| control: form.control, | |||
| }); | |||
| const buildCategorizeToOptions = useBuildFormSelectOptions( | |||
| Operator.Switch, | |||
| node?.id, | |||
| ); | |||
| const getSelectedConditionTos = () => { | |||
| const conditions: ISwitchForm['conditions'] = | |||
| form?.getFieldValue('conditions'); | |||
| const conditions: ISwitchForm['conditions'] = form?.getValues('conditions'); | |||
| return conditions?.filter((x) => !!x).map((x) => x?.to) ?? []; | |||
| }; | |||
| @@ -44,159 +192,52 @@ const SwitchForm = ({ onValuesChange, node, form }: IOperatorForm) => { | |||
| })); | |||
| }, [t]); | |||
| const componentIdOptions = useBuildComponentIdSelectOptions( | |||
| node?.id, | |||
| node?.parentId, | |||
| ); | |||
| const componentIdOptions = useBuildVariableOptions(node?.id, node?.parentId); | |||
| return ( | |||
| <Form | |||
| form={form} | |||
| name="dynamic_form_complex" | |||
| autoComplete="off" | |||
| initialValues={{ conditions: [{}] }} | |||
| onValuesChange={onValuesChange} | |||
| layout={'vertical'} | |||
| > | |||
| <Form.List name="conditions"> | |||
| {(fields, { add, remove }) => ( | |||
| <div style={{ display: 'flex', rowGap: 16, flexDirection: 'column' }}> | |||
| {fields.map((field) => { | |||
| return ( | |||
| <Card | |||
| size="small" | |||
| title={`Case ${field.name + 1}`} | |||
| key={field.key} | |||
| className={styles.caseCard} | |||
| extra={ | |||
| <CloseOutlined | |||
| onClick={() => { | |||
| remove(field.name); | |||
| }} | |||
| /> | |||
| } | |||
| > | |||
| <Form.Item noStyle dependencies={[field.name, 'items']}> | |||
| {({ getFieldValue }) => | |||
| getFieldValue(['conditions', field.name, 'items']) | |||
| ?.length > 1 && ( | |||
| <Form.Item | |||
| label={t('flow.logicalOperator')} | |||
| name={[field.name, 'logical_operator']} | |||
| > | |||
| <Select options={switchLogicOperatorOptions} /> | |||
| </Form.Item> | |||
| ) | |||
| } | |||
| </Form.Item> | |||
| <Form.Item | |||
| label={t('flow.nextStep')} | |||
| name={[field.name, 'to']} | |||
| > | |||
| <Select | |||
| allowClear | |||
| options={buildCategorizeToOptions([ | |||
| form?.getFieldValue(SwitchElseTo), | |||
| ...getOtherFieldValues( | |||
| form!, | |||
| 'conditions', | |||
| field, | |||
| 'to', | |||
| ), | |||
| ])} | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item label="Condition"> | |||
| <Form.List name={[field.name, 'items']}> | |||
| {(subFields, subOpt) => ( | |||
| <div | |||
| style={{ | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| rowGap: 16, | |||
| }} | |||
| > | |||
| {subFields.map((subField) => ( | |||
| <Card | |||
| key={subField.key} | |||
| title={null} | |||
| size="small" | |||
| className={styles.conditionCard} | |||
| bordered | |||
| extra={ | |||
| <CloseOutlined | |||
| onClick={() => { | |||
| subOpt.remove(subField.name); | |||
| }} | |||
| /> | |||
| } | |||
| > | |||
| <Form.Item | |||
| label={t('flow.componentId')} | |||
| name={[subField.name, 'cpn_id']} | |||
| > | |||
| <Select | |||
| placeholder={t('flow.componentId')} | |||
| options={componentIdOptions} | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label={t('flow.operator')} | |||
| name={[subField.name, 'operator']} | |||
| > | |||
| <Select | |||
| placeholder={t('flow.operator')} | |||
| options={switchOperatorOptions} | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label={t('flow.value')} | |||
| name={[subField.name, 'value']} | |||
| > | |||
| <Input placeholder={t('flow.value')} /> | |||
| </Form.Item> | |||
| </Card> | |||
| ))} | |||
| <Button | |||
| onClick={() => { | |||
| form?.setFieldValue( | |||
| ['conditions', field.name, 'logical_operator'], | |||
| SwitchLogicOperatorOptions[0], | |||
| ); | |||
| subOpt.add({ | |||
| operator: SwitchOperatorOptions[0].value, | |||
| }); | |||
| }} | |||
| block | |||
| className={styles.addButton} | |||
| > | |||
| + Add Condition | |||
| </Button> | |||
| </div> | |||
| )} | |||
| </Form.List> | |||
| </Form.Item> | |||
| </Card> | |||
| ); | |||
| })} | |||
| <Button onClick={() => add()} block className={styles.addButton}> | |||
| + Add Case | |||
| </Button> | |||
| </div> | |||
| )} | |||
| </Form.List> | |||
| <Divider /> | |||
| <Form.Item | |||
| label={'ELSE'} | |||
| name={[SwitchElseTo]} | |||
| className={styles.elseCase} | |||
| <Form {...form}> | |||
| <form | |||
| className="space-y-6 p-5 " | |||
| onSubmit={(e) => { | |||
| e.preventDefault(); | |||
| }} | |||
| > | |||
| <Select | |||
| allowClear | |||
| options={buildCategorizeToOptions(getSelectedConditionTos())} | |||
| /> | |||
| </Form.Item> | |||
| {fields.map((field, index) => { | |||
| return ( | |||
| <FormContainer key={field.id} className=""> | |||
| <div>IF</div> | |||
| <section className="flex items-center gap-2 !mt-2"> | |||
| <FormField | |||
| control={form.control} | |||
| name={`${ConditionKey}.${index}.logical_operator`} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormControl> | |||
| <RAGFlowSelect | |||
| {...field} | |||
| options={switchLogicOperatorOptions} | |||
| triggerClassName="w-18" | |||
| /> | |||
| </FormControl> | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <ConditionCards | |||
| name={`${ConditionKey}.${index}`} | |||
| ></ConditionCards> | |||
| </section> | |||
| </FormContainer> | |||
| ); | |||
| })} | |||
| <BlockButton | |||
| onClick={() => | |||
| append({ logical_operator: SwitchLogicOperatorOptions[0] }) | |||
| } | |||
| > | |||
| add | |||
| </BlockButton> | |||
| </form> | |||
| </Form> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,20 @@ | |||
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | |||
| import { isEmpty } from 'lodash'; | |||
| import { useMemo } from 'react'; | |||
| const defaultValues = { | |||
| conditions: [], | |||
| }; | |||
| export function useValues(node?: RAGFlowNodeType) { | |||
| const values = useMemo(() => { | |||
| const formData = node?.data?.form; | |||
| if (isEmpty(formData)) { | |||
| return defaultValues; | |||
| } | |||
| return formData; | |||
| }, [node]); | |||
| return values; | |||
| } | |||
| @@ -104,42 +104,9 @@ const ExcludedNodes = [ | |||
| Operator.Note, | |||
| ]; | |||
| export const useBuildComponentIdSelectOptions = ( | |||
| nodeId?: string, | |||
| parentId?: string, | |||
| ) => { | |||
| const nodes = useGraphStore((state) => state.nodes); | |||
| export function useBuildBeginVariableOptions() { | |||
| const getBeginNodeDataQuery = useGetBeginNodeDataQuery(); | |||
| const nodeOutputOptions = useBuildNodeOutputOptions(nodeId); | |||
| // Limit the nodes inside iteration to only reference peer nodes with the same parentId and other external nodes other than their parent nodes | |||
| const filterChildNodesToSameParentOrExternal = useCallback( | |||
| (node: RAGFlowNodeType) => { | |||
| // Node inside iteration | |||
| if (parentId) { | |||
| return ( | |||
| (node.parentId === parentId || node.parentId === undefined) && | |||
| node.id !== parentId | |||
| ); | |||
| } | |||
| return node.parentId === undefined; // The outermost node | |||
| }, | |||
| [parentId], | |||
| ); | |||
| const componentIdOptions = useMemo(() => { | |||
| return nodes | |||
| .filter( | |||
| (x) => | |||
| x.id !== nodeId && | |||
| !ExcludedNodes.some((y) => y === x.data.label) && | |||
| filterChildNodesToSameParentOrExternal(x), | |||
| ) | |||
| .map((x) => ({ label: x.data.name, value: x.id })); | |||
| }, [nodes, nodeId, filterChildNodesToSameParentOrExternal]); | |||
| const options = useMemo(() => { | |||
| const query: BeginQuery[] = getBeginNodeDataQuery(); | |||
| return [ | |||
| @@ -151,15 +118,25 @@ export const useBuildComponentIdSelectOptions = ( | |||
| value: `begin@${x.key}`, | |||
| })), | |||
| }, | |||
| ...nodeOutputOptions, | |||
| ]; | |||
| }, [getBeginNodeDataQuery, nodeOutputOptions]); | |||
| }, [getBeginNodeDataQuery]); | |||
| return options; | |||
| } | |||
| export const useBuildVariableOptions = (nodeId?: string) => { | |||
| const nodeOutputOptions = useBuildNodeOutputOptions(nodeId); | |||
| const beginOptions = useBuildBeginVariableOptions(); | |||
| const options = useMemo(() => { | |||
| return [...beginOptions, ...nodeOutputOptions]; | |||
| }, [beginOptions, nodeOutputOptions]); | |||
| return options; | |||
| }; | |||
| export const useGetComponentLabelByValue = (nodeId: string) => { | |||
| const options = useBuildComponentIdSelectOptions(nodeId); | |||
| const options = useBuildVariableOptions(nodeId); | |||
| const flattenOptions = useMemo(() => { | |||
| return options.reduce<DefaultOptionType[]>((pre, cur) => { | |||
| @@ -179,7 +156,7 @@ export const useGetComponentLabelByValue = (nodeId: string) => { | |||
| export function useBuildQueryVariableOptions() { | |||
| const { data } = useFetchAgent(); | |||
| const node = useContext(AgentFormContext); | |||
| const options = useBuildComponentIdSelectOptions(node?.id, node?.parentId); | |||
| const options = useBuildVariableOptions(node?.id); | |||
| const nextOptions = useMemo(() => { | |||
| const globalOptions = Object.keys(data?.dsl?.globals ?? {}).map((x) => ({ | |||
| @@ -194,3 +171,52 @@ export function useBuildQueryVariableOptions() { | |||
| return nextOptions; | |||
| } | |||
| export function useBuildComponentIdOptions(nodeId?: string, parentId?: string) { | |||
| const nodes = useGraphStore((state) => state.nodes); | |||
| // Limit the nodes inside iteration to only reference peer nodes with the same parentId and other external nodes other than their parent nodes | |||
| const filterChildNodesToSameParentOrExternal = useCallback( | |||
| (node: RAGFlowNodeType) => { | |||
| // Node inside iteration | |||
| if (parentId) { | |||
| return ( | |||
| (node.parentId === parentId || node.parentId === undefined) && | |||
| node.id !== parentId | |||
| ); | |||
| } | |||
| return node.parentId === undefined; // The outermost node | |||
| }, | |||
| [parentId], | |||
| ); | |||
| const componentIdOptions = useMemo(() => { | |||
| return nodes | |||
| .filter( | |||
| (x) => | |||
| x.id !== nodeId && | |||
| !ExcludedNodes.some((y) => y === x.data.label) && | |||
| filterChildNodesToSameParentOrExternal(x), | |||
| ) | |||
| .map((x) => ({ label: x.data.name, value: x.id })); | |||
| }, [nodes, nodeId, filterChildNodesToSameParentOrExternal]); | |||
| return [ | |||
| { | |||
| label: <span>Component Output</span>, | |||
| title: 'Component Output', | |||
| options: componentIdOptions, | |||
| }, | |||
| ]; | |||
| } | |||
| export function useBuildComponentIdAndBeginOptions( | |||
| nodeId?: string, | |||
| parentId?: string, | |||
| ) { | |||
| const componentIdOptions = useBuildComponentIdOptions(nodeId, parentId); | |||
| const beginOptions = useBuildBeginVariableOptions(); | |||
| return [...beginOptions, ...componentIdOptions]; | |||
| } | |||