### What problem does this PR solve? Feat: Modify the anchor point positioning of the classification operator node #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.1
| import LLMLabel from '@/components/llm-select/llm-label'; | import LLMLabel from '@/components/llm-select/llm-label'; | ||||
| import { useTheme } from '@/components/theme-provider'; | |||||
| import { ICategorizeNode } from '@/interfaces/database/flow'; | import { ICategorizeNode } from '@/interfaces/database/flow'; | ||||
| import { Handle, NodeProps, Position } from '@xyflow/react'; | |||||
| import { Flex } from 'antd'; | |||||
| import classNames from 'classnames'; | |||||
| import { NodeProps, Position } from '@xyflow/react'; | |||||
| import { get } from 'lodash'; | import { get } from 'lodash'; | ||||
| import { memo } from 'react'; | import { memo } from 'react'; | ||||
| import { CommonHandle } from './handle'; | |||||
| import { RightHandleStyle } from './handle-icon'; | import { RightHandleStyle } from './handle-icon'; | ||||
| import { useBuildCategorizeHandlePositions } from './hooks'; | |||||
| import styles from './index.less'; | |||||
| import NodeHeader from './node-header'; | import NodeHeader from './node-header'; | ||||
| import { NodeWrapper } from './node-wrapper'; | |||||
| import { ToolBar } from './toolbar'; | |||||
| import { useBuildCategorizeHandlePositions } from './use-build-categorize-handle-positions'; | |||||
| export function InnerCategorizeNode({ | export function InnerCategorizeNode({ | ||||
| id, | id, | ||||
| selected, | selected, | ||||
| }: NodeProps<ICategorizeNode>) { | }: NodeProps<ICategorizeNode>) { | ||||
| const { positions } = useBuildCategorizeHandlePositions({ data, id }); | const { positions } = useBuildCategorizeHandlePositions({ data, id }); | ||||
| const { theme } = useTheme(); | |||||
| return ( | return ( | ||||
| <section | |||||
| className={classNames( | |||||
| styles.logicNode, | |||||
| theme === 'dark' ? styles.dark : '', | |||||
| { | |||||
| [styles.selectedNode]: selected, | |||||
| }, | |||||
| )} | |||||
| > | |||||
| <Handle | |||||
| type="target" | |||||
| position={Position.Left} | |||||
| isConnectable | |||||
| className={styles.handle} | |||||
| id={'a'} | |||||
| ></Handle> | |||||
| <ToolBar selected={selected} id={id} label={data.label}> | |||||
| <NodeWrapper> | |||||
| <CommonHandle | |||||
| type="target" | |||||
| position={Position.Left} | |||||
| isConnectable | |||||
| id={'a'} | |||||
| ></CommonHandle> | |||||
| <NodeHeader | |||||
| id={id} | |||||
| name={data.name} | |||||
| label={data.label} | |||||
| className={styles.nodeHeader} | |||||
| ></NodeHeader> | |||||
| <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> | |||||
| <Flex vertical gap={8}> | |||||
| <div className={styles.nodeText}> | |||||
| <LLMLabel value={get(data, 'form.llm_id')}></LLMLabel> | |||||
| </div> | |||||
| {positions.map((position, idx) => { | |||||
| return ( | |||||
| <div key={idx}> | |||||
| <div className={styles.nodeText}>{position.text}</div> | |||||
| <Handle | |||||
| key={position.text} | |||||
| id={position.text} | |||||
| type="source" | |||||
| position={Position.Right} | |||||
| isConnectable | |||||
| className={styles.handle} | |||||
| style={{ ...RightHandleStyle, top: position.top }} | |||||
| ></Handle> | |||||
| </div> | |||||
| ); | |||||
| })} | |||||
| </Flex> | |||||
| </section> | |||||
| <section className="flex flex-col gap-2"> | |||||
| <div className={'bg-background-card rounded-sm px-1'}> | |||||
| <LLMLabel value={get(data, 'form.llm_id')}></LLMLabel> | |||||
| </div> | |||||
| {positions.map((position, idx) => { | |||||
| return ( | |||||
| <div key={idx}> | |||||
| <div className={'bg-background-card rounded-sm p-1'}> | |||||
| {position.text} | |||||
| </div> | |||||
| <CommonHandle | |||||
| key={position.text} | |||||
| id={position.text} | |||||
| type="source" | |||||
| position={Position.Right} | |||||
| isConnectable | |||||
| style={{ ...RightHandleStyle, top: position.top }} | |||||
| ></CommonHandle> | |||||
| </div> | |||||
| ); | |||||
| })} | |||||
| </section> | |||||
| </NodeWrapper> | |||||
| </ToolBar> | |||||
| ); | ); | ||||
| } | } | ||||
| return ( | return ( | ||||
| <section | <section | ||||
| className={cn( | className={cn( | ||||
| 'bg-background-header-bar p-2.5 rounded-md w-[200px]', | |||||
| 'bg-background-header-bar p-2.5 rounded-md w-[200px] text-xs', | |||||
| className, | className, | ||||
| )} | )} | ||||
| > | > |
| import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; | import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query'; | ||||
| import { CommonHandle } from './handle'; | import { CommonHandle } from './handle'; | ||||
| import { RightHandleStyle } from './handle-icon'; | import { RightHandleStyle } from './handle-icon'; | ||||
| import { useBuildSwitchHandlePositions } from './hooks'; | |||||
| import NodeHeader from './node-header'; | import NodeHeader from './node-header'; | ||||
| import { NodeWrapper } from './node-wrapper'; | import { NodeWrapper } from './node-wrapper'; | ||||
| import { ToolBar } from './toolbar'; | import { ToolBar } from './toolbar'; | ||||
| import { useBuildSwitchHandlePositions } from './use-build-switch-handle-positions'; | |||||
| const getConditionKey = (idx: number, length: number) => { | const getConditionKey = (idx: number, length: number) => { | ||||
| if (idx === 0 && length !== 1) { | if (idx === 0 && length !== 1) { |
| import { ICategorizeItemResult } from '@/interfaces/database/agent'; | |||||
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | |||||
| import { useUpdateNodeInternals } from '@xyflow/react'; | |||||
| import { get } from 'lodash'; | |||||
| import { useEffect, useMemo } from 'react'; | |||||
| export const useBuildCategorizeHandlePositions = ({ | |||||
| data, | |||||
| id, | |||||
| }: { | |||||
| id: string; | |||||
| data: RAGFlowNodeType['data']; | |||||
| }) => { | |||||
| const updateNodeInternals = useUpdateNodeInternals(); | |||||
| const categoryData: ICategorizeItemResult = useMemo(() => { | |||||
| return get(data, `form.category_description`, {}); | |||||
| }, [data]); | |||||
| const positions = useMemo(() => { | |||||
| const list: Array<{ | |||||
| text: string; | |||||
| top: number; | |||||
| idx: number; | |||||
| }> = []; | |||||
| Object.keys(categoryData) | |||||
| .sort((a, b) => categoryData[a].index - categoryData[b].index) | |||||
| .forEach((x, idx) => { | |||||
| list.push({ | |||||
| text: x, | |||||
| idx, | |||||
| top: idx === 0 ? 86 : list[idx - 1].top + 8 + 24, | |||||
| }); | |||||
| }); | |||||
| return list; | |||||
| }, [categoryData]); | |||||
| useEffect(() => { | |||||
| updateNodeInternals(id); | |||||
| }, [id, updateNodeInternals, categoryData]); | |||||
| return { positions }; | |||||
| }; |
| import { ISwitchCondition, RAGFlowNodeType } from '@/interfaces/database/flow'; | |||||
| import { useUpdateNodeInternals } from '@xyflow/react'; | import { useUpdateNodeInternals } from '@xyflow/react'; | ||||
| import get from 'lodash/get'; | import get from 'lodash/get'; | ||||
| import { useEffect, useMemo } from 'react'; | import { useEffect, useMemo } from 'react'; | ||||
| import { SwitchElseTo } from '../../constant'; | import { SwitchElseTo } from '../../constant'; | ||||
| import { | |||||
| ICategorizeItemResult, | |||||
| ISwitchCondition, | |||||
| RAGFlowNodeType, | |||||
| } from '@/interfaces/database/flow'; | |||||
| import { generateSwitchHandleText } from '../../utils'; | import { generateSwitchHandleText } from '../../utils'; | ||||
| export const useBuildCategorizeHandlePositions = ({ | |||||
| data, | |||||
| id, | |||||
| }: { | |||||
| id: string; | |||||
| data: RAGFlowNodeType['data']; | |||||
| }) => { | |||||
| const updateNodeInternals = useUpdateNodeInternals(); | |||||
| const categoryData: ICategorizeItemResult = useMemo(() => { | |||||
| return get(data, `form.category_description`, {}); | |||||
| }, [data]); | |||||
| const positions = useMemo(() => { | |||||
| const list: Array<{ | |||||
| text: string; | |||||
| top: number; | |||||
| idx: number; | |||||
| }> = []; | |||||
| Object.keys(categoryData) | |||||
| .sort((a, b) => categoryData[a].index - categoryData[b].index) | |||||
| .forEach((x, idx) => { | |||||
| list.push({ | |||||
| text: x, | |||||
| idx, | |||||
| top: idx === 0 ? 98 + 20 : list[idx - 1].top + 8 + 26, | |||||
| }); | |||||
| }); | |||||
| return list; | |||||
| }, [categoryData]); | |||||
| useEffect(() => { | |||||
| updateNodeInternals(id); | |||||
| }, [id, updateNodeInternals, categoryData]); | |||||
| return { positions }; | |||||
| }; | |||||
| export const useBuildSwitchHandlePositions = ({ | export const useBuildSwitchHandlePositions = ({ | ||||
| data, | data, | ||||
| id, | id, | ||||
| return get(data, 'form.conditions', []); | return get(data, 'form.conditions', []); | ||||
| }, [data]); | }, [data]); | ||||
| useEffect(() => { | |||||
| console.info('xxx0000'); | |||||
| }, [conditions]); | |||||
| const positions = useMemo(() => { | const positions = useMemo(() => { | ||||
| const list: Array<{ | const list: Array<{ | ||||
| text: string; | text: string; | ||||
| }> = []; | }> = []; | ||||
| [...conditions, ''].forEach((x, idx) => { | [...conditions, ''].forEach((x, idx) => { | ||||
| let top = idx === 0 ? 58 + 20 : list[idx - 1].top + 10; // case number (Case 1) height + flex gap | |||||
| if (idx - 1 >= 0) { | |||||
| let top = idx === 0 ? 53 : list[idx - 1].top + 10 + 14; // case number (Case 1) height + flex gap | |||||
| if (idx >= 1) { | |||||
| const previousItems = conditions[idx - 1]?.items ?? []; | const previousItems = conditions[idx - 1]?.items ?? []; | ||||
| if (previousItems.length > 0) { | if (previousItems.length > 0) { | ||||
| // top += 12; // ConditionBlock padding | // top += 12; // ConditionBlock padding | ||||
| top += previousItems.length * 22; // condition variable height | |||||
| top += previousItems.length * 26; // condition variable height | |||||
| // top += (previousItems.length - 1) * 25; // operator height | // top += (previousItems.length - 1) * 25; // operator height | ||||
| } | } | ||||
| } | } |
| } from '../../constant'; | } from '../../constant'; | ||||
| import { useBuildFormSelectOptions } from '../../form-hooks'; | import { useBuildFormSelectOptions } from '../../form-hooks'; | ||||
| import { useBuildComponentIdAndBeginOptions } from '../../hooks/use-get-begin-query'; | import { useBuildComponentIdAndBeginOptions } from '../../hooks/use-get-begin-query'; | ||||
| import { useWatchFormChange } from '../../hooks/use-watch-form-change'; | |||||
| import { IOperatorForm } from '../../interface'; | import { IOperatorForm } from '../../interface'; | ||||
| import { useValues } from './use-values'; | import { useValues } from './use-values'; | ||||
| import { useWatchFormChange } from './use-watch-change'; | |||||
| const ConditionKey = 'conditions'; | const ConditionKey = 'conditions'; | ||||
| const ItemKey = 'items'; | const ItemKey = 'items'; |
| import { ISwitchCondition } from '@/interfaces/database/agent'; | |||||
| import { useEffect } from 'react'; | |||||
| import { UseFormReturn, useWatch } from 'react-hook-form'; | |||||
| import useGraphStore from '../../store'; | |||||
| export function useWatchFormChange(id?: string, form?: UseFormReturn) { | |||||
| let values = useWatch({ control: form?.control }); | |||||
| const updateNodeForm = useGraphStore((state) => state.updateNodeForm); | |||||
| useEffect(() => { | |||||
| // Manually triggered form updates are synchronized to the canvas | |||||
| if (id && form?.formState.isDirty) { | |||||
| values = form?.getValues(); | |||||
| let nextValues: any = { | |||||
| ...values, | |||||
| conditions: | |||||
| values?.conditions?.map((x: ISwitchCondition) => ({ ...x })) ?? [], // Changing the form value with useFieldArray does not change the array reference | |||||
| }; | |||||
| updateNodeForm(id, nextValues); | |||||
| } | |||||
| }, [form?.formState.isDirty, id, updateNodeForm, values]); | |||||
| } |