mcpField
R
reactflow-expert

reactflow-expert

github.com/curiositech开发257167 次安装2 天前更新
代码仓库

关于 reactflow-expert

用于使用 ReactFlow v12 构建工作流和 DAG 仪表盘的技能,涵盖自定义 Agent 节点、状态管理、自动布局及执行状态更新。

主要功能

  1. 01自定义 Agent 节点模式
  2. 02Zustand 状态管理
  3. 03ELKjs 图形自动布局
  4. 04WebSocket 执行状态更新
  5. 05ReactFlow 交互与 v12 指引

适用场景

  1. 01构建工作流可视化仪表盘
  2. 02在图节点中显示 Agent 执行状态
  3. 03为 DAG 编辑器添加自动定位

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/curiositech/some_claude_skills --skill reactflow-expert

安装后在 Claude Code 中输入/reactflow-expert确认已加载。

附带文件.claude-plugin/plugin.json

详细介绍

ReactFlow 专家

使用 ReactFlow v12 构建 DAG 可视化,具备自定义 Agent 节点、ELKjs 自动布局、Zustand 状态管理,以及实时执行状态更新。


适用场景

✅ 适用于:

  • 构建工作流/DAG 可视化仪表板
  • 创建用于显示 Agent 状态的自定义 ReactFlow 节点组件
  • 集成 ELKjs 自动布局,实现图形自动定位
  • 将 WebSocket 执行事件接入 ReactFlow 状态
  • 实现缩放、平移、选择和节点交互

❌ 不适用于:

  • 静态 Mermaid 图表(请使用 mermaid-graph-writer)
  • 通用 React 组件开发
  • 非图形类可视化(图表、表格)

架构

flowchart TD
  subgraph "State Layer"
    Z[Zustand Store] --> N[nodes + edges]
    Z --> U[updateNodeData]
    Z --> A[applyNodeChanges / applyEdgeChanges]
  end
  
  subgraph "Layout Layer"
    E[ELKjs] --> P[Compute positions]
    P --> Z
  end
  
  subgraph "Data Layer"
    WS[WebSocket] --> Z
    API[REST API] --> Z
  end
  
  subgraph "Render Layer"
    Z --> RF[ReactFlow component]
    RF --> CN[Custom AgentNode]
    RF --> CE[Custom edges]
    RF --> PA[Panel controls]
  end

核心模式(ReactFlow v12)

Zustand Store(复杂编辑器中优于 useNodesState 的推荐方案)
import { create } from 'zustand';
import { applyNodeChanges, applyEdgeChanges, type Node, type Edge } from '@xyflow/react';

interface DAGStore {
  nodes: Node[];
  edges: Edge[];
  onNodesChange: (changes: any) => void;
  onEdgesChange: (changes: any) => void;
  setNodes: (nodes: Node[]) => void;
  setEdges: (edges: Edge[]) => void;
  updateNodeData: (nodeId: string, data: Record<string, any>) => void;
}

const useDAGStore = create<DAGStore>((set, get) => ({
  nodes: [],
  edges: [],
  onNodesChange: (changes) => set({ nodes: applyNodeChanges(changes, get().nodes) }),
  onEdgesChange: (changes) => set({ edges: applyEdgeChanges(changes, get().edges) }),
  setNodes: (nodes) => set({ nodes }),
  setEdges: (edges) => set({ edges }),
  // CRITICAL: create NEW object to trigger ReactFlow re-render
  updateNodeData: (nodeId, data) => set({
    nodes: get().nodes.map((n) =>
      n.id === nodeId ? { ...n, data: { ...n.data, ...data } } : n
    ),
  }),
}));
自定义 Agent 节点
import { Handle, Position, type NodeProps } from '@xyflow/react';

const STATUS_COLORS = {
  pending: '#9CA3AF', scheduled: '#60A5FA', running: '#3B82F6',
  completed: '#10B981', failed: '#EF4444', retrying: '#F59E0B',
  paused: '#8B5CF6', skipped: '#D1D5DB', mutated: '#EAB308',
};

function AgentNode({ data }: NodeProps) {
  return (
    <div className={`agent-node status-${data.status}`}
         style={{ borderColor: STATUS_COLORS[data.status] }}>
      <Handle type="target" position={Position.Top} />
      <div className="node-header">
        <span className={`status-dot ${data.status}`} />
        <span>{data.role}</span>
      </div>
      {data.skills && (
        <div className="node-skills">
          {data.skills.map((s: string) => <span key={s} className="badge">{s}</span>)}
        </div>
      )}
      {data.status === 'completed' && data.output?.summary && (
        <div className="node-output">{data.output.summary.slice(0, 60)}...</div>
      )}
      {data.metrics?.cost_usd > 0 && (
        <div className="node-meta">${data.metrics.cost_usd.toFixed(3)}</div>
      )}
      <Handle type="source" position={Position.Bottom} />
    </div>
  );
}

// MUST define outside component (or useMemo) to avoid re-registration
const nodeTypes = { agentNode: AgentNode };
ELKjs 自动布局 Hook
import ELK from 'elkjs/lib/elk.bundled.js';
import { useCallback } from 'react';
import { useReactFlow } from '@xyflow/react';

const elk = new ELK();

export function useAutoLayout() {
  const { fitView } = useReactFlow();

  return useCallback(async (nodes: Node[], edges: Edge[], direction = 'DOWN') => {
    const isHorizontal = direction === 'RIGHT';
    const layouted = await elk.layout({
      id: 'root',
      layoutOptions: {
        'elk.algorithm': 'layered',
        'elk.direction': direction,
        'elk.spacing.nodeNode': '80',
        'elk.layered.spacing.nodeNodeBetweenLayers': '100',
        'elk.edgeRouting': 'ORTHOGONAL',
      },
      children: nodes.map((n) => ({
        ...n,
        targetPosition: isHorizontal ? 'left' : 'top',
        sourcePosition: isHorizontal ? 'right' : 'bottom',
        width: n.measured?.width ?? 220,
        height: n.measured?.height ?? 120,
      })),
      edges,
    });
    const result = layouted.children!.map((elkN) => ({
      ...nodes.find((n) => n.id === elkN.id)!,
      position: { x: elkN.x!, y: elkN.y! },
    }));
    window.requestAnimationFrame(() => fitView());
    return result;
  }, [fitView]);
}
仪表板组装
import { ReactFlow, ReactFlowProvider, Panel } from '@xyflow/react';
import '@xyflow/react/dist/style.css';

function DAGDashboard({ dagId }: { dagId: string }) {
  const { nodes, edges, onNodesChange, onEdgesChange } = useDAGStore();
  const layout = useAutoLayout();

  // WebSocket → Zustand (see websocket-streaming skill)
  useDAGStream(dagId);

  return (
    <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes}
      onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} fitView>
      <Panel position="top-right">
        <button onClick={() => layout(nodes, edges, 'DOWN')}>↓ Vertical</button>
        <button onClick={() => layout(nodes, edges, 'RIGHT')}>→ Horizontal</button>
      </Panel>
    </ReactFlow>
  );
}

export default function DAGPage({ dagId }: { dagId: string }) {
  return <ReactFlowProvider><DAGDashboard dagId={dagId} /></ReactFlowProvider>;
}

v12 注意事项

问题修复方法
在组件内部定义 nodeTypes → 导致无限重新渲染在组件外部定义,或使用 useMemo 包装
状态更新未触发重新渲染必须创建新的节点对象:{ ...node, data: { ...node.data, ...update } }
自定义节点中的 xPos/yPos → 未定义使用 positionAbsoluteX/positionAbsoluteY(v12 更名)
nodeInternals → 未定义使用 nodeLookup(v12 更名)
ELK 布局忽略节点尺寸显式传入 node.measured?.width 和 height
fitView 在 DOM 绘制前触发使用 requestAnimationFrame(() => fitView()) 包装
交互元素会拖动节点为输入框、按钮和选择框添加 className="nodrag"

反模式

使用 Canvas 渲染进行调试

错误:使用基于 Canvas 的库(GoJS),导致无法在开发工具中检查节点。 正确:ReactFlow 使用 SVG + HTML 渲染。每个节点都可以在 React DevTools 和 DOM 中检查。

每次状态更新都重新运行布局

错误:每当节点状态变化时都调用 ELK 布局(开销大,还会造成视觉抖动)。 正确:仅在拓扑结构变化(添加/删除节点或边)时重新布局。状态颜色变化只是数据更新,无需重新布局。

单体式节点组件

错误:使用一个庞大的节点组件处理所有节点类型。 正确:注册不同的节点类型:agentNode、humanGateNode、pluripotentNode。每种类型都是专注的 React 组件。

评论

支持 Markdown