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(); });