feat: add FlowCanvas (#593)

### What problem does this PR solve?

feat: handle operator drag
feat: add FlowCanvas
#592

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2024-04-28 19:03:54 +08:00
committed by GitHub
parent daf215d266
commit aee8b48d2f
11 changed files with 847 additions and 0 deletions

View File

@ -0,0 +1,92 @@
import { useCallback, useEffect, useState } from 'react';
import ReactFlow, {
Background,
Controls,
Edge,
Node,
OnConnect,
OnEdgesChange,
OnNodesChange,
addEdge,
applyEdgeChanges,
applyNodeChanges,
} from 'reactflow';
import 'reactflow/dist/style.css';
import { useHandleDrop } from '../hooks';
import { TextUpdaterNode } from './node';
const nodeTypes = { textUpdater: TextUpdaterNode };
const initialNodes = [
{
id: 'node-1',
type: 'textUpdater',
position: { x: 200, y: 50 },
data: { value: 123 },
},
{
id: '1',
data: { label: 'Hello' },
position: { x: 0, y: 0 },
type: 'input',
},
{
id: '2',
data: { label: 'World' },
position: { x: 100, y: 100 },
},
];
const initialEdges = [
{ id: '1-2', source: '1', target: '2', label: 'to the', type: 'step' },
];
function FlowCanvas() {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onNodesChange: OnNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[],
);
const onEdgesChange: OnEdgesChange = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[],
);
const onConnect: OnConnect = useCallback(
(params) => setEdges((eds) => addEdge(params, eds)),
[],
);
const { handleDrop, allowDrop } = useHandleDrop(setNodes);
useEffect(() => {
console.info('nodes:', nodes);
console.info('edges:', edges);
}, [nodes, edges]);
return (
<div
style={{ height: '100%', width: '100%' }}
onDrop={handleDrop}
onDragOver={allowDrop}
>
<ReactFlow
nodes={nodes}
onNodesChange={onNodesChange}
edges={edges}
onEdgesChange={onEdgesChange}
// fitView
onConnect={onConnect}
nodeTypes={nodeTypes}
>
<Background />
<Controls />
</ReactFlow>
</div>
);
}
export default FlowCanvas;

View File

@ -0,0 +1,12 @@
.textUpdaterNode {
height: 50px;
border: 1px solid #eee;
padding: 5px;
border-radius: 5px;
background: white;
label {
display: block;
color: #777;
font-size: 12px;
}
}

View File

@ -0,0 +1,41 @@
import { useCallback } from 'react';
import { Handle, NodeProps, Position } from 'reactflow';
import styles from './index.less';
const handleStyle = { left: 10 };
export function TextUpdaterNode({
data,
isConnectable = true,
}: NodeProps<{ value: number }>) {
const onChange = useCallback((evt) => {
console.log(evt.target.value);
}, []);
return (
<div className={styles.textUpdaterNode}>
<Handle
type="target"
position={Position.Top}
isConnectable={isConnectable}
/>
<Handle
type="source"
position={Position.Bottom}
// style={handleStyle}
isConnectable={isConnectable}
/>
<div>
<label htmlFor="text">Text:</label>
<input id="text" name="text" onChange={onChange} className="nodrag" />
</div>
{/* <Handle
type="source"
position={Position.Bottom}
id="b"
isConnectable={isConnectable}
/> */}
</div>
);
}

View File

@ -0,0 +1,14 @@
.operatorCard {
:global(.ant-card-body) {
padding: 10px;
}
.cubeIcon {
&:hover {
cursor: pointer;
}
}
}
.siderContent {
padding: 10px 4px;
}

View File

@ -0,0 +1,48 @@
import { Avatar, Card, Flex, Layout, Space } from 'antd';
import classNames from 'classnames';
import { useState } from 'react';
import { componentList } from '../mock';
import { useHandleDrag } from '../hooks';
import styles from './index.less';
const { Sider } = Layout;
const FlowSider = () => {
const [collapsed, setCollapsed] = useState(true);
const { handleDrag } = useHandleDrag();
return (
<Sider
collapsible
collapsed={collapsed}
collapsedWidth={0}
theme={'light'}
onCollapse={(value) => setCollapsed(value)}
>
<Flex vertical gap={10} className={styles.siderContent}>
{componentList.map((x) => (
<Card
key={x.name}
hoverable
draggable
className={classNames(styles.operatorCard)}
onDragStart={handleDrag(x.name)}
>
<Flex justify="space-between" align="center">
<Space size={15}>
<Avatar icon={x.icon} shape={'square'} />
<section>
<b>{x.name}</b>
<div>{x.description}</div>
</section>
</Space>
</Flex>
</Card>
))}
</Flex>
</Sider>
);
};
export default FlowSider;

View File

@ -0,0 +1,47 @@
import React, { Dispatch, SetStateAction, useCallback } from 'react';
import { Node } from 'reactflow';
export const useHandleDrag = () => {
const handleDrag = useCallback(
(operatorId: string) => (ev: React.DragEvent<HTMLDivElement>) => {
console.info(ev.clientX, ev.pageY);
ev.dataTransfer.setData('operatorId', operatorId);
ev.dataTransfer.setData('startClientX', ev.clientX.toString());
ev.dataTransfer.setData('startClientY', ev.clientY.toString());
},
[],
);
return { handleDrag };
};
export const useHandleDrop = (setNodes: Dispatch<SetStateAction<Node[]>>) => {
const allowDrop = (ev: React.DragEvent<HTMLDivElement>) => {
ev.preventDefault();
};
const handleDrop = useCallback(
(ev: React.DragEvent<HTMLDivElement>) => {
ev.preventDefault();
const operatorId = ev.dataTransfer.getData('operatorId');
const startClientX = ev.dataTransfer.getData('startClientX');
const startClientY = ev.dataTransfer.getData('startClientY');
console.info(operatorId);
console.info(ev.pageX, ev.pageY);
console.info(ev.clientX, ev.clientY);
console.info(ev.movementX, ev.movementY);
const x = ev.clientX - 200;
const y = ev.clientY - 72;
setNodes((pre) => {
return pre.concat({
id: operatorId,
position: { x, y },
data: { label: operatorId },
});
});
},
[setNodes],
);
return { handleDrop, allowDrop };
};

View File

@ -0,0 +1,20 @@
import { Layout } from 'antd';
import FlowCanvas from './canvas';
import Sider from './flow-sider';
const { Content } = Layout;
function RagFlow() {
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider></Sider>
<Layout>
<Content style={{ margin: '0 16px' }}>
<FlowCanvas></FlowCanvas>
</Content>
</Layout>
</Layout>
);
}
export default RagFlow;

View File

@ -0,0 +1,11 @@
import {
MergeCellsOutlined,
RocketOutlined,
SendOutlined,
} from '@ant-design/icons';
export const componentList = [
{ name: 'Begin', icon: <SendOutlined />, description: '' },
{ name: 'Retrieval', icon: <RocketOutlined />, description: '' },
{ name: 'Generate', icon: <MergeCellsOutlined />, description: '' },
];