### What problem does this PR solve? feat: duplicate node #918 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.8.0
| 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(); | ||||
| } | } | ||||
| }; | }; | ||||
| const items: MenuProps['items'] = [ | |||||
| { | |||||
| key: '1', | |||||
| label: ( | |||||
| <Space> | |||||
| {t('common.delete')} | |||||
| <DeleteOutlined /> | |||||
| </Space> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| const items: MenuProps['items'] = useMemo(() => { | |||||
| return [ | |||||
| { | |||||
| key: '1', | |||||
| label: ( | |||||
| <Space> | |||||
| {t('common.delete')} | |||||
| <DeleteOutlined /> | |||||
| </Space> | |||||
| ), | |||||
| }, | |||||
| ...otherItems, | |||||
| ]; | |||||
| }, [t, otherItems]); | |||||
| return ( | return ( | ||||
| <Dropdown | <Dropdown |
| ); | ); | ||||
| } | } | ||||
| /* @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); |
| } 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'; | ||||
| }; | }; | ||||
| 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, | ||||
| onSelectionChange, | onSelectionChange, | ||||
| } = useSelectCanvasData(); | } = useSelectCanvasData(); | ||||
| const { ref, menu, onNodeContextMenu, onPaneClick } = | |||||
| useHandleNodeContextMenu(sideWidth); | |||||
| const { drawerVisible, hideDrawer, showDrawer, clickedNode } = | const { drawerVisible, hideDrawer, showDrawer, clickedNode } = | ||||
| useShowDrawer(); | useShowDrawer(); | ||||
| 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} | ||||
| > | > | ||||
| <Background /> | <Background /> | ||||
| <Controls /> | <Controls /> | ||||
| {Object.keys(menu).length > 0 && ( | |||||
| <NodeContextMenu onClick={onPaneClick} {...(menu as any)} /> | |||||
| )} | |||||
| </ReactFlow> | </ReactFlow> | ||||
| <FlowDrawer | <FlowDrawer | ||||
| node={clickedNode} | node={clickedNode} |
| 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, { | ||||
| <OperateDropdown | <OperateDropdown | ||||
| iconFontSize={14} | iconFontSize={14} | ||||
| deleteItem={deleteNode} | deleteItem={deleteNode} | ||||
| items={items} | |||||
| ></OperateDropdown> | ></OperateDropdown> | ||||
| </Flex> | </Flex> | ||||
| <div className={styles.description}> | |||||
| {operatorMap[data.label as Operator].description} | |||||
| <div> | |||||
| <Text | |||||
| ellipsis={{ tooltip: description }} | |||||
| style={{ width: 130 }} | |||||
| className={styles.description} | |||||
| > | |||||
| {description} | |||||
| </Text> | |||||
| </div> | </div> | ||||
| </section> | </section> | ||||
| ); | ); |
| <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> |
| import type {} from '@redux-devtools/extension'; | import type {} from '@redux-devtools/extension'; | ||||
| import { humanId } from 'human-id'; | |||||
| import { | import { | ||||
| Connection, | Connection, | ||||
| Edge, | Edge, | ||||
| 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; | ||||
| 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({ |