feat: add delete menu to graph node #918 (#1133)

### 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)
This commit is contained in:
balibabu 2024-06-12 17:38:41 +08:00 committed by GitHub
parent e05395d2a7
commit 3b7b6240c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 149 additions and 97 deletions

View File

@ -1,6 +1,5 @@
import { ReactComponent as MoreIcon } from '@/assets/svg/more.svg';
import { useShowDeleteConfirm } from '@/hooks/commonHooks'; 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 { Dropdown, MenuProps, Space } from 'antd';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -8,12 +7,14 @@ import React from 'react';
import styles from './index.less'; import styles from './index.less';
interface IProps { interface IProps {
deleteItem: () => Promise<any>; deleteItem: () => Promise<any> | void;
iconFontSize?: number;
} }
const OperateDropdown = ({ const OperateDropdown = ({
deleteItem, deleteItem,
children, children,
iconFontSize = 30,
}: React.PropsWithChildren<IProps>) => { }: React.PropsWithChildren<IProps>) => {
const { t } = useTranslation(); const { t } = useTranslation();
const showDeleteConfirm = useShowDeleteConfirm(); const showDeleteConfirm = useShowDeleteConfirm();
@ -51,7 +52,10 @@ const OperateDropdown = ({
> >
{children || ( {children || (
<span className={styles.delete}> <span className={styles.delete}>
<MoreIcon /> <MoreOutlined
rotate={90}
style={{ fontSize: iconFontSize, color: 'gray', cursor: 'pointer' }}
/>
</span> </span>
)} )}
</Dropdown> </Dropdown>

View File

@ -18,12 +18,12 @@ import {
useSelectCanvasData, useSelectCanvasData,
useShowDrawer, useShowDrawer,
} from '../hooks'; } from '../hooks';
import { TextUpdaterNode } from './node'; import { RagNode } from './node';
import ChatDrawer from '../chat/drawer'; import ChatDrawer from '../chat/drawer';
import styles from './index.less'; import styles from './index.less';
const nodeTypes = { textUpdater: TextUpdaterNode }; const nodeTypes = { ragNode: RagNode };
const edgeTypes = { const edgeTypes = {
buttonEdge: ButtonEdge, buttonEdge: ButtonEdge,

View File

@ -1,5 +1,6 @@
.textUpdaterNode { .ragNode {
// height: 50px; // height: 50px;
position: relative;
box-shadow: box-shadow:
-6px 0 12px 0 rgba(179, 177, 177, 0.08), -6px 0 12px 0 rgba(179, 177, 177, 0.08),
-3px 0 6px -4px rgba(0, 0, 0, 0.12), -3px 0 6px -4px rgba(0, 0, 0, 0.12),
@ -13,6 +14,9 @@
color: #777; color: #777;
font-size: 12px; font-size: 12px;
} }
.description {
font-size: 10px;
}
} }
.selectedNode { .selectedNode {
border: 1px solid rgb(59, 118, 244); border: 1px solid rgb(59, 118, 244);

View File

@ -1,19 +1,28 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { Handle, NodeProps, Position } from 'reactflow'; import { Handle, NodeProps, Position } from 'reactflow';
import { Space } from 'antd'; import OperateDropdown from '@/components/operate-dropdown';
import { Operator } from '../../constant'; import { Flex, Space } from 'antd';
import { useCallback } from 'react';
import { Operator, operatorMap } from '../../constant';
import OperatorIcon from '../../operator-icon'; import OperatorIcon from '../../operator-icon';
import useGraphStore from '../../store';
import styles from './index.less'; import styles from './index.less';
export function TextUpdaterNode({ export function RagNode({
id,
data, data,
isConnectable = true, isConnectable = true,
selected, selected,
}: NodeProps<{ label: string }>) { }: NodeProps<{ label: string }>) {
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
const deleteNode = useCallback(() => {
deleteNodeById(id);
}, [id, deleteNodeById]);
return ( return (
<section <section
className={classNames(styles.textUpdaterNode, { className={classNames(styles.ragNode, {
[styles.selectedNode]: selected, [styles.selectedNode]: selected,
})} })}
> >
@ -37,14 +46,21 @@ export function TextUpdaterNode({
{/* <PlusCircleOutlined style={{ fontSize: 10 }} /> */} {/* <PlusCircleOutlined style={{ fontSize: 10 }} /> */}
</Handle> </Handle>
<Handle type="source" position={Position.Bottom} id="a" isConnectable /> <Handle type="source" position={Position.Bottom} id="a" isConnectable />
<div> <Flex gap={10} justify={'space-between'}>
<Space size={4}> <Space size={6}>
<OperatorIcon <OperatorIcon
name={data.label as Operator} name={data.label as Operator}
fontSize={12} fontSize={12}
></OperatorIcon> ></OperatorIcon>
{data.label} <span>{data.label}</span>
</Space> </Space>
<OperateDropdown
iconFontSize={14}
deleteItem={deleteNode}
></OperateDropdown>
</Flex>
<div className={styles.description}>
{operatorMap[data.label as Operator].description}
</div> </div>
</section> </section>
); );

View File

@ -19,18 +19,27 @@ export const operatorIconMap = {
[Operator.Begin]: SlidersOutlined, [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, name: Operator.Retrieval,
description: '', description: operatorMap[Operator.Retrieval].description,
}, },
{ {
name: Operator.Generate, name: Operator.Generate,
description: '', description: operatorMap[Operator.Generate].description,
}, },
{ {
name: Operator.Answer, name: Operator.Answer,
description: '', description: operatorMap[Operator.Answer].description,
}, },
]; ];

View File

@ -1,13 +1,15 @@
import { Card, Flex, Layout, Space } from 'antd'; import { Card, Flex, Layout, Space, Typography } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { componentList } from '../constant'; import { componentMenuList } from '../constant';
import { useHandleDrag } from '../hooks'; import { useHandleDrag } from '../hooks';
import OperatorIcon from '../operator-icon'; import OperatorIcon from '../operator-icon';
import styles from './index.less'; import styles from './index.less';
const { Sider } = Layout; const { Sider } = Layout;
const { Text } = Typography;
interface IProps { interface IProps {
setCollapsed: (width: boolean) => void; setCollapsed: (width: boolean) => void;
collapsed: boolean; collapsed: boolean;
@ -25,7 +27,7 @@ const FlowSide = ({ setCollapsed, collapsed }: IProps) => {
onCollapse={(value) => setCollapsed(value)} onCollapse={(value) => setCollapsed(value)}
> >
<Flex vertical gap={10} className={styles.siderContent}> <Flex vertical gap={10} className={styles.siderContent}>
{componentList.map((x) => { {componentMenuList.map((x) => {
return ( return (
<Card <Card
key={x.name} key={x.name}
@ -37,13 +39,14 @@ const FlowSide = ({ setCollapsed, collapsed }: IProps) => {
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Space size={15}> <Space size={15}>
<OperatorIcon name={x.name}></OperatorIcon> <OperatorIcon name={x.name}></OperatorIcon>
{/* <Avatar
icon={<OperatorIcon name={x.name}></OperatorIcon>}
shape={'square'}
/> */}
<section> <section>
<b>{x.name}</b> <b>{x.name}</b>
<div>{x.description}</div> <Text
ellipsis={{ tooltip: x.description }}
style={{ width: 130 }}
>
{x.description}
</Text>
</section> </section>
</Space> </Space>
</Flex> </Flex>

View File

@ -18,6 +18,7 @@ import { Node, Position, ReactFlowInstance } from 'reactflow';
import { useDebounceEffect } from 'ahooks'; import { useDebounceEffect } from 'ahooks';
import { humanId } from 'human-id'; import { humanId } from 'human-id';
import { useParams } from 'umi'; import { useParams } from 'umi';
import { Operator } from './constant';
import useGraphStore, { RFState } from './store'; import useGraphStore, { RFState } from './store';
import { buildDslComponentsByGraph } from './utils'; import { buildDslComponentsByGraph } from './utils';
@ -79,7 +80,7 @@ export const useHandleDrop = () => {
}); });
const newNode = { const newNode = {
id: `${type}:${humanId()}`, id: `${type}:${humanId()}`,
type: 'textUpdater', type: 'ragNode',
position: position || { position: position || {
x: 0, x: 0,
y: 0, y: 0,
@ -110,7 +111,9 @@ export const useShowDrawer = () => {
const handleShow = useCallback( const handleShow = useCallback(
(node: Node) => { (node: Node) => {
setClickedNode(node); setClickedNode(node);
showDrawer(); if (node.data.label !== Operator.Answer) {
showDrawer();
}
}, },
[showDrawer], [showDrawer],
); );

View File

@ -5,7 +5,7 @@ export const initialNodes = [
sourcePosition: Position.Left, sourcePosition: Position.Left,
targetPosition: Position.Right, targetPosition: Position.Right,
id: 'node-1', id: 'node-1',
type: 'textUpdater', type: 'ragNode',
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
// position: { x: 400, y: 100 }, // position: { x: 400, y: 100 },
data: { label: 123 }, data: { label: 123 },
@ -38,7 +38,7 @@ export const dsl = {
nodes: [ nodes: [
{ {
id: 'begin', id: 'begin',
type: 'textUpdater', type: 'ragNode',
position: { position: {
x: 50, x: 50,
y: 200, y: 200,
@ -51,7 +51,7 @@ export const dsl = {
}, },
// { // {
// id: 'Answer:China', // id: 'Answer:China',
// type: 'textUpdater', // type: 'ragNode',
// position: { // position: {
// x: 150, // x: 150,
// y: 200, // y: 200,
@ -64,7 +64,7 @@ export const dsl = {
// }, // },
// { // {
// id: 'Retrieval:China', // id: 'Retrieval:China',
// type: 'textUpdater', // type: 'ragNode',
// position: { // position: {
// x: 250, // x: 250,
// y: 200, // y: 200,
@ -77,7 +77,7 @@ export const dsl = {
// }, // },
// { // {
// id: 'Generate:China', // id: 'Generate:China',
// type: 'textUpdater', // type: 'ragNode',
// position: { // position: {
// x: 100, // x: 100,
// y: 100, // y: 100,

View File

@ -34,75 +34,88 @@ export type RFState = {
addNode: (nodes: Node) => void; addNode: (nodes: Node) => void;
deleteEdge: () => void; deleteEdge: () => void;
deleteEdgeById: (id: string) => void; deleteEdgeById: (id: string) => void;
deleteNodeById: (id: string) => void;
findNodeByName: (operatorName: Operator) => Node | undefined; 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 // 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>()( const useGraphStore = create<RFState>()(
devtools((set, get) => ({ devtools(
nodes: [] as Node[], (set, get) => ({
edges: [] as Edge[], nodes: [] as Node[],
selectedNodeIds: [], edges: [] as Edge[],
selectedEdgeIds: [], selectedNodeIds: [] as string[],
onNodesChange: (changes: NodeChange[]) => { selectedEdgeIds: [] as string[],
set({ onNodesChange: (changes: NodeChange[]) => {
nodes: applyNodeChanges(changes, get().nodes), set({
}); nodes: applyNodeChanges(changes, get().nodes),
}, });
onEdgesChange: (changes: EdgeChange[]) => { },
set({ onEdgesChange: (changes: EdgeChange[]) => {
edges: applyEdgeChanges(changes, get().edges), set({
}); edges: applyEdgeChanges(changes, get().edges),
}, });
onConnect: (connection: Connection) => { },
set({ onConnect: (connection: Connection) => {
edges: addEdge(connection, get().edges), set({
}); edges: addEdge(connection, get().edges),
}, });
onSelectionChange: ({ nodes, edges }: OnSelectionChangeParams) => { },
set({ onSelectionChange: ({ nodes, edges }: OnSelectionChangeParams) => {
selectedEdgeIds: edges.map((x) => x.id), set({
selectedNodeIds: nodes.map((x) => x.id), selectedEdgeIds: edges.map((x) => x.id),
}); selectedNodeIds: nodes.map((x) => x.id),
}, });
setNodes: (nodes: Node[]) => { },
set({ nodes }); setNodes: (nodes: Node[]) => {
}, set({ nodes });
setEdges: (edges: Edge[]) => { },
set({ edges }); setEdges: (edges: Edge[]) => {
}, set({ edges });
addNode: (node: Node) => { },
set({ nodes: get().nodes.concat(node) }); addNode: (node: Node) => {
}, set({ nodes: get().nodes.concat(node) });
deleteEdge: () => { },
const { edges, selectedEdgeIds } = get(); deleteEdge: () => {
set({ const { edges, selectedEdgeIds } = get();
edges: edges.filter((edge) => set({
selectedEdgeIds.every((x) => x !== edge.id), edges: edges.filter((edge) =>
), selectedEdgeIds.every((x) => x !== edge.id),
}); ),
}, });
deleteEdgeById: (id: string) => { },
const { edges } = get(); deleteEdgeById: (id: string) => {
set({ const { edges } = get();
edges: edges.filter((edge) => edge.id !== id), set({
}); edges: edges.filter((edge) => edge.id !== id),
}, });
findNodeByName: (name: Operator) => { },
return get().nodes.find((x) => x.data.label === name); deleteNodeById: (id: string) => {
}, const { nodes, edges } = get();
updateNodeForm: (nodeId: string, values: any) => { set({
set({ nodes: nodes.filter((node) => node.id !== id),
nodes: get().nodes.map((node) => { edges: edges
if (node.id === nodeId) { .filter((edge) => edge.source !== id)
node.data = { ...node.data, form: values }; .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; export default useGraphStore;

View File

@ -41,7 +41,7 @@ export const buildNodesAndEdgesFromDSLComponents = (data: DSLComponents) => {
const upstream = [...value.upstream]; const upstream = [...value.upstream];
nodes.push({ nodes.push({
id: key, id: key,
type: 'textUpdater', type: 'ragNode',
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
data: { data: {
label: value.obj.component_name, label: value.obj.component_name,