背景
公司仿照Coze
想要实现一套自己的工作流,那么什么是工作流?
工作流是一系列可执行指令的集合,用于实现业务逻辑或完成特定任务。它为应用/智能体的数据流动和任务处理提供了一个结构化框架。工作流的核心在于将大模型的强大能力与特定的业务逻辑相结合,通过系统化、流程化的方法来实现高效、可扩展的AI
应用开发。
下面图中体现的为简易版工作流,节点中详细内容无法展示(涉及隐私)。
此外
Coze
的工作流模块已改版。
图中提供了节点选项列表和可视化画布,通过拖拽节点迅速搭建工作流。同时,支持在画布实时调试工作流。
节点选项
首先着手实现左侧节点选项列表,用于展示可供拖动的节点选项。代码中仅展示了两个节点选项:LLM
和 Condition
(作为示例)。用户可以从节点选项列表拖动这些节点选项到画布中进行操作。
NodeTools
局部公共组件不做赘述。详情可访问 github
tsx
import { Tabs } from 'antd';
import { DragEvent } from 'react';
import NodeTools from './NodeTools';
import conditionSvg from '@/assets/svg/condition.svg';
import llmSvg from '@/assets/svg/llm.svg';
const Sidebar = () => {
// 用于处理节点选项的拖动事件。
// 在拖动开始时,设置了数据传输的类型为 'application/reactflow',并设置了拖动效果为 'move'。
const onDragStart = (event: DragEvent<HTMLDivElement>, nodeType: string) => {
event.dataTransfer.setData('application/reactflow', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
const BasicNodes = () => (
<div className="flex flex-col gap-4">
<NodeTools
title="LLM"
imgSrc={llmSvg}
description="大模型节点,利用提示词和变量调用大模型输出结果。"
onDragStart={(event) => onDragStart(event, 'llm')}
/>
<NodeTools
title="Condition"
imgSrc={conditionSvg}
description="条件节点,根据设定条件进行判断。"
onDragStart={(event) => onDragStart(event, 'condition')}
/>
</div>
);
return (
<div className="relative flex flex-col p-4 work-flow-details-sidebar">
<Tabs
items={[{ key: '1', label: '基础组件', children: <BasicNodes /> }]}
/>
</div>
);
};
export default Sidebar;
可视化画布
其次再实现右侧画布,需要考虑了以下几个点:
- 画布中信息的状态更新
- 节点实现
- 如何连接节点
- 自定义连线 (需要符合设计稿)
- 流程图编辑器
状态管理
使用React
状态管理来管理画布的状态,确保画布上的节点和连接信息能够实时更新。
tsx
import { createContext, useContext } from 'react';
import type { Edge, EdgeChange, Node, NodeChange } from '@xyflow/react';
import type { Dispatch, SetStateAction } from 'react';
type OnChange<ChangesType> = (changes: ChangesType[]) => void;
type FlowNodeType = Node<{ id: string; data: Record<string, unknown> }>;
type FlowEdgeType = Edge<{ id: string; source: string; target: string }>;
type useFlowProviderStoreType = {
nodes: FlowNodeType[];
setNodes: Dispatch<SetStateAction<FlowNodeType[]>>;
onNodesChange: OnChange<NodeChange>;
edges: FlowEdgeType[];
setEdges: Dispatch<SetStateAction<FlowEdgeType[]>>;
onEdgesChange: OnChange<EdgeChange>;
};
export const StateContext = createContext<useFlowProviderStoreType>({
nodes: [],
setNodes: () => {},
onNodesChange: () => {},
edges: [],
setEdges: () => {},
onEdgesChange: () => {},
});
export const useFlowProviderStore = () => useContext(StateContext);
节点
此处仅展示LLm
节点的简单示例代码。
NodeCard
局部公共组件不做赘述。详情可访问 github
tsx
import { FC, memo } from 'react';
import SourceHandle from '../modules/SourceHandle';
import TargetHandle from '../modules/TargetHandle';
import NodeCard from '../render/NodeCard';
import llmSvg from '@/assets/svg/llm.svg';
import { NodeProps } from '../../types';
const NodeLLm: FC<NodeProps> = ({ id }) => {
return (
<NodeCard
id={id}
title="LLM"
description="大模型节点,利用提示词和变量调用大模型输出结果。"
imgSrc={llmSvg}
style={{ width: '500px' }}
isDelete={true}
>
<>
<SourceHandle hanleKey="llm-target-0" />
<TargetHandle hanleKey="llm-source-1" />
this is NodeLLm
</>
</NodeCard>
);
};
export default memo(NodeLLm);
连接节点
Handle
用于连接节点之间的关系。通过拖动Handle
来创建节点之间的连接线,形成节点之间的逻辑关系或数据流向。
TargetHandle
与SourceHandle
同理,仅需更改position
即可。
tsx
import { Handle, Position } from '@xyflow/react';
import { FC, memo } from 'react';
import { HandleProps } from '../../types';
import '../../index.less';
const SourceHandle: FC<HandleProps> = ({ hanleKey, className, styles }) => (
<div className={className}>
<Handle
style={{
background: `var(--global-color)`,
border: '2px solid #fff',
...styles,
}}
type="target"
id={hanleKey}
position={Position.Left}
isConnectable={true}
/>
</div>
);
export default memo(SourceHandle);
自定义连线
这段代码实现了自定义连线(Edge
)组件DeleteableEdge
,用于在React Flow
中绘制特定样式的连线,并在连线上添加一个关闭按钮,点击该按钮可以删除这条连线。
tsx
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getBezierPath,
useReactFlow,
} from '@xyflow/react';
import { ReactComponent as CloseSvg } from '@/assets/svg/close.svg';
const DeleteableEdge = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
}: EdgeProps) => {
const { setEdges } = useReactFlow();
// 调用 getBezierPath 函数计算连线的路径 edgePath,以及标签位置 labelX 和 labelY。
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
// 用于点击关闭按钮时删除这条连线。
const onEdgeClick = () =>
setEdges((edges) => edges.filter((edge) => edge.id !== id));
return (
<>
{/* 渲染连线 */}
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
{/* 渲染标签内容 */}
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
fontSize: 12,
pointerEvents: 'all',
}}
className="nodrag nopan"
>
<CloseSvg className="w-3.5 h-3.5" onClick={onEdgeClick} />
</div>
</EdgeLabelRenderer>
</>
);
};
export default DeleteableEdge;
流程图编辑器
通过ReactFlowProvider
提供React Flow
的上下文信息,同时使用自定义的StateContext.Provider
来传递节点、边以及相应的变化处理函数。在ReactFlow
组件中,配置了默认视口、节点、边、节点类型、边类型等属性,同时设置了各种回调函数来处理节点和边的变化、连接以及拖拽操作。
通过添加Controls
组件和Background
组件,实现了在编辑器中显示控件和自定义背景样式。
整体上,这段代码展示了如何利用React Flow
的组件和属性来构建一个交互式的流程图编辑器界面,实现了节点和连线的管理、拖放功能以及界面的定制化。
tsx
import { useModel } from '@umijs/max';
import {
addEdge,
Background,
Connection,
Controls,
Edge,
Node,
OnInit,
ReactFlow,
ReactFlowInstance,
ReactFlowProvider,
useEdgesState,
useNodesState,
} from '@xyflow/react';
import { DragEvent, useCallback, useRef, useState } from 'react';
import DeleteableEdge from './modules/DeleteableEdge';
import NodeCondition from './nodes/NodeCondition';
import NodeLLm from './nodes/NodeLLm';
import { defaultViewport } from '../utils/constants';
import { StateContext } from './StateContext';
import { NodeTypes } from '../types';
// 不同类型节点的映射关系
const nodeTypes: NodeTypes = {
llm: NodeLLm,
condition: NodeCondition,
};
// 边类型的映射关系
const edgeTypes = {
buttonedge: DeleteableEdge,
};
const FlowProvider = () => {
const reactFlowWrapper = useRef(null);
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance<
Node<any, string>,
Edge<any, string | undefined>
> | null>(null); // 保存react flow实例,操作flow各种功能。
const [nodes, setNodes, onNodesChange] = useNodesState<Node<any, string>>([]); // 管理节点
const [edges, setEdges, onEdgesChange] = useEdgesState<
Edge<any, string | undefined>
>([]); // 管理连线状态
const { preventScrolling } = useModel('flowModel');
const handleInit: OnInit<Node<any, string>, Edge<any, string | undefined>> = (
instance,
) => setReactFlowInstance(instance);
// 节点连接
const onConnect = useCallback((params: Edge | Connection) => {
// 检查source与target是否相同
if (params.source === params.target) return;
setEdges((eds) =>
addEdge({ ...params, animated: true, type: 'buttonedge' }, eds),
);
}, []);
// 处理拖放操作
const onDrop = useCallback(
(event: DragEvent) => {
event.preventDefault();
// 拖动事件中获取数据类型
const type = event.dataTransfer.getData('application/reactflow');
if (typeof type === 'undefined' || !type) return;
// 屏幕坐标转换为流程图中的坐标位置
const position = reactFlowInstance?.screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
if (!position) return;
// 创建一个新节点对象,并且保证id唯一(此处代码可优化)
const filteredArray = nodes.filter((item) => item.type === type);
const quantity = filteredArray.length
? Math.max(
...filteredArray.map((item) => parseInt(item.id.split('-')[0])),
) + 1
: 0;
const id = `${type}-${quantity}`;
const title = `${type.toUpperCase()}-${quantity}`;
const newNode = { id, type, position, data: { title } };
setNodes((nds) => [...nds, newNode]);
},
[nodes, reactFlowInstance],
);
// 处理拖拽元素在目标区域上方移动时的操作
const onDragOver = useCallback(
(event: {
preventDefault: () => void;
dataTransfer: { dropEffect: string };
}) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
},
[],
);
return (
<ReactFlowProvider>
<StateContext.Provider
value={{
nodes,
setNodes,
onNodesChange,
edges,
setEdges,
onEdgesChange,
}}
>
<div className="flex flex-1" ref={reactFlowWrapper}>
<ReactFlow
defaultViewport={defaultViewport}
preventScrolling={preventScrolling}
deleteKeyCode={[]}
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onInit={handleInit}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onDrop={onDrop}
onDragOver={onDragOver}
>
<Controls position="bottom-right" />
<Background gap={14} size={2} color="rgba(0,0,0, .08)" />
</ReactFlow>
</div>
</StateContext.Provider>
</ReactFlowProvider>
);
};
export default FlowProvider;
完整代码可访问 github