kiku-agent 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +4 -12
- package/server/agent/eval/AGENTS.md +228 -0
- package/server/agent/eval/analyze.py +684 -0
- package/server/agent/eval/gates.json +15 -0
- package/server/agent/eval/modules/analysis/compare.js +198 -0
- package/server/agent/eval/modules/analysis/gates.js +72 -0
- package/server/agent/eval/modules/analysis/judge.js +70 -0
- package/server/agent/eval/modules/core/cli.js +453 -0
- package/server/agent/eval/modules/core/config.js +60 -0
- package/server/agent/eval/modules/core/store.js +236 -0
- package/server/agent/eval/modules/evaluation/cases.js +689 -0
- package/server/agent/eval/modules/evaluation/runner.js +298 -0
- package/server/agent/eval/modules/evaluation/scoring.js +351 -0
- package/server/agent/eval/modules/evaluation/worker.js +122 -0
- package/server/agent/eval/modules/reporting/benchmark.js +265 -0
- package/server/agent/eval/modules/reporting/benchmarkPng.js +487 -0
- package/server/agent/eval/modules/reporting/dashboard.js +1406 -0
- package/server/agent/eval/modules/server/evalApi.js +1099 -0
- package/server/agent/eval/modules/ui/evalUi.js +4796 -0
- package/server/agent/eval/modules/utils/smokes.js +119 -0
- package/server/agent/tests/agentBubbleSystem.test.js +65 -0
- package/server/agent/tests/agentFollowUps.test.js +127 -0
- package/server/agent/tests/apiRouter.test.js +108 -0
- package/server/agent/tests/architectureFixes.test.js +81 -0
- package/server/agent/tests/architectureRefactoring.test.js +194 -0
- package/server/agent/tests/autoResearch.test.js +64 -0
- package/server/agent/tests/bilibiliEmbed.test.js +251 -0
- package/server/agent/tests/catalogSync.test.js +130 -0
- package/server/agent/tests/citations.test.js +180 -0
- package/server/agent/tests/clarificationAnalytics.test.js +219 -0
- package/server/agent/tests/clarifyingQuestions.test.js +594 -0
- package/server/agent/tests/cliAutomation.test.js +121 -0
- package/server/agent/tests/cloudProviders.test.js +234 -0
- package/server/agent/tests/commandInput.test.js +24 -0
- package/server/agent/tests/commands.test.js +417 -0
- package/server/agent/tests/contextIntelligence.test.js +162 -0
- package/server/agent/tests/conversationSafety.test.js +53 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +135 -0
- package/server/agent/tests/dramaAnswerQuality.test.js +73 -0
- package/server/agent/tests/dynamicCatalogIngestion.test.js +285 -0
- package/server/agent/tests/dynamicRoutingAndSearch.test.js +92 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +49 -0
- package/server/agent/tests/entertainmentMetrics.test.js +126 -0
- package/server/agent/tests/envBootstrap.test.js +87 -0
- package/server/agent/tests/envCommand.test.js +215 -0
- package/server/agent/tests/errorHandling.test.js +80 -0
- package/server/agent/tests/errors.test.js +13 -0
- package/server/agent/tests/evalAnalyze.test.js +54 -0
- package/server/agent/tests/evalCommand.test.js +54 -0
- package/server/agent/tests/evalCorpus.test.js +126 -0
- package/server/agent/tests/evalHarness.test.js +62 -0
- package/server/agent/tests/evalJudgeContext.test.js +38 -0
- package/server/agent/tests/evalMetrics.test.js +312 -0
- package/server/agent/tests/evalStore.test.js +190 -0
- package/server/agent/tests/evalStudio.test.js +179 -0
- package/server/agent/tests/feedbackLearning.test.js +186 -0
- package/server/agent/tests/firstRun.test.js +44 -0
- package/server/agent/tests/gemma4eOptimization.test.js +95 -0
- package/server/agent/tests/hardwareTiers.test.js +206 -0
- package/server/agent/tests/hybridWebSearch.test.js +192 -0
- package/server/agent/tests/judgeService.test.js +251 -0
- package/server/agent/tests/lanDiscovery.test.js +319 -0
- package/server/agent/tests/mcpClient.test.js +201 -0
- package/server/agent/tests/modelManager.test.js +109 -0
- package/server/agent/tests/multilingualScenarios.test.js +69 -0
- package/server/agent/tests/newCommands.test.js +347 -0
- package/server/agent/tests/newCommandsTui.test.js +141 -0
- package/server/agent/tests/newTools.test.js +113 -0
- package/server/agent/tests/newsIntelligence.test.js +178 -0
- package/server/agent/tests/nextGenArchitecture.test.js +99 -0
- package/server/agent/tests/offlineInput.test.js +217 -0
- package/server/agent/tests/packagingGuard.test.js +57 -0
- package/server/agent/tests/presentation.test.js +61 -0
- package/server/agent/tests/providerPersistence.test.js +39 -0
- package/server/agent/tests/queryBenchmarks.test.js +83 -0
- package/server/agent/tests/queryDetails.test.js +126 -0
- package/server/agent/tests/realWorldValidation.test.js +391 -0
- package/server/agent/tests/resilienceAndConcurrency.test.js +117 -0
- package/server/agent/tests/runtimeWatchdog.test.js +32 -0
- package/server/agent/tests/sanitizer.test.js +37 -0
- package/server/agent/tests/scenariosData.js +378 -0
- package/server/agent/tests/schemaRouting.test.js +54 -0
- package/server/agent/tests/securityAuditRemediation.test.js +214 -0
- package/server/agent/tests/securityHardening.test.js +146 -0
- package/server/agent/tests/securitySanitizer.test.js +38 -0
- package/server/agent/tests/sessionAndCopy.test.js +232 -0
- package/server/agent/tests/sidebarControls.test.js +101 -0
- package/server/agent/tests/statefulMemory.test.js +270 -0
- package/server/agent/tests/subagent.test.js +216 -0
- package/server/agent/tests/subagentProgress.test.js +109 -0
- package/server/agent/tests/subagentTextFallback.test.js +115 -0
- package/server/agent/tests/suggestions.test.js +199 -0
- package/server/agent/tests/suggestionsTui.test.js +251 -0
- package/server/agent/tests/telemetryAndProfiles.test.js +179 -0
- package/server/agent/tests/terminalImage.test.js +80 -0
- package/server/agent/tests/terminalMarkdown.test.js +166 -0
- package/server/agent/tests/terminalText.test.js +133 -0
- package/server/agent/tests/textProtocolLoop.test.js +126 -0
- package/server/agent/tests/textToolParsing.test.js +139 -0
- package/server/agent/tests/theme.test.js +26 -0
- package/server/agent/tests/toolRegistryParity.test.js +53 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +119 -0
- package/server/agent/tests/tools.test.js +259 -0
- package/server/agent/tests/unifiedEndpoints.test.js +152 -0
- package/server/agent/tests/v110Features.test.js +262 -0
- package/server/agent/tests/versionBump.test.js +69 -0
- package/server/agent/tests/vramEstimator.test.js +520 -0
- package/server/agent/tests/webChatInspection.test.js +300 -0
- package/server/agent/tests/webChatMemory.test.js +112 -0
- package/server/agent/tests/webChatSecurity.test.js +169 -0
- package/server/agent/tests/webExtract.test.js +261 -0
- package/server/agent/tests/webSources.test.js +247 -0
- package/server/eval/judgeRubrics.js +110 -0
- package/server/eval/judgeService.js +290 -0
- package/server/eval/runBenchmark.js +182 -0
- package/server/mcp/examples/librechat.example.yaml +16 -0
- package/server/mcp/tests/connection.test.js +156 -0
- package/server/supabase_schema.sql +63 -0
|
@@ -0,0 +1,1406 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { allCases, cases } from '../evaluation/cases.js';
|
|
6
|
+
import {
|
|
7
|
+
evaluateToolCorrectness,
|
|
8
|
+
detectTrajectoryLoops,
|
|
9
|
+
computeLatencyBreakdown,
|
|
10
|
+
checkGroundedness,
|
|
11
|
+
summarize
|
|
12
|
+
} from '../evaluation/scoring.js';
|
|
13
|
+
import { diffReports } from '../analysis/compare.js';
|
|
14
|
+
import { BENCHMARK_ROWS, computeBenchmarkSummary } from './benchmark.js';
|
|
15
|
+
import { REPO_ROOT } from '../core/config.js';
|
|
16
|
+
|
|
17
|
+
const caseLookup = new Map(allCases.map(c => [c.id, c]));
|
|
18
|
+
|
|
19
|
+
export function enrichEvaluationReport(report) {
|
|
20
|
+
if (!report || !Array.isArray(report.rows)) return report;
|
|
21
|
+
|
|
22
|
+
for (const row of report.rows) {
|
|
23
|
+
const c = caseLookup.get(row.id) || cases.find(item => item.id === row.id) || { id: row.id, groups: [] };
|
|
24
|
+
if (!row.toolCorrectness) {
|
|
25
|
+
row.toolCorrectness = evaluateToolCorrectness(c, row.trace);
|
|
26
|
+
}
|
|
27
|
+
if (!row.trajectoryHealth) {
|
|
28
|
+
row.trajectoryHealth = detectTrajectoryLoops(row.trace);
|
|
29
|
+
}
|
|
30
|
+
if (!row.latencyBreakdown) {
|
|
31
|
+
row.latencyBreakdown = computeLatencyBreakdown(row);
|
|
32
|
+
}
|
|
33
|
+
if (!row.groundedness && row.status === 'completed') {
|
|
34
|
+
row.groundedness = checkGroundedness(c, row.answer, row.trace);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const planned = report.metadata?.caseIds?.length || report.rows.length;
|
|
39
|
+
report.summary = summarize(report.rows, planned);
|
|
40
|
+
return report;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function generateDashboardHtml(report, compareReport = null) {
|
|
44
|
+
enrichEvaluationReport(report);
|
|
45
|
+
if (compareReport) enrichEvaluationReport(compareReport);
|
|
46
|
+
const { metadata = {}, summary = {}, rows = [] } = report;
|
|
47
|
+
|
|
48
|
+
let comparison = null;
|
|
49
|
+
if (compareReport) {
|
|
50
|
+
try {
|
|
51
|
+
comparison = diffReports(compareReport, report);
|
|
52
|
+
} catch {
|
|
53
|
+
comparison = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const escapeHtml = str => String(str ?? '')
|
|
58
|
+
.replace(/&/g, '&')
|
|
59
|
+
.replace(/</g, '<')
|
|
60
|
+
.replace(/>/g, '>')
|
|
61
|
+
.replace(/"/g, '"')
|
|
62
|
+
.replace(/'/g, ''');
|
|
63
|
+
|
|
64
|
+
const deltaBadge = (metricKey, isRate = true) => {
|
|
65
|
+
if (!comparison) return '';
|
|
66
|
+
const d = comparison.metricDeltas?.[metricKey]?.delta;
|
|
67
|
+
if (d === null || d === undefined || !Number.isFinite(d)) return '';
|
|
68
|
+
if (Math.abs(d) < 0.0001) return '<span class="delta-badge delta-neutral">±0%</span>';
|
|
69
|
+
const isGood = d > 0;
|
|
70
|
+
const sign = d > 0 ? '+' : '';
|
|
71
|
+
const formatted = isRate ? `${sign}${(d * 100).toFixed(1)}%` : `${sign}${d.toFixed(2)}`;
|
|
72
|
+
const cls = isGood ? 'delta-good' : 'delta-bad';
|
|
73
|
+
const arrow = isGood ? '▲' : '▼';
|
|
74
|
+
return `<span class="delta-badge ${cls}">${arrow} ${formatted}</span>`;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const passRate = summary.lexicalCoverageRate !== null ? Math.round(summary.lexicalCoverageRate * 100) : 0;
|
|
78
|
+
const toolRate = summary.toolAccuracyRate !== null ? Math.round(summary.toolAccuracyRate * 100) : 0;
|
|
79
|
+
const loopFreeRate = summary.loopFreeRate !== null ? Math.round(summary.loopFreeRate * 100) : 100;
|
|
80
|
+
const groundedRate = summary.groundednessRate !== null ? Math.round(summary.groundednessRate * 100) : 100;
|
|
81
|
+
const medianSec = summary.medianMs ? (summary.medianMs / 1000).toFixed(2) : '-';
|
|
82
|
+
const p95Sec = summary.p95Ms ? (summary.p95Ms / 1000).toFixed(2) : '-';
|
|
83
|
+
const meanLlmSec = summary.meanLlmMs ? (summary.meanLlmMs / 1000).toFixed(2) : '-';
|
|
84
|
+
const meanToolSec = summary.meanToolMs ? (summary.meanToolMs / 1000).toFixed(2) : '-';
|
|
85
|
+
const decodeTps = summary.meanDecodeTps ?? '-';
|
|
86
|
+
const bench = summary.benchmark && summary.benchmark.values ? summary.benchmark : computeBenchmarkSummary(rows, summary);
|
|
87
|
+
const formatObserved = row => {
|
|
88
|
+
const v = bench.values[row.key];
|
|
89
|
+
if (v === null || v === undefined) return '-';
|
|
90
|
+
if (row.format === 'percent') return `${(v * 100).toFixed(1)}%`;
|
|
91
|
+
if (row.format === 'cost') return `$${Number(v).toFixed(3)}`;
|
|
92
|
+
return Number(v).toFixed(2);
|
|
93
|
+
};
|
|
94
|
+
let benchmarkTableRowsHtml = '';
|
|
95
|
+
for (const bRow of BENCHMARK_ROWS) {
|
|
96
|
+
const observed = formatObserved(bRow);
|
|
97
|
+
const st = bench.status[bRow.key];
|
|
98
|
+
const badge = st === null ? '<span class="badge badge-category">N-A</span>' : (st ? '<span class="badge badge-success">Pass</span>' : '<span class="badge badge-danger">Fail</span>');
|
|
99
|
+
benchmarkTableRowsHtml += `<tr><td><strong>${escapeHtml(bRow.dimension)}</strong><div class="bench-sub">${escapeHtml(bRow.iso)} · ${escapeHtml(bRow.nist)}</div></td><td>${escapeHtml(bRow.indicator)}</td><td>${escapeHtml(bRow.benchmark)}</td><td>${escapeHtml(bRow.target)}</td><td><strong>${escapeHtml(observed)}</strong></td><td>${badge}</td></tr>`;
|
|
100
|
+
}
|
|
101
|
+
let failureBarsHtml = '';
|
|
102
|
+
for (const key of Object.keys(bench.failureDistribution || {})) {
|
|
103
|
+
const f = bench.failureDistribution[key];
|
|
104
|
+
const pct = Math.round((f.rate || 0) * 1000) / 10;
|
|
105
|
+
const blocks = Math.round((f.rate || 0) * 20);
|
|
106
|
+
const bar = `${'█'.repeat(blocks)}${'░'.repeat(Math.max(0, 20 - blocks))}`;
|
|
107
|
+
failureBarsHtml += `<div class="failure-row"><div class="failure-label">${escapeHtml(f.label)}</div><div class="failure-bar">${bar}</div><div class="failure-pct">${pct.toFixed(1)}% (${f.count})</div></div>`;
|
|
108
|
+
}
|
|
109
|
+
const paretoW = 920;
|
|
110
|
+
const paretoH = 300;
|
|
111
|
+
const paretoX = c => 60 + Math.min(Math.max(Number(c) || 0, 0), 0.08) * (820 / 0.08);
|
|
112
|
+
const paretoY = s => 270 - ((Math.min(Math.max(Number(s) || 0, 40), 100) - 40) * (240 / 60));
|
|
113
|
+
const paretoColors = { candidate: '#34d399', baseline: '#fbbf24', legacy: '#94a3b8', live: '#f43f5e' };
|
|
114
|
+
let paretoDotsHtml = '';
|
|
115
|
+
for (const p of (bench.pareto || [])) {
|
|
116
|
+
const cx = paretoX(p.cost).toFixed(1);
|
|
117
|
+
const cy = paretoY(p.successRate).toFixed(1);
|
|
118
|
+
const fill = paretoColors[p.kind] || '#e2e8f0';
|
|
119
|
+
paretoDotsHtml += `<circle cx="${cx}" cy="${cy}" r="${p.kind === 'live' ? 8 : 6}" fill="${fill}" stroke="#0b0f19" stroke-width="2"><title>${escapeHtml(p.label)}: ${escapeHtml(String(p.successRate))}% at $${escapeHtml(String(p.cost))}</title></circle><text x="${Number(cx) + 10}" y="${Number(cy) - 8}" class="svg-val">${escapeHtml(p.label)} (${escapeHtml(String(p.successRate))}%, $${escapeHtml(String(p.cost))})</text>`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
const validRows = rows.filter(r => r.status === 'completed' && r.durationMs > 0);
|
|
124
|
+
const maxDuration = Math.max(...validRows.map(r => r.durationMs || 1000), 1000);
|
|
125
|
+
const chartWidth = 960;
|
|
126
|
+
const barHeight = 14;
|
|
127
|
+
const gap = 6;
|
|
128
|
+
const chartHeight = Math.max(220, validRows.length * (barHeight + gap) + 40);
|
|
129
|
+
|
|
130
|
+
let latencySvgBars = '';
|
|
131
|
+
validRows.forEach((r, idx) => {
|
|
132
|
+
const y = 30 + idx * (barHeight + gap);
|
|
133
|
+
const totalW = Math.max(4, Math.round((r.durationMs / maxDuration) * (chartWidth - 180)));
|
|
134
|
+
const toolMs = r.latencyBreakdown?.toolExecutionMs || 0;
|
|
135
|
+
const toolW = Math.min(totalW, Math.round((toolMs / maxDuration) * (chartWidth - 180)));
|
|
136
|
+
const llmW = Math.max(0, totalW - toolW);
|
|
137
|
+
|
|
138
|
+
latencySvgBars += `
|
|
139
|
+
<g class="chart-row" data-id="${escapeHtml(r.id)}">
|
|
140
|
+
<text x="10" y="${y + 11}" class="svg-label">${escapeHtml(r.id)}</text>
|
|
141
|
+
<rect x="65" y="${y}" width="${llmW}" height="${barHeight}" rx="3" fill="#818cf8" class="bar-llm">
|
|
142
|
+
<title>${escapeHtml(r.id)} LLM Time: ${Math.round(r.latencyBreakdown?.llmInferenceMs || 0)}ms</title>
|
|
143
|
+
</rect>
|
|
144
|
+
<rect x="${65 + llmW}" y="${y}" width="${toolW}" height="${barHeight}" rx="3" fill="#f43f5e" class="bar-tool">
|
|
145
|
+
<title>${escapeHtml(r.id)} Tool Time: ${Math.round(toolMs)}ms</title>
|
|
146
|
+
</rect>
|
|
147
|
+
<text x="${72 + totalW}" y="${y + 11}" class="svg-val">${(r.durationMs / 1000).toFixed(2)}s</text>
|
|
148
|
+
</g>
|
|
149
|
+
`;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const catStats = summary.categoryStats || {};
|
|
153
|
+
let categoryCardsHtml = '';
|
|
154
|
+
for (const [cat, data] of Object.entries(catStats)) {
|
|
155
|
+
const catPass = data.completed > 0 ? Math.round((data.factsMatched / data.completed) * 100) : 0;
|
|
156
|
+
const toolDenom = data.toolEligible ?? data.completed;
|
|
157
|
+
const catTools = toolDenom > 0 ? Math.round((data.toolsCorrect / toolDenom) * 100) : 0;
|
|
158
|
+
categoryCardsHtml += `
|
|
159
|
+
<div class="category-card">
|
|
160
|
+
<div class="category-header">
|
|
161
|
+
<span class="category-name">${escapeHtml(cat.toUpperCase())}</span>
|
|
162
|
+
<span class="category-count">${data.completed}/${data.total} cases</span>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="category-metrics">
|
|
165
|
+
<div class="metric-row">
|
|
166
|
+
<span>Lexical Pass:</span>
|
|
167
|
+
<strong>${catPass}%</strong>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="progress-bar"><div class="progress-fill" style="width: ${catPass}%"></div></div>
|
|
170
|
+
<div class="metric-row">
|
|
171
|
+
<span>Tool Accuracy:</span>
|
|
172
|
+
<strong>${catTools}%</strong>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="progress-bar progress-tool"><div class="progress-fill" style="width: ${catTools}%"></div></div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const langStats = summary.languageStats || {};
|
|
181
|
+
let languageCardsHtml = '';
|
|
182
|
+
for (const [lang, data] of Object.entries(langStats)) {
|
|
183
|
+
const langPass = data.completed > 0 ? Math.round((data.factsMatched / data.completed) * 100) : 0;
|
|
184
|
+
const toolDenom = data.toolEligible ?? data.completed;
|
|
185
|
+
const langTools = toolDenom > 0 ? Math.round((data.toolsCorrect / toolDenom) * 100) : 0;
|
|
186
|
+
const avgLatency = data.meanMs ? `${(data.meanMs / 1000).toFixed(2)}s` : '-';
|
|
187
|
+
languageCardsHtml += `
|
|
188
|
+
<div class="category-card language-card">
|
|
189
|
+
<div class="category-header">
|
|
190
|
+
<span class="category-name">LANG: ${escapeHtml(lang.toUpperCase())}</span>
|
|
191
|
+
<span class="category-count">${data.completed}/${data.total} cases (${avgLatency})</span>
|
|
192
|
+
</div>
|
|
193
|
+
<div class="category-metrics">
|
|
194
|
+
<div class="metric-row">
|
|
195
|
+
<span>Coverage:</span>
|
|
196
|
+
<strong>${langPass}%</strong>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="progress-bar"><div class="progress-fill" style="width: ${langPass}%"></div></div>
|
|
199
|
+
<div class="metric-row">
|
|
200
|
+
<span>Tool Accuracy:</span>
|
|
201
|
+
<strong>${langTools}%</strong>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="progress-bar progress-tool"><div class="progress-fill" style="width: ${langTools}%"></div></div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const baseRowMap = compareReport ? new Map(compareReport.rows?.map(r => [r.id, r]) || []) : null;
|
|
210
|
+
|
|
211
|
+
let tableRowsHtml = '';
|
|
212
|
+
rows.forEach((r, idx) => {
|
|
213
|
+
const isCompleted = r.status === 'completed';
|
|
214
|
+
const allMatched = r.coverage?.allMatched;
|
|
215
|
+
const hasLoop = r.trajectoryHealth?.hasLoop;
|
|
216
|
+
const statusBadge = isCompleted
|
|
217
|
+
? (allMatched ? '<span class="badge badge-success">PASS</span>' : '<span class="badge badge-warning">PARTIAL</span>')
|
|
218
|
+
: '<span class="badge badge-danger">ERROR</span>';
|
|
219
|
+
|
|
220
|
+
let compareBadge = '';
|
|
221
|
+
if (baseRowMap && baseRowMap.has(r.id)) {
|
|
222
|
+
const bRow = baseRowMap.get(r.id);
|
|
223
|
+
const bPass = bRow.status === 'completed' && bRow.coverage?.allMatched === true;
|
|
224
|
+
const cPass = isCompleted && allMatched === true;
|
|
225
|
+
if (bPass && !cPass) {
|
|
226
|
+
compareBadge = '<span class="badge badge-regression">REGRESSION</span>';
|
|
227
|
+
} else if (!bPass && cPass) {
|
|
228
|
+
compareBadge = '<span class="badge badge-improved">IMPROVED</span>';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const toolsUsed = [...new Set(r.trace?.filter(t => t.event === 'start').map(t => t.name) || [])];
|
|
233
|
+
const toolsHtml = toolsUsed.length
|
|
234
|
+
? toolsUsed.map(t => `<span class="tag-tool">${escapeHtml(t)}</span>`).join('')
|
|
235
|
+
: '<span class="tag-empty">None</span>';
|
|
236
|
+
|
|
237
|
+
const loopBadge = hasLoop ? '<span class="badge badge-loop">LOOP</span>' : '';
|
|
238
|
+
const durationStr = r.durationMs ? `${(r.durationMs / 1000).toFixed(2)}s` : '-';
|
|
239
|
+
|
|
240
|
+
tableRowsHtml += `
|
|
241
|
+
<tr class="case-row" onclick="toggleDetails('row-detail-${idx}')" id="row-${idx}" data-category="${escapeHtml(r.category)}" data-language="${escapeHtml(r.language)}" data-status="${allMatched ? 'pass' : 'fail'}" data-has-loop="${hasLoop ? 'true' : 'false'}">
|
|
242
|
+
<td class="col-id"><strong>${escapeHtml(r.id)}</strong></td>
|
|
243
|
+
<td><span class="badge badge-category">${escapeHtml(r.category)}</span></td>
|
|
244
|
+
<td><span class="badge badge-lang">${escapeHtml((r.language || '').toUpperCase())}</span></td>
|
|
245
|
+
<td class="col-question" title="${escapeHtml(r.question)}">${escapeHtml(r.question)}</td>
|
|
246
|
+
<td>${statusBadge} ${loopBadge} ${compareBadge}</td>
|
|
247
|
+
<td><div class="tool-tags-cell">${toolsHtml}</div></td>
|
|
248
|
+
<td class="col-duration">${durationStr}</td>
|
|
249
|
+
</tr>
|
|
250
|
+
<tr class="detail-row" id="row-detail-${idx}" style="display: none;">
|
|
251
|
+
<td colspan="7">
|
|
252
|
+
<div class="detail-drawer">
|
|
253
|
+
<div class="detail-grid">
|
|
254
|
+
<div class="detail-pane">
|
|
255
|
+
<h4>Reference & Fact Matching</h4>
|
|
256
|
+
<p><strong>Question:</strong> ${escapeHtml(r.question)}</p>
|
|
257
|
+
<div class="fact-coverage-box">
|
|
258
|
+
<strong>Matched Facts:</strong> ${r.coverage?.matched ?? 0} / ${r.coverage?.total ?? 0}
|
|
259
|
+
${r.coverage?.missing?.length ? `<div class="missing-facts">[FAIL] Missing: ${escapeHtml(JSON.stringify(r.coverage.missing))}</div>` : '<div class="all-facts-ok">[OK] All target aliases identified</div>'}
|
|
260
|
+
</div>
|
|
261
|
+
<h4 style="margin-top: 14px;">Latency & Execution Breakdown</h4>
|
|
262
|
+
<div class="latency-stat-row">
|
|
263
|
+
<div>LLM Thought & Gen: <strong>${Math.round(r.latencyBreakdown?.llmInferenceMs || 0)}ms</strong> (${r.latencyBreakdown?.llmPercentage || 0}%)</div>
|
|
264
|
+
<div>Tool Execution: <strong>${Math.round(r.latencyBreakdown?.toolExecutionMs || 0)}ms</strong> (${r.latencyBreakdown?.toolPercentage || 0}%)</div>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="dual-progress">
|
|
267
|
+
<div class="dp-llm" style="width: ${r.latencyBreakdown?.llmPercentage || 0}%"></div>
|
|
268
|
+
<div class="dp-tool" style="width: ${r.latencyBreakdown?.toolPercentage || 0}%"></div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
<div class="detail-pane">
|
|
272
|
+
<h4>Agent Final Response</h4>
|
|
273
|
+
<div class="answer-box">${escapeHtml(r.answer || r.error || 'No answer generated')}</div>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<h4 style="margin-top: 16px;">Full Trajectory Trace Events (${r.trace?.length || 0} events)</h4>
|
|
278
|
+
<div class="trace-timeline">
|
|
279
|
+
${(r.trace || []).map((t, stepIdx) => `
|
|
280
|
+
<div class="trace-event trace-${escapeHtml(t.event)}">
|
|
281
|
+
<div class="trace-badge">${stepIdx + 1}. [${escapeHtml(t.event.toUpperCase())}] ${escapeHtml(t.name || '')} ${t.durationMs ? `<span class="trace-time">${t.durationMs}ms</span>` : ''}</div>
|
|
282
|
+
${t.parameters ? `<pre class="trace-json">Params: ${escapeHtml(JSON.stringify(t.parameters, null, 2))}</pre>` : ''}
|
|
283
|
+
${t.value ? `<pre class="trace-json trace-val">Output: ${escapeHtml(typeof t.value === 'object' ? JSON.stringify(t.value, null, 2) : String(t.value).slice(0, 300))}</pre>` : ''}
|
|
284
|
+
${t.message ? `<div class="trace-msg">${escapeHtml(t.message)}</div>` : ''}
|
|
285
|
+
</div>
|
|
286
|
+
`).join('')}
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
</td>
|
|
290
|
+
</tr>
|
|
291
|
+
`;
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return `<!DOCTYPE html>
|
|
295
|
+
<html lang="en">
|
|
296
|
+
<head>
|
|
297
|
+
<meta charset="UTF-8">
|
|
298
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
299
|
+
<title>Kiku Agent Evaluation & Performance Dashboard</title>
|
|
300
|
+
<link rel="icon" href="/favicon.ico">
|
|
301
|
+
<link rel="stylesheet" href="/fonts/google-sans-official.css">
|
|
302
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
303
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
304
|
+
<link href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;600;700&family=Google+Sans+Text:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
305
|
+
<style>
|
|
306
|
+
:root {
|
|
307
|
+
--bg: #0b0f19;
|
|
308
|
+
--card-bg: rgba(22, 27, 46, 0.85);
|
|
309
|
+
--card-border: rgba(255, 255, 255, 0.08);
|
|
310
|
+
--text: #f1f5f9;
|
|
311
|
+
--text-muted: #94a3b8;
|
|
312
|
+
--primary: #f43f5e;
|
|
313
|
+
--primary-glow: rgba(244, 63, 94, 0.35);
|
|
314
|
+
--accent-purple: #818cf8;
|
|
315
|
+
--success: #10b981;
|
|
316
|
+
--warning: #f59e0b;
|
|
317
|
+
--danger: #ef4444;
|
|
318
|
+
--font: 'Google Sans', 'Google Sans Text', 'Product Sans', 'Noto Sans Thai', 'Noto Sans SC', system-ui, -apple-system, sans-serif;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
* { box-sizing: border-box; margin: 0; padding: 0; font-family: var(--font); }
|
|
322
|
+
html {
|
|
323
|
+
scroll-behavior: smooth;
|
|
324
|
+
-webkit-text-size-adjust: 100%;
|
|
325
|
+
max-width: 100vw;
|
|
326
|
+
overflow-x: hidden;
|
|
327
|
+
}
|
|
328
|
+
body {
|
|
329
|
+
background: var(--bg);
|
|
330
|
+
color: var(--text);
|
|
331
|
+
font-family: var(--font);
|
|
332
|
+
line-height: 1.5;
|
|
333
|
+
padding: clamp(12px, 3vw, 24px);
|
|
334
|
+
-webkit-font-smoothing: antialiased;
|
|
335
|
+
-webkit-overflow-scrolling: touch;
|
|
336
|
+
max-width: 100vw;
|
|
337
|
+
overflow-x: hidden;
|
|
338
|
+
overflow-wrap: break-word;
|
|
339
|
+
word-break: break-word;
|
|
340
|
+
}
|
|
341
|
+
::-webkit-scrollbar {
|
|
342
|
+
width: 6px;
|
|
343
|
+
height: 6px;
|
|
344
|
+
}
|
|
345
|
+
::-webkit-scrollbar-track {
|
|
346
|
+
background: rgba(0, 0, 0, 0.2);
|
|
347
|
+
}
|
|
348
|
+
::-webkit-scrollbar-thumb {
|
|
349
|
+
background: rgba(244, 63, 94, 0.35);
|
|
350
|
+
border-radius: 999px;
|
|
351
|
+
}
|
|
352
|
+
::-webkit-scrollbar-thumb:hover {
|
|
353
|
+
background: var(--primary);
|
|
354
|
+
}
|
|
355
|
+
* {
|
|
356
|
+
scrollbar-width: thin;
|
|
357
|
+
scrollbar-color: rgba(244, 63, 94, 0.35) rgba(0, 0, 0, 0.2);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.dashboard-container {
|
|
361
|
+
max-width: 1280px;
|
|
362
|
+
margin: 0 auto;
|
|
363
|
+
width: 100%;
|
|
364
|
+
min-width: 0;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.header-bar {
|
|
368
|
+
display: flex;
|
|
369
|
+
justify-content: space-between;
|
|
370
|
+
align-items: center;
|
|
371
|
+
padding-bottom: 20px;
|
|
372
|
+
border-bottom: 1px solid var(--card-border);
|
|
373
|
+
margin-bottom: 24px;
|
|
374
|
+
flex-wrap: wrap;
|
|
375
|
+
gap: 16px;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.title-area {
|
|
379
|
+
flex: 1 1 300px;
|
|
380
|
+
min-width: 0;
|
|
381
|
+
}
|
|
382
|
+
.title-area h1 {
|
|
383
|
+
font-size: clamp(20px, 4.5vw, 26px);
|
|
384
|
+
font-weight: 800;
|
|
385
|
+
background: linear-gradient(135deg, #fff 30%, var(--primary) 100%);
|
|
386
|
+
-webkit-background-clip: text;
|
|
387
|
+
-webkit-text-fill-color: transparent;
|
|
388
|
+
display: flex;
|
|
389
|
+
align-items: center;
|
|
390
|
+
gap: 10px;
|
|
391
|
+
flex-wrap: wrap;
|
|
392
|
+
line-height: 1.25;
|
|
393
|
+
word-break: break-word;
|
|
394
|
+
overflow-wrap: break-word;
|
|
395
|
+
hyphens: auto;
|
|
396
|
+
}
|
|
397
|
+
.title-area p {
|
|
398
|
+
color: var(--text-muted);
|
|
399
|
+
font-size: clamp(12px, 2.5vw, 13px);
|
|
400
|
+
margin-top: 6px;
|
|
401
|
+
line-height: 1.45;
|
|
402
|
+
word-break: break-word;
|
|
403
|
+
overflow-wrap: break-word;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.header-meta {
|
|
407
|
+
display: flex;
|
|
408
|
+
gap: 12px;
|
|
409
|
+
font-size: 12px;
|
|
410
|
+
color: var(--text-muted);
|
|
411
|
+
background: var(--card-bg);
|
|
412
|
+
padding: 8px 14px;
|
|
413
|
+
border-radius: 10px;
|
|
414
|
+
border: 1px solid var(--card-border);
|
|
415
|
+
flex-wrap: wrap;
|
|
416
|
+
}
|
|
417
|
+
.meta-tag strong { color: var(--text); }
|
|
418
|
+
|
|
419
|
+
.kpi-grid {
|
|
420
|
+
display: grid;
|
|
421
|
+
grid-template-columns: repeat(auto-fit, minmax(clamp(140px, 45vw, 190px), 1fr));
|
|
422
|
+
gap: 12px;
|
|
423
|
+
margin-bottom: 24px;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.kpi-card {
|
|
427
|
+
background: var(--card-bg);
|
|
428
|
+
border: 1px solid var(--card-border);
|
|
429
|
+
border-radius: 14px;
|
|
430
|
+
padding: clamp(12px, 2.5vw, 18px);
|
|
431
|
+
position: relative;
|
|
432
|
+
overflow: hidden;
|
|
433
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
434
|
+
min-width: 0;
|
|
435
|
+
}
|
|
436
|
+
.kpi-card::before {
|
|
437
|
+
content: '';
|
|
438
|
+
position: absolute;
|
|
439
|
+
top: 0; left: 0; right: 0; height: 3px;
|
|
440
|
+
background: linear-gradient(90deg, var(--primary), var(--accent-purple));
|
|
441
|
+
}
|
|
442
|
+
.kpi-label {
|
|
443
|
+
font-size: 11px;
|
|
444
|
+
font-weight: 600;
|
|
445
|
+
text-transform: uppercase;
|
|
446
|
+
letter-spacing: 0.05em;
|
|
447
|
+
color: var(--text-muted);
|
|
448
|
+
display: flex;
|
|
449
|
+
align-items: center;
|
|
450
|
+
gap: 6px;
|
|
451
|
+
flex-wrap: wrap;
|
|
452
|
+
word-break: break-word;
|
|
453
|
+
}
|
|
454
|
+
.kpi-value {
|
|
455
|
+
font-size: clamp(20px, 4.5vw, 26px);
|
|
456
|
+
font-weight: 800;
|
|
457
|
+
margin-top: 6px;
|
|
458
|
+
color: #fff;
|
|
459
|
+
display: flex;
|
|
460
|
+
align-items: center;
|
|
461
|
+
flex-wrap: wrap;
|
|
462
|
+
gap: 6px;
|
|
463
|
+
word-break: break-word;
|
|
464
|
+
}
|
|
465
|
+
.kpi-sub {
|
|
466
|
+
font-size: 11px;
|
|
467
|
+
color: var(--text-muted);
|
|
468
|
+
margin-top: 3px;
|
|
469
|
+
word-break: break-word;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.delta-badge {
|
|
473
|
+
font-size: 11px;
|
|
474
|
+
font-weight: 700;
|
|
475
|
+
padding: 2px 6px;
|
|
476
|
+
border-radius: 6px;
|
|
477
|
+
display: inline-flex;
|
|
478
|
+
align-items: center;
|
|
479
|
+
gap: 2px;
|
|
480
|
+
flex-shrink: 0;
|
|
481
|
+
}
|
|
482
|
+
.delta-good { background: rgba(16, 185, 129, 0.2); color: #34d399; }
|
|
483
|
+
.delta-bad { background: rgba(239, 68, 68, 0.2); color: #f87171; }
|
|
484
|
+
.delta-neutral { background: rgba(148, 163, 184, 0.2); color: #94a3b8; }
|
|
485
|
+
.badge-regression { background: rgba(239, 68, 68, 0.25); color: #f87171; border: 1px solid #ef4444; }
|
|
486
|
+
.badge-improved { background: rgba(16, 185, 129, 0.25); color: #34d399; border: 1px solid #10b981; }
|
|
487
|
+
|
|
488
|
+
.section-card {
|
|
489
|
+
background: var(--card-bg);
|
|
490
|
+
border: 1px solid var(--card-border);
|
|
491
|
+
border-radius: 14px;
|
|
492
|
+
padding: clamp(14px, 3vw, 20px);
|
|
493
|
+
margin-bottom: 24px;
|
|
494
|
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
|
495
|
+
min-width: 0;
|
|
496
|
+
overflow: hidden;
|
|
497
|
+
}
|
|
498
|
+
.section-title {
|
|
499
|
+
font-size: clamp(14px, 3.2vw, 16px);
|
|
500
|
+
font-weight: 700;
|
|
501
|
+
margin-bottom: 16px;
|
|
502
|
+
display: flex;
|
|
503
|
+
align-items: center;
|
|
504
|
+
gap: 8px;
|
|
505
|
+
flex-wrap: wrap;
|
|
506
|
+
word-break: break-word;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.category-grid {
|
|
510
|
+
display: grid;
|
|
511
|
+
grid-template-columns: repeat(auto-fit, minmax(clamp(150px, 45vw, 220px), 1fr));
|
|
512
|
+
gap: 12px;
|
|
513
|
+
}
|
|
514
|
+
.category-card {
|
|
515
|
+
background: rgba(15, 20, 36, 0.65);
|
|
516
|
+
border: 1px solid var(--card-border);
|
|
517
|
+
border-radius: 10px;
|
|
518
|
+
padding: 12px;
|
|
519
|
+
min-width: 0;
|
|
520
|
+
overflow: hidden;
|
|
521
|
+
}
|
|
522
|
+
.language-card {
|
|
523
|
+
border-left: 3px solid var(--accent-purple);
|
|
524
|
+
}
|
|
525
|
+
.category-header {
|
|
526
|
+
display: flex;
|
|
527
|
+
justify-content: space-between;
|
|
528
|
+
margin-bottom: 10px;
|
|
529
|
+
gap: 6px;
|
|
530
|
+
}
|
|
531
|
+
.category-name { font-weight: 700; font-size: 12px; color: #cbd5e1; word-break: break-word; }
|
|
532
|
+
.category-count { font-size: 11px; color: var(--text-muted); flex-shrink: 0; }
|
|
533
|
+
.metric-row {
|
|
534
|
+
display: flex;
|
|
535
|
+
justify-content: space-between;
|
|
536
|
+
font-size: 11.5px;
|
|
537
|
+
margin-top: 6px;
|
|
538
|
+
color: var(--text-muted);
|
|
539
|
+
gap: 6px;
|
|
540
|
+
}
|
|
541
|
+
.metric-row strong { color: #f8fafc; }
|
|
542
|
+
.progress-bar {
|
|
543
|
+
height: 6px;
|
|
544
|
+
background: rgba(255, 255, 255, 0.08);
|
|
545
|
+
border-radius: 999px;
|
|
546
|
+
overflow: hidden;
|
|
547
|
+
margin-top: 4px;
|
|
548
|
+
}
|
|
549
|
+
.progress-fill {
|
|
550
|
+
height: 100%;
|
|
551
|
+
background: linear-gradient(90deg, #10b981, #059669);
|
|
552
|
+
border-radius: 999px;
|
|
553
|
+
}
|
|
554
|
+
.progress-tool .progress-fill {
|
|
555
|
+
background: linear-gradient(90deg, var(--accent-purple), #6366f1);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.chart-legend {
|
|
559
|
+
display: flex;
|
|
560
|
+
gap: 16px;
|
|
561
|
+
margin-bottom: 14px;
|
|
562
|
+
font-size: 12px;
|
|
563
|
+
flex-wrap: wrap;
|
|
564
|
+
}
|
|
565
|
+
.legend-item { display: flex; align-items: center; gap: 6px; }
|
|
566
|
+
.legend-box { width: 12px; height: 12px; border-radius: 3px; }
|
|
567
|
+
.svg-scroll-wrap {
|
|
568
|
+
overflow-x: auto;
|
|
569
|
+
max-height: 480px;
|
|
570
|
+
overflow-y: auto;
|
|
571
|
+
-webkit-overflow-scrolling: touch;
|
|
572
|
+
max-width: 100%;
|
|
573
|
+
}
|
|
574
|
+
.svg-label { fill: #94a3b8; font-size: 11px; font-weight: 600; font-family: var(--font); }
|
|
575
|
+
.svg-val { fill: #cbd5e1; font-size: 10px; font-family: var(--font); }
|
|
576
|
+
.bar-llm { transition: opacity 0.2s ease; cursor: pointer; }
|
|
577
|
+
.bar-tool { transition: opacity 0.2s ease; cursor: pointer; }
|
|
578
|
+
.chart-row:hover .bar-llm, .chart-row:hover .bar-tool { opacity: 0.8; }
|
|
579
|
+
|
|
580
|
+
.table-toolbar {
|
|
581
|
+
display: flex;
|
|
582
|
+
justify-content: space-between;
|
|
583
|
+
gap: 10px;
|
|
584
|
+
margin-bottom: 14px;
|
|
585
|
+
flex-wrap: wrap;
|
|
586
|
+
}
|
|
587
|
+
.search-box {
|
|
588
|
+
flex: 1 1 200px;
|
|
589
|
+
min-width: 0;
|
|
590
|
+
position: relative;
|
|
591
|
+
}
|
|
592
|
+
.search-box input {
|
|
593
|
+
width: 100%;
|
|
594
|
+
background: rgba(15, 20, 36, 0.8);
|
|
595
|
+
border: 1px solid var(--card-border);
|
|
596
|
+
border-radius: 8px;
|
|
597
|
+
padding: 8px 12px;
|
|
598
|
+
color: #fff;
|
|
599
|
+
font-size: 13px;
|
|
600
|
+
outline: none;
|
|
601
|
+
}
|
|
602
|
+
.search-box input:focus {
|
|
603
|
+
border-color: var(--primary);
|
|
604
|
+
}
|
|
605
|
+
.filter-group {
|
|
606
|
+
display: flex;
|
|
607
|
+
gap: 6px;
|
|
608
|
+
flex-wrap: wrap;
|
|
609
|
+
max-width: 100%;
|
|
610
|
+
}
|
|
611
|
+
.filter-btn {
|
|
612
|
+
background: rgba(255, 255, 255, 0.05);
|
|
613
|
+
border: 1px solid var(--card-border);
|
|
614
|
+
color: var(--text-muted);
|
|
615
|
+
padding: 6px 12px;
|
|
616
|
+
border-radius: 8px;
|
|
617
|
+
font-size: 12px;
|
|
618
|
+
cursor: pointer;
|
|
619
|
+
font-weight: 600;
|
|
620
|
+
transition: all 0.2s ease;
|
|
621
|
+
white-space: nowrap;
|
|
622
|
+
}
|
|
623
|
+
.filter-btn:hover { color: #fff; background: rgba(255, 255, 255, 0.1); }
|
|
624
|
+
.filter-btn.active {
|
|
625
|
+
background: linear-gradient(135deg, var(--primary), #e11d48);
|
|
626
|
+
color: #fff;
|
|
627
|
+
border-color: transparent;
|
|
628
|
+
box-shadow: 0 0 10px var(--primary-glow);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.table-wrap {
|
|
632
|
+
overflow-x: auto;
|
|
633
|
+
-webkit-overflow-scrolling: touch;
|
|
634
|
+
border: 1px solid var(--card-border);
|
|
635
|
+
border-radius: 10px;
|
|
636
|
+
max-width: 100%;
|
|
637
|
+
}
|
|
638
|
+
table {
|
|
639
|
+
width: 100%;
|
|
640
|
+
min-width: 620px;
|
|
641
|
+
border-collapse: collapse;
|
|
642
|
+
font-size: 12.5px;
|
|
643
|
+
text-align: left;
|
|
644
|
+
}
|
|
645
|
+
th {
|
|
646
|
+
background: rgba(15, 20, 36, 0.9);
|
|
647
|
+
color: var(--text-muted);
|
|
648
|
+
font-weight: 600;
|
|
649
|
+
padding: 10px 14px;
|
|
650
|
+
border-bottom: 1px solid var(--card-border);
|
|
651
|
+
white-space: nowrap;
|
|
652
|
+
}
|
|
653
|
+
td {
|
|
654
|
+
padding: 12px 14px;
|
|
655
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
|
656
|
+
color: #cbd5e1;
|
|
657
|
+
}
|
|
658
|
+
.case-row { cursor: pointer; transition: background 0.15s ease; }
|
|
659
|
+
.case-row:hover { background: rgba(255, 255, 255, 0.03); }
|
|
660
|
+
|
|
661
|
+
.badge {
|
|
662
|
+
display: inline-block;
|
|
663
|
+
padding: 2px 7px;
|
|
664
|
+
border-radius: 6px;
|
|
665
|
+
font-size: 10.5px;
|
|
666
|
+
font-weight: 700;
|
|
667
|
+
text-transform: uppercase;
|
|
668
|
+
white-space: nowrap;
|
|
669
|
+
}
|
|
670
|
+
.badge-success { background: rgba(16, 185, 129, 0.18); color: #34d399; }
|
|
671
|
+
.badge-warning { background: rgba(245, 158, 11, 0.18); color: #fbbf24; }
|
|
672
|
+
.badge-danger { background: rgba(239, 68, 68, 0.18); color: #f87171; }
|
|
673
|
+
.badge-category { background: rgba(255, 255, 255, 0.08); color: #94a3b8; }
|
|
674
|
+
.badge-lang { background: rgba(129, 140, 248, 0.18); color: #a5b4fc; }
|
|
675
|
+
.badge-loop { background: rgba(244, 63, 94, 0.2); color: #fb7185; }
|
|
676
|
+
|
|
677
|
+
.tool-tags-cell { display: flex; flex-wrap: wrap; gap: 4px; max-width: 320px; }
|
|
678
|
+
.tag-tool {
|
|
679
|
+
background: rgba(255, 255, 255, 0.06);
|
|
680
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
681
|
+
color: #e2e8f0;
|
|
682
|
+
padding: 2px 6px;
|
|
683
|
+
border-radius: 4px;
|
|
684
|
+
font-size: 10.5px;
|
|
685
|
+
font-family: monospace;
|
|
686
|
+
word-break: break-all;
|
|
687
|
+
}
|
|
688
|
+
.tag-empty { color: #64748b; font-style: italic; font-size: 11px; }
|
|
689
|
+
|
|
690
|
+
.detail-row td { background: rgba(11, 15, 25, 0.95); padding: 0; }
|
|
691
|
+
.detail-drawer { padding: clamp(12px, 2.5vw, 18px); border-bottom: 2px solid var(--primary); min-width: 0; }
|
|
692
|
+
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 14px; }
|
|
693
|
+
.detail-pane {
|
|
694
|
+
background: rgba(22, 27, 46, 0.5);
|
|
695
|
+
border: 1px solid var(--card-border);
|
|
696
|
+
border-radius: 8px;
|
|
697
|
+
padding: 12px;
|
|
698
|
+
min-width: 0;
|
|
699
|
+
overflow: hidden;
|
|
700
|
+
}
|
|
701
|
+
.detail-pane h4 { font-size: 13px; color: var(--accent-purple); margin-bottom: 8px; font-weight: 700; word-break: break-word; }
|
|
702
|
+
.fact-coverage-box {
|
|
703
|
+
margin-top: 8px;
|
|
704
|
+
padding: 8px 10px;
|
|
705
|
+
background: rgba(0, 0, 0, 0.25);
|
|
706
|
+
border-radius: 6px;
|
|
707
|
+
font-size: 12px;
|
|
708
|
+
word-break: break-word;
|
|
709
|
+
overflow-wrap: break-word;
|
|
710
|
+
}
|
|
711
|
+
.missing-facts { color: #f87171; margin-top: 4px; font-family: monospace; font-size: 11px; word-break: break-all; }
|
|
712
|
+
.all-facts-ok { color: #34d399; margin-top: 4px; font-weight: 600; word-break: break-word; }
|
|
713
|
+
.answer-box {
|
|
714
|
+
max-height: 180px;
|
|
715
|
+
overflow-y: auto;
|
|
716
|
+
overflow-x: hidden;
|
|
717
|
+
font-size: 12px;
|
|
718
|
+
color: #e2e8f0;
|
|
719
|
+
white-space: pre-wrap;
|
|
720
|
+
word-break: break-word;
|
|
721
|
+
overflow-wrap: break-word;
|
|
722
|
+
background: rgba(0, 0, 0, 0.3);
|
|
723
|
+
padding: 10px;
|
|
724
|
+
border-radius: 6px;
|
|
725
|
+
}
|
|
726
|
+
.latency-stat-row { display: flex; justify-content: space-between; font-size: 11.5px; margin-bottom: 6px; gap: 6px; }
|
|
727
|
+
.dual-progress { display: flex; height: 6px; border-radius: 999px; overflow: hidden; background: rgba(255, 255, 255, 0.08); }
|
|
728
|
+
.dp-llm { background: #818cf8; }
|
|
729
|
+
.dp-tool { background: #f43f5e; }
|
|
730
|
+
|
|
731
|
+
.trace-timeline {
|
|
732
|
+
max-height: 280px;
|
|
733
|
+
overflow-y: auto;
|
|
734
|
+
border: 1px solid var(--card-border);
|
|
735
|
+
border-radius: 8px;
|
|
736
|
+
padding: 10px;
|
|
737
|
+
background: rgba(0, 0, 0, 0.3);
|
|
738
|
+
}
|
|
739
|
+
.trace-event {
|
|
740
|
+
padding: 6px 8px;
|
|
741
|
+
margin-bottom: 6px;
|
|
742
|
+
border-left: 3px solid #64748b;
|
|
743
|
+
background: rgba(255, 255, 255, 0.02);
|
|
744
|
+
border-radius: 0 4px 4px 0;
|
|
745
|
+
font-size: 11px;
|
|
746
|
+
word-break: break-word;
|
|
747
|
+
overflow-wrap: break-word;
|
|
748
|
+
}
|
|
749
|
+
.trace-start { border-left-color: #38bdf8; }
|
|
750
|
+
.trace-result { border-left-color: #34d399; }
|
|
751
|
+
.trace-error { border-left-color: #f87171; }
|
|
752
|
+
.trace-badge { font-weight: 700; color: #f1f5f9; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 4px; }
|
|
753
|
+
.trace-time { color: var(--text-muted); font-weight: normal; }
|
|
754
|
+
.trace-json { font-size: 10.5px; color: #cbd5e1; margin-top: 4px; overflow-x: auto; font-family: monospace; word-break: break-all; white-space: pre-wrap; }
|
|
755
|
+
.bench-sub { font-size: 10.5px; color: var(--text-muted); font-weight: 400; margin-top: 2px; }
|
|
756
|
+
.bench-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
|
757
|
+
.formula-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(clamp(160px, 45vw, 240px), 1fr)); gap: 12px; margin-top: 14px; }
|
|
758
|
+
.formula-card { background: rgba(0, 0, 0, 0.3); border: 1px solid var(--card-border); border-radius: 8px; padding: 12px; font-size: 12px; min-width: 0; }
|
|
759
|
+
.formula-card strong { color: #a5b4fc; display: block; margin-bottom: 6px; word-break: break-word; }
|
|
760
|
+
.formula-card code { font-family: monospace; color: #e2e8f0; font-size: 11.5px; word-break: break-all; }
|
|
761
|
+
.failure-row { display: grid; grid-template-columns: 170px 1fr 110px; gap: 10px; align-items: center; font-size: 12px; padding: 6px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.04); min-width: 0; }
|
|
762
|
+
.failure-label { color: #cbd5e1; font-weight: 600; word-break: break-word; }
|
|
763
|
+
.failure-bar { font-family: monospace; color: #fb7185; letter-spacing: 1px; overflow: hidden; white-space: nowrap; }
|
|
764
|
+
.failure-pct { color: var(--text-muted); text-align: right; font-variant-numeric: tabular-nums; flex-shrink: 0; }
|
|
765
|
+
.pareto-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; max-width: 100%; }
|
|
766
|
+
.pareto-axis { stroke: rgba(255, 255, 255, 0.2); stroke-width: 1; }
|
|
767
|
+
|
|
768
|
+
@media (max-width: 768px) {
|
|
769
|
+
.header-bar {
|
|
770
|
+
flex-direction: column;
|
|
771
|
+
align-items: stretch;
|
|
772
|
+
gap: 14px;
|
|
773
|
+
}
|
|
774
|
+
.title-area h1 {
|
|
775
|
+
font-size: clamp(20px, 5.5vw, 24px);
|
|
776
|
+
}
|
|
777
|
+
.header-meta {
|
|
778
|
+
width: 100%;
|
|
779
|
+
display: grid;
|
|
780
|
+
grid-template-columns: repeat(2, 1fr);
|
|
781
|
+
gap: 8px;
|
|
782
|
+
box-sizing: border-box;
|
|
783
|
+
}
|
|
784
|
+
.detail-grid {
|
|
785
|
+
grid-template-columns: 1fr;
|
|
786
|
+
}
|
|
787
|
+
.failure-row {
|
|
788
|
+
grid-template-columns: 1fr auto;
|
|
789
|
+
gap: 8px;
|
|
790
|
+
}
|
|
791
|
+
.failure-bar {
|
|
792
|
+
display: none;
|
|
793
|
+
}
|
|
794
|
+
.table-toolbar {
|
|
795
|
+
flex-direction: column;
|
|
796
|
+
}
|
|
797
|
+
.search-box {
|
|
798
|
+
width: 100%;
|
|
799
|
+
}
|
|
800
|
+
.filter-group {
|
|
801
|
+
overflow-x: auto;
|
|
802
|
+
flex-wrap: nowrap;
|
|
803
|
+
padding-bottom: 4px;
|
|
804
|
+
-webkit-overflow-scrolling: touch;
|
|
805
|
+
max-width: 100%;
|
|
806
|
+
}
|
|
807
|
+
.filter-btn {
|
|
808
|
+
flex-shrink: 0;
|
|
809
|
+
}
|
|
810
|
+
.formula-grid {
|
|
811
|
+
grid-template-columns: 1fr;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
@media (max-width: 480px) {
|
|
816
|
+
body {
|
|
817
|
+
padding: 10px 8px;
|
|
818
|
+
}
|
|
819
|
+
.title-area h1 {
|
|
820
|
+
font-size: 20px;
|
|
821
|
+
line-height: 1.3;
|
|
822
|
+
}
|
|
823
|
+
.header-meta {
|
|
824
|
+
grid-template-columns: 1fr;
|
|
825
|
+
padding: 10px;
|
|
826
|
+
}
|
|
827
|
+
.kpi-grid {
|
|
828
|
+
grid-template-columns: repeat(2, 1fr);
|
|
829
|
+
gap: 8px;
|
|
830
|
+
}
|
|
831
|
+
.category-grid {
|
|
832
|
+
grid-template-columns: 1fr;
|
|
833
|
+
}
|
|
834
|
+
.kpi-card {
|
|
835
|
+
padding: 10px;
|
|
836
|
+
}
|
|
837
|
+
.kpi-label {
|
|
838
|
+
font-size: 10px;
|
|
839
|
+
}
|
|
840
|
+
.kpi-value {
|
|
841
|
+
font-size: 18px;
|
|
842
|
+
}
|
|
843
|
+
.section-card {
|
|
844
|
+
padding: 12px 10px;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
.live-badge {
|
|
849
|
+
display: inline-flex;
|
|
850
|
+
align-items: center;
|
|
851
|
+
gap: 6px;
|
|
852
|
+
padding: 3px 8px;
|
|
853
|
+
border-radius: 999px;
|
|
854
|
+
font-size: 11px;
|
|
855
|
+
font-weight: 700;
|
|
856
|
+
letter-spacing: 0.05em;
|
|
857
|
+
text-transform: uppercase;
|
|
858
|
+
background: rgba(16, 185, 129, 0.15);
|
|
859
|
+
color: #34d399;
|
|
860
|
+
border: 1px solid rgba(16, 185, 129, 0.3);
|
|
861
|
+
vertical-align: middle;
|
|
862
|
+
}
|
|
863
|
+
.live-dot {
|
|
864
|
+
width: 7px;
|
|
865
|
+
height: 7px;
|
|
866
|
+
border-radius: 50%;
|
|
867
|
+
background: #10b981;
|
|
868
|
+
box-shadow: 0 0 8px #10b981;
|
|
869
|
+
animation: livePulse 2s infinite ease-in-out;
|
|
870
|
+
}
|
|
871
|
+
.live-dot.paused {
|
|
872
|
+
background: #94a3b8;
|
|
873
|
+
box-shadow: none;
|
|
874
|
+
animation: none;
|
|
875
|
+
}
|
|
876
|
+
@keyframes livePulse {
|
|
877
|
+
0% { transform: scale(0.9); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
|
|
878
|
+
70% { transform: scale(1.15); box-shadow: 0 0 0 5px rgba(16, 185, 129, 0); }
|
|
879
|
+
100% { transform: scale(0.9); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
|
880
|
+
}
|
|
881
|
+
</style>
|
|
882
|
+
</head>
|
|
883
|
+
<body>
|
|
884
|
+
<div class="dashboard-container">
|
|
885
|
+
<div class="header-bar">
|
|
886
|
+
<div class="title-area">
|
|
887
|
+
<h1>Kiku Agent Evaluation & Performance <span class="live-badge"><span class="live-dot" id="live-dot"></span><span id="live-status-text">LIVE AUTO-SYNC</span></span></h1>
|
|
888
|
+
<p>Real-time evaluation suite monitoring reasoning accuracy, tool invocation telemetry, trajectory loop health, and end-to-end latency breakdowns.</p>
|
|
889
|
+
</div>
|
|
890
|
+
<div class="header-meta">
|
|
891
|
+
<a href="/eval" style="text-decoration:none; display:inline-flex; align-items:center; gap:6px; background: linear-gradient(135deg, rgba(236,72,153,0.2), rgba(168,85,247,0.25)); border: 1px solid rgba(236,72,153,0.4); color: #f472b6; padding: 6px 12px; border-radius: 6px; font-weight: 600; font-size: 12px; margin-right: 4px;" title="Open Workbench & AI Summary">
|
|
892
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
|
|
893
|
+
<span>Workbench • AI Summary</span>
|
|
894
|
+
</a>
|
|
895
|
+
<div class="meta-tag">Model: <strong id="meta-model">${escapeHtml(metadata.model || 'Unknown')}</strong></div>
|
|
896
|
+
<div class="meta-tag">Planned Cases: <strong id="meta-planned">${metadata.caseIds?.length || summary.planned || rows.length}</strong></div>
|
|
897
|
+
<div class="meta-tag">Completed: <strong id="meta-completed">${summary.completed || 0}</strong></div>
|
|
898
|
+
<div class="meta-tag">Platform: <strong>${escapeHtml(metadata.platform || process.platform)}</strong></div>
|
|
899
|
+
<div class="meta-tag">Sync: <strong id="meta-sync-status">Real-time Stream</strong></div>
|
|
900
|
+
</div>
|
|
901
|
+
</div>
|
|
902
|
+
|
|
903
|
+
<div class="kpi-grid">
|
|
904
|
+
<div class="kpi-card">
|
|
905
|
+
<div class="kpi-label">Lexical Pass Rate</div>
|
|
906
|
+
<div class="kpi-value" id="kpi-pass-rate"><span id="val-pass-rate">${passRate}%</span> ${deltaBadge('lexicalCoverageRate')}</div>
|
|
907
|
+
<div class="kpi-sub" id="kpi-pass-sub">${summary.allFactsMatched || 0} of ${summary.coverageEligibleCompleted || 0} evaluated cases</div>
|
|
908
|
+
</div>
|
|
909
|
+
<div class="kpi-card">
|
|
910
|
+
<div class="kpi-label">Tool Accuracy F1</div>
|
|
911
|
+
<div class="kpi-value" id="kpi-tool-rate"><span id="val-tool-rate">${toolRate}%</span> ${deltaBadge('toolAccuracyRate')}</div>
|
|
912
|
+
<div class="kpi-sub" id="kpi-tool-sub">Target tools selected & executed</div>
|
|
913
|
+
</div>
|
|
914
|
+
<div class="kpi-card">
|
|
915
|
+
<div class="kpi-label">Trajectory Health</div>
|
|
916
|
+
<div class="kpi-value" id="kpi-loop-rate"><span id="val-loop-rate">${loopFreeRate}%</span> ${deltaBadge('loopFreeRate')}</div>
|
|
917
|
+
<div class="kpi-sub" id="kpi-loop-sub">Loop-free execution trajectories</div>
|
|
918
|
+
</div>
|
|
919
|
+
<div class="kpi-card">
|
|
920
|
+
<div class="kpi-label">Groundedness Rate</div>
|
|
921
|
+
<div class="kpi-value" id="kpi-grounded-rate"><span id="val-grounded-rate">${groundedRate}%</span> ${deltaBadge('groundednessRate')}</div>
|
|
922
|
+
<div class="kpi-sub" id="kpi-grounded-sub">Facts backed by tool observations</div>
|
|
923
|
+
</div>
|
|
924
|
+
<div class="kpi-card">
|
|
925
|
+
<div class="kpi-label">Latency (Median)</div>
|
|
926
|
+
<div class="kpi-value" id="kpi-latency-median"><span id="val-latency-median">${medianSec}s</span> ${deltaBadge('medianMs', false)}</div>
|
|
927
|
+
<div class="kpi-sub" id="kpi-latency-sub">P95: ${p95Sec}s | LLM: ${meanLlmSec}s | Tool: ${meanToolSec}s</div>
|
|
928
|
+
</div>
|
|
929
|
+
<div class="kpi-card">
|
|
930
|
+
<div class="kpi-label">Throughput TPS</div>
|
|
931
|
+
<div class="kpi-value" id="kpi-tps"><span id="val-tps">${decodeTps}</span> ${deltaBadge('meanDecodeTps', false)}</div>
|
|
932
|
+
<div class="kpi-sub" id="kpi-tps-sub">Average decode tokens / second</div>
|
|
933
|
+
</div>
|
|
934
|
+
</div>
|
|
935
|
+
|
|
936
|
+
<div class="section-card">
|
|
937
|
+
<div class="section-title">Agent Evaluation and Benchmark Dashboard (ISO-IEC 42001 and NIST AI RMF)</div>
|
|
938
|
+
<div class="table-wrap">
|
|
939
|
+
<table class="bench-table">
|
|
940
|
+
<thead><tr><th>Metric Dimension</th><th>Technical Indicator</th><th>Benchmark</th><th>Target</th><th>Observed</th><th>Status</th></tr></thead>
|
|
941
|
+
<tbody>${benchmarkTableRowsHtml}</tbody>
|
|
942
|
+
</table>
|
|
943
|
+
</div>
|
|
944
|
+
<div class="formula-grid">
|
|
945
|
+
<div class="formula-card"><strong>Task Success Rate Pass at k</strong><code>Pass at k = E[ 1 - C(n-c, k) over C(n, k) ]</code><div>n runs per task, c passing runs</div></div>
|
|
946
|
+
<div class="formula-card"><strong>Trajectory Step Optimality</strong><code>eta_traj = L* over max(L, L*)</code><div>L* shortest ground truth path, L actual steps</div></div>
|
|
947
|
+
<div class="formula-card"><strong>Grounded Faithfulness</strong><code>S_faith = sum I(Claim in K) over m</code><div>K retrieval context or tool output, m claims</div></div>
|
|
948
|
+
</div>
|
|
949
|
+
</div>
|
|
950
|
+
|
|
951
|
+
<div class="section-card">
|
|
952
|
+
<div class="section-title">Trajectory vs Cost Pareto Frontier</div>
|
|
953
|
+
<div class="pareto-wrap">
|
|
954
|
+
<svg width="${paretoW}" height="${paretoH}" viewBox="0 0 ${paretoW} ${paretoH}">
|
|
955
|
+
<line x1="60" y1="270" x2="880" y2="270" class="pareto-axis"></line>
|
|
956
|
+
<line x1="60" y1="270" x2="60" y2="30" class="pareto-axis"></line>
|
|
957
|
+
<text x="430" y="295" class="svg-label">Cost per task (USD): 0.00 to 0.08</text>
|
|
958
|
+
<text x="8" y="150" class="svg-label">Success</text>
|
|
959
|
+
<text x="400" y="18" class="svg-label">Target Optimal Zone near 82 percent and low cost</text>
|
|
960
|
+
${paretoDotsHtml}
|
|
961
|
+
</svg>
|
|
962
|
+
</div>
|
|
963
|
+
</div>
|
|
964
|
+
|
|
965
|
+
<div class="section-card">
|
|
966
|
+
<div class="section-title">Failure Mode Distribution</div>
|
|
967
|
+
${failureBarsHtml}
|
|
968
|
+
</div>
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
<div class="section-card">
|
|
973
|
+
<div class="section-title">Category Health & Coverage</div>
|
|
974
|
+
<div class="category-grid" id="categoryCardsContainer">
|
|
975
|
+
${categoryCardsHtml}
|
|
976
|
+
</div>
|
|
977
|
+
</div>
|
|
978
|
+
|
|
979
|
+
${languageCardsHtml ? `
|
|
980
|
+
<div class="section-card">
|
|
981
|
+
<div class="section-title">Multi-Lingual Breakdown & Performance</div>
|
|
982
|
+
<div class="category-grid" id="languageCardsContainer">
|
|
983
|
+
${languageCardsHtml}
|
|
984
|
+
</div>
|
|
985
|
+
</div>` : ''}
|
|
986
|
+
|
|
987
|
+
<div class="section-card">
|
|
988
|
+
<div class="section-title">Latency Distribution & Inference Breakdown</div>
|
|
989
|
+
<div class="chart-legend">
|
|
990
|
+
<div class="legend-item">
|
|
991
|
+
<div class="legend-box" style="background: #818cf8;"></div>
|
|
992
|
+
<span>LLM Inference & Generation</span>
|
|
993
|
+
</div>
|
|
994
|
+
<div class="legend-item">
|
|
995
|
+
<div class="legend-box" style="background: #f43f5e;"></div>
|
|
996
|
+
<span>Tool Calling & I/O</span>
|
|
997
|
+
</div>
|
|
998
|
+
</div>
|
|
999
|
+
<div class="svg-scroll-wrap">
|
|
1000
|
+
<svg width="${chartWidth}" height="${chartHeight}" viewBox="0 0 ${chartWidth} ${chartHeight}">
|
|
1001
|
+
${latencySvgBars}
|
|
1002
|
+
</svg>
|
|
1003
|
+
</div>
|
|
1004
|
+
</div>
|
|
1005
|
+
|
|
1006
|
+
<div class="section-card">
|
|
1007
|
+
<div class="section-title">Case Inspection Matrix & Trajectory Explorer</div>
|
|
1008
|
+
<div class="table-toolbar">
|
|
1009
|
+
<div class="search-box">
|
|
1010
|
+
<input type="text" id="searchInput" placeholder="Search by case ID, question, or tool name..." oninput="filterCases()">
|
|
1011
|
+
</div>
|
|
1012
|
+
<div class="filter-group">
|
|
1013
|
+
<button class="filter-btn active" onclick="setFilter('all', this)">All Status</button>
|
|
1014
|
+
<button class="filter-btn" onclick="setFilter('pass', this)">Passed</button>
|
|
1015
|
+
<button class="filter-btn" onclick="setFilter('fail', this)">Failed / Partial</button>
|
|
1016
|
+
<button class="filter-btn" onclick="setFilter('loops', this)">Loops</button>
|
|
1017
|
+
</div>
|
|
1018
|
+
<div class="filter-group">
|
|
1019
|
+
<button class="filter-btn active" onclick="setLangFilter('all', this)" id="btn-lang-all">All Langs</button>
|
|
1020
|
+
<button class="filter-btn" onclick="setLangFilter('th', this)">Thai (TH)</button>
|
|
1021
|
+
<button class="filter-btn" onclick="setLangFilter('en', this)">English (EN)</button>
|
|
1022
|
+
<button class="filter-btn" onclick="setLangFilter('zh', this)">Chinese (ZH)</button>
|
|
1023
|
+
</div>
|
|
1024
|
+
</div>
|
|
1025
|
+
|
|
1026
|
+
<div class="table-wrap">
|
|
1027
|
+
<table>
|
|
1028
|
+
<thead>
|
|
1029
|
+
<tr>
|
|
1030
|
+
<th style="width: 80px;">ID</th>
|
|
1031
|
+
<th style="width: 120px;">Category</th>
|
|
1032
|
+
<th style="width: 80px;">Lang</th>
|
|
1033
|
+
<th>Question</th>
|
|
1034
|
+
<th style="width: 130px;">Status</th>
|
|
1035
|
+
<th style="width: 280px;">Tools Used</th>
|
|
1036
|
+
<th style="width: 90px;">Latency</th>
|
|
1037
|
+
</tr>
|
|
1038
|
+
</thead>
|
|
1039
|
+
<tbody id="caseTableBody">
|
|
1040
|
+
${tableRowsHtml}
|
|
1041
|
+
</tbody>
|
|
1042
|
+
</table>
|
|
1043
|
+
</div>
|
|
1044
|
+
</div>
|
|
1045
|
+
</div>
|
|
1046
|
+
|
|
1047
|
+
<script>
|
|
1048
|
+
let activeFilter = 'all';
|
|
1049
|
+
let activeLang = 'all';
|
|
1050
|
+
|
|
1051
|
+
function escapeHtml(str) {
|
|
1052
|
+
return String(str ?? '')
|
|
1053
|
+
.replace(/&/g, '&')
|
|
1054
|
+
.replace(/</g, '<')
|
|
1055
|
+
.replace(/>/g, '>')
|
|
1056
|
+
.replace(/"/g, '"')
|
|
1057
|
+
.replace(/'/g, ''');
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function toggleDetails(idOrIdx) {
|
|
1061
|
+
var id = typeof idOrIdx === 'number' ? ('row-detail-' + idOrIdx) : String(idOrIdx);
|
|
1062
|
+
var el = document.getElementById(id);
|
|
1063
|
+
if (!el) return;
|
|
1064
|
+
el.style.display = el.style.display === 'none' ? 'table-row' : 'none';
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function setFilter(type, btn) {
|
|
1068
|
+
activeFilter = type;
|
|
1069
|
+
document.querySelectorAll('[onclick^="setFilter"]').forEach(b => b.classList.remove('active'));
|
|
1070
|
+
if (btn) btn.classList.add('active');
|
|
1071
|
+
filterCases();
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function setLangFilter(lang, btn) {
|
|
1075
|
+
activeLang = lang;
|
|
1076
|
+
document.querySelectorAll('[onclick^="setLangFilter"]').forEach(b => b.classList.remove('active'));
|
|
1077
|
+
if (btn) btn.classList.add('active');
|
|
1078
|
+
filterCases();
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function filterCases() {
|
|
1082
|
+
const query = (document.getElementById('searchInput')?.value || '').toLowerCase().trim();
|
|
1083
|
+
const rows = document.querySelectorAll('.case-row');
|
|
1084
|
+
|
|
1085
|
+
rows.forEach((row, idx) => {
|
|
1086
|
+
const text = row.textContent.toLowerCase();
|
|
1087
|
+
const status = row.getAttribute('data-status');
|
|
1088
|
+
const lang = (row.getAttribute('data-language') || '').toLowerCase();
|
|
1089
|
+
const hasLoop = row.getAttribute('data-has-loop') === 'true';
|
|
1090
|
+
|
|
1091
|
+
let matchesFilter = true;
|
|
1092
|
+
if (activeFilter === 'pass') matchesFilter = (status === 'pass');
|
|
1093
|
+
else if (activeFilter === 'fail') matchesFilter = (status === 'fail');
|
|
1094
|
+
else if (activeFilter === 'loops') matchesFilter = hasLoop;
|
|
1095
|
+
|
|
1096
|
+
const matchesLang = (activeLang === 'all' || lang === activeLang);
|
|
1097
|
+
const matchesQuery = !query || text.includes(query);
|
|
1098
|
+
const shouldShow = matchesFilter && matchesLang && matchesQuery;
|
|
1099
|
+
|
|
1100
|
+
row.style.display = shouldShow ? 'table-row' : 'none';
|
|
1101
|
+
const detailRow = document.getElementById('row-detail-' + idx);
|
|
1102
|
+
if (detailRow && !shouldShow) {
|
|
1103
|
+
detailRow.style.display = 'none';
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function renderCaseRow(r, idx) {
|
|
1109
|
+
var isCompleted = r.status === 'completed';
|
|
1110
|
+
var allMatched = r.coverage && r.coverage.allMatched;
|
|
1111
|
+
var hasLoop = r.trajectoryHealth && r.trajectoryHealth.hasLoop;
|
|
1112
|
+
var statusBadge = isCompleted
|
|
1113
|
+
? (allMatched ? '<span class="badge badge-success">PASS</span>' : '<span class="badge badge-warning">PARTIAL</span>')
|
|
1114
|
+
: '<span class="badge badge-danger">ERROR</span>';
|
|
1115
|
+
|
|
1116
|
+
var toolsUsed = Array.isArray(r.trace)
|
|
1117
|
+
? Array.from(new Set(r.trace.filter(function(t) { return t.event === 'start'; }).map(function(t) { return t.name; })))
|
|
1118
|
+
: [];
|
|
1119
|
+
var toolsHtml = toolsUsed.length
|
|
1120
|
+
? toolsUsed.map(function(t) { return '<span class="tag-tool">' + escapeHtml(t) + '</span>'; }).join('')
|
|
1121
|
+
: '<span class="tag-empty">None</span>';
|
|
1122
|
+
|
|
1123
|
+
var loopBadge = hasLoop ? '<span class="badge badge-loop">LOOP</span>' : '';
|
|
1124
|
+
var durationStr = r.durationMs ? (r.durationMs / 1000).toFixed(2) + 's' : '-';
|
|
1125
|
+
|
|
1126
|
+
var matchedCount = (r.coverage && r.coverage.matched) || 0;
|
|
1127
|
+
var totalCount = (r.coverage && r.coverage.total) || 0;
|
|
1128
|
+
var missingFactsHtml = (r.coverage && r.coverage.missing && r.coverage.missing.length)
|
|
1129
|
+
? '<div class="missing-facts">[FAIL] Missing: ' + escapeHtml(JSON.stringify(r.coverage.missing)) + '</div>'
|
|
1130
|
+
: '<div class="all-facts-ok">[OK] All target aliases identified</div>';
|
|
1131
|
+
|
|
1132
|
+
var llmMs = Math.round((r.latencyBreakdown && (r.latencyBreakdown.llmInferenceMs || r.latencyBreakdown.llmMs)) || 0);
|
|
1133
|
+
var toolMs = Math.round((r.latencyBreakdown && (r.latencyBreakdown.toolExecutionMs || r.latencyBreakdown.toolMs)) || 0);
|
|
1134
|
+
var llmPct = (r.latencyBreakdown && r.latencyBreakdown.llmPercentage) || 0;
|
|
1135
|
+
var toolPct = (r.latencyBreakdown && r.latencyBreakdown.toolPercentage) || 0;
|
|
1136
|
+
|
|
1137
|
+
var traces = Array.isArray(r.trace) ? r.trace : [];
|
|
1138
|
+
var tracesHtml = traces.map(function(t, stepIdx) {
|
|
1139
|
+
var durationBadge = t.durationMs ? '<span class="trace-time">' + t.durationMs + 'ms</span>' : '';
|
|
1140
|
+
var paramsHtml = t.parameters ? '<pre class="trace-json">Params: ' + escapeHtml(JSON.stringify(t.parameters, null, 2)) + '</pre>' : '';
|
|
1141
|
+
var valStr = typeof t.value === 'object' ? JSON.stringify(t.value, null, 2) : String(t.value || '').slice(0, 300);
|
|
1142
|
+
var valHtml = t.value ? '<pre class="trace-json trace-val">Output: ' + escapeHtml(valStr) + '</pre>' : '';
|
|
1143
|
+
var msgHtml = t.message ? '<div class="trace-msg">' + escapeHtml(t.message) + '</div>' : '';
|
|
1144
|
+
return '<div class="trace-event trace-' + escapeHtml(t.event) + '">'
|
|
1145
|
+
+ '<div class="trace-badge">' + (stepIdx + 1) + '. [' + escapeHtml((t.event || '').toUpperCase()) + '] ' + escapeHtml(t.name || '') + ' ' + durationBadge + '</div>'
|
|
1146
|
+
+ paramsHtml + valHtml + msgHtml
|
|
1147
|
+
+ '</div>';
|
|
1148
|
+
}).join('');
|
|
1149
|
+
|
|
1150
|
+
var rowHtml = '<tr class="case-row" onclick="toggleDetails(' + idx + ')" id="row-' + idx + '" data-category="' + escapeHtml(r.category) + '" data-language="' + escapeHtml(r.language) + '" data-status="' + (allMatched ? 'pass' : 'fail') + '" data-has-loop="' + (hasLoop ? 'true' : 'false') + '">'
|
|
1151
|
+
+ '<td class="col-id"><strong>' + escapeHtml(r.id) + '</strong></td>'
|
|
1152
|
+
+ '<td><span class="badge badge-category">' + escapeHtml(r.category) + '</span></td>'
|
|
1153
|
+
+ '<td><span class="badge badge-lang">' + escapeHtml((r.language || '').toUpperCase()) + '</span></td>'
|
|
1154
|
+
+ '<td class="col-question" title="' + escapeHtml(r.question) + '">' + escapeHtml(r.question) + '</td>'
|
|
1155
|
+
+ '<td>' + statusBadge + ' ' + loopBadge + '</td>'
|
|
1156
|
+
+ '<td><div class="tool-tags-cell">' + toolsHtml + '</div></td>'
|
|
1157
|
+
+ '<td class="col-duration">' + durationStr + '</td>'
|
|
1158
|
+
+ '</tr>'
|
|
1159
|
+
+ '<tr class="detail-row" id="row-detail-' + idx + '" style="display: none;">'
|
|
1160
|
+
+ '<td colspan="7">'
|
|
1161
|
+
+ '<div class="detail-drawer">'
|
|
1162
|
+
+ '<div class="detail-grid">'
|
|
1163
|
+
+ '<div class="detail-pane">'
|
|
1164
|
+
+ '<h4>Reference & Fact Matching</h4>'
|
|
1165
|
+
+ '<p><strong>Question:</strong> ' + escapeHtml(r.question) + '</p>'
|
|
1166
|
+
+ '<div class="fact-coverage-box"><strong>Matched Facts:</strong> ' + matchedCount + ' / ' + totalCount + missingFactsHtml + '</div>'
|
|
1167
|
+
+ '<h4 style="margin-top: 14px;">Latency & Execution Breakdown</h4>'
|
|
1168
|
+
+ '<div class="latency-stat-row"><div>LLM Thought & Gen: <strong>' + llmMs + 'ms</strong> (' + llmPct + '%)</div><div>Tool Execution: <strong>' + toolMs + 'ms</strong> (' + toolPct + '%)</div></div>'
|
|
1169
|
+
+ '<div class="dual-progress"><div class="dp-llm" style="width: ' + llmPct + '%"></div><div class="dp-tool" style="width: ' + toolPct + '%"></div></div>'
|
|
1170
|
+
+ '</div>'
|
|
1171
|
+
+ '<div class="detail-pane">'
|
|
1172
|
+
+ '<h4>Agent Final Response</h4>'
|
|
1173
|
+
+ '<div class="answer-box">' + escapeHtml(r.answer || r.error || 'No answer generated') + '</div>'
|
|
1174
|
+
+ '</div>'
|
|
1175
|
+
+ '</div>'
|
|
1176
|
+
+ '<h4 style="margin-top: 16px;">Full Trajectory Trace Events (' + traces.length + ' events)</h4>'
|
|
1177
|
+
+ '<div class="trace-timeline">' + tracesHtml + '</div>'
|
|
1178
|
+
+ '</div>'
|
|
1179
|
+
+ '</td>'
|
|
1180
|
+
+ '</tr>';
|
|
1181
|
+
return rowHtml;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function renderCategoryCards(catStats) {
|
|
1185
|
+
if (!catStats) return '';
|
|
1186
|
+
var html = '';
|
|
1187
|
+
var entries = Object.entries(catStats);
|
|
1188
|
+
for (var i = 0; i < entries.length; i++) {
|
|
1189
|
+
var cat = entries[i][0];
|
|
1190
|
+
var data = entries[i][1];
|
|
1191
|
+
var catPass = data.completed > 0 ? Math.round((data.factsMatched / data.completed) * 100) : 0;
|
|
1192
|
+
var toolDenom = data.toolEligible != null ? data.toolEligible : data.completed;
|
|
1193
|
+
var catTools = toolDenom > 0 ? Math.round((data.toolsCorrect / toolDenom) * 100) : 0;
|
|
1194
|
+
html += '<div class="category-card">'
|
|
1195
|
+
+ '<div class="category-header"><span class="category-name">' + escapeHtml(cat.toUpperCase()) + '</span><span class="category-count">' + data.completed + '/' + data.total + ' cases</span></div>'
|
|
1196
|
+
+ '<div class="category-metrics">'
|
|
1197
|
+
+ '<div class="metric-row"><span>Lexical Pass:</span><strong>' + catPass + '%</strong></div>'
|
|
1198
|
+
+ '<div class="progress-bar"><div class="progress-fill" style="width: ' + catPass + '%"></div></div>'
|
|
1199
|
+
+ '<div class="metric-row"><span>Tool Accuracy:</span><strong>' + catTools + '%</strong></div>'
|
|
1200
|
+
+ '<div class="progress-bar progress-tool"><div class="progress-fill" style="width: ' + catTools + '%"></div></div>'
|
|
1201
|
+
+ '</div>'
|
|
1202
|
+
+ '</div>';
|
|
1203
|
+
}
|
|
1204
|
+
return html;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function applyLiveReport(rep) {
|
|
1208
|
+
if (!rep) return;
|
|
1209
|
+
var s = rep.summary || {};
|
|
1210
|
+
var meta = rep.metadata || {};
|
|
1211
|
+
var rows = Array.isArray(rep.rows) ? rep.rows : [];
|
|
1212
|
+
|
|
1213
|
+
var mModel = document.getElementById('meta-model');
|
|
1214
|
+
if (mModel && meta.model) mModel.textContent = meta.model;
|
|
1215
|
+
var mPlanned = document.getElementById('meta-planned');
|
|
1216
|
+
if (mPlanned) mPlanned.textContent = (meta.caseIds && meta.caseIds.length) || s.planned || rows.length;
|
|
1217
|
+
var mComp = document.getElementById('meta-completed');
|
|
1218
|
+
if (mComp) mComp.textContent = s.completed != null ? s.completed : rows.length;
|
|
1219
|
+
|
|
1220
|
+
var completedCount = s.completed != null ? s.completed : rows.filter(function(r) { return r.status === 'completed'; }).length;
|
|
1221
|
+
var passCount = s.allFactsMatched != null ? s.allFactsMatched : rows.filter(function(r) { return r.coverage && r.coverage.allMatched; }).length;
|
|
1222
|
+
var coverageEligible = s.coverageEligibleCompleted != null ? s.coverageEligibleCompleted : completedCount;
|
|
1223
|
+
var pRate = s.lexicalCoverageRate != null
|
|
1224
|
+
? Math.round(s.lexicalCoverageRate * 100)
|
|
1225
|
+
: (coverageEligible > 0 ? Math.round((passCount / coverageEligible) * 100) : 0);
|
|
1226
|
+
|
|
1227
|
+
var tRate = s.toolAccuracyRate != null ? Math.round(s.toolAccuracyRate * 100) : 0;
|
|
1228
|
+
var lRate = s.loopFreeRate != null ? Math.round(s.loopFreeRate * 100) : 100;
|
|
1229
|
+
var gRate = s.groundednessRate != null ? Math.round(s.groundednessRate * 100) : 100;
|
|
1230
|
+
var medSec = s.medianMs ? (s.medianMs / 1000).toFixed(2) : '-';
|
|
1231
|
+
var p95 = s.p95Ms ? (s.p95Ms / 1000).toFixed(2) : '-';
|
|
1232
|
+
var mLlm = s.meanLlmMs ? (s.meanLlmMs / 1000).toFixed(2) : '-';
|
|
1233
|
+
var mTool = s.meanToolMs ? (s.meanToolMs / 1000).toFixed(2) : '-';
|
|
1234
|
+
var tps = s.meanDecodeTps != null ? s.meanDecodeTps : '-';
|
|
1235
|
+
|
|
1236
|
+
var elPass = document.getElementById('val-pass-rate');
|
|
1237
|
+
if (elPass) elPass.textContent = pRate + '%';
|
|
1238
|
+
var elPassSub = document.getElementById('kpi-pass-sub');
|
|
1239
|
+
if (elPassSub) elPassSub.textContent = passCount + ' of ' + coverageEligible + ' evaluated cases';
|
|
1240
|
+
|
|
1241
|
+
var elTool = document.getElementById('val-tool-rate');
|
|
1242
|
+
if (elTool) elTool.textContent = tRate + '%';
|
|
1243
|
+
|
|
1244
|
+
var elLoop = document.getElementById('val-loop-rate');
|
|
1245
|
+
if (elLoop) elLoop.textContent = lRate + '%';
|
|
1246
|
+
|
|
1247
|
+
var elGrounded = document.getElementById('val-grounded-rate');
|
|
1248
|
+
if (elGrounded) elGrounded.textContent = gRate + '%';
|
|
1249
|
+
|
|
1250
|
+
var elLatency = document.getElementById('val-latency-median');
|
|
1251
|
+
if (elLatency) elLatency.textContent = medSec + 's';
|
|
1252
|
+
var elLatencySub = document.getElementById('kpi-latency-sub');
|
|
1253
|
+
if (elLatencySub) elLatencySub.textContent = 'P95: ' + p95 + 's | LLM: ' + mLlm + 's | Tool: ' + mTool + 's';
|
|
1254
|
+
|
|
1255
|
+
var elTps = document.getElementById('val-tps');
|
|
1256
|
+
if (elTps) elTps.textContent = tps;
|
|
1257
|
+
|
|
1258
|
+
if (s.categoryStats) {
|
|
1259
|
+
var catBox = document.getElementById('categoryCardsContainer');
|
|
1260
|
+
if (catBox) catBox.innerHTML = renderCategoryCards(s.categoryStats);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
var tbody = document.getElementById('caseTableBody');
|
|
1264
|
+
if (tbody && rows.length > 0) {
|
|
1265
|
+
var openIds = new Set(Array.from(document.querySelectorAll('.detail-row')).filter(function(el) { return el.style.display !== 'none'; }).map(function(el) { return el.id; }));
|
|
1266
|
+
tbody.innerHTML = rows.map(function(r, idx) { return renderCaseRow(r, idx); }).join('');
|
|
1267
|
+
openIds.forEach(function(id) {
|
|
1268
|
+
var el = document.getElementById(id);
|
|
1269
|
+
if (el) el.style.display = 'table-row';
|
|
1270
|
+
});
|
|
1271
|
+
filterCases();
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function initRealtimeStream() {
|
|
1276
|
+
var isHttp = window.location.protocol.indexOf('http') === 0;
|
|
1277
|
+
var apiHost = isHttp ? window.location.origin : 'http://localhost:3000';
|
|
1278
|
+
var eventsUrl = apiHost + '/api/eval/events';
|
|
1279
|
+
var statusUrl = apiHost + '/api/eval/status';
|
|
1280
|
+
var sseConnected = false;
|
|
1281
|
+
|
|
1282
|
+
function updateStatus(text, ok) {
|
|
1283
|
+
var el = document.getElementById('meta-sync-status');
|
|
1284
|
+
var dot = document.getElementById('live-dot');
|
|
1285
|
+
var txt = document.getElementById('live-status-text');
|
|
1286
|
+
if (el) el.textContent = text;
|
|
1287
|
+
if (txt) txt.textContent = ok ? 'LIVE STREAM' : 'AUTO-RETRYING';
|
|
1288
|
+
if (dot) {
|
|
1289
|
+
if (ok) dot.classList.remove('paused');
|
|
1290
|
+
else dot.classList.add('paused');
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function setupSSE() {
|
|
1295
|
+
try {
|
|
1296
|
+
var es = new EventSource(eventsUrl);
|
|
1297
|
+
es.onopen = function() {
|
|
1298
|
+
sseConnected = true;
|
|
1299
|
+
updateStatus('Real-time Stream Connected', true);
|
|
1300
|
+
};
|
|
1301
|
+
es.addEventListener('sync_state', function(e) {
|
|
1302
|
+
try {
|
|
1303
|
+
var data = JSON.parse(e.data);
|
|
1304
|
+
if (data.report) {
|
|
1305
|
+
applyLiveReport(data.report);
|
|
1306
|
+
updateStatus('Stream Active (' + new Date().toLocaleTimeString() + ')', true);
|
|
1307
|
+
}
|
|
1308
|
+
} catch (err) {}
|
|
1309
|
+
});
|
|
1310
|
+
es.addEventListener('live_file_update', function(e) {
|
|
1311
|
+
try {
|
|
1312
|
+
var data = JSON.parse(e.data);
|
|
1313
|
+
if (data.report) {
|
|
1314
|
+
applyLiveReport(data.report);
|
|
1315
|
+
updateStatus('Stream Active (' + new Date().toLocaleTimeString() + ')', true);
|
|
1316
|
+
}
|
|
1317
|
+
} catch (err) {}
|
|
1318
|
+
});
|
|
1319
|
+
es.addEventListener('case_complete', function(e) {
|
|
1320
|
+
try {
|
|
1321
|
+
var data = JSON.parse(e.data);
|
|
1322
|
+
if (data.report) {
|
|
1323
|
+
applyLiveReport(data.report);
|
|
1324
|
+
updateStatus('Stream Active (' + new Date().toLocaleTimeString() + ')', true);
|
|
1325
|
+
}
|
|
1326
|
+
} catch (err) {}
|
|
1327
|
+
});
|
|
1328
|
+
es.addEventListener('eval_complete', function(e) {
|
|
1329
|
+
try {
|
|
1330
|
+
var data = JSON.parse(e.data);
|
|
1331
|
+
if (data.report) {
|
|
1332
|
+
applyLiveReport(data.report);
|
|
1333
|
+
updateStatus('Eval Complete (' + new Date().toLocaleTimeString() + ')', true);
|
|
1334
|
+
}
|
|
1335
|
+
} catch (err) {}
|
|
1336
|
+
});
|
|
1337
|
+
es.onerror = function() {
|
|
1338
|
+
sseConnected = false;
|
|
1339
|
+
updateStatus('Reconnecting...', false);
|
|
1340
|
+
es.close();
|
|
1341
|
+
setTimeout(setupSSE, 3000);
|
|
1342
|
+
};
|
|
1343
|
+
} catch (err) {
|
|
1344
|
+
sseConnected = false;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function pollStatus() {
|
|
1349
|
+
if (sseConnected) return;
|
|
1350
|
+
fetch(statusUrl, { headers: { 'Accept': 'application/json' } })
|
|
1351
|
+
.then(function(res) {
|
|
1352
|
+
if (res.ok) return res.json();
|
|
1353
|
+
throw new Error('HTTP ' + res.status);
|
|
1354
|
+
})
|
|
1355
|
+
.then(function(json) {
|
|
1356
|
+
if (json.currentReport && Array.isArray(json.currentReport.rows)) {
|
|
1357
|
+
applyLiveReport(json.currentReport);
|
|
1358
|
+
updateStatus('Live Polling Active (' + new Date().toLocaleTimeString() + ')', true);
|
|
1359
|
+
}
|
|
1360
|
+
})
|
|
1361
|
+
.catch(function() {
|
|
1362
|
+
updateStatus('Waiting for Server...', false);
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
setupSSE();
|
|
1367
|
+
setInterval(pollStatus, 2000);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
initRealtimeStream();
|
|
1371
|
+
</script>
|
|
1372
|
+
</body>
|
|
1373
|
+
</html>`;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
export async function writeDashboardReport(report, outputPath, compareReport = null) {
|
|
1377
|
+
const html = generateDashboardHtml(report, compareReport);
|
|
1378
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
1379
|
+
await fs.writeFile(outputPath, html, 'utf8');
|
|
1380
|
+
return outputPath;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
1384
|
+
const { values: args } = parseArgs({ options: {
|
|
1385
|
+
in: { type: 'string', default: 'artifacts/eval/live.json' },
|
|
1386
|
+
out: { type: 'string', default: 'artifacts/eval/dashboard.html' },
|
|
1387
|
+
compare: { type: 'string' }
|
|
1388
|
+
} });
|
|
1389
|
+
|
|
1390
|
+
const inPath = path.resolve(REPO_ROOT, args.in);
|
|
1391
|
+
const outPath = path.resolve(REPO_ROOT, args.out);
|
|
1392
|
+
|
|
1393
|
+
try {
|
|
1394
|
+
const raw = await fs.readFile(inPath, 'utf8');
|
|
1395
|
+
const report = JSON.parse(raw);
|
|
1396
|
+
let compareReport = null;
|
|
1397
|
+
if (args.compare) {
|
|
1398
|
+
const compRaw = await fs.readFile(path.resolve(REPO_ROOT, args.compare), 'utf8');
|
|
1399
|
+
compareReport = JSON.parse(compRaw);
|
|
1400
|
+
}
|
|
1401
|
+
await writeDashboardReport(report, outPath, compareReport);
|
|
1402
|
+
} catch (err) {
|
|
1403
|
+
console.error(`Failed to generate dashboard: ${err.message}`);
|
|
1404
|
+
process.exit(1);
|
|
1405
|
+
}
|
|
1406
|
+
}
|