基于 React Flow 的可视化工作流

背景

公司仿照Coze想要实现一套自己的工作流,那么什么是工作流?

工作流是一系列可执行指令的集合,用于实现业务逻辑或完成特定任务。它为应用/智能体的数据流动和任务处理提供了一个结构化框架。工作流的核心在于将大模型的强大能力与特定的业务逻辑相结合,通过系统化、流程化的方法来实现高效、可扩展的AI应用开发。

下面图中体现的为简易版工作流,节点中详细内容无法展示(涉及隐私)。

此外Coze的工作流模块已改版。

图中提供了节点选项列表和可视化画布,通过拖拽节点迅速搭建工作流。同时,支持在画布实时调试工作流。

节点选项

首先着手实现左侧节点选项列表,用于展示可供拖动的节点选项。代码中仅展示了两个节点选项:LLMCondition(作为示例)。用户可以从节点选项列表拖动这些节点选项到画布中进行操作。

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;

可视化画布

其次再实现右侧画布,需要考虑了以下几个点:

  1. 画布中信息的状态更新
  2. 节点实现
  3. 如何连接节点
  4. 自定义连线 (需要符合设计稿)
  5. 流程图编辑器

状态管理

使用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来创建节点之间的连接线,形成节点之间的逻辑关系或数据流向。

TargetHandleSourceHandle同理,仅需更改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

相关推荐
answerball37 分钟前
🔥 Vue3响应式源码深度解剖:从Proxy魔法到依赖收集,手把手教你造轮子!🚀
前端·响应式设计·响应式编程
Slow菜鸟2 小时前
ES5 vs ES6:JavaScript 演进之路
前端·javascript·es6
小冯的编程学习之路2 小时前
【前端基础】:HTML
前端·css·前端框架·html·postman
Jiaberrr3 小时前
Vue 3 中搭建菜单权限配置界面的详细指南
前端·javascript·vue.js·elementui
懒大王95273 小时前
uniapp+Vue3 组件之间的传值方法
前端·javascript·uni-app
烛阴4 小时前
秒懂 JSON:JavaScript JSON 方法详解,让你轻松驾驭数据交互!
前端·javascript
拉不动的猪4 小时前
刷刷题31(vue实际项目问题)
前端·javascript·面试
zeijiershuai4 小时前
Ajax-入门、axios请求方式、async、await、Vue生命周期
前端·javascript·ajax
恋猫de小郭4 小时前
Flutter 小技巧之通过 MediaQuery 优化 App 性能
android·前端·flutter
只会写Bug的程序员4 小时前
面试之《webpack从输入到输出经历了什么》
前端·面试·webpack