feat: duplicate node #918 (#1136)

### What problem does this PR solve?
feat: duplicate node #918


### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2024-06-13 09:09:34 +08:00 committed by GitHub
parent 3b7b6240c3
commit 64c83f300a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 79 additions and 27 deletions

View File

@ -3,18 +3,20 @@ import { DeleteOutlined, MoreOutlined } from '@ant-design/icons';
import { Dropdown, MenuProps, Space } from 'antd'; import { Dropdown, MenuProps, Space } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import React from 'react'; import React, { useMemo } from 'react';
import styles from './index.less'; import styles from './index.less';
interface IProps { interface IProps {
deleteItem: () => Promise<any> | void; deleteItem: () => Promise<any> | void;
iconFontSize?: number; iconFontSize?: number;
items?: MenuProps['items'];
} }
const OperateDropdown = ({ const OperateDropdown = ({
deleteItem, deleteItem,
children, children,
iconFontSize = 30, iconFontSize = 30,
items: otherItems = [],
}: React.PropsWithChildren<IProps>) => { }: React.PropsWithChildren<IProps>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const showDeleteConfirm = useShowDeleteConfirm(); const showDeleteConfirm = useShowDeleteConfirm();
@ -31,17 +33,20 @@ const OperateDropdown = ({
} }
}; };
const items: MenuProps['items'] = [ const items: MenuProps['items'] = useMemo(() => {
{ return [
key: '1', {
label: ( key: '1',
<Space> label: (
{t('common.delete')} <Space>
<DeleteOutlined /> {t('common.delete')}
</Space> <DeleteOutlined />
), </Space>
}, ),
]; },
...otherItems,
];
}, [t, otherItems]);
return ( return (
<Dropdown <Dropdown

View File

@ -63,6 +63,8 @@ export function NodeContextMenu({
); );
} }
/* @deprecated
*/
export const useHandleNodeContextMenu = (sideWidth: number) => { export const useHandleNodeContextMenu = (sideWidth: number) => {
const [menu, setMenu] = useState<INodeContextMenu>({} as INodeContextMenu); const [menu, setMenu] = useState<INodeContextMenu>({} as INodeContextMenu);
const ref = useRef<any>(null); const ref = useRef<any>(null);

View File

@ -8,7 +8,6 @@ import ReactFlow, {
} from 'reactflow'; } from 'reactflow';
import 'reactflow/dist/style.css'; import 'reactflow/dist/style.css';
import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';
import { ButtonEdge } from './edge'; import { ButtonEdge } from './edge';
import FlowDrawer from '../flow-drawer'; import FlowDrawer from '../flow-drawer';
@ -30,12 +29,11 @@ const edgeTypes = {
}; };
interface IProps { interface IProps {
sideWidth: number;
chatDrawerVisible: boolean; chatDrawerVisible: boolean;
hideChatDrawer(): void; hideChatDrawer(): void;
} }
function FlowCanvas({ sideWidth, chatDrawerVisible, hideChatDrawer }: IProps) { function FlowCanvas({ chatDrawerVisible, hideChatDrawer }: IProps) {
const { const {
nodes, nodes,
edges, edges,
@ -45,8 +43,6 @@ function FlowCanvas({ sideWidth, chatDrawerVisible, hideChatDrawer }: IProps) {
onSelectionChange, onSelectionChange,
} = useSelectCanvasData(); } = useSelectCanvasData();
const { ref, menu, onNodeContextMenu, onPaneClick } =
useHandleNodeContextMenu(sideWidth);
const { drawerVisible, hideDrawer, showDrawer, clickedNode } = const { drawerVisible, hideDrawer, showDrawer, clickedNode } =
useShowDrawer(); useShowDrawer();
@ -64,18 +60,15 @@ function FlowCanvas({ sideWidth, chatDrawerVisible, hideChatDrawer }: IProps) {
return ( return (
<div className={styles.canvasWrapper}> <div className={styles.canvasWrapper}>
<ReactFlow <ReactFlow
ref={ref}
connectionMode={ConnectionMode.Loose} connectionMode={ConnectionMode.Loose}
nodes={nodes} nodes={nodes}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onNodeContextMenu={onNodeContextMenu}
edges={edges} edges={edges}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
fitView fitView
onConnect={onConnect} onConnect={onConnect}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
onPaneClick={onPaneClick}
onDrop={onDrop} onDrop={onDrop}
onDragOver={onDragOver} onDragOver={onDragOver}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
@ -95,9 +88,6 @@ function FlowCanvas({ sideWidth, chatDrawerVisible, hideChatDrawer }: IProps) {
> >
<Background /> <Background />
<Controls /> <Controls />
{Object.keys(menu).length > 0 && (
<NodeContextMenu onClick={onPaneClick} {...(menu as any)} />
)}
</ReactFlow> </ReactFlow>
<FlowDrawer <FlowDrawer
node={clickedNode} node={clickedNode}

View File

@ -2,24 +2,50 @@ import classNames from 'classnames';
import { Handle, NodeProps, Position } from 'reactflow'; import { Handle, NodeProps, Position } from 'reactflow';
import OperateDropdown from '@/components/operate-dropdown'; import OperateDropdown from '@/components/operate-dropdown';
import { Flex, Space } from 'antd'; import { CopyOutlined } from '@ant-design/icons';
import { Flex, MenuProps, Space, Typography } from 'antd';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Operator, operatorMap } from '../../constant'; import { Operator, operatorMap } from '../../constant';
import OperatorIcon from '../../operator-icon'; import OperatorIcon from '../../operator-icon';
import useGraphStore from '../../store'; import useGraphStore from '../../store';
import styles from './index.less'; import styles from './index.less';
const { Text } = Typography;
export function RagNode({ export function RagNode({
id, id,
data, data,
isConnectable = true, isConnectable = true,
selected, selected,
}: NodeProps<{ label: string }>) { }: NodeProps<{ label: string }>) {
const { t } = useTranslation();
const deleteNodeById = useGraphStore((store) => store.deleteNodeById); const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
const duplicateNodeById = useGraphStore((store) => store.duplicateNode);
const deleteNode = useCallback(() => { const deleteNode = useCallback(() => {
deleteNodeById(id); deleteNodeById(id);
}, [id, deleteNodeById]); }, [id, deleteNodeById]);
const duplicateNode = useCallback(() => {
duplicateNodeById(id);
}, [id, duplicateNodeById]);
const description = operatorMap[data.label as Operator].description;
const items: MenuProps['items'] = [
{
key: '2',
onClick: duplicateNode,
label: (
<Flex justify={'space-between'}>
{t('common.copy')}
<CopyOutlined />
</Flex>
),
},
];
return ( return (
<section <section
className={classNames(styles.ragNode, { className={classNames(styles.ragNode, {
@ -57,10 +83,17 @@ export function RagNode({
<OperateDropdown <OperateDropdown
iconFontSize={14} iconFontSize={14}
deleteItem={deleteNode} deleteItem={deleteNode}
items={items}
></OperateDropdown> ></OperateDropdown>
</Flex> </Flex>
<div className={styles.description}> <div>
{operatorMap[data.label as Operator].description} <Text
ellipsis={{ tooltip: description }}
style={{ width: 130 }}
className={styles.description}
>
{description}
</Text>
</div> </div>
</section> </section>
); );

View File

@ -27,7 +27,6 @@ function RagFlow() {
<FlowHeader showChatDrawer={showChatDrawer}></FlowHeader> <FlowHeader showChatDrawer={showChatDrawer}></FlowHeader>
<Content style={{ margin: 0 }}> <Content style={{ margin: 0 }}>
<FlowCanvas <FlowCanvas
sideWidth={collapsed ? 0 : 200}
chatDrawerVisible={chatDrawerVisible} chatDrawerVisible={chatDrawerVisible}
hideChatDrawer={hideChatDrawer} hideChatDrawer={hideChatDrawer}
></FlowCanvas> ></FlowCanvas>

View File

@ -1,4 +1,5 @@
import type {} from '@redux-devtools/extension'; import type {} from '@redux-devtools/extension';
import { humanId } from 'human-id';
import { import {
Connection, Connection,
Edge, Edge,
@ -32,6 +33,8 @@ export type RFState = {
updateNodeForm: (nodeId: string, values: any) => void; updateNodeForm: (nodeId: string, values: any) => void;
onSelectionChange: OnSelectionChangeFunc; onSelectionChange: OnSelectionChangeFunc;
addNode: (nodes: Node) => void; addNode: (nodes: Node) => void;
getNode: (id: string) => Node | undefined;
duplicateNode: (id: string) => void;
deleteEdge: () => void; deleteEdge: () => void;
deleteEdgeById: (id: string) => void; deleteEdgeById: (id: string) => void;
deleteNodeById: (id: string) => void; deleteNodeById: (id: string) => void;
@ -76,6 +79,26 @@ const useGraphStore = create<RFState>()(
addNode: (node: Node) => { addNode: (node: Node) => {
set({ nodes: get().nodes.concat(node) }); set({ nodes: get().nodes.concat(node) });
}, },
getNode: (id: string) => {
return get().nodes.find((x) => x.id === id);
},
duplicateNode: (id: string) => {
const { getNode, addNode } = get();
const node = getNode(id);
const position = {
x: (node?.position?.x || 0) + 30,
y: (node?.position?.y || 0) + 20,
};
addNode({
...(node || {}),
data: node?.data,
selected: false,
dragging: false,
id: `${node?.data?.label}:${humanId()}`,
position,
});
},
deleteEdge: () => { deleteEdge: () => {
const { edges, selectedEdgeIds } = get(); const { edges, selectedEdgeIds } = get();
set({ set({