# 流程图可视化系统需求文档（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 选择下拉框
     - 刷新按钮
   - 筛选条件变化时触发数据重新加载

**数据结构**:

```typescript
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 到该节点的完整路径
- 同时显示该节点最近的边的内容

**视觉效果**:

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

**数据需求**:

```typescript
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 边交互

**交互行为**:

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

**数据需求**:

```typescript
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. **边详情模式**
   - 显示边的层级内容
   - 使用树形或折叠面板组件展示层级结构
   - 支持展开/收起操作
   - 支持搜索和筛选

**数据结构**:

```typescript
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）|

**响应数据**:

```typescript
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` 渲染流程节点

**响应数据**:

```typescript
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，用于渲染流程节点之间的边的详细执行内容

**响应数据**:

```typescript
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 相关接口

```typescript
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 相关接口

```typescript
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 客户端配置

```typescript
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` - 顶部导航栏组件

```typescript
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

```typescript
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

```typescript
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）**

```typescript
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`

```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`

```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 进行状态管理：

```typescript
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`

```typescript
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`

```typescript
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`

```typescript
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`

```typescript
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`

```typescript
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`

```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`

```typescript
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. 交互体验**:

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