### What problem does this PR solve? feat: monitor changes in the table of relevant operators and synchronize them to the edge #918 feat: fixed the issue of repeated requests when opening the graph page #918 feat: cache node anchor coordinate information #918 feat: monitor changes in the data.form field of the categorize and relevant operators and then synchronize them to the edge #918 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)tags/v0.9.0
| } = useQuery({ | } = useQuery({ | ||||
| queryKey: ['flowDetail'], | queryKey: ['flowDetail'], | ||||
| initialData: {} as IFlow, | initialData: {} as IFlow, | ||||
| refetchOnReconnect: false, | |||||
| refetchOnMount: false, | |||||
| queryFn: async () => { | queryFn: async () => { | ||||
| const { data } = await flowService.getCanvas({}, id); | const { data } = await flowService.getCanvas({}, id); | ||||
| answer: 'Answer', | answer: 'Answer', | ||||
| categorize: 'Categorize', | categorize: 'Categorize', | ||||
| relevant: 'Relevant', | relevant: 'Relevant', | ||||
| rewriteQuestion: 'RewriteQuestion', | |||||
| rewriteQuestion: 'Rewrite', | |||||
| rewrite: 'Rewrite', | rewrite: 'Rewrite', | ||||
| begin: 'Begin', | begin: 'Begin', | ||||
| message: 'Message', | message: 'Message', |
| } from 'reactflow'; | } from 'reactflow'; | ||||
| import useGraphStore from '../../store'; | import useGraphStore from '../../store'; | ||||
| import { useFetchFlow } from '@/hooks/flow-hooks'; | |||||
| import { IFlow } from '@/interfaces/database/flow'; | |||||
| import { useQueryClient } from '@tanstack/react-query'; | |||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| }; | }; | ||||
| // highlight the nodes that the workflow passes through | // highlight the nodes that the workflow passes through | ||||
| const { data: flowDetail } = useFetchFlow(); | |||||
| const queryClient = useQueryClient(); | |||||
| const flowDetail = queryClient.getQueryData<IFlow>(['flowDetail']); | |||||
| const graphPath = useMemo(() => { | const graphPath = useMemo(() => { | ||||
| // TODO: this will be called multiple times | // TODO: this will be called multiple times | ||||
| const path = flowDetail.dsl.path ?? []; | |||||
| const path = flowDetail?.dsl.path ?? []; | |||||
| // The second to last | // The second to last | ||||
| const previousGraphPath: string[] = path.at(-2) ?? []; | const previousGraphPath: string[] = path.at(-2) ?? []; | ||||
| let graphPath: string[] = path.at(-1) ?? []; | let graphPath: string[] = path.at(-1) ?? []; | ||||
| graphPath = [previousLatestElement, ...graphPath]; | graphPath = [previousLatestElement, ...graphPath]; | ||||
| } | } | ||||
| return graphPath; | return graphPath; | ||||
| }, [flowDetail.dsl.path]); | |||||
| }, [flowDetail?.dsl.path]); | |||||
| const highlightStyle = useMemo(() => { | const highlightStyle = useMemo(() => { | ||||
| const idx = graphPath.findIndex((x) => x === source); | const idx = graphPath.findIndex((x) => x === source); |
| useSelectCanvasData, | useSelectCanvasData, | ||||
| useShowDrawer, | useShowDrawer, | ||||
| useValidateConnection, | useValidateConnection, | ||||
| useWatchNodeFormDataChange, | |||||
| } from '../hooks'; | } from '../hooks'; | ||||
| import { RagNode } from './node'; | import { RagNode } from './node'; | ||||
| const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(); | const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(); | ||||
| const { handleKeyUp } = useHandleKeyUp(); | const { handleKeyUp } = useHandleKeyUp(); | ||||
| useWatchNodeFormDataChange(); | |||||
| return ( | return ( | ||||
| <div className={styles.canvasWrapper}> | <div className={styles.canvasWrapper}> |
| import { useTranslate } from '@/hooks/commonHooks'; | import { useTranslate } from '@/hooks/commonHooks'; | ||||
| import { Flex } from 'antd'; | import { Flex } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { pick } from 'lodash'; | |||||
| import get from 'lodash/get'; | import get from 'lodash/get'; | ||||
| import intersectionWith from 'lodash/intersectionWith'; | |||||
| import isEqual from 'lodash/isEqual'; | |||||
| import lowerFirst from 'lodash/lowerFirst'; | import lowerFirst from 'lodash/lowerFirst'; | ||||
| import { Handle, NodeProps, Position } from 'reactflow'; | |||||
| import { | |||||
| CategorizeAnchorPointPositions, | |||||
| Operator, | |||||
| operatorMap, | |||||
| } from '../../constant'; | |||||
| import { NodeData } from '../../interface'; | |||||
| import { useEffect, useMemo, useState } from 'react'; | |||||
| import { Handle, NodeProps, Position, useUpdateNodeInternals } from 'reactflow'; | |||||
| import { Operator, operatorMap } from '../../constant'; | |||||
| import { IPosition, NodeData } from '../../interface'; | |||||
| import OperatorIcon from '../../operator-icon'; | import OperatorIcon from '../../operator-icon'; | ||||
| import { buildNewPositionMap } from '../../utils'; | |||||
| import CategorizeHandle from './categorize-handle'; | import CategorizeHandle from './categorize-handle'; | ||||
| import NodeDropdown from './dropdown'; | import NodeDropdown from './dropdown'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import NodePopover from './popover'; | import NodePopover from './popover'; | ||||
| export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) { | export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) { | ||||
| const categoryData = get(data, 'form.category_description') ?? {}; | |||||
| const updateNodeInternals = useUpdateNodeInternals(); | |||||
| const [postionMap, setPositionMap] = useState<Record<string, IPosition>>({}); | |||||
| const categoryData = useMemo( | |||||
| () => get(data, 'form.category_description') ?? {}, | |||||
| [data], | |||||
| ); | |||||
| const style = operatorMap[data.label as Operator]; | const style = operatorMap[data.label as Operator]; | ||||
| const { t } = useTranslate('flow'); | const { t } = useTranslate('flow'); | ||||
| useEffect(() => { | |||||
| // Cache used coordinates | |||||
| setPositionMap((state) => { | |||||
| // index in use | |||||
| const indexesInUse = Object.values(state).map((x) => x.idx); | |||||
| const categoryDataKeys = Object.keys(categoryData); | |||||
| const stateKeys = Object.keys(state); | |||||
| if (!isEqual(categoryDataKeys.sort(), stateKeys.sort())) { | |||||
| const intersectionKeys = intersectionWith( | |||||
| stateKeys, | |||||
| categoryDataKeys, | |||||
| (categoryDataKey, postionMapKey) => categoryDataKey === postionMapKey, | |||||
| ); | |||||
| const newPositionMap = buildNewPositionMap( | |||||
| categoryDataKeys.filter( | |||||
| (x) => !intersectionKeys.some((y) => y === x), | |||||
| ), | |||||
| indexesInUse, | |||||
| ); | |||||
| console.info('newPositionMap:', newPositionMap); | |||||
| const nextPostionMap = { | |||||
| ...pick(state, intersectionKeys), | |||||
| ...newPositionMap, | |||||
| }; | |||||
| return nextPostionMap; | |||||
| } | |||||
| return state; | |||||
| }); | |||||
| }, [categoryData]); | |||||
| useEffect(() => { | |||||
| updateNodeInternals(id); | |||||
| }, [id, updateNodeInternals, postionMap]); | |||||
| return ( | return ( | ||||
| <NodePopover nodeId={id}> | <NodePopover nodeId={id}> | ||||
| <section | <section | ||||
| id={'c'} | id={'c'} | ||||
| ></Handle> | ></Handle> | ||||
| {Object.keys(categoryData).map((x, idx) => { | {Object.keys(categoryData).map((x, idx) => { | ||||
| const position = postionMap[x]; | |||||
| return ( | return ( | ||||
| <CategorizeHandle | |||||
| top={CategorizeAnchorPointPositions[idx].top} | |||||
| right={CategorizeAnchorPointPositions[idx].right} | |||||
| key={idx} | |||||
| text={x} | |||||
| idx={idx} | |||||
| ></CategorizeHandle> | |||||
| position && ( | |||||
| <CategorizeHandle | |||||
| top={position.top} | |||||
| right={position.right} | |||||
| key={idx} | |||||
| text={x} | |||||
| idx={idx} | |||||
| ></CategorizeHandle> | |||||
| ) | |||||
| ); | ); | ||||
| })} | })} | ||||
| <Flex vertical align="center" justify="center" gap={6}> | <Flex vertical align="center" justify="center" gap={6}> |
| import { useTranslate } from '@/hooks/commonHooks'; | import { useTranslate } from '@/hooks/commonHooks'; | ||||
| import { CloseOutlined } from '@ant-design/icons'; | import { CloseOutlined } from '@ant-design/icons'; | ||||
| import { Button, Card, Form, Input, Select } from 'antd'; | import { Button, Card, Form, Input, Select } from 'antd'; | ||||
| import { humanId } from 'human-id'; | |||||
| import { useUpdateNodeInternals } from 'reactflow'; | import { useUpdateNodeInternals } from 'reactflow'; | ||||
| import { Operator } from '../constant'; | import { Operator } from '../constant'; | ||||
| import { | |||||
| useBuildFormSelectOptions, | |||||
| useHandleFormSelectChange, | |||||
| } from '../form-hooks'; | |||||
| import { useBuildFormSelectOptions } from '../form-hooks'; | |||||
| import { ICategorizeItem } from '../interface'; | import { ICategorizeItem } from '../interface'; | ||||
| interface IProps { | interface IProps { | ||||
| Operator.Categorize, | Operator.Categorize, | ||||
| nodeId, | nodeId, | ||||
| ); | ); | ||||
| const { handleSelectChange } = useHandleFormSelectChange(nodeId); | |||||
| const { t } = useTranslate('flow'); | const { t } = useTranslate('flow'); | ||||
| return ( | return ( | ||||
| <Form.List name="items"> | <Form.List name="items"> | ||||
| {(fields, { add, remove }) => { | {(fields, { add, remove }) => { | ||||
| const handleAdd = () => { | const handleAdd = () => { | ||||
| const idx = fields.length; | |||||
| add({ name: `Categorize ${idx + 1}` }); | |||||
| add({ name: humanId() }); | |||||
| if (nodeId) updateNodeInternals(nodeId); | if (nodeId) updateNodeInternals(nodeId); | ||||
| }; | }; | ||||
| return ( | return ( | ||||
| form.getFieldValue(['items', field.name, 'to']), | form.getFieldValue(['items', field.name, 'to']), | ||||
| ), | ), | ||||
| )} | )} | ||||
| onChange={handleSelectChange( | |||||
| form.getFieldValue(['items', field.name, 'name']), | |||||
| )} | |||||
| /> | /> | ||||
| </Form.Item> | </Form.Item> | ||||
| </Card> | </Card> |
| import get from 'lodash/get'; | import get from 'lodash/get'; | ||||
| import omit from 'lodash/omit'; | import omit from 'lodash/omit'; | ||||
| import { useCallback, useEffect } from 'react'; | import { useCallback, useEffect } from 'react'; | ||||
| import { Edge, Node } from 'reactflow'; | |||||
| import { | import { | ||||
| ICategorizeItem, | ICategorizeItem, | ||||
| ICategorizeItemResult, | ICategorizeItemResult, | ||||
| IOperatorForm, | IOperatorForm, | ||||
| NodeData, | |||||
| } from '../interface'; | } from '../interface'; | ||||
| import useGraphStore from '../store'; | import useGraphStore from '../store'; | ||||
| */ | */ | ||||
| const buildCategorizeListFromObject = ( | const buildCategorizeListFromObject = ( | ||||
| categorizeItem: ICategorizeItemResult, | categorizeItem: ICategorizeItemResult, | ||||
| edges: Edge[], | |||||
| node?: Node<NodeData>, | |||||
| ) => { | ) => { | ||||
| // Categorize's to field has two data sources, with edges as the data source. | // Categorize's to field has two data sources, with edges as the data source. | ||||
| // Changes in the edge or to field need to be synchronized to the form field. | // Changes in the edge or to field need to be synchronized to the form field. | ||||
| return Object.keys(categorizeItem).reduce<Array<ICategorizeItem>>( | return Object.keys(categorizeItem).reduce<Array<ICategorizeItem>>( | ||||
| (pre, cur) => { | (pre, cur) => { | ||||
| // synchronize edge data to the to field | // synchronize edge data to the to field | ||||
| const edge = edges.find( | |||||
| (x) => x.source === node?.id && x.sourceHandle === cur, | |||||
| ); | |||||
| pre.push({ name: cur, ...categorizeItem[cur], to: edge?.target }); | |||||
| pre.push({ name: cur, ...categorizeItem[cur] }); | |||||
| return pre; | return pre; | ||||
| }, | }, | ||||
| [], | [], | ||||
| form, | form, | ||||
| nodeId, | nodeId, | ||||
| }: IOperatorForm) => { | }: IOperatorForm) => { | ||||
| const edges = useGraphStore((state) => state.edges); | |||||
| const getNode = useGraphStore((state) => state.getNode); | const getNode = useGraphStore((state) => state.getNode); | ||||
| const node = getNode(nodeId); | const node = getNode(nodeId); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| const items = buildCategorizeListFromObject( | const items = buildCategorizeListFromObject( | ||||
| get(node, 'data.form.category_description', {}), | get(node, 'data.form.category_description', {}), | ||||
| edges, | |||||
| node, | |||||
| ); | ); | ||||
| console.info('effect:', items); | |||||
| form?.setFieldsValue({ | form?.setFieldsValue({ | ||||
| items, | items, | ||||
| }); | }); | ||||
| }, [form, node, edges]); | |||||
| }, [form, node]); | |||||
| return { handleValuesChange }; | return { handleValuesChange }; | ||||
| }; | }; |
| return buildCategorizeToOptions; | return buildCategorizeToOptions; | ||||
| }; | }; | ||||
| /** | |||||
| * dumped | |||||
| * @param nodeId | |||||
| * @returns | |||||
| */ | |||||
| export const useHandleFormSelectChange = (nodeId?: string) => { | export const useHandleFormSelectChange = (nodeId?: string) => { | ||||
| const { addEdge, deleteEdgeBySourceAndSourceHandle } = useGraphStore( | const { addEdge, deleteEdgeBySourceAndSourceHandle } = useGraphStore( | ||||
| (state) => state, | (state) => state, |
| const { getNode, updateNodeForm } = useGraphStore((state) => state); | const { getNode, updateNodeForm } = useGraphStore((state) => state); | ||||
| const node = getNode(nodeId); | const node = getNode(nodeId); | ||||
| const dataSource: IGenerateParameter[] = useMemo( | const dataSource: IGenerateParameter[] = useMemo( | ||||
| () => get(node, 'data.form.parameters', []), | |||||
| () => get(node, 'data.form.parameters', []) as IGenerateParameter[], | |||||
| [node], | [node], | ||||
| ); | ); | ||||
| // const [x, setDataSource] = useState<IGenerateParameter[]>([]); | |||||
| const handleComponentIdChange = useCallback( | const handleComponentIdChange = useCallback( | ||||
| (row: IGenerateParameter) => (value: string) => { | (row: IGenerateParameter) => (value: string) => { | ||||
| const newData = [...dataSource]; | const newData = [...dataSource]; | ||||
| }); | }); | ||||
| updateNodeForm(nodeId, { parameters: newData }); | updateNodeForm(nodeId, { parameters: newData }); | ||||
| // setDataSource(newData); | |||||
| }, | }, | ||||
| [updateNodeForm, nodeId, dataSource], | [updateNodeForm, nodeId, dataSource], | ||||
| ); | ); | ||||
| (id?: string) => () => { | (id?: string) => () => { | ||||
| const newData = dataSource.filter((item) => item.id !== id); | const newData = dataSource.filter((item) => item.id !== id); | ||||
| updateNodeForm(nodeId, { parameters: newData }); | updateNodeForm(nodeId, { parameters: newData }); | ||||
| // setDataSource(newData); | |||||
| }, | }, | ||||
| [updateNodeForm, nodeId, dataSource], | [updateNodeForm, nodeId, dataSource], | ||||
| ); | ); | ||||
| const handleAdd = useCallback(() => { | const handleAdd = useCallback(() => { | ||||
| // setDataSource((state) => [ | |||||
| // ...state, | |||||
| // { | |||||
| // id: uuid(), | |||||
| // key: '', | |||||
| // component_id: undefined, | |||||
| // }, | |||||
| // ]); | |||||
| updateNodeForm(nodeId, { | updateNodeForm(nodeId, { | ||||
| parameters: [ | parameters: [ | ||||
| ...dataSource, | ...dataSource, | ||||
| }); | }); | ||||
| updateNodeForm(nodeId, { parameters: newData }); | updateNodeForm(nodeId, { parameters: newData }); | ||||
| // setDataSource(newData); | |||||
| }; | }; | ||||
| return { | return { |
| useEffect, | useEffect, | ||||
| useState, | useState, | ||||
| } from 'react'; | } from 'react'; | ||||
| import { Connection, Node, Position, ReactFlowInstance } from 'reactflow'; | |||||
| import { Connection, Edge, Node, Position, ReactFlowInstance } from 'reactflow'; | |||||
| // import { shallow } from 'zustand/shallow'; | // import { shallow } from 'zustand/shallow'; | ||||
| import { variableEnabledFieldMap } from '@/constants/chat'; | import { variableEnabledFieldMap } from '@/constants/chat'; | ||||
| import { | import { | ||||
| import { humanId } from 'human-id'; | import { humanId } from 'human-id'; | ||||
| import trim from 'lodash/trim'; | import trim from 'lodash/trim'; | ||||
| import { useParams } from 'umi'; | import { useParams } from 'umi'; | ||||
| import { v4 as uuid } from 'uuid'; | |||||
| import { | import { | ||||
| NodeMap, | NodeMap, | ||||
| Operator, | Operator, | ||||
| initialRetrievalValues, | initialRetrievalValues, | ||||
| initialRewriteQuestionValues, | initialRewriteQuestionValues, | ||||
| } from './constant'; | } from './constant'; | ||||
| import { ICategorizeForm, IRelevantForm } from './interface'; | |||||
| import useGraphStore, { RFState } from './store'; | import useGraphStore, { RFState } from './store'; | ||||
| import { | import { | ||||
| buildDslComponentsByGraph, | buildDslComponentsByGraph, | ||||
| }; | }; | ||||
| export const useFetchDataOnMount = () => { | export const useFetchDataOnMount = () => { | ||||
| const { loading, data } = useFetchFlow(); | |||||
| const { loading, data, refetch } = useFetchFlow(); | |||||
| const setGraphInfo = useSetGraphInfo(); | const setGraphInfo = useSetGraphInfo(); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| useFetchLlmList(); | useFetchLlmList(); | ||||
| useEffect(() => { | |||||
| refetch(); | |||||
| }, [refetch]); | |||||
| return { loading, flowDetail: data }; | return { loading, flowDetail: data }; | ||||
| }; | }; | ||||
| return replaceIdWithText(output, getNameById); | return replaceIdWithText(output, getNameById); | ||||
| }; | }; | ||||
| /** | |||||
| * monitor changes in the data.form field of the categorize and relevant operators | |||||
| * and then synchronize them to the edge | |||||
| */ | |||||
| export const useWatchNodeFormDataChange = () => { | |||||
| const { getNode, nodes, setEdgesByNodeId } = useGraphStore((state) => state); | |||||
| const buildCategorizeEdgesByFormData = useCallback( | |||||
| (nodeId: string, form: ICategorizeForm) => { | |||||
| // add | |||||
| // delete | |||||
| // edit | |||||
| const categoryDescription = form.category_description; | |||||
| const downstreamEdges = Object.keys(categoryDescription).reduce<Edge[]>( | |||||
| (pre, sourceHandle) => { | |||||
| const target = categoryDescription[sourceHandle]?.to; | |||||
| if (target) { | |||||
| pre.push({ | |||||
| id: uuid(), | |||||
| source: nodeId, | |||||
| target, | |||||
| sourceHandle, | |||||
| }); | |||||
| } | |||||
| return pre; | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| setEdgesByNodeId(nodeId, downstreamEdges); | |||||
| }, | |||||
| [setEdgesByNodeId], | |||||
| ); | |||||
| const buildRelevantEdgesByFormData = useCallback( | |||||
| (nodeId: string, form: IRelevantForm) => { | |||||
| const downstreamEdges = ['yes', 'no'].reduce<Edge[]>((pre, cur) => { | |||||
| const target = form[cur as keyof IRelevantForm] as string; | |||||
| if (target) { | |||||
| pre.push({ id: uuid(), source: nodeId, target, sourceHandle: cur }); | |||||
| } | |||||
| return pre; | |||||
| }, []); | |||||
| setEdgesByNodeId(nodeId, downstreamEdges); | |||||
| }, | |||||
| [setEdgesByNodeId], | |||||
| ); | |||||
| useEffect(() => { | |||||
| nodes.forEach((node) => { | |||||
| const currentNode = getNode(node.id); | |||||
| const form = currentNode?.data.form ?? {}; | |||||
| const operatorType = currentNode?.data.label; | |||||
| switch (operatorType) { | |||||
| case Operator.Relevant: | |||||
| buildRelevantEdgesByFormData(node.id, form as IRelevantForm); | |||||
| break; | |||||
| case Operator.Categorize: | |||||
| buildCategorizeEdgesByFormData(node.id, form as ICategorizeForm); | |||||
| break; | |||||
| default: | |||||
| break; | |||||
| } | |||||
| }); | |||||
| }, [ | |||||
| nodes, | |||||
| buildCategorizeEdgesByFormData, | |||||
| getNode, | |||||
| buildRelevantEdgesByFormData, | |||||
| ]); | |||||
| }; |
| color: string; | color: string; | ||||
| form: IBeginForm | IRetrievalForm | IGenerateForm | ICategorizeForm; | form: IBeginForm | IRetrievalForm | IGenerateForm | ICategorizeForm; | ||||
| }; | }; | ||||
| export type IPosition = { top: number; right: number; idx: number }; |
| import { useTranslate } from '@/hooks/commonHooks'; | import { useTranslate } from '@/hooks/commonHooks'; | ||||
| import { Form, Select } from 'antd'; | import { Form, Select } from 'antd'; | ||||
| import { Operator } from '../constant'; | import { Operator } from '../constant'; | ||||
| import { | |||||
| useBuildFormSelectOptions, | |||||
| useHandleFormSelectChange, | |||||
| } from '../form-hooks'; | |||||
| import { useBuildFormSelectOptions } from '../form-hooks'; | |||||
| import { useSetLlmSetting } from '../hooks'; | import { useSetLlmSetting } from '../hooks'; | ||||
| import { IOperatorForm } from '../interface'; | import { IOperatorForm } from '../interface'; | ||||
| import { useWatchConnectionChanges } from './hooks'; | import { useWatchConnectionChanges } from './hooks'; | ||||
| node?.id, | node?.id, | ||||
| ); | ); | ||||
| useWatchConnectionChanges({ nodeId: node?.id, form }); | useWatchConnectionChanges({ nodeId: node?.id, form }); | ||||
| const { handleSelectChange } = useHandleFormSelectChange(node?.id); | |||||
| return ( | return ( | ||||
| <Form | <Form | ||||
| <Select | <Select | ||||
| allowClear | allowClear | ||||
| options={buildRelevantOptions([form?.getFieldValue('no')])} | options={buildRelevantOptions([form?.getFieldValue('no')])} | ||||
| onChange={handleSelectChange('yes')} | |||||
| /> | /> | ||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item label={t('no')} name={'no'}> | <Form.Item label={t('no')} name={'no'}> | ||||
| <Select | <Select | ||||
| allowClear | allowClear | ||||
| options={buildRelevantOptions([form?.getFieldValue('yes')])} | options={buildRelevantOptions([form?.getFieldValue('yes')])} | ||||
| onChange={handleSelectChange('no')} | |||||
| /> | /> | ||||
| </Form.Item> | </Form.Item> | ||||
| </Form> | </Form> |
| import type {} from '@redux-devtools/extension'; | import type {} from '@redux-devtools/extension'; | ||||
| import { humanId } from 'human-id'; | import { humanId } from 'human-id'; | ||||
| import differenceWith from 'lodash/differenceWith'; | |||||
| import intersectionWith from 'lodash/intersectionWith'; | |||||
| import lodashSet from 'lodash/set'; | import lodashSet from 'lodash/set'; | ||||
| import { | import { | ||||
| Connection, | Connection, | ||||
| import { immer } from 'zustand/middleware/immer'; | import { immer } from 'zustand/middleware/immer'; | ||||
| import { Operator } from './constant'; | import { Operator } from './constant'; | ||||
| import { NodeData } from './interface'; | import { NodeData } from './interface'; | ||||
| import { isEdgeEqual } from './utils'; | |||||
| export type RFState = { | export type RFState = { | ||||
| nodes: Node<NodeData>[]; | nodes: Node<NodeData>[]; | ||||
| onConnect: OnConnect; | onConnect: OnConnect; | ||||
| setNodes: (nodes: Node[]) => void; | setNodes: (nodes: Node[]) => void; | ||||
| setEdges: (edges: Edge[]) => void; | setEdges: (edges: Edge[]) => void; | ||||
| setEdgesByNodeId: (nodeId: string, edges: Edge[]) => void; | |||||
| updateNodeForm: (nodeId: string, values: any, path?: string[]) => void; | updateNodeForm: (nodeId: string, values: any, path?: string[]) => void; | ||||
| onSelectionChange: OnSelectionChangeFunc; | onSelectionChange: OnSelectionChangeFunc; | ||||
| addNode: (nodes: Node) => void; | addNode: (nodes: Node) => void; | ||||
| setEdges: (edges: Edge[]) => { | setEdges: (edges: Edge[]) => { | ||||
| set({ edges }); | set({ edges }); | ||||
| }, | }, | ||||
| setEdgesByNodeId: (nodeId: string, currentDownstreamEdges: Edge[]) => { | |||||
| const { edges, setEdges } = get(); | |||||
| // the previous downstream edge of this node | |||||
| const previousDownstreamEdges = edges.filter( | |||||
| (x) => x.source === nodeId, | |||||
| ); | |||||
| const isDifferent = | |||||
| previousDownstreamEdges.length !== currentDownstreamEdges.length || | |||||
| !previousDownstreamEdges.every((x) => | |||||
| currentDownstreamEdges.some( | |||||
| (y) => | |||||
| y.source === x.source && | |||||
| y.target === x.target && | |||||
| y.sourceHandle === x.sourceHandle, | |||||
| ), | |||||
| ) || | |||||
| !currentDownstreamEdges.every((x) => | |||||
| previousDownstreamEdges.some( | |||||
| (y) => | |||||
| y.source === x.source && | |||||
| y.target === x.target && | |||||
| y.sourceHandle === x.sourceHandle, | |||||
| ), | |||||
| ); | |||||
| const intersectionDownstreamEdges = intersectionWith( | |||||
| previousDownstreamEdges, | |||||
| currentDownstreamEdges, | |||||
| isEdgeEqual, | |||||
| ); | |||||
| if (isDifferent) { | |||||
| // other operator's edges | |||||
| const irrelevantEdges = edges.filter((x) => x.source !== nodeId); | |||||
| // the abandoned edges | |||||
| const selfAbandonedEdges = []; | |||||
| // the added downstream edges | |||||
| const selfAddedDownstreamEdges = differenceWith( | |||||
| currentDownstreamEdges, | |||||
| intersectionDownstreamEdges, | |||||
| isEdgeEqual, | |||||
| ); | |||||
| setEdges([ | |||||
| ...irrelevantEdges, | |||||
| ...intersectionDownstreamEdges, | |||||
| ...selfAddedDownstreamEdges, | |||||
| ]); | |||||
| } | |||||
| }, | |||||
| addNode: (node: Node) => { | addNode: (node: Node) => { | ||||
| set({ nodes: get().nodes.concat(node) }); | set({ nodes: get().nodes.concat(node) }); | ||||
| }, | }, | ||||
| set({ | set({ | ||||
| nodes: get().nodes.map((node) => { | nodes: get().nodes.map((node) => { | ||||
| if (node.id === nodeId) { | if (node.id === nodeId) { | ||||
| // node.data = { | |||||
| // ...node.data, | |||||
| // form: { ...node.data.form, ...values }, | |||||
| // }; | |||||
| let nextForm: Record<string, unknown> = { ...node.data.form }; | let nextForm: Record<string, unknown> = { ...node.data.form }; | ||||
| if (path.length === 0) { | if (path.length === 0) { | ||||
| nextForm = Object.assign(nextForm, values); | nextForm = Object.assign(nextForm, values); |
| import { removeUselessFieldsFromValues } from '@/utils/form'; | import { removeUselessFieldsFromValues } from '@/utils/form'; | ||||
| import dagre from 'dagre'; | import dagre from 'dagre'; | ||||
| import { humanId } from 'human-id'; | import { humanId } from 'human-id'; | ||||
| import { curry } from 'lodash'; | |||||
| import { curry, sample } from 'lodash'; | |||||
| import pipe from 'lodash/fp/pipe'; | import pipe from 'lodash/fp/pipe'; | ||||
| import isObject from 'lodash/isObject'; | import isObject from 'lodash/isObject'; | ||||
| import { Edge, Node, Position } from 'reactflow'; | import { Edge, Node, Position } from 'reactflow'; | ||||
| import { v4 as uuidv4 } from 'uuid'; | import { v4 as uuidv4 } from 'uuid'; | ||||
| import { NodeMap, Operator } from './constant'; | |||||
| import { ICategorizeItemResult, NodeData } from './interface'; | |||||
| import { CategorizeAnchorPointPositions, NodeMap, Operator } from './constant'; | |||||
| import { ICategorizeItemResult, IPosition, NodeData } from './interface'; | |||||
| const buildEdges = ( | const buildEdges = ( | ||||
| operatorIds: string[], | operatorIds: string[], | ||||
| return obj; | return obj; | ||||
| }; | }; | ||||
| export const isEdgeEqual = (previous: Edge, current: Edge) => | |||||
| previous.source === current.source && | |||||
| previous.target === current.target && | |||||
| previous.sourceHandle === current.sourceHandle; | |||||
| export const buildNewPositionMap = ( | |||||
| categoryDataKeys: string[], | |||||
| indexesInUse: number[], | |||||
| ) => { | |||||
| return categoryDataKeys.reduce<Record<string, IPosition>>((pre, cur) => { | |||||
| // take a coordinate | |||||
| const effectiveIdxes = CategorizeAnchorPointPositions.map( | |||||
| (x, idx) => idx, | |||||
| ).filter((x) => !indexesInUse.some((y) => y === x)); | |||||
| const idx = sample(effectiveIdxes); | |||||
| if (idx !== undefined) { | |||||
| indexesInUse.push(idx); | |||||
| pre[cur] = { ...CategorizeAnchorPointPositions[idx], idx }; | |||||
| } | |||||
| return pre; | |||||
| }, {}); | |||||
| }; |