class TraceWorkbench {
constructor() {
this.trace = null;
this.selectedToolId = null;
this.activeFilter = 'all';
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');
this.toolLedger = document.getElementById('toolLedger');
this.toolFilters = document.getElementById('toolFilters');
this.skillBoundary = document.getElementById('skillBoundary');
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();
}
bindEvents() {
this.sessionForm.addEventListener('submit', (event) => {
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') || this.getLastTraceSessionId();
if (sessionId) {
this.sessionIdInput.value = sessionId;
this.loadTrace(sessionId);
return;
}
this.renderEmpty();
}
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 = '
没有匹配的最近会话
';
return;
}
this.sessionOptions.innerHTML = this.filteredSessionCandidates.map((candidate, index) => `
`).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 {
const response = await fetch(`/api/diagnosis/${encodeURIComponent(sessionId)}/trace`);
const data = await this.handleResponse(response);
this.trace = data;
this.selectedToolId = data.toolInvocations && data.toolInvocations.length
? data.toolInvocations[0].id
: null;
this.activeFilter = 'all';
try {
localStorage.setItem('lastTraceSessionId', sessionId);
} catch (error) {
// ignore storage failures
}
this.renderTrace();
this.setState(`已加载 ${sessionId}。`, '');
} catch (error) {
this.trace = null;
this.renderEmpty();
this.setState(error.message, 'error');
} finally {
this.loadButton.disabled = false;
}
}
async handleResponse(response) {
let payload = null;
try {
payload = await response.json();
} catch (error) {
throw new Error(`HTTP ${response.status}: 响应不是 JSON`);
}
if (!response.ok || payload.code !== 200) {
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);
window.history.replaceState({}, '', url.toString());
}
renderTrace() {
const session = this.trace.session || {};
const verifier = this.getVerifier();
const summary = this.trace.summary || {};
const steps = this.trace.steps || [];
const tools = this.trace.toolInvocations || [];
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 ?? '-'}`);
this.text('summaryDuration', this.formatDuration(session.totalDurationMs));
this.text('summaryTokens', this.formatNumber(session.totalTokenCount));
this.text('stepCount', steps.length);
this.text('toolCount', tools.length);
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 || '未持久化最终回答。';
}
renderEmpty() {
this.text('summaryStatus', '-');
this.text('summaryVerdict', '-');
this.text('summaryScore', '-');
this.text('summarySteps', '-');
this.text('summaryTools', '-');
this.text('summaryDuration', '-');
this.text('summaryTokens', '-');
this.text('stepCount', '0');
this.text('toolCount', '0');
this.text('inspectorMode', '空闲');
this.renderOutcome({}, {}, [], []);
this.stepsRail.innerHTML = '尚未加载诊断链路。
';
this.toolFilters.innerHTML = '';
this.toolLedger.innerHTML = '尚未加载工具调用。
';
this.skillBoundary.innerHTML = this.renderBoundaryItems({
selectedSkill: '-',
plannerReadSkill: 0,
executorReadSkill: 0,
verifierReadSkill: 0
});
this.verifierFacts.innerHTML = '尚未加载校验结果。
';
this.toolInspector.innerHTML = '加载诊断链路后选择一个工具调用。
';
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 => `
${this.escape(item)}
`).join('');
}
renderSteps(steps) {
if (!steps.length) {
this.stepsRail.innerHTML = '没有持久化智能体步骤。
';
return;
}
this.stepsRail.innerHTML = steps.map((step, index) => {
const agent = (step.agentName || 'agent').toLowerCase();
const cls = this.agentClass(agent);
return `
${this.escape(step.agentName || '智能体')}
${this.renderBadge(step.hasToolCall ? '工具' : '模型', step.hasToolCall ? 'warn' : 'ok')}
执行序号 ${this.escape(String(index + 1).padStart(2, '0'))}
内部步 ${this.escape(step.stepIndex ?? index)}
${this.escape(this.formatDuration(step.durationMs))}
${this.escape(this.formatNumber(step.tokenCount))} 令牌
${this.escape(this.formatDate(step.createdAt))}
${this.renderTextDetail('模型输入', step.modelInput)}
${this.renderTextDetail('模型输出', step.modelOutput)}
${this.renderTextDetail('思考过程', step.thought)}
`;
}).join('');
}
renderToolFilters(tools) {
if (!tools.length) {
this.toolFilters.innerHTML = '';
return;
}
const groups = this.groupByTool(tools);
const filters = [
{ 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}` }))
];
this.toolFilters.innerHTML = filters.map(filter => `
`).join('');
this.toolFilters.querySelectorAll('button').forEach((button) => {
button.addEventListener('click', () => {
this.activeFilter = button.dataset.filter;
this.renderToolFilters(this.trace.toolInvocations || []);
this.renderTools(this.trace.toolInvocations || []);
});
});
}
renderTools(tools) {
const filtered = tools.filter(tool => this.matchesFilter(tool));
if (!filtered.length) {
this.toolLedger.innerHTML = '当前筛选下没有工具调用。
';
return;
}
this.toolLedger.innerHTML = filtered.map((tool, index) => {
const failed = tool.success === false;
const selected = String(tool.id) === String(this.selectedToolId);
return `
`;
}).join('');
this.toolLedger.querySelectorAll('.tool-row').forEach((row) => {
row.addEventListener('click', () => {
this.selectedToolId = row.dataset.toolId;
this.renderTools(this.trace.toolInvocations || []);
this.renderToolInspector();
});
});
}
renderSkillBoundary() {
const steps = this.trace.steps || [];
const selectedSkill = this.extractSelectedSkill(steps);
const counts = {
selectedSkill: selectedSkill || '-',
plannerReadSkill: this.countReadSkill(steps, 'planner'),
executorReadSkill: this.countReadSkill(steps, 'executor'),
verifierReadSkill: this.countReadSkill(steps, 'verifier')
};
this.skillBoundary.innerHTML = this.renderBoundaryItems(counts);
}
renderBoundaryItems(counts) {
return `
选中技能
${this.escape(counts.selectedSkill)}
规划器提及
${this.escape(counts.plannerReadSkill)}
执行器提及
${this.escape(counts.executorReadSkill)}
校验器提及
${this.escape(counts.verifierReadSkill)}
`;
}
renderVerifier(verifier) {
if (!verifier || !Object.keys(verifier).length) {
this.verifierFacts.innerHTML = '自评数据中没有校验结果。
';
return;
}
const facts = Array.isArray(verifier.facts_checked) ? verifier.facts_checked : [];
const traceSummary = Array.isArray(verifier.tool_trace_summary) ? verifier.tool_trace_summary : [];
const header = `
${this.renderBadge(this.displayVerdict(verifier.verdict), this.verdictClass(verifier.verdict))}
证据分 ${this.escape(this.formatScore(verifier.groundedness_score))}
事实 ${facts.length}
引用 ${traceSummary.length}
`;
if (!facts.length) {
this.verifierFacts.innerHTML = header + '校验器没有返回 facts_checked 明细。
';
return;
}
this.verifierFacts.innerHTML = header + facts.map((fact) => {
const refs = Array.isArray(fact.evidence_refs) ? fact.evidence_refs : [];
return `
${this.escape(fact.fact || '-')}
${this.renderBadge(this.displayFactVerification(fact.verification), this.factClass(fact.verification))}
${fact.is_critical ? '关键事实' : '辅助事实'}
引用 ${refs.length}
${fact.detail ? `${this.escape(fact.detail)}` : ''}
${refs.length ? `${this.escape(this.prettyJson(refs))}` : ''}
`;
}).join('');
}
renderToolInspector() {
const tools = this.trace ? (this.trace.toolInvocations || []) : [];
const tool = tools.find(item => String(item.id) === String(this.selectedToolId));
if (!tool) {
this.toolInspector.innerHTML = '尚未选择工具调用。
';
return;
}
const retrieval = tool.retrievalDetails || {};
const ragFields = [
['输入参数', 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 = `
${this.renderBadge(tool.success === false ? '失败' : '正常', tool.success === false ? 'bad' : 'ok')}
${this.escape(tool.toolName || '工具')}
${this.escape(this.formatDuration(tool.durationMs))}
输出 ${this.escape(this.formatNumber(tool.outputLength))}
${tool.truncated ? '已截断' : ''}
${tool.errorMessage ? `${this.escape(tool.errorMessage)}` : ''}
${ragFields.map(([label, value]) => this.renderField(label, value)).join('')}
`;
}
renderField(label, value) {
const empty = value === undefined || value === null || value === '';
return `
${this.escape(label)}
${this.escape(empty ? '-' : this.stringifyValue(value))}
`;
}
renderTextDetail(label, text) {
if (!text) {
return '';
}
return `
${this.escape(label)}
${this.escape(text)}
`;
}
renderBadge(text, cls) {
return `${this.escape(text)}`;
}
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) {
return evaluation.verifier_evaluation;
}
if (evaluation.verdict || evaluation.groundedness_score || evaluation.facts_checked) {
return evaluation;
}
return {};
}
extractSelectedSkill(steps) {
const plannerText = steps
.filter(step => (step.agentName || '').toLowerCase().includes('planner'))
.map(step => `${step.modelOutput || ''}\n${step.thought || ''}`)
.join('\n');
const allText = steps
.map(step => `${step.modelInput || ''}\n${step.modelOutput || ''}\n${step.thought || ''}`)
.join('\n');
const sourceText = `${plannerText}\n${allText}`;
const direct = sourceText.match(/"selected_skill"\s*:\s*"([^"]+)"/);
if (direct) {
return direct[1];
}
const skillName = sourceText.match(/"skill_name"\s*:\s*"([^"]+)"/);
if (skillName) {
return skillName[1];
}
const loose = sourceText.match(/selected_skill\s*[:=]\s*([A-Za-z0-9_.-]+)/);
return loose ? loose[1] : '';
}
countReadSkill(steps, agentName) {
const text = steps
.filter(step => (step.agentName || '').toLowerCase().includes(agentName))
.map(step => `${step.modelInput || ''}\n${step.modelOutput || ''}\n${step.thought || ''}`)
.join('\n');
const matches = text.match(/\bread_skill\b/g);
return matches ? matches.length : 0;
}
matchesFilter(tool) {
if (this.activeFilter === 'all') {
return true;
}
if (this.activeFilter === 'success') {
return tool.success !== false;
}
if (this.activeFilter === 'failed') {
return tool.success === false;
}
if (this.activeFilter.startsWith('tool:')) {
return tool.toolName === this.activeFilter.slice(5);
}
return true;
}
groupByTool(tools) {
return tools.reduce((groups, tool) => {
const name = tool.toolName || 'tool';
groups[name] = (groups[name] || 0) + 1;
return groups;
}, {});
}
agentClass(agent) {
if (agent.includes('planner')) {
return 'planner';
}
if (agent.includes('executor')) {
return 'executor';
}
if (agent.includes('verifier')) {
return 'verifier';
}
return '';
}
verdictClass(verdict) {
if (!verdict) {
return 'warn';
}
const value = String(verdict).toUpperCase();
if (value.includes('PASS')) {
return 'ok';
}
if (value.includes('REJECT')) {
return 'bad';
}
return 'warn';
}
factClass(verification) {
if (!verification) {
return 'warn';
}
const value = String(verification).toUpperCase();
if (value.includes('SUPPORTED') || value.includes('PASS')) {
return 'ok';
}
if (value.includes('UNSUPPORTED') || value.includes('CONTRADICT')) {
return 'bad';
}
return 'warn';
}
parseJson(raw) {
if (!raw || typeof raw !== 'string') {
return raw;
}
try {
return JSON.parse(raw);
} catch (error) {
return raw;
}
}
stringifyValue(value) {
if (typeof value === 'string') {
return value;
}
return this.prettyJson(value);
}
prettyJson(value) {
try {
return JSON.stringify(value, null, 2);
} catch (error) {
return String(value);
}
}
formatDuration(ms) {
if (ms === undefined || ms === null || ms === '') {
return '-';
}
if (ms < 1000) {
return `${ms}ms`;
}
const seconds = ms / 1000;
if (seconds < 60) {
return `${seconds.toFixed(1)}s`;
}
const minutes = Math.floor(seconds / 60);
const remaining = Math.round(seconds % 60);
return `${minutes}m ${remaining}s`;
}
formatNumber(value) {
if (value === undefined || value === null || value === '') {
return '-';
}
return Number(value).toLocaleString('en-US');
}
formatScore(value) {
if (value === undefined || value === null || value === '') {
return '-';
}
const number = Number(value);
return Number.isFinite(number) ? number.toFixed(2) : String(value);
}
formatDate(value) {
if (!value) {
return '-';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return String(value);
}
return date.toLocaleString('en-US', { hour12: false });
}
text(id, value) {
const node = document.getElementById(id);
if (node) {
node.textContent = value;
}
}
setState(message, statusClass) {
this.stateLine.textContent = message;
this.stateLine.className = `state-line ${statusClass || ''}`.trim();
}
escape(value) {
return String(value)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
}
document.addEventListener('DOMContentLoaded', () => {
new TraceWorkbench();
});