balibabu commited on
Commit
5365cac
·
1 Parent(s): 3b1b360

feat: fixed issue with threshold translation #882 and add NodeContextMenu (#906)

Browse files

### What problem does this PR solve?

feat: fixed issue with threshold translation #882
feat: add NodeContextMenu

### Type of change


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

web/src/locales/zh-traditional.ts CHANGED
@@ -247,8 +247,8 @@ export default {
247
  以上就是你需要總結的內容。`,
248
  maxToken: '最大token數',
249
  maxTokenMessage: '最大token數是必填項',
250
- threshold: '臨界點',
251
- thresholdMessage: '臨界點是必填項',
252
  maxCluster: '最大聚類數',
253
  maxClusterMessage: '最大聚類數是必填項',
254
  randomSeed: '隨機種子',
 
247
  以上就是你需要總結的內容。`,
248
  maxToken: '最大token數',
249
  maxTokenMessage: '最大token數是必填項',
250
+ threshold: '閾值',
251
+ thresholdMessage: '閾值是必填項',
252
  maxCluster: '最大聚類數',
253
  maxClusterMessage: '最大聚類數是必填項',
254
  randomSeed: '隨機種子',
web/src/locales/zh.ts CHANGED
@@ -264,8 +264,8 @@ export default {
264
  以上就是你需要总结的内容。`,
265
  maxToken: '最大token数',
266
  maxTokenMessage: '最大token数是必填项',
267
- threshold: '临界点',
268
- thresholdMessage: '临界点是必填项',
269
  maxCluster: '最大聚类数',
270
  maxClusterMessage: '最大聚类数是必填项',
271
  randomSeed: '随机种子',
 
264
  以上就是你需要总结的内容。`,
265
  maxToken: '最大token数',
266
  maxTokenMessage: '最大token数是必填项',
267
+ threshold: '阈值',
268
+ thresholdMessage: '阈值是必填项',
269
  maxCluster: '最大聚类数',
270
  maxClusterMessage: '最大聚类数是必填项',
271
  randomSeed: '随机种子',
web/src/pages/flow/canvas/context-menu/index.less ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .contextMenu {
2
+ background: white;
3
+ border-style: solid;
4
+ box-shadow: 10px 19px 20px rgba(0, 0, 0, 10%);
5
+ position: absolute;
6
+ z-index: 10;
7
+ button {
8
+ border: none;
9
+ display: block;
10
+ padding: 0.5em;
11
+ text-align: left;
12
+ width: 100%;
13
+ }
14
+
15
+ button:hover {
16
+ background: white;
17
+ }
18
+ }
web/src/pages/flow/canvas/context-menu/index.tsx ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useCallback, useRef, useState } from 'react';
2
+ import { NodeMouseHandler, useReactFlow } from 'reactflow';
3
+
4
+ import styles from './index.less';
5
+
6
+ export interface INodeContextMenu {
7
+ id: string;
8
+ top: number;
9
+ left: number;
10
+ right?: number;
11
+ bottom?: number;
12
+ [key: string]: unknown;
13
+ }
14
+
15
+ export function NodeContextMenu({
16
+ id,
17
+ top,
18
+ left,
19
+ right,
20
+ bottom,
21
+ ...props
22
+ }: INodeContextMenu) {
23
+ const { getNode, setNodes, addNodes, setEdges } = useReactFlow();
24
+
25
+ const duplicateNode = useCallback(() => {
26
+ const node = getNode(id);
27
+ const position = {
28
+ x: node?.position?.x || 0 + 50,
29
+ y: node?.position?.y || 0 + 50,
30
+ };
31
+
32
+ addNodes({
33
+ ...(node || {}),
34
+ data: node?.data,
35
+ selected: false,
36
+ dragging: false,
37
+ id: `${node?.id}-copy`,
38
+ position,
39
+ });
40
+ }, [id, getNode, addNodes]);
41
+
42
+ const deleteNode = useCallback(() => {
43
+ setNodes((nodes) => nodes.filter((node) => node.id !== id));
44
+ setEdges((edges) => edges.filter((edge) => edge.source !== id));
45
+ }, [id, setNodes, setEdges]);
46
+
47
+ return (
48
+ <div
49
+ style={{ top, left, right, bottom }}
50
+ className={styles.contextMenu}
51
+ {...props}
52
+ >
53
+ <p style={{ margin: '0.5em' }}>
54
+ <small>node: {id}</small>
55
+ </p>
56
+ <button onClick={duplicateNode} type={'button'}>
57
+ duplicate
58
+ </button>
59
+ <button onClick={deleteNode} type={'button'}>
60
+ delete
61
+ </button>
62
+ </div>
63
+ );
64
+ }
65
+
66
+ export const useHandleNodeContextMenu = (sideWidth: number) => {
67
+ const [menu, setMenu] = useState<INodeContextMenu>({} as INodeContextMenu);
68
+ const ref = useRef<any>(null);
69
+
70
+ const onNodeContextMenu: NodeMouseHandler = useCallback(
71
+ (event, node) => {
72
+ // Prevent native context menu from showing
73
+ event.preventDefault();
74
+
75
+ // Calculate position of the context menu. We want to make sure it
76
+ // doesn't get positioned off-screen.
77
+ const pane = ref.current?.getBoundingClientRect();
78
+ // setMenu({
79
+ // id: node.id,
80
+ // top: event.clientY < pane.height - 200 ? event.clientY : 0,
81
+ // left: event.clientX < pane.width - 200 ? event.clientX : 0,
82
+ // right: event.clientX >= pane.width - 200 ? pane.width - event.clientX : 0,
83
+ // bottom:
84
+ // event.clientY >= pane.height - 200 ? pane.height - event.clientY : 0,
85
+ // });
86
+
87
+ console.info('clientX:', event.clientX);
88
+ console.info('clientY:', event.clientY);
89
+
90
+ setMenu({
91
+ id: node.id,
92
+ top: event.clientY - 72,
93
+ left: event.clientX - sideWidth,
94
+ // top: event.clientY < pane.height - 200 ? event.clientY - 72 : 0,
95
+ // left: event.clientX < pane.width - 200 ? event.clientX : 0,
96
+ });
97
+ },
98
+ [sideWidth],
99
+ );
100
+
101
+ // Close the context menu if it's open whenever the window is clicked.
102
+ const onPaneClick = useCallback(
103
+ () => setMenu({} as INodeContextMenu),
104
+ [setMenu],
105
+ );
106
+
107
+ return { onNodeContextMenu, menu, onPaneClick, ref };
108
+ };
web/src/pages/flow/canvas/index.tsx CHANGED
@@ -4,6 +4,7 @@ import ReactFlow, {
4
  Controls,
5
  Edge,
6
  Node,
 
7
  OnConnect,
8
  OnEdgesChange,
9
  OnNodesChange,
@@ -13,7 +14,10 @@ import ReactFlow, {
13
  } from 'reactflow';
14
  import 'reactflow/dist/style.css';
15
 
16
- import { useHandleDrop } from '../hooks';
 
 
 
17
  import { TextUpdaterNode } from './node';
18
 
19
  const nodeTypes = { textUpdater: TextUpdaterNode };
@@ -42,9 +46,17 @@ const initialEdges = [
42
  { id: '1-2', source: '1', target: '2', label: 'to the', type: 'step' },
43
  ];
44
 
45
- function FlowCanvas() {
 
 
 
 
 
46
  const [nodes, setNodes] = useState<Node[]>(initialNodes);
47
  const [edges, setEdges] = useState<Edge[]>(initialEdges);
 
 
 
48
 
49
  const onNodesChange: OnNodesChange = useCallback(
50
  (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
@@ -60,7 +72,11 @@ function FlowCanvas() {
60
  [],
61
  );
62
 
63
- const { handleDrop, allowDrop } = useHandleDrop(setNodes);
 
 
 
 
64
 
65
  useEffect(() => {
66
  console.info('nodes:', nodes);
@@ -68,23 +84,30 @@ function FlowCanvas() {
68
  }, [nodes, edges]);
69
 
70
  return (
71
- <div
72
- style={{ height: '100%', width: '100%' }}
73
- onDrop={handleDrop}
74
- onDragOver={allowDrop}
75
- >
76
  <ReactFlow
 
77
  nodes={nodes}
78
  onNodesChange={onNodesChange}
 
79
  edges={edges}
80
  onEdgesChange={onEdgesChange}
81
- // fitView
82
  onConnect={onConnect}
83
  nodeTypes={nodeTypes}
 
 
 
 
 
84
  >
85
  <Background />
86
  <Controls />
 
 
 
87
  </ReactFlow>
 
88
  </div>
89
  );
90
  }
 
4
  Controls,
5
  Edge,
6
  Node,
7
+ NodeMouseHandler,
8
  OnConnect,
9
  OnEdgesChange,
10
  OnNodesChange,
 
14
  } from 'reactflow';
15
  import 'reactflow/dist/style.css';
16
 
17
+ import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';
18
+
19
+ import FlowDrawer from '../flow-drawer';
20
+ import { useHandleDrop, useShowDrawer } from '../hooks';
21
  import { TextUpdaterNode } from './node';
22
 
23
  const nodeTypes = { textUpdater: TextUpdaterNode };
 
46
  { id: '1-2', source: '1', target: '2', label: 'to the', type: 'step' },
47
  ];
48
 
49
+ interface IProps {
50
+ sideWidth: number;
51
+ showDrawer(): void;
52
+ }
53
+
54
+ function FlowCanvas({ sideWidth }: IProps) {
55
  const [nodes, setNodes] = useState<Node[]>(initialNodes);
56
  const [edges, setEdges] = useState<Edge[]>(initialEdges);
57
+ const { ref, menu, onNodeContextMenu, onPaneClick } =
58
+ useHandleNodeContextMenu(sideWidth);
59
+ const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer();
60
 
61
  const onNodesChange: OnNodesChange = useCallback(
62
  (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
 
72
  [],
73
  );
74
 
75
+ const onNodeClick: NodeMouseHandler = useCallback(() => {
76
+ showDrawer();
77
+ }, [showDrawer]);
78
+
79
+ const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(setNodes);
80
 
81
  useEffect(() => {
82
  console.info('nodes:', nodes);
 
84
  }, [nodes, edges]);
85
 
86
  return (
87
+ <div style={{ height: '100%', width: '100%' }}>
 
 
 
 
88
  <ReactFlow
89
+ ref={ref}
90
  nodes={nodes}
91
  onNodesChange={onNodesChange}
92
+ onNodeContextMenu={onNodeContextMenu}
93
  edges={edges}
94
  onEdgesChange={onEdgesChange}
95
+ fitView
96
  onConnect={onConnect}
97
  nodeTypes={nodeTypes}
98
+ onPaneClick={onPaneClick}
99
+ onDrop={onDrop}
100
+ onDragOver={onDragOver}
101
+ onNodeClick={onNodeClick}
102
+ onInit={setReactFlowInstance}
103
  >
104
  <Background />
105
  <Controls />
106
+ {Object.keys(menu).length > 0 && (
107
+ <NodeContextMenu onClick={onPaneClick} {...(menu as any)} />
108
+ )}
109
  </ReactFlow>
110
+ <FlowDrawer visible={drawerVisible} hideModal={hideDrawer}></FlowDrawer>
111
  </div>
112
  );
113
  }
web/src/pages/flow/flow-drawer/index.tsx ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IModalProps } from '@/interfaces/common';
2
+ import { Drawer } from 'antd';
3
+
4
+ const FlowDrawer = ({ visible, hideModal }: IModalProps<any>) => {
5
+ return (
6
+ <Drawer
7
+ title="Basic Drawer"
8
+ placement="right"
9
+ // closable={false}
10
+ onClose={hideModal}
11
+ open={visible}
12
+ getContainer={false}
13
+ mask={false}
14
+ >
15
+ <p>Some contents...</p>
16
+ </Drawer>
17
+ );
18
+ };
19
+
20
+ export default FlowDrawer;
web/src/pages/flow/flow-sider/index.tsx CHANGED
@@ -1,6 +1,5 @@
1
  import { Avatar, Card, Flex, Layout, Space } from 'antd';
2
  import classNames from 'classnames';
3
- import { useState } from 'react';
4
  import { componentList } from '../mock';
5
 
6
  import { useHandleDrag } from '../hooks';
@@ -8,9 +7,13 @@ import styles from './index.less';
8
 
9
  const { Sider } = Layout;
10
 
11
- const FlowSider = () => {
12
- const [collapsed, setCollapsed] = useState(true);
13
- const { handleDrag } = useHandleDrag();
 
 
 
 
14
 
15
  return (
16
  <Sider
@@ -27,7 +30,7 @@ const FlowSider = () => {
27
  hoverable
28
  draggable
29
  className={classNames(styles.operatorCard)}
30
- onDragStart={handleDrag(x.name)}
31
  >
32
  <Flex justify="space-between" align="center">
33
  <Space size={15}>
@@ -45,4 +48,4 @@ const FlowSider = () => {
45
  );
46
  };
47
 
48
- export default FlowSider;
 
1
  import { Avatar, Card, Flex, Layout, Space } from 'antd';
2
  import classNames from 'classnames';
 
3
  import { componentList } from '../mock';
4
 
5
  import { useHandleDrag } from '../hooks';
 
7
 
8
  const { Sider } = Layout;
9
 
10
+ interface IProps {
11
+ setCollapsed: (width: boolean) => void;
12
+ collapsed: boolean;
13
+ }
14
+
15
+ const FlowSide = ({ setCollapsed, collapsed }: IProps) => {
16
+ const { handleDragStart } = useHandleDrag();
17
 
18
  return (
19
  <Sider
 
30
  hoverable
31
  draggable
32
  className={classNames(styles.operatorCard)}
33
+ onDragStart={handleDragStart(x.name)}
34
  >
35
  <Flex justify="space-between" align="center">
36
  <Space size={15}>
 
48
  );
49
  };
50
 
51
+ export default FlowSide;
web/src/pages/flow/hooks.ts CHANGED
@@ -1,47 +1,75 @@
1
- import React, { Dispatch, SetStateAction, useCallback } from 'react';
2
- import { Node } from 'reactflow';
 
 
3
 
4
  export const useHandleDrag = () => {
5
- const handleDrag = useCallback(
6
  (operatorId: string) => (ev: React.DragEvent<HTMLDivElement>) => {
7
- console.info(ev.clientX, ev.pageY);
8
- ev.dataTransfer.setData('operatorId', operatorId);
9
- ev.dataTransfer.setData('startClientX', ev.clientX.toString());
10
- ev.dataTransfer.setData('startClientY', ev.clientY.toString());
11
  },
12
  [],
13
  );
14
 
15
- return { handleDrag };
16
  };
17
 
18
  export const useHandleDrop = (setNodes: Dispatch<SetStateAction<Node[]>>) => {
19
- const allowDrop = (ev: React.DragEvent<HTMLDivElement>) => {
20
- ev.preventDefault();
21
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- const handleDrop = useCallback(
24
- (ev: React.DragEvent<HTMLDivElement>) => {
25
- ev.preventDefault();
26
- const operatorId = ev.dataTransfer.getData('operatorId');
27
- const startClientX = ev.dataTransfer.getData('startClientX');
28
- const startClientY = ev.dataTransfer.getData('startClientY');
29
- console.info(operatorId);
30
- console.info(ev.pageX, ev.pageY);
31
- console.info(ev.clientX, ev.clientY);
32
- console.info(ev.movementX, ev.movementY);
33
- const x = ev.clientX - 200;
34
- const y = ev.clientY - 72;
35
- setNodes((pre) => {
36
- return pre.concat({
37
- id: operatorId,
38
- position: { x, y },
39
- data: { label: operatorId },
40
- });
41
  });
 
 
 
 
 
 
 
 
 
 
 
42
  },
43
- [setNodes],
44
  );
45
 
46
- return { handleDrop, allowDrop };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  };
 
1
+ import { useSetModalState } from '@/hooks/commonHooks';
2
+ import React, { Dispatch, SetStateAction, useCallback, useState } from 'react';
3
+ import { Node, ReactFlowInstance } from 'reactflow';
4
+ import { v4 as uuidv4 } from 'uuid';
5
 
6
  export const useHandleDrag = () => {
7
+ const handleDragStart = useCallback(
8
  (operatorId: string) => (ev: React.DragEvent<HTMLDivElement>) => {
9
+ ev.dataTransfer.setData('application/reactflow', operatorId);
10
+ ev.dataTransfer.effectAllowed = 'move';
 
 
11
  },
12
  [],
13
  );
14
 
15
+ return { handleDragStart };
16
  };
17
 
18
  export const useHandleDrop = (setNodes: Dispatch<SetStateAction<Node[]>>) => {
19
+ const [reactFlowInstance, setReactFlowInstance] =
20
+ useState<ReactFlowInstance<any, any>>();
21
+
22
+ const onDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
23
+ event.preventDefault();
24
+ event.dataTransfer.dropEffect = 'move';
25
+ }, []);
26
+
27
+ const onDrop = useCallback(
28
+ (event: React.DragEvent<HTMLDivElement>) => {
29
+ event.preventDefault();
30
+
31
+ const type = event.dataTransfer.getData('application/reactflow');
32
+
33
+ // check if the dropped element is valid
34
+ if (typeof type === 'undefined' || !type) {
35
+ return;
36
+ }
37
 
38
+ // reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition
39
+ // and you don't need to subtract the reactFlowBounds.left/top anymore
40
+ // details: https://reactflow.dev/whats-new/2023-11-10
41
+ const position = reactFlowInstance?.screenToFlowPosition({
42
+ x: event.clientX,
43
+ y: event.clientY,
 
 
 
 
 
 
 
 
 
 
 
 
44
  });
45
+ const newNode = {
46
+ id: uuidv4(),
47
+ type,
48
+ position: position || {
49
+ x: 0,
50
+ y: 0,
51
+ },
52
+ data: { label: `${type} node` },
53
+ };
54
+
55
+ setNodes((nds) => nds.concat(newNode));
56
  },
57
+ [reactFlowInstance, setNodes],
58
  );
59
 
60
+ return { onDrop, onDragOver, setReactFlowInstance };
61
+ };
62
+
63
+ export const useShowDrawer = () => {
64
+ const {
65
+ visible: drawerVisible,
66
+ hideModal: hideDrawer,
67
+ showModal: showDrawer,
68
+ } = useSetModalState();
69
+
70
+ return {
71
+ drawerVisible,
72
+ hideDrawer,
73
+ showDrawer,
74
+ };
75
  };
web/src/pages/flow/index.tsx CHANGED
@@ -1,18 +1,24 @@
1
  import { Layout } from 'antd';
 
 
2
  import FlowCanvas from './canvas';
3
  import Sider from './flow-sider';
4
 
5
  const { Content } = Layout;
6
 
7
  function RagFlow() {
 
 
8
  return (
9
- <Layout style={{ minHeight: '100vh' }}>
10
- <Sider></Sider>
11
- <Layout>
12
- <Content style={{ margin: '0 16px' }}>
13
- <FlowCanvas></FlowCanvas>
14
- </Content>
15
- </Layout>
 
 
16
  </Layout>
17
  );
18
  }
 
1
  import { Layout } from 'antd';
2
+ import { useState } from 'react';
3
+ import { ReactFlowProvider } from 'reactflow';
4
  import FlowCanvas from './canvas';
5
  import Sider from './flow-sider';
6
 
7
  const { Content } = Layout;
8
 
9
  function RagFlow() {
10
+ const [collapsed, setCollapsed] = useState(false);
11
+
12
  return (
13
+ <Layout>
14
+ <ReactFlowProvider>
15
+ <Sider setCollapsed={setCollapsed} collapsed={collapsed}></Sider>
16
+ <Layout>
17
+ <Content style={{ margin: '0 16px' }}>
18
+ <FlowCanvas sideWidth={collapsed ? 0 : 200}></FlowCanvas>
19
+ </Content>
20
+ </Layout>
21
+ </ReactFlowProvider>
22
  </Layout>
23
  );
24
  }