trace_template.html 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. <!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta
  6. name="viewport"
  7. content="width=device-width, initial-scale=1.0"
  8. />
  9. <title>Trace 执行流程可视化</title>
  10. <script src="https://d3js.org/d3.v7.min.js"></script>
  11. <style>
  12. body {
  13. margin: 0;
  14. padding: 20px;
  15. font-family: "Microsoft YaHei", Arial, sans-serif;
  16. background: #f5f5f5;
  17. }
  18. .main-container {
  19. display: flex;
  20. gap: 20px;
  21. height: calc(100vh - 40px);
  22. }
  23. .container {
  24. flex: 1;
  25. background: white;
  26. border-radius: 8px;
  27. padding: 20px;
  28. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  29. overflow: hidden;
  30. display: flex;
  31. flex-direction: column;
  32. }
  33. /* 顶部导航栏 */
  34. .top-nav {
  35. margin-bottom: 20px;
  36. padding: 15px;
  37. background: #f8f9fa;
  38. border-radius: 8px;
  39. border: 1px solid #dee2e6;
  40. }
  41. .top-nav h1 {
  42. margin: 0 0 15px 0;
  43. color: #333;
  44. font-size: 24px;
  45. }
  46. .filter-section {
  47. display: flex;
  48. gap: 15px;
  49. align-items: center;
  50. flex-wrap: wrap;
  51. }
  52. .filter-item {
  53. display: flex;
  54. align-items: center;
  55. gap: 8px;
  56. }
  57. .filter-item label {
  58. font-weight: bold;
  59. color: #333;
  60. font-size: 14px;
  61. }
  62. .filter-item select,
  63. .filter-item button {
  64. padding: 8px 12px;
  65. font-size: 14px;
  66. border: 1px solid #ced4da;
  67. border-radius: 4px;
  68. background: white;
  69. cursor: pointer;
  70. }
  71. .filter-item select:hover,
  72. .filter-item button:hover {
  73. border-color: #4e79a7;
  74. }
  75. .filter-item button {
  76. background: #4e79a7;
  77. color: white;
  78. border-color: #4e79a7;
  79. }
  80. .filter-item button:hover {
  81. background: #356391;
  82. }
  83. /* 主体内容区域 */
  84. #chart {
  85. flex: 1;
  86. min-height: 0;
  87. position: relative;
  88. }
  89. svg {
  90. width: 100%;
  91. height: 100%;
  92. border: 1px solid #ddd;
  93. border-radius: 4px;
  94. background: #fff;
  95. }
  96. /* 节点样式 */
  97. .node {
  98. cursor: pointer;
  99. }
  100. .node rect {
  101. fill: transparent;
  102. stroke: none;
  103. }
  104. .node.selected rect {
  105. stroke: #ff6b6b;
  106. stroke-width: 2px;
  107. stroke-dasharray: 5, 5;
  108. }
  109. .node text {
  110. font-size: 14px;
  111. fill: #000;
  112. text-anchor: middle;
  113. dominant-baseline: middle;
  114. pointer-events: none;
  115. }
  116. .node.trace_root text {
  117. font-size: 16px;
  118. font-weight: bold;
  119. }
  120. /* 连线样式 */
  121. .link {
  122. fill: none;
  123. stroke: #5ba85f;
  124. stroke-width: 2px;
  125. cursor: pointer;
  126. }
  127. .link.highlighted {
  128. stroke: #ff6b6b;
  129. stroke-width: 3px;
  130. }
  131. .link-text {
  132. font-size: 12px;
  133. fill: #5ba85f;
  134. font-weight: bold;
  135. text-anchor: middle;
  136. pointer-events: auto;
  137. cursor: pointer;
  138. text-shadow:
  139. -2px -2px 0 #fff,
  140. 2px -2px 0 #fff,
  141. -2px 2px 0 #fff,
  142. 2px 2px 0 #fff;
  143. }
  144. /* 右侧详情面板 */
  145. .resizer {
  146. width: 8px;
  147. background: #e0e0e0;
  148. cursor: col-resize;
  149. flex-shrink: 0;
  150. position: relative;
  151. transition: background 0.2s;
  152. }
  153. .resizer:hover {
  154. background: #4e79a7;
  155. }
  156. .resizer::before {
  157. content: "";
  158. position: absolute;
  159. left: 50%;
  160. top: 0;
  161. bottom: 0;
  162. width: 2px;
  163. background: #999;
  164. transform: translateX(-50%);
  165. }
  166. .resizer:hover::before {
  167. background: #4e79a7;
  168. }
  169. .detail-panel {
  170. width: 400px;
  171. min-width: 250px;
  172. max-width: 60%;
  173. background: white;
  174. border-radius: 8px;
  175. padding: 20px;
  176. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  177. overflow-y: auto;
  178. max-height: calc(100vh - 40px);
  179. flex-shrink: 0;
  180. transition:
  181. width 0.2s ease,
  182. padding 0.2s ease;
  183. }
  184. .detail-panel.collapsed {
  185. width: 32px;
  186. min-width: 32px;
  187. max-width: 32px;
  188. padding: 8px 4px;
  189. overflow: hidden;
  190. }
  191. .detail-panel.collapsed #detail-content {
  192. display: none;
  193. }
  194. .detail-panel.collapsed h2 {
  195. margin: 0;
  196. padding: 0;
  197. border-bottom: none;
  198. writing-mode: vertical-rl;
  199. text-orientation: mixed;
  200. font-size: 12px;
  201. display: flex;
  202. align-items: center;
  203. justify-content: center;
  204. }
  205. .detail-panel h2 {
  206. margin-top: 0;
  207. margin-bottom: 15px;
  208. color: #333;
  209. font-size: 18px;
  210. border-bottom: 2px solid #4e79a7;
  211. padding-bottom: 10px;
  212. display: flex;
  213. align-items: center;
  214. gap: 8px;
  215. }
  216. .detail-toggle {
  217. margin-left: auto;
  218. padding: 2px 8px;
  219. font-size: 12px;
  220. border: 1px solid #4e79a7;
  221. border-radius: 4px;
  222. background: #f0f4f8;
  223. color: #4e79a7;
  224. cursor: pointer;
  225. }
  226. .detail-toggle:hover {
  227. background: #e1e9f0;
  228. }
  229. .detail-content {
  230. color: #666;
  231. line-height: 1.6;
  232. }
  233. .detail-item {
  234. margin-bottom: 20px;
  235. }
  236. .detail-item label {
  237. font-weight: bold;
  238. color: #333;
  239. display: block;
  240. margin-bottom: 8px;
  241. font-size: 14px;
  242. }
  243. .detail-item-value {
  244. color: #666;
  245. word-break: break-word;
  246. font-size: 14px;
  247. line-height: 1.6;
  248. padding: 10px;
  249. background: #f8f9fa;
  250. border-radius: 4px;
  251. }
  252. .detail-empty {
  253. color: #999;
  254. font-style: italic;
  255. text-align: center;
  256. padding: 40px 20px;
  257. }
  258. /* 统计信息样式 */
  259. .stats-grid {
  260. display: grid;
  261. grid-template-columns: repeat(2, 1fr);
  262. gap: 10px;
  263. margin-top: 10px;
  264. }
  265. .stat-item {
  266. padding: 8px;
  267. background: #f0f4f8;
  268. border-radius: 4px;
  269. text-align: center;
  270. }
  271. .stat-label {
  272. font-size: 12px;
  273. color: #666;
  274. margin-bottom: 4px;
  275. }
  276. .stat-value {
  277. font-size: 16px;
  278. font-weight: bold;
  279. color: #4e79a7;
  280. }
  281. /* 消息列表样式 */
  282. .message-list {
  283. margin-top: 10px;
  284. }
  285. .message-item {
  286. padding: 12px;
  287. margin-bottom: 10px;
  288. background: #f8f9fa;
  289. border-radius: 4px;
  290. border-left: 3px solid #4e79a7;
  291. }
  292. .message-item.assistant {
  293. border-left-color: #4caf50;
  294. }
  295. .message-item.tool {
  296. border-left-color: #ff9800;
  297. }
  298. .message-header {
  299. display: flex;
  300. justify-content: space-between;
  301. margin-bottom: 8px;
  302. font-size: 12px;
  303. color: #666;
  304. }
  305. .message-role {
  306. font-weight: bold;
  307. color: #333;
  308. }
  309. .message-content {
  310. font-size: 13px;
  311. color: #666;
  312. line-height: 1.5;
  313. }
  314. .tool-calls {
  315. margin-top: 8px;
  316. padding: 8px;
  317. background: #fff;
  318. border-radius: 4px;
  319. font-size: 12px;
  320. }
  321. .tool-call-item {
  322. margin-bottom: 4px;
  323. }
  324. .tool-call-name {
  325. font-weight: bold;
  326. color: #ff9800;
  327. }
  328. </style>
  329. </head>
  330. <body>
  331. <div class="main-container">
  332. <div class="container">
  333. <!-- 顶部导航栏 -->
  334. <div class="top-nav">
  335. <h1 id="task-title">Trace 执行流程可视化</h1>
  336. <div class="filter-section">
  337. <div class="filter-item">
  338. <label for="status-filter">状态筛选:</label>
  339. <select id="status-filter">
  340. <option value="">全部</option>
  341. <option value="running">运行中</option>
  342. <option value="completed">已完成</option>
  343. <option value="failed">失败</option>
  344. </select>
  345. </div>
  346. <div class="filter-item">
  347. <label for="trace-select">Trace 选择:</label>
  348. <select id="trace-select">
  349. <!-- 动态填充 -->
  350. </select>
  351. </div>
  352. <div class="filter-item">
  353. <button id="refresh-btn">刷新</button>
  354. </div>
  355. </div>
  356. </div>
  357. <!-- 主体内容区域 -->
  358. <div id="chart"></div>
  359. </div>
  360. <!-- 拖拽调整器 -->
  361. <div
  362. class="resizer"
  363. id="resizer"
  364. ></div>
  365. <!-- 右侧详情面板 -->
  366. <div
  367. class="detail-panel"
  368. id="detail-panel"
  369. >
  370. <h2>
  371. <span>详情信息</span>
  372. <button
  373. id="detail-toggle"
  374. class="detail-toggle"
  375. >
  376. 收起
  377. </button>
  378. </h2>
  379. <div
  380. id="detail-content"
  381. class="detail-content"
  382. >
  383. <div class="detail-empty">点击节点或边查看详情</div>
  384. </div>
  385. </div>
  386. </div>
  387. <script>
  388. const rawTraceListData = "__TRACE_LIST_DATA__";
  389. const traceListData =
  390. typeof rawTraceListData === "string" && rawTraceListData === "__TRACE_LIST_DATA__"
  391. ? { traces: [] }
  392. : rawTraceListData;
  393. const rawTraceDetailData = "__TRACE_DETAIL_DATA__";
  394. const traceDetailData =
  395. typeof rawTraceDetailData === "string" && rawTraceDetailData === "__TRACE_DETAIL_DATA__"
  396. ? { trace_id: "mock", goal_tree: { goals: [] } }
  397. : rawTraceDetailData;
  398. let currentTraceId = traceDetailData.trace_id;
  399. let selectedNode = null;
  400. let svg, g, zoom;
  401. // 初始化
  402. function init() {
  403. // 更新标题
  404. updateTitle();
  405. // 渲染图表
  406. renderGraph();
  407. // 绑定事件
  408. bindEvents();
  409. }
  410. // 更新标题
  411. function updateTitle() {
  412. const traces = Array.isArray(traceListData)
  413. ? traceListData
  414. : Array.isArray(traceListData.traces)
  415. ? traceListData.traces
  416. : traceListData.trace_id
  417. ? [traceListData]
  418. : [];
  419. const trace = traces.find((t) => t.trace_id === currentTraceId);
  420. if (trace) {
  421. document.getElementById("task-title").textContent = trace.task;
  422. }
  423. }
  424. // 渲染图表
  425. function renderGraph() {
  426. const chartDiv = document.getElementById("chart");
  427. const width = chartDiv.offsetWidth || 1000;
  428. const height = chartDiv.offsetHeight || 700;
  429. // 清除之前的内容
  430. d3.select("#chart").selectAll("svg").remove();
  431. // 创建 SVG
  432. svg = d3
  433. .select("#chart")
  434. .append("svg")
  435. .attr("viewBox", `0 0 ${width} ${height}`)
  436. .attr("preserveAspectRatio", "xMidYMid meet");
  437. // 定义箭头
  438. svg
  439. .append("defs")
  440. .append("marker")
  441. .attr("id", "arrowhead")
  442. .attr("viewBox", "0 -5 10 10")
  443. .attr("refX", 10)
  444. .attr("refY", 0)
  445. .attr("markerWidth", 6)
  446. .attr("markerHeight", 6)
  447. .attr("orient", "auto")
  448. .append("path")
  449. .attr("d", "M0,-5L10,0L0,5")
  450. .attr("fill", "#4e79a7");
  451. g = svg.append("g");
  452. const goals =
  453. traceDetailData.goal_tree && Array.isArray(traceDetailData.goal_tree.goals)
  454. ? traceDetailData.goal_tree.goals
  455. : Array.isArray(traceDetailData.goals)
  456. ? traceDetailData.goals
  457. : [];
  458. const nodeMap = new Map();
  459. // 1. 创建虚拟根节点
  460. const virtualRoot = {
  461. id: "VIRTUAL_ROOT",
  462. children: [],
  463. };
  464. // 2. 构建节点映射
  465. goals.forEach((goal) => {
  466. nodeMap.set(goal.id, {
  467. ...goal,
  468. children: [],
  469. });
  470. });
  471. // 3. 构建树结构 - 还原 parent_id 关系,多根节点挂载到虚拟根节点
  472. goals.forEach((goal) => {
  473. const node = nodeMap.get(goal.id);
  474. if (goal.parent_id === null) {
  475. // 根节点(parent_id 为 null),挂载到虚拟根节点
  476. virtualRoot.children.push(node);
  477. } else {
  478. // 子节点,挂载到对应的父节点
  479. const parent = nodeMap.get(goal.parent_id);
  480. if (parent) {
  481. parent.children.push(node);
  482. } else {
  483. // 如果找不到父节点,作为根节点处理(容错)
  484. console.warn(`Parent node ${goal.parent_id} not found for goal ${goal.id}`);
  485. virtualRoot.children.push(node);
  486. }
  487. }
  488. });
  489. // 创建层次结构 - 从虚拟根节点开始
  490. const root = d3.hierarchy(virtualRoot);
  491. // 创建树布局
  492. // 注意:由于隐藏了第一层(虚拟根节点),实际显示的节点从第二层开始
  493. // 我们需要调整布局大小,或者在绘制时进行坐标偏移
  494. const treeLayout = d3
  495. .tree()
  496. .size([height - 100, width - 200])
  497. .separation((a, b) => (a.parent == b.parent ? 1.2 : 1.5)); // 增加节点间距
  498. treeLayout(root);
  499. // 过滤掉虚拟根节点及其连接的边
  500. const nodesData = root.descendants().filter((d) => d.depth > 0);
  501. const linksData = root.links().filter((d) => d.source.depth > 0);
  502. // 绘制连线
  503. const linkGroups = g.selectAll(".link-group").data(linksData).enter().append("g").attr("class", "link-group");
  504. linkGroups
  505. .append("path")
  506. .attr("class", "link")
  507. .attr("d", (d) => {
  508. // 调整坐标:减去虚拟根节点带来的层级偏移
  509. // 假设每一层级大约占用的宽度,这里我们简单地减去一定偏移量让其靠左
  510. // 但更好的方式是依赖 fitToView 自动调整
  511. const sourceX = d.source.y + 100 + 80;
  512. const sourceY = d.source.x + 50;
  513. const targetX = d.target.y + 100 - 80;
  514. const targetY = d.target.x + 50;
  515. return `M${sourceX},${sourceY} C${(sourceX + targetX) / 2},${sourceY} ${(sourceX + targetX) / 2},${targetY} ${targetX},${targetY}`;
  516. })
  517. .attr("marker-end", "url(#arrowhead)")
  518. .on("click", function (event, d) {
  519. event.stopPropagation();
  520. showEdgeDetail(d);
  521. });
  522. // 添加连线文字
  523. linkGroups
  524. .append("text")
  525. .attr("class", "link-text")
  526. .attr("x", (d) => {
  527. const sourceX = d.source.y + 100 + 80;
  528. const targetX = d.target.y + 100 - 80;
  529. return (sourceX + targetX) / 2;
  530. })
  531. .attr("y", (d) => {
  532. const sourceY = d.source.x + 50;
  533. const targetY = d.target.x + 50;
  534. return (sourceY + targetY) / 2 - 5;
  535. })
  536. .text((d) => d.target.data.edgeLabel || "");
  537. // 绘制节点
  538. const nodes = g
  539. .selectAll(".node")
  540. .data(nodesData)
  541. .enter()
  542. .append("g")
  543. .attr("class", (d) => `node ${d.data.status} ${d.data.type}`)
  544. .attr("transform", (d) => `translate(${d.y + 100},${d.x + 50})`)
  545. .on("click", function (event, d) {
  546. event.stopPropagation();
  547. selectNode(d);
  548. });
  549. nodes.append("rect").attr("x", -80).attr("y", -30).attr("width", 160).attr("height", 60);
  550. nodes
  551. .append("text")
  552. .attr("dy", 5)
  553. .text((d) => d.data.description.substring(0, 10) + (d.data.description.length > 10 ? "..." : ""));
  554. // 添加缩放功能
  555. zoom = d3
  556. .zoom()
  557. .scaleExtent([0.1, 5])
  558. .on("zoom", function (event) {
  559. g.attr("transform", event.transform);
  560. });
  561. svg.call(zoom);
  562. // 自动缩放以适应屏幕
  563. setTimeout(() => {
  564. fitToView();
  565. }, 0);
  566. }
  567. // 自动缩放以适应屏幕
  568. function fitToView() {
  569. if (!g || !svg) return;
  570. const bounds = g.node().getBBox();
  571. if (bounds.width === 0 || bounds.height === 0) return;
  572. const chartDiv = document.getElementById("chart");
  573. const width = chartDiv.offsetWidth;
  574. const height = chartDiv.offsetHeight;
  575. const scale = 0.85 / Math.max(bounds.width / width, bounds.height / height);
  576. const clampedScale = Math.max(0.1, Math.min(3, scale));
  577. const centerX = bounds.x + bounds.width / 2;
  578. const centerY = bounds.y + bounds.height / 2;
  579. svg.call(
  580. zoom.transform,
  581. d3.zoomIdentity
  582. .translate(width / 2 - clampedScale * centerX, height / 2 - clampedScale * centerY)
  583. .scale(clampedScale),
  584. );
  585. }
  586. // 选择节点
  587. function selectNode(d) {
  588. // 移除之前的选中状态
  589. d3.selectAll(".node").classed("selected", false);
  590. // 添加选中状态
  591. d3.select(event.currentTarget).classed("selected", true);
  592. selectedNode = d;
  593. // 高亮路径
  594. highlightPath(d);
  595. // 显示详情
  596. showNodeDetail(d);
  597. }
  598. // 高亮路径
  599. function highlightPath(d) {
  600. // 移除之前的高亮
  601. d3.selectAll(".link").classed("highlighted", false);
  602. // 找到从根节点到当前节点的路径
  603. const path = d.ancestors().reverse();
  604. // 高亮路径上的连线
  605. d3.selectAll(".link").each(function (linkData) {
  606. const sourceInPath = path.includes(linkData.source);
  607. const targetInPath = path.includes(linkData.target);
  608. if (sourceInPath && targetInPath) {
  609. d3.select(this).classed("highlighted", true);
  610. }
  611. });
  612. }
  613. // 显示节点详情
  614. function showNodeDetail(d) {
  615. const detailContent = document.getElementById("detail-content");
  616. const goal = d.data;
  617. let html = `
  618. <div class="detail-item">
  619. <label>节点 ID:</label>
  620. <div class="detail-item-value">${goal.id}</div>
  621. </div>
  622. <div class="detail-item">
  623. <label>描述:</label>
  624. <div class="detail-item-value">${goal.description}</div>
  625. </div>
  626. <div class="detail-item">
  627. <label>创建理由:</label>
  628. <div class="detail-item-value">${goal.reason}</div>
  629. </div>
  630. <div class="detail-item">
  631. <label>状态:</label>
  632. <div class="detail-item-value">${goal.status}</div>
  633. </div>
  634. `;
  635. if (goal.summary) {
  636. html += `
  637. <div class="detail-item">
  638. <label>总结:</label>
  639. <div class="detail-item-value">${goal.summary}</div>
  640. </div>
  641. `;
  642. }
  643. // 显示统计信息
  644. if (goal.self_stats) {
  645. html += `
  646. <div class="detail-item">
  647. <label>当前节点统计:</label>
  648. <div class="stats-grid">
  649. <div class="stat-item">
  650. <div class="stat-label">消息数</div>
  651. <div class="stat-value">${goal.self_stats.message_count}</div>
  652. </div>
  653. <div class="stat-item">
  654. <div class="stat-label">Token 数</div>
  655. <div class="stat-value">${goal.self_stats.total_tokens}</div>
  656. </div>
  657. <div class="stat-item">
  658. <div class="stat-label">成本</div>
  659. <div class="stat-value">$${goal.self_stats.total_cost.toFixed(3)}</div>
  660. </div>
  661. <div class="stat-item">
  662. <div class="stat-label">预览</div>
  663. <div class="stat-value" style="font-size: 12px;">${goal.self_stats.preview || "-"}</div>
  664. </div>
  665. </div>
  666. </div>
  667. `;
  668. }
  669. if (goal.cumulative_stats) {
  670. html += `
  671. <div class="detail-item">
  672. <label>累计统计:</label>
  673. <div class="stats-grid">
  674. <div class="stat-item">
  675. <div class="stat-label">消息数</div>
  676. <div class="stat-value">${goal.cumulative_stats.message_count}</div>
  677. </div>
  678. <div class="stat-item">
  679. <div class="stat-label">Token 数</div>
  680. <div class="stat-value">${goal.cumulative_stats.total_tokens}</div>
  681. </div>
  682. <div class="stat-item">
  683. <div class="stat-label">成本</div>
  684. <div class="stat-value">$${goal.cumulative_stats.total_cost.toFixed(3)}</div>
  685. </div>
  686. <div class="stat-item">
  687. <div class="stat-label">预览</div>
  688. <div class="stat-value" style="font-size: 12px;">${goal.cumulative_stats.preview || "-"}</div>
  689. </div>
  690. </div>
  691. </div>
  692. `;
  693. }
  694. detailContent.innerHTML = html;
  695. }
  696. // 显示边详情
  697. function showEdgeDetail(d) {
  698. const detailContent = document.getElementById("detail-content");
  699. const html = `
  700. <div class="detail-item">
  701. <label>连线:</label>
  702. <div class="detail-item-value">${d.source.data.description} → ${d.target.data.description}</div>
  703. </div>
  704. <div class="detail-item">
  705. <label>目标节点:</label>
  706. <div class="detail-item-value">${d.target.data.description}</div>
  707. </div>
  708. `;
  709. detailContent.innerHTML = html;
  710. }
  711. // 绑定事件
  712. function bindEvents() {
  713. // Trace 选择器
  714. document.getElementById("trace-select").addEventListener("change", function () {
  715. currentTraceId = this.value;
  716. updateTitle();
  717. renderGraph();
  718. });
  719. // 刷新按钮
  720. document.getElementById("refresh-btn").addEventListener("click", function () {
  721. renderGraph();
  722. });
  723. // 详情面板收起/展开
  724. const detailToggle = document.getElementById("detail-toggle");
  725. const detailPanel = document.getElementById("detail-panel");
  726. let isDetailCollapsed = false;
  727. detailToggle.addEventListener("click", function () {
  728. isDetailCollapsed = !isDetailCollapsed;
  729. if (isDetailCollapsed) {
  730. detailPanel.classList.add("collapsed");
  731. detailToggle.textContent = "展开";
  732. } else {
  733. detailPanel.classList.remove("collapsed");
  734. detailToggle.textContent = "收起";
  735. }
  736. setTimeout(() => {
  737. renderGraph();
  738. }, 0);
  739. });
  740. // 窗口大小变化
  741. let resizeTimer;
  742. window.addEventListener("resize", function () {
  743. clearTimeout(resizeTimer);
  744. resizeTimer = setTimeout(function () {
  745. renderGraph();
  746. }, 250);
  747. });
  748. }
  749. // 启动
  750. init();
  751. </script>
  752. </body>
  753. </html>