| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987 |
- <!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;
- stroke-width: 0;
- }
- .node.selected rect {
- stroke: none;
- }
- .node text {
- font-size: 10px;
- font-family: sans-serif;
- text-anchor: middle;
- }
- .node.running rect {
- stroke: none;
- fill: transparent;
- }
- .node.trace_root text {
- font-size: 16px;
- font-weight: bold;
- }
- /* 连线样式 */
- .link {
- fill: none;
- stroke: #5ba85f;
- stroke-width: 2px;
- cursor: pointer;
- }
- .link.highlighted {
- stroke: #ff6b6b;
- }
- .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;
- }
- .node.message rect {
- stroke: none;
- fill: transparent;
- }
- .node.message text {
- fill: #666;
- }
- </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">
- <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 rawGoalList = "__GOAL_LIST__";
- const goalList = typeof rawGoalList === "string" && rawGoalList === "__GOAL_LIST__" ? [] : rawGoalList;
- console.log("%c [ goalList ]-401", "font-size:13px; background:pink; color:#bf2c9f;", goalList);
- const rawMsgGroups = "__MSG_GROUPS__";
- const msgGroups = typeof rawMsgGroups === "string" && rawMsgGroups === "__MSG_GROUPS__" ? {} : rawMsgGroups;
- console.log("%c [ msgGroups ]-404", "font-size:13px; background:pink; color:#bf2c9f;", msgGroups);
- let selectedNode = null;
- let selectedLink = null;
- let svg, g, zoom;
- let root;
- // Set to store IDs of expanded nodes (both goals and messages)
- const expandedState = new Set();
- // 初始化
- function init() {
- updateTitle();
- // Initialize: Ensure at least the first goal is in the chain if needed,
- // but our logic handles it.
- renderWrapper();
- bindEvents();
- }
- function renderWrapper() {
- root = setupData();
- renderGraph();
- }
- // 更新标题
- function updateTitle() {
- if (goalList.length > 0 && goalList[0].description) {
- document.getElementById("task-title").textContent = goalList[0].description;
- }
- }
- // 构建数据
- function setupData() {
- const goals = Array.isArray(goalList) ? goalList : [];
- const nodeMap = new Map();
- // Helper to create a node object
- const createNode = (data, type) => ({
- ...data,
- type: type,
- children: [],
- _original: data,
- });
- // 1. Identify Top-Level Goals and Child Goals
- const topLevelGoals = [];
- const childGoalsMap = new Map(); // parent_id -> [goals]
- goals.forEach((goal) => {
- if (goal.parent_id === null) {
- topLevelGoals.push(goal);
- } else {
- if (!childGoalsMap.has(goal.parent_id)) {
- childGoalsMap.set(goal.parent_id, []);
- }
- childGoalsMap.get(goal.parent_id).push(goal);
- }
- });
- // 2. Build the Tree
- const virtualRoot = {
- id: "VIRTUAL_ROOT",
- children: [],
- };
- let currentParent = virtualRoot;
- topLevelGoals.forEach((goal, index) => {
- const goalNode = createNode(goal, "goal");
- const nextGoal = index < topLevelGoals.length - 1 ? topLevelGoals[index + 1] : null;
- goalNode._nextGoalId = nextGoal ? nextGoal.id : null;
- if (childGoalsMap.has(goal.id)) {
- childGoalsMap.get(goal.id).forEach((subGoal) => {
- goalNode.children.push(createNode(subGoal, "goal"));
- });
- }
- currentParent.children.push(goalNode);
- if (msgGroups && msgGroups[goal.id] && expandedState.has(goal.id)) {
- const msgs = msgGroups[goal.id];
- let msgParent = goalNode;
- for (let i = 0; i < msgs.length; i++) {
- const msg = msgs[i];
- const msgNode = createNode(msg, "message");
- msgNode.id = `${goal.id}-msg-${i}`;
- msgParent.children.push(msgNode);
- msgParent = msgNode;
- }
- }
- currentParent = goalNode;
- });
- return d3.hierarchy(virtualRoot);
- }
- function buildLayoutData(node) {
- const children = Array.isArray(node.children)
- ? node.children.filter((child) => child.type !== "message").map((child) => buildLayoutData(child))
- : [];
- return { ...node, children };
- }
- // 渲染图表
- 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");
- // 定义箭头
- const defs = svg.append("defs");
- // 默认蓝色箭头
- 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");
- // 选中红色箭头
- defs
- .append("marker")
- .attr("id", "arrowhead-selected")
- .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", "#ff6b6b");
- g = svg.append("g");
- const treeLayout = d3
- .tree()
- .size([height - 100, width - 200])
- .separation((a, b) => (a.parent == b.parent ? 1.2 : 1.5));
- const layoutData = buildLayoutData(root.data);
- const layoutRoot = d3.hierarchy(layoutData);
- treeLayout(layoutRoot);
- const positionMap = new Map();
- layoutRoot.descendants().forEach((d) => {
- if (d.data && d.data.id !== undefined) {
- positionMap.set(d.data.id, { x: d.x, y: d.y });
- }
- });
- root.descendants().forEach((d) => {
- if (d.data && d.data.type !== "message" && positionMap.has(d.data.id)) {
- const pos = positionMap.get(d.data.id);
- d.x = pos.x;
- d.y = pos.y;
- }
- });
- const getMessageChain = (goalNode) => {
- const chain = [];
- if (!goalNode.children) return chain;
- let current = goalNode.children.find((child) => child.data && child.data.type === "message");
- while (current && current.data && current.data.type === "message") {
- chain.push(current);
- if (!current.children) break;
- const next = current.children.find((child) => child.data && child.data.type === "message");
- current = next || null;
- }
- return chain;
- };
- const goalNodes = root.descendants().filter((d) => d.data && d.data.type === "goal");
- const goalNodeMap = new Map();
- goalNodes.forEach((goalNode) => {
- goalNodeMap.set(goalNode.data.id, goalNode);
- });
- const msgBaseDown = 120;
- const msgStepDown = 75;
- const msgIndent = 40;
- goalNodes.forEach((goalNode) => {
- const messageNodes = getMessageChain(goalNode);
- if (messageNodes.length === 0) return;
- messageNodes.forEach((messageNode, idx) => {
- messageNode.x = goalNode.x + msgBaseDown + idx * msgStepDown;
- messageNode.y = goalNode.y + msgIndent;
- });
- });
- const nodesData = root.descendants().filter((d) => d.depth > 0);
- const baseLinksData = root.links().filter((d) => d.source.depth > 0);
- const extraLinks = [];
- goalNodes.forEach((goalNode) => {
- if (!expandedState.has(goalNode.data.id)) return;
- const messageNodes = getMessageChain(goalNode);
- if (messageNodes.length === 0) return;
- const lastMessage = messageNodes[messageNodes.length - 1];
- const nextGoalId = goalNode.data._nextGoalId;
- const nextGoalNode = nextGoalId ? goalNodeMap.get(nextGoalId) : null;
- if (nextGoalNode) {
- extraLinks.push({
- source: lastMessage,
- target: nextGoalNode,
- _linkType: "message-to-next-goal",
- });
- }
- });
- const linksData = baseLinksData.concat(extraLinks);
- // 绘制连线
- const linkGroups = g.selectAll(".link-group").data(linksData).enter().append("g").attr("class", "link-group");
- linkGroups
- .append("path")
- .attr("class", (d) => {
- // 保持高亮状态
- return d === selectedLink ? "link highlighted" : "link";
- })
- .attr("d", (d) => {
- const sourceY = d.source.x + 50;
- const targetY = d.target.x + 50;
- if (d._linkType === "message-to-next-goal") {
- const sourceX = d.source.y + 100;
- const targetX = d.target.y + 100 - 80;
- const midY = Math.max(sourceY, targetY) + 60;
- const cx1 = sourceX + 40;
- const cy1 = midY;
- const cx2 = targetX - 40;
- const cy2 = midY;
- return `M${sourceX},${sourceY} C${cx1},${cy1} ${cx2},${cy2} ${targetX},${targetY}`;
- }
- if (d.target.data.type === "message") {
- const sourceX = d.source.y + 100;
- const targetX = d.target.y + 100;
- const midY = Math.max(sourceY, targetY) + 40;
- return `M${sourceX},${sourceY} C${sourceX},${midY} ${targetX},${midY} ${targetX},${targetY}`;
- }
- const sourceX = d.source.y + 100 + 80;
- const targetX = d.target.y + 100 - 80;
- return `M${sourceX},${sourceY} C${(sourceX + targetX) / 2},${sourceY} ${(sourceX + targetX) / 2},${targetY} ${targetX},${targetY}`;
- })
- .attr("marker-end", (d) => {
- // 如果选中,使用红色箭头
- if (d === selectedLink) return "url(#arrowhead-selected)";
- return "url(#arrowhead)";
- })
- .attr("stroke-dasharray", (d) => {
- if (d._linkType === "message-to-next-goal") return "5,5";
- if (d.target.data.type === "message") return "5,5";
- return null;
- })
- .on("click", function (event, d) {
- event.stopPropagation();
- selectedLink = d;
- renderWrapper(); // 重绘以更新样式
- showEdgeDetail(d);
- });
- // 绘制节点
- 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();
- handleNodeClick(d);
- });
- // 节点文本
- const textNode = nodes
- .append("text")
- .attr("dy", 5)
- .text((d) => {
- const text = d.data.description || "";
- const limit = d.data.type === "message" ? 6 : 15; // Message limited to 6 chars
- return text.length > limit ? text.substring(0, limit) + "..." : text;
- });
- // Add tooltip for full description
- textNode.append("title").text((d) => d.data.description || "");
- // 缩放
- zoom = d3
- .zoom()
- .scaleExtent([0.1, 5])
- .on("zoom", function (event) {
- g.attr("transform", event.transform);
- });
- svg.call(zoom);
- // 初始自适应(仅在第一次或重置时?)
- // 为简单起见,这里不每次重置视图,除非是第一次
- // 或者保留当前的 transform
- }
- function handleNodeClick(d) {
- // Toggle expanded state
- if (expandedState.has(d.data.id)) {
- expandedState.delete(d.data.id);
- } else {
- expandedState.add(d.data.id);
- }
- // 2. 选中逻辑
- selectedNode = d;
- // 3. 重绘
- renderWrapper();
- // 4. 显示详情
- selectNode(d);
- }
- // 自动缩放以适应屏幕
- 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).attr("marker-end", "url(#arrowhead)");
- // 找到从根节点到当前节点的路径
- 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).attr("marker-end", "url(#arrowhead-selected)");
- }
- });
- }
- // 显示节点详情
- 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>
- `;
- }
- const messages = msgGroups[goal.id] || [];
- if (Array.isArray(messages) && messages.length > 0) {
- const messageHtml = messages
- .map((message) => {
- const role = message.role || message.sender || "unknown";
- const content = message.content || message.text || "";
- return `
- <div class="message-item ${role}">
- <div class="message-header">
- <span class="message-role">${role}</span>
- <span class="message-time">${message.created_at || ""}</span>
- </div>
- <div class="message-content">${content}</div>
- </div>
- `;
- })
- .join("");
- html += `
- <div class="detail-item">
- <label>节点内容:</label>
- <div class="message-list">${messageHtml}</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() {
- // 状态筛选器
- document.getElementById("status-filter").addEventListener("change", function () {
- 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>
|