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 { 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>

View File

@ -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,

View File

@ -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);

View File

@ -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>
);

View File

@ -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,
},
];

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 { 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>

View File

@ -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],
);

View File

@ -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,

View File

@ -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;

View File

@ -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,