diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts
index 03aae27cc..d12babd58 100644
--- a/web/src/locales/zh-traditional.ts
+++ b/web/src/locales/zh-traditional.ts
@@ -247,8 +247,8 @@ export default {
以上就是你需要總結的內容。`,
maxToken: '最大token數',
maxTokenMessage: '最大token數是必填項',
- threshold: '臨界點',
- thresholdMessage: '臨界點是必填項',
+ threshold: '閾值',
+ thresholdMessage: '閾值是必填項',
maxCluster: '最大聚類數',
maxClusterMessage: '最大聚類數是必填項',
randomSeed: '隨機種子',
diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts
index 215f0a953..a32e6e4b2 100644
--- a/web/src/locales/zh.ts
+++ b/web/src/locales/zh.ts
@@ -264,8 +264,8 @@ export default {
以上就是你需要总结的内容。`,
maxToken: '最大token数',
maxTokenMessage: '最大token数是必填项',
- threshold: '临界点',
- thresholdMessage: '临界点是必填项',
+ threshold: '阈值',
+ thresholdMessage: '阈值是必填项',
maxCluster: '最大聚类数',
maxClusterMessage: '最大聚类数是必填项',
randomSeed: '随机种子',
diff --git a/web/src/pages/flow/canvas/context-menu/index.less b/web/src/pages/flow/canvas/context-menu/index.less
new file mode 100644
index 000000000..a674e0018
--- /dev/null
+++ b/web/src/pages/flow/canvas/context-menu/index.less
@@ -0,0 +1,18 @@
+.contextMenu {
+ background: white;
+ border-style: solid;
+ box-shadow: 10px 19px 20px rgba(0, 0, 0, 10%);
+ position: absolute;
+ z-index: 10;
+ button {
+ border: none;
+ display: block;
+ padding: 0.5em;
+ text-align: left;
+ width: 100%;
+ }
+
+ button:hover {
+ background: white;
+ }
+}
diff --git a/web/src/pages/flow/canvas/context-menu/index.tsx b/web/src/pages/flow/canvas/context-menu/index.tsx
new file mode 100644
index 000000000..c03e87cdd
--- /dev/null
+++ b/web/src/pages/flow/canvas/context-menu/index.tsx
@@ -0,0 +1,108 @@
+import { useCallback, useRef, useState } from 'react';
+import { NodeMouseHandler, useReactFlow } from 'reactflow';
+
+import styles from './index.less';
+
+export interface INodeContextMenu {
+ id: string;
+ top: number;
+ left: number;
+ right?: number;
+ bottom?: number;
+ [key: string]: unknown;
+}
+
+export function NodeContextMenu({
+ id,
+ top,
+ left,
+ right,
+ bottom,
+ ...props
+}: INodeContextMenu) {
+ const { getNode, setNodes, addNodes, setEdges } = useReactFlow();
+
+ const duplicateNode = useCallback(() => {
+ const node = getNode(id);
+ const position = {
+ x: node?.position?.x || 0 + 50,
+ y: node?.position?.y || 0 + 50,
+ };
+
+ addNodes({
+ ...(node || {}),
+ data: node?.data,
+ selected: false,
+ dragging: false,
+ id: `${node?.id}-copy`,
+ position,
+ });
+ }, [id, getNode, addNodes]);
+
+ const deleteNode = useCallback(() => {
+ setNodes((nodes) => nodes.filter((node) => node.id !== id));
+ setEdges((edges) => edges.filter((edge) => edge.source !== id));
+ }, [id, setNodes, setEdges]);
+
+ return (
+
+
+ node: {id}
+
+
+
+
+ );
+}
+
+export const useHandleNodeContextMenu = (sideWidth: number) => {
+ const [menu, setMenu] = useState({} as INodeContextMenu);
+ const ref = useRef(null);
+
+ const onNodeContextMenu: NodeMouseHandler = useCallback(
+ (event, node) => {
+ // Prevent native context menu from showing
+ event.preventDefault();
+
+ // Calculate position of the context menu. We want to make sure it
+ // doesn't get positioned off-screen.
+ const pane = ref.current?.getBoundingClientRect();
+ // setMenu({
+ // id: node.id,
+ // top: event.clientY < pane.height - 200 ? event.clientY : 0,
+ // left: event.clientX < pane.width - 200 ? event.clientX : 0,
+ // right: event.clientX >= pane.width - 200 ? pane.width - event.clientX : 0,
+ // bottom:
+ // event.clientY >= pane.height - 200 ? pane.height - event.clientY : 0,
+ // });
+
+ console.info('clientX:', event.clientX);
+ console.info('clientY:', event.clientY);
+
+ setMenu({
+ id: node.id,
+ top: event.clientY - 72,
+ left: event.clientX - sideWidth,
+ // top: event.clientY < pane.height - 200 ? event.clientY - 72 : 0,
+ // left: event.clientX < pane.width - 200 ? event.clientX : 0,
+ });
+ },
+ [sideWidth],
+ );
+
+ // Close the context menu if it's open whenever the window is clicked.
+ const onPaneClick = useCallback(
+ () => setMenu({} as INodeContextMenu),
+ [setMenu],
+ );
+
+ return { onNodeContextMenu, menu, onPaneClick, ref };
+};
diff --git a/web/src/pages/flow/canvas/index.tsx b/web/src/pages/flow/canvas/index.tsx
index 1ad3226a1..a8e66a611 100644
--- a/web/src/pages/flow/canvas/index.tsx
+++ b/web/src/pages/flow/canvas/index.tsx
@@ -4,6 +4,7 @@ import ReactFlow, {
Controls,
Edge,
Node,
+ NodeMouseHandler,
OnConnect,
OnEdgesChange,
OnNodesChange,
@@ -13,7 +14,10 @@ import ReactFlow, {
} from 'reactflow';
import 'reactflow/dist/style.css';
-import { useHandleDrop } from '../hooks';
+import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';
+
+import FlowDrawer from '../flow-drawer';
+import { useHandleDrop, useShowDrawer } from '../hooks';
import { TextUpdaterNode } from './node';
const nodeTypes = { textUpdater: TextUpdaterNode };
@@ -42,9 +46,17 @@ const initialEdges = [
{ id: '1-2', source: '1', target: '2', label: 'to the', type: 'step' },
];
-function FlowCanvas() {
+interface IProps {
+ sideWidth: number;
+ showDrawer(): void;
+}
+
+function FlowCanvas({ sideWidth }: IProps) {
const [nodes, setNodes] = useState(initialNodes);
const [edges, setEdges] = useState(initialEdges);
+ const { ref, menu, onNodeContextMenu, onPaneClick } =
+ useHandleNodeContextMenu(sideWidth);
+ const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer();
const onNodesChange: OnNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
@@ -60,7 +72,11 @@ function FlowCanvas() {
[],
);
- const { handleDrop, allowDrop } = useHandleDrop(setNodes);
+ const onNodeClick: NodeMouseHandler = useCallback(() => {
+ showDrawer();
+ }, [showDrawer]);
+
+ const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(setNodes);
useEffect(() => {
console.info('nodes:', nodes);
@@ -68,23 +84,30 @@ function FlowCanvas() {
}, [nodes, edges]);
return (
-
+
+ {Object.keys(menu).length > 0 && (
+
+ )}
+
);
}
diff --git a/web/src/pages/flow/flow-drawer/index.tsx b/web/src/pages/flow/flow-drawer/index.tsx
new file mode 100644
index 000000000..7e395b257
--- /dev/null
+++ b/web/src/pages/flow/flow-drawer/index.tsx
@@ -0,0 +1,20 @@
+import { IModalProps } from '@/interfaces/common';
+import { Drawer } from 'antd';
+
+const FlowDrawer = ({ visible, hideModal }: IModalProps
) => {
+ return (
+
+ Some contents...
+
+ );
+};
+
+export default FlowDrawer;
diff --git a/web/src/pages/flow/flow-sider/index.tsx b/web/src/pages/flow/flow-sider/index.tsx
index 439c1265a..e82043e9d 100644
--- a/web/src/pages/flow/flow-sider/index.tsx
+++ b/web/src/pages/flow/flow-sider/index.tsx
@@ -1,6 +1,5 @@
import { Avatar, Card, Flex, Layout, Space } from 'antd';
import classNames from 'classnames';
-import { useState } from 'react';
import { componentList } from '../mock';
import { useHandleDrag } from '../hooks';
@@ -8,9 +7,13 @@ import styles from './index.less';
const { Sider } = Layout;
-const FlowSider = () => {
- const [collapsed, setCollapsed] = useState(true);
- const { handleDrag } = useHandleDrag();
+interface IProps {
+ setCollapsed: (width: boolean) => void;
+ collapsed: boolean;
+}
+
+const FlowSide = ({ setCollapsed, collapsed }: IProps) => {
+ const { handleDragStart } = useHandleDrag();
return (
{
hoverable
draggable
className={classNames(styles.operatorCard)}
- onDragStart={handleDrag(x.name)}
+ onDragStart={handleDragStart(x.name)}
>
@@ -45,4 +48,4 @@ const FlowSider = () => {
);
};
-export default FlowSider;
+export default FlowSide;
diff --git a/web/src/pages/flow/hooks.ts b/web/src/pages/flow/hooks.ts
index 3fde00654..23754145f 100644
--- a/web/src/pages/flow/hooks.ts
+++ b/web/src/pages/flow/hooks.ts
@@ -1,47 +1,75 @@
-import React, { Dispatch, SetStateAction, useCallback } from 'react';
-import { Node } from 'reactflow';
+import { useSetModalState } from '@/hooks/commonHooks';
+import React, { Dispatch, SetStateAction, useCallback, useState } from 'react';
+import { Node, ReactFlowInstance } from 'reactflow';
+import { v4 as uuidv4 } from 'uuid';
export const useHandleDrag = () => {
- const handleDrag = useCallback(
+ const handleDragStart = useCallback(
(operatorId: string) => (ev: React.DragEvent) => {
- console.info(ev.clientX, ev.pageY);
- ev.dataTransfer.setData('operatorId', operatorId);
- ev.dataTransfer.setData('startClientX', ev.clientX.toString());
- ev.dataTransfer.setData('startClientY', ev.clientY.toString());
+ ev.dataTransfer.setData('application/reactflow', operatorId);
+ ev.dataTransfer.effectAllowed = 'move';
},
[],
);
- return { handleDrag };
+ return { handleDragStart };
};
export const useHandleDrop = (setNodes: Dispatch>) => {
- const allowDrop = (ev: React.DragEvent) => {
- ev.preventDefault();
- };
+ const [reactFlowInstance, setReactFlowInstance] =
+ useState>();
- const handleDrop = useCallback(
- (ev: React.DragEvent) => {
- ev.preventDefault();
- const operatorId = ev.dataTransfer.getData('operatorId');
- const startClientX = ev.dataTransfer.getData('startClientX');
- const startClientY = ev.dataTransfer.getData('startClientY');
- console.info(operatorId);
- console.info(ev.pageX, ev.pageY);
- console.info(ev.clientX, ev.clientY);
- console.info(ev.movementX, ev.movementY);
- const x = ev.clientX - 200;
- const y = ev.clientY - 72;
- setNodes((pre) => {
- return pre.concat({
- id: operatorId,
- position: { x, y },
- data: { label: operatorId },
- });
+ const onDragOver = useCallback((event: React.DragEvent) => {
+ event.preventDefault();
+ event.dataTransfer.dropEffect = 'move';
+ }, []);
+
+ const onDrop = useCallback(
+ (event: React.DragEvent) => {
+ event.preventDefault();
+
+ const type = event.dataTransfer.getData('application/reactflow');
+
+ // check if the dropped element is valid
+ if (typeof type === 'undefined' || !type) {
+ return;
+ }
+
+ // reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition
+ // and you don't need to subtract the reactFlowBounds.left/top anymore
+ // details: https://reactflow.dev/whats-new/2023-11-10
+ const position = reactFlowInstance?.screenToFlowPosition({
+ x: event.clientX,
+ y: event.clientY,
});
+ const newNode = {
+ id: uuidv4(),
+ type,
+ position: position || {
+ x: 0,
+ y: 0,
+ },
+ data: { label: `${type} node` },
+ };
+
+ setNodes((nds) => nds.concat(newNode));
},
- [setNodes],
+ [reactFlowInstance, setNodes],
);
- return { handleDrop, allowDrop };
+ return { onDrop, onDragOver, setReactFlowInstance };
+};
+
+export const useShowDrawer = () => {
+ const {
+ visible: drawerVisible,
+ hideModal: hideDrawer,
+ showModal: showDrawer,
+ } = useSetModalState();
+
+ return {
+ drawerVisible,
+ hideDrawer,
+ showDrawer,
+ };
};
diff --git a/web/src/pages/flow/index.tsx b/web/src/pages/flow/index.tsx
index d4c7b653f..5aa32abd7 100644
--- a/web/src/pages/flow/index.tsx
+++ b/web/src/pages/flow/index.tsx
@@ -1,18 +1,24 @@
import { Layout } from 'antd';
+import { useState } from 'react';
+import { ReactFlowProvider } from 'reactflow';
import FlowCanvas from './canvas';
import Sider from './flow-sider';
const { Content } = Layout;
function RagFlow() {
+ const [collapsed, setCollapsed] = useState(false);
+
return (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
}