feat(trace): improve session workbench review
This commit is contained in:
@@ -8,6 +8,7 @@ class SuperBizAgentApp {
|
||||
this.currentChatHistory = []; // 当前对话的消息历史
|
||||
this.chatHistories = this.loadChatHistories(); // 所有历史对话
|
||||
this.isCurrentChatFromHistory = false; // 标记当前对话是否是从历史记录加载的
|
||||
this.lastTraceSessionId = this.loadLastTraceSessionId();
|
||||
|
||||
this.initializeElements();
|
||||
this.bindEvents();
|
||||
@@ -15,6 +16,7 @@ class SuperBizAgentApp {
|
||||
this.initMarkdown();
|
||||
this.checkAndSetCentered();
|
||||
this.renderChatHistory();
|
||||
this.updateTraceWorkbenchLink();
|
||||
}
|
||||
|
||||
// 初始化Markdown配置
|
||||
@@ -116,6 +118,7 @@ class SuperBizAgentApp {
|
||||
this.chatContainer = document.querySelector('.chat-container');
|
||||
this.welcomeGreeting = document.getElementById('welcomeGreeting');
|
||||
this.chatHistoryList = document.getElementById('chatHistoryList');
|
||||
this.traceWorkbenchLink = document.getElementById('traceWorkbenchLink');
|
||||
|
||||
// 初始化时检查是否需要居中
|
||||
this.checkAndSetCentered();
|
||||
@@ -265,6 +268,7 @@ class SuperBizAgentApp {
|
||||
|
||||
// 生成新的会话ID
|
||||
this.sessionId = this.generateSessionId();
|
||||
this.updateTraceWorkbenchLink();
|
||||
|
||||
// 重置模式为快速
|
||||
this.currentMode = 'quick';
|
||||
@@ -312,6 +316,7 @@ class SuperBizAgentApp {
|
||||
|
||||
// 添加到历史记录列表的开头
|
||||
this.chatHistories.unshift(chatHistory);
|
||||
this.rememberTraceSessionId(this.sessionId);
|
||||
|
||||
// 限制历史记录数量(最多保存50条)
|
||||
if (this.chatHistories.length > 50) {
|
||||
@@ -439,6 +444,7 @@ class SuperBizAgentApp {
|
||||
|
||||
// 加载历史对话
|
||||
this.sessionId = history.id;
|
||||
this.rememberTraceSessionId(this.sessionId);
|
||||
this.currentChatHistory = [...history.messages];
|
||||
this.isCurrentChatFromHistory = true; // 标记为从历史记录加载
|
||||
|
||||
@@ -468,10 +474,45 @@ class SuperBizAgentApp {
|
||||
this.chatMessages.innerHTML = '';
|
||||
}
|
||||
this.sessionId = this.generateSessionId();
|
||||
this.updateTraceWorkbenchLink();
|
||||
this.checkAndSetCentered();
|
||||
}
|
||||
}
|
||||
|
||||
loadLastTraceSessionId() {
|
||||
try {
|
||||
return localStorage.getItem('lastTraceSessionId') || '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
rememberTraceSessionId(sessionId) {
|
||||
if (!sessionId) {
|
||||
return;
|
||||
}
|
||||
this.lastTraceSessionId = sessionId;
|
||||
try {
|
||||
localStorage.setItem('lastTraceSessionId', sessionId);
|
||||
} catch (e) {
|
||||
// localStorage may be unavailable in private or restricted contexts.
|
||||
}
|
||||
this.updateTraceWorkbenchLink();
|
||||
}
|
||||
|
||||
updateTraceWorkbenchLink() {
|
||||
if (!this.traceWorkbenchLink) {
|
||||
return;
|
||||
}
|
||||
const recentHistory = this.chatHistories.find(history => history && history.id);
|
||||
const sessionId = this.lastTraceSessionId
|
||||
|| (this.currentChatHistory.length > 0 ? this.sessionId : '')
|
||||
|| (recentHistory ? recentHistory.id : '');
|
||||
this.traceWorkbenchLink.href = sessionId
|
||||
? `trace.html?sessionId=${encodeURIComponent(sessionId)}`
|
||||
: 'trace.html';
|
||||
}
|
||||
|
||||
// 切换模式下拉菜单
|
||||
toggleModeDropdown() {
|
||||
if (this.modeSelectorBtn && this.modeDropdown) {
|
||||
@@ -637,6 +678,7 @@ class SuperBizAgentApp {
|
||||
// 保存后端返回的 sessionId,用于 feedback 提交
|
||||
if (chatResponse.sessionId) {
|
||||
this.lastSessionId = chatResponse.sessionId;
|
||||
this.rememberTraceSessionId(chatResponse.sessionId);
|
||||
}
|
||||
// 成功:添加实际响应消息(即使 answer 为空也显示)
|
||||
const answer = chatResponse.answer || '(无回复内容)';
|
||||
@@ -1231,7 +1273,10 @@ class SuperBizAgentApp {
|
||||
for (const jsonStr of matches) {
|
||||
try {
|
||||
const sseMessage = JSON.parse(jsonStr);
|
||||
if (sseMessage.type === 'content') {
|
||||
if (sseMessage.type === 'session') {
|
||||
this.lastSessionId = sseMessage.data;
|
||||
this.rememberTraceSessionId(sseMessage.data);
|
||||
} else if (sseMessage.type === 'content') {
|
||||
fullResponse += sseMessage.data || '';
|
||||
} else if (sseMessage.type === 'done') {
|
||||
console.log('AI Ops 流完成,最终内容长度:', fullResponse.length);
|
||||
@@ -1261,7 +1306,10 @@ class SuperBizAgentApp {
|
||||
try {
|
||||
const sseMessage = JSON.parse(rawData);
|
||||
if (sseMessage && sseMessage.type) {
|
||||
if (sseMessage.type === 'content') {
|
||||
if (sseMessage.type === 'session') {
|
||||
this.lastSessionId = sseMessage.data;
|
||||
this.rememberTraceSessionId(sseMessage.data);
|
||||
} else if (sseMessage.type === 'content') {
|
||||
fullResponse += sseMessage.data || '';
|
||||
if (loadingMessageElement) {
|
||||
this.updateAIOpsStreamContent(loadingMessageElement, fullResponse);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>智能OnCall助手</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<!-- Markdown 渲染库 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/marked.min.js"></script>
|
||||
@@ -34,12 +35,12 @@
|
||||
<span style="font-size: 14px; font-weight: 500;">文档管理</span>
|
||||
</a>
|
||||
|
||||
<a href="trace.html" class="sidebar-btn" style="display: flex; align-items: center; gap: 12px; padding: 12px; border-radius: 12px; color: #202124; text-decoration: none; transition: background 0.3s ease; margin-top: 8px;">
|
||||
<a href="trace.html" id="traceWorkbenchLink" class="sidebar-btn" style="display: flex; align-items: center; gap: 12px; padding: 12px; border-radius: 12px; color: #202124; text-decoration: none; transition: background 0.3s ease; margin-top: 8px;">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width: 20px; height: 20px; flex-shrink: 0;">
|
||||
<path d="M4 19V5M4 5H20M4 12H16M4 19H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 5V19M16 5V19" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span style="font-size: 14px; font-weight: 500;">Trace Workbench</span>
|
||||
<span style="font-size: 14px; font-weight: 500;">链路检查台</span>
|
||||
</a>
|
||||
|
||||
<div class="chat-history-section">
|
||||
@@ -142,6 +143,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
<script src="app.js?v=20260707-session-picker"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -123,11 +123,16 @@ svg {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.session-picker {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.session-control input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 42px;
|
||||
padding: 0 12px;
|
||||
padding: 0 46px 0 12px;
|
||||
border: 1px solid #4b5563;
|
||||
border-radius: 6px;
|
||||
background: #0b1220;
|
||||
@@ -135,7 +140,76 @@ svg {
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
|
||||
}
|
||||
|
||||
.session-control button {
|
||||
.session-picker-button {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 5px;
|
||||
background: #111827;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.session-options {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: 48px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
padding: 6px;
|
||||
border: 1px solid #4b5563;
|
||||
border-radius: 8px;
|
||||
background: #0b1220;
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
.session-option {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--white);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.session-option:hover,
|
||||
.session-option.active {
|
||||
border-color: #2f8375;
|
||||
background: rgba(31, 122, 107, 0.18);
|
||||
}
|
||||
|
||||
.session-option-title {
|
||||
font-size: 13px;
|
||||
font-weight: 720;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.session-option-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
color: #94a3b8;
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.session-option-empty {
|
||||
padding: 12px;
|
||||
color: #94a3b8;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.session-control > button {
|
||||
height: 42px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -149,7 +223,7 @@ svg {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.session-control button:disabled {
|
||||
.session-control > button:disabled {
|
||||
opacity: 0.62;
|
||||
cursor: wait;
|
||||
}
|
||||
@@ -176,6 +250,132 @@ svg {
|
||||
background: var(--evidence-soft);
|
||||
}
|
||||
|
||||
.outcome-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr);
|
||||
gap: 0;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.outcome-main {
|
||||
min-width: 0;
|
||||
padding: 20px;
|
||||
border-left: 7px solid var(--steel);
|
||||
}
|
||||
|
||||
.outcome-panel.ok .outcome-main {
|
||||
border-left-color: var(--evidence);
|
||||
}
|
||||
|
||||
.outcome-panel.warn .outcome-main {
|
||||
border-left-color: var(--warn);
|
||||
}
|
||||
|
||||
.outcome-panel.bad .outcome-main {
|
||||
border-left-color: var(--reject);
|
||||
}
|
||||
|
||||
.outcome-eyebrow {
|
||||
margin: 0 0 8px;
|
||||
color: var(--steel);
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.outcome-main h2 {
|
||||
margin: 0;
|
||||
font-size: clamp(24px, 3vw, 38px);
|
||||
line-height: 1.08;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.outcome-main > p:not(.outcome-eyebrow) {
|
||||
margin: 10px 0 0;
|
||||
color: var(--steel);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.outcome-answer {
|
||||
margin-top: 16px;
|
||||
padding: 14px 16px;
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: #fffaf1;
|
||||
color: #172033;
|
||||
line-height: 1.65;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.outcome-side {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
border-left: 1px solid var(--line);
|
||||
background: #fbf7ef;
|
||||
}
|
||||
|
||||
.outcome-verdict {
|
||||
padding: 18px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.outcome-verdict span,
|
||||
.outcome-insights h3 {
|
||||
color: var(--steel);
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.outcome-verdict strong {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
font-size: 30px;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
.outcome-verdict p {
|
||||
margin: 10px 0 0;
|
||||
color: var(--ink-soft);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.outcome-insights {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.outcome-insights > div {
|
||||
min-width: 0;
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.outcome-insights > div + div {
|
||||
border-left: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.outcome-insights h3 {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.outcome-insights ul {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.outcome-insights li {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(120px, 1fr));
|
||||
@@ -547,6 +747,15 @@ pre,
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.outcome-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.outcome-side {
|
||||
border-left: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.summary-strip {
|
||||
grid-template-columns: repeat(4, minmax(120px, 1fr));
|
||||
}
|
||||
@@ -581,6 +790,15 @@ pre,
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.outcome-insights {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.outcome-insights > div + div {
|
||||
border-left: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.summary-cell,
|
||||
.summary-cell:nth-child(4) {
|
||||
border-right: 1px solid var(--line);
|
||||
|
||||
@@ -1,67 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Trace Workbench - SuperBizAgent</title>
|
||||
<link rel="stylesheet" href="trace.css">
|
||||
<title>诊断链路检查台 - SuperBizAgent</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<link rel="stylesheet" href="trace.css?v=20260707-outcome">
|
||||
</head>
|
||||
<body>
|
||||
<main class="trace-app">
|
||||
<header class="trace-header">
|
||||
<div>
|
||||
<a class="trace-back-link" href="index.html" aria-label="Back to chat">
|
||||
<a class="trace-back-link" href="index.html" aria-label="返回聊天">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M19 12H5M12 19L5 12L12 5" />
|
||||
</svg>
|
||||
<span>Chat</span>
|
||||
<span>返回聊天</span>
|
||||
</a>
|
||||
<h1>Trace workbench</h1>
|
||||
<h1>诊断链路检查台</h1>
|
||||
</div>
|
||||
|
||||
<form class="session-form" id="sessionForm">
|
||||
<label for="sessionIdInput">Session id</label>
|
||||
<label for="sessionIdInput">会话 ID</label>
|
||||
<div class="session-control">
|
||||
<input id="sessionIdInput" name="sessionId" autocomplete="off" spellcheck="false" placeholder="e2e-mysql-skill-rag-20260706-2355">
|
||||
<div class="session-picker">
|
||||
<input id="sessionIdInput" name="sessionId" autocomplete="off" spellcheck="false" placeholder="e2e-mysql-skill-rag-20260706-2355" aria-controls="sessionOptions" aria-expanded="false">
|
||||
<button class="session-picker-button" id="sessionPickerButton" type="button" aria-label="选择最近会话">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M6 9L12 15L18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="session-options" id="sessionOptions" role="listbox" aria-label="最近会话" hidden></div>
|
||||
</div>
|
||||
<button type="submit" id="loadButton">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M5 12H19M13 6L19 12L13 18" />
|
||||
</svg>
|
||||
<span>Load</span>
|
||||
<span>加载</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</header>
|
||||
|
||||
<section class="state-line" id="stateLine" role="status">Enter a session id to load a persisted diagnosis trace.</section>
|
||||
<section class="state-line" id="stateLine" role="status">输入会话 ID,加载已持久化的诊断链路。</section>
|
||||
|
||||
<section class="summary-strip" id="summaryStrip" aria-label="Trace summary">
|
||||
<section class="outcome-panel" id="outcomePanel" aria-label="会话结果">
|
||||
<div class="outcome-main">
|
||||
<p class="outcome-eyebrow">本次会话结果</p>
|
||||
<h2 id="outcomeTitle">尚未加载会话</h2>
|
||||
<p id="outcomeSubtitle">选择一个会话后,这里会先展示最终结论和证据状态。</p>
|
||||
<div class="outcome-answer" id="outcomeAnswerSummary">-</div>
|
||||
</div>
|
||||
<div class="outcome-side">
|
||||
<div class="outcome-verdict">
|
||||
<span>校验判断</span>
|
||||
<strong id="outcomeVerdict">-</strong>
|
||||
<p id="outcomeEvidenceState">-</p>
|
||||
</div>
|
||||
<div class="outcome-insights">
|
||||
<div>
|
||||
<h3>已确认</h3>
|
||||
<ul id="outcomeConfirmed"></ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>仍需关注</h3>
|
||||
<ul id="outcomeGaps"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="summary-strip" id="summaryStrip" aria-label="诊断链路汇总">
|
||||
<div class="summary-cell">
|
||||
<span>Status</span>
|
||||
<span>状态</span>
|
||||
<strong id="summaryStatus">-</strong>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<span>Verdict</span>
|
||||
<span>校验结论</span>
|
||||
<strong id="summaryVerdict">-</strong>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<span>Groundedness</span>
|
||||
<span>证据分</span>
|
||||
<strong id="summaryScore">-</strong>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<span>Steps</span>
|
||||
<span>步骤</span>
|
||||
<strong id="summarySteps">-</strong>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<span>Tools</span>
|
||||
<span>工具</span>
|
||||
<strong id="summaryTools">-</strong>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<span>Duration</span>
|
||||
<span>耗时</span>
|
||||
<strong id="summaryDuration">-</strong>
|
||||
</div>
|
||||
<div class="summary-cell">
|
||||
<span>Tokens</span>
|
||||
<span>令牌</span>
|
||||
<strong id="summaryTokens">-</strong>
|
||||
</div>
|
||||
</section>
|
||||
@@ -70,8 +105,8 @@
|
||||
<section class="trace-pane agent-pane" aria-labelledby="agentPaneTitle">
|
||||
<div class="pane-heading">
|
||||
<div>
|
||||
<p>Agent rail</p>
|
||||
<h2 id="agentPaneTitle">Persisted steps</h2>
|
||||
<p>智能体轨迹</p>
|
||||
<h2 id="agentPaneTitle">持久化步骤</h2>
|
||||
</div>
|
||||
<span class="pane-count" id="stepCount">0</span>
|
||||
</div>
|
||||
@@ -81,8 +116,8 @@
|
||||
<section class="trace-pane evidence-pane" aria-labelledby="evidencePaneTitle">
|
||||
<div class="pane-heading">
|
||||
<div>
|
||||
<p>Evidence ledger</p>
|
||||
<h2 id="evidencePaneTitle">Tool invocations</h2>
|
||||
<p>证据账本</p>
|
||||
<h2 id="evidencePaneTitle">工具调用</h2>
|
||||
</div>
|
||||
<span class="pane-count" id="toolCount">0</span>
|
||||
</div>
|
||||
@@ -93,30 +128,30 @@
|
||||
<section class="trace-pane inspector-pane" aria-labelledby="inspectorPaneTitle">
|
||||
<div class="pane-heading">
|
||||
<div>
|
||||
<p>Inspector</p>
|
||||
<h2 id="inspectorPaneTitle">Verifier and RAG</h2>
|
||||
<p>检查器</p>
|
||||
<h2 id="inspectorPaneTitle">校验与检索增强</h2>
|
||||
</div>
|
||||
<span class="pane-count" id="inspectorMode">idle</span>
|
||||
<span class="pane-count" id="inspectorMode">空闲</span>
|
||||
</div>
|
||||
|
||||
<div class="inspector-stack">
|
||||
<section class="inspector-section" aria-labelledby="skillBoundaryTitle">
|
||||
<h3 id="skillBoundaryTitle">Skill boundary</h3>
|
||||
<h3 id="skillBoundaryTitle">技能边界</h3>
|
||||
<div class="boundary-grid" id="skillBoundary"></div>
|
||||
</section>
|
||||
|
||||
<section class="inspector-section" aria-labelledby="verifierTitle">
|
||||
<h3 id="verifierTitle">Verifier facts</h3>
|
||||
<h3 id="verifierTitle">校验事实</h3>
|
||||
<div id="verifierFacts"></div>
|
||||
</section>
|
||||
|
||||
<section class="inspector-section" aria-labelledby="toolInspectorTitle">
|
||||
<h3 id="toolInspectorTitle">Selected tool</h3>
|
||||
<h3 id="toolInspectorTitle">选中工具</h3>
|
||||
<div id="toolInspector"></div>
|
||||
</section>
|
||||
|
||||
<section class="inspector-section" aria-labelledby="answerTitle">
|
||||
<h3 id="answerTitle">Final answer</h3>
|
||||
<h3 id="answerTitle">最终回答</h3>
|
||||
<div class="answer-box" id="finalAnswer">-</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -124,6 +159,6 @@
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="trace.js"></script>
|
||||
<script src="trace.js?v=20260707-outcome"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -6,6 +6,8 @@ class TraceWorkbench {
|
||||
|
||||
this.sessionForm = document.getElementById('sessionForm');
|
||||
this.sessionIdInput = document.getElementById('sessionIdInput');
|
||||
this.sessionPickerButton = document.getElementById('sessionPickerButton');
|
||||
this.sessionOptions = document.getElementById('sessionOptions');
|
||||
this.loadButton = document.getElementById('loadButton');
|
||||
this.stateLine = document.getElementById('stateLine');
|
||||
this.stepsRail = document.getElementById('stepsRail');
|
||||
@@ -15,6 +17,17 @@ class TraceWorkbench {
|
||||
this.verifierFacts = document.getElementById('verifierFacts');
|
||||
this.toolInspector = document.getElementById('toolInspector');
|
||||
this.finalAnswer = document.getElementById('finalAnswer');
|
||||
this.outcomePanel = document.getElementById('outcomePanel');
|
||||
this.outcomeTitle = document.getElementById('outcomeTitle');
|
||||
this.outcomeSubtitle = document.getElementById('outcomeSubtitle');
|
||||
this.outcomeVerdict = document.getElementById('outcomeVerdict');
|
||||
this.outcomeEvidenceState = document.getElementById('outcomeEvidenceState');
|
||||
this.outcomeAnswerSummary = document.getElementById('outcomeAnswerSummary');
|
||||
this.outcomeConfirmed = document.getElementById('outcomeConfirmed');
|
||||
this.outcomeGaps = document.getElementById('outcomeGaps');
|
||||
this.sessionCandidates = [];
|
||||
this.filteredSessionCandidates = [];
|
||||
this.activeSessionIndex = 0;
|
||||
|
||||
this.bindEvents();
|
||||
this.bootstrapFromUrl();
|
||||
@@ -25,11 +38,34 @@ class TraceWorkbench {
|
||||
event.preventDefault();
|
||||
this.loadTrace(this.sessionIdInput.value.trim());
|
||||
});
|
||||
|
||||
this.sessionIdInput.addEventListener('focus', () => {
|
||||
this.renderSessionOptions(this.sessionIdInput.value);
|
||||
this.openSessionOptions();
|
||||
});
|
||||
|
||||
this.sessionIdInput.addEventListener('input', () => {
|
||||
this.renderSessionOptions(this.sessionIdInput.value);
|
||||
this.openSessionOptions();
|
||||
});
|
||||
|
||||
this.sessionIdInput.addEventListener('keydown', (event) => this.handleSessionInputKeydown(event));
|
||||
|
||||
this.sessionPickerButton.addEventListener('click', () => {
|
||||
this.renderSessionOptions(this.sessionIdInput.value);
|
||||
this.toggleSessionOptions();
|
||||
});
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!this.sessionForm.contains(event.target)) {
|
||||
this.closeSessionOptions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bootstrapFromUrl() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const sessionId = params.get('sessionId');
|
||||
const sessionId = params.get('sessionId') || this.getLastTraceSessionId();
|
||||
if (sessionId) {
|
||||
this.sessionIdInput.value = sessionId;
|
||||
this.loadTrace(sessionId);
|
||||
@@ -38,13 +74,204 @@ class TraceWorkbench {
|
||||
this.renderEmpty();
|
||||
}
|
||||
|
||||
async loadTrace(sessionId) {
|
||||
if (!sessionId) {
|
||||
this.setState('Enter a session id before loading.', 'error');
|
||||
getLastTraceSessionId() {
|
||||
try {
|
||||
const remembered = localStorage.getItem('lastTraceSessionId');
|
||||
if (remembered) {
|
||||
return remembered;
|
||||
}
|
||||
const histories = JSON.parse(localStorage.getItem('chatHistories') || '[]');
|
||||
const recent = Array.isArray(histories) ? histories.find(history => history && history.id) : null;
|
||||
return recent ? recent.id : '';
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
loadSessionCandidates() {
|
||||
const candidates = [];
|
||||
const seen = new Set();
|
||||
const addCandidate = (candidate) => {
|
||||
if (!candidate || !candidate.id || seen.has(candidate.id)) {
|
||||
return;
|
||||
}
|
||||
seen.add(candidate.id);
|
||||
candidates.push(candidate);
|
||||
};
|
||||
|
||||
try {
|
||||
const remembered = localStorage.getItem('lastTraceSessionId');
|
||||
const histories = JSON.parse(localStorage.getItem('chatHistories') || '[]');
|
||||
if (Array.isArray(histories)) {
|
||||
histories.forEach((history) => addCandidate({
|
||||
id: history.id,
|
||||
title: history.title || '未命名对话',
|
||||
updatedAt: history.updatedAt || history.createdAt || '',
|
||||
source: 'history'
|
||||
}));
|
||||
}
|
||||
if (remembered) {
|
||||
const existing = candidates.find(candidate => candidate.id === remembered);
|
||||
if (existing) {
|
||||
existing.source = 'recent';
|
||||
} else {
|
||||
addCandidate({
|
||||
id: remembered,
|
||||
title: '最近查看的会话',
|
||||
updatedAt: '',
|
||||
source: 'recent'
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return candidates;
|
||||
}
|
||||
|
||||
return candidates
|
||||
.sort((left, right) => {
|
||||
if (left.source === 'recent' && right.source !== 'recent') {
|
||||
return -1;
|
||||
}
|
||||
if (right.source === 'recent' && left.source !== 'recent') {
|
||||
return 1;
|
||||
}
|
||||
return this.timestampOf(right.updatedAt) - this.timestampOf(left.updatedAt);
|
||||
})
|
||||
.slice(0, 20);
|
||||
}
|
||||
|
||||
timestampOf(value) {
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
const timestamp = new Date(value).getTime();
|
||||
return Number.isNaN(timestamp) ? 0 : timestamp;
|
||||
}
|
||||
|
||||
renderSessionOptions(query = '') {
|
||||
this.sessionCandidates = this.loadSessionCandidates();
|
||||
const keyword = query.trim().toLowerCase();
|
||||
this.filteredSessionCandidates = keyword
|
||||
? this.sessionCandidates.filter((candidate) => {
|
||||
return candidate.id.toLowerCase().includes(keyword)
|
||||
|| candidate.title.toLowerCase().includes(keyword);
|
||||
})
|
||||
: this.sessionCandidates;
|
||||
this.activeSessionIndex = 0;
|
||||
|
||||
if (!this.filteredSessionCandidates.length) {
|
||||
this.sessionOptions.innerHTML = '<div class="session-option-empty">没有匹配的最近会话</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(`Loading ${sessionId}...`, 'loading');
|
||||
this.sessionOptions.innerHTML = this.filteredSessionCandidates.map((candidate, index) => `
|
||||
<button class="session-option ${index === this.activeSessionIndex ? 'active' : ''}" type="button" role="option" data-session-id="${this.escape(candidate.id)}">
|
||||
<span class="session-option-title">${this.escape(candidate.title)}</span>
|
||||
<span class="session-option-meta">
|
||||
<span>${this.escape(this.shortSessionId(candidate.id))}</span>
|
||||
<span>${this.escape(this.formatOptionDate(candidate.updatedAt))}</span>
|
||||
</span>
|
||||
</button>
|
||||
`).join('');
|
||||
|
||||
this.sessionOptions.querySelectorAll('.session-option').forEach((button) => {
|
||||
button.addEventListener('click', () => this.chooseSession(button.dataset.sessionId));
|
||||
});
|
||||
}
|
||||
|
||||
openSessionOptions() {
|
||||
this.sessionOptions.hidden = false;
|
||||
this.sessionIdInput.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
|
||||
closeSessionOptions() {
|
||||
this.sessionOptions.hidden = true;
|
||||
this.sessionIdInput.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
toggleSessionOptions() {
|
||||
if (this.sessionOptions.hidden) {
|
||||
this.openSessionOptions();
|
||||
this.sessionIdInput.focus();
|
||||
return;
|
||||
}
|
||||
this.closeSessionOptions();
|
||||
}
|
||||
|
||||
handleSessionInputKeydown(event) {
|
||||
if (event.key === 'Escape') {
|
||||
this.closeSessionOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.sessionOptions.hidden || !this.filteredSessionCandidates.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
this.activeSessionIndex = Math.min(this.activeSessionIndex + 1, this.filteredSessionCandidates.length - 1);
|
||||
this.markActiveSessionOption();
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
this.activeSessionIndex = Math.max(this.activeSessionIndex - 1, 0);
|
||||
this.markActiveSessionOption();
|
||||
} else if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
const candidate = this.filteredSessionCandidates[this.activeSessionIndex];
|
||||
if (candidate) {
|
||||
this.chooseSession(candidate.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
markActiveSessionOption() {
|
||||
this.sessionOptions.querySelectorAll('.session-option').forEach((button, index) => {
|
||||
button.classList.toggle('active', index === this.activeSessionIndex);
|
||||
if (index === this.activeSessionIndex) {
|
||||
button.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
chooseSession(sessionId) {
|
||||
this.sessionIdInput.value = sessionId;
|
||||
this.closeSessionOptions();
|
||||
this.loadTrace(sessionId);
|
||||
}
|
||||
|
||||
shortSessionId(sessionId) {
|
||||
if (!sessionId || sessionId.length <= 34) {
|
||||
return sessionId || '-';
|
||||
}
|
||||
return `${sessionId.slice(0, 18)}...${sessionId.slice(-10)}`;
|
||||
}
|
||||
|
||||
formatOptionDate(value) {
|
||||
if (!value) {
|
||||
return '最近';
|
||||
}
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '最近';
|
||||
}
|
||||
return date.toLocaleString('zh-CN', {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
|
||||
async loadTrace(sessionId) {
|
||||
if (!sessionId) {
|
||||
this.setState('请先输入会话 ID。', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(`正在加载 ${sessionId}...`, 'loading');
|
||||
this.updateUrl(sessionId);
|
||||
this.loadButton.disabled = true;
|
||||
|
||||
try {
|
||||
@@ -55,9 +282,13 @@ class TraceWorkbench {
|
||||
? data.toolInvocations[0].id
|
||||
: null;
|
||||
this.activeFilter = 'all';
|
||||
this.updateUrl(sessionId);
|
||||
try {
|
||||
localStorage.setItem('lastTraceSessionId', sessionId);
|
||||
} catch (error) {
|
||||
// ignore storage failures
|
||||
}
|
||||
this.renderTrace();
|
||||
this.setState(`Loaded ${sessionId}.`, '');
|
||||
this.setState(`已加载 ${sessionId}。`, '');
|
||||
} catch (error) {
|
||||
this.trace = null;
|
||||
this.renderEmpty();
|
||||
@@ -72,16 +303,27 @@ class TraceWorkbench {
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch (error) {
|
||||
throw new Error(`HTTP ${response.status}: response is not JSON`);
|
||||
throw new Error(`HTTP ${response.status}: 响应不是 JSON`);
|
||||
}
|
||||
|
||||
if (!response.ok || payload.code !== 200) {
|
||||
throw new Error(payload.message || `HTTP ${response.status}: trace request failed`);
|
||||
throw new Error(this.localizeError(payload.message) || `HTTP ${response.status}: 诊断链路请求失败`);
|
||||
}
|
||||
|
||||
return payload.data;
|
||||
}
|
||||
|
||||
localizeError(message) {
|
||||
if (!message) {
|
||||
return '';
|
||||
}
|
||||
const value = String(message);
|
||||
if (value.startsWith('Session not found:')) {
|
||||
return value.replace('Session not found:', '未找到会话:');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
updateUrl(sessionId) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('sessionId', sessionId);
|
||||
@@ -95,8 +337,8 @@ class TraceWorkbench {
|
||||
const steps = this.trace.steps || [];
|
||||
const tools = this.trace.toolInvocations || [];
|
||||
|
||||
this.text('summaryStatus', session.status || '-');
|
||||
this.text('summaryVerdict', verifier.verdict || '-');
|
||||
this.text('summaryStatus', this.displayStatus(session.status));
|
||||
this.text('summaryVerdict', this.displayVerdict(verifier.verdict));
|
||||
this.text('summaryScore', this.formatScore(verifier.groundedness_score));
|
||||
this.text('summarySteps', `${summary.returnedStepCount ?? steps.length}/${summary.persistedStepCount ?? session.stepCount ?? '-'}`);
|
||||
this.text('summaryTools', `${summary.returnedToolCallCount ?? tools.length}/${summary.persistedToolCallCount ?? session.toolCallCount ?? '-'}`);
|
||||
@@ -104,15 +346,16 @@ class TraceWorkbench {
|
||||
this.text('summaryTokens', this.formatNumber(session.totalTokenCount));
|
||||
this.text('stepCount', steps.length);
|
||||
this.text('toolCount', tools.length);
|
||||
this.text('inspectorMode', tools.length ? 'tool' : 'trace');
|
||||
this.text('inspectorMode', tools.length ? '工具' : '链路');
|
||||
|
||||
this.renderOutcome(session, verifier, steps, tools);
|
||||
this.renderSteps(steps);
|
||||
this.renderToolFilters(tools);
|
||||
this.renderTools(tools);
|
||||
this.renderSkillBoundary();
|
||||
this.renderVerifier(verifier);
|
||||
this.renderToolInspector();
|
||||
this.finalAnswer.textContent = session.answer || 'No final answer persisted.';
|
||||
this.finalAnswer.textContent = session.answer || '未持久化最终回答。';
|
||||
}
|
||||
|
||||
renderEmpty() {
|
||||
@@ -125,25 +368,150 @@ class TraceWorkbench {
|
||||
this.text('summaryTokens', '-');
|
||||
this.text('stepCount', '0');
|
||||
this.text('toolCount', '0');
|
||||
this.text('inspectorMode', 'idle');
|
||||
this.text('inspectorMode', '空闲');
|
||||
|
||||
this.stepsRail.innerHTML = '<div class="empty-panel">No trace loaded.</div>';
|
||||
this.renderOutcome({}, {}, [], []);
|
||||
this.stepsRail.innerHTML = '<div class="empty-panel">尚未加载诊断链路。</div>';
|
||||
this.toolFilters.innerHTML = '';
|
||||
this.toolLedger.innerHTML = '<div class="empty-panel">No tool invocations loaded.</div>';
|
||||
this.toolLedger.innerHTML = '<div class="empty-panel">尚未加载工具调用。</div>';
|
||||
this.skillBoundary.innerHTML = this.renderBoundaryItems({
|
||||
selectedSkill: '-',
|
||||
plannerReadSkill: 0,
|
||||
executorReadSkill: 0,
|
||||
verifierReadSkill: 0
|
||||
});
|
||||
this.verifierFacts.innerHTML = '<div class="empty-panel">No verifier evaluation loaded.</div>';
|
||||
this.toolInspector.innerHTML = '<div class="empty-panel">Select a tool invocation after loading a trace.</div>';
|
||||
this.verifierFacts.innerHTML = '<div class="empty-panel">尚未加载校验结果。</div>';
|
||||
this.toolInspector.innerHTML = '<div class="empty-panel">加载诊断链路后选择一个工具调用。</div>';
|
||||
this.finalAnswer.textContent = '-';
|
||||
}
|
||||
|
||||
renderOutcome(session, verifier, steps, tools) {
|
||||
const status = String(session.status || '').toUpperCase();
|
||||
const verdict = String(verifier.verdict || '').toUpperCase();
|
||||
const title = this.outcomeTitleFor(status, verdict);
|
||||
const checked = Array.isArray(verifier.facts_checked) ? verifier.facts_checked : [];
|
||||
const traceSummary = Array.isArray(verifier.tool_trace_summary) ? verifier.tool_trace_summary : [];
|
||||
const tracedFacts = checked.filter(fact => Array.isArray(fact.evidence_refs) && fact.evidence_refs.length).length;
|
||||
const score = this.formatScore(verifier.groundedness_score);
|
||||
const answer = session.answer || '未持久化最终回答。';
|
||||
const subtitleParts = [];
|
||||
|
||||
if (session.sessionId) {
|
||||
subtitleParts.push(`会话 ${session.sessionId}`);
|
||||
}
|
||||
if (session.status) {
|
||||
subtitleParts.push(`状态 ${this.displayStatus(session.status)}`);
|
||||
}
|
||||
if (steps.length || tools.length) {
|
||||
subtitleParts.push(`${steps.length} 个步骤 / ${tools.length} 次工具调用`);
|
||||
}
|
||||
if (session.totalDurationMs !== undefined && session.totalDurationMs !== null) {
|
||||
subtitleParts.push(`耗时 ${this.formatDuration(session.totalDurationMs)}`);
|
||||
}
|
||||
|
||||
this.outcomePanel.className = `outcome-panel ${this.outcomeClass(status, verdict)}`.trim();
|
||||
this.outcomeTitle.textContent = title;
|
||||
this.outcomeSubtitle.textContent = subtitleParts.length
|
||||
? subtitleParts.join(' · ')
|
||||
: '选择一个会话后,这里会先展示最终结论和证据状态。';
|
||||
this.outcomeVerdict.textContent = this.displayVerdict(verifier.verdict);
|
||||
this.outcomeEvidenceState.textContent = checked.length
|
||||
? `证据分 ${score},检查 ${checked.length} 个事实,${tracedFacts} 个事实带引用,${traceSummary.length} 条证据摘要。`
|
||||
: '暂无结构化校验事实。';
|
||||
this.outcomeAnswerSummary.textContent = this.summarizeAnswer(answer);
|
||||
this.renderOutcomeList(this.outcomeConfirmed, this.confirmedFacts(checked), '暂无直接确认的关键事实。');
|
||||
this.renderOutcomeList(this.outcomeGaps, this.evidenceGaps(checked, verdict), '没有明显证据缺口。');
|
||||
}
|
||||
|
||||
outcomeTitleFor(status, verdict) {
|
||||
if (!status && !verdict) {
|
||||
return '尚未加载会话';
|
||||
}
|
||||
if (status === 'FAILED') {
|
||||
return '会话执行失败';
|
||||
}
|
||||
if (verdict.includes('REJECT')) {
|
||||
return '结论被校验器拒绝';
|
||||
}
|
||||
if (verdict.includes('LOW_CONFID')) {
|
||||
return '结论低置信,需要复核';
|
||||
}
|
||||
if (verdict.includes('PASS')) {
|
||||
return '结论已通过校验';
|
||||
}
|
||||
if (status === 'SUCCESS') {
|
||||
return '会话已完成,暂无明确校验结论';
|
||||
}
|
||||
return '会话仍在处理中';
|
||||
}
|
||||
|
||||
outcomeClass(status, verdict) {
|
||||
if (status === 'FAILED' || verdict.includes('REJECT')) {
|
||||
return 'bad';
|
||||
}
|
||||
if (verdict.includes('LOW_CONFID') || status === 'RUNNING' || status === 'PENDING') {
|
||||
return 'warn';
|
||||
}
|
||||
if (status === 'SUCCESS' || verdict.includes('PASS')) {
|
||||
return 'ok';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
summarizeAnswer(answer) {
|
||||
const text = String(answer || '').trim();
|
||||
if (!text) {
|
||||
return '-';
|
||||
}
|
||||
const compact = text
|
||||
.replace(/\r/g, '')
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
return compact.length > 620 ? `${compact.slice(0, 620)}...` : compact;
|
||||
}
|
||||
|
||||
confirmedFacts(facts) {
|
||||
return facts
|
||||
.filter(fact => {
|
||||
const verification = String(fact.verification || '').toUpperCase();
|
||||
return fact.is_critical && (verification.includes('DIRECT') || verification.includes('SUPPORTED'));
|
||||
})
|
||||
.map(fact => fact.fact || fact.detail)
|
||||
.filter(Boolean)
|
||||
.slice(0, 4);
|
||||
}
|
||||
|
||||
evidenceGaps(facts, verdict) {
|
||||
const gaps = facts
|
||||
.filter(fact => {
|
||||
const verification = String(fact.verification || '').toUpperCase();
|
||||
return fact.is_critical && (
|
||||
verification.includes('NO_EVIDENCE')
|
||||
|| verification.includes('UNSUPPORTED')
|
||||
|| verification.includes('CONTRADICT')
|
||||
|| verification.includes('INDIRECT')
|
||||
);
|
||||
})
|
||||
.map(fact => fact.detail ? `${fact.fact}:${fact.detail}` : fact.fact)
|
||||
.filter(Boolean)
|
||||
.slice(0, 4);
|
||||
if (!gaps.length && String(verdict || '').toUpperCase().includes('LOW_CONFID')) {
|
||||
return ['校验器给出低置信结论,建议人工复核关键事实和证据引用。'];
|
||||
}
|
||||
return gaps;
|
||||
}
|
||||
|
||||
renderOutcomeList(node, items, emptyText) {
|
||||
node.innerHTML = (items.length ? items : [emptyText]).map(item => `
|
||||
<li>${this.escape(item)}</li>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
renderSteps(steps) {
|
||||
if (!steps.length) {
|
||||
this.stepsRail.innerHTML = '<div class="empty-panel">No persisted agent steps.</div>';
|
||||
this.stepsRail.innerHTML = '<div class="empty-panel">没有持久化智能体步骤。</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,18 +521,19 @@ class TraceWorkbench {
|
||||
return `
|
||||
<article class="step-entry ${cls}" data-index="${this.escape(String(index + 1).padStart(2, '0'))}">
|
||||
<div class="step-title">
|
||||
<h3>${this.escape(step.agentName || 'agent')}</h3>
|
||||
${this.renderBadge(step.hasToolCall ? 'tool' : 'model', step.hasToolCall ? 'warn' : 'ok')}
|
||||
<h3>${this.escape(step.agentName || '智能体')}</h3>
|
||||
${this.renderBadge(step.hasToolCall ? '工具' : '模型', step.hasToolCall ? 'warn' : 'ok')}
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-pill">step ${this.escape(step.stepIndex ?? index)}</span>
|
||||
<span class="meta-pill">执行序号 ${this.escape(String(index + 1).padStart(2, '0'))}</span>
|
||||
<span class="meta-pill">内部步 ${this.escape(step.stepIndex ?? index)}</span>
|
||||
<span class="meta-pill">${this.escape(this.formatDuration(step.durationMs))}</span>
|
||||
<span class="meta-pill">${this.escape(this.formatNumber(step.tokenCount))} tokens</span>
|
||||
<span class="meta-pill">${this.escape(this.formatNumber(step.tokenCount))} 令牌</span>
|
||||
<span class="meta-pill">${this.escape(this.formatDate(step.createdAt))}</span>
|
||||
</div>
|
||||
${this.renderTextDetail('Model input', step.modelInput)}
|
||||
${this.renderTextDetail('Model output', step.modelOutput)}
|
||||
${this.renderTextDetail('Thought', step.thought)}
|
||||
${this.renderTextDetail('模型输入', step.modelInput)}
|
||||
${this.renderTextDetail('模型输出', step.modelOutput)}
|
||||
${this.renderTextDetail('思考过程', step.thought)}
|
||||
</article>
|
||||
`;
|
||||
}).join('');
|
||||
@@ -178,9 +547,9 @@ class TraceWorkbench {
|
||||
|
||||
const groups = this.groupByTool(tools);
|
||||
const filters = [
|
||||
{ key: 'all', label: `all ${tools.length}` },
|
||||
{ key: 'success', label: `success ${tools.filter(tool => tool.success !== false).length}` },
|
||||
{ key: 'failed', label: `failed ${tools.filter(tool => tool.success === false).length}` },
|
||||
{ key: 'all', label: `全部 ${tools.length}` },
|
||||
{ key: 'success', label: `成功 ${tools.filter(tool => tool.success !== false).length}` },
|
||||
{ key: 'failed', label: `失败 ${tools.filter(tool => tool.success === false).length}` },
|
||||
...Object.entries(groups).map(([name, count]) => ({ key: `tool:${name}`, label: `${name} ${count}` }))
|
||||
];
|
||||
|
||||
@@ -202,7 +571,7 @@ class TraceWorkbench {
|
||||
renderTools(tools) {
|
||||
const filtered = tools.filter(tool => this.matchesFilter(tool));
|
||||
if (!filtered.length) {
|
||||
this.toolLedger.innerHTML = '<div class="empty-panel">No tool invocations match this filter.</div>';
|
||||
this.toolLedger.innerHTML = '<div class="empty-panel">当前筛选下没有工具调用。</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -212,13 +581,13 @@ class TraceWorkbench {
|
||||
return `
|
||||
<button class="tool-row ${failed ? 'failed' : ''} ${selected ? 'selected' : ''}" data-tool-id="${this.escape(tool.id)}">
|
||||
<div class="tool-title">
|
||||
<h3>${this.escape(tool.toolName || 'tool')}</h3>
|
||||
${this.renderBadge(failed ? 'failed' : 'ok', failed ? 'bad' : 'ok')}
|
||||
<h3>${this.escape(tool.toolName || '工具')}</h3>
|
||||
${this.renderBadge(failed ? '失败' : '正常', failed ? 'bad' : 'ok')}
|
||||
</div>
|
||||
<div class="tool-meta">
|
||||
<span>#${index + 1}</span>
|
||||
<span>id ${this.escape(tool.id ?? '-')}</span>
|
||||
<span>step ${this.escape(tool.stepId ?? '-')}</span>
|
||||
<span>ID ${this.escape(tool.id ?? '-')}</span>
|
||||
<span>步骤 ${this.escape(tool.stepId ?? '-')}</span>
|
||||
<span>${this.escape(this.formatDuration(tool.durationMs))}</span>
|
||||
${tool.retrievalLayer ? `<span>${this.escape(tool.retrievalLayer)}</span>` : ''}
|
||||
${tool.relevanceLevel ? `<span>${this.escape(tool.relevanceLevel)}</span>` : ''}
|
||||
@@ -251,19 +620,19 @@ class TraceWorkbench {
|
||||
renderBoundaryItems(counts) {
|
||||
return `
|
||||
<div class="boundary-item">
|
||||
<span class="field-label">Selected skill</span>
|
||||
<span class="field-label">选中技能</span>
|
||||
<strong>${this.escape(counts.selectedSkill)}</strong>
|
||||
</div>
|
||||
<div class="boundary-item">
|
||||
<span class="field-label">Planner mentions</span>
|
||||
<span class="field-label">规划器提及</span>
|
||||
<strong>${this.escape(counts.plannerReadSkill)}</strong>
|
||||
</div>
|
||||
<div class="boundary-item">
|
||||
<span class="field-label">Executor mentions</span>
|
||||
<span class="field-label">执行器提及</span>
|
||||
<strong>${this.escape(counts.executorReadSkill)}</strong>
|
||||
</div>
|
||||
<div class="boundary-item">
|
||||
<span class="field-label">Verifier mentions</span>
|
||||
<span class="field-label">校验器提及</span>
|
||||
<strong>${this.escape(counts.verifierReadSkill)}</strong>
|
||||
</div>
|
||||
`;
|
||||
@@ -271,7 +640,7 @@ class TraceWorkbench {
|
||||
|
||||
renderVerifier(verifier) {
|
||||
if (!verifier || !Object.keys(verifier).length) {
|
||||
this.verifierFacts.innerHTML = '<div class="empty-panel">No verifier evaluation in selfEvaluation.</div>';
|
||||
this.verifierFacts.innerHTML = '<div class="empty-panel">自评数据中没有校验结果。</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -279,15 +648,15 @@ class TraceWorkbench {
|
||||
const traceSummary = Array.isArray(verifier.tool_trace_summary) ? verifier.tool_trace_summary : [];
|
||||
const header = `
|
||||
<div class="meta-line">
|
||||
${this.renderBadge(verifier.verdict || 'verdict -', this.verdictClass(verifier.verdict))}
|
||||
<span class="meta-pill">score ${this.escape(this.formatScore(verifier.groundedness_score))}</span>
|
||||
<span class="meta-pill">facts ${facts.length}</span>
|
||||
<span class="meta-pill">trace refs ${traceSummary.length}</span>
|
||||
${this.renderBadge(this.displayVerdict(verifier.verdict), this.verdictClass(verifier.verdict))}
|
||||
<span class="meta-pill">证据分 ${this.escape(this.formatScore(verifier.groundedness_score))}</span>
|
||||
<span class="meta-pill">事实 ${facts.length}</span>
|
||||
<span class="meta-pill">引用 ${traceSummary.length}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!facts.length) {
|
||||
this.verifierFacts.innerHTML = header + '<div class="empty-panel">Verifier returned no facts_checked rows.</div>';
|
||||
this.verifierFacts.innerHTML = header + '<div class="empty-panel">校验器没有返回 facts_checked 明细。</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -297,9 +666,9 @@ class TraceWorkbench {
|
||||
<article class="fact-row">
|
||||
<p>${this.escape(fact.fact || '-')}</p>
|
||||
<div class="meta-line">
|
||||
${this.renderBadge(fact.verification || '-', this.factClass(fact.verification))}
|
||||
<span class="meta-pill">${fact.is_critical ? 'critical' : 'supporting'}</span>
|
||||
<span class="meta-pill">refs ${refs.length}</span>
|
||||
${this.renderBadge(this.displayFactVerification(fact.verification), this.factClass(fact.verification))}
|
||||
<span class="meta-pill">${fact.is_critical ? '关键事实' : '辅助事实'}</span>
|
||||
<span class="meta-pill">引用 ${refs.length}</span>
|
||||
</div>
|
||||
${fact.detail ? `<pre>${this.escape(fact.detail)}</pre>` : ''}
|
||||
${refs.length ? `<pre>${this.escape(this.prettyJson(refs))}</pre>` : ''}
|
||||
@@ -312,33 +681,33 @@ class TraceWorkbench {
|
||||
const tools = this.trace ? (this.trace.toolInvocations || []) : [];
|
||||
const tool = tools.find(item => String(item.id) === String(this.selectedToolId));
|
||||
if (!tool) {
|
||||
this.toolInspector.innerHTML = '<div class="empty-panel">No tool invocation selected.</div>';
|
||||
this.toolInspector.innerHTML = '<div class="empty-panel">尚未选择工具调用。</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const retrieval = tool.retrievalDetails || {};
|
||||
const ragFields = [
|
||||
['Input params', tool.inputParams || this.parseJson(tool.inputParamsRaw)],
|
||||
['Output preview', tool.outputPreview || '-'],
|
||||
['Retrieval layer', tool.retrievalLayer || '-'],
|
||||
['L0 matches', tool.l0MatchCount ?? '-'],
|
||||
['L1 matches', tool.l1MatchCount ?? '-'],
|
||||
['Relevance', tool.relevanceLevel || '-'],
|
||||
['Dedup reason', tool.dedupReason || '-'],
|
||||
['Query transform', retrieval.query_transform],
|
||||
['Retrieval trace', retrieval.retrieval_trace],
|
||||
['Context pack', retrieval.context_pack_summary],
|
||||
['Rerank trace', retrieval.rerank_trace],
|
||||
['Evidence blocks', retrieval.evidence_blocks]
|
||||
['输入参数', tool.inputParams || this.parseJson(tool.inputParamsRaw)],
|
||||
['输出预览', tool.outputPreview || '-'],
|
||||
['检索层级', tool.retrievalLayer || '-'],
|
||||
['L0 命中数', tool.l0MatchCount ?? '-'],
|
||||
['L1 命中数', tool.l1MatchCount ?? '-'],
|
||||
['相关性', tool.relevanceLevel || '-'],
|
||||
['去重原因', tool.dedupReason || '-'],
|
||||
['查询改写', retrieval.query_transform],
|
||||
['检索轨迹', retrieval.retrieval_trace],
|
||||
['上下文打包', retrieval.context_pack_summary],
|
||||
['重排轨迹', retrieval.rerank_trace],
|
||||
['证据块', retrieval.evidence_blocks]
|
||||
];
|
||||
|
||||
this.toolInspector.innerHTML = `
|
||||
<div class="meta-line">
|
||||
${this.renderBadge(tool.success === false ? 'failed' : 'ok', tool.success === false ? 'bad' : 'ok')}
|
||||
<span class="meta-pill">${this.escape(tool.toolName || 'tool')}</span>
|
||||
${this.renderBadge(tool.success === false ? '失败' : '正常', tool.success === false ? 'bad' : 'ok')}
|
||||
<span class="meta-pill">${this.escape(tool.toolName || '工具')}</span>
|
||||
<span class="meta-pill">${this.escape(this.formatDuration(tool.durationMs))}</span>
|
||||
<span class="meta-pill">output ${this.escape(this.formatNumber(tool.outputLength))}</span>
|
||||
${tool.truncated ? '<span class="meta-pill">truncated</span>' : ''}
|
||||
<span class="meta-pill">输出 ${this.escape(this.formatNumber(tool.outputLength))}</span>
|
||||
${tool.truncated ? '<span class="meta-pill">已截断</span>' : ''}
|
||||
</div>
|
||||
${tool.errorMessage ? `<pre>${this.escape(tool.errorMessage)}</pre>` : ''}
|
||||
<div class="field-grid">
|
||||
@@ -373,6 +742,50 @@ class TraceWorkbench {
|
||||
return `<span class="badge ${this.escape(cls || 'ok')}">${this.escape(text)}</span>`;
|
||||
}
|
||||
|
||||
displayStatus(status) {
|
||||
if (!status) {
|
||||
return '-';
|
||||
}
|
||||
const statuses = {
|
||||
SUCCESS: '成功',
|
||||
FAILED: '失败',
|
||||
RUNNING: '运行中',
|
||||
PENDING: '等待中'
|
||||
};
|
||||
return statuses[String(status).toUpperCase()] || String(status);
|
||||
}
|
||||
|
||||
displayVerdict(verdict) {
|
||||
if (!verdict) {
|
||||
return '-';
|
||||
}
|
||||
const verdicts = {
|
||||
PASS: '通过',
|
||||
REJECT: '拒绝',
|
||||
LOW_CONFID: '低置信',
|
||||
LOW_CONFIDENCE: '低置信',
|
||||
REVIEW: '需复核'
|
||||
};
|
||||
return verdicts[String(verdict).toUpperCase()] || String(verdict);
|
||||
}
|
||||
|
||||
displayFactVerification(verification) {
|
||||
if (!verification) {
|
||||
return '-';
|
||||
}
|
||||
const verifications = {
|
||||
DIRECT_EVIDENCE: '直接证据',
|
||||
INDIRECT_SUPPORT: '间接支持',
|
||||
NO_EVIDENCE: '无证据',
|
||||
UNSUPPORTED: '未支持',
|
||||
CONTRADICTED: '有矛盾',
|
||||
SUPPORTED: '已支持',
|
||||
PASS: '通过',
|
||||
REJECT: '拒绝'
|
||||
};
|
||||
return verifications[String(verification).toUpperCase()] || String(verification);
|
||||
}
|
||||
|
||||
getVerifier() {
|
||||
const evaluation = (this.trace && this.trace.session && this.trace.session.selfEvaluation) || {};
|
||||
if (evaluation.verifier_evaluation) {
|
||||
|
||||
Reference in New Issue
Block a user