react.md 34 KB

流程图可视化系统需求文档(React 版本)

一、项目概述

基于 design.html 的现有实现,开发一个新的流程图可视化系统。支持节点和边的交互式展示,提供清晰的层级结构和详细信息面板。

设计原则:

  • 采用组件化架构,提高代码复用性
  • 清晰的目录结构,便于团队协作

二、技术栈

  • UI 框架: React 18+ / Vue 3+
  • 构建工具: Vite / Webpack
  • 数据可视化: D3.js 或 React+SVG
  • 数据通信: Axios (HTTP) + WebSocket API
  • 状态管理: Zustand / Pinia / Redux Toolkit
  • 样式方案: CSS Modules / Styled Components / SCSS

架构原则:

  • 组件化: 每个功能模块独立组件
  • 关注点分离: 组件、样式、API 请求必须分别在不同文件中实现
  • 类型安全: 推荐使用 TypeScript
  • 模块化: 清晰的文件组织结构

三、整体框架结构

3.1 布局组成

┌─────────────────────────────────────────────────────┐
│                    顶部导航栏                         │
├─────────────────────────────────┬───────────────────┤
│                                 │                   │
│                                 │                   │
│         主体内容区域              │    右侧详情面板     │
│        (流程图可视化)             │                   │
│                                 │                   │
│                                 │                   │
└─────────────────────────────────┴───────────────────┘

3.2 组件层级

  • App (根组件)
    • TopBar (顶部导航栏)
    • Title (标题)
    • FilterBar (筛选条件)
    • MainContent (主体内容区域)
    • FlowChart (流程图组件)
    • DetailPanel (右侧详情面板)
    • NodeDetail (节点详情)
    • EdgeDetail (边详情)

四、功能需求详述

4.1 顶部导航栏 (TopBar)

实现文件:

  • 组件: src/components/TopBar/TopBar.tsx
  • 样式: src/components/TopBar/TopBar.module.css
  • 类型: src/components/TopBar/types.ts

内容:

  1. 标题区域

    • 显示当前选中 Trace 的 task 名称
    • 可配置的图标和文字
  2. 筛选条件区域

    • 使用 UI 组件库的表单组件(Select、Input 等)
    • 筛选项包括但不限于:
      • 状态筛选(running/completed/failed)
      • Trace 选择下拉框
      • 刷新按钮
    • 筛选条件变化时触发数据重新加载

数据结构:

interface FilterConfig {
  status?: "running" | "completed" | "failed";
  traceId?: string;
}

4.2 主体内容区域 (MainContent)

实现文件:

  • 组件: src/components/MainContent/MainContent.tsx
  • 样式: src/components/MainContent/MainContent.module.css
  • 子组件: src/components/FlowChart/FlowChart.tsx

功能: 流程图可视化展示

4.2.1 节点交互

交互行为:

  • 点击节点时,在主体内容区域高亮显示从 root 到该节点的完整路径
  • 同时显示该节点最近的边的内容

视觉效果:

  • 路径上的节点和边高亮显示
  • 非路径部分半透明或灰化
  • 平滑的动画过渡

数据需求:

interface Node {
  id: string;
  label: string;
  type: string;
  parentId?: string;
  metadata: Record<string, any>;
}

interface PathInfo {
  nodes: Node[];
  edges: Edge[];
  nearestEdge?: Edge;
}

4.2.2 边交互

交互行为:

  • 点击边时,在右侧详情面板显示该边的所有内容
  • 内容以层级结构展示,支持展开/收起

数据需求:

interface Edge {
  id: string;
  source: string;
  target: string;
  label: string;
  content: EdgeContent;
}

interface EdgeContent {
  id: string;
  title: string;
  children?: EdgeContent[];
  data: Record<string, any>;
}

4.3 右侧详情面板 (DetailPanel)

实现文件:

  • 组件: src/components/DetailPanel/DetailPanel.tsx
  • 样式: src/components/DetailPanel/DetailPanel.module.css
  • 子组件:
    • src/components/DetailPanel/NodeDetail.tsx
    • src/components/DetailPanel/EdgeDetail.tsx

显示模式:

  1. 节点详情模式

    • 显示节点的基本信息
    • 显示节点的元数据
    • 显示相关的边信息
  2. 边详情模式

    • 显示边的层级内容
    • 使用树形或折叠面板组件展示层级结构
    • 支持展开/收起操作
    • 支持搜索和筛选

数据结构:

interface DetailPanelState {
  type: "node" | "edge" | null;
  data: Node | Edge | null;
}

五、数据交互规范

5.1 HTTP 接口

Base URL: http://43.106.118.91:8000

5.1.1 获取 Trace 列表

接口: GET /api/traces?status=running&limit=20

用途: 获取 trace_id 列表,顶部 title 显示选中 trace 的 task

查询参数: | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | status | string | 否 | 过滤状态:running / completed / failed | | mode | string | 否 | 过滤模式:call / agent | | limit | int | 否 | 返回数量(默认 50,最大 100)|

响应数据:

interface TraceListResponse {
  traces: Array<{
    trace_id: string;
    mode: string;
    task: string; // 任务描述,用于顶部标题显示
    status: string;
    total_messages: number;
    total_tokens: number;
    total_cost: number;
    current_goal_id: string;
    created_at: string;
  }>;
  total: number;
}

5.1.2 获取 GoalTree 数据(渲染流程节点)

接口: GET /api/traces/{trace_id}

用途: 获取完整的 GoalTree 数据,根据 goal_tree.goals 渲染流程节点

响应数据:

interface TraceDetailResponse {
  trace_id: string;
  mode: string;
  task: string;
  status: string;
  total_messages: number;
  total_tokens: number;
  total_cost: number;
  created_at: string;
  completed_at: string | null;

  goal_tree: {
    mission: string;
    current_id: string | null;
    goals: Array<Goal>;
  };

  branches: Record<string, BranchContext>;
}

5.1.3 获取 Messages 数据(渲染边的详细内容)

接口: GET /api/traces/{trace_id}/messages?goal_id={goal_id}

用途: 获取指定 Goal 关联的所有 Messages,用于渲染流程节点之间的边的详细执行内容

响应数据:

interface MessagesResponse {
  messages: Array<Message>;
  total: number;
}

5.2 WebSocket 实时通信

连接地址: ws://43.106.118.91:8000/api/traces/{trace_id}/watch?since_event_id=0

事件类型:

  • connected - 连接成功
  • goal_added - 新增节点
  • goal_updated - 节点状态变化
  • message_added - 新消息
  • trace_completed - 任务完成
  • branch_started - 分支开始
  • branch_completed - 分支完成

六、项目文件组织结构

6.1 整体目录结构(React 版本)

src/
├── components/              # 组件目录
│   ├── TopBar/
│   │   ├── TopBar.tsx      # 顶部导航栏组件
│   │   ├── TopBar.module.css  # 组件样式
│   │   └── types.ts        # 类型定义
│   ├── MainContent/
│   │   ├── MainContent.tsx
│   │   └── MainContent.module.css
│   ├── FlowChart/
│   │   ├── FlowChart.tsx
│   │   ├── FlowChart.module.css
│   │   └── hooks/
│   │       └── useFlowChart.ts
│   ├── DetailPanel/
│   │   ├── DetailPanel.tsx
│   │   ├── DetailPanel.module.css
│   │   ├── NodeDetail.tsx
│   │   └── EdgeDetail.tsx
│   └── common/             # 通用组件
│       ├── Loading.tsx
│       └── ErrorBoundary.tsx
│
├── api/                    # API 请求层(接口独立文件夹)
│   ├── client.ts          # HTTP 客户端配置
│   ├── websocket.ts       # WebSocket 连接管理
│   ├── traceApi.ts        # Trace 相关接口
│   ├── goalApi.ts         # Goal 相关接口
│   └── messageApi.ts      # Message 相关接口
│
├── styles/                 # 全局样式
│   ├── variables.css      # CSS 变量
│   ├── reset.css          # 样式重置
│   ├── global.css         # 全局样式
│   └── themes/            # 主题
│       ├── light.css
│       └── dark.css
│
├── hooks/                  # 自定义 Hooks
│   ├── useTrace.ts
│   ├── useWebSocket.ts
│   └── useTheme.ts
│
├── store/                  # 状态管理
│   ├── index.ts
│   ├── traceStore.ts
│   └── uiStore.ts
│
├── utils/                  # 工具函数
│   ├── dagGenerator.ts
│   ├── pathCalculator.ts
│   └── formatters.ts
│
├── types/                  # TypeScript 类型定义
│   ├── trace.ts
│   ├── goal.ts
│   └── message.ts
│
├── App.tsx                 # 应用根组件
├── main.tsx                # 应用入口
└── vite-env.d.ts          # Vite 类型声明

6.2 关键文件说明

1. API 层(src/api/)

src/api/traceApi.ts - Trace 相关接口

import { client } from "./client";
import type { TraceListResponse, TraceDetailResponse } from "@/types/trace";

export const traceApi = {
  // 获取 Trace 列表
  async fetchTraces(params?: { status?: string; limit?: number }): Promise<TraceListResponse> {
    const response = await client.get("/api/traces", { params });
    return response.data;
  },

  // 获取 Trace 详情
  async fetchTraceDetail(traceId: string): Promise<TraceDetailResponse> {
    const response = await client.get(`/api/traces/${traceId}`);
    return response.data;
  },
};

src/api/messageApi.ts - Message 相关接口

import { client } from "./client";
import type { MessagesResponse } from "@/types/message";

export const messageApi = {
  // 获取 Goal 的 Messages
  async fetchMessages(traceId: string, goalId: string): Promise<MessagesResponse> {
    const response = await client.get(`/api/traces/${traceId}/messages`, { params: { goal_id: goalId } });
    return response.data;
  },
};

src/api/client.ts - HTTP 客户端配置

import axios from "axios";

export const client = axios.create({
  baseURL: "http://43.106.118.91:8000",
  timeout: 10000,
  headers: {
    "Content-Type": "application/json",
  },
});

// 请求拦截器
client.interceptors.request.use(
  (config) => {
    // 可以在这里添加 token 等
    return config;
  },
  (error) => {
    return Promise.reject(error);
  },
);

// 响应拦截器
client.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    console.error("API Error:", error);
    return Promise.reject(error);
  },
);

2. 组件层(src/components/)

src/components/TopBar/TopBar.tsx - 顶部导航栏组件

import React, { useEffect, useState } from 'react';
import { traceApi } from '@/api/traceApi';
import styles from './TopBar.module.css';

interface TopBarProps {
  onTraceSelect: (traceId: string) => void;
}

export const TopBar: React.FC<TopBarProps> = ({ onTraceSelect }) => {
  const [traces, setTraces] = useState([]);
  const [selectedTraceId, setSelectedTraceId] = useState('');
  const [title, setTitle] = useState('流程图可视化系统');

  useEffect(() => {
    loadTraces();
  }, []);

  const loadTraces = async () => {
    try {
      const data = await traceApi.fetchTraces({
        status: 'running',
        limit: 20
      });
      setTraces(data.traces);
    } catch (error) {
      console.error('Failed to load traces:', error);
    }
  };

  const handleTraceChange = (traceId: string) => {
    setSelectedTraceId(traceId);
    const trace = traces.find(t => t.trace_id === traceId);
    if (trace) {
      setTitle(trace.task);
      onTraceSelect(traceId);
    }
  };

  return (
    <header className={styles.topbar}>
      <div className={styles.title}>
        <h1>{title}</h1>
      </div>
      <div className={styles.filters}>
        <select
          value={selectedTraceId}
          onChange={(e) => handleTraceChange(e.target.value)}
          className={styles.select}
        >
          <option value="">选择 Trace</option>
          {traces.map(trace => (
            <option key={trace.trace_id} value={trace.trace_id}>
              {trace.task}
            </option>
          ))}
        </select>
        <button onClick={loadTraces} className={styles.button}>
          刷新
        </button>
      </div>
    </header>
  );
};

3. 自定义 Hooks(src/hooks/)

src/hooks/useTrace.ts - Trace 数据管理 Hook

import { useState, useEffect } from "react";
import { traceApi } from "@/api/traceApi";
import type { TraceDetailResponse } from "@/types/trace";

export const useTrace = (traceId: string | null) => {
  const [trace, setTrace] = useState<TraceDetailResponse | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    if (!traceId) return;

    const loadTrace = async () => {
      setLoading(true);
      setError(null);
      try {
        const data = await traceApi.fetchTraceDetail(traceId);
        setTrace(data);
      } catch (err) {
        setError(err as Error);
      } finally {
        setLoading(false);
      }
    };

    loadTrace();
  }, [traceId]);

  return { trace, loading, error };
};

src/hooks/useWebSocket.ts - WebSocket 连接 Hook

import { useEffect, useRef, useState } from "react";

interface UseWebSocketOptions {
  onMessage?: (data: any) => void;
  onError?: (error: Event) => void;
  onClose?: () => void;
}

export const useWebSocket = (traceId: string | null, options: UseWebSocketOptions = {}) => {
  const wsRef = useRef<WebSocket | null>(null);
  const [connected, setConnected] = useState(false);

  useEffect(() => {
    if (!traceId) return;

    const url = `ws://43.106.118.91:8000/api/traces/${traceId}/watch?since_event_id=0`;
    const ws = new WebSocket(url);

    ws.onopen = () => {
      console.log("WebSocket connected");
      setConnected(true);
    };

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      options.onMessage?.(data);
    };

    ws.onerror = (error) => {
      console.error("WebSocket error:", error);
      options.onError?.(error);
    };

    ws.onclose = () => {
      console.log("WebSocket closed");
      setConnected(false);
      options.onClose?.();
    };

    wsRef.current = ws;

    return () => {
      ws.close();
    };
  }, [traceId]);

  return { connected, ws: wsRef.current };
};

4. 应用入口(src/App.tsx)

import React, { useState } from 'react';
import { TopBar } from './components/TopBar/TopBar';
import { MainContent } from './components/MainContent/MainContent';
import { DetailPanel } from './components/DetailPanel/DetailPanel';
import { useTrace } from './hooks/useTrace';
import { useWebSocket } from './hooks/useWebSocket';
import './styles/global.css';

function App() {
  const [selectedTraceId, setSelectedTraceId] = useState<string | null>(null);
  const [selectedNode, setSelectedNode] = useState(null);
  const [selectedEdge, setSelectedEdge] = useState(null);

  const { trace, loading } = useTrace(selectedTraceId);

  useWebSocket(selectedTraceId, {
    onMessage: (data) => {
      console.log('WebSocket message:', data);
      // 处理实时更新
    }
  });

  return (
    <div className="app">
      <TopBar onTraceSelect={setSelectedTraceId} />
      <MainContent
        trace={trace}
        loading={loading}
        onNodeClick={setSelectedNode}
        onEdgeClick={setSelectedEdge}
      />
      {(selectedNode || selectedEdge) && (
        <DetailPanel
          node={selectedNode}
          edge={selectedEdge}
          onClose={() => {
            setSelectedNode(null);
            setSelectedEdge(null);
          }}
        />
      )}
    </div>
  );
}

export default App;

6.3 模块化原则

关注点分离:

  • ✅ API 请求逻辑 → src/api/ 目录
  • ✅ 组件 UI 结构 → src/components/ 目录
  • ✅ 样式定义 → src/styles/ 目录(全局)+ 组件内 .module.css
  • ✅ 主题配置 → src/styles/themes/ 目录
  • ✅ 业务逻辑 → src/hooks/src/store/ 目录
  • ✅ 类型定义 → src/types/ 目录

禁止做法:

  • ❌ 在组件文件中直接写 API 请求(应导入 api 层函数)
  • ❌ 在组件文件中写大量内联样式(应使用 CSS Modules)
  • ❌ 在样式文件中混入业务逻辑
  • ❌ 主题样式和组件样式混在一起

推荐做法:

  • ✅ 使用 TypeScript 提供类型安全
  • ✅ 使用自定义 Hooks 封装业务逻辑
  • ✅ 使用 CSS Modules 避免样式冲突
  • ✅ 使用状态管理库管理全局状态
  • ✅ API 请求统一通过 src/api/ 层调用

七、样式规范

7.1 CSS 变量

src/styles/variables.css

:root {
  /* 颜色 */
  --color-primary: #0070f3;
  --color-success: #00c853;
  --color-warning: #ff9800;
  --color-error: #f44336;

  /* 背景色 */
  --bg-primary: #ffffff;
  --bg-secondary: #f5f5f5;

  /* 文字颜色 */
  --text-primary: #333333;
  --text-secondary: #666666;

  /* 间距 */
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;

  /* 字体 */
  --font-size-sm: 12px;
  --font-size-md: 14px;
  --font-size-lg: 16px;
}

7.2 组件样式

使用 CSS Modules 避免样式冲突:

src/components/TopBar/TopBar.module.css

.topbar {
  height: 60px;
  background: var(--bg-primary);
  border-bottom: 1px solid var(--border-color);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--spacing-lg);
}

.title h1 {
  font-size: var(--font-size-lg);
  font-weight: 600;
  color: var(--text-primary);
  margin: 0;
}

.filters {
  display: flex;
  gap: var(--spacing-md);
}

八、开发优先级

P0 (核心功能)

  • 基础框架搭建(TopBar + MainContent + DetailPanel)
  • 流程图基础渲染
  • 节点点击交互
  • HTTP 数据获取

P1 (重要功能)

  • 边点击交互
  • 层级内容展开/收起
  • 筛选功能
  • WebSocket 实时更新

P2 (优化功能)

  • 动画效果优化
  • 性能优化
  • 主题切换
  • 响应式布局

九、技术实现建议

9.1 状态管理

使用 Zustand 进行状态管理:

import create from "zustand";

interface AppState {
  currentTraceId: string | null;
  trace: TraceDetailResponse | null;
  selectedNode: Node | null;
  selectedEdge: Edge | null;

  setCurrentTraceId: (id: string) => void;
  setTrace: (trace: TraceDetailResponse) => void;
  setSelectedNode: (node: Node | null) => void;
  setSelectedEdge: (edge: Edge | null) => void;
}

export const useAppStore = create<AppState>((set) => ({
  currentTraceId: null,
  trace: null,
  selectedNode: null,
  selectedEdge: null,

  setCurrentTraceId: (id) => set({ currentTraceId: id }),
  setTrace: (trace) => set({ trace }),
  setSelectedNode: (node) => set({ selectedNode: node }),
  setSelectedEdge: (edge) => set({ selectedEdge: edge }),
}));

9.2 性能优化

  1. React.memo: 对不经常变化的组件使用 memo
  2. useMemo/useCallback: 缓存计算结果和回调函数
  3. 虚拟滚动: 大量节点时使用虚拟滚动
  4. 懒加载: 按需加载组件和数据

十、测试要点

  1. 单元测试: 使用 Vitest 测试组件和工具函数
  2. 集成测试: 测试组件间的交互
  3. E2E 测试: 使用 Playwright 测试完整流程
  4. 性能测试: 测试大数据量下的渲染性能

十一、部署说明

  1. 使用 Vite 构建生产版本:npm run build
  2. 配置环境变量(API 地址、WebSocket 地址等)
  3. 部署到静态服务器或 CDN
  4. 配置 WebSocket 代理(如果需要)

十二、FlowChart 核心实现指南

12.1 渲染方案选择:React + D3 Hybrid

推荐方案: 使用 React 控制 SVG 渲染,D3.js 仅用于布局计算

技术架构:

  • D3.js: 仅用于数据处理和布局计算(d3.tree(), d3.hierarchy()
  • React: 完全控制 SVG DOM 渲染和更新
  • 优势:
    • 避免 D3 直接操作 DOM 与 React 虚拟 DOM 冲突
    • 充分利用 React 的状态管理和组件化
    • 更易于维护和测试
    • 完美支持 WebSocket 实时更新

12.2 FlowChart 组件实现

src/components/FlowChart/FlowChart.tsx

import React, { useMemo, useRef, useEffect, useState } from 'react';
import * as d3 from 'd3';
import styles from './FlowChart.module.css';
import type { Goal } from '@/types/goal';

interface FlowChartProps {
  goals: Goal[];
  onNodeClick?: (node: Goal) => void;
  onEdgeClick?: (edge: Edge) => void;
}

interface LayoutNode extends d3.HierarchyPointNode<Goal> {
  x: number;
  y: number;
}

export const FlowChart: React.FC<FlowChartProps> = ({
  goals,
  onNodeClick,
  onEdgeClick
}) => {
  const svgRef = useRef<SVGSVGElement>(null);
  const [dimensions, setDimensions] = useState({ width: 1200, height: 800 });
  const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);

  // 使用 D3 计算布局
  const layoutData = useMemo(() => {
    if (!goals || goals.length === 0) return null;

    // 构建层级数据结构
    const root = buildHierarchy(goals);

    // 使用 d3.tree() 计算布局
    const treeLayout = d3.tree<Goal>()
      .size([dimensions.height - 100, dimensions.width - 200])
      .separation((a, b) => (a.parent === b.parent ? 1.2 : 1.5));

    const treeData = treeLayout(root);

    return {
      nodes: treeData.descendants() as LayoutNode[],
      links: treeData.links()
    };
  }, [goals, dimensions]);

  // 处理节点点击
  const handleNodeClick = (node: LayoutNode) => {
    setSelectedNodeId(node.data.id);
    onNodeClick?.(node.data);
  };

  // 处理边点击
  const handleEdgeClick = (link: any) => {
    const edge = {
      id: `${link.source.data.id}-${link.target.data.id}`,
      source: link.source.data.id,
      target: link.target.data.id,
      data: link.target.data
    };
    onEdgeClick?.(edge);
  };

  if (!layoutData) return <div>Loading...</div>;

  return (
    <div className={styles.container}>
      <svg
        ref={svgRef}
        width={dimensions.width}
        height={dimensions.height}
        className={styles.svg}
      >
        {/* 定义箭头标记 */}
        <defs>
          <ArrowMarkers />
        </defs>

        {/* 渲染连接线(边) */}
        <g className={styles.links}>
          {layoutData.links.map((link, index) => (
            <Edge
              key={index}
              link={link}
              selected={selectedNodeId === link.target.data.id}
              onClick={() => handleEdgeClick(link)}
            />
          ))}
        </g>

        {/* 渲染节点 */}
        <g className={styles.nodes}>
          {layoutData.nodes.map((node) => (
            <Node
              key={node.data.id}
              node={node}
              selected={selectedNodeId === node.data.id}
              onClick={() => handleNodeClick(node)}
            />
          ))}
        </g>
      </svg>
    </div>
  );
};

// 构建层级数据结构
function buildHierarchy(goals: Goal[]): d3.HierarchyNode<Goal> {
  const goalsMap = new Map(goals.map(g => [g.id, g]));
  const root = goals.find(g => !g.parent_id);

  if (!root) throw new Error('No root goal found');

  function buildTree(goal: Goal): any {
    const children = goals
      .filter(g => g.parent_id === goal.id)
      .map(child => buildTree(child));

    return {
      ...goal,
      children: children.length > 0 ? children : undefined
    };
  }

  return d3.hierarchy(buildTree(root));
}

12.3 箭头样式实现(基于 design.png)

src/components/FlowChart/ArrowMarkers.tsx

import React from 'react';

export const ArrowMarkers: React.FC = () => {
  return (
    <>
      {/* 默认箭头 - 蓝色 */}
      <marker
        id="arrow-default"
        viewBox="0 -5 10 10"
        refX={10}
        refY={0}
        markerWidth={6}
        markerHeight={6}
        orient="auto"
      >
        <path
          d="M0,-5L10,0L0,5"
          fill="#4e79a7"
          stroke="none"
        />
      </marker>

      {/* 成功状态箭头 - 绿色 */}
      <marker
        id="arrow-success"
        viewBox="0 -5 10 10"
        refX={10}
        refY={0}
        markerWidth={6}
        markerHeight={6}
        orient="auto"
      >
        <path
          d="M0,-5L10,0L0,5"
          fill="#00c853"
          stroke="none"
        />
      </marker>

      {/* 失败状态箭头 - 红色 */}
      <marker
        id="arrow-error"
        viewBox="0 -5 10 10"
        refX={10}
        refY={0}
        markerWidth={6}
        markerHeight={6}
        orient="auto"
      >
        <path
          d="M0,-5L10,0L0,5"
          fill="#f44336"
          stroke="none"
        />
      </marker>

      {/* 运行中状态箭头 - 橙色 */}
      <marker
        id="arrow-running"
        viewBox="0 -5 10 10"
        refX={10}
        refY={0}
        markerWidth={6}
        markerHeight={6}
        orient="auto"
      >
        <path
          d="M0,-5L10,0L0,5"
          fill="#ff9800"
          stroke="none"
        />
      </marker>

      {/* 选中状态箭头 - 高亮蓝色 */}
      <marker
        id="arrow-selected"
        viewBox="0 -5 10 10"
        refX={10}
        refY={0}
        markerWidth={8}
        markerHeight={8}
        orient="auto"
      >
        <path
          d="M0,-5L10,0L0,5"
          fill="#0070f3"
          stroke="none"
        />
      </marker>
    </>
  );
};

12.4 边(Edge)组件实现 - Bezier 曲线

src/components/FlowChart/Edge.tsx

import React from 'react';
import styles from './Edge.module.css';

interface EdgeProps {
  link: any;
  selected: boolean;
  onClick: () => void;
}

export const Edge: React.FC<EdgeProps> = ({ link, selected, onClick }) => {
  const { source, target } = link;

  // 计算 Bezier 曲线路径(垂直布局)
  const createPath = () => {
    const sourceX = source.y;
    const sourceY = source.x;
    const targetX = target.y;
    const targetY = target.x;

    // 使用三次 Bezier 曲线创建平滑连接
    const midY = (sourceY + targetY) / 2;

    return `M${sourceX},${sourceY} C${sourceX},${midY} ${targetX},${midY} ${targetX},${targetY}`;
  };

  // 根据目标节点状态选择箭头样式
  const getMarkerUrl = () => {
    if (selected) return 'url(#arrow-selected)';

    const status = target.data.status;
    switch (status) {
      case 'completed':
        return 'url(#arrow-success)';
      case 'failed':
        return 'url(#arrow-error)';
      case 'running':
        return 'url(#arrow-running)';
      default:
        return 'url(#arrow-default)';
    }
  };

  // 根据状态选择线条颜色
  const getStrokeColor = () => {
    if (selected) return '#0070f3';

    const status = target.data.status;
    switch (status) {
      case 'completed':
        return '#00c853';
      case 'failed':
        return '#f44336';
      case 'running':
        return '#ff9800';
      default:
        return '#4e79a7';
    }
  };

  return (
    <g className={styles.edge}>
      {/* 透明的宽路径用于点击检测 */}
      <path
        d={createPath()}
        fill="none"
        stroke="transparent"
        strokeWidth={20}
        onClick={onClick}
        style={{ cursor: 'pointer' }}
      />

      {/* 实际显示的路径 */}
      <path
        d={createPath()}
        fill="none"
        stroke={getStrokeColor()}
        strokeWidth={selected ? 3 : 2}
        markerEnd={getMarkerUrl()}
        className={selected ? styles.selected : ''}
        style={{
          transition: 'all 0.3s ease',
          opacity: selected ? 1 : 0.7
        }}
      />
    </g>
  );
};

12.5 节点(Node)组件实现

src/components/FlowChart/Node.tsx

import React from 'react';
import styles from './Node.module.css';

interface NodeProps {
  node: any;
  selected: boolean;
  onClick: () => void;
}

export const Node: React.FC<NodeProps> = ({ node, selected, onClick }) => {
  const { x, y, data } = node;

  // 根据状态选择节点颜色
  const getNodeColor = () => {
    switch (data.status) {
      case 'completed':
        return '#e8f5e9';
      case 'failed':
        return '#ffebee';
      case 'running':
        return '#fff3e0';
      default:
        return '#e3f2fd';
    }
  };

  const getBorderColor = () => {
    if (selected) return '#0070f3';

    switch (data.status) {
      case 'completed':
        return '#00c853';
      case 'failed':
        return '#f44336';
      case 'running':
        return '#ff9800';
      default:
        return '#4e79a7';
    }
  };

  return (
    <g
      className={styles.node}
      transform={`translate(${y},${x})`}
      onClick={onClick}
      style={{ cursor: 'pointer' }}
    >
      {/* 节点背景 */}
      <rect
        x={-60}
        y={-25}
        width={120}
        height={50}
        rx={8}
        fill={getNodeColor()}
        stroke={getBorderColor()}
        strokeWidth={selected ? 3 : 2}
        className={selected ? styles.selected : ''}
        style={{
          transition: 'all 0.3s ease',
          filter: selected ? 'drop-shadow(0 4px 8px rgba(0,0,0,0.2))' : 'none'
        }}
      />

      {/* 节点文本 */}
      <text
        textAnchor="middle"
        dy=".35em"
        fontSize={14}
        fill="#333"
        fontWeight={selected ? 600 : 400}
      >
        {truncateText(data.description || data.id, 12)}
      </text>

      {/* 状态指示器 */}
      {data.status === 'running' && (
        <circle
          cx={50}
          cy={-15}
          r={4}
          fill="#ff9800"
          className={styles.pulse}
        />
      )}
    </g>
  );
};

function truncateText(text: string, maxLength: number): string {
  return text.length > maxLength ? text.slice(0, maxLength) + '...' : text;
}

12.6 WebSocket 实时更新实现

src/components/FlowChart/useFlowChartData.ts

import { useState, useEffect, useCallback } from "react";
import { useWebSocket } from "@/hooks/useWebSocket";
import type { Goal } from "@/types/goal";

export const useFlowChartData = (traceId: string | null) => {
  const [goals, setGoals] = useState<Goal[]>([]);

  // 处理 WebSocket 消息
  const handleWebSocketMessage = useCallback((event: any) => {
    const { type, data } = event;

    switch (type) {
      case "goal_added":
        // 新增节点 - 动态添加到图中
        setGoals((prev) => [...prev, data.goal]);
        break;

      case "goal_updated":
        // 更新节点状态 - 触发重新渲染
        setGoals((prev) => prev.map((g) => (g.id === data.goal.id ? { ...g, ...data.goal } : g)));
        break;

      case "message_added":
        // 新消息添加 - 可能需要更新边的数据
        // 这里可以触发边的高亮或动画效果
        console.log("New message:", data.message);
        break;

      case "trace_completed":
        // 任务完成 - 可以显示完成状态
        console.log("Trace completed");
        break;

      default:
        console.log("Unknown event type:", type);
    }
  }, []);

  // 建立 WebSocket 连接
  useWebSocket(traceId, {
    onMessage: handleWebSocketMessage,
  });

  return { goals, setGoals };
};

12.7 样式定义

src/components/FlowChart/FlowChart.module.css

.container {
  width: 100%;
  height: 100%;
  overflow: auto;
  background: #fafafa;
}

.svg {
  display: block;
  margin: 0 auto;
}

.links {
  pointer-events: none;
}

.links path {
  pointer-events: stroke;
}

.nodes {
  pointer-events: all;
}

/* 边的样式 */
.edge path {
  transition:
    stroke-width 0.3s ease,
    opacity 0.3s ease;
}

.edge.selected path {
  stroke-width: 3px;
  opacity: 1;
}

/* 节点的样式 */
.node rect {
  transition: all 0.3s ease;
}

.node.selected rect {
  filter: drop-shadow(0 4px 8px rgba(0, 112, 243, 0.3));
}

/* 运行中状态的脉冲动画 */
@keyframes pulse {
  0%,
  100% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.5;
    transform: scale(1.2);
  }
}

.pulse {
  animation: pulse 2s ease-in-out infinite;
}

12.8 完整使用示例

src/components/MainContent/MainContent.tsx

import React, { useEffect } from 'react';
import { FlowChart } from '../FlowChart/FlowChart';
import { useFlowChartData } from '../FlowChart/useFlowChartData';
import { traceApi } from '@/api/traceApi';
import styles from './MainContent.module.css';

interface MainContentProps {
  traceId: string | null;
  onNodeClick?: (node: any) => void;
  onEdgeClick?: (edge: any) => void;
}

export const MainContent: React.FC<MainContentProps> = ({
  traceId,
  onNodeClick,
  onEdgeClick
}) => {
  const { goals, setGoals } = useFlowChartData(traceId);

  // 初始加载数据
  useEffect(() => {
    if (!traceId) return;

    const loadInitialData = async () => {
      try {
        const trace = await traceApi.fetchTraceDetail(traceId);
        setGoals(trace.goal_tree.goals);
      } catch (error) {
        console.error('Failed to load trace data:', error);
      }
    };

    loadInitialData();
  }, [traceId]);

  if (!traceId) {
    return (
      <div className={styles.empty}>
        <p>请选择一个 Trace 查看流程图</p>
      </div>
    );
  }

  return (
    <div className={styles.container}>
      <FlowChart
        goals={goals}
        onNodeClick={onNodeClick}
        onEdgeClick={onEdgeClick}
      />
    </div>
  );
};

12.9 关键技术要点总结

1. React + D3 Hybrid 模式:

  • D3 仅用于布局计算(d3.tree(), d3.hierarchy()
  • React 完全控制 SVG 渲染
  • 避免 DOM 操作冲突

2. 实时更新策略:

  • WebSocket 接收事件 → 更新 React state
  • State 变化 → 触发 useMemo 重新计算布局
  • 布局变化 → React 自动重新渲染 SVG

3. 箭头样式设计:

  • 使用 SVG <marker> 定义箭头
  • 根据节点状态动态选择箭头颜色
  • Bezier 曲线创建平滑连接线
  • 支持选中状态的高亮效果

4. 性能优化:

  • 使用 useMemo 缓存布局计算结果
  • 使用 useCallback 避免不必要的重新渲染
  • 透明宽路径提升点击体验
  • CSS transition 实现平滑动画

5. 交互体验:

  • 节点/边点击高亮
  • 状态颜色编码(绿色=完成,红色=失败,橙色=运行中)
  • 运行中节点的脉冲动画
  • 选中状态的阴影效果