### What problem does this PR solve? feat: add delete menu to graph node #918 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.8.0
| @@ -1,6 +1,5 @@ | |||
| import { ReactComponent as MoreIcon } from '@/assets/svg/more.svg'; | |||
| import { useShowDeleteConfirm } from '@/hooks/commonHooks'; | |||
| import { DeleteOutlined } from '@ant-design/icons'; | |||
| import { DeleteOutlined, MoreOutlined } from '@ant-design/icons'; | |||
| import { Dropdown, MenuProps, Space } from 'antd'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| @@ -8,12 +7,14 @@ import React from 'react'; | |||
| import styles from './index.less'; | |||
| interface IProps { | |||
| deleteItem: () => Promise<any>; | |||
| deleteItem: () => Promise<any> | void; | |||
| iconFontSize?: number; | |||
| } | |||
| const OperateDropdown = ({ | |||
| deleteItem, | |||
| children, | |||
| iconFontSize = 30, | |||
| }: React.PropsWithChildren<IProps>) => { | |||
| const { t } = useTranslation(); | |||
| const showDeleteConfirm = useShowDeleteConfirm(); | |||
| @@ -51,7 +52,10 @@ const OperateDropdown = ({ | |||
| > | |||
| {children || ( | |||
| <span className={styles.delete}> | |||
| <MoreIcon /> | |||
| <MoreOutlined | |||
| rotate={90} | |||
| style={{ fontSize: iconFontSize, color: 'gray', cursor: 'pointer' }} | |||
| /> | |||
| </span> | |||
| )} | |||
| </Dropdown> | |||
| @@ -18,12 +18,12 @@ import { | |||
| useSelectCanvasData, | |||
| useShowDrawer, | |||
| } from '../hooks'; | |||
| import { TextUpdaterNode } from './node'; | |||
| import { RagNode } from './node'; | |||
| import ChatDrawer from '../chat/drawer'; | |||
| import styles from './index.less'; | |||
| const nodeTypes = { textUpdater: TextUpdaterNode }; | |||
| const nodeTypes = { ragNode: RagNode }; | |||
| const edgeTypes = { | |||
| buttonEdge: ButtonEdge, | |||
| @@ -1,5 +1,6 @@ | |||
| .textUpdaterNode { | |||
| .ragNode { | |||
| // height: 50px; | |||
| position: relative; | |||
| box-shadow: | |||
| -6px 0 12px 0 rgba(179, 177, 177, 0.08), | |||
| -3px 0 6px -4px rgba(0, 0, 0, 0.12), | |||
| @@ -13,6 +14,9 @@ | |||
| color: #777; | |||
| font-size: 12px; | |||
| } | |||
| .description { | |||
| font-size: 10px; | |||
| } | |||
| } | |||
| .selectedNode { | |||
| border: 1px solid rgb(59, 118, 244); | |||
| @@ -1,19 +1,28 @@ | |||
| import classNames from 'classnames'; | |||
| import { Handle, NodeProps, Position } from 'reactflow'; | |||
| import { Space } from 'antd'; | |||
| import { Operator } from '../../constant'; | |||
| import OperateDropdown from '@/components/operate-dropdown'; | |||
| import { Flex, Space } from 'antd'; | |||
| import { useCallback } from 'react'; | |||
| import { Operator, operatorMap } from '../../constant'; | |||
| import OperatorIcon from '../../operator-icon'; | |||
| import useGraphStore from '../../store'; | |||
| import styles from './index.less'; | |||
| export function TextUpdaterNode({ | |||
| export function RagNode({ | |||
| id, | |||
| data, | |||
| isConnectable = true, | |||
| selected, | |||
| }: NodeProps<{ label: string }>) { | |||
| const deleteNodeById = useGraphStore((store) => store.deleteNodeById); | |||
| const deleteNode = useCallback(() => { | |||
| deleteNodeById(id); | |||
| }, [id, deleteNodeById]); | |||
| return ( | |||
| <section | |||
| className={classNames(styles.textUpdaterNode, { | |||
| className={classNames(styles.ragNode, { | |||
| [styles.selectedNode]: selected, | |||
| })} | |||
| > | |||
| @@ -37,14 +46,21 @@ export function TextUpdaterNode({ | |||
| {/* <PlusCircleOutlined style={{ fontSize: 10 }} /> */} | |||
| </Handle> | |||
| <Handle type="source" position={Position.Bottom} id="a" isConnectable /> | |||
| <div> | |||
| <Space size={4}> | |||
| <Flex gap={10} justify={'space-between'}> | |||
| <Space size={6}> | |||
| <OperatorIcon | |||
| name={data.label as Operator} | |||
| fontSize={12} | |||
| ></OperatorIcon> | |||
| {data.label} | |||
| <span>{data.label}</span> | |||
| </Space> | |||
| <OperateDropdown | |||
| iconFontSize={14} | |||
| deleteItem={deleteNode} | |||
| ></OperateDropdown> | |||
| </Flex> | |||
| <div className={styles.description}> | |||
| {operatorMap[data.label as Operator].description} | |||
| </div> | |||
| </section> | |||
| ); | |||
| @@ -19,18 +19,27 @@ export const operatorIconMap = { | |||
| [Operator.Begin]: SlidersOutlined, | |||
| }; | |||
| export const componentList = [ | |||
| export const operatorMap = { | |||
| [Operator.Retrieval]: { | |||
| description: 'Retrieval description drjlftglrthjftl', | |||
| }, | |||
| [Operator.Generate]: { description: 'Generate description' }, | |||
| [Operator.Answer]: { description: 'Answer description' }, | |||
| [Operator.Begin]: { description: 'Begin description' }, | |||
| }; | |||
| export const componentMenuList = [ | |||
| { | |||
| name: Operator.Retrieval, | |||
| description: '', | |||
| description: operatorMap[Operator.Retrieval].description, | |||
| }, | |||
| { | |||
| name: Operator.Generate, | |||
| description: '', | |||
| description: operatorMap[Operator.Generate].description, | |||
| }, | |||
| { | |||
| name: Operator.Answer, | |||
| description: '', | |||
| description: operatorMap[Operator.Answer].description, | |||
| }, | |||
| ]; | |||
| @@ -1,13 +1,15 @@ | |||
| import { Card, Flex, Layout, Space } from 'antd'; | |||
| import { Card, Flex, Layout, Space, Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { componentList } from '../constant'; | |||
| import { componentMenuList } from '../constant'; | |||
| import { useHandleDrag } from '../hooks'; | |||
| import OperatorIcon from '../operator-icon'; | |||
| import styles from './index.less'; | |||
| const { Sider } = Layout; | |||
| const { Text } = Typography; | |||
| interface IProps { | |||
| setCollapsed: (width: boolean) => void; | |||
| collapsed: boolean; | |||
| @@ -25,7 +27,7 @@ const FlowSide = ({ setCollapsed, collapsed }: IProps) => { | |||
| onCollapse={(value) => setCollapsed(value)} | |||
| > | |||
| <Flex vertical gap={10} className={styles.siderContent}> | |||
| {componentList.map((x) => { | |||
| {componentMenuList.map((x) => { | |||
| return ( | |||
| <Card | |||
| key={x.name} | |||
| @@ -37,13 +39,14 @@ const FlowSide = ({ setCollapsed, collapsed }: IProps) => { | |||
| <Flex justify="space-between" align="center"> | |||
| <Space size={15}> | |||
| <OperatorIcon name={x.name}></OperatorIcon> | |||
| {/* <Avatar | |||
| icon={<OperatorIcon name={x.name}></OperatorIcon>} | |||
| shape={'square'} | |||
| /> */} | |||
| <section> | |||
| <b>{x.name}</b> | |||
| <div>{x.description}</div> | |||
| <Text | |||
| ellipsis={{ tooltip: x.description }} | |||
| style={{ width: 130 }} | |||
| > | |||
| {x.description} | |||
| </Text> | |||
| </section> | |||
| </Space> | |||
| </Flex> | |||
| @@ -18,6 +18,7 @@ import { Node, Position, ReactFlowInstance } from 'reactflow'; | |||
| import { useDebounceEffect } from 'ahooks'; | |||
| import { humanId } from 'human-id'; | |||
| import { useParams } from 'umi'; | |||
| import { Operator } from './constant'; | |||
| import useGraphStore, { RFState } from './store'; | |||
| import { buildDslComponentsByGraph } from './utils'; | |||
| @@ -79,7 +80,7 @@ export const useHandleDrop = () => { | |||
| }); | |||
| const newNode = { | |||
| id: `${type}:${humanId()}`, | |||
| type: 'textUpdater', | |||
| type: 'ragNode', | |||
| position: position || { | |||
| x: 0, | |||
| y: 0, | |||
| @@ -110,7 +111,9 @@ export const useShowDrawer = () => { | |||
| const handleShow = useCallback( | |||
| (node: Node) => { | |||
| setClickedNode(node); | |||
| showDrawer(); | |||
| if (node.data.label !== Operator.Answer) { | |||
| showDrawer(); | |||
| } | |||
| }, | |||
| [showDrawer], | |||
| ); | |||
| @@ -5,7 +5,7 @@ export const initialNodes = [ | |||
| sourcePosition: Position.Left, | |||
| targetPosition: Position.Right, | |||
| id: 'node-1', | |||
| type: 'textUpdater', | |||
| type: 'ragNode', | |||
| position: { x: 0, y: 0 }, | |||
| // position: { x: 400, y: 100 }, | |||
| data: { label: 123 }, | |||
| @@ -38,7 +38,7 @@ export const dsl = { | |||
| nodes: [ | |||
| { | |||
| id: 'begin', | |||
| type: 'textUpdater', | |||
| type: 'ragNode', | |||
| position: { | |||
| x: 50, | |||
| y: 200, | |||
| @@ -51,7 +51,7 @@ export const dsl = { | |||
| }, | |||
| // { | |||
| // id: 'Answer:China', | |||
| // type: 'textUpdater', | |||
| // type: 'ragNode', | |||
| // position: { | |||
| // x: 150, | |||
| // y: 200, | |||
| @@ -64,7 +64,7 @@ export const dsl = { | |||
| // }, | |||
| // { | |||
| // id: 'Retrieval:China', | |||
| // type: 'textUpdater', | |||
| // type: 'ragNode', | |||
| // position: { | |||
| // x: 250, | |||
| // y: 200, | |||
| @@ -77,7 +77,7 @@ export const dsl = { | |||
| // }, | |||
| // { | |||
| // id: 'Generate:China', | |||
| // type: 'textUpdater', | |||
| // type: 'ragNode', | |||
| // position: { | |||
| // x: 100, | |||
| // y: 100, | |||
| @@ -34,75 +34,88 @@ export type RFState = { | |||
| addNode: (nodes: Node) => void; | |||
| deleteEdge: () => void; | |||
| deleteEdgeById: (id: string) => void; | |||
| deleteNodeById: (id: string) => void; | |||
| findNodeByName: (operatorName: Operator) => Node | undefined; | |||
| }; | |||
| // this is our useStore hook that we can use in our components to get parts of the store and call actions | |||
| const useGraphStore = create<RFState>()( | |||
| devtools((set, get) => ({ | |||
| nodes: [] as Node[], | |||
| edges: [] as Edge[], | |||
| selectedNodeIds: [], | |||
| selectedEdgeIds: [], | |||
| onNodesChange: (changes: NodeChange[]) => { | |||
| set({ | |||
| nodes: applyNodeChanges(changes, get().nodes), | |||
| }); | |||
| }, | |||
| onEdgesChange: (changes: EdgeChange[]) => { | |||
| set({ | |||
| edges: applyEdgeChanges(changes, get().edges), | |||
| }); | |||
| }, | |||
| onConnect: (connection: Connection) => { | |||
| set({ | |||
| edges: addEdge(connection, get().edges), | |||
| }); | |||
| }, | |||
| onSelectionChange: ({ nodes, edges }: OnSelectionChangeParams) => { | |||
| set({ | |||
| selectedEdgeIds: edges.map((x) => x.id), | |||
| selectedNodeIds: nodes.map((x) => x.id), | |||
| }); | |||
| }, | |||
| setNodes: (nodes: Node[]) => { | |||
| set({ nodes }); | |||
| }, | |||
| setEdges: (edges: Edge[]) => { | |||
| set({ edges }); | |||
| }, | |||
| addNode: (node: Node) => { | |||
| set({ nodes: get().nodes.concat(node) }); | |||
| }, | |||
| deleteEdge: () => { | |||
| const { edges, selectedEdgeIds } = get(); | |||
| set({ | |||
| edges: edges.filter((edge) => | |||
| selectedEdgeIds.every((x) => x !== edge.id), | |||
| ), | |||
| }); | |||
| }, | |||
| deleteEdgeById: (id: string) => { | |||
| const { edges } = get(); | |||
| set({ | |||
| edges: edges.filter((edge) => edge.id !== id), | |||
| }); | |||
| }, | |||
| findNodeByName: (name: Operator) => { | |||
| return get().nodes.find((x) => x.data.label === name); | |||
| }, | |||
| updateNodeForm: (nodeId: string, values: any) => { | |||
| set({ | |||
| nodes: get().nodes.map((node) => { | |||
| if (node.id === nodeId) { | |||
| node.data = { ...node.data, form: values }; | |||
| } | |||
| devtools( | |||
| (set, get) => ({ | |||
| nodes: [] as Node[], | |||
| edges: [] as Edge[], | |||
| selectedNodeIds: [] as string[], | |||
| selectedEdgeIds: [] as string[], | |||
| onNodesChange: (changes: NodeChange[]) => { | |||
| set({ | |||
| nodes: applyNodeChanges(changes, get().nodes), | |||
| }); | |||
| }, | |||
| onEdgesChange: (changes: EdgeChange[]) => { | |||
| set({ | |||
| edges: applyEdgeChanges(changes, get().edges), | |||
| }); | |||
| }, | |||
| onConnect: (connection: Connection) => { | |||
| set({ | |||
| edges: addEdge(connection, get().edges), | |||
| }); | |||
| }, | |||
| onSelectionChange: ({ nodes, edges }: OnSelectionChangeParams) => { | |||
| set({ | |||
| selectedEdgeIds: edges.map((x) => x.id), | |||
| selectedNodeIds: nodes.map((x) => x.id), | |||
| }); | |||
| }, | |||
| setNodes: (nodes: Node[]) => { | |||
| set({ nodes }); | |||
| }, | |||
| setEdges: (edges: Edge[]) => { | |||
| set({ edges }); | |||
| }, | |||
| addNode: (node: Node) => { | |||
| set({ nodes: get().nodes.concat(node) }); | |||
| }, | |||
| deleteEdge: () => { | |||
| const { edges, selectedEdgeIds } = get(); | |||
| set({ | |||
| edges: edges.filter((edge) => | |||
| selectedEdgeIds.every((x) => x !== edge.id), | |||
| ), | |||
| }); | |||
| }, | |||
| deleteEdgeById: (id: string) => { | |||
| const { edges } = get(); | |||
| set({ | |||
| edges: edges.filter((edge) => edge.id !== id), | |||
| }); | |||
| }, | |||
| deleteNodeById: (id: string) => { | |||
| const { nodes, edges } = get(); | |||
| set({ | |||
| nodes: nodes.filter((node) => node.id !== id), | |||
| edges: edges | |||
| .filter((edge) => edge.source !== id) | |||
| .filter((edge) => edge.target !== id), | |||
| }); | |||
| }, | |||
| findNodeByName: (name: Operator) => { | |||
| return get().nodes.find((x) => x.data.label === name); | |||
| }, | |||
| updateNodeForm: (nodeId: string, values: any) => { | |||
| set({ | |||
| nodes: get().nodes.map((node) => { | |||
| if (node.id === nodeId) { | |||
| node.data = { ...node.data, form: values }; | |||
| } | |||
| return node; | |||
| }), | |||
| }); | |||
| }, | |||
| })), | |||
| return node; | |||
| }), | |||
| }); | |||
| }, | |||
| }), | |||
| { name: 'graph' }, | |||
| ), | |||
| ); | |||
| export default useGraphStore; | |||
| @@ -41,7 +41,7 @@ export const buildNodesAndEdgesFromDSLComponents = (data: DSLComponents) => { | |||
| const upstream = [...value.upstream]; | |||
| nodes.push({ | |||
| id: key, | |||
| type: 'textUpdater', | |||
| type: 'ragNode', | |||
| position: { x: 0, y: 0 }, | |||
| data: { | |||
| label: value.obj.component_name, | |||