balibabu
commited on
Commit
·
4a674c0
1
Parent(s):
ddd1aa2
feat: duplicate node #918 (#1136)
Browse files### What problem does this PR solve?
feat: duplicate node #918
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
web/src/components/operate-dropdown/index.tsx
CHANGED
@@ -3,18 +3,20 @@ import { DeleteOutlined, MoreOutlined } from '@ant-design/icons';
|
|
3 |
import { Dropdown, MenuProps, Space } from 'antd';
|
4 |
import { useTranslation } from 'react-i18next';
|
5 |
|
6 |
-
import React from 'react';
|
7 |
import styles from './index.less';
|
8 |
|
9 |
interface IProps {
|
10 |
deleteItem: () => Promise<any> | void;
|
11 |
iconFontSize?: number;
|
|
|
12 |
}
|
13 |
|
14 |
const OperateDropdown = ({
|
15 |
deleteItem,
|
16 |
children,
|
17 |
iconFontSize = 30,
|
|
|
18 |
}: React.PropsWithChildren<IProps>) => {
|
19 |
const { t } = useTranslation();
|
20 |
const showDeleteConfirm = useShowDeleteConfirm();
|
@@ -31,17 +33,20 @@ const OperateDropdown = ({
|
|
31 |
}
|
32 |
};
|
33 |
|
34 |
-
const items: MenuProps['items'] =
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
|
|
|
|
|
|
45 |
|
46 |
return (
|
47 |
<Dropdown
|
|
|
3 |
import { Dropdown, MenuProps, Space } from 'antd';
|
4 |
import { useTranslation } from 'react-i18next';
|
5 |
|
6 |
+
import React, { useMemo } from 'react';
|
7 |
import styles from './index.less';
|
8 |
|
9 |
interface IProps {
|
10 |
deleteItem: () => Promise<any> | void;
|
11 |
iconFontSize?: number;
|
12 |
+
items?: MenuProps['items'];
|
13 |
}
|
14 |
|
15 |
const OperateDropdown = ({
|
16 |
deleteItem,
|
17 |
children,
|
18 |
iconFontSize = 30,
|
19 |
+
items: otherItems = [],
|
20 |
}: React.PropsWithChildren<IProps>) => {
|
21 |
const { t } = useTranslation();
|
22 |
const showDeleteConfirm = useShowDeleteConfirm();
|
|
|
33 |
}
|
34 |
};
|
35 |
|
36 |
+
const items: MenuProps['items'] = useMemo(() => {
|
37 |
+
return [
|
38 |
+
{
|
39 |
+
key: '1',
|
40 |
+
label: (
|
41 |
+
<Space>
|
42 |
+
{t('common.delete')}
|
43 |
+
<DeleteOutlined />
|
44 |
+
</Space>
|
45 |
+
),
|
46 |
+
},
|
47 |
+
...otherItems,
|
48 |
+
];
|
49 |
+
}, [t, otherItems]);
|
50 |
|
51 |
return (
|
52 |
<Dropdown
|
web/src/pages/flow/canvas/context-menu/index.tsx
CHANGED
@@ -63,6 +63,8 @@ export function NodeContextMenu({
|
|
63 |
);
|
64 |
}
|
65 |
|
|
|
|
|
66 |
export const useHandleNodeContextMenu = (sideWidth: number) => {
|
67 |
const [menu, setMenu] = useState<INodeContextMenu>({} as INodeContextMenu);
|
68 |
const ref = useRef<any>(null);
|
|
|
63 |
);
|
64 |
}
|
65 |
|
66 |
+
/* @deprecated
|
67 |
+
*/
|
68 |
export const useHandleNodeContextMenu = (sideWidth: number) => {
|
69 |
const [menu, setMenu] = useState<INodeContextMenu>({} as INodeContextMenu);
|
70 |
const ref = useRef<any>(null);
|
web/src/pages/flow/canvas/index.tsx
CHANGED
@@ -8,7 +8,6 @@ import ReactFlow, {
|
|
8 |
} from 'reactflow';
|
9 |
import 'reactflow/dist/style.css';
|
10 |
|
11 |
-
import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';
|
12 |
import { ButtonEdge } from './edge';
|
13 |
|
14 |
import FlowDrawer from '../flow-drawer';
|
@@ -30,12 +29,11 @@ const edgeTypes = {
|
|
30 |
};
|
31 |
|
32 |
interface IProps {
|
33 |
-
sideWidth: number;
|
34 |
chatDrawerVisible: boolean;
|
35 |
hideChatDrawer(): void;
|
36 |
}
|
37 |
|
38 |
-
function FlowCanvas({
|
39 |
const {
|
40 |
nodes,
|
41 |
edges,
|
@@ -45,8 +43,6 @@ function FlowCanvas({ sideWidth, chatDrawerVisible, hideChatDrawer }: IProps) {
|
|
45 |
onSelectionChange,
|
46 |
} = useSelectCanvasData();
|
47 |
|
48 |
-
const { ref, menu, onNodeContextMenu, onPaneClick } =
|
49 |
-
useHandleNodeContextMenu(sideWidth);
|
50 |
const { drawerVisible, hideDrawer, showDrawer, clickedNode } =
|
51 |
useShowDrawer();
|
52 |
|
@@ -64,18 +60,15 @@ function FlowCanvas({ sideWidth, chatDrawerVisible, hideChatDrawer }: IProps) {
|
|
64 |
return (
|
65 |
<div className={styles.canvasWrapper}>
|
66 |
<ReactFlow
|
67 |
-
ref={ref}
|
68 |
connectionMode={ConnectionMode.Loose}
|
69 |
nodes={nodes}
|
70 |
onNodesChange={onNodesChange}
|
71 |
-
onNodeContextMenu={onNodeContextMenu}
|
72 |
edges={edges}
|
73 |
onEdgesChange={onEdgesChange}
|
74 |
fitView
|
75 |
onConnect={onConnect}
|
76 |
nodeTypes={nodeTypes}
|
77 |
edgeTypes={edgeTypes}
|
78 |
-
onPaneClick={onPaneClick}
|
79 |
onDrop={onDrop}
|
80 |
onDragOver={onDragOver}
|
81 |
onNodeClick={onNodeClick}
|
@@ -95,9 +88,6 @@ function FlowCanvas({ sideWidth, chatDrawerVisible, hideChatDrawer }: IProps) {
|
|
95 |
>
|
96 |
<Background />
|
97 |
<Controls />
|
98 |
-
{Object.keys(menu).length > 0 && (
|
99 |
-
<NodeContextMenu onClick={onPaneClick} {...(menu as any)} />
|
100 |
-
)}
|
101 |
</ReactFlow>
|
102 |
<FlowDrawer
|
103 |
node={clickedNode}
|
|
|
8 |
} from 'reactflow';
|
9 |
import 'reactflow/dist/style.css';
|
10 |
|
|
|
11 |
import { ButtonEdge } from './edge';
|
12 |
|
13 |
import FlowDrawer from '../flow-drawer';
|
|
|
29 |
};
|
30 |
|
31 |
interface IProps {
|
|
|
32 |
chatDrawerVisible: boolean;
|
33 |
hideChatDrawer(): void;
|
34 |
}
|
35 |
|
36 |
+
function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
|
37 |
const {
|
38 |
nodes,
|
39 |
edges,
|
|
|
43 |
onSelectionChange,
|
44 |
} = useSelectCanvasData();
|
45 |
|
|
|
|
|
46 |
const { drawerVisible, hideDrawer, showDrawer, clickedNode } =
|
47 |
useShowDrawer();
|
48 |
|
|
|
60 |
return (
|
61 |
<div className={styles.canvasWrapper}>
|
62 |
<ReactFlow
|
|
|
63 |
connectionMode={ConnectionMode.Loose}
|
64 |
nodes={nodes}
|
65 |
onNodesChange={onNodesChange}
|
|
|
66 |
edges={edges}
|
67 |
onEdgesChange={onEdgesChange}
|
68 |
fitView
|
69 |
onConnect={onConnect}
|
70 |
nodeTypes={nodeTypes}
|
71 |
edgeTypes={edgeTypes}
|
|
|
72 |
onDrop={onDrop}
|
73 |
onDragOver={onDragOver}
|
74 |
onNodeClick={onNodeClick}
|
|
|
88 |
>
|
89 |
<Background />
|
90 |
<Controls />
|
|
|
|
|
|
|
91 |
</ReactFlow>
|
92 |
<FlowDrawer
|
93 |
node={clickedNode}
|
web/src/pages/flow/canvas/node/index.tsx
CHANGED
@@ -2,24 +2,50 @@ import classNames from 'classnames';
|
|
2 |
import { Handle, NodeProps, Position } from 'reactflow';
|
3 |
|
4 |
import OperateDropdown from '@/components/operate-dropdown';
|
5 |
-
import {
|
|
|
6 |
import { useCallback } from 'react';
|
|
|
7 |
import { Operator, operatorMap } from '../../constant';
|
8 |
import OperatorIcon from '../../operator-icon';
|
9 |
import useGraphStore from '../../store';
|
10 |
import styles from './index.less';
|
11 |
|
|
|
|
|
12 |
export function RagNode({
|
13 |
id,
|
14 |
data,
|
15 |
isConnectable = true,
|
16 |
selected,
|
17 |
}: NodeProps<{ label: string }>) {
|
|
|
18 |
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
|
|
|
|
|
19 |
const deleteNode = useCallback(() => {
|
20 |
deleteNodeById(id);
|
21 |
}, [id, deleteNodeById]);
|
22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
return (
|
24 |
<section
|
25 |
className={classNames(styles.ragNode, {
|
@@ -57,10 +83,17 @@ export function RagNode({
|
|
57 |
<OperateDropdown
|
58 |
iconFontSize={14}
|
59 |
deleteItem={deleteNode}
|
|
|
60 |
></OperateDropdown>
|
61 |
</Flex>
|
62 |
-
<div
|
63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
</div>
|
65 |
</section>
|
66 |
);
|
|
|
2 |
import { Handle, NodeProps, Position } from 'reactflow';
|
3 |
|
4 |
import OperateDropdown from '@/components/operate-dropdown';
|
5 |
+
import { CopyOutlined } from '@ant-design/icons';
|
6 |
+
import { Flex, MenuProps, Space, Typography } from 'antd';
|
7 |
import { useCallback } from 'react';
|
8 |
+
import { useTranslation } from 'react-i18next';
|
9 |
import { Operator, operatorMap } from '../../constant';
|
10 |
import OperatorIcon from '../../operator-icon';
|
11 |
import useGraphStore from '../../store';
|
12 |
import styles from './index.less';
|
13 |
|
14 |
+
const { Text } = Typography;
|
15 |
+
|
16 |
export function RagNode({
|
17 |
id,
|
18 |
data,
|
19 |
isConnectable = true,
|
20 |
selected,
|
21 |
}: NodeProps<{ label: string }>) {
|
22 |
+
const { t } = useTranslation();
|
23 |
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
|
24 |
+
const duplicateNodeById = useGraphStore((store) => store.duplicateNode);
|
25 |
+
|
26 |
const deleteNode = useCallback(() => {
|
27 |
deleteNodeById(id);
|
28 |
}, [id, deleteNodeById]);
|
29 |
|
30 |
+
const duplicateNode = useCallback(() => {
|
31 |
+
duplicateNodeById(id);
|
32 |
+
}, [id, duplicateNodeById]);
|
33 |
+
|
34 |
+
const description = operatorMap[data.label as Operator].description;
|
35 |
+
|
36 |
+
const items: MenuProps['items'] = [
|
37 |
+
{
|
38 |
+
key: '2',
|
39 |
+
onClick: duplicateNode,
|
40 |
+
label: (
|
41 |
+
<Flex justify={'space-between'}>
|
42 |
+
{t('common.copy')}
|
43 |
+
<CopyOutlined />
|
44 |
+
</Flex>
|
45 |
+
),
|
46 |
+
},
|
47 |
+
];
|
48 |
+
|
49 |
return (
|
50 |
<section
|
51 |
className={classNames(styles.ragNode, {
|
|
|
83 |
<OperateDropdown
|
84 |
iconFontSize={14}
|
85 |
deleteItem={deleteNode}
|
86 |
+
items={items}
|
87 |
></OperateDropdown>
|
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>
|
99 |
);
|
web/src/pages/flow/index.tsx
CHANGED
@@ -27,7 +27,6 @@ function RagFlow() {
|
|
27 |
<FlowHeader showChatDrawer={showChatDrawer}></FlowHeader>
|
28 |
<Content style={{ margin: 0 }}>
|
29 |
<FlowCanvas
|
30 |
-
sideWidth={collapsed ? 0 : 200}
|
31 |
chatDrawerVisible={chatDrawerVisible}
|
32 |
hideChatDrawer={hideChatDrawer}
|
33 |
></FlowCanvas>
|
|
|
27 |
<FlowHeader showChatDrawer={showChatDrawer}></FlowHeader>
|
28 |
<Content style={{ margin: 0 }}>
|
29 |
<FlowCanvas
|
|
|
30 |
chatDrawerVisible={chatDrawerVisible}
|
31 |
hideChatDrawer={hideChatDrawer}
|
32 |
></FlowCanvas>
|
web/src/pages/flow/store.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1 |
import type {} from '@redux-devtools/extension';
|
|
|
2 |
import {
|
3 |
Connection,
|
4 |
Edge,
|
@@ -32,6 +33,8 @@ export type RFState = {
|
|
32 |
updateNodeForm: (nodeId: string, values: any) => void;
|
33 |
onSelectionChange: OnSelectionChangeFunc;
|
34 |
addNode: (nodes: Node) => void;
|
|
|
|
|
35 |
deleteEdge: () => void;
|
36 |
deleteEdgeById: (id: string) => void;
|
37 |
deleteNodeById: (id: string) => void;
|
@@ -76,6 +79,26 @@ const useGraphStore = create<RFState>()(
|
|
76 |
addNode: (node: Node) => {
|
77 |
set({ nodes: get().nodes.concat(node) });
|
78 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
deleteEdge: () => {
|
80 |
const { edges, selectedEdgeIds } = get();
|
81 |
set({
|
|
|
1 |
import type {} from '@redux-devtools/extension';
|
2 |
+
import { humanId } from 'human-id';
|
3 |
import {
|
4 |
Connection,
|
5 |
Edge,
|
|
|
33 |
updateNodeForm: (nodeId: string, values: any) => void;
|
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;
|
|
|
79 |
addNode: (node: Node) => {
|
80 |
set({ nodes: get().nodes.concat(node) });
|
81 |
},
|
82 |
+
getNode: (id: string) => {
|
83 |
+
return get().nodes.find((x) => x.id === id);
|
84 |
+
},
|
85 |
+
duplicateNode: (id: string) => {
|
86 |
+
const { getNode, addNode } = get();
|
87 |
+
const node = getNode(id);
|
88 |
+
const position = {
|
89 |
+
x: (node?.position?.x || 0) + 30,
|
90 |
+
y: (node?.position?.y || 0) + 20,
|
91 |
+
};
|
92 |
+
|
93 |
+
addNode({
|
94 |
+
...(node || {}),
|
95 |
+
data: node?.data,
|
96 |
+
selected: false,
|
97 |
+
dragging: false,
|
98 |
+
id: `${node?.data?.label}:${humanId()}`,
|
99 |
+
position,
|
100 |
+
});
|
101 |
+
},
|
102 |
deleteEdge: () => {
|
103 |
const { edges, selectedEdgeIds } = get();
|
104 |
set({
|