diff --git a/web/src/pages/flow/canvas/index.tsx b/web/src/pages/flow/canvas/index.tsx
index 2794ff526..d92420e58 100644
--- a/web/src/pages/flow/canvas/index.tsx
+++ b/web/src/pages/flow/canvas/index.tsx
@@ -125,7 +125,6 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
onInit={setReactFlowInstance}
- // onKeyUp={handleKeyUp}
onSelectionChange={onSelectionChange}
nodeOrigin={[0.5, 0]}
isValidConnection={isValidConnection}
@@ -141,6 +140,18 @@ function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
},
}}
deleteKeyCode={['Delete', 'Backspace']}
+ onPaste={(...params) => {
+ console.info('onPaste:', ...params);
+ }}
+ onPasteCapture={(...params) => {
+ console.info('onPasteCapture:', ...params);
+ }}
+ onCopy={(...params) => {
+ console.info('onCopy:', ...params);
+ }}
+ onCopyCapture={(...params) => {
+ console.info('onCopyCapture:', ...params);
+ }}
>
diff --git a/web/src/pages/flow/canvas/node/dropdown.tsx b/web/src/pages/flow/canvas/node/dropdown.tsx
index ea8fcc1fd..7e6fb1e98 100644
--- a/web/src/pages/flow/canvas/node/dropdown.tsx
+++ b/web/src/pages/flow/canvas/node/dropdown.tsx
@@ -3,7 +3,7 @@ import { CopyOutlined } from '@ant-design/icons';
import { Flex, MenuProps } from 'antd';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
-import { useGetNodeName } from '../../hooks';
+import { useDuplicateNode } from '../../hooks';
import useGraphStore from '../../store';
interface IProps {
@@ -15,21 +15,17 @@ interface IProps {
const NodeDropdown = ({ id, iconFontColor, label }: IProps) => {
const { t } = useTranslation();
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
- const duplicateNodeById = useGraphStore((store) => store.duplicateNode);
- const getNodeName = useGetNodeName();
const deleteNode = useCallback(() => {
deleteNodeById(id);
}, [id, deleteNodeById]);
- const duplicateNode = useCallback(() => {
- duplicateNodeById(id, getNodeName(label));
- }, [duplicateNodeById, id, getNodeName, label]);
+ const duplicateNode = useDuplicateNode();
const items: MenuProps['items'] = [
{
key: '2',
- onClick: duplicateNode,
+ onClick: () => duplicateNode(id, label),
label: (
{t('common.copy')}
diff --git a/web/src/pages/flow/hooks.ts b/web/src/pages/flow/hooks.ts
index c6937d87b..0fb390db1 100644
--- a/web/src/pages/flow/hooks.ts
+++ b/web/src/pages/flow/hooks.ts
@@ -4,7 +4,6 @@ import { IGraph } from '@/interfaces/database/flow';
import { useIsFetching } from '@tanstack/react-query';
import React, {
ChangeEvent,
- KeyboardEventHandler,
useCallback,
useEffect,
useMemo,
@@ -20,7 +19,6 @@ import {
import { useFetchModelId, useSendMessageWithSse } from '@/hooks/logic-hooks';
import { Variable } from '@/interfaces/database/chat';
import api from '@/utils/api';
-import { useDebounceEffect } from 'ahooks';
import { FormInstance, message } from 'antd';
import { humanId } from 'human-id';
import { lowerFirst } from 'lodash';
@@ -253,20 +251,6 @@ export const useShowDrawer = () => {
};
};
-export const useHandleKeyUp = () => {
- const deleteEdge = useGraphStore((state) => state.deleteEdge);
- const handleKeyUp: KeyboardEventHandler = useCallback(
- (e) => {
- if (e.code === 'Delete') {
- deleteEdge();
- }
- },
- [deleteEdge],
- );
-
- return { handleKeyUp };
-};
-
export const useSaveGraph = () => {
const { data } = useFetchFlow();
const { setFlow } = useSetFlow();
@@ -284,20 +268,6 @@ export const useSaveGraph = () => {
return { saveGraph };
};
-export const useWatchGraphChange = () => {
- const nodes = useGraphStore((state) => state.nodes);
- const edges = useGraphStore((state) => state.edges);
- useDebounceEffect(
- () => {
- // console.info('useDebounceEffect');
- },
- [nodes, edges],
- {
- wait: 1000,
- },
- );
-};
-
export const useHandleFormValuesChange = (id?: string) => {
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
const handleValuesChange = useCallback(
@@ -348,8 +318,6 @@ export const useFetchDataOnMount = () => {
setGraphInfo(data?.dsl?.graph ?? ({} as IGraph));
}, [setGraphInfo, data]);
- useWatchGraphChange();
-
useEffect(() => {
refetch();
}, [refetch]);
@@ -640,3 +608,63 @@ export const useGetComponentLabelByValue = (nodeId: string) => {
);
return getLabel;
};
+
+export const useDuplicateNode = () => {
+ const duplicateNodeById = useGraphStore((store) => store.duplicateNode);
+ const getNodeName = useGetNodeName();
+
+ const duplicateNode = useCallback(
+ (id: string, label: string) => {
+ duplicateNodeById(id, getNodeName(label));
+ },
+ [duplicateNodeById, getNodeName],
+ );
+
+ return duplicateNode;
+};
+
+export const useCopyPaste = () => {
+ const nodes = useGraphStore((state) => state.nodes);
+ const duplicateNode = useDuplicateNode();
+
+ const onCopyCapture = useCallback(
+ (event: ClipboardEvent) => {
+ event.preventDefault();
+ const nodesStr = JSON.stringify(
+ nodes.filter((n) => n.selected && n.data.label !== Operator.Begin),
+ );
+
+ event.clipboardData?.setData('agent:nodes', nodesStr);
+ },
+ [nodes],
+ );
+
+ const onPasteCapture = useCallback(
+ (event: ClipboardEvent) => {
+ event.preventDefault();
+ const nodes = JSON.parse(
+ event.clipboardData?.getData('agent:nodes') || '[]',
+ ) as Node[] | undefined;
+ if (nodes) {
+ nodes.forEach((n) => {
+ duplicateNode(n.id, n.data.label);
+ });
+ }
+ },
+ [duplicateNode],
+ );
+
+ useEffect(() => {
+ window.addEventListener('copy', onCopyCapture);
+ return () => {
+ window.removeEventListener('copy', onCopyCapture);
+ };
+ }, [onCopyCapture]);
+
+ useEffect(() => {
+ window.addEventListener('paste', onPasteCapture);
+ return () => {
+ window.removeEventListener('paste', onPasteCapture);
+ };
+ }, [onPasteCapture]);
+};
diff --git a/web/src/pages/flow/index.tsx b/web/src/pages/flow/index.tsx
index 52bc54d5c..980c08b01 100644
--- a/web/src/pages/flow/index.tsx
+++ b/web/src/pages/flow/index.tsx
@@ -5,7 +5,7 @@ import { ReactFlowProvider } from 'reactflow';
import FlowCanvas from './canvas';
import Sider from './flow-sider';
import FlowHeader from './header';
-import { useFetchDataOnMount } from './hooks';
+import { useCopyPaste, useFetchDataOnMount } from './hooks';
const { Content } = Layout;
@@ -18,6 +18,7 @@ function RagFlow() {
} = useSetModalState();
useFetchDataOnMount();
+ useCopyPaste();
return (
diff --git a/web/src/pages/flow/store.ts b/web/src/pages/flow/store.ts
index cd15a4089..ea302ef9a 100644
--- a/web/src/pages/flow/store.ts
+++ b/web/src/pages/flow/store.ts
@@ -236,8 +236,8 @@ const useGraphStore = create()(
const { getNode, addNode, generateNodeName } = get();
const node = getNode(id);
const position = {
- x: (node?.position?.x || 0) + 30,
- y: (node?.position?.y || 0) + 20,
+ x: (node?.position?.x || 0) + 50,
+ y: (node?.position?.y || 0) + 50,
};
addNode({