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 +39 -0
- web/src/pages/flow/canvas/node/index.less +6 -0
- web/src/pages/flow/canvas/node/index.tsx +18 -19
- web/src/pages/flow/categorize-form/dynamic-categorize.tsx +64 -44
- web/src/pages/flow/categorize-form/hooks.ts +37 -1
- web/src/pages/flow/categorize-form/index.tsx +1 -1
- web/src/pages/flow/constant.tsx +15 -0
- web/src/pages/flow/store.ts +18 -0
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 {
|
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
|
|
|
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 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
6 |
const form = Form.useFormInstance();
|
7 |
const options = useBuildCategorizeToOptions();
|
|
|
|
|
|
|
|
|
8 |
|
9 |
return (
|
10 |
<>
|
11 |
<Form.List name="items">
|
12 |
-
{(fields, { add, remove }) =>
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
]}
|
33 |
>
|
34 |
-
<
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
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({
|