opencode-rag-plugin 1.10.6 → 1.12.1
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 +44 -6
- package/dist/api.d.ts +1 -0
- package/dist/api.js +1 -0
- 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/cli.js +220 -2
- package/dist/cli.js.map +1 -1
- package/dist/core/config.d.ts +17 -0
- package/dist/core/config.js +30 -5
- package/dist/core/config.js.map +1 -1
- package/dist/core/interfaces.d.ts +12 -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 +6 -0
- package/dist/core/runtime-overrides.js +20 -1
- 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 +18 -0
- package/dist/embedder/health.js +291 -0
- package/dist/embedder/health.js.map +1 -0
- package/dist/eval/index.d.ts +3 -0
- package/dist/eval/index.js +2 -0
- package/dist/eval/index.js.map +1 -1
- package/dist/eval/run-token-test.d.ts +7 -0
- package/dist/eval/run-token-test.js +288 -0
- package/dist/eval/run-token-test.js.map +1 -0
- package/dist/eval/token-analysis.d.ts +111 -0
- package/dist/eval/token-analysis.js +286 -0
- package/dist/eval/token-analysis.js.map +1 -0
- package/dist/eval/token-counter.d.ts +41 -0
- package/dist/eval/token-counter.js +102 -0
- package/dist/eval/token-counter.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 +65 -13
- package/dist/indexer.js.map +1 -1
- package/dist/opencode/tools.js +12 -5
- package/dist/opencode/tools.js.map +1 -1
- package/dist/plugin.js +71 -29
- package/dist/plugin.js.map +1 -1
- package/dist/retriever/keyword-index.d.ts +1 -0
- package/dist/retriever/keyword-index.js +11 -0
- package/dist/retriever/keyword-index.js.map +1 -1
- package/dist/retriever/retriever.d.ts +1 -0
- package/dist/retriever/retriever.js +49 -7
- package/dist/retriever/retriever.js.map +1 -1
- package/dist/tui.js +54 -5
- package/dist/tui.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/server.js +31 -0
- package/dist/web/server.js.map +1 -1
- package/dist/web/ui/app.css +1 -0
- package/dist/web/ui/github-dark.css +118 -0
- package/dist/web/ui/highlight.min.js +1213 -0
- package/dist/web/ui/index.html +504 -125
- package/package.json +8 -2
package/dist/web/ui/index.html
CHANGED
|
@@ -4,34 +4,15 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>OpenCodeRAG - Vector Database Explorer</title>
|
|
7
|
-
<
|
|
8
|
-
<link rel="stylesheet" href="
|
|
9
|
-
<script src="
|
|
10
|
-
<script>
|
|
11
|
-
tailwind.config = {
|
|
12
|
-
darkMode: 'class',
|
|
13
|
-
theme: {
|
|
14
|
-
extend: {
|
|
15
|
-
colors: {
|
|
16
|
-
brand: { 400: '#22d3ee', 500: '#06b6d4', 600: '#0891b2' }
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
</script>
|
|
7
|
+
<link rel="stylesheet" href="/ui/app.css">
|
|
8
|
+
<link rel="stylesheet" href="/ui/github-dark.css">
|
|
9
|
+
<script src="/ui/highlight.min.js"></script>
|
|
22
10
|
<style>
|
|
23
|
-
|
|
24
|
-
.
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
.
|
|
28
|
-
.chunk-row:hover { background: #1e293b; }
|
|
29
|
-
.chunk-row.selected { background: #1e293b; border-left-color: #06b6d4; }
|
|
30
|
-
.file-item:hover { background: #1e293b; }
|
|
31
|
-
.file-item.active { background: #1e293b; border-left: 2px solid #06b6d4; }
|
|
32
|
-
.kpi-card { background: #1e293b; border: 1px solid #334155; border-radius: 0.75rem; }
|
|
33
|
-
.nav-btn.active { background: #06b6d4; color: #0f172a; }
|
|
34
|
-
pre code.hljs { background: transparent; padding: 0; }
|
|
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; }
|
|
35
16
|
</style>
|
|
36
17
|
</head>
|
|
37
18
|
<body class="h-screen flex flex-col overflow-hidden">
|
|
@@ -42,7 +23,6 @@
|
|
|
42
23
|
<button id="nav-dashboard" class="nav-btn active px-3 py-1 rounded text-sm font-medium transition">Dashboard</button>
|
|
43
24
|
<button id="nav-chunks" class="nav-btn px-3 py-1 rounded text-sm font-medium hover:bg-slate-700 transition">Chunks</button>
|
|
44
25
|
<button id="nav-files" class="nav-btn px-3 py-1 rounded text-sm font-medium hover:bg-slate-700 transition">Files</button>
|
|
45
|
-
<button id="nav-compare" class="nav-btn px-3 py-1 rounded text-sm font-medium hover:bg-slate-700 transition">Compare</button>
|
|
46
26
|
<button id="nav-evaluate" class="nav-btn px-3 py-1 rounded text-sm font-medium hover:bg-slate-700 transition">Evaluate</button>
|
|
47
27
|
</nav>
|
|
48
28
|
<div class="flex-1"></div>
|
|
@@ -75,8 +55,7 @@
|
|
|
75
55
|
<div id="chunks-detail" class="w-1/2 overflow-y-auto scrollbar-thin pl-1"></div>
|
|
76
56
|
</div>
|
|
77
57
|
<div id="view-files" class="h-full overflow-y-auto scrollbar-thin"></div>
|
|
78
|
-
<div id="view-
|
|
79
|
-
<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>
|
|
80
59
|
</main>
|
|
81
60
|
</div>
|
|
82
61
|
|
|
@@ -103,6 +82,9 @@
|
|
|
103
82
|
async evalSession(id) { const r = await fetch('/api/eval/sessions/' + encodeURIComponent(id)); return r.json(); },
|
|
104
83
|
async evalDeleteSession(id) { const r = await fetch('/api/eval/sessions/' + encodeURIComponent(id), { method: 'DELETE' }); return r.json(); },
|
|
105
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(); },
|
|
106
88
|
};
|
|
107
89
|
|
|
108
90
|
const state = {
|
|
@@ -112,7 +94,7 @@
|
|
|
112
94
|
selectedChunkId: null,
|
|
113
95
|
chunkOffset: 0,
|
|
114
96
|
chunkLimit: 50,
|
|
115
|
-
|
|
97
|
+
|
|
116
98
|
collapsedDirs: new Set(),
|
|
117
99
|
stats: null,
|
|
118
100
|
files: [],
|
|
@@ -165,6 +147,213 @@
|
|
|
165
147
|
});
|
|
166
148
|
}
|
|
167
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
|
+
|
|
168
357
|
async function renderDashboard() {
|
|
169
358
|
const [stats, files] = await Promise.all([API.stats(), API.files()]);
|
|
170
359
|
state.stats = stats;
|
|
@@ -235,7 +424,6 @@
|
|
|
235
424
|
<table class="w-full text-sm">
|
|
236
425
|
<thead>
|
|
237
426
|
<tr class="bg-slate-800 text-slate-400 text-xs">
|
|
238
|
-
<th class="px-3 py-2 text-left w-8"></th>
|
|
239
427
|
<th class="px-3 py-2 text-left">File</th>
|
|
240
428
|
<th class="px-3 py-2 text-left w-20">Lang</th>
|
|
241
429
|
<th class="px-3 py-2 text-left">Description</th>
|
|
@@ -244,11 +432,9 @@
|
|
|
244
432
|
<tbody>
|
|
245
433
|
${data.chunks.map((c, i) => {
|
|
246
434
|
const id = c.id || `chunk-${i}`;
|
|
247
|
-
const checked = state.compareIds.has(id) ? 'checked' : '';
|
|
248
435
|
const selected = state.selectedChunkId === id ? ' selected' : '';
|
|
249
436
|
return `
|
|
250
437
|
<tr class="chunk-row${selected} border-t border-slate-800 cursor-pointer" data-id="${id}" data-index="${i}">
|
|
251
|
-
<td class="px-3 py-2"><input type="checkbox" class="compare-check rounded" data-id="${id}" ${checked}></td>
|
|
252
438
|
<td class="px-3 py-2 text-yellow-400 font-mono text-xs">${truncate(c.filePath, 40)}:${c.startLine}-${c.endLine}</td>
|
|
253
439
|
<td class="px-3 py-2">${langBadge(c.language)}</td>
|
|
254
440
|
<td class="px-3 py-2 text-slate-400 text-xs">${truncate(c.description || '', 50)}</td>
|
|
@@ -267,7 +453,6 @@
|
|
|
267
453
|
// Row click -> select and show detail
|
|
268
454
|
$$('.chunk-row').forEach(row => {
|
|
269
455
|
row.addEventListener('click', (e) => {
|
|
270
|
-
if (e.target.type === 'checkbox') return;
|
|
271
456
|
const id = row.dataset.id;
|
|
272
457
|
const idx = parseInt(row.dataset.index, 10);
|
|
273
458
|
if (!id) return;
|
|
@@ -278,14 +463,6 @@
|
|
|
278
463
|
});
|
|
279
464
|
});
|
|
280
465
|
|
|
281
|
-
$$('.compare-check').forEach(cb => {
|
|
282
|
-
cb.addEventListener('change', (e) => {
|
|
283
|
-
const id = e.target.dataset.id;
|
|
284
|
-
if (e.target.checked) state.compareIds.add(id);
|
|
285
|
-
else state.compareIds.delete(id);
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
|
|
289
466
|
const prevBtn = $('#prev-page');
|
|
290
467
|
const nextBtn = $('#next-page');
|
|
291
468
|
if (prevBtn) prevBtn.addEventListener('click', () => { state.chunkOffset = Math.max(0, state.chunkOffset - state.chunkLimit); renderChunks(); });
|
|
@@ -511,52 +688,7 @@
|
|
|
511
688
|
});
|
|
512
689
|
}
|
|
513
690
|
|
|
514
|
-
async function renderCompare() {
|
|
515
|
-
const el = $('#view-compare');
|
|
516
|
-
const ids = [...state.compareIds];
|
|
517
|
-
|
|
518
|
-
if (ids.length === 0) {
|
|
519
|
-
el.innerHTML = `
|
|
520
|
-
<div class="text-center py-20">
|
|
521
|
-
<h2 class="text-lg font-semibold text-slate-400 mb-2">No chunks selected</h2>
|
|
522
|
-
<p class="text-sm text-slate-500">Go to the Chunks view, select 2-3 chunks using checkboxes, then come back here.</p>
|
|
523
|
-
</div>`;
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
if (ids.length > 3) {
|
|
528
|
-
el.innerHTML = `
|
|
529
|
-
<div class="text-center py-20">
|
|
530
|
-
<h2 class="text-lg font-semibold text-slate-400 mb-2">Too many selected</h2>
|
|
531
|
-
<p class="text-sm text-slate-500">Select at most 3 chunks to compare. Currently selected: ${ids.length}</p>
|
|
532
|
-
</div>`;
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const data = await API.compare(ids);
|
|
537
|
-
|
|
538
|
-
el.innerHTML = `
|
|
539
|
-
<div class="flex items-center gap-4 mb-4">
|
|
540
|
-
<h2 class="text-lg font-semibold text-white">Compare</h2>
|
|
541
|
-
<span class="text-sm text-slate-400">${data.chunks.length} chunks</span>
|
|
542
|
-
<button id="clear-compare" class="text-xs text-slate-500 hover:text-white">Clear selection</button>
|
|
543
|
-
</div>
|
|
544
|
-
<div class="grid gap-4" style="grid-template-columns: repeat(${data.chunks.length}, 1fr)">
|
|
545
|
-
${data.chunks.map(c => `
|
|
546
|
-
<div class="bg-slate-900 rounded-lg border border-slate-700 overflow-hidden">
|
|
547
|
-
<div class="px-3 py-2 bg-slate-800 border-b border-slate-700">
|
|
548
|
-
<div class="text-xs text-yellow-400 font-mono">${c.filePath}:${c.startLine}-${c.endLine}</div>
|
|
549
|
-
<div class="flex items-center gap-2 mt-1">${langBadge(c.language)}</div>
|
|
550
|
-
</div>
|
|
551
|
-
${c.description ? `<div class="px-3 py-2 text-xs text-slate-400 border-b border-slate-800">${c.description}</div>` : ''}
|
|
552
|
-
<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>
|
|
553
|
-
</div>
|
|
554
|
-
`).join('')}
|
|
555
|
-
</div>`;
|
|
556
691
|
|
|
557
|
-
el.querySelectorAll('pre code').forEach(b => hljs.highlightElement(b));
|
|
558
|
-
$('#clear-compare')?.addEventListener('click', () => { state.compareIds.clear(); renderCompare(); });
|
|
559
|
-
}
|
|
560
692
|
|
|
561
693
|
async function selectChunkFromSearch(id) {
|
|
562
694
|
showView('chunks');
|
|
@@ -641,7 +773,7 @@
|
|
|
641
773
|
showView('files');
|
|
642
774
|
renderFiles();
|
|
643
775
|
});
|
|
644
|
-
|
|
776
|
+
|
|
645
777
|
$('#nav-evaluate')?.addEventListener('click', () => { showView('evaluate'); renderEvaluate(); });
|
|
646
778
|
|
|
647
779
|
// ── Evaluate View ──────────────────────────────────────────────────
|
|
@@ -686,13 +818,15 @@
|
|
|
686
818
|
|
|
687
819
|
if (sessions.length === 0) {
|
|
688
820
|
el.innerHTML = `
|
|
689
|
-
<div class="text-center
|
|
690
|
-
<div
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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>
|
|
696
830
|
</div>`;
|
|
697
831
|
return;
|
|
698
832
|
}
|
|
@@ -700,16 +834,17 @@
|
|
|
700
834
|
const selected = state.evalSelectedSessions;
|
|
701
835
|
|
|
702
836
|
el.innerHTML = `
|
|
703
|
-
<div class="
|
|
704
|
-
<
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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>
|
|
711
846
|
|
|
712
|
-
|
|
847
|
+
<div class="bg-slate-900 rounded-lg border border-slate-700 overflow-hidden">
|
|
713
848
|
<table class="w-full text-sm">
|
|
714
849
|
<thead>
|
|
715
850
|
<tr class="bg-slate-800 text-slate-400 text-xs">
|
|
@@ -748,10 +883,66 @@
|
|
|
748
883
|
}).join('')}
|
|
749
884
|
</tbody>
|
|
750
885
|
</table>
|
|
886
|
+
</div>
|
|
751
887
|
</div>
|
|
752
888
|
|
|
753
|
-
<div id="eval-
|
|
754
|
-
|
|
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>
|
|
755
946
|
`;
|
|
756
947
|
|
|
757
948
|
// Row click -> show detail
|
|
@@ -787,6 +978,7 @@
|
|
|
787
978
|
// Compare button
|
|
788
979
|
$('#eval-compare-btn')?.addEventListener('click', () => {
|
|
789
980
|
const ids = [...selected];
|
|
981
|
+
console.log('[Compare] clicked, selected ids:', ids);
|
|
790
982
|
if (ids.length === 2) renderEvalComparison(ids[0], ids[1]);
|
|
791
983
|
});
|
|
792
984
|
|
|
@@ -809,6 +1001,57 @@
|
|
|
809
1001
|
btn.textContent = `Compare Selected (${selected.size})`;
|
|
810
1002
|
}
|
|
811
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
|
+
}
|
|
812
1055
|
}
|
|
813
1056
|
|
|
814
1057
|
async function renderEvalDetail(sessionId) {
|
|
@@ -827,8 +1070,6 @@
|
|
|
827
1070
|
const totalIn = s.totalTokens.input + s.totalTokens.cacheRead;
|
|
828
1071
|
const totalAll = totalIn + s.totalTokens.output + s.totalTokens.reasoning;
|
|
829
1072
|
|
|
830
|
-
const toolEntries = Object.entries(s.toolCallCounts).sort((a, b) => b[1] - a[1]);
|
|
831
|
-
|
|
832
1073
|
area.innerHTML = `
|
|
833
1074
|
<div class="mb-4">
|
|
834
1075
|
<div class="flex items-center gap-3 mb-3">
|
|
@@ -859,6 +1100,8 @@
|
|
|
859
1100
|
</div>
|
|
860
1101
|
</div>
|
|
861
1102
|
|
|
1103
|
+
${renderTokenDonut(s)}
|
|
1104
|
+
|
|
862
1105
|
<div class="grid grid-cols-4 gap-3 mb-4">
|
|
863
1106
|
<div class="kpi-card p-3">
|
|
864
1107
|
<div class="text-slate-400 text-xs mb-1">Messages</div>
|
|
@@ -878,18 +1121,6 @@
|
|
|
878
1121
|
</div>
|
|
879
1122
|
</div>
|
|
880
1123
|
|
|
881
|
-
${toolEntries.length > 0 ? `
|
|
882
|
-
<div class="kpi-card p-3 mb-4">
|
|
883
|
-
<h4 class="text-xs font-semibold text-slate-400 mb-2">Tool Calls</h4>
|
|
884
|
-
<div class="flex flex-wrap gap-2">
|
|
885
|
-
${toolEntries.map(([name, count]) => {
|
|
886
|
-
const isRag = ['search_semantic','get_file_skeleton','find_usages','read'].includes(name);
|
|
887
|
-
const cls = isRag ? 'bg-cyan-900/50 text-cyan-300 border-cyan-700' : 'bg-slate-800 text-slate-400 border-slate-700';
|
|
888
|
-
return `<span class="inline-flex items-center gap-1 px-2 py-1 rounded text-xs border ${cls}">${escapeHtml(name)} <strong>${count}</strong></span>`;
|
|
889
|
-
}).join('')}
|
|
890
|
-
</div>
|
|
891
|
-
</div>` : ''}
|
|
892
|
-
|
|
893
1124
|
${s.models.length > 0 ? `
|
|
894
1125
|
<div class="kpi-card p-3 mb-4">
|
|
895
1126
|
<h4 class="text-xs font-semibold text-slate-400 mb-2">Models Used</h4>
|
|
@@ -899,6 +1130,8 @@
|
|
|
899
1130
|
</div>` : ''}
|
|
900
1131
|
</div>
|
|
901
1132
|
|
|
1133
|
+
<div id="token-analysis-area" class="mb-4"></div>
|
|
1134
|
+
|
|
902
1135
|
<div class="bg-slate-900 rounded-lg border border-slate-700 overflow-hidden">
|
|
903
1136
|
<div class="px-3 py-2 bg-slate-800 border-b border-slate-700">
|
|
904
1137
|
<span class="text-xs font-semibold text-slate-400">Event Timeline</span>
|
|
@@ -909,6 +1142,104 @@
|
|
|
909
1142
|
</div>
|
|
910
1143
|
</div>
|
|
911
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
|
+
`;
|
|
912
1243
|
}
|
|
913
1244
|
|
|
914
1245
|
function renderTimelineEvent(ev) {
|
|
@@ -986,13 +1317,24 @@
|
|
|
986
1317
|
|
|
987
1318
|
area.innerHTML = '<div class="text-center py-4 text-slate-500">Loading comparison...</div>';
|
|
988
1319
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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>`;
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (!basicData || basicData.error) {
|
|
1332
|
+
area.innerHTML = `<div class="text-center py-4 text-red-400">${escapeHtml(basicData?.error || 'Comparison failed')}</div>`;
|
|
992
1333
|
return;
|
|
993
1334
|
}
|
|
994
1335
|
|
|
995
|
-
|
|
1336
|
+
try {
|
|
1337
|
+
const { sessionA: a, sessionB: b, delta } = basicData;
|
|
996
1338
|
const aTotalIn = a.totalTokens.input + a.totalTokens.cacheRead;
|
|
997
1339
|
const bTotalIn = b.totalTokens.input + b.totalTokens.cacheRead;
|
|
998
1340
|
const aTotal = aTotalIn + a.totalTokens.output + a.totalTokens.reasoning;
|
|
@@ -1018,6 +1360,14 @@
|
|
|
1018
1360
|
{ label: 'Avg Response Time', a: a.avgResponseTimeMs || 0, b: b.avgResponseTimeMs || 0, fmt: formatMs, pct: true },
|
|
1019
1361
|
];
|
|
1020
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
|
+
|
|
1021
1371
|
area.innerHTML = `
|
|
1022
1372
|
<div class="mt-6 mb-4">
|
|
1023
1373
|
<div class="flex items-center gap-3 mb-3">
|
|
@@ -1025,6 +1375,18 @@
|
|
|
1025
1375
|
<button id="eval-close-compare" class="text-xs text-slate-500 hover:text-white ml-auto">Close</button>
|
|
1026
1376
|
</div>
|
|
1027
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
|
+
|
|
1028
1390
|
<div class="bg-slate-900 rounded-lg border border-slate-700 overflow-hidden">
|
|
1029
1391
|
<table class="w-full text-sm">
|
|
1030
1392
|
<thead>
|
|
@@ -1063,12 +1425,29 @@
|
|
|
1063
1425
|
: `RAG reduced context by ${formatTokens(Math.abs(delta.ragContextTokens))} tokens. The LLM processed ${formatTokens(Math.abs(delta.inputTokens))} fewer input tokens.`}
|
|
1064
1426
|
</div>
|
|
1065
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>` : ''}
|
|
1066
1442
|
</div>
|
|
1067
1443
|
`;
|
|
1068
1444
|
|
|
1069
1445
|
$('#eval-close-compare')?.addEventListener('click', () => {
|
|
1070
1446
|
area.innerHTML = '';
|
|
1071
1447
|
});
|
|
1448
|
+
} catch (err) {
|
|
1449
|
+
area.innerHTML = `<div class="text-center py-4 text-red-400">Comparison rendering failed: ${escapeHtml(String(err))}</div>`;
|
|
1450
|
+
}
|
|
1072
1451
|
}
|
|
1073
1452
|
|
|
1074
1453
|
// Init
|