mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
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:
92
web/src/pages/flow/canvas/index.tsx
Normal file
92
web/src/pages/flow/canvas/index.tsx
Normal 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;
|
||||
12
web/src/pages/flow/canvas/node/index.less
Normal file
12
web/src/pages/flow/canvas/node/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
41
web/src/pages/flow/canvas/node/index.tsx
Normal file
41
web/src/pages/flow/canvas/node/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
14
web/src/pages/flow/flow-sider/index.less
Normal file
14
web/src/pages/flow/flow-sider/index.less
Normal file
@ -0,0 +1,14 @@
|
||||
.operatorCard {
|
||||
:global(.ant-card-body) {
|
||||
padding: 10px;
|
||||
}
|
||||
.cubeIcon {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.siderContent {
|
||||
padding: 10px 4px;
|
||||
}
|
||||
48
web/src/pages/flow/flow-sider/index.tsx
Normal file
48
web/src/pages/flow/flow-sider/index.tsx
Normal 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;
|
||||
47
web/src/pages/flow/hooks.ts
Normal file
47
web/src/pages/flow/hooks.ts
Normal 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 };
|
||||
};
|
||||
20
web/src/pages/flow/index.tsx
Normal file
20
web/src/pages/flow/index.tsx
Normal 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;
|
||||
11
web/src/pages/flow/mock.tsx
Normal file
11
web/src/pages/flow/mock.tsx
Normal 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: '' },
|
||||
];
|
||||
Reference in New Issue
Block a user