基于 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

相关推荐
Monly2121 分钟前
Vue:Table合并行于列
前端·javascript·vue.js
No Silver Bullet22 分钟前
ReactNative进阶(五十九):存量 react-native 项目适配 HarmonyOS NEXT
react native·react.js·harmonyos
子非鱼9211 小时前
使用ES5和ES6求函数参数的和、解析URL Params为对象
前端·javascript·es6
爱学英语的程序员2 小时前
React 中常见的Hooks,安排!
前端·react.js·前端框架
zhanggongzichu2 小时前
零基础Vue入门6——Vue router
前端·javascript·vue.js·vue3·路由·vue router
江湖行骗老中医2 小时前
React Native 开发 安卓项目构建工具Gradle的配置和使用
android·react native·react.js
stark张宇2 小时前
Web - CSS3过渡与动画
前端·css·css3
ssrswk92 小时前
通过制作docker镜像的方式在阿里云部署前端后台服务
前端·阿里云·docker
qq_316837752 小时前
uniapp 打包apk 播放带透明通道的webm格式视频
java·前端·uni-app
qianshang2332 小时前
基于web前端对简书页眉的开发及登陆的跳转
前端·css