balibabu commited on
Commit
36669dc
·
1 Parent(s): 97cc326

feat: add CategorizeHandle #918 (#1282)

Browse files

### What problem does this PR solve?

feat: add CategorizeHandle #918

### Type of change


- [x] New Feature (non-breaking change which adds functionality)

web/src/pages/flow/canvas/node/categorize-handle.tsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Handle, Position } from 'reactflow';
2
+ // import { v4 as uuid } from 'uuid';
3
+
4
+ import styles from './index.less';
5
+
6
+ const DEFAULT_HANDLE_STYLE = {
7
+ width: 6,
8
+ height: 6,
9
+ bottom: -5,
10
+ fontSize: 8,
11
+ };
12
+
13
+ interface IProps {
14
+ top: number;
15
+ right: number;
16
+ text: string;
17
+ idx: number;
18
+ }
19
+
20
+ const CategorizeHandle = ({ top, right, text, idx }: IProps) => {
21
+ return (
22
+ <Handle
23
+ type="source"
24
+ position={Position.Right}
25
+ id={`CategorizeHandle${idx}`}
26
+ isConnectable
27
+ style={{
28
+ ...DEFAULT_HANDLE_STYLE,
29
+ top: `${top}%`,
30
+ right: `${right}%`,
31
+ background: 'red',
32
+ }}
33
+ >
34
+ <span className={styles.categorizeAnchorPointText}>{text}</span>
35
+ </Handle>
36
+ );
37
+ };
38
+
39
+ export default CategorizeHandle;
web/src/pages/flow/canvas/node/index.less CHANGED
@@ -37,6 +37,12 @@
37
  -3px 0 6px -4px rgba(0, 0, 0, 0.12),
38
  -6px 0 16px 6px rgba(0, 0, 0, 0.05);
39
  }
 
 
 
 
 
 
40
  }
41
  .selectedNode {
42
  border: 1px solid rgb(59, 118, 244);
 
37
  -3px 0 6px -4px rgba(0, 0, 0, 0.12),
38
  -6px 0 16px 6px rgba(0, 0, 0, 0.05);
39
  }
40
+ .categorizeAnchorPointText {
41
+ position: absolute;
42
+ top: -4px;
43
+ left: 8px;
44
+ white-space: nowrap;
45
+ }
46
  }
47
  .selectedNode {
48
  border: 1px solid rgb(59, 118, 244);
web/src/pages/flow/canvas/node/index.tsx CHANGED
@@ -4,12 +4,14 @@ import { Handle, NodeProps, Position } from 'reactflow';
4
  import OperateDropdown from '@/components/operate-dropdown';
5
  import { CopyOutlined } from '@ant-design/icons';
6
  import { Flex, MenuProps, Space } from 'antd';
 
7
  import { useCallback } from 'react';
8
  import { useTranslation } from 'react-i18next';
9
- import { Operator, operatorMap } from '../../constant';
10
  import { NodeData } from '../../interface';
11
  import OperatorIcon from '../../operator-icon';
12
  import useGraphStore from '../../store';
 
13
  import styles from './index.less';
14
 
15
  export function RagNode({
@@ -30,7 +32,8 @@ export function RagNode({
30
  duplicateNodeById(id);
31
  }, [id, duplicateNodeById]);
32
 
33
- const description = operatorMap[data.label as Operator].description;
 
34
 
35
  const items: MenuProps['items'] = [
36
  {
@@ -57,9 +60,7 @@ export function RagNode({
57
  position={Position.Left}
58
  isConnectable={isConnectable}
59
  className={styles.handle}
60
- >
61
- {/* <PlusCircleOutlined style={{ fontSize: 10 }} /> */}
62
- </Handle>
63
  <Handle type="source" position={Position.Top} id="d" isConnectable />
64
  <Handle
65
  type="source"
@@ -67,34 +68,32 @@ export function RagNode({
67
  isConnectable={isConnectable}
68
  className={styles.handle}
69
  id="b"
70
- >
71
- {/* <PlusCircleOutlined style={{ fontSize: 10 }} /> */}
72
- </Handle>
73
  <Handle type="source" position={Position.Bottom} id="a" isConnectable />
 
 
 
 
 
 
 
 
 
 
74
  <Flex vertical align="center" justify="center">
75
  <Space size={6}>
76
  <OperatorIcon
77
  name={data.label as Operator}
78
  fontSize={16}
79
  ></OperatorIcon>
80
- {/* {data.label} */}
81
  <OperateDropdown
82
  iconFontSize={14}
83
  deleteItem={deleteNode}
84
  items={items}
85
  ></OperateDropdown>
86
  </Space>
87
- {/* <div className={styles.nodeName}>{id}</div> */}
88
  </Flex>
89
- {/* <div>
90
- <Text
91
- ellipsis={{ tooltip: description }}
92
- style={{ width: 130 }}
93
- className={styles.description}
94
- >
95
- {description}
96
- </Text>
97
- </div> */}
98
  <section className={styles.bottomBox}>
99
  <div className={styles.nodeName}>{id}</div>
100
  </section>
 
4
  import OperateDropdown from '@/components/operate-dropdown';
5
  import { CopyOutlined } from '@ant-design/icons';
6
  import { Flex, MenuProps, Space } from 'antd';
7
+ import get from 'lodash/get';
8
  import { useCallback } from 'react';
9
  import { useTranslation } from 'react-i18next';
10
+ import { CategorizeAnchorPointPositions, Operator } from '../../constant';
11
  import { NodeData } from '../../interface';
12
  import OperatorIcon from '../../operator-icon';
13
  import useGraphStore from '../../store';
14
+ import CategorizeHandle from './categorize-handle';
15
  import styles from './index.less';
16
 
17
  export function RagNode({
 
32
  duplicateNodeById(id);
33
  }, [id, duplicateNodeById]);
34
 
35
+ const isCategorize = data.label === Operator.Categorize;
36
+ const categoryData = get(data, 'form.category_description') ?? {};
37
 
38
  const items: MenuProps['items'] = [
39
  {
 
60
  position={Position.Left}
61
  isConnectable={isConnectable}
62
  className={styles.handle}
63
+ ></Handle>
 
 
64
  <Handle type="source" position={Position.Top} id="d" isConnectable />
65
  <Handle
66
  type="source"
 
68
  isConnectable={isConnectable}
69
  className={styles.handle}
70
  id="b"
71
+ ></Handle>
 
 
72
  <Handle type="source" position={Position.Bottom} id="a" isConnectable />
73
+ {isCategorize &&
74
+ Object.keys(categoryData).map((x, idx) => (
75
+ <CategorizeHandle
76
+ top={CategorizeAnchorPointPositions[idx].top}
77
+ right={CategorizeAnchorPointPositions[idx].right}
78
+ key={idx}
79
+ text={x}
80
+ idx={idx}
81
+ ></CategorizeHandle>
82
+ ))}
83
  <Flex vertical align="center" justify="center">
84
  <Space size={6}>
85
  <OperatorIcon
86
  name={data.label as Operator}
87
  fontSize={16}
88
  ></OperatorIcon>
 
89
  <OperateDropdown
90
  iconFontSize={14}
91
  deleteItem={deleteNode}
92
  items={items}
93
  ></OperateDropdown>
94
  </Space>
 
95
  </Flex>
96
+
 
 
 
 
 
 
 
 
97
  <section className={styles.bottomBox}>
98
  <div className={styles.nodeName}>{id}</div>
99
  </section>
web/src/pages/flow/categorize-form/dynamic-categorize.tsx CHANGED
@@ -1,58 +1,78 @@
1
  import { CloseOutlined } from '@ant-design/icons';
2
  import { Button, Card, Form, Input, Select, Typography } from 'antd';
3
- import { useBuildCategorizeToOptions } from './hooks';
4
 
5
- const DynamicCategorize = () => {
 
 
 
 
6
  const form = Form.useFormInstance();
7
  const options = useBuildCategorizeToOptions();
 
 
 
 
8
 
9
  return (
10
  <>
11
  <Form.List name="items">
12
- {(fields, { add, remove }) => (
13
- <div style={{ display: 'flex', rowGap: 16, flexDirection: 'column' }}>
14
- {fields.map((field) => (
15
- <Card
16
- size="small"
17
- key={field.key}
18
- extra={
19
- <CloseOutlined
20
- onClick={() => {
21
- remove(field.name);
22
- }}
23
- />
24
- }
25
- >
26
- <Form.Item
27
- label="name"
28
- name={[field.name, 'name']}
29
- initialValue={`Categorize ${field.name + 1}`}
30
- rules={[
31
- { required: true, message: 'Please input your name!' },
32
- ]}
33
  >
34
- <Input />
35
- </Form.Item>
36
- <Form.Item
37
- label="description"
38
- name={[field.name, 'description']}
39
- >
40
- <Input.TextArea rows={3} />
41
- </Form.Item>
42
- <Form.Item label="examples" name={[field.name, 'examples']}>
43
- <Input.TextArea rows={3} />
44
- </Form.Item>
45
- <Form.Item label="to" name={[field.name, 'to']}>
46
- <Select options={options} />
47
- </Form.Item>
48
- </Card>
49
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- <Button type="dashed" onClick={() => add()} block>
52
- + Add Item
53
- </Button>
54
- </div>
55
- )}
 
56
  </Form.List>
57
 
58
  <Form.Item noStyle shouldUpdate>
 
1
  import { CloseOutlined } from '@ant-design/icons';
2
  import { Button, Card, Form, Input, Select, Typography } from 'antd';
3
+ import { useBuildCategorizeToOptions, useHandleToSelectChange } from './hooks';
4
 
5
+ interface IProps {
6
+ nodeId?: string;
7
+ }
8
+
9
+ const DynamicCategorize = ({ nodeId }: IProps) => {
10
  const form = Form.useFormInstance();
11
  const options = useBuildCategorizeToOptions();
12
+ const { handleSelectChange } = useHandleToSelectChange(
13
+ options.map((x) => x.value),
14
+ nodeId,
15
+ );
16
 
17
  return (
18
  <>
19
  <Form.List name="items">
20
+ {(fields, { add, remove }) => {
21
+ const handleAdd = () => {
22
+ const idx = fields.length;
23
+ add({ name: `Categorize ${idx + 1}` });
24
+ };
25
+ return (
26
+ <div
27
+ style={{ display: 'flex', rowGap: 10, flexDirection: 'column' }}
28
+ >
29
+ {fields.map((field) => (
30
+ <Card
31
+ size="small"
32
+ key={field.key}
33
+ extra={
34
+ <CloseOutlined
35
+ onClick={() => {
36
+ remove(field.name);
37
+ }}
38
+ />
39
+ }
 
40
  >
41
+ <Form.Item
42
+ label="name"
43
+ name={[field.name, 'name']}
44
+ // initialValue={`Categorize ${field.name + 1}`}
45
+ rules={[
46
+ { required: true, message: 'Please input your name!' },
47
+ ]}
48
+ >
49
+ <Input />
50
+ </Form.Item>
51
+ <Form.Item
52
+ label="description"
53
+ name={[field.name, 'description']}
54
+ >
55
+ <Input.TextArea rows={3} />
56
+ </Form.Item>
57
+ <Form.Item label="examples" name={[field.name, 'examples']}>
58
+ <Input.TextArea rows={3} />
59
+ </Form.Item>
60
+ <Form.Item label="to" name={[field.name, 'to']}>
61
+ <Select
62
+ allowClear
63
+ options={options}
64
+ onChange={handleSelectChange}
65
+ />
66
+ </Form.Item>
67
+ </Card>
68
+ ))}
69
 
70
+ <Button type="dashed" onClick={handleAdd} block>
71
+ + Add Item
72
+ </Button>
73
+ </div>
74
+ );
75
+ }}
76
  </Form.List>
77
 
78
  <Form.Item noStyle shouldUpdate>
web/src/pages/flow/categorize-form/hooks.ts CHANGED
@@ -1,6 +1,6 @@
1
  import get from 'lodash/get';
2
  import omit from 'lodash/omit';
3
- import { useCallback, useEffect } from 'react';
4
  import { Operator } from '../constant';
5
  import {
6
  ICategorizeItem,
@@ -72,6 +72,7 @@ export const useHandleFormValuesChange = ({
72
  }: IOperatorForm) => {
73
  const handleValuesChange = useCallback(
74
  (changedValues: any, values: any) => {
 
75
  onValuesChange?.(changedValues, {
76
  ...omit(values, 'items'),
77
  category_description: buildCategorizeObjectFromList(values.items),
@@ -90,3 +91,38 @@ export const useHandleFormValuesChange = ({
90
 
91
  return { handleValuesChange };
92
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import get from 'lodash/get';
2
  import omit from 'lodash/omit';
3
+ import { useCallback, useEffect, useRef } from 'react';
4
  import { Operator } from '../constant';
5
  import {
6
  ICategorizeItem,
 
72
  }: IOperatorForm) => {
73
  const handleValuesChange = useCallback(
74
  (changedValues: any, values: any) => {
75
+ console.info(changedValues, values);
76
  onValuesChange?.(changedValues, {
77
  ...omit(values, 'items'),
78
  category_description: buildCategorizeObjectFromList(values.items),
 
91
 
92
  return { handleValuesChange };
93
  };
94
+
95
+ export const useHandleToSelectChange = (
96
+ opstionIds: string[],
97
+ nodeId?: string,
98
+ ) => {
99
+ // const [previousTarget, setPreviousTarget] = useState('');
100
+ const previousTarget = useRef('');
101
+ const { addEdge, deleteEdgeBySourceAndTarget } = useGraphStore(
102
+ (state) => state,
103
+ );
104
+ const handleSelectChange = useCallback(
105
+ (value?: string) => {
106
+ if (nodeId) {
107
+ if (previousTarget.current) {
108
+ // delete previous edge
109
+ deleteEdgeBySourceAndTarget(nodeId, previousTarget.current);
110
+ }
111
+ if (value) {
112
+ addEdge({
113
+ source: nodeId,
114
+ target: value,
115
+ sourceHandle: 'b',
116
+ targetHandle: 'd',
117
+ });
118
+ } else {
119
+ // if the value is empty, delete the edges between the current node and all nodes in the drop-down box.
120
+ }
121
+ previousTarget.current = value;
122
+ }
123
+ },
124
+ [addEdge, nodeId, deleteEdgeBySourceAndTarget],
125
+ );
126
+
127
+ return { handleSelectChange };
128
+ };
web/src/pages/flow/categorize-form/index.tsx CHANGED
@@ -32,7 +32,7 @@ const CategorizeForm = ({ form, onValuesChange, node }: IOperatorForm) => {
32
  >
33
  <LLMSelect></LLMSelect>
34
  </Form.Item>
35
- <DynamicCategorize></DynamicCategorize>
36
  </Form>
37
  );
38
  };
 
32
  >
33
  <LLMSelect></LLMSelect>
34
  </Form.Item>
35
+ <DynamicCategorize nodeId={node?.id}></DynamicCategorize>
36
  </Form>
37
  );
38
  };
web/src/pages/flow/constant.tsx CHANGED
@@ -82,3 +82,18 @@ export const initialFormValuesMap = {
82
  [Operator.Answer]: {},
83
  [Operator.Categorize]: {},
84
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  [Operator.Answer]: {},
83
  [Operator.Categorize]: {},
84
  };
85
+
86
+ export const CategorizeAnchorPointPositions = [
87
+ { top: 1, right: 34 },
88
+ { top: 8, right: 18 },
89
+ { top: 15, right: 10 },
90
+ { top: 24, right: 4 },
91
+ { top: 31, right: 1 },
92
+ { top: 38, right: -2 },
93
+ { top: 62, right: -2 }, //bottom
94
+ { top: 71, right: 1 },
95
+ { top: 79, right: 6 },
96
+ { top: 86, right: 12 },
97
+ { top: 91, right: 20 },
98
+ { top: 98, right: 34 },
99
+ ];
web/src/pages/flow/store.ts CHANGED
@@ -34,10 +34,12 @@ export type RFState = {
34
  onSelectionChange: OnSelectionChangeFunc;
35
  addNode: (nodes: Node) => void;
36
  getNode: (id: string) => Node | undefined;
 
37
  duplicateNode: (id: string) => void;
38
  deleteEdge: () => void;
39
  deleteEdgeById: (id: string) => void;
40
  deleteNodeById: (id: string) => void;
 
41
  findNodeByName: (operatorName: Operator) => Node | undefined;
42
  findNodeById: (id: string) => Node | undefined;
43
  };
@@ -83,6 +85,14 @@ const useGraphStore = create<RFState>()(
83
  getNode: (id: string) => {
84
  return get().nodes.find((x) => x.id === id);
85
  },
 
 
 
 
 
 
 
 
86
  duplicateNode: (id: string) => {
87
  const { getNode, addNode } = get();
88
  const node = getNode(id);
@@ -114,6 +124,14 @@ const useGraphStore = create<RFState>()(
114
  edges: edges.filter((edge) => edge.id !== id),
115
  });
116
  },
 
 
 
 
 
 
 
 
117
  deleteNodeById: (id: string) => {
118
  const { nodes, edges } = get();
119
  set({
 
34
  onSelectionChange: OnSelectionChangeFunc;
35
  addNode: (nodes: Node) => void;
36
  getNode: (id: string) => Node | undefined;
37
+ addEdge: (connection: Connection) => void;
38
  duplicateNode: (id: string) => void;
39
  deleteEdge: () => void;
40
  deleteEdgeById: (id: string) => void;
41
  deleteNodeById: (id: string) => void;
42
+ deleteEdgeBySourceAndTarget: (source: string, target: string) => void;
43
  findNodeByName: (operatorName: Operator) => Node | undefined;
44
  findNodeById: (id: string) => Node | undefined;
45
  };
 
85
  getNode: (id: string) => {
86
  return get().nodes.find((x) => x.id === id);
87
  },
88
+ addEdge: (connection: Connection) => {
89
+ set({
90
+ edges: addEdge(connection, get().edges),
91
+ });
92
+ },
93
+ // addOnlyOneEdgeBetweenTwoNodes: (connection: Connection) => {
94
+
95
+ // },
96
  duplicateNode: (id: string) => {
97
  const { getNode, addNode } = get();
98
  const node = getNode(id);
 
124
  edges: edges.filter((edge) => edge.id !== id),
125
  });
126
  },
127
+ deleteEdgeBySourceAndTarget: (source: string, target: string) => {
128
+ const { edges } = get();
129
+ set({
130
+ edges: edges.filter(
131
+ (edge) => edge.target !== target && edge.source !== source,
132
+ ),
133
+ });
134
+ },
135
  deleteNodeById: (id: string) => {
136
  const { nodes, edges } = get();
137
  set({