balibabu commited on
Commit
970e973
·
1 Parent(s): 789efbc

fix: monitor changes in the data.form field of the categorize and relevant operators and then synchronize them to the edge #918 (#1469)

Browse files

### 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)

web/src/hooks/flow-hooks.ts CHANGED
@@ -93,6 +93,8 @@ export const useFetchFlow = (): {
93
  } = useQuery({
94
  queryKey: ['flowDetail'],
95
  initialData: {} as IFlow,
 
 
96
  queryFn: async () => {
97
  const { data } = await flowService.getCanvas({}, id);
98
 
 
93
  } = useQuery({
94
  queryKey: ['flowDetail'],
95
  initialData: {} as IFlow,
96
+ refetchOnReconnect: false,
97
+ refetchOnMount: false,
98
  queryFn: async () => {
99
  const { data } = await flowService.getCanvas({}, id);
100
 
web/src/locales/en.ts CHANGED
@@ -589,7 +589,7 @@ The above is the content you need to summarize.`,
589
  answer: 'Answer',
590
  categorize: 'Categorize',
591
  relevant: 'Relevant',
592
- rewriteQuestion: 'RewriteQuestion',
593
  rewrite: 'Rewrite',
594
  begin: 'Begin',
595
  message: 'Message',
 
589
  answer: 'Answer',
590
  categorize: 'Categorize',
591
  relevant: 'Relevant',
592
+ rewriteQuestion: 'Rewrite',
593
  rewrite: 'Rewrite',
594
  begin: 'Begin',
595
  message: 'Message',
web/src/pages/flow/canvas/edge/index.tsx CHANGED
@@ -6,7 +6,8 @@ import {
6
  } from 'reactflow';
7
  import useGraphStore from '../../store';
8
 
9
- import { useFetchFlow } from '@/hooks/flow-hooks';
 
10
  import { useMemo } from 'react';
11
  import styles from './index.less';
12
 
@@ -43,10 +44,12 @@ export function ButtonEdge({
43
  };
44
 
45
  // highlight the nodes that the workflow passes through
46
- const { data: flowDetail } = useFetchFlow();
 
 
47
  const graphPath = useMemo(() => {
48
  // TODO: this will be called multiple times
49
- const path = flowDetail.dsl.path ?? [];
50
  // The second to last
51
  const previousGraphPath: string[] = path.at(-2) ?? [];
52
  let graphPath: string[] = path.at(-1) ?? [];
@@ -56,7 +59,7 @@ export function ButtonEdge({
56
  graphPath = [previousLatestElement, ...graphPath];
57
  }
58
  return graphPath;
59
- }, [flowDetail.dsl.path]);
60
 
61
  const highlightStyle = useMemo(() => {
62
  const idx = graphPath.findIndex((x) => x === source);
 
6
  } from 'reactflow';
7
  import useGraphStore from '../../store';
8
 
9
+ import { IFlow } from '@/interfaces/database/flow';
10
+ import { useQueryClient } from '@tanstack/react-query';
11
  import { useMemo } from 'react';
12
  import styles from './index.less';
13
 
 
44
  };
45
 
46
  // highlight the nodes that the workflow passes through
47
+ const queryClient = useQueryClient();
48
+ const flowDetail = queryClient.getQueryData<IFlow>(['flowDetail']);
49
+
50
  const graphPath = useMemo(() => {
51
  // TODO: this will be called multiple times
52
+ const path = flowDetail?.dsl.path ?? [];
53
  // The second to last
54
  const previousGraphPath: string[] = path.at(-2) ?? [];
55
  let graphPath: string[] = path.at(-1) ?? [];
 
59
  graphPath = [previousLatestElement, ...graphPath];
60
  }
61
  return graphPath;
62
+ }, [flowDetail?.dsl.path]);
63
 
64
  const highlightStyle = useMemo(() => {
65
  const idx = graphPath.findIndex((x) => x === source);
web/src/pages/flow/canvas/index.tsx CHANGED
@@ -16,6 +16,7 @@ import {
16
  useSelectCanvasData,
17
  useShowDrawer,
18
  useValidateConnection,
 
19
  } from '../hooks';
20
  import { RagNode } from './node';
21
 
@@ -69,6 +70,7 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
69
  const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop();
70
 
71
  const { handleKeyUp } = useHandleKeyUp();
 
72
 
73
  return (
74
  <div className={styles.canvasWrapper}>
 
16
  useSelectCanvasData,
17
  useShowDrawer,
18
  useValidateConnection,
19
+ useWatchNodeFormDataChange,
20
  } from '../hooks';
21
  import { RagNode } from './node';
22
 
 
70
  const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop();
71
 
72
  const { handleKeyUp } = useHandleKeyUp();
73
+ useWatchNodeFormDataChange();
74
 
75
  return (
76
  <div className={styles.canvasWrapper}>
web/src/pages/flow/canvas/node/categorize-node.tsx CHANGED
@@ -1,25 +1,68 @@
1
  import { useTranslate } from '@/hooks/commonHooks';
2
  import { Flex } from 'antd';
3
  import classNames from 'classnames';
 
4
  import get from 'lodash/get';
 
 
5
  import lowerFirst from 'lodash/lowerFirst';
6
- import { Handle, NodeProps, Position } from 'reactflow';
7
- import {
8
- CategorizeAnchorPointPositions,
9
- Operator,
10
- operatorMap,
11
- } from '../../constant';
12
- import { NodeData } from '../../interface';
13
  import OperatorIcon from '../../operator-icon';
 
14
  import CategorizeHandle from './categorize-handle';
15
  import NodeDropdown from './dropdown';
16
  import styles from './index.less';
17
  import NodePopover from './popover';
18
 
19
  export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) {
20
- const categoryData = get(data, 'form.category_description') ?? {};
 
 
 
 
 
21
  const style = operatorMap[data.label as Operator];
22
  const { t } = useTranslate('flow');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  return (
24
  <NodePopover nodeId={id}>
25
  <section
@@ -53,14 +96,17 @@ export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) {
53
  id={'c'}
54
  ></Handle>
55
  {Object.keys(categoryData).map((x, idx) => {
 
56
  return (
57
- <CategorizeHandle
58
- top={CategorizeAnchorPointPositions[idx].top}
59
- right={CategorizeAnchorPointPositions[idx].right}
60
- key={idx}
61
- text={x}
62
- idx={idx}
63
- ></CategorizeHandle>
 
 
64
  );
65
  })}
66
  <Flex vertical align="center" justify="center" gap={6}>
 
1
  import { useTranslate } from '@/hooks/commonHooks';
2
  import { Flex } from 'antd';
3
  import classNames from 'classnames';
4
+ import { pick } from 'lodash';
5
  import get from 'lodash/get';
6
+ import intersectionWith from 'lodash/intersectionWith';
7
+ import isEqual from 'lodash/isEqual';
8
  import lowerFirst from 'lodash/lowerFirst';
9
+ import { useEffect, useMemo, useState } from 'react';
10
+ import { Handle, NodeProps, Position, useUpdateNodeInternals } from 'reactflow';
11
+ import { Operator, operatorMap } from '../../constant';
12
+ import { IPosition, NodeData } from '../../interface';
 
 
 
13
  import OperatorIcon from '../../operator-icon';
14
+ import { buildNewPositionMap } from '../../utils';
15
  import CategorizeHandle from './categorize-handle';
16
  import NodeDropdown from './dropdown';
17
  import styles from './index.less';
18
  import NodePopover from './popover';
19
 
20
  export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) {
21
+ const updateNodeInternals = useUpdateNodeInternals();
22
+ const [postionMap, setPositionMap] = useState<Record<string, IPosition>>({});
23
+ const categoryData = useMemo(
24
+ () => get(data, 'form.category_description') ?? {},
25
+ [data],
26
+ );
27
  const style = operatorMap[data.label as Operator];
28
  const { t } = useTranslate('flow');
29
+
30
+ useEffect(() => {
31
+ // Cache used coordinates
32
+ setPositionMap((state) => {
33
+ // index in use
34
+ const indexesInUse = Object.values(state).map((x) => x.idx);
35
+ const categoryDataKeys = Object.keys(categoryData);
36
+ const stateKeys = Object.keys(state);
37
+ if (!isEqual(categoryDataKeys.sort(), stateKeys.sort())) {
38
+ const intersectionKeys = intersectionWith(
39
+ stateKeys,
40
+ categoryDataKeys,
41
+ (categoryDataKey, postionMapKey) => categoryDataKey === postionMapKey,
42
+ );
43
+ const newPositionMap = buildNewPositionMap(
44
+ categoryDataKeys.filter(
45
+ (x) => !intersectionKeys.some((y) => y === x),
46
+ ),
47
+ indexesInUse,
48
+ );
49
+ console.info('newPositionMap:', newPositionMap);
50
+
51
+ const nextPostionMap = {
52
+ ...pick(state, intersectionKeys),
53
+ ...newPositionMap,
54
+ };
55
+
56
+ return nextPostionMap;
57
+ }
58
+ return state;
59
+ });
60
+ }, [categoryData]);
61
+
62
+ useEffect(() => {
63
+ updateNodeInternals(id);
64
+ }, [id, updateNodeInternals, postionMap]);
65
+
66
  return (
67
  <NodePopover nodeId={id}>
68
  <section
 
96
  id={'c'}
97
  ></Handle>
98
  {Object.keys(categoryData).map((x, idx) => {
99
+ const position = postionMap[x];
100
  return (
101
+ position && (
102
+ <CategorizeHandle
103
+ top={position.top}
104
+ right={position.right}
105
+ key={idx}
106
+ text={x}
107
+ idx={idx}
108
+ ></CategorizeHandle>
109
+ )
110
  );
111
  })}
112
  <Flex vertical align="center" justify="center" gap={6}>
web/src/pages/flow/categorize-form/dynamic-categorize.tsx CHANGED
@@ -1,12 +1,10 @@
1
  import { useTranslate } from '@/hooks/commonHooks';
2
  import { CloseOutlined } from '@ant-design/icons';
3
  import { Button, Card, Form, Input, Select } from 'antd';
 
4
  import { useUpdateNodeInternals } from 'reactflow';
5
  import { Operator } from '../constant';
6
- import {
7
- useBuildFormSelectOptions,
8
- useHandleFormSelectChange,
9
- } from '../form-hooks';
10
  import { ICategorizeItem } from '../interface';
11
 
12
  interface IProps {
@@ -20,7 +18,6 @@ const DynamicCategorize = ({ nodeId }: IProps) => {
20
  Operator.Categorize,
21
  nodeId,
22
  );
23
- const { handleSelectChange } = useHandleFormSelectChange(nodeId);
24
  const { t } = useTranslate('flow');
25
 
26
  return (
@@ -28,8 +25,7 @@ const DynamicCategorize = ({ nodeId }: IProps) => {
28
  <Form.List name="items">
29
  {(fields, { add, remove }) => {
30
  const handleAdd = () => {
31
- const idx = fields.length;
32
- add({ name: `Categorize ${idx + 1}` });
33
  if (nodeId) updateNodeInternals(nodeId);
34
  };
35
  return (
@@ -79,9 +75,6 @@ const DynamicCategorize = ({ nodeId }: IProps) => {
79
  form.getFieldValue(['items', field.name, 'to']),
80
  ),
81
  )}
82
- onChange={handleSelectChange(
83
- form.getFieldValue(['items', field.name, 'name']),
84
- )}
85
  />
86
  </Form.Item>
87
  </Card>
 
1
  import { useTranslate } from '@/hooks/commonHooks';
2
  import { CloseOutlined } from '@ant-design/icons';
3
  import { Button, Card, Form, Input, Select } from 'antd';
4
+ import { humanId } from 'human-id';
5
  import { useUpdateNodeInternals } from 'reactflow';
6
  import { Operator } from '../constant';
7
+ import { useBuildFormSelectOptions } from '../form-hooks';
 
 
 
8
  import { ICategorizeItem } from '../interface';
9
 
10
  interface IProps {
 
18
  Operator.Categorize,
19
  nodeId,
20
  );
 
21
  const { t } = useTranslate('flow');
22
 
23
  return (
 
25
  <Form.List name="items">
26
  {(fields, { add, remove }) => {
27
  const handleAdd = () => {
28
+ add({ name: humanId() });
 
29
  if (nodeId) updateNodeInternals(nodeId);
30
  };
31
  return (
 
75
  form.getFieldValue(['items', field.name, 'to']),
76
  ),
77
  )}
 
 
 
78
  />
79
  </Form.Item>
80
  </Card>
web/src/pages/flow/categorize-form/hooks.ts CHANGED
@@ -1,12 +1,10 @@
1
  import get from 'lodash/get';
2
  import omit from 'lodash/omit';
3
  import { useCallback, useEffect } from 'react';
4
- import { Edge, Node } from 'reactflow';
5
  import {
6
  ICategorizeItem,
7
  ICategorizeItemResult,
8
  IOperatorForm,
9
- NodeData,
10
  } from '../interface';
11
  import useGraphStore from '../store';
12
 
@@ -23,18 +21,14 @@ import useGraphStore from '../store';
23
  */
24
  const buildCategorizeListFromObject = (
25
  categorizeItem: ICategorizeItemResult,
26
- edges: Edge[],
27
- node?: Node<NodeData>,
28
  ) => {
29
  // Categorize's to field has two data sources, with edges as the data source.
30
  // Changes in the edge or to field need to be synchronized to the form field.
31
  return Object.keys(categorizeItem).reduce<Array<ICategorizeItem>>(
32
  (pre, cur) => {
33
  // synchronize edge data to the to field
34
- const edge = edges.find(
35
- (x) => x.source === node?.id && x.sourceHandle === cur,
36
- );
37
- pre.push({ name: cur, ...categorizeItem[cur], to: edge?.target });
38
  return pre;
39
  },
40
  [],
@@ -68,7 +62,6 @@ export const useHandleFormValuesChange = ({
68
  form,
69
  nodeId,
70
  }: IOperatorForm) => {
71
- const edges = useGraphStore((state) => state.edges);
72
  const getNode = useGraphStore((state) => state.getNode);
73
  const node = getNode(nodeId);
74
 
@@ -86,13 +79,12 @@ export const useHandleFormValuesChange = ({
86
  useEffect(() => {
87
  const items = buildCategorizeListFromObject(
88
  get(node, 'data.form.category_description', {}),
89
- edges,
90
- node,
91
  );
 
92
  form?.setFieldsValue({
93
  items,
94
  });
95
- }, [form, node, edges]);
96
 
97
  return { handleValuesChange };
98
  };
 
1
  import get from 'lodash/get';
2
  import omit from 'lodash/omit';
3
  import { useCallback, useEffect } from 'react';
 
4
  import {
5
  ICategorizeItem,
6
  ICategorizeItemResult,
7
  IOperatorForm,
 
8
  } from '../interface';
9
  import useGraphStore from '../store';
10
 
 
21
  */
22
  const buildCategorizeListFromObject = (
23
  categorizeItem: ICategorizeItemResult,
 
 
24
  ) => {
25
  // Categorize's to field has two data sources, with edges as the data source.
26
  // Changes in the edge or to field need to be synchronized to the form field.
27
  return Object.keys(categorizeItem).reduce<Array<ICategorizeItem>>(
28
  (pre, cur) => {
29
  // synchronize edge data to the to field
30
+
31
+ pre.push({ name: cur, ...categorizeItem[cur] });
 
 
32
  return pre;
33
  },
34
  [],
 
62
  form,
63
  nodeId,
64
  }: IOperatorForm) => {
 
65
  const getNode = useGraphStore((state) => state.getNode);
66
  const node = getNode(nodeId);
67
 
 
79
  useEffect(() => {
80
  const items = buildCategorizeListFromObject(
81
  get(node, 'data.form.category_description', {}),
 
 
82
  );
83
+ console.info('effect:', items);
84
  form?.setFieldsValue({
85
  items,
86
  });
87
+ }, [form, node]);
88
 
89
  return { handleValuesChange };
90
  };
web/src/pages/flow/form-hooks.ts CHANGED
@@ -33,6 +33,11 @@ export const useBuildFormSelectOptions = (
33
  return buildCategorizeToOptions;
34
  };
35
 
 
 
 
 
 
36
  export const useHandleFormSelectChange = (nodeId?: string) => {
37
  const { addEdge, deleteEdgeBySourceAndSourceHandle } = useGraphStore(
38
  (state) => state,
 
33
  return buildCategorizeToOptions;
34
  };
35
 
36
+ /**
37
+ * dumped
38
+ * @param nodeId
39
+ * @returns
40
+ */
41
  export const useHandleFormSelectChange = (nodeId?: string) => {
42
  const { addEdge, deleteEdgeBySourceAndSourceHandle } = useGraphStore(
43
  (state) => state,
web/src/pages/flow/generate-form/hooks.ts CHANGED
@@ -27,12 +27,10 @@ export const useHandleOperateParameters = (nodeId: string) => {
27
  const { getNode, updateNodeForm } = useGraphStore((state) => state);
28
  const node = getNode(nodeId);
29
  const dataSource: IGenerateParameter[] = useMemo(
30
- () => get(node, 'data.form.parameters', []),
31
  [node],
32
  );
33
 
34
- // const [x, setDataSource] = useState<IGenerateParameter[]>([]);
35
-
36
  const handleComponentIdChange = useCallback(
37
  (row: IGenerateParameter) => (value: string) => {
38
  const newData = [...dataSource];
@@ -44,7 +42,6 @@ export const useHandleOperateParameters = (nodeId: string) => {
44
  });
45
 
46
  updateNodeForm(nodeId, { parameters: newData });
47
- // setDataSource(newData);
48
  },
49
  [updateNodeForm, nodeId, dataSource],
50
  );
@@ -53,20 +50,11 @@ export const useHandleOperateParameters = (nodeId: string) => {
53
  (id?: string) => () => {
54
  const newData = dataSource.filter((item) => item.id !== id);
55
  updateNodeForm(nodeId, { parameters: newData });
56
- // setDataSource(newData);
57
  },
58
  [updateNodeForm, nodeId, dataSource],
59
  );
60
 
61
  const handleAdd = useCallback(() => {
62
- // setDataSource((state) => [
63
- // ...state,
64
- // {
65
- // id: uuid(),
66
- // key: '',
67
- // component_id: undefined,
68
- // },
69
- // ]);
70
  updateNodeForm(nodeId, {
71
  parameters: [
72
  ...dataSource,
@@ -89,7 +77,6 @@ export const useHandleOperateParameters = (nodeId: string) => {
89
  });
90
 
91
  updateNodeForm(nodeId, { parameters: newData });
92
- // setDataSource(newData);
93
  };
94
 
95
  return {
 
27
  const { getNode, updateNodeForm } = useGraphStore((state) => state);
28
  const node = getNode(nodeId);
29
  const dataSource: IGenerateParameter[] = useMemo(
30
+ () => get(node, 'data.form.parameters', []) as IGenerateParameter[],
31
  [node],
32
  );
33
 
 
 
34
  const handleComponentIdChange = useCallback(
35
  (row: IGenerateParameter) => (value: string) => {
36
  const newData = [...dataSource];
 
42
  });
43
 
44
  updateNodeForm(nodeId, { parameters: newData });
 
45
  },
46
  [updateNodeForm, nodeId, dataSource],
47
  );
 
50
  (id?: string) => () => {
51
  const newData = dataSource.filter((item) => item.id !== id);
52
  updateNodeForm(nodeId, { parameters: newData });
 
53
  },
54
  [updateNodeForm, nodeId, dataSource],
55
  );
56
 
57
  const handleAdd = useCallback(() => {
 
 
 
 
 
 
 
 
58
  updateNodeForm(nodeId, {
59
  parameters: [
60
  ...dataSource,
 
77
  });
78
 
79
  updateNodeForm(nodeId, { parameters: newData });
 
80
  };
81
 
82
  return {
web/src/pages/flow/hooks.ts CHANGED
@@ -10,7 +10,7 @@ import React, {
10
  useEffect,
11
  useState,
12
  } from 'react';
13
- import { Connection, Node, Position, ReactFlowInstance } from 'reactflow';
14
  // import { shallow } from 'zustand/shallow';
15
  import { variableEnabledFieldMap } from '@/constants/chat';
16
  import {
@@ -25,6 +25,7 @@ import { FormInstance, message } from 'antd';
25
  import { humanId } from 'human-id';
26
  import trim from 'lodash/trim';
27
  import { useParams } from 'umi';
 
28
  import {
29
  NodeMap,
30
  Operator,
@@ -37,6 +38,7 @@ import {
37
  initialRetrievalValues,
38
  initialRewriteQuestionValues,
39
  } from './constant';
 
40
  import useGraphStore, { RFState } from './store';
41
  import {
42
  buildDslComponentsByGraph,
@@ -253,7 +255,7 @@ const useSetGraphInfo = () => {
253
  };
254
 
255
  export const useFetchDataOnMount = () => {
256
- const { loading, data } = useFetchFlow();
257
  const setGraphInfo = useSetGraphInfo();
258
 
259
  useEffect(() => {
@@ -264,6 +266,10 @@ export const useFetchDataOnMount = () => {
264
 
265
  useFetchLlmList();
266
 
 
 
 
 
267
  return { loading, flowDetail: data };
268
  };
269
 
@@ -390,3 +396,78 @@ export const useReplaceIdWithText = (output: unknown) => {
390
 
391
  return replaceIdWithText(output, getNameById);
392
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  useEffect,
11
  useState,
12
  } from 'react';
13
+ import { Connection, Edge, Node, Position, ReactFlowInstance } from 'reactflow';
14
  // import { shallow } from 'zustand/shallow';
15
  import { variableEnabledFieldMap } from '@/constants/chat';
16
  import {
 
25
  import { humanId } from 'human-id';
26
  import trim from 'lodash/trim';
27
  import { useParams } from 'umi';
28
+ import { v4 as uuid } from 'uuid';
29
  import {
30
  NodeMap,
31
  Operator,
 
38
  initialRetrievalValues,
39
  initialRewriteQuestionValues,
40
  } from './constant';
41
+ import { ICategorizeForm, IRelevantForm } from './interface';
42
  import useGraphStore, { RFState } from './store';
43
  import {
44
  buildDslComponentsByGraph,
 
255
  };
256
 
257
  export const useFetchDataOnMount = () => {
258
+ const { loading, data, refetch } = useFetchFlow();
259
  const setGraphInfo = useSetGraphInfo();
260
 
261
  useEffect(() => {
 
266
 
267
  useFetchLlmList();
268
 
269
+ useEffect(() => {
270
+ refetch();
271
+ }, [refetch]);
272
+
273
  return { loading, flowDetail: data };
274
  };
275
 
 
396
 
397
  return replaceIdWithText(output, getNameById);
398
  };
399
+
400
+ /**
401
+ * monitor changes in the data.form field of the categorize and relevant operators
402
+ * and then synchronize them to the edge
403
+ */
404
+ export const useWatchNodeFormDataChange = () => {
405
+ const { getNode, nodes, setEdgesByNodeId } = useGraphStore((state) => state);
406
+
407
+ const buildCategorizeEdgesByFormData = useCallback(
408
+ (nodeId: string, form: ICategorizeForm) => {
409
+ // add
410
+ // delete
411
+ // edit
412
+ const categoryDescription = form.category_description;
413
+ const downstreamEdges = Object.keys(categoryDescription).reduce<Edge[]>(
414
+ (pre, sourceHandle) => {
415
+ const target = categoryDescription[sourceHandle]?.to;
416
+ if (target) {
417
+ pre.push({
418
+ id: uuid(),
419
+ source: nodeId,
420
+ target,
421
+ sourceHandle,
422
+ });
423
+ }
424
+
425
+ return pre;
426
+ },
427
+ [],
428
+ );
429
+
430
+ setEdgesByNodeId(nodeId, downstreamEdges);
431
+ },
432
+ [setEdgesByNodeId],
433
+ );
434
+
435
+ const buildRelevantEdgesByFormData = useCallback(
436
+ (nodeId: string, form: IRelevantForm) => {
437
+ const downstreamEdges = ['yes', 'no'].reduce<Edge[]>((pre, cur) => {
438
+ const target = form[cur as keyof IRelevantForm] as string;
439
+ if (target) {
440
+ pre.push({ id: uuid(), source: nodeId, target, sourceHandle: cur });
441
+ }
442
+
443
+ return pre;
444
+ }, []);
445
+
446
+ setEdgesByNodeId(nodeId, downstreamEdges);
447
+ },
448
+ [setEdgesByNodeId],
449
+ );
450
+
451
+ useEffect(() => {
452
+ nodes.forEach((node) => {
453
+ const currentNode = getNode(node.id);
454
+ const form = currentNode?.data.form ?? {};
455
+ const operatorType = currentNode?.data.label;
456
+ switch (operatorType) {
457
+ case Operator.Relevant:
458
+ buildRelevantEdgesByFormData(node.id, form as IRelevantForm);
459
+ break;
460
+ case Operator.Categorize:
461
+ buildCategorizeEdgesByFormData(node.id, form as ICategorizeForm);
462
+ break;
463
+ default:
464
+ break;
465
+ }
466
+ });
467
+ }, [
468
+ nodes,
469
+ buildCategorizeEdgesByFormData,
470
+ getNode,
471
+ buildRelevantEdgesByFormData,
472
+ ]);
473
+ };
web/src/pages/flow/interface.ts CHANGED
@@ -70,3 +70,5 @@ export type NodeData = {
70
  color: string;
71
  form: IBeginForm | IRetrievalForm | IGenerateForm | ICategorizeForm;
72
  };
 
 
 
70
  color: string;
71
  form: IBeginForm | IRetrievalForm | IGenerateForm | ICategorizeForm;
72
  };
73
+
74
+ export type IPosition = { top: number; right: number; idx: number };
web/src/pages/flow/relevant-form/index.tsx CHANGED
@@ -2,10 +2,7 @@ import LLMSelect from '@/components/llm-select';
2
  import { useTranslate } from '@/hooks/commonHooks';
3
  import { Form, Select } from 'antd';
4
  import { Operator } from '../constant';
5
- import {
6
- useBuildFormSelectOptions,
7
- useHandleFormSelectChange,
8
- } from '../form-hooks';
9
  import { useSetLlmSetting } from '../hooks';
10
  import { IOperatorForm } from '../interface';
11
  import { useWatchConnectionChanges } from './hooks';
@@ -18,7 +15,6 @@ const RelevantForm = ({ onValuesChange, form, node }: IOperatorForm) => {
18
  node?.id,
19
  );
20
  useWatchConnectionChanges({ nodeId: node?.id, form });
21
- const { handleSelectChange } = useHandleFormSelectChange(node?.id);
22
 
23
  return (
24
  <Form
@@ -40,14 +36,12 @@ const RelevantForm = ({ onValuesChange, form, node }: IOperatorForm) => {
40
  <Select
41
  allowClear
42
  options={buildRelevantOptions([form?.getFieldValue('no')])}
43
- onChange={handleSelectChange('yes')}
44
  />
45
  </Form.Item>
46
  <Form.Item label={t('no')} name={'no'}>
47
  <Select
48
  allowClear
49
  options={buildRelevantOptions([form?.getFieldValue('yes')])}
50
- onChange={handleSelectChange('no')}
51
  />
52
  </Form.Item>
53
  </Form>
 
2
  import { useTranslate } from '@/hooks/commonHooks';
3
  import { Form, Select } from 'antd';
4
  import { Operator } from '../constant';
5
+ import { useBuildFormSelectOptions } from '../form-hooks';
 
 
 
6
  import { useSetLlmSetting } from '../hooks';
7
  import { IOperatorForm } from '../interface';
8
  import { useWatchConnectionChanges } from './hooks';
 
15
  node?.id,
16
  );
17
  useWatchConnectionChanges({ nodeId: node?.id, form });
 
18
 
19
  return (
20
  <Form
 
36
  <Select
37
  allowClear
38
  options={buildRelevantOptions([form?.getFieldValue('no')])}
 
39
  />
40
  </Form.Item>
41
  <Form.Item label={t('no')} name={'no'}>
42
  <Select
43
  allowClear
44
  options={buildRelevantOptions([form?.getFieldValue('yes')])}
 
45
  />
46
  </Form.Item>
47
  </Form>
web/src/pages/flow/store.ts CHANGED
@@ -1,5 +1,7 @@
1
  import type {} from '@redux-devtools/extension';
2
  import { humanId } from 'human-id';
 
 
3
  import lodashSet from 'lodash/set';
4
  import {
5
  Connection,
@@ -21,6 +23,7 @@ import { devtools } from 'zustand/middleware';
21
  import { immer } from 'zustand/middleware/immer';
22
  import { Operator } from './constant';
23
  import { NodeData } from './interface';
 
24
 
25
  export type RFState = {
26
  nodes: Node<NodeData>[];
@@ -33,6 +36,7 @@ export type RFState = {
33
  onConnect: OnConnect;
34
  setNodes: (nodes: Node[]) => void;
35
  setEdges: (edges: Edge[]) => void;
 
36
  updateNodeForm: (nodeId: string, values: any, path?: string[]) => void;
37
  onSelectionChange: OnSelectionChangeFunc;
38
  addNode: (nodes: Node) => void;
@@ -95,6 +99,55 @@ const useGraphStore = create<RFState>()(
95
  setEdges: (edges: Edge[]) => {
96
  set({ edges });
97
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  addNode: (node: Node) => {
99
  set({ nodes: get().nodes.concat(node) });
100
  },
@@ -242,10 +295,6 @@ const useGraphStore = create<RFState>()(
242
  set({
243
  nodes: get().nodes.map((node) => {
244
  if (node.id === nodeId) {
245
- // node.data = {
246
- // ...node.data,
247
- // form: { ...node.data.form, ...values },
248
- // };
249
  let nextForm: Record<string, unknown> = { ...node.data.form };
250
  if (path.length === 0) {
251
  nextForm = Object.assign(nextForm, values);
 
1
  import type {} from '@redux-devtools/extension';
2
  import { humanId } from 'human-id';
3
+ import differenceWith from 'lodash/differenceWith';
4
+ import intersectionWith from 'lodash/intersectionWith';
5
  import lodashSet from 'lodash/set';
6
  import {
7
  Connection,
 
23
  import { immer } from 'zustand/middleware/immer';
24
  import { Operator } from './constant';
25
  import { NodeData } from './interface';
26
+ import { isEdgeEqual } from './utils';
27
 
28
  export type RFState = {
29
  nodes: Node<NodeData>[];
 
36
  onConnect: OnConnect;
37
  setNodes: (nodes: Node[]) => void;
38
  setEdges: (edges: Edge[]) => void;
39
+ setEdgesByNodeId: (nodeId: string, edges: Edge[]) => void;
40
  updateNodeForm: (nodeId: string, values: any, path?: string[]) => void;
41
  onSelectionChange: OnSelectionChangeFunc;
42
  addNode: (nodes: Node) => void;
 
99
  setEdges: (edges: Edge[]) => {
100
  set({ edges });
101
  },
102
+ setEdgesByNodeId: (nodeId: string, currentDownstreamEdges: Edge[]) => {
103
+ const { edges, setEdges } = get();
104
+ // the previous downstream edge of this node
105
+ const previousDownstreamEdges = edges.filter(
106
+ (x) => x.source === nodeId,
107
+ );
108
+ const isDifferent =
109
+ previousDownstreamEdges.length !== currentDownstreamEdges.length ||
110
+ !previousDownstreamEdges.every((x) =>
111
+ currentDownstreamEdges.some(
112
+ (y) =>
113
+ y.source === x.source &&
114
+ y.target === x.target &&
115
+ y.sourceHandle === x.sourceHandle,
116
+ ),
117
+ ) ||
118
+ !currentDownstreamEdges.every((x) =>
119
+ previousDownstreamEdges.some(
120
+ (y) =>
121
+ y.source === x.source &&
122
+ y.target === x.target &&
123
+ y.sourceHandle === x.sourceHandle,
124
+ ),
125
+ );
126
+
127
+ const intersectionDownstreamEdges = intersectionWith(
128
+ previousDownstreamEdges,
129
+ currentDownstreamEdges,
130
+ isEdgeEqual,
131
+ );
132
+ if (isDifferent) {
133
+ // other operator's edges
134
+ const irrelevantEdges = edges.filter((x) => x.source !== nodeId);
135
+ // the abandoned edges
136
+ const selfAbandonedEdges = [];
137
+ // the added downstream edges
138
+ const selfAddedDownstreamEdges = differenceWith(
139
+ currentDownstreamEdges,
140
+ intersectionDownstreamEdges,
141
+ isEdgeEqual,
142
+ );
143
+ setEdges([
144
+ ...irrelevantEdges,
145
+ ...intersectionDownstreamEdges,
146
+ ...selfAddedDownstreamEdges,
147
+ ]);
148
+ }
149
+ },
150
+
151
  addNode: (node: Node) => {
152
  set({ nodes: get().nodes.concat(node) });
153
  },
 
295
  set({
296
  nodes: get().nodes.map((node) => {
297
  if (node.id === nodeId) {
 
 
 
 
298
  let nextForm: Record<string, unknown> = { ...node.data.form };
299
  if (path.length === 0) {
300
  nextForm = Object.assign(nextForm, values);
web/src/pages/flow/utils.ts CHANGED
@@ -2,13 +2,13 @@ import { DSLComponents } from '@/interfaces/database/flow';
2
  import { removeUselessFieldsFromValues } from '@/utils/form';
3
  import dagre from 'dagre';
4
  import { humanId } from 'human-id';
5
- import { curry } from 'lodash';
6
  import pipe from 'lodash/fp/pipe';
7
  import isObject from 'lodash/isObject';
8
  import { Edge, Node, Position } from 'reactflow';
9
  import { v4 as uuidv4 } from 'uuid';
10
- import { NodeMap, Operator } from './constant';
11
- import { ICategorizeItemResult, NodeData } from './interface';
12
 
13
  const buildEdges = (
14
  operatorIds: string[],
@@ -208,3 +208,27 @@ export const replaceIdWithText = (
208
 
209
  return obj;
210
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import { removeUselessFieldsFromValues } from '@/utils/form';
3
  import dagre from 'dagre';
4
  import { humanId } from 'human-id';
5
+ import { curry, sample } from 'lodash';
6
  import pipe from 'lodash/fp/pipe';
7
  import isObject from 'lodash/isObject';
8
  import { Edge, Node, Position } from 'reactflow';
9
  import { v4 as uuidv4 } from 'uuid';
10
+ import { CategorizeAnchorPointPositions, NodeMap, Operator } from './constant';
11
+ import { ICategorizeItemResult, IPosition, NodeData } from './interface';
12
 
13
  const buildEdges = (
14
  operatorIds: string[],
 
208
 
209
  return obj;
210
  };
211
+
212
+ export const isEdgeEqual = (previous: Edge, current: Edge) =>
213
+ previous.source === current.source &&
214
+ previous.target === current.target &&
215
+ previous.sourceHandle === current.sourceHandle;
216
+
217
+ export const buildNewPositionMap = (
218
+ categoryDataKeys: string[],
219
+ indexesInUse: number[],
220
+ ) => {
221
+ return categoryDataKeys.reduce<Record<string, IPosition>>((pre, cur) => {
222
+ // take a coordinate
223
+ const effectiveIdxes = CategorizeAnchorPointPositions.map(
224
+ (x, idx) => idx,
225
+ ).filter((x) => !indexesInUse.some((y) => y === x));
226
+ const idx = sample(effectiveIdxes);
227
+ if (idx !== undefined) {
228
+ indexesInUse.push(idx);
229
+ pre[cur] = { ...CategorizeAnchorPointPositions[idx], idx };
230
+ }
231
+
232
+ return pre;
233
+ }, {});
234
+ };