opencode-rag-plugin 1.11.0 → 1.12.4
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 +14 -6
- package/dist/api.js +21 -82
- package/dist/api.js.map +1 -1
- package/dist/chunker/image.d.ts +14 -0
- package/dist/chunker/image.js +356 -0
- package/dist/chunker/image.js.map +1 -0
- package/dist/chunker/loader.js +5 -0
- package/dist/chunker/loader.js.map +1 -1
- package/dist/chunker/xml.js +1 -1
- package/dist/chunker/xml.js.map +1 -1
- package/dist/cli.js +123 -108
- package/dist/cli.js.map +1 -1
- package/dist/core/bootstrap.d.ts +18 -0
- package/dist/core/bootstrap.js +80 -0
- package/dist/core/bootstrap.js.map +1 -0
- package/dist/core/config.d.ts +15 -0
- package/dist/core/config.js +39 -3
- package/dist/core/config.js.map +1 -1
- package/dist/core/interfaces.d.ts +1 -0
- package/dist/core/resolve-api-key.js +3 -0
- package/dist/core/resolve-api-key.js.map +1 -1
- package/dist/core/runtime-overrides.d.ts +5 -0
- package/dist/core/runtime-overrides.js +17 -0
- package/dist/core/runtime-overrides.js.map +1 -1
- package/dist/describer/describer.js +1 -1
- package/dist/describer/describer.js.map +1 -1
- package/dist/embedder/health.d.ts +25 -0
- package/dist/embedder/health.js +346 -0
- package/dist/embedder/health.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/indexer.d.ts +1 -1
- package/dist/indexer.js +192 -22
- package/dist/indexer.js.map +1 -1
- package/dist/mcp/server.js +7 -61
- package/dist/mcp/server.js.map +1 -1
- package/dist/retriever/keyword-index.js +38 -5
- package/dist/retriever/keyword-index.js.map +1 -1
- package/dist/web/api.d.ts +8 -0
- package/dist/web/api.js +81 -0
- package/dist/web/api.js.map +1 -1
- package/dist/web/ui/index.html +503 -98
- package/package.json +3 -2
package/dist/web/ui/index.html
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
<link rel="stylesheet" href="/ui/app.css">
|
|
8
8
|
<link rel="stylesheet" href="/ui/github-dark.css">
|
|
9
9
|
<script src="/ui/highlight.min.js"></script>
|
|
10
|
+
<style>
|
|
11
|
+
.chart-svg text { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; }
|
|
12
|
+
.chart-svg rect { transition: opacity 0.15s; }
|
|
13
|
+
.chart-svg rect:hover { opacity: 0.85; }
|
|
14
|
+
.chart-svg circle { transition: r 0.15s; }
|
|
15
|
+
.chart-svg circle:hover { r: 5; }
|
|
16
|
+
</style>
|
|
10
17
|
</head>
|
|
11
18
|
<body class="h-screen flex flex-col overflow-hidden">
|
|
12
19
|
<!-- Top Bar -->
|
|
@@ -16,7 +23,6 @@
|
|
|
16
23
|
<button id="nav-dashboard" class="nav-btn active px-3 py-1 rounded text-sm font-medium transition">Dashboard</button>
|
|
17
24
|
<button id="nav-chunks" class="nav-btn px-3 py-1 rounded text-sm font-medium hover:bg-slate-700 transition">Chunks</button>
|
|
18
25
|
<button id="nav-files" class="nav-btn px-3 py-1 rounded text-sm font-medium hover:bg-slate-700 transition">Files</button>
|
|
19
|
-
<button id="nav-compare" class="nav-btn px-3 py-1 rounded text-sm font-medium hover:bg-slate-700 transition">Compare</button>
|
|
20
26
|
<button id="nav-evaluate" class="nav-btn px-3 py-1 rounded text-sm font-medium hover:bg-slate-700 transition">Evaluate</button>
|
|
21
27
|
</nav>
|
|
22
28
|
<div class="flex-1"></div>
|
|
@@ -49,8 +55,7 @@
|
|
|
49
55
|
<div id="chunks-detail" class="w-1/2 overflow-y-auto scrollbar-thin pl-1"></div>
|
|
50
56
|
</div>
|
|
51
57
|
<div id="view-files" class="h-full overflow-y-auto scrollbar-thin"></div>
|
|
52
|
-
<div id="view-
|
|
53
|
-
<div id="view-evaluate" class="hidden h-full overflow-y-auto scrollbar-thin"></div>
|
|
58
|
+
<div id="view-evaluate" class="hidden h-full flex flex-col"></div>
|
|
54
59
|
</main>
|
|
55
60
|
</div>
|
|
56
61
|
|
|
@@ -77,6 +82,9 @@
|
|
|
77
82
|
async evalSession(id) { const r = await fetch('/api/eval/sessions/' + encodeURIComponent(id)); return r.json(); },
|
|
78
83
|
async evalDeleteSession(id) { const r = await fetch('/api/eval/sessions/' + encodeURIComponent(id), { method: 'DELETE' }); return r.json(); },
|
|
79
84
|
async evalCompare(a, b) { const p = new URLSearchParams({a, b}); const r = await fetch('/api/eval/compare?' + p); return r.json(); },
|
|
85
|
+
async evalAnalysis(id) { const r = await fetch('/api/eval/sessions/' + encodeURIComponent(id) + '/analysis'); return r.json(); },
|
|
86
|
+
async evalTokenCompare(a, b) { const p = new URLSearchParams({a, b}); const r = await fetch('/api/eval/token-compare?' + p); return r.json(); },
|
|
87
|
+
async evalProjectSavings(params) { const r = await fetch('/api/eval/project-savings', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(params) }); return r.json(); },
|
|
80
88
|
};
|
|
81
89
|
|
|
82
90
|
const state = {
|
|
@@ -86,7 +94,7 @@
|
|
|
86
94
|
selectedChunkId: null,
|
|
87
95
|
chunkOffset: 0,
|
|
88
96
|
chunkLimit: 50,
|
|
89
|
-
|
|
97
|
+
|
|
90
98
|
collapsedDirs: new Set(),
|
|
91
99
|
stats: null,
|
|
92
100
|
files: [],
|
|
@@ -139,6 +147,213 @@
|
|
|
139
147
|
});
|
|
140
148
|
}
|
|
141
149
|
|
|
150
|
+
// ── SVG Chart Helpers ──────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
const CHART_COLORS = {
|
|
153
|
+
input: '#3b82f6', // blue-500
|
|
154
|
+
output: '#a855f7', // purple-500
|
|
155
|
+
rag: '#06b6d4', // cyan-500
|
|
156
|
+
reasoning: '#f59e0b', // amber-500
|
|
157
|
+
cache: '#64748b', // slate-500
|
|
158
|
+
cost: '#22c55e', // green-500
|
|
159
|
+
time: '#f97316', // orange-500
|
|
160
|
+
sessionA: '#3b82f6',
|
|
161
|
+
sessionB: '#06b6d4',
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
function svgAttr(attrs) {
|
|
165
|
+
return Object.entries(attrs).map(([k, v]) => `${k}="${v}"`).join(' ');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function svgRect(x, y, w, h, fill, extra = '') {
|
|
169
|
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${fill}" ${extra}/>`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function svgText(x, y, text, extra = '') {
|
|
173
|
+
return `<text x="${x}" y="${y}" ${extra}>${escapeHtml(String(text))}</text>`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function svgLine(x1, y1, x2, y2, extra = '') {
|
|
177
|
+
return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" ${extra}/>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function svgCircle(cx, cy, r, extra = '') {
|
|
181
|
+
return `<circle cx="${cx}" cy="${cy}" r="${r}" ${extra}/>`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function svgPath(d, extra = '') {
|
|
185
|
+
return `<path d="${d}" ${extra}/>`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function svgDonutArc(cx, cy, r, startAngle, endAngle, fill, innerR) {
|
|
189
|
+
if (endAngle - startAngle >= 359.99) {
|
|
190
|
+
return svgCircle(cx, cy, r, `fill="none" stroke="${fill}" stroke-width="${r - (innerR || r * 0.6)}"`) +
|
|
191
|
+
svgCircle(cx, cy, innerR || r * 0.6, `fill="#0f172a"`);
|
|
192
|
+
}
|
|
193
|
+
const toRad = a => (a - 90) * Math.PI / 180;
|
|
194
|
+
const x1 = cx + r * Math.cos(toRad(startAngle));
|
|
195
|
+
const y1 = cy + r * Math.sin(toRad(startAngle));
|
|
196
|
+
const x2 = cx + r * Math.cos(toRad(endAngle));
|
|
197
|
+
const y2 = cy + r * Math.sin(toRad(endAngle));
|
|
198
|
+
const ix1 = cx + (innerR || r * 0.6) * Math.cos(toRad(endAngle));
|
|
199
|
+
const iy1 = cy + (innerR || r * 0.6) * Math.sin(toRad(endAngle));
|
|
200
|
+
const ix2 = cx + (innerR || r * 0.6) * Math.cos(toRad(startAngle));
|
|
201
|
+
const iy2 = cy + (innerR || r * 0.6) * Math.sin(toRad(startAngle));
|
|
202
|
+
const large = endAngle - startAngle > 180 ? 1 : 0;
|
|
203
|
+
const d = `M${x1},${y1} A${r},${r} 0 ${large} 1 ${x2},${y2} L${ix1},${iy1} A${innerR || r * 0.6},${innerR || r * 0.6} 0 ${large} 0 ${ix2},${iy2} Z`;
|
|
204
|
+
return svgPath(d, `fill="${fill}"`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function chartLegend(items) {
|
|
208
|
+
return `<div class="flex flex-wrap gap-3 mt-2">${items.map(([label, color]) =>
|
|
209
|
+
`<div class="flex items-center gap-1.5 text-xs text-slate-400">
|
|
210
|
+
<span class="inline-block w-3 h-3 rounded-sm" style="background:${color}"></span>${escapeHtml(label)}
|
|
211
|
+
</div>`
|
|
212
|
+
).join('')}</div>`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Vertical Bar Chart: Session Comparison ─────────────────────────
|
|
216
|
+
|
|
217
|
+
function renderComparisonBars(a, b) {
|
|
218
|
+
const aName = truncate(a.title || a.sessionID.slice(0, 8), 14);
|
|
219
|
+
const bName = truncate(b.title || b.sessionID.slice(0, 8), 14);
|
|
220
|
+
|
|
221
|
+
const aIn = a.totalTokens.input + a.totalTokens.cacheRead;
|
|
222
|
+
const bIn = b.totalTokens.input + b.totalTokens.cacheRead;
|
|
223
|
+
const aTotalTokens = aIn + a.totalTokens.output + a.totalTokens.reasoning;
|
|
224
|
+
const bTotalTokens = bIn + b.totalTokens.output + b.totalTokens.reasoning;
|
|
225
|
+
|
|
226
|
+
const aRagToolCount = (a.toolCallCounts?.search_semantic || 0) + (a.toolCallCounts?.get_file_skeleton || 0) + (a.toolCallCounts?.find_usages || 0);
|
|
227
|
+
const bRagToolCount = (b.toolCallCounts?.search_semantic || 0) + (b.toolCallCounts?.get_file_skeleton || 0) + (b.toolCallCounts?.find_usages || 0);
|
|
228
|
+
|
|
229
|
+
const metrics = [
|
|
230
|
+
{ label: 'Total Tokens', aVal: aTotalTokens, bVal: bTotalTokens, color: CHART_COLORS.input, stacked: [
|
|
231
|
+
{ label: 'Input', color: CHART_COLORS.input, aFn: () => aIn - a.ragContextTokens, bFn: () => bIn - b.ragContextTokens },
|
|
232
|
+
{ label: 'RAG Ctx', color: CHART_COLORS.rag, aFn: () => a.ragContextTokens, bFn: () => b.ragContextTokens },
|
|
233
|
+
{ label: 'Output', color: CHART_COLORS.output, aFn: () => a.totalTokens.output, bFn: () => b.totalTokens.output },
|
|
234
|
+
{ label: 'Reasoning', color: CHART_COLORS.reasoning, aFn: () => a.totalTokens.reasoning, bFn: () => b.totalTokens.reasoning },
|
|
235
|
+
]},
|
|
236
|
+
{ label: 'Cost', aVal: a.totalCost, bVal: b.totalCost, color: CHART_COLORS.cost, format: formatCost },
|
|
237
|
+
{ label: 'Avg Response', aVal: a.avgResponseTimeMs || 0, bVal: b.avgResponseTimeMs || 0, color: CHART_COLORS.time, format: formatMs },
|
|
238
|
+
{ label: 'Messages', aVal: a.messageCount, bVal: b.messageCount, color: '#94a3b8' },
|
|
239
|
+
{ label: 'RAG Injections', aVal: a.ragContextCount, bVal: b.ragContextCount, color: CHART_COLORS.rag },
|
|
240
|
+
{ label: 'RAG Tool Calls', aVal: aRagToolCount, bVal: bRagToolCount, color: CHART_COLORS.rag },
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const n = metrics.length;
|
|
244
|
+
const svgW = 700, svgH = 260;
|
|
245
|
+
const padL = 10, padR = 20, padTop = 30, padBot = 50;
|
|
246
|
+
const chartW = svgW - padL - padR;
|
|
247
|
+
const chartH = svgH - padTop - padBot;
|
|
248
|
+
const groupW = chartW / n;
|
|
249
|
+
const barW = Math.min(groupW * 0.3, 40);
|
|
250
|
+
const barGap = 4;
|
|
251
|
+
const baseline = padTop + chartH;
|
|
252
|
+
|
|
253
|
+
const maxGlobal = Math.max(...metrics.map(m => Math.max(m.aVal, m.bVal)), 1);
|
|
254
|
+
|
|
255
|
+
const gridLines = [0.25, 0.5, 0.75, 1].map(pct => {
|
|
256
|
+
const y = baseline - chartH * pct;
|
|
257
|
+
return svgLine(padL, y, svgW - padR, y, 'stroke="#1e293b" stroke-width="1"');
|
|
258
|
+
}).join('');
|
|
259
|
+
|
|
260
|
+
const axisLine = svgLine(padL, baseline, svgW - padR, baseline, 'stroke="#334155" stroke-width="1"');
|
|
261
|
+
|
|
262
|
+
const barGroups = metrics.map((m, i) => {
|
|
263
|
+
const cx = padL + groupW * i + groupW / 2;
|
|
264
|
+
const aBarX = cx - barW - barGap / 2;
|
|
265
|
+
const bBarX = cx + barGap / 2;
|
|
266
|
+
const scale = chartH / maxGlobal;
|
|
267
|
+
const aH = m.aVal * scale;
|
|
268
|
+
const bH = m.bVal * scale;
|
|
269
|
+
|
|
270
|
+
const fmt = m.format || formatTokens;
|
|
271
|
+
const aLabel = fmt(m.aVal);
|
|
272
|
+
const bLabel = fmt(m.bVal);
|
|
273
|
+
|
|
274
|
+
let bars;
|
|
275
|
+
if (m.stacked) {
|
|
276
|
+
const stackScale = chartH / maxGlobal;
|
|
277
|
+
let aStackY = baseline, bStackY = baseline;
|
|
278
|
+
const aStackBars = m.stacked.filter(s => s.aFn() > 0).map(s => {
|
|
279
|
+
const val = s.aFn();
|
|
280
|
+
const h = val * stackScale;
|
|
281
|
+
aStackY -= h;
|
|
282
|
+
return svgRect(aBarX, aStackY, barW, h, s.color, `rx="2"`);
|
|
283
|
+
}).join('');
|
|
284
|
+
const bStackBars = m.stacked.filter(s => s.bFn() > 0).map(s => {
|
|
285
|
+
const val = s.bFn();
|
|
286
|
+
const h = val * stackScale;
|
|
287
|
+
bStackY -= h;
|
|
288
|
+
return svgRect(bBarX, bStackY, barW, h, s.color, `rx="2"`);
|
|
289
|
+
}).join('');
|
|
290
|
+
bars = aStackBars + bStackBars;
|
|
291
|
+
} else {
|
|
292
|
+
bars = svgRect(aBarX, baseline - aH, barW, aH, m.color, `rx="2"`) +
|
|
293
|
+
svgRect(bBarX, baseline - bH, barW, bH, m.color === CHART_COLORS.cost ? '#16a34a' : m.color === CHART_COLORS.time ? '#ea580c' : CHART_COLORS.sessionB, `rx="2" opacity="0.7"`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return `
|
|
297
|
+
${bars}
|
|
298
|
+
<text x="${cx}" y="${baseline + 14}" text-anchor="middle" fill="#94a3b8" font-size="9" font-family="inherit">${escapeHtml(m.label)}</text>
|
|
299
|
+
${m.aVal > 0 ? svgText(aBarX + barW / 2, baseline - aH - 4, aLabel, 'text-anchor="middle" fill="#94a3b8" font-size="8" font-family="inherit"') : ''}
|
|
300
|
+
${m.bVal > 0 ? svgText(bBarX + barW / 2, baseline - bH - 4, bLabel, 'text-anchor="middle" fill="#94a3b8" font-size="8" font-family="inherit"') : ''}
|
|
301
|
+
`;
|
|
302
|
+
}).join('');
|
|
303
|
+
|
|
304
|
+
return `
|
|
305
|
+
<div class="bg-slate-900 rounded-lg border border-slate-700 p-4 mb-4">
|
|
306
|
+
<h4 class="text-sm font-semibold text-white mb-3">Session Comparison</h4>
|
|
307
|
+
<svg width="100%" viewBox="0 0 ${svgW} ${svgH}" class="chart-svg">
|
|
308
|
+
${gridLines}${axisLine}${barGroups}
|
|
309
|
+
</svg>
|
|
310
|
+
<div class="flex gap-6 mt-1 justify-center">
|
|
311
|
+
${chartLegend([[aName, CHART_COLORS.sessionA], [bName, CHART_COLORS.sessionB]])}
|
|
312
|
+
</div>
|
|
313
|
+
</div>`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── Donut Chart: Token Composition (Session Detail) ────────────────
|
|
317
|
+
|
|
318
|
+
function renderTokenDonut(s) {
|
|
319
|
+
const totalIn = s.totalTokens.input + s.totalTokens.cacheRead;
|
|
320
|
+
const segments = [
|
|
321
|
+
{ label: 'Input (non-RAG)', value: totalIn - s.ragContextTokens, color: CHART_COLORS.input },
|
|
322
|
+
{ label: 'RAG Context', value: s.ragContextTokens, color: CHART_COLORS.rag },
|
|
323
|
+
{ label: 'Output', value: s.totalTokens.output, color: CHART_COLORS.output },
|
|
324
|
+
{ label: 'Reasoning', value: s.totalTokens.reasoning, color: CHART_COLORS.reasoning },
|
|
325
|
+
{ label: 'Cache Read', value: s.totalTokens.cacheRead, color: CHART_COLORS.cache },
|
|
326
|
+
].filter(s => s.value > 0);
|
|
327
|
+
|
|
328
|
+
const total = segments.reduce((sum, s) => sum + s.value, 0) || 1;
|
|
329
|
+
const cx = 90, cy = 90, r = 75, innerR = 48;
|
|
330
|
+
|
|
331
|
+
let angle = 0;
|
|
332
|
+
const arcs = segments.map(seg => {
|
|
333
|
+
const sweep = (seg.value / total) * 360;
|
|
334
|
+
const arc = svgDonutArc(cx, cy, r, angle, angle + sweep, seg.color, innerR);
|
|
335
|
+
angle += sweep;
|
|
336
|
+
return arc;
|
|
337
|
+
}).join('');
|
|
338
|
+
|
|
339
|
+
const pctLabel = total >= 1000000 ? (total / 1000000).toFixed(1) + 'M' : total >= 1000 ? (total / 1000).toFixed(1) + 'k' : String(total);
|
|
340
|
+
|
|
341
|
+
return `
|
|
342
|
+
<div class="flex items-center gap-6 mb-4">
|
|
343
|
+
<div class="shrink-0">
|
|
344
|
+
<svg width="180" height="180" viewBox="0 0 180 180" class="chart-svg">
|
|
345
|
+
${arcs}
|
|
346
|
+
<text x="${cx}" y="${cy - 4}" text-anchor="middle" fill="white" font-size="16" font-weight="bold" font-family="inherit">${pctLabel}</text>
|
|
347
|
+
<text x="${cx}" y="${cy + 12}" text-anchor="middle" fill="#64748b" font-size="9" font-family="inherit">tokens</text>
|
|
348
|
+
</svg>
|
|
349
|
+
</div>
|
|
350
|
+
<div class="flex-1">
|
|
351
|
+
${chartLegend(segments.map(s => [`${s.label}: ${formatTokens(s.value)} (${((s.value / total) * 100).toFixed(1)}%)`, s.color]))}
|
|
352
|
+
</div>
|
|
353
|
+
</div>`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
|
|
142
357
|
async function renderDashboard() {
|
|
143
358
|
const [stats, files] = await Promise.all([API.stats(), API.files()]);
|
|
144
359
|
state.stats = stats;
|
|
@@ -209,7 +424,6 @@
|
|
|
209
424
|
<table class="w-full text-sm">
|
|
210
425
|
<thead>
|
|
211
426
|
<tr class="bg-slate-800 text-slate-400 text-xs">
|
|
212
|
-
<th class="px-3 py-2 text-left w-8"></th>
|
|
213
427
|
<th class="px-3 py-2 text-left">File</th>
|
|
214
428
|
<th class="px-3 py-2 text-left w-20">Lang</th>
|
|
215
429
|
<th class="px-3 py-2 text-left">Description</th>
|
|
@@ -218,11 +432,9 @@
|
|
|
218
432
|
<tbody>
|
|
219
433
|
${data.chunks.map((c, i) => {
|
|
220
434
|
const id = c.id || `chunk-${i}`;
|
|
221
|
-
const checked = state.compareIds.has(id) ? 'checked' : '';
|
|
222
435
|
const selected = state.selectedChunkId === id ? ' selected' : '';
|
|
223
436
|
return `
|
|
224
437
|
<tr class="chunk-row${selected} border-t border-slate-800 cursor-pointer" data-id="${id}" data-index="${i}">
|
|
225
|
-
<td class="px-3 py-2"><input type="checkbox" class="compare-check rounded" data-id="${id}" ${checked}></td>
|
|
226
438
|
<td class="px-3 py-2 text-yellow-400 font-mono text-xs">${truncate(c.filePath, 40)}:${c.startLine}-${c.endLine}</td>
|
|
227
439
|
<td class="px-3 py-2">${langBadge(c.language)}</td>
|
|
228
440
|
<td class="px-3 py-2 text-slate-400 text-xs">${truncate(c.description || '', 50)}</td>
|
|
@@ -241,7 +453,6 @@
|
|
|
241
453
|
// Row click -> select and show detail
|
|
242
454
|
$$('.chunk-row').forEach(row => {
|
|
243
455
|
row.addEventListener('click', (e) => {
|
|
244
|
-
if (e.target.type === 'checkbox') return;
|
|
245
456
|
const id = row.dataset.id;
|
|
246
457
|
const idx = parseInt(row.dataset.index, 10);
|
|
247
458
|
if (!id) return;
|
|
@@ -252,14 +463,6 @@
|
|
|
252
463
|
});
|
|
253
464
|
});
|
|
254
465
|
|
|
255
|
-
$$('.compare-check').forEach(cb => {
|
|
256
|
-
cb.addEventListener('change', (e) => {
|
|
257
|
-
const id = e.target.dataset.id;
|
|
258
|
-
if (e.target.checked) state.compareIds.add(id);
|
|
259
|
-
else state.compareIds.delete(id);
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
466
|
const prevBtn = $('#prev-page');
|
|
264
467
|
const nextBtn = $('#next-page');
|
|
265
468
|
if (prevBtn) prevBtn.addEventListener('click', () => { state.chunkOffset = Math.max(0, state.chunkOffset - state.chunkLimit); renderChunks(); });
|
|
@@ -485,52 +688,7 @@
|
|
|
485
688
|
});
|
|
486
689
|
}
|
|
487
690
|
|
|
488
|
-
async function renderCompare() {
|
|
489
|
-
const el = $('#view-compare');
|
|
490
|
-
const ids = [...state.compareIds];
|
|
491
691
|
|
|
492
|
-
if (ids.length === 0) {
|
|
493
|
-
el.innerHTML = `
|
|
494
|
-
<div class="text-center py-20">
|
|
495
|
-
<h2 class="text-lg font-semibold text-slate-400 mb-2">No chunks selected</h2>
|
|
496
|
-
<p class="text-sm text-slate-500">Go to the Chunks view, select 2-3 chunks using checkboxes, then come back here.</p>
|
|
497
|
-
</div>`;
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (ids.length > 3) {
|
|
502
|
-
el.innerHTML = `
|
|
503
|
-
<div class="text-center py-20">
|
|
504
|
-
<h2 class="text-lg font-semibold text-slate-400 mb-2">Too many selected</h2>
|
|
505
|
-
<p class="text-sm text-slate-500">Select at most 3 chunks to compare. Currently selected: ${ids.length}</p>
|
|
506
|
-
</div>`;
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
const data = await API.compare(ids);
|
|
511
|
-
|
|
512
|
-
el.innerHTML = `
|
|
513
|
-
<div class="flex items-center gap-4 mb-4">
|
|
514
|
-
<h2 class="text-lg font-semibold text-white">Compare</h2>
|
|
515
|
-
<span class="text-sm text-slate-400">${data.chunks.length} chunks</span>
|
|
516
|
-
<button id="clear-compare" class="text-xs text-slate-500 hover:text-white">Clear selection</button>
|
|
517
|
-
</div>
|
|
518
|
-
<div class="grid gap-4" style="grid-template-columns: repeat(${data.chunks.length}, 1fr)">
|
|
519
|
-
${data.chunks.map(c => `
|
|
520
|
-
<div class="bg-slate-900 rounded-lg border border-slate-700 overflow-hidden">
|
|
521
|
-
<div class="px-3 py-2 bg-slate-800 border-b border-slate-700">
|
|
522
|
-
<div class="text-xs text-yellow-400 font-mono">${c.filePath}:${c.startLine}-${c.endLine}</div>
|
|
523
|
-
<div class="flex items-center gap-2 mt-1">${langBadge(c.language)}</div>
|
|
524
|
-
</div>
|
|
525
|
-
${c.description ? `<div class="px-3 py-2 text-xs text-slate-400 border-b border-slate-800">${c.description}</div>` : ''}
|
|
526
|
-
<pre class="p-3 overflow-x-auto text-xs max-h-[600px] overflow-y-auto"><code class="language-${c.language}">${escapeHtml(c.content || '')}</code></pre>
|
|
527
|
-
</div>
|
|
528
|
-
`).join('')}
|
|
529
|
-
</div>`;
|
|
530
|
-
|
|
531
|
-
el.querySelectorAll('pre code').forEach(b => hljs.highlightElement(b));
|
|
532
|
-
$('#clear-compare')?.addEventListener('click', () => { state.compareIds.clear(); renderCompare(); });
|
|
533
|
-
}
|
|
534
692
|
|
|
535
693
|
async function selectChunkFromSearch(id) {
|
|
536
694
|
showView('chunks');
|
|
@@ -615,7 +773,7 @@
|
|
|
615
773
|
showView('files');
|
|
616
774
|
renderFiles();
|
|
617
775
|
});
|
|
618
|
-
|
|
776
|
+
|
|
619
777
|
$('#nav-evaluate')?.addEventListener('click', () => { showView('evaluate'); renderEvaluate(); });
|
|
620
778
|
|
|
621
779
|
// ── Evaluate View ──────────────────────────────────────────────────
|
|
@@ -660,13 +818,15 @@
|
|
|
660
818
|
|
|
661
819
|
if (sessions.length === 0) {
|
|
662
820
|
el.innerHTML = `
|
|
663
|
-
<div class="text-center
|
|
664
|
-
<div
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
821
|
+
<div class="text-center flex-1 flex items-center justify-center">
|
|
822
|
+
<div>
|
|
823
|
+
<div class="text-4xl mb-3">📊</div>
|
|
824
|
+
<h2 class="text-lg font-semibold text-slate-400 mb-2">No sessions recorded yet</h2>
|
|
825
|
+
<p class="text-sm text-slate-500 max-w-md mx-auto">
|
|
826
|
+
Sessions are logged automatically when the OpenCodeRAG plugin is active.
|
|
827
|
+
Use OpenCode normally — every conversation is captured here with token usage, timing, and RAG context data.
|
|
828
|
+
</p>
|
|
829
|
+
</div>
|
|
670
830
|
</div>`;
|
|
671
831
|
return;
|
|
672
832
|
}
|
|
@@ -674,16 +834,17 @@
|
|
|
674
834
|
const selected = state.evalSelectedSessions;
|
|
675
835
|
|
|
676
836
|
el.innerHTML = `
|
|
677
|
-
<div class="
|
|
678
|
-
<
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
837
|
+
<div id="eval-top" class="h-1/2 overflow-y-auto scrollbar-thin border-b border-slate-700">
|
|
838
|
+
<div class="flex items-center gap-4 mb-4">
|
|
839
|
+
<h2 class="text-lg font-semibold text-white">Evaluate</h2>
|
|
840
|
+
<span class="text-sm text-slate-400">${sessions.length} session${sessions.length === 1 ? '' : 's'}</span>
|
|
841
|
+
<div class="flex-1"></div>
|
|
842
|
+
<button id="eval-compare-btn" class="px-3 py-1 bg-brand-600 rounded text-sm font-medium hover:bg-brand-500 disabled:opacity-50 disabled:cursor-not-allowed" ${selected.size === 2 ? '' : 'disabled'}>Compare Selected (2)</button>
|
|
843
|
+
<button id="eval-select-all" class="px-2 py-1 bg-slate-700 rounded text-xs hover:bg-slate-600">Select All</button>
|
|
844
|
+
<button id="eval-clear-sel" class="px-2 py-1 bg-slate-700 rounded text-xs hover:bg-slate-600">Clear</button>
|
|
845
|
+
</div>
|
|
685
846
|
|
|
686
|
-
|
|
847
|
+
<div class="bg-slate-900 rounded-lg border border-slate-700 overflow-hidden">
|
|
687
848
|
<table class="w-full text-sm">
|
|
688
849
|
<thead>
|
|
689
850
|
<tr class="bg-slate-800 text-slate-400 text-xs">
|
|
@@ -722,10 +883,66 @@
|
|
|
722
883
|
}).join('')}
|
|
723
884
|
</tbody>
|
|
724
885
|
</table>
|
|
886
|
+
</div>
|
|
725
887
|
</div>
|
|
726
888
|
|
|
727
|
-
<div id="eval-
|
|
728
|
-
|
|
889
|
+
<div id="eval-bottom" class="h-1/2 overflow-y-auto scrollbar-thin">
|
|
890
|
+
<div id="eval-compare-area"></div>
|
|
891
|
+
<div id="eval-detail-area"></div>
|
|
892
|
+
|
|
893
|
+
<div id="eval-projection-area" class="mt-4">
|
|
894
|
+
<details class="group">
|
|
895
|
+
<summary class="text-sm font-semibold text-slate-400 cursor-pointer hover:text-white select-none mb-2">What-If Projection</summary>
|
|
896
|
+
<div class="bg-slate-900 rounded-lg border border-slate-700 p-4">
|
|
897
|
+
<div class="grid grid-cols-5 gap-4 mb-4">
|
|
898
|
+
<div>
|
|
899
|
+
<label class="block text-xs text-slate-500 mb-1">Avg Chunk Size (chars)</label>
|
|
900
|
+
<input type="range" id="proj-chunk-size" min="200" max="2000" step="100" value="600" class="w-full">
|
|
901
|
+
<div class="text-xs text-slate-400 text-center mt-1" id="proj-chunk-size-val">600</div>
|
|
902
|
+
</div>
|
|
903
|
+
<div>
|
|
904
|
+
<label class="block text-xs text-slate-500 mb-1">Chunks/Query</label>
|
|
905
|
+
<input type="range" id="proj-chunks-query" min="1" max="10" step="1" value="3" class="w-full">
|
|
906
|
+
<div class="text-xs text-slate-400 text-center mt-1" id="proj-chunks-query-val">3</div>
|
|
907
|
+
</div>
|
|
908
|
+
<div>
|
|
909
|
+
<label class="block text-xs text-slate-500 mb-1">Reads/Query (no RAG)</label>
|
|
910
|
+
<input type="range" id="proj-reads-without" min="0" max="10" step="0.5" value="2.5" class="w-full">
|
|
911
|
+
<div class="text-xs text-slate-400 text-center mt-1" id="proj-reads-without-val">2.5</div>
|
|
912
|
+
</div>
|
|
913
|
+
<div>
|
|
914
|
+
<label class="block text-xs text-slate-500 mb-1">Reads/Query (with RAG)</label>
|
|
915
|
+
<input type="range" id="proj-reads-with" min="0" max="10" step="0.5" value="0.5" class="w-full">
|
|
916
|
+
<div class="text-xs text-slate-400 text-center mt-1" id="proj-reads-with-val">0.5</div>
|
|
917
|
+
</div>
|
|
918
|
+
<div>
|
|
919
|
+
<label class="block text-xs text-slate-500 mb-1">Query Count</label>
|
|
920
|
+
<input type="range" id="proj-query-count" min="1" max="100" step="1" value="10" class="w-full">
|
|
921
|
+
<div class="text-xs text-slate-400 text-center mt-1" id="proj-query-count-val">10</div>
|
|
922
|
+
</div>
|
|
923
|
+
</div>
|
|
924
|
+
<div class="grid grid-cols-4 gap-3">
|
|
925
|
+
<div class="kpi-card p-3 text-center">
|
|
926
|
+
<div class="text-slate-500 text-xs mb-1">RAG Overhead</div>
|
|
927
|
+
<div class="text-lg font-bold text-yellow-400" id="proj-overhead">-</div>
|
|
928
|
+
</div>
|
|
929
|
+
<div class="kpi-card p-3 text-center">
|
|
930
|
+
<div class="text-slate-500 text-xs mb-1">Reads Saved</div>
|
|
931
|
+
<div class="text-lg font-bold text-green-400" id="proj-saved">-</div>
|
|
932
|
+
</div>
|
|
933
|
+
<div class="kpi-card p-3 text-center">
|
|
934
|
+
<div class="text-slate-500 text-xs mb-1">Net Savings</div>
|
|
935
|
+
<div class="text-lg font-bold" id="proj-net">-</div>
|
|
936
|
+
</div>
|
|
937
|
+
<div class="kpi-card p-3 text-center">
|
|
938
|
+
<div class="text-slate-500 text-xs mb-1">Verdict</div>
|
|
939
|
+
<div class="text-lg font-bold" id="proj-verdict">-</div>
|
|
940
|
+
</div>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
</details>
|
|
944
|
+
</div>
|
|
945
|
+
</div>
|
|
729
946
|
`;
|
|
730
947
|
|
|
731
948
|
// Row click -> show detail
|
|
@@ -761,6 +978,7 @@
|
|
|
761
978
|
// Compare button
|
|
762
979
|
$('#eval-compare-btn')?.addEventListener('click', () => {
|
|
763
980
|
const ids = [...selected];
|
|
981
|
+
console.log('[Compare] clicked, selected ids:', ids);
|
|
764
982
|
if (ids.length === 2) renderEvalComparison(ids[0], ids[1]);
|
|
765
983
|
});
|
|
766
984
|
|
|
@@ -783,6 +1001,57 @@
|
|
|
783
1001
|
btn.textContent = `Compare Selected (${selected.size})`;
|
|
784
1002
|
}
|
|
785
1003
|
}
|
|
1004
|
+
|
|
1005
|
+
// What-If Projection sliders
|
|
1006
|
+
let projDebounce = null;
|
|
1007
|
+
const projSliders = [
|
|
1008
|
+
{ id: 'proj-chunk-size', valId: 'proj-chunk-size-val' },
|
|
1009
|
+
{ id: 'proj-chunks-query', valId: 'proj-chunks-query-val' },
|
|
1010
|
+
{ id: 'proj-reads-without', valId: 'proj-reads-without-val' },
|
|
1011
|
+
{ id: 'proj-reads-with', valId: 'proj-reads-with-val' },
|
|
1012
|
+
{ id: 'proj-query-count', valId: 'proj-query-count-val' },
|
|
1013
|
+
];
|
|
1014
|
+
projSliders.forEach(({ id, valId }) => {
|
|
1015
|
+
const slider = document.getElementById(id);
|
|
1016
|
+
const label = document.getElementById(valId);
|
|
1017
|
+
if (!slider || !label) return;
|
|
1018
|
+
slider.addEventListener('input', () => {
|
|
1019
|
+
label.textContent = slider.value;
|
|
1020
|
+
if (projDebounce) clearTimeout(projDebounce);
|
|
1021
|
+
projDebounce = setTimeout(updateProjection, 200);
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
updateProjection();
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
async function updateProjection() {
|
|
1028
|
+
const params = {
|
|
1029
|
+
avgChunkSize: Number(document.getElementById('proj-chunk-size')?.value ?? 600),
|
|
1030
|
+
avgChunksPerQuery: Number(document.getElementById('proj-chunks-query')?.value ?? 3),
|
|
1031
|
+
avgReadsPerQueryWithoutRAG: Number(document.getElementById('proj-reads-without')?.value ?? 2.5),
|
|
1032
|
+
avgReadsPerQueryWithRAG: Number(document.getElementById('proj-reads-with')?.value ?? 0.5),
|
|
1033
|
+
queryCount: Number(document.getElementById('proj-query-count')?.value ?? 10),
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
const data = await API.evalProjectSavings(params);
|
|
1037
|
+
const p = data?.projection;
|
|
1038
|
+
if (!p) return;
|
|
1039
|
+
|
|
1040
|
+
const overheadEl = document.getElementById('proj-overhead');
|
|
1041
|
+
const savedEl = document.getElementById('proj-saved');
|
|
1042
|
+
const netEl = document.getElementById('proj-net');
|
|
1043
|
+
const verdictEl = document.getElementById('proj-verdict');
|
|
1044
|
+
|
|
1045
|
+
if (overheadEl) overheadEl.textContent = formatTokens(p.ragOverheadTokens);
|
|
1046
|
+
if (savedEl) savedEl.textContent = formatTokens(p.savedReadTokens);
|
|
1047
|
+
if (netEl) {
|
|
1048
|
+
netEl.textContent = (p.netSavings > 0 ? '+' : '') + formatTokens(p.netSavings);
|
|
1049
|
+
netEl.className = `text-lg font-bold ${p.netSavings > 0 ? 'text-green-400' : p.netSavings < 0 ? 'text-red-400' : 'text-slate-400'}`;
|
|
1050
|
+
}
|
|
1051
|
+
if (verdictEl) {
|
|
1052
|
+
verdictEl.textContent = p.isPositive ? 'SAVES' : 'COSTS';
|
|
1053
|
+
verdictEl.className = `text-lg font-bold ${p.isPositive ? 'text-green-400' : 'text-red-400'}`;
|
|
1054
|
+
}
|
|
786
1055
|
}
|
|
787
1056
|
|
|
788
1057
|
async function renderEvalDetail(sessionId) {
|
|
@@ -801,8 +1070,6 @@
|
|
|
801
1070
|
const totalIn = s.totalTokens.input + s.totalTokens.cacheRead;
|
|
802
1071
|
const totalAll = totalIn + s.totalTokens.output + s.totalTokens.reasoning;
|
|
803
1072
|
|
|
804
|
-
const toolEntries = Object.entries(s.toolCallCounts).sort((a, b) => b[1] - a[1]);
|
|
805
|
-
|
|
806
1073
|
area.innerHTML = `
|
|
807
1074
|
<div class="mb-4">
|
|
808
1075
|
<div class="flex items-center gap-3 mb-3">
|
|
@@ -833,6 +1100,8 @@
|
|
|
833
1100
|
</div>
|
|
834
1101
|
</div>
|
|
835
1102
|
|
|
1103
|
+
${renderTokenDonut(s)}
|
|
1104
|
+
|
|
836
1105
|
<div class="grid grid-cols-4 gap-3 mb-4">
|
|
837
1106
|
<div class="kpi-card p-3">
|
|
838
1107
|
<div class="text-slate-400 text-xs mb-1">Messages</div>
|
|
@@ -852,18 +1121,6 @@
|
|
|
852
1121
|
</div>
|
|
853
1122
|
</div>
|
|
854
1123
|
|
|
855
|
-
${toolEntries.length > 0 ? `
|
|
856
|
-
<div class="kpi-card p-3 mb-4">
|
|
857
|
-
<h4 class="text-xs font-semibold text-slate-400 mb-2">Tool Calls</h4>
|
|
858
|
-
<div class="flex flex-wrap gap-2">
|
|
859
|
-
${toolEntries.map(([name, count]) => {
|
|
860
|
-
const isRag = ['search_semantic','get_file_skeleton','find_usages','read'].includes(name);
|
|
861
|
-
const cls = isRag ? 'bg-cyan-900/50 text-cyan-300 border-cyan-700' : 'bg-slate-800 text-slate-400 border-slate-700';
|
|
862
|
-
return `<span class="inline-flex items-center gap-1 px-2 py-1 rounded text-xs border ${cls}">${escapeHtml(name)} <strong>${count}</strong></span>`;
|
|
863
|
-
}).join('')}
|
|
864
|
-
</div>
|
|
865
|
-
</div>` : ''}
|
|
866
|
-
|
|
867
1124
|
${s.models.length > 0 ? `
|
|
868
1125
|
<div class="kpi-card p-3 mb-4">
|
|
869
1126
|
<h4 class="text-xs font-semibold text-slate-400 mb-2">Models Used</h4>
|
|
@@ -873,6 +1130,8 @@
|
|
|
873
1130
|
</div>` : ''}
|
|
874
1131
|
</div>
|
|
875
1132
|
|
|
1133
|
+
<div id="token-analysis-area" class="mb-4"></div>
|
|
1134
|
+
|
|
876
1135
|
<div class="bg-slate-900 rounded-lg border border-slate-700 overflow-hidden">
|
|
877
1136
|
<div class="px-3 py-2 bg-slate-800 border-b border-slate-700">
|
|
878
1137
|
<span class="text-xs font-semibold text-slate-400">Event Timeline</span>
|
|
@@ -883,6 +1142,104 @@
|
|
|
883
1142
|
</div>
|
|
884
1143
|
</div>
|
|
885
1144
|
`;
|
|
1145
|
+
|
|
1146
|
+
renderTokenAnalysis(sessionId);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
async function renderTokenAnalysis(sessionId) {
|
|
1150
|
+
const area = $('#token-analysis-area');
|
|
1151
|
+
if (!area) return;
|
|
1152
|
+
|
|
1153
|
+
const data = await API.evalAnalysis(sessionId);
|
|
1154
|
+
if (!data || data.error || !data.analysis) {
|
|
1155
|
+
area.innerHTML = '';
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const a = data.analysis;
|
|
1160
|
+
const e = a.estimates;
|
|
1161
|
+
const savingsColor = e.netSavings > 0 ? 'text-green-400' : e.netSavings < 0 ? 'text-red-400' : 'text-slate-400';
|
|
1162
|
+
const savingsSign = e.netSavings > 0 ? '+' : '';
|
|
1163
|
+
|
|
1164
|
+
area.innerHTML = `
|
|
1165
|
+
<div class="bg-slate-900 rounded-lg border border-slate-700 p-4 mb-4">
|
|
1166
|
+
<h4 class="text-sm font-semibold text-white mb-3">Token Analysis</h4>
|
|
1167
|
+
|
|
1168
|
+
<div class="grid grid-cols-3 gap-3 mb-4">
|
|
1169
|
+
<div class="kpi-card p-3">
|
|
1170
|
+
<div class="text-slate-400 text-xs mb-1">Est. Tokens Without RAG</div>
|
|
1171
|
+
<div class="text-xl font-bold text-white">${formatTokens(e.tokensWithoutRAG)}</div>
|
|
1172
|
+
</div>
|
|
1173
|
+
<div class="kpi-card p-3">
|
|
1174
|
+
<div class="text-slate-400 text-xs mb-1">Tokens With RAG (actual)</div>
|
|
1175
|
+
<div class="text-xl font-bold text-white">${formatTokens(e.tokensWithRAG)}</div>
|
|
1176
|
+
</div>
|
|
1177
|
+
<div class="kpi-card p-3">
|
|
1178
|
+
<div class="text-slate-400 text-xs mb-1">Net Savings</div>
|
|
1179
|
+
<div class="text-xl font-bold ${savingsColor}">${savingsSign}${formatTokens(e.netSavings)}</div>
|
|
1180
|
+
<div class="text-xs ${savingsColor}">${savingsSign}${e.percentSavings}%</div>
|
|
1181
|
+
</div>
|
|
1182
|
+
</div>
|
|
1183
|
+
|
|
1184
|
+
<div class="grid grid-cols-4 gap-3 mb-4 text-xs">
|
|
1185
|
+
<div class="text-slate-400">
|
|
1186
|
+
<span class="block text-slate-500">RAG Context</span>
|
|
1187
|
+
<span class="text-white font-semibold">${formatTokens(a.totals.ragContextTokens)}</span>
|
|
1188
|
+
</div>
|
|
1189
|
+
<div class="text-slate-400">
|
|
1190
|
+
<span class="block text-slate-500">System Guidance</span>
|
|
1191
|
+
<span class="text-white font-semibold">${formatTokens(a.totals.systemGuidanceTokens)}</span>
|
|
1192
|
+
</div>
|
|
1193
|
+
<div class="text-slate-400">
|
|
1194
|
+
<span class="block text-slate-500">Read Tool Calls</span>
|
|
1195
|
+
<span class="text-white font-semibold">${a.totals.readToolCalls}</span>
|
|
1196
|
+
</div>
|
|
1197
|
+
<div class="text-slate-400">
|
|
1198
|
+
<span class="block text-slate-500">RAG Tool Calls</span>
|
|
1199
|
+
<span class="text-cyan-400 font-semibold">${a.totals.ragToolCalls}</span>
|
|
1200
|
+
</div>
|
|
1201
|
+
</div>
|
|
1202
|
+
|
|
1203
|
+
${a.breakdowns.length > 0 ? `
|
|
1204
|
+
<details class="group">
|
|
1205
|
+
<summary class="text-xs font-semibold text-slate-400 cursor-pointer hover:text-white select-none">Per-Query Breakdown (${a.breakdowns.length} queries)</summary>
|
|
1206
|
+
<div class="mt-2 overflow-x-auto">
|
|
1207
|
+
<table class="w-full text-xs">
|
|
1208
|
+
<thead>
|
|
1209
|
+
<tr class="text-slate-500 border-b border-slate-800">
|
|
1210
|
+
<th class="px-2 py-1 text-left">#</th>
|
|
1211
|
+
<th class="px-2 py-1 text-right">Input</th>
|
|
1212
|
+
<th class="px-2 py-1 text-right">Output</th>
|
|
1213
|
+
<th class="px-2 py-1 text-right">RAG Ctx</th>
|
|
1214
|
+
<th class="px-2 py-1 text-right">Chunks</th>
|
|
1215
|
+
<th class="px-2 py-1 text-right">Reads</th>
|
|
1216
|
+
<th class="px-2 py-1 text-right">RAG Tools</th>
|
|
1217
|
+
<th class="px-2 py-1 text-right">Score</th>
|
|
1218
|
+
<th class="px-2 py-1 text-right">Time</th>
|
|
1219
|
+
</tr>
|
|
1220
|
+
</thead>
|
|
1221
|
+
<tbody>
|
|
1222
|
+
${a.breakdowns.map((b, i) => {
|
|
1223
|
+
const hasRag = b.ragContextTokens > 0;
|
|
1224
|
+
const borderCls = hasRag ? 'border-l-2 border-l-cyan-600' : '';
|
|
1225
|
+
return `<tr class="border-b border-slate-800/50 ${borderCls}">
|
|
1226
|
+
<td class="px-2 py-1 text-slate-500">${i + 1}</td>
|
|
1227
|
+
<td class="px-2 py-1 text-right text-slate-300">${formatTokens(b.inputTokens)}</td>
|
|
1228
|
+
<td class="px-2 py-1 text-right text-slate-300">${formatTokens(b.outputTokens)}</td>
|
|
1229
|
+
<td class="px-2 py-1 text-right ${hasRag ? 'text-cyan-400' : 'text-slate-500'}">${hasRag ? formatTokens(b.ragContextTokens) : '-'}</td>
|
|
1230
|
+
<td class="px-2 py-1 text-right text-slate-400">${hasRag ? b.ragChunkCount : '-'}</td>
|
|
1231
|
+
<td class="px-2 py-1 text-right text-slate-400">${b.readToolCalls}</td>
|
|
1232
|
+
<td class="px-2 py-1 text-right ${b.ragToolCalls > 0 ? 'text-cyan-400' : 'text-slate-500'}">${b.ragToolCalls || '-'}</td>
|
|
1233
|
+
<td class="px-2 py-1 text-right text-slate-400">${b.ragTopScore > 0 ? b.ragTopScore.toFixed(2) : '-'}</td>
|
|
1234
|
+
<td class="px-2 py-1 text-right text-slate-500">${b.responseTimeMs > 0 ? formatMs(b.responseTimeMs) : '-'}</td>
|
|
1235
|
+
</tr>`;
|
|
1236
|
+
}).join('')}
|
|
1237
|
+
</tbody>
|
|
1238
|
+
</table>
|
|
1239
|
+
</div>
|
|
1240
|
+
</details>` : ''}
|
|
1241
|
+
</div>
|
|
1242
|
+
`;
|
|
886
1243
|
}
|
|
887
1244
|
|
|
888
1245
|
function renderTimelineEvent(ev) {
|
|
@@ -960,13 +1317,24 @@
|
|
|
960
1317
|
|
|
961
1318
|
area.innerHTML = '<div class="text-center py-4 text-slate-500">Loading comparison...</div>';
|
|
962
1319
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1320
|
+
let basicData, tokenData;
|
|
1321
|
+
try {
|
|
1322
|
+
[basicData, tokenData] = await Promise.all([
|
|
1323
|
+
API.evalCompare(idA, idB),
|
|
1324
|
+
API.evalTokenCompare(idA, idB),
|
|
1325
|
+
]);
|
|
1326
|
+
} catch (err) {
|
|
1327
|
+
area.innerHTML = `<div class="text-center py-4 text-red-400">Comparison failed: ${escapeHtml(String(err))}</div>`;
|
|
966
1328
|
return;
|
|
967
1329
|
}
|
|
968
1330
|
|
|
969
|
-
|
|
1331
|
+
if (!basicData || basicData.error) {
|
|
1332
|
+
area.innerHTML = `<div class="text-center py-4 text-red-400">${escapeHtml(basicData?.error || 'Comparison failed')}</div>`;
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
try {
|
|
1337
|
+
const { sessionA: a, sessionB: b, delta } = basicData;
|
|
970
1338
|
const aTotalIn = a.totalTokens.input + a.totalTokens.cacheRead;
|
|
971
1339
|
const bTotalIn = b.totalTokens.input + b.totalTokens.cacheRead;
|
|
972
1340
|
const aTotal = aTotalIn + a.totalTokens.output + a.totalTokens.reasoning;
|
|
@@ -992,6 +1360,14 @@
|
|
|
992
1360
|
{ label: 'Avg Response Time', a: a.avgResponseTimeMs || 0, b: b.avgResponseTimeMs || 0, fmt: formatMs, pct: true },
|
|
993
1361
|
];
|
|
994
1362
|
|
|
1363
|
+
// Add token analysis rows if available
|
|
1364
|
+
const tc = tokenData?.comparison;
|
|
1365
|
+
if (tc) {
|
|
1366
|
+
rows.push(
|
|
1367
|
+
{ label: 'Read Tool Calls', a: a.ragToolCalls || 0, b: b.ragToolCalls || 0, fmt: String, pct: false },
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
995
1371
|
area.innerHTML = `
|
|
996
1372
|
<div class="mt-6 mb-4">
|
|
997
1373
|
<div class="flex items-center gap-3 mb-3">
|
|
@@ -999,6 +1375,18 @@
|
|
|
999
1375
|
<button id="eval-close-compare" class="text-xs text-slate-500 hover:text-white ml-auto">Close</button>
|
|
1000
1376
|
</div>
|
|
1001
1377
|
|
|
1378
|
+
${tc ? `
|
|
1379
|
+
<div class="p-3 rounded-lg mb-4 ${tc.delta.inputTokens < 0 ? 'bg-green-900/20 border border-green-800' : tc.delta.inputTokens > 0 ? 'bg-red-900/20 border border-red-800' : 'bg-slate-800 border border-slate-700'}">
|
|
1380
|
+
<div class="text-sm font-semibold ${tc.delta.inputTokens < 0 ? 'text-green-300' : tc.delta.inputTokens > 0 ? 'text-red-300' : 'text-slate-300'}">${escapeHtml(tc.verdict)}</div>
|
|
1381
|
+
${tokenData?.ragOn?.estimates ? `
|
|
1382
|
+
<div class="text-xs text-slate-400 mt-1">
|
|
1383
|
+
Token savings estimate: ${tokenData.ragOn.estimates.netSavings > 0 ? '+' : ''}${formatTokens(tokenData.ragOn.estimates.netSavings)} (${tokenData.ragOn.estimates.percentSavings}%)
|
|
1384
|
+
for RAG-on session
|
|
1385
|
+
</div>` : ''}
|
|
1386
|
+
</div>` : ''}
|
|
1387
|
+
|
|
1388
|
+
${renderComparisonBars(a, b)}
|
|
1389
|
+
|
|
1002
1390
|
<div class="bg-slate-900 rounded-lg border border-slate-700 overflow-hidden">
|
|
1003
1391
|
<table class="w-full text-sm">
|
|
1004
1392
|
<thead>
|
|
@@ -1037,12 +1425,29 @@
|
|
|
1037
1425
|
: `RAG reduced context by ${formatTokens(Math.abs(delta.ragContextTokens))} tokens. The LLM processed ${formatTokens(Math.abs(delta.inputTokens))} fewer input tokens.`}
|
|
1038
1426
|
</div>
|
|
1039
1427
|
</div>` : ''}
|
|
1428
|
+
|
|
1429
|
+
${tokenData?.ragOn?.estimates ? `
|
|
1430
|
+
<div class="grid grid-cols-2 gap-3 mt-3">
|
|
1431
|
+
<div class="kpi-card p-3">
|
|
1432
|
+
<div class="text-slate-400 text-xs mb-1">RAG-On Session Savings</div>
|
|
1433
|
+
<div class="text-lg font-bold ${tokenData.ragOn.estimates.netSavings > 0 ? 'text-green-400' : 'text-red-400'}">${tokenData.ragOn.estimates.netSavings > 0 ? '+' : ''}${formatTokens(tokenData.ragOn.estimates.netSavings)}</div>
|
|
1434
|
+
<div class="text-xs text-slate-500">est. ${tokenData.ragOn.estimates.percentSavings}% ${tokenData.ragOn.estimates.netSavings > 0 ? 'saved' : 'overhead'}</div>
|
|
1435
|
+
</div>
|
|
1436
|
+
<div class="kpi-card p-3">
|
|
1437
|
+
<div class="text-slate-400 text-xs mb-1">RAG-Off Session Savings</div>
|
|
1438
|
+
<div class="text-lg font-bold ${tokenData.ragOff.estimates.netSavings > 0 ? 'text-green-400' : 'text-red-400'}">${tokenData.ragOff.estimates.netSavings > 0 ? '+' : ''}${formatTokens(tokenData.ragOff.estimates.netSavings)}</div>
|
|
1439
|
+
<div class="text-xs text-slate-500">est. ${tokenData.ragOff.estimates.percentSavings}% ${tokenData.ragOff.estimates.netSavings > 0 ? 'saved' : 'overhead'}</div>
|
|
1440
|
+
</div>
|
|
1441
|
+
</div>` : ''}
|
|
1040
1442
|
</div>
|
|
1041
1443
|
`;
|
|
1042
1444
|
|
|
1043
1445
|
$('#eval-close-compare')?.addEventListener('click', () => {
|
|
1044
1446
|
area.innerHTML = '';
|
|
1045
1447
|
});
|
|
1448
|
+
} catch (err) {
|
|
1449
|
+
area.innerHTML = `<div class="text-center py-4 text-red-400">Comparison rendering failed: ${escapeHtml(String(err))}</div>`;
|
|
1450
|
+
}
|
|
1046
1451
|
}
|
|
1047
1452
|
|
|
1048
1453
|
// Init
|