Parcourir la source

feat(frontend): 添加trace执行流程可视化html模板生成功能

添加templateHtml.py用于生成trace可视化HTML文件,包含模板引擎和mock数据集成
添加templateData.py用于生成mock数据,支持trace列表、详情、消息和分支
添加trace_template.html作为可视化模板,支持节点交互和详情展示
添加需求文档说明功能设计和数据交互规范
更新.gitignore忽略mock_data目录
max_liu il y a 1 mois
Parent
commit
f9d58d4588

+ 2 - 0
.gitignore

@@ -62,3 +62,5 @@ output
 .trace_test/
 .trace_test/
 .trace_test2/
 .trace_test2/
 examples/**/output*/
 examples/**/output*/
+
+frontend/htmlTemplate/mock_data

+ 401 - 0
frontend/htmlTemplate/templateData.py

@@ -0,0 +1,401 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+templateData.py - 生成 Trace 可视化的 Mock 数据
+"""
+
+import json
+from datetime import datetime
+from typing import Dict, List, Any
+
+
+def generate_mock_trace_list() -> Dict[str, Any]:
+    """生成 Trace 列表的 Mock 数据"""
+    return {
+        "traces": [
+            {
+                "trace_id": "trace_001",
+                "mode": "agent",
+                "task": "实现用户登录功能",
+                "status": "completed",
+                "total_messages": 15,
+                "total_tokens": 3500,
+                "total_cost": 0.05,
+                "current_goal_id": "goal_005",
+                "created_at": "2024-01-15T10:30:00Z"
+            },
+            {
+                "trace_id": "trace_002",
+                "mode": "agent",
+                "task": "优化数据库查询性能",
+                "status": "running",
+                "total_messages": 8,
+                "total_tokens": 2100,
+                "total_cost": 0.03,
+                "current_goal_id": "goal_003",
+                "created_at": "2024-01-15T11:00:00Z"
+            },
+            {
+                "trace_id": "trace_003",
+                "mode": "call",
+                "task": "修复支付接口bug",
+                "status": "failed",
+                "total_messages": 5,
+                "total_tokens": 1200,
+                "total_cost": 0.02,
+                "current_goal_id": "goal_002",
+                "created_at": "2024-01-15T09:45:00Z"
+            }
+        ],
+        "total": 3
+    }
+
+
+def generate_mock_trace_detail(trace_id: str = "trace_001") -> Dict[str, Any]:
+    """生成 Trace 详情的 Mock 数据(包含 GoalTree)"""
+    return {
+        "trace_id": trace_id,
+        "mode": "agent",
+        "task": "实现用户登录功能",
+        "status": "completed",
+        "total_messages": 15,
+        "total_tokens": 3500,
+        "total_cost": 0.05,
+        "created_at": "2024-01-15T10:30:00Z",
+        "completed_at": "2024-01-15T11:45:00Z",
+        "goal_tree": {
+            "mission": "实现用户登录功能",
+            "current_id": "goal_005",
+            "goals": [
+                {
+                    "id": "goal_001",
+                    "parent_id": None,
+                    "branch_id": None,
+                    "type": "normal",
+                    "description": "分析登录功能需求",
+                    "reason": "需要先理解需求才能开始实现",
+                    "status": "completed",
+                    "summary": "已完成需求分析,确定需要实现用户名密码登录和第三方登录",
+                    "self_stats": {
+                        "message_count": 3,
+                        "total_tokens": 800,
+                        "total_cost": 0.01,
+                        "preview": "read × 2 → analyze"
+                    },
+                    "cumulative_stats": {
+                        "message_count": 15,
+                        "total_tokens": 3500,
+                        "total_cost": 0.05,
+                        "preview": "read × 5 → write × 3 → test × 2"
+                    }
+                },
+                {
+                    "id": "goal_002",
+                    "parent_id": "goal_001",
+                    "branch_id": None,
+                    "type": "normal",
+                    "description": "设计数据库表结构",
+                    "reason": "需要存储用户信息和登录凭证",
+                    "status": "completed",
+                    "summary": "已创建 users 表和 auth_tokens 表",
+                    "self_stats": {
+                        "message_count": 2,
+                        "total_tokens": 500,
+                        "total_cost": 0.008,
+                        "preview": "design → create"
+                    },
+                    "cumulative_stats": {
+                        "message_count": 12,
+                        "total_tokens": 2700,
+                        "total_cost": 0.04,
+                        "preview": "design → write × 3 → test × 2"
+                    }
+                },
+                {
+                    "id": "goal_003",
+                    "parent_id": "goal_002",
+                    "branch_id": None,
+                    "type": "explore_start",
+                    "description": "探索不同的认证方案",
+                    "reason": "需要选择最合适的认证方式",
+                    "status": "completed",
+                    "summary": "决定使用 JWT 作为认证方案",
+                    "self_stats": {
+                        "message_count": 4,
+                        "total_tokens": 900,
+                        "total_cost": 0.012,
+                        "preview": "research × 3 → compare"
+                    },
+                    "cumulative_stats": {
+                        "message_count": 10,
+                        "total_tokens": 2200,
+                        "total_cost": 0.032,
+                        "preview": "research × 3 → write × 2 → test"
+                    },
+                    "branch_ids": ["branch_001", "branch_002"]
+                },
+                {
+                    "id": "goal_004",
+                    "parent_id": "goal_003",
+                    "branch_id": None,
+                    "type": "explore_merge",
+                    "description": "整合认证方案",
+                    "reason": "需要将选定的方案整合到系统中",
+                    "status": "completed",
+                    "summary": "已完成 JWT 认证的集成",
+                    "self_stats": {
+                        "message_count": 3,
+                        "total_tokens": 700,
+                        "total_cost": 0.01,
+                        "preview": "integrate × 2 → test"
+                    },
+                    "cumulative_stats": {
+                        "message_count": 6,
+                        "total_tokens": 1300,
+                        "total_cost": 0.02,
+                        "preview": "integrate × 2 → test × 2"
+                    },
+                    "explore_start_id": "goal_003",
+                    "merge_summary": "JWT 方案性能最优,安全性高",
+                    "selected_branch": "branch_001"
+                },
+                {
+                    "id": "goal_005",
+                    "parent_id": "goal_004",
+                    "branch_id": None,
+                    "type": "normal",
+                    "description": "实现登录API接口",
+                    "reason": "需要提供登录的HTTP接口",
+                    "status": "completed",
+                    "summary": "已完成 /api/login 接口的实现和测试",
+                    "self_stats": {
+                        "message_count": 3,
+                        "total_tokens": 600,
+                        "total_cost": 0.009,
+                        "preview": "write × 2 → test"
+                    },
+                    "cumulative_stats": {
+                        "message_count": 3,
+                        "total_tokens": 600,
+                        "total_cost": 0.009,
+                        "preview": "write × 2 → test"
+                    }
+                }
+            ]
+        },
+        "branches": {
+            "branch_001": {
+                "id": "branch_001",
+                "explore_start_id": "goal_003",
+                "description": "JWT 认证方案",
+                "status": "completed",
+                "summary": "JWT 方案实现完成,性能测试通过",
+                "cumulative_stats": {
+                    "message_count": 5,
+                    "total_tokens": 1100,
+                    "total_cost": 0.015,
+                    "preview": "implement × 3 → test × 2"
+                },
+                "goal_count": 3,
+                "last_message": {
+                    "message_id": "msg_015",
+                    "role": "assistant",
+                    "content": "JWT 认证方案实现完成"
+                }
+            },
+            "branch_002": {
+                "id": "branch_002",
+                "explore_start_id": "goal_003",
+                "description": "Session 认证方案",
+                "status": "abandoned",
+                "summary": "Session 方案在分布式环境下存在问题,已放弃",
+                "cumulative_stats": {
+                    "message_count": 3,
+                    "total_tokens": 600,
+                    "total_cost": 0.008,
+                    "preview": "implement × 2 → test"
+                },
+                "goal_count": 2,
+                "last_message": {
+                    "message_id": "msg_010",
+                    "role": "assistant",
+                    "content": "Session 方案不适合当前架构"
+                }
+            }
+        }
+    }
+
+
+def generate_mock_messages(trace_id: str = "trace_001", goal_id: str = "goal_001") -> Dict[str, Any]:
+    """生成 Messages 的 Mock 数据"""
+    return {
+        "messages": [
+            {
+                "message_id": "msg_001",
+                "role": "assistant",
+                "sequence": 1,
+                "goal_id": goal_id,
+                "branch_id": None,
+                "description": "开始分析需求文档",
+                "content": {
+                    "text": "我将开始分析登录功能的需求文档",
+                    "tool_calls": [
+                        {
+                            "id": "call_001",
+                            "name": "read_file",
+                            "arguments": {
+                                "path": "docs/requirements.md"
+                            }
+                        }
+                    ]
+                },
+                "tokens": 150,
+                "cost": 0.002,
+                "created_at": "2024-01-15T10:30:05Z"
+            },
+            {
+                "message_id": "msg_002",
+                "role": "tool",
+                "sequence": 2,
+                "goal_id": goal_id,
+                "branch_id": None,
+                "tool_call_id": "call_001",
+                "tokens": 200,
+                "cost": 0.003,
+                "created_at": "2024-01-15T10:30:06Z"
+            },
+            {
+                "message_id": "msg_003",
+                "role": "assistant",
+                "sequence": 3,
+                "goal_id": goal_id,
+                "branch_id": None,
+                "description": "分析需求并总结",
+                "content": {
+                    "text": "根据需求文档,需要实现以下功能:\n1. 用户名密码登录\n2. 第三方登录(微信、支付宝)\n3. 记住登录状态\n4. 登录失败重试限制",
+                    "tool_calls": []
+                },
+                "tokens": 180,
+                "cost": 0.0025,
+                "created_at": "2024-01-15T10:30:10Z"
+            }
+        ],
+        "total": 3
+    }
+
+
+def generate_mock_branch_detail(trace_id: str = "trace_001", branch_id: str = "branch_001") -> Dict[str, Any]:
+    """生成分支详情的 Mock 数据"""
+    return {
+        "id": branch_id,
+        "explore_start_id": "goal_003",
+        "description": "JWT 认证方案",
+        "status": "completed",
+        "summary": "JWT 方案实现完成,性能测试通过",
+        "goal_tree": {
+            "mission": "实现 JWT 认证",
+            "current_id": "branch_goal_003",
+            "goals": [
+                {
+                    "id": "branch_goal_001",
+                    "parent_id": None,
+                    "branch_id": branch_id,
+                    "type": "normal",
+                    "description": "研究 JWT 原理",
+                    "reason": "需要理解 JWT 的工作机制",
+                    "status": "completed",
+                    "summary": "已完成 JWT 原理学习",
+                    "self_stats": {
+                        "message_count": 2,
+                        "total_tokens": 400,
+                        "total_cost": 0.005,
+                        "preview": "research × 2"
+                    },
+                    "cumulative_stats": {
+                        "message_count": 5,
+                        "total_tokens": 1100,
+                        "total_cost": 0.015,
+                        "preview": "research × 2 → implement × 3"
+                    }
+                },
+                {
+                    "id": "branch_goal_002",
+                    "parent_id": "branch_goal_001",
+                    "branch_id": branch_id,
+                    "type": "normal",
+                    "description": "实现 JWT 生成和验证",
+                    "reason": "需要实现核心功能",
+                    "status": "completed",
+                    "summary": "已完成 JWT 的生成和验证逻辑",
+                    "self_stats": {
+                        "message_count": 2,
+                        "total_tokens": 500,
+                        "total_cost": 0.007,
+                        "preview": "implement × 2"
+                    },
+                    "cumulative_stats": {
+                        "message_count": 3,
+                        "total_tokens": 700,
+                        "total_cost": 0.01,
+                        "preview": "implement × 2 → test"
+                    }
+                },
+                {
+                    "id": "branch_goal_003",
+                    "parent_id": "branch_goal_002",
+                    "branch_id": branch_id,
+                    "type": "normal",
+                    "description": "测试 JWT 性能",
+                    "reason": "需要验证性能是否满足要求",
+                    "status": "completed",
+                    "summary": "性能测试通过,QPS 达到 5000+",
+                    "self_stats": {
+                        "message_count": 1,
+                        "total_tokens": 200,
+                        "total_cost": 0.003,
+                        "preview": "test"
+                    },
+                    "cumulative_stats": {
+                        "message_count": 1,
+                        "total_tokens": 200,
+                        "total_cost": 0.003,
+                        "preview": "test"
+                    }
+                }
+            ]
+        },
+        "cumulative_stats": {
+            "message_count": 5,
+            "total_tokens": 1100,
+            "total_cost": 0.015,
+            "preview": "research × 2 → implement × 2 → test"
+        }
+    }
+
+
+def save_mock_data_to_file():
+    """将 Mock 数据保存到文件"""
+    import os
+
+    # 创建 mock_data 目录
+    mock_dir = os.path.join(os.path.dirname(__file__), "mock_data")
+    os.makedirs(mock_dir, exist_ok=True)
+
+    # 保存各类 Mock 数据
+    with open(os.path.join(mock_dir, "trace_list.json"), "w", encoding="utf-8") as f:
+        json.dump(generate_mock_trace_list(), f, ensure_ascii=False, indent=2)
+
+    with open(os.path.join(mock_dir, "trace_detail.json"), "w", encoding="utf-8") as f:
+        json.dump(generate_mock_trace_detail(), f, ensure_ascii=False, indent=2)
+
+    with open(os.path.join(mock_dir, "messages.json"), "w", encoding="utf-8") as f:
+        json.dump(generate_mock_messages(), f, ensure_ascii=False, indent=2)
+
+    with open(os.path.join(mock_dir, "branch_detail.json"), "w", encoding="utf-8") as f:
+        json.dump(generate_mock_branch_detail(), f, ensure_ascii=False, indent=2)
+
+    print(f"Mock 数据已保存到: {mock_dir}")
+
+
+if __name__ == "__main__":
+    save_mock_data_to_file()

+ 57 - 0
frontend/htmlTemplate/templateHtml.py

@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+templateHtml.py - 生成 Trace 执行流程可视化 HTML
+参考 visualize.py 的样式设计
+"""
+
+import json
+import os
+from pathlib import Path
+
+
+def generate_trace_visualization_html(output_path: str = None):
+    """
+    生成 Trace 可视化 HTML 文件
+
+    Args:
+        output_path: 输出的 HTML 文件路径
+    """
+    # 读取 mock 数据
+    mock_dir = Path(__file__).parent / "mock_data"
+
+    with open(mock_dir / "trace_list.json", "r", encoding="utf-8") as f:
+        trace_list_data = json.load(f)
+
+    with open(mock_dir / "trace_detail.json", "r", encoding="utf-8") as f:
+        trace_detail_data = json.load(f)
+
+    # 读取 HTML 模板
+    template_path = Path(__file__).parent / "trace_template.html"
+    with open(template_path, "r", encoding="utf-8") as f:
+        template_content = f.read()
+
+    html_content = template_content.replace(
+        '"__TRACE_LIST_DATA__"',
+        json.dumps(trace_list_data, ensure_ascii=False)
+    ).replace(
+        '"__TRACE_DETAIL_DATA__"',
+        json.dumps(trace_detail_data, ensure_ascii=False)
+    )
+
+    # 确定输出路径
+    if output_path is None:
+        output_path = Path(__file__).parent / "trace_visualization.html"
+    else:
+        output_path = Path(output_path)
+
+    # 写入 HTML 文件
+    with open(output_path, 'w', encoding='utf-8') as f:
+        f.write(html_content)
+
+    print(f"可视化文件已生成: {output_path}")
+    return output_path
+
+
+if __name__ == "__main__":
+    generate_trace_visualization_html()

+ 822 - 0
frontend/htmlTemplate/trace_template.html

@@ -0,0 +1,822 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1.0"
+    />
+    <title>Trace 执行流程可视化</title>
+    <script src="https://d3js.org/d3.v7.min.js"></script>
+    <style>
+      body {
+        margin: 0;
+        padding: 20px;
+        font-family: "Microsoft YaHei", Arial, sans-serif;
+        background: #f5f5f5;
+      }
+      .main-container {
+        display: flex;
+        gap: 20px;
+        height: calc(100vh - 40px);
+      }
+      .container {
+        flex: 1;
+        background: white;
+        border-radius: 8px;
+        padding: 20px;
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+        overflow: hidden;
+        display: flex;
+        flex-direction: column;
+      }
+
+      /* 顶部导航栏 */
+      .top-nav {
+        margin-bottom: 20px;
+        padding: 15px;
+        background: #f8f9fa;
+        border-radius: 8px;
+        border: 1px solid #dee2e6;
+      }
+      .top-nav h1 {
+        margin: 0 0 15px 0;
+        color: #333;
+        font-size: 24px;
+      }
+      .filter-section {
+        display: flex;
+        gap: 15px;
+        align-items: center;
+        flex-wrap: wrap;
+      }
+      .filter-item {
+        display: flex;
+        align-items: center;
+        gap: 8px;
+      }
+      .filter-item label {
+        font-weight: bold;
+        color: #333;
+        font-size: 14px;
+      }
+      .filter-item select,
+      .filter-item button {
+        padding: 8px 12px;
+        font-size: 14px;
+        border: 1px solid #ced4da;
+        border-radius: 4px;
+        background: white;
+        cursor: pointer;
+      }
+      .filter-item select:hover,
+      .filter-item button:hover {
+        border-color: #4e79a7;
+      }
+      .filter-item button {
+        background: #4e79a7;
+        color: white;
+        border-color: #4e79a7;
+      }
+      .filter-item button:hover {
+        background: #356391;
+      }
+
+      /* 主体内容区域 */
+      #chart {
+        flex: 1;
+        min-height: 0;
+        position: relative;
+      }
+      svg {
+        width: 100%;
+        height: 100%;
+        border: 1px solid #ddd;
+        border-radius: 4px;
+        background: #fff;
+      }
+
+      /* 节点样式 */
+      .node {
+        cursor: pointer;
+      }
+      .node rect {
+        fill: transparent;
+        stroke: none;
+      }
+      .node.selected rect {
+        stroke: #ff6b6b;
+        stroke-width: 2px;
+        stroke-dasharray: 5, 5;
+      }
+      .node text {
+        font-size: 14px;
+        fill: #000;
+        text-anchor: middle;
+        dominant-baseline: middle;
+        pointer-events: none;
+      }
+      .node.trace_root text {
+        font-size: 16px;
+        font-weight: bold;
+      }
+
+      /* 连线样式 */
+      .link {
+        fill: none;
+        stroke: #5ba85f;
+        stroke-width: 2px;
+        cursor: pointer;
+      }
+      .link.highlighted {
+        stroke: #ff6b6b;
+        stroke-width: 3px;
+      }
+      .link-text {
+        font-size: 12px;
+        fill: #5ba85f;
+        font-weight: bold;
+        text-anchor: middle;
+        pointer-events: auto;
+        cursor: pointer;
+        text-shadow:
+          -2px -2px 0 #fff,
+          2px -2px 0 #fff,
+          -2px 2px 0 #fff,
+          2px 2px 0 #fff;
+      }
+
+      /* 右侧详情面板 */
+      .resizer {
+        width: 8px;
+        background: #e0e0e0;
+        cursor: col-resize;
+        flex-shrink: 0;
+        position: relative;
+        transition: background 0.2s;
+      }
+      .resizer:hover {
+        background: #4e79a7;
+      }
+      .resizer::before {
+        content: "";
+        position: absolute;
+        left: 50%;
+        top: 0;
+        bottom: 0;
+        width: 2px;
+        background: #999;
+        transform: translateX(-50%);
+      }
+      .resizer:hover::before {
+        background: #4e79a7;
+      }
+      .detail-panel {
+        width: 400px;
+        min-width: 250px;
+        max-width: 60%;
+        background: white;
+        border-radius: 8px;
+        padding: 20px;
+        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+        overflow-y: auto;
+        max-height: calc(100vh - 40px);
+        flex-shrink: 0;
+        transition:
+          width 0.2s ease,
+          padding 0.2s ease;
+      }
+      .detail-panel.collapsed {
+        width: 32px;
+        min-width: 32px;
+        max-width: 32px;
+        padding: 8px 4px;
+        overflow: hidden;
+      }
+      .detail-panel.collapsed #detail-content {
+        display: none;
+      }
+      .detail-panel.collapsed h2 {
+        margin: 0;
+        padding: 0;
+        border-bottom: none;
+        writing-mode: vertical-rl;
+        text-orientation: mixed;
+        font-size: 12px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+      .detail-panel h2 {
+        margin-top: 0;
+        margin-bottom: 15px;
+        color: #333;
+        font-size: 18px;
+        border-bottom: 2px solid #4e79a7;
+        padding-bottom: 10px;
+        display: flex;
+        align-items: center;
+        gap: 8px;
+      }
+      .detail-toggle {
+        margin-left: auto;
+        padding: 2px 8px;
+        font-size: 12px;
+        border: 1px solid #4e79a7;
+        border-radius: 4px;
+        background: #f0f4f8;
+        color: #4e79a7;
+        cursor: pointer;
+      }
+      .detail-toggle:hover {
+        background: #e1e9f0;
+      }
+      .detail-content {
+        color: #666;
+        line-height: 1.6;
+      }
+      .detail-item {
+        margin-bottom: 20px;
+      }
+      .detail-item label {
+        font-weight: bold;
+        color: #333;
+        display: block;
+        margin-bottom: 8px;
+        font-size: 14px;
+      }
+      .detail-item-value {
+        color: #666;
+        word-break: break-word;
+        font-size: 14px;
+        line-height: 1.6;
+        padding: 10px;
+        background: #f8f9fa;
+        border-radius: 4px;
+      }
+      .detail-empty {
+        color: #999;
+        font-style: italic;
+        text-align: center;
+        padding: 40px 20px;
+      }
+
+      /* 统计信息样式 */
+      .stats-grid {
+        display: grid;
+        grid-template-columns: repeat(2, 1fr);
+        gap: 10px;
+        margin-top: 10px;
+      }
+      .stat-item {
+        padding: 8px;
+        background: #f0f4f8;
+        border-radius: 4px;
+        text-align: center;
+      }
+      .stat-label {
+        font-size: 12px;
+        color: #666;
+        margin-bottom: 4px;
+      }
+      .stat-value {
+        font-size: 16px;
+        font-weight: bold;
+        color: #4e79a7;
+      }
+
+      /* 消息列表样式 */
+      .message-list {
+        margin-top: 10px;
+      }
+      .message-item {
+        padding: 12px;
+        margin-bottom: 10px;
+        background: #f8f9fa;
+        border-radius: 4px;
+        border-left: 3px solid #4e79a7;
+      }
+      .message-item.assistant {
+        border-left-color: #4caf50;
+      }
+      .message-item.tool {
+        border-left-color: #ff9800;
+      }
+      .message-header {
+        display: flex;
+        justify-content: space-between;
+        margin-bottom: 8px;
+        font-size: 12px;
+        color: #666;
+      }
+      .message-role {
+        font-weight: bold;
+        color: #333;
+      }
+      .message-content {
+        font-size: 13px;
+        color: #666;
+        line-height: 1.5;
+      }
+      .tool-calls {
+        margin-top: 8px;
+        padding: 8px;
+        background: #fff;
+        border-radius: 4px;
+        font-size: 12px;
+      }
+      .tool-call-item {
+        margin-bottom: 4px;
+      }
+      .tool-call-name {
+        font-weight: bold;
+        color: #ff9800;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="main-container">
+      <div class="container">
+        <!-- 顶部导航栏 -->
+        <div class="top-nav">
+          <h1 id="task-title">Trace 执行流程可视化</h1>
+          <div class="filter-section">
+            <div class="filter-item">
+              <label for="status-filter">状态筛选:</label>
+              <select id="status-filter">
+                <option value="">全部</option>
+                <option value="running">运行中</option>
+                <option value="completed">已完成</option>
+                <option value="failed">失败</option>
+              </select>
+            </div>
+            <div class="filter-item">
+              <label for="trace-select">Trace 选择:</label>
+              <select id="trace-select">
+                <!-- 动态填充 -->
+              </select>
+            </div>
+            <div class="filter-item">
+              <button id="refresh-btn">刷新</button>
+            </div>
+          </div>
+        </div>
+
+        <!-- 主体内容区域 -->
+        <div id="chart"></div>
+      </div>
+
+      <!-- 拖拽调整器 -->
+      <div
+        class="resizer"
+        id="resizer"
+      ></div>
+
+      <!-- 右侧详情面板 -->
+      <div
+        class="detail-panel"
+        id="detail-panel"
+      >
+        <h2>
+          <span>详情信息</span>
+          <button
+            id="detail-toggle"
+            class="detail-toggle"
+          >
+            收起
+          </button>
+        </h2>
+        <div
+          id="detail-content"
+          class="detail-content"
+        >
+          <div class="detail-empty">点击节点或边查看详情</div>
+        </div>
+      </div>
+    </div>
+
+    <script>
+      const rawTraceListData = "__TRACE_LIST_DATA__";
+      const traceListData =
+        typeof rawTraceListData === "string" && rawTraceListData === "__TRACE_LIST_DATA__"
+          ? { traces: [] }
+          : rawTraceListData;
+
+      const rawTraceDetailData = "__TRACE_DETAIL_DATA__";
+      const traceDetailData =
+        typeof rawTraceDetailData === "string" && rawTraceDetailData === "__TRACE_DETAIL_DATA__"
+          ? { trace_id: "mock", goal_tree: { goals: [] } }
+          : rawTraceDetailData;
+
+      let currentTraceId = traceDetailData.trace_id;
+      let selectedNode = null;
+      let svg, g, zoom;
+
+      // 初始化
+      function init() {
+        // 更新标题
+        updateTitle();
+
+        // 渲染图表
+        renderGraph();
+
+        // 绑定事件
+        bindEvents();
+      }
+
+      // 更新标题
+      function updateTitle() {
+        const traces = Array.isArray(traceListData)
+          ? traceListData
+          : Array.isArray(traceListData.traces)
+            ? traceListData.traces
+            : traceListData.trace_id
+              ? [traceListData]
+              : [];
+
+        const trace = traces.find((t) => t.trace_id === currentTraceId);
+        if (trace) {
+          document.getElementById("task-title").textContent = trace.task;
+        }
+      }
+
+      // 渲染图表
+      function renderGraph() {
+        const chartDiv = document.getElementById("chart");
+        const width = chartDiv.offsetWidth || 1000;
+        const height = chartDiv.offsetHeight || 700;
+
+        // 清除之前的内容
+        d3.select("#chart").selectAll("svg").remove();
+
+        // 创建 SVG
+        svg = d3
+          .select("#chart")
+          .append("svg")
+          .attr("viewBox", `0 0 ${width} ${height}`)
+          .attr("preserveAspectRatio", "xMidYMid meet");
+
+        // 定义箭头
+        svg
+          .append("defs")
+          .append("marker")
+          .attr("id", "arrowhead")
+          .attr("viewBox", "0 -5 10 10")
+          .attr("refX", 10)
+          .attr("refY", 0)
+          .attr("markerWidth", 6)
+          .attr("markerHeight", 6)
+          .attr("orient", "auto")
+          .append("path")
+          .attr("d", "M0,-5L10,0L0,5")
+          .attr("fill", "#4e79a7");
+
+        g = svg.append("g");
+
+        const goals =
+          traceDetailData.goal_tree && Array.isArray(traceDetailData.goal_tree.goals)
+            ? traceDetailData.goal_tree.goals
+            : Array.isArray(traceDetailData.goals)
+              ? traceDetailData.goals
+              : [];
+        const nodeMap = new Map();
+
+        // 1. 创建虚拟根节点
+        const virtualRoot = {
+          id: "VIRTUAL_ROOT",
+          children: [],
+        };
+
+        // 2. 构建节点映射
+        goals.forEach((goal) => {
+          nodeMap.set(goal.id, {
+            ...goal,
+            children: [],
+          });
+        });
+
+        // 3. 构建树结构 - 还原 parent_id 关系,多根节点挂载到虚拟根节点
+        goals.forEach((goal) => {
+          const node = nodeMap.get(goal.id);
+          if (goal.parent_id === null) {
+            // 根节点(parent_id 为 null),挂载到虚拟根节点
+            virtualRoot.children.push(node);
+          } else {
+            // 子节点,挂载到对应的父节点
+            const parent = nodeMap.get(goal.parent_id);
+            if (parent) {
+              parent.children.push(node);
+            } else {
+              // 如果找不到父节点,作为根节点处理(容错)
+              console.warn(`Parent node ${goal.parent_id} not found for goal ${goal.id}`);
+              virtualRoot.children.push(node);
+            }
+          }
+        });
+
+        // 创建层次结构 - 从虚拟根节点开始
+        const root = d3.hierarchy(virtualRoot);
+
+        // 创建树布局
+        // 注意:由于隐藏了第一层(虚拟根节点),实际显示的节点从第二层开始
+        // 我们需要调整布局大小,或者在绘制时进行坐标偏移
+        const treeLayout = d3
+          .tree()
+          .size([height - 100, width - 200])
+          .separation((a, b) => (a.parent == b.parent ? 1.2 : 1.5)); // 增加节点间距
+
+        treeLayout(root);
+
+        // 过滤掉虚拟根节点及其连接的边
+        const nodesData = root.descendants().filter((d) => d.depth > 0);
+        const linksData = root.links().filter((d) => d.source.depth > 0);
+
+        // 绘制连线
+        const linkGroups = g.selectAll(".link-group").data(linksData).enter().append("g").attr("class", "link-group");
+
+        linkGroups
+          .append("path")
+          .attr("class", "link")
+          .attr("d", (d) => {
+            // 调整坐标:减去虚拟根节点带来的层级偏移
+            // 假设每一层级大约占用的宽度,这里我们简单地减去一定偏移量让其靠左
+            // 但更好的方式是依赖 fitToView 自动调整
+            const sourceX = d.source.y + 100 + 80;
+            const sourceY = d.source.x + 50;
+            const targetX = d.target.y + 100 - 80;
+            const targetY = d.target.x + 50;
+            return `M${sourceX},${sourceY} C${(sourceX + targetX) / 2},${sourceY} ${(sourceX + targetX) / 2},${targetY} ${targetX},${targetY}`;
+          })
+          .attr("marker-end", "url(#arrowhead)")
+          .on("click", function (event, d) {
+            event.stopPropagation();
+            showEdgeDetail(d);
+          });
+
+        // 添加连线文字
+        linkGroups
+          .append("text")
+          .attr("class", "link-text")
+          .attr("x", (d) => {
+            const sourceX = d.source.y + 100 + 80;
+            const targetX = d.target.y + 100 - 80;
+            return (sourceX + targetX) / 2;
+          })
+          .attr("y", (d) => {
+            const sourceY = d.source.x + 50;
+            const targetY = d.target.x + 50;
+            return (sourceY + targetY) / 2 - 5;
+          })
+          .text((d) => d.target.data.edgeLabel || "");
+
+        // 绘制节点
+        const nodes = g
+          .selectAll(".node")
+          .data(nodesData)
+          .enter()
+          .append("g")
+          .attr("class", (d) => `node ${d.data.status} ${d.data.type}`)
+          .attr("transform", (d) => `translate(${d.y + 100},${d.x + 50})`)
+          .on("click", function (event, d) {
+            event.stopPropagation();
+            selectNode(d);
+          });
+
+        nodes.append("rect").attr("x", -80).attr("y", -30).attr("width", 160).attr("height", 60);
+
+        nodes
+          .append("text")
+          .attr("dy", 5)
+          .text((d) => d.data.description.substring(0, 10) + (d.data.description.length > 10 ? "..." : ""));
+
+        // 添加缩放功能
+        zoom = d3
+          .zoom()
+          .scaleExtent([0.1, 5])
+          .on("zoom", function (event) {
+            g.attr("transform", event.transform);
+          });
+
+        svg.call(zoom);
+
+        // 自动缩放以适应屏幕
+        setTimeout(() => {
+          fitToView();
+        }, 0);
+      }
+
+      // 自动缩放以适应屏幕
+      function fitToView() {
+        if (!g || !svg) return;
+
+        const bounds = g.node().getBBox();
+        if (bounds.width === 0 || bounds.height === 0) return;
+
+        const chartDiv = document.getElementById("chart");
+        const width = chartDiv.offsetWidth;
+        const height = chartDiv.offsetHeight;
+
+        const scale = 0.85 / Math.max(bounds.width / width, bounds.height / height);
+        const clampedScale = Math.max(0.1, Math.min(3, scale));
+
+        const centerX = bounds.x + bounds.width / 2;
+        const centerY = bounds.y + bounds.height / 2;
+
+        svg.call(
+          zoom.transform,
+          d3.zoomIdentity
+            .translate(width / 2 - clampedScale * centerX, height / 2 - clampedScale * centerY)
+            .scale(clampedScale),
+        );
+      }
+
+      // 选择节点
+      function selectNode(d) {
+        // 移除之前的选中状态
+        d3.selectAll(".node").classed("selected", false);
+
+        // 添加选中状态
+        d3.select(event.currentTarget).classed("selected", true);
+
+        selectedNode = d;
+
+        // 高亮路径
+        highlightPath(d);
+
+        // 显示详情
+        showNodeDetail(d);
+      }
+
+      // 高亮路径
+      function highlightPath(d) {
+        // 移除之前的高亮
+        d3.selectAll(".link").classed("highlighted", false);
+
+        // 找到从根节点到当前节点的路径
+        const path = d.ancestors().reverse();
+
+        // 高亮路径上的连线
+        d3.selectAll(".link").each(function (linkData) {
+          const sourceInPath = path.includes(linkData.source);
+          const targetInPath = path.includes(linkData.target);
+          if (sourceInPath && targetInPath) {
+            d3.select(this).classed("highlighted", true);
+          }
+        });
+      }
+
+      // 显示节点详情
+      function showNodeDetail(d) {
+        const detailContent = document.getElementById("detail-content");
+        const goal = d.data;
+
+        let html = `
+                <div class="detail-item">
+                    <label>节点 ID:</label>
+                    <div class="detail-item-value">${goal.id}</div>
+                </div>
+                <div class="detail-item">
+                    <label>描述:</label>
+                    <div class="detail-item-value">${goal.description}</div>
+                </div>
+                <div class="detail-item">
+                    <label>创建理由:</label>
+                    <div class="detail-item-value">${goal.reason}</div>
+                </div>
+                <div class="detail-item">
+                    <label>状态:</label>
+                    <div class="detail-item-value">${goal.status}</div>
+                </div>
+            `;
+
+        if (goal.summary) {
+          html += `
+                    <div class="detail-item">
+                        <label>总结:</label>
+                        <div class="detail-item-value">${goal.summary}</div>
+                    </div>
+                `;
+        }
+
+        // 显示统计信息
+        if (goal.self_stats) {
+          html += `
+                    <div class="detail-item">
+                        <label>当前节点统计:</label>
+                        <div class="stats-grid">
+                            <div class="stat-item">
+                                <div class="stat-label">消息数</div>
+                                <div class="stat-value">${goal.self_stats.message_count}</div>
+                            </div>
+                            <div class="stat-item">
+                                <div class="stat-label">Token 数</div>
+                                <div class="stat-value">${goal.self_stats.total_tokens}</div>
+                            </div>
+                            <div class="stat-item">
+                                <div class="stat-label">成本</div>
+                                <div class="stat-value">$${goal.self_stats.total_cost.toFixed(3)}</div>
+                            </div>
+                            <div class="stat-item">
+                                <div class="stat-label">预览</div>
+                                <div class="stat-value" style="font-size: 12px;">${goal.self_stats.preview || "-"}</div>
+                            </div>
+                        </div>
+                    </div>
+                `;
+        }
+
+        if (goal.cumulative_stats) {
+          html += `
+                    <div class="detail-item">
+                        <label>累计统计:</label>
+                        <div class="stats-grid">
+                            <div class="stat-item">
+                                <div class="stat-label">消息数</div>
+                                <div class="stat-value">${goal.cumulative_stats.message_count}</div>
+                            </div>
+                            <div class="stat-item">
+                                <div class="stat-label">Token 数</div>
+                                <div class="stat-value">${goal.cumulative_stats.total_tokens}</div>
+                            </div>
+                            <div class="stat-item">
+                                <div class="stat-label">成本</div>
+                                <div class="stat-value">$${goal.cumulative_stats.total_cost.toFixed(3)}</div>
+                            </div>
+                            <div class="stat-item">
+                                <div class="stat-label">预览</div>
+                                <div class="stat-value" style="font-size: 12px;">${goal.cumulative_stats.preview || "-"}</div>
+                            </div>
+                        </div>
+                    </div>
+                `;
+        }
+
+        detailContent.innerHTML = html;
+      }
+
+      // 显示边详情
+      function showEdgeDetail(d) {
+        const detailContent = document.getElementById("detail-content");
+
+        const html = `
+                <div class="detail-item">
+                    <label>连线:</label>
+                    <div class="detail-item-value">${d.source.data.description} → ${d.target.data.description}</div>
+                </div>
+                <div class="detail-item">
+                    <label>目标节点:</label>
+                    <div class="detail-item-value">${d.target.data.description}</div>
+                </div>
+            `;
+
+        detailContent.innerHTML = html;
+      }
+
+      // 绑定事件
+      function bindEvents() {
+        // Trace 选择器
+        document.getElementById("trace-select").addEventListener("change", function () {
+          currentTraceId = this.value;
+          updateTitle();
+          renderGraph();
+        });
+
+        // 刷新按钮
+        document.getElementById("refresh-btn").addEventListener("click", function () {
+          renderGraph();
+        });
+
+        // 详情面板收起/展开
+        const detailToggle = document.getElementById("detail-toggle");
+        const detailPanel = document.getElementById("detail-panel");
+        let isDetailCollapsed = false;
+
+        detailToggle.addEventListener("click", function () {
+          isDetailCollapsed = !isDetailCollapsed;
+          if (isDetailCollapsed) {
+            detailPanel.classList.add("collapsed");
+            detailToggle.textContent = "展开";
+          } else {
+            detailPanel.classList.remove("collapsed");
+            detailToggle.textContent = "收起";
+          }
+          setTimeout(() => {
+            renderGraph();
+          }, 0);
+        });
+
+        // 窗口大小变化
+        let resizeTimer;
+        window.addEventListener("resize", function () {
+          clearTimeout(resizeTimer);
+          resizeTimer = setTimeout(function () {
+            renderGraph();
+          }, 250);
+        });
+      }
+
+      // 启动
+      init();
+    </script>
+  </body>
+</html>

Fichier diff supprimé car celui-ci est trop grand
+ 404 - 0
frontend/htmlTemplate/trace_visualization.html


+ 417 - 0
frontend/htmlTemplate/需求.md

@@ -0,0 +1,417 @@
+
+一、html需求详述
+新建templateHtml.py来生成html文件
+1.1 顶部导航栏
+
+**内容**:
+
+1. **标题区域**
+   - 显示当前选中 Trace 的 task 名称
+   - 可配置的图标和文字
+
+2. **筛选条件区域**
+   - 使用原生 HTML 表单元素(select、input 等)
+   - 筛选项包括但不限于:
+     - 状态筛选(running/completed/failed)
+     - Trace 选择下拉框
+     - 刷新按钮
+   - 筛选条件变化时触发数据重新加载
+
+
+1.2 主体内容区域 
+
+**功能**: 流程图可视化展示
+
+1.2.1 节点交互
+
+**交互行为**:
+
+- 点击节点时,在主体内容区域高亮显示从 root 到该节点的完整路径
+- 同时显示该节点最近的边的内容
+
+1.2.2 边交互
+
+**交互行为**:
+- 内容以层级结构展示,支持展开/收起
+
+1.3 右侧详情面板
+
+
+**显示模式**:
+
+1. **节点详情模式**
+   - 显示节点的基本信息
+   - 显示节点的元数据
+   - 显示相关的边信息
+
+
+
+二、数据交互规范
+新建templateData.py来获取接口数据
+2.1 HTTP 接口
+
+**Base URL**: `http://www.bai.com`
+
+2.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; // "agent" | "call"
+task: string; // 任务描述,用于顶部标题显示
+status: string; // "running" | "completed" | "failed"
+total_messages: number;
+total_tokens: number;
+total_cost: number;
+current_goal_id: string;
+created_at: string;
+}>;
+total: number;
+}
+\`\`\`
+
+**html使用**:
+
+- 顶部 title 显示 `trace.task`
+- 可根据 status 筛选正在运行/已完成的任务
+
+2.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;
+
+// GoalTree 数据 - 用于渲染流程节点
+goal_tree: {
+mission: string;
+current_id: string | null;
+goals: Array<{
+id: string; // Goal 唯一 ID
+parent_id: string | null; // 父节点 ID(用于构建层级)
+branch_id: string | null; // 所属分支(null=主线)
+type: string; // "normal" | "explore_start" | "explore_merge"
+description: string; // 目标描述
+reason: string; // 创建理由
+status: string; // "pending" | "in_progress" | "completed" | "abandoned"
+summary: string | null; // 完成时的总结
+
+      // 统计数据(用于边的显示)
+      self_stats: {
+        message_count: number;
+        total_tokens: number;
+        total_cost: number;
+        preview: string | null;  // 工具调用摘要,如 "read → edit × 2"
+      };
+      cumulative_stats: {
+        message_count: number;
+        total_tokens: number;
+        total_cost: number;
+        preview: string | null;
+      };
+
+      // 分支相关(仅 explore 类型)
+      branch_ids?: string[];  // explore_start 关联的分支
+      explore_start_id?: string;  // explore_merge 关联的 explore_start
+      merge_summary?: string;
+      selected_branch?: string;
+    }>;
+
+};
+
+// 分支元数据(用于分支节点显示)
+branches: Record<string, {
+id: string;
+explore_start_id: string;
+description: string;
+status: string;
+summary: string | null;
+cumulative_stats: {
+message_count: number;
+total_tokens: number;
+total_cost: number;
+preview: string | null;
+};
+goal_count: number;
+last_message: any;
+}>;
+}
+\`\`\`
+
+**html使用**:
+
+- **节点渲染**: 遍历 `goal_tree.goals` 数组,每个 Goal 对象渲染为一个流程节点
+- **节点层级**: 通过 `parent_id` 构建父子关系
+- **节点类型**: 根据 `type` 字段区分普通节点、分支开始、分支汇合
+- **节点状态**: 使用 `status` 字段设置节点样式(进行中/已完成/待处理)
+- **边的统计**: 使用 `self_stats` 或 `cumulative_stats` 显示边上的统计信息
+
+#### 5.1.3 获取 Messages 数据(渲染边的详细内容)
+
+**接口**: `GET /api/traces/{trace_id}/messages?goal_id={goal_id}`
+
+**用途**: 获取指定 Goal 关联的所有 Messages,用于渲染流程节点之间的边的详细执行内容
+
+**查询参数**:
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `goal_id` | string | 否 | 过滤指定 Goal 的 Messages |
+| `branch_id` | string | 否 | 过滤指定分支的所有 Messages |
+
+**响应数据**:
+\`\`\`typescript
+interface MessagesResponse {
+messages: Array<{
+message_id: string;
+role: string; // "assistant" | "tool"
+sequence: number; // 全局顺序
+goal_id: string; // 关联的 Goal ID
+branch_id: string | null;
+
+    // assistant 消息
+    description?: string;
+    content?: {
+      text: string;
+      tool_calls?: Array<{
+        id: string;
+        name: string;  // 工具名称
+        arguments: any;
+      }>;
+    };
+
+    // tool 消息
+    tool_call_id?: string;
+
+    // 统计信息
+    tokens: number | null;
+    cost: number | null;
+    created_at: string;
+
+}>;
+total: number;
+}
+\`\`\`
+
+**html使用**:
+
+- **边详情显示**: 点击边时,调用此接口获取该边对应的 Goal 的所有 Messages
+- **层级展示**: 将 Messages 按 sequence 排序,组织成时间线或对话形式
+- **工具调用**: 显示 assistant 消息中的 tool_calls 和对应的 tool 返回结果
+
+#### 5.1.4 获取分支详情(按需加载)
+
+**接口**: `GET /api/traces/{trace_id}/branches/{branch_id}`
+
+**用途**: 展开分支时,按需加载分支内的详细 GoalTree
+
+**响应数据**:
+\`\`\`typescript
+interface BranchDetailResponse {
+id: string;
+explore_start_id: string;
+description: string;
+status: string;
+summary: string | null;
+goal_tree: {
+mission: string;
+current_id: string | null;
+goals: Array<{/_ 同 5.1.2 的 Goal 结构 _/}>;
+};
+cumulative_stats: {
+message_count: number;
+total_tokens: number;
+total_cost: number;
+preview: string | null;
+};
+}
+\`\`\`
+
+### 5.2 WebSocket 实时通信
+
+**连接地址**: `ws://localhost:8000/api/traces/{trace_id}/watch?since_event_id=0`
+
+**查询参数**:
+| 参数 | 类型 | 默认值 | 说明 |
+|------|------|--------|------|
+| `since_event_id` | int | `0` | 从哪个事件 ID 开始。`0` = 补发所有历史 |
+
+**事件类型**:
+
+#### 1. connected(连接成功)
+
+连接后推送完整 GoalTree,前端据此初始化流程图。
+
+\`\`\`typescript
+interface ConnectedEvent {
+event: 'connected';
+trace_id: string;
+current_event_id: number;
+goal_tree: {/_ 同 5.1.2 _/};
+}
+\`\`\`
+
+#### 2. goal_added(新增节点)
+
+\`\`\`typescript
+interface GoalAddedEvent {
+event: 'goal_added';
+event_id: number;
+goal: {/_ Goal 对象 _/};
+parent_id: string | null;
+}
+\`\`\`
+
+**前端处理**: 插入新节点,重新生成 DAG
+
+#### 3. goal_updated(节点状态变化)
+
+\`\`\`typescript
+interface GoalUpdatedEvent {
+event: 'goal_updated';
+event_id: number;
+goal_id: string;
+updates: {
+status?: string;
+summary?: string;
+};
+affected_goals: Array<{
+goal_id: string;
+status?: string;
+summary?: string;
+cumulative_stats?: {/_ 统计数据 _/};
+}>;
+}
+\`\`\`
+
+**前端处理**: 更新节点状态和统计数据,更新边的显示
+
+#### 4. message_added(新消息)
+
+\`\`\`typescript
+interface MessageAddedEvent {
+event: 'message_added';
+event_id: number;
+message: {/_ Message 对象 _/};
+affected_goals: Array<{
+goal_id: string;
+self_stats?: {/_ 统计数据 _/};
+cumulative_stats?: {/_ 统计数据 _/};
+}>;
+affected_branches?: Array<{
+branch_id: string;
+explore_start_id: string;
+cumulative_stats: {/_ 统计数据 _/};
+}>;
+}
+\`\`\`
+
+**前端处理**: 更新相关节点和边的统计数据
+
+#### 5. trace_completed(任务完成)
+
+\`\`\`typescript
+interface TraceCompletedEvent {
+event: 'trace_completed';
+event_id: number;
+trace_id: string;
+total_messages: number;
+total_tokens: number;
+total_cost: number;
+}
+\`\`\`
+
+**前端处理**: 标记任务完成,关闭 WebSocket
+
+#### 6. branch_started(分支开始)
+
+\`\`\`typescript
+interface BranchStartedEvent {
+event: 'branch_started';
+event_id: number;
+explore_start_id: string;
+branch: {/_ Branch 对象 _/};
+}
+\`\`\`
+
+#### 7. branch_completed(分支完成)
+
+\`\`\`typescript
+interface BranchCompletedEvent {
+event: 'branch_completed';
+event_id: number;
+explore_start_id: string;
+branch_id: string;
+summary: string;
+cumulative_stats: {/_ 统计数据 _/};
+last_message: any;
+}
+\`\`\`
+
+### 5.3 数据加载流程
+
+**初始化流程**:
+
+1. 调用 `GET /api/traces?status=running&limit=20` 获取 trace 列表
+2. 顶部 title 显示 `trace.task`
+3. 调用 `GET /api/traces/{trace_id}` 获取 GoalTree 数据
+4. 根据 `goal_tree.goals` 渲染流程节点
+5. 建立 WebSocket 连接 `ws://localhost:8000/api/traces/{trace_id}/watch` 进行实时更新
+
+**节点交互流程**:
+
+1. 用户点击节点(Goal)
+2. 在主体内容区域高亮显示从 root 到该节点的路径
+3. 右侧详情面板显示节点的基本信息(description, reason, status, summary)
+4. 显示节点的统计信息(self_stats 和 cumulative_stats)
+
+**边交互流程**:
+
+1. 用户点击边(两个节点之间的连线)
+2. 确定边对应的 target Goal 的 goal_id
+3. 调用 `GET /api/traces/{trace_id}/messages?goal_id={goal_id}` 获取该 Goal 的 Messages
+4. 右侧详情面板显示 Messages 列表,按时间顺序展示
+5. 支持展开/收起每条消息的详细内容(tool_calls、返回结果等)
+
+
+三、交互细节
+
+3.1 节点点击交互流程
+
+1. 用户点击节点
+2. 系统计算从 root 到该节点的路径
+3. 主体内容区域高亮显示路径
+4. 右侧详情面板显示节点详情
+5. 如果有最近的边,同时显示边的内容
+
+3.2 边点击交互流程
+
+1. 用户点击边
+2. 系统获取边的完整内容(包括层级结构)
+3. 右侧详情面板切换到边详情模式
+4. 显示层级内容,默认展开第一层
+5. 用户可以点击展开/收起子层级

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff