@raquezha/notrace 0.0.4 → 0.0.5
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/CHANGELOG.md +7 -0
- package/README.md +59 -6
- package/dist/notrace.js +466 -337
- package/extensions/notrace.ts +500 -337
- package/package.json +1 -1
package/dist/notrace.js
CHANGED
|
@@ -14,7 +14,31 @@ function getCaptureMode() {
|
|
|
14
14
|
return "redacted";
|
|
15
15
|
}
|
|
16
16
|
function isSensitiveKey(key) {
|
|
17
|
-
|
|
17
|
+
const normalized = key.replace(/[^a-z0-9]/gi, "");
|
|
18
|
+
if (/^(inputtokens|outputtokens|totaltokens|prompttokens|completiontokens|reasoningtokens|cachedtokens|cachecreationinputtokens|cachereadinputtokens|cost|total|input|output|prompt|completion|reasoning|read|write)$/i.test(normalized)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return SENSITIVE_KEY_RE.test(normalized);
|
|
22
|
+
}
|
|
23
|
+
function sanitizeUsageValue(usage) {
|
|
24
|
+
if (getCaptureMode() === "full")
|
|
25
|
+
return usage;
|
|
26
|
+
if (!usage || typeof usage !== "object")
|
|
27
|
+
return sanitizeTraceValue(usage);
|
|
28
|
+
const source = usage;
|
|
29
|
+
const output = {};
|
|
30
|
+
for (const [key, value] of Object.entries(source)) {
|
|
31
|
+
if (typeof value === "number" || typeof value === "boolean" || value == null) {
|
|
32
|
+
output[key] = value;
|
|
33
|
+
}
|
|
34
|
+
else if (typeof value === "object" && value) {
|
|
35
|
+
output[key] = sanitizeUsageValue(value);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
output[key] = sanitizeTraceValue(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return output;
|
|
18
42
|
}
|
|
19
43
|
function redactString(value) {
|
|
20
44
|
const redacted = value.replace(SENSITIVE_VALUE_RE, REDACTED);
|
|
@@ -73,6 +97,172 @@ function escapeHtml(value) {
|
|
|
73
97
|
}
|
|
74
98
|
});
|
|
75
99
|
}
|
|
100
|
+
function ensureWorkflowDir(workflowDir) {
|
|
101
|
+
if (existsSync(workflowDir))
|
|
102
|
+
return;
|
|
103
|
+
try {
|
|
104
|
+
mkdirSync(workflowDir, { recursive: true, mode: 0o700 });
|
|
105
|
+
}
|
|
106
|
+
catch { }
|
|
107
|
+
}
|
|
108
|
+
function safePathSegment(value) {
|
|
109
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "-").slice(0, 160) || `session-${Date.now()}`;
|
|
110
|
+
}
|
|
111
|
+
function getNotraceLocation(cwd, sessionId) {
|
|
112
|
+
const workflowDir = path.resolve(cwd, ".workflow");
|
|
113
|
+
const notraceDir = path.resolve(cwd, ".notrace");
|
|
114
|
+
const outputDir = path.join(notraceDir, "sessions", safePathSegment(sessionId));
|
|
115
|
+
ensureWorkflowDir(workflowDir);
|
|
116
|
+
try {
|
|
117
|
+
const activeTaskJsonPath = path.join(workflowDir, "active_task.json");
|
|
118
|
+
if (existsSync(activeTaskJsonPath)) {
|
|
119
|
+
const content = JSON.parse(readFileSync(activeTaskJsonPath, "utf-8"));
|
|
120
|
+
const candidate = typeof content.taskPath === "string"
|
|
121
|
+
? safeResolveUnder(cwd, content.taskPath)
|
|
122
|
+
: typeof content.active_task === "string"
|
|
123
|
+
? safeResolveUnder(workflowDir, path.join("tasks", content.active_task))
|
|
124
|
+
: null;
|
|
125
|
+
if (candidate && safeResolveUnder(workflowDir, path.relative(workflowDir, candidate))) {
|
|
126
|
+
return {
|
|
127
|
+
workflowDir,
|
|
128
|
+
notraceDir,
|
|
129
|
+
outputDir,
|
|
130
|
+
taskDir: candidate,
|
|
131
|
+
taskId: typeof content.active_task === "string" ? content.active_task : path.basename(candidate),
|
|
132
|
+
taskPath: path.relative(cwd, candidate)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// fallback
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
workflowDir,
|
|
142
|
+
notraceDir,
|
|
143
|
+
outputDir,
|
|
144
|
+
taskDir: null,
|
|
145
|
+
taskId: null,
|
|
146
|
+
taskPath: null
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function updateNotraceIndex(cwd, location, entry) {
|
|
150
|
+
const indexPath = path.join(location.notraceDir, "index.json");
|
|
151
|
+
const base = {
|
|
152
|
+
schemaVersion: 1,
|
|
153
|
+
kind: "notrace-index",
|
|
154
|
+
workdir: ".",
|
|
155
|
+
sessions: []
|
|
156
|
+
};
|
|
157
|
+
try {
|
|
158
|
+
mkdirSync(location.notraceDir, { recursive: true, mode: 0o700 });
|
|
159
|
+
const existing = existsSync(indexPath)
|
|
160
|
+
? JSON.parse(readFileSync(indexPath, "utf-8"))
|
|
161
|
+
: base;
|
|
162
|
+
const sessions = Array.isArray(existing.sessions) ? existing.sessions : [];
|
|
163
|
+
const sessionId = entry.sessionId;
|
|
164
|
+
const nextSessions = sessions.filter((item) => item?.sessionId !== sessionId);
|
|
165
|
+
nextSessions.push(entry);
|
|
166
|
+
const next = {
|
|
167
|
+
...base,
|
|
168
|
+
...existing,
|
|
169
|
+
schemaVersion: 1,
|
|
170
|
+
kind: "notrace-index",
|
|
171
|
+
workdir: ".",
|
|
172
|
+
sessions: nextSessions
|
|
173
|
+
};
|
|
174
|
+
writeFileSync(indexPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// keep notrace non-fatal if index update fails
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function appendWorkLogEntry(taskDir, message) {
|
|
181
|
+
const workMd = path.join(taskDir, "WORK.md");
|
|
182
|
+
if (!existsSync(workMd))
|
|
183
|
+
return;
|
|
184
|
+
try {
|
|
185
|
+
const text = readFileSync(workMd, "utf-8");
|
|
186
|
+
const entry = `- ${new Date().toISOString()}: ${message}`;
|
|
187
|
+
if (!/^(## )?\[LOG\]\s*$/m.test(text)) {
|
|
188
|
+
writeFileSync(workMd, `${text.trimEnd()}\n\n## [LOG]\n${entry}\n`, { encoding: "utf-8" });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const lines = text.split("\n");
|
|
192
|
+
const logIndex = lines.findIndex((line) => /^(## )?\[LOG\]\s*$/.test(line));
|
|
193
|
+
if (logIndex === -1)
|
|
194
|
+
return;
|
|
195
|
+
let nextSectionIndex = lines.length;
|
|
196
|
+
for (let i = logIndex + 1; i < lines.length; i++) {
|
|
197
|
+
if (/^(## )?\[[A-Z0-9_-]+\]\s*$/.test(lines[i])) {
|
|
198
|
+
nextSectionIndex = i;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const before = lines.slice(0, nextSectionIndex);
|
|
203
|
+
const after = lines.slice(nextSectionIndex);
|
|
204
|
+
while (before.length > logIndex + 1 && before[before.length - 1]?.trim() === "") {
|
|
205
|
+
before.pop();
|
|
206
|
+
}
|
|
207
|
+
before.push(entry);
|
|
208
|
+
writeFileSync(workMd, `${[...before, ...after].join("\n").replace(/\n*$/, "\n")}`, { encoding: "utf-8" });
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// keep notrace non-fatal if WORK.md append fails
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function collectMetrics(events) {
|
|
215
|
+
let totalTokens = 0;
|
|
216
|
+
let inputTokens = 0;
|
|
217
|
+
let outputTokens = 0;
|
|
218
|
+
let totalCost = 0;
|
|
219
|
+
let toolCallCount = 0;
|
|
220
|
+
let toolErrorCount = 0;
|
|
221
|
+
let llmCallCount = 0;
|
|
222
|
+
let turnCount = 0;
|
|
223
|
+
const models = new Set();
|
|
224
|
+
const providers = new Set();
|
|
225
|
+
for (const event of events) {
|
|
226
|
+
if (event.type === "turn_start") {
|
|
227
|
+
turnCount++;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (event.type === "tool_start") {
|
|
231
|
+
toolCallCount++;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (event.type === "tool_end") {
|
|
235
|
+
if (event.isError)
|
|
236
|
+
toolErrorCount++;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (event.type === "llm_completion") {
|
|
240
|
+
llmCallCount++;
|
|
241
|
+
if (typeof event.model === "string" && event.model)
|
|
242
|
+
models.add(event.model);
|
|
243
|
+
if (typeof event.provider === "string" && event.provider)
|
|
244
|
+
providers.add(event.provider);
|
|
245
|
+
if (event.usage) {
|
|
246
|
+
inputTokens += event.usage.input || 0;
|
|
247
|
+
outputTokens += event.usage.output || 0;
|
|
248
|
+
totalTokens += event.usage.totalTokens || 0;
|
|
249
|
+
totalCost += event.usage.cost?.total || 0;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
totalTokens,
|
|
255
|
+
inputTokens,
|
|
256
|
+
outputTokens,
|
|
257
|
+
totalCost,
|
|
258
|
+
toolCallCount,
|
|
259
|
+
toolErrorCount,
|
|
260
|
+
llmCallCount,
|
|
261
|
+
turnCount,
|
|
262
|
+
models: [...models],
|
|
263
|
+
providers: [...providers]
|
|
264
|
+
};
|
|
265
|
+
}
|
|
76
266
|
/**
|
|
77
267
|
* html-observability extension
|
|
78
268
|
*
|
|
@@ -87,35 +277,6 @@ export default function (pi) {
|
|
|
87
277
|
let activeLlmPayload = null;
|
|
88
278
|
let llmStartTime = 0;
|
|
89
279
|
const activeToolTimes = {};
|
|
90
|
-
// Helper to extract active task path from .workflow/active_task.json.
|
|
91
|
-
// Reports are constrained to cwd/.workflow to avoid task metadata causing writes elsewhere.
|
|
92
|
-
function getActiveTaskDir(cwd) {
|
|
93
|
-
const workflowDir = path.resolve(cwd, ".workflow");
|
|
94
|
-
try {
|
|
95
|
-
const activeTaskJsonPath = path.join(workflowDir, "active_task.json");
|
|
96
|
-
if (existsSync(activeTaskJsonPath)) {
|
|
97
|
-
const content = JSON.parse(readFileSync(activeTaskJsonPath, "utf-8"));
|
|
98
|
-
const candidate = typeof content.taskPath === "string"
|
|
99
|
-
? safeResolveUnder(cwd, content.taskPath)
|
|
100
|
-
: typeof content.active_task === "string"
|
|
101
|
-
? safeResolveUnder(workflowDir, path.join("tasks", content.active_task))
|
|
102
|
-
: null;
|
|
103
|
-
if (candidate && safeResolveUnder(workflowDir, path.relative(workflowDir, candidate))) {
|
|
104
|
-
return candidate;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
// fallback
|
|
110
|
-
}
|
|
111
|
-
if (!existsSync(workflowDir)) {
|
|
112
|
-
try {
|
|
113
|
-
mkdirSync(workflowDir, { recursive: true, mode: 0o700 });
|
|
114
|
-
}
|
|
115
|
-
catch { }
|
|
116
|
-
}
|
|
117
|
-
return workflowDir;
|
|
118
|
-
}
|
|
119
280
|
// 1. Session start
|
|
120
281
|
pi.on("session_start", async (event, ctx) => {
|
|
121
282
|
traceId = ctx.sessionManager.getSessionId() || `session-${Date.now()}`;
|
|
@@ -185,7 +346,7 @@ export default function (pi) {
|
|
|
185
346
|
provider: message.provider || "unknown",
|
|
186
347
|
inputPayload: activeLlmPayload,
|
|
187
348
|
outputContent: getCaptureMode() === "metadata" ? "[metadata-only capture]" : sanitizeTraceValue(message.content),
|
|
188
|
-
usage:
|
|
349
|
+
usage: sanitizeUsageValue(message.usage),
|
|
189
350
|
durationMs,
|
|
190
351
|
timestamp: Date.now()
|
|
191
352
|
});
|
|
@@ -196,54 +357,101 @@ export default function (pi) {
|
|
|
196
357
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
197
358
|
const sessionEndTime = Date.now();
|
|
198
359
|
const totalDurationMs = sessionEndTime - sessionStartTime;
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
let llmCallCount = 0;
|
|
208
|
-
events.forEach((e) => {
|
|
209
|
-
if (e.type === "llm_completion") {
|
|
210
|
-
llmCallCount++;
|
|
211
|
-
if (e.usage) {
|
|
212
|
-
inputTokens += e.usage.input || 0;
|
|
213
|
-
outputTokens += e.usage.output || 0;
|
|
214
|
-
totalTokens += e.usage.totalTokens || 0;
|
|
215
|
-
totalCost += e.usage.cost?.total || 0;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
else if (e.type === "tool_start") {
|
|
219
|
-
toolCallCount++;
|
|
220
|
-
}
|
|
221
|
-
});
|
|
360
|
+
const projectName = process.env.PHOENIX_PROJECT_NAME || "pi-coding-agent";
|
|
361
|
+
const captureMode = getCaptureMode();
|
|
362
|
+
const location = getNotraceLocation(ctx.cwd, traceId);
|
|
363
|
+
const reportPath = path.join(location.outputDir, "notrace.html");
|
|
364
|
+
const recordPath = path.join(location.outputDir, "notrace.json");
|
|
365
|
+
const reviewPath = path.join(location.outputDir, "notrace.review.json");
|
|
366
|
+
const metrics = collectMetrics(events);
|
|
367
|
+
const sessionFile = ctx.sessionManager.getSessionFile() || null;
|
|
222
368
|
const htmlContent = generateHtmlReport({
|
|
223
369
|
traceId,
|
|
224
|
-
projectName
|
|
370
|
+
projectName,
|
|
225
371
|
startTime: new Date(sessionStartTime).toISOString(),
|
|
226
372
|
endTime: new Date(sessionEndTime).toISOString(),
|
|
227
373
|
durationMs: totalDurationMs,
|
|
228
374
|
metrics: {
|
|
229
|
-
totalTokens,
|
|
230
|
-
inputTokens,
|
|
231
|
-
outputTokens,
|
|
232
|
-
totalCost: totalCost.toFixed(5),
|
|
233
|
-
toolCallCount,
|
|
234
|
-
llmCallCount
|
|
375
|
+
totalTokens: metrics.totalTokens,
|
|
376
|
+
inputTokens: metrics.inputTokens,
|
|
377
|
+
outputTokens: metrics.outputTokens,
|
|
378
|
+
totalCost: metrics.totalCost.toFixed(5),
|
|
379
|
+
toolCallCount: metrics.toolCallCount,
|
|
380
|
+
llmCallCount: metrics.llmCallCount
|
|
235
381
|
},
|
|
236
382
|
events
|
|
237
383
|
});
|
|
384
|
+
const runRecord = {
|
|
385
|
+
schemaVersion: 1,
|
|
386
|
+
kind: "notrace-run",
|
|
387
|
+
traceId,
|
|
388
|
+
runtime: "pi",
|
|
389
|
+
projectName,
|
|
390
|
+
captureMode,
|
|
391
|
+
session: {
|
|
392
|
+
id: traceId,
|
|
393
|
+
cwd: ctx.cwd,
|
|
394
|
+
file: sessionFile
|
|
395
|
+
},
|
|
396
|
+
task: location.taskId || location.taskPath
|
|
397
|
+
? {
|
|
398
|
+
id: location.taskId,
|
|
399
|
+
path: location.taskPath
|
|
400
|
+
}
|
|
401
|
+
: null,
|
|
402
|
+
conditions: {
|
|
403
|
+
models: metrics.models,
|
|
404
|
+
providers: metrics.providers
|
|
405
|
+
},
|
|
406
|
+
activity: {
|
|
407
|
+
startTime: new Date(sessionStartTime).toISOString(),
|
|
408
|
+
endTime: new Date(sessionEndTime).toISOString(),
|
|
409
|
+
durationMs: totalDurationMs,
|
|
410
|
+
turnCount: metrics.turnCount,
|
|
411
|
+
llmCallCount: metrics.llmCallCount,
|
|
412
|
+
toolCallCount: metrics.toolCallCount,
|
|
413
|
+
toolErrorCount: metrics.toolErrorCount,
|
|
414
|
+
totals: {
|
|
415
|
+
totalTokens: metrics.totalTokens,
|
|
416
|
+
inputTokens: metrics.inputTokens,
|
|
417
|
+
outputTokens: metrics.outputTokens,
|
|
418
|
+
totalCostUsd: Number(metrics.totalCost.toFixed(5))
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
artifacts: {
|
|
422
|
+
htmlReportPath: path.relative(ctx.cwd, reportPath),
|
|
423
|
+
recordPath: path.relative(ctx.cwd, recordPath),
|
|
424
|
+
reviewPath: path.relative(ctx.cwd, reviewPath)
|
|
425
|
+
},
|
|
426
|
+
evidence: {
|
|
427
|
+
events
|
|
428
|
+
}
|
|
429
|
+
};
|
|
238
430
|
try {
|
|
239
|
-
mkdirSync(
|
|
431
|
+
mkdirSync(location.outputDir, { recursive: true, mode: 0o700 });
|
|
240
432
|
writeFileSync(reportPath, htmlContent, { encoding: "utf-8", mode: 0o600 });
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
433
|
+
writeFileSync(recordPath, `${JSON.stringify(runRecord, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
434
|
+
updateNotraceIndex(ctx.cwd, location, {
|
|
435
|
+
sessionId: traceId,
|
|
436
|
+
startedAt: new Date(sessionStartTime).toISOString(),
|
|
437
|
+
endedAt: new Date(sessionEndTime).toISOString(),
|
|
438
|
+
taskId: location.taskId,
|
|
439
|
+
taskPath: location.taskPath,
|
|
440
|
+
artifacts: {
|
|
441
|
+
record: path.relative(ctx.cwd, recordPath),
|
|
442
|
+
html: path.relative(ctx.cwd, reportPath),
|
|
443
|
+
review: path.relative(ctx.cwd, reviewPath)
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
if (location.taskDir && (location.taskId || location.taskPath)) {
|
|
447
|
+
appendWorkLogEntry(location.taskDir, `notrace captured artifacts: ${path.relative(location.taskDir, reportPath)}, ${path.relative(location.taskDir, recordPath)}`);
|
|
448
|
+
}
|
|
449
|
+
console.log(`\n📊 [notrace] Observability artifacts generated:`);
|
|
450
|
+
console.log(`👉 \x1b[36mfile://${reportPath}\x1b[0m`);
|
|
451
|
+
console.log(`🧾 \x1b[36mfile://${recordPath}\x1b[0m\n`);
|
|
244
452
|
}
|
|
245
453
|
catch (err) {
|
|
246
|
-
console.warn(`[notrace] Failed to write
|
|
454
|
+
console.warn(`[notrace] Failed to write notrace artifacts: ${err?.message || err}`);
|
|
247
455
|
}
|
|
248
456
|
});
|
|
249
457
|
}
|
|
@@ -273,301 +481,222 @@ function generateHtmlReport(data) {
|
|
|
273
481
|
<title>notrace - ${escapedTraceId}</title>
|
|
274
482
|
<style>
|
|
275
483
|
:root {
|
|
276
|
-
--bg: #
|
|
277
|
-
--bg-
|
|
278
|
-
--
|
|
279
|
-
--
|
|
280
|
-
--
|
|
281
|
-
--
|
|
282
|
-
--text
|
|
283
|
-
--
|
|
284
|
-
--
|
|
285
|
-
--
|
|
286
|
-
--
|
|
287
|
-
--
|
|
288
|
-
--
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
484
|
+
--bg: #191613;
|
|
485
|
+
--bg-glow: #2a221c;
|
|
486
|
+
--panel: rgba(244, 237, 228, 0.055);
|
|
487
|
+
--panel-strong: rgba(244, 237, 228, 0.075);
|
|
488
|
+
--paper: #f6efe7;
|
|
489
|
+
--paper-soft: #e7dbce;
|
|
490
|
+
--text: #f5eee7;
|
|
491
|
+
--text-soft: #ddd0c2;
|
|
492
|
+
--text-muted: #b9ab9d;
|
|
493
|
+
--border: rgba(255, 255, 255, 0.09);
|
|
494
|
+
--border-strong: rgba(255, 255, 255, 0.16);
|
|
495
|
+
--accent: #d88462;
|
|
496
|
+
--accent-soft: rgba(216, 132, 98, 0.14);
|
|
497
|
+
--session: #8ab7ff;
|
|
498
|
+
--turn: #e0b46b;
|
|
499
|
+
--tool: #86cca4;
|
|
500
|
+
--error: #f08e8e;
|
|
501
|
+
--shadow: 0 24px 80px rgba(0, 0, 0, 0.34);
|
|
502
|
+
--card-shadow: 0 10px 28px rgba(0, 0, 0, 0.18);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
296
506
|
|
|
297
507
|
body {
|
|
298
|
-
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
299
|
-
background
|
|
508
|
+
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
509
|
+
background:
|
|
510
|
+
radial-gradient(900px 420px at 50% -10%, rgba(216, 132, 98, 0.16), transparent 60%),
|
|
511
|
+
radial-gradient(680px 360px at 10% 0%, rgba(255, 255, 255, 0.03), transparent 60%),
|
|
512
|
+
linear-gradient(180deg, #171411 0%, #1b1714 100%);
|
|
300
513
|
color: var(--text);
|
|
301
|
-
line-height: 1.
|
|
302
|
-
padding:
|
|
514
|
+
line-height: 1.65;
|
|
515
|
+
padding: 14px 12px 40px;
|
|
303
516
|
min-height: 100vh;
|
|
304
|
-
|
|
305
|
-
radial-gradient(circle at 10% 20%, rgba(139, 92, 246, 0.08) 0%, transparent 40%),
|
|
306
|
-
radial-gradient(circle at 90% 80%, rgba(59, 130, 246, 0.06) 0%, transparent 40%);
|
|
307
|
-
background-attachment: fixed;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
.container {
|
|
311
|
-
max-width: 1100px;
|
|
312
|
-
margin: 0 auto;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
header {
|
|
316
|
-
display: flex;
|
|
317
|
-
justify-content: space-between;
|
|
318
|
-
align-items: center;
|
|
319
|
-
margin-bottom: 2rem;
|
|
320
|
-
padding-bottom: 1.5rem;
|
|
321
|
-
border-bottom: 1px solid var(--border);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
.brand {
|
|
325
|
-
display: flex;
|
|
326
|
-
align-items: center;
|
|
327
|
-
gap: 0.75rem;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
.brand h1 {
|
|
331
|
-
font-size: 1.75rem;
|
|
332
|
-
font-weight: 700;
|
|
333
|
-
background: var(--accent-gradient);
|
|
334
|
-
-webkit-background-clip: text;
|
|
335
|
-
-webkit-text-fill-color: transparent;
|
|
517
|
+
letter-spacing: 0.01em;
|
|
336
518
|
}
|
|
337
519
|
|
|
338
|
-
.
|
|
339
|
-
|
|
340
|
-
text-transform: uppercase;
|
|
341
|
-
letter-spacing: 0.05em;
|
|
342
|
-
background: rgba(139, 92, 246, 0.15);
|
|
343
|
-
color: #c084fc;
|
|
344
|
-
padding: 0.2rem 0.5rem;
|
|
345
|
-
border-radius: 4px;
|
|
346
|
-
border: 1px solid rgba(167, 139, 250, 0.3);
|
|
347
|
-
}
|
|
520
|
+
.container { max-width: 920px; margin: 0 auto; }
|
|
521
|
+
header { margin-bottom: 24px; }
|
|
348
522
|
|
|
349
|
-
.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
gap: 1rem;
|
|
359
|
-
margin-bottom: 2.5rem;
|
|
523
|
+
.hero {
|
|
524
|
+
position: relative;
|
|
525
|
+
overflow: hidden;
|
|
526
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.025));
|
|
527
|
+
border: 1px solid var(--border);
|
|
528
|
+
border-radius: 22px;
|
|
529
|
+
padding: 18px 16px 16px;
|
|
530
|
+
box-shadow: var(--shadow);
|
|
531
|
+
backdrop-filter: blur(10px);
|
|
360
532
|
}
|
|
361
533
|
|
|
534
|
+
.hero::after {
|
|
535
|
+
content: "";
|
|
536
|
+
position: absolute;
|
|
537
|
+
inset: auto -10% -30% auto;
|
|
538
|
+
width: 260px;
|
|
539
|
+
height: 260px;
|
|
540
|
+
background: radial-gradient(circle, rgba(216,132,98,0.14), transparent 70%);
|
|
541
|
+
pointer-events: none;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.brand { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; position: relative; z-index: 1; }
|
|
545
|
+
.brand h1 { font-size: 2rem; line-height: 1; font-weight: 680; color: var(--text); letter-spacing: -0.03em; }
|
|
546
|
+
.brand-tag { font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase; color: #f4ccb9; background: var(--accent-soft); border: 1px solid rgba(216, 132, 98, 0.24); padding: 0.35rem 0.6rem; border-radius: 999px; }
|
|
547
|
+
.hero-subtitle { position: relative; z-index: 1; color: var(--text-soft); margin-bottom: 20px; max-width: 720px; font-size: 1rem; }
|
|
548
|
+
.hero-meta { position: relative; z-index: 1; display: flex; flex-wrap: wrap; gap: 10px; }
|
|
549
|
+
.meta-pill { display: inline-flex; align-items: center; gap: 6px; background: rgba(255,255,255,0.04); border: 1px solid var(--border); border-radius: 999px; padding: 0.48rem 0.82rem; font-size: 0.86rem; color: var(--text-muted); }
|
|
550
|
+
.meta-pill strong { color: var(--text); font-weight: 570; }
|
|
551
|
+
|
|
552
|
+
.metrics-grid { display: grid; grid-template-columns: 1fr; gap: 10px; margin: 16px 0 24px; }
|
|
362
553
|
.metric-card {
|
|
363
|
-
background:
|
|
554
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.028));
|
|
364
555
|
border: 1px solid var(--border);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
transition:
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
.metric-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
556
|
+
border-radius: 20px;
|
|
557
|
+
padding: 18px 16px;
|
|
558
|
+
box-shadow: var(--card-shadow);
|
|
559
|
+
transition: border-color 0.18s ease, transform 0.18s ease, background 0.18s ease;
|
|
560
|
+
}
|
|
561
|
+
.metric-card:hover { border-color: var(--border-strong); transform: translateY(-1px); background: linear-gradient(180deg, rgba(255,255,255,0.055), rgba(255,255,255,0.03)); }
|
|
562
|
+
.metric-label { font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); margin-bottom: 7px; }
|
|
563
|
+
.metric-value { font-size: 1.42rem; font-weight: 640; color: var(--text); letter-spacing: -0.03em; }
|
|
564
|
+
|
|
565
|
+
.section-title {
|
|
566
|
+
font-size: 0.95rem;
|
|
567
|
+
font-weight: 620;
|
|
568
|
+
color: var(--paper-soft);
|
|
569
|
+
margin-bottom: 16px;
|
|
570
|
+
padding-left: 2px;
|
|
571
|
+
letter-spacing: 0.04em;
|
|
379
572
|
text-transform: uppercase;
|
|
380
|
-
letter-spacing: 0.05em;
|
|
381
|
-
color: var(--text-muted);
|
|
382
|
-
margin-bottom: 0.5rem;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
.metric-value {
|
|
386
|
-
font-size: 1.5rem;
|
|
387
|
-
font-weight: 600;
|
|
388
|
-
color: #fff;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
.timeline {
|
|
392
|
-
position: relative;
|
|
393
|
-
padding-left: 2rem;
|
|
394
|
-
border-left: 2px solid var(--border);
|
|
395
|
-
margin-left: 1rem;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
.timeline-event {
|
|
399
|
-
position: relative;
|
|
400
|
-
margin-bottom: 2rem;
|
|
401
|
-
animation: slideIn 0.4s ease-out;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
@keyframes slideIn {
|
|
405
|
-
from { opacity: 0; transform: translateX(-10px); }
|
|
406
|
-
to { opacity: 1; transform: translateX(0); }
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
.timeline-dot {
|
|
410
|
-
position: absolute;
|
|
411
|
-
left: calc(-2rem - 6px);
|
|
412
|
-
top: 1.25rem;
|
|
413
|
-
width: 10px;
|
|
414
|
-
height: 10px;
|
|
415
|
-
border-radius: 50%;
|
|
416
|
-
background: var(--text-muted);
|
|
417
|
-
box-shadow: 0 0 0 4px var(--bg);
|
|
418
|
-
transition: all 0.3s ease;
|
|
419
573
|
}
|
|
420
574
|
|
|
421
|
-
|
|
422
|
-
.timeline-event
|
|
423
|
-
|
|
424
|
-
.timeline-
|
|
425
|
-
.timeline-event.tool-call.error .timeline-dot { background: var(--error); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--error); }
|
|
426
|
-
.timeline-event.llm-call .timeline-dot { background: var(--success); box-shadow: 0 0 0 4px var(--bg), 0 0 8px var(--success); }
|
|
575
|
+
.timeline { display: flex; flex-direction: column; gap: 16px; }
|
|
576
|
+
.timeline-event { position: relative; animation: slideIn 0.28s ease-out; }
|
|
577
|
+
@keyframes slideIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
578
|
+
.timeline-dot { display: none; }
|
|
427
579
|
|
|
428
580
|
.card {
|
|
429
|
-
|
|
581
|
+
position: relative;
|
|
582
|
+
overflow: hidden;
|
|
583
|
+
background: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.024));
|
|
430
584
|
border: 1px solid var(--border);
|
|
431
|
-
border-radius:
|
|
432
|
-
padding:
|
|
433
|
-
|
|
434
|
-
transition: border-color 0.
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
.card:hover {
|
|
438
|
-
border-color: var(--border-hover);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
.card-header {
|
|
442
|
-
display: flex;
|
|
443
|
-
justify-content: space-between;
|
|
444
|
-
align-items: center;
|
|
445
|
-
cursor: pointer;
|
|
446
|
-
user-select: none;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
.card-title {
|
|
450
|
-
display: flex;
|
|
451
|
-
align-items: center;
|
|
452
|
-
gap: 0.75rem;
|
|
453
|
-
font-weight: 600;
|
|
585
|
+
border-radius: 18px;
|
|
586
|
+
padding: 14px 14px 13px;
|
|
587
|
+
box-shadow: var(--card-shadow);
|
|
588
|
+
transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
|
|
454
589
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
.
|
|
465
|
-
.
|
|
466
|
-
.
|
|
467
|
-
.
|
|
468
|
-
|
|
469
|
-
.card-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
.expanded .arrow-icon {
|
|
489
|
-
transform: rotate(90deg);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
.card-body {
|
|
493
|
-
margin-top: 1rem;
|
|
494
|
-
padding-top: 1rem;
|
|
495
|
-
border-top: 1px solid var(--border);
|
|
496
|
-
display: none;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
.expanded .card-body {
|
|
500
|
-
display: block;
|
|
501
|
-
}
|
|
502
|
-
|
|
590
|
+
.card::before {
|
|
591
|
+
content: "";
|
|
592
|
+
position: absolute;
|
|
593
|
+
left: 0;
|
|
594
|
+
top: 0;
|
|
595
|
+
bottom: 0;
|
|
596
|
+
width: 3px;
|
|
597
|
+
background: rgba(255,255,255,0.1);
|
|
598
|
+
}
|
|
599
|
+
.timeline-event.session-start .card::before { background: var(--session); }
|
|
600
|
+
.timeline-event.turn-start .card::before { background: var(--turn); }
|
|
601
|
+
.timeline-event.tool-start .card::before { background: var(--tool); }
|
|
602
|
+
.timeline-event.tool-start.error .card::before { background: var(--error); }
|
|
603
|
+
.timeline-event.llm-start .card::before { background: var(--accent); }
|
|
604
|
+
.card:hover { border-color: var(--border-strong); background: linear-gradient(180deg, rgba(255,255,255,0.055), rgba(255,255,255,0.028)); transform: translateY(-1px); }
|
|
605
|
+
|
|
606
|
+
.card-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; cursor: pointer; user-select: none; }
|
|
607
|
+
.card-title { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-weight: 600; color: var(--text); }
|
|
608
|
+
.card-title span:last-child { font-size: 1rem; letter-spacing: -0.01em; }
|
|
609
|
+
.card-badge { font-size: 0.69rem; font-weight: 650; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.34rem 0.62rem; border-radius: 999px; border: 1px solid transparent; }
|
|
610
|
+
.badge-session { background: rgba(138, 183, 255, 0.14); color: var(--session); border-color: rgba(138, 183, 255, 0.24); }
|
|
611
|
+
.badge-turn { background: rgba(224, 180, 107, 0.14); color: var(--turn); border-color: rgba(224, 180, 107, 0.22); }
|
|
612
|
+
.badge-tool { background: rgba(134, 204, 164, 0.14); color: var(--tool); border-color: rgba(134, 204, 164, 0.22); }
|
|
613
|
+
.badge-tool.error { background: rgba(240, 142, 142, 0.15); color: var(--error); border-color: rgba(240, 142, 142, 0.24); }
|
|
614
|
+
.badge-llm { background: rgba(216, 132, 98, 0.14); color: #f2c2ae; border-color: rgba(216, 132, 98, 0.22); }
|
|
615
|
+
.card-time { flex-shrink: 0; font-size: 0.82rem; color: var(--text-muted); display: flex; align-items: center; gap: 8px; padding-top: 3px; }
|
|
616
|
+
.arrow-icon { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; transition: transform 0.2s; }
|
|
617
|
+
.expanded .arrow-icon { transform: rotate(90deg); }
|
|
618
|
+
.card-body { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); display: none; }
|
|
619
|
+
.expanded .card-body { display: block; }
|
|
620
|
+
|
|
621
|
+
.detail-label { font-size: 0.76rem; font-weight: 650; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; margin: 14px 0 8px; display: block; }
|
|
503
622
|
.code-block {
|
|
504
623
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
505
|
-
font-size: 0.
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
624
|
+
font-size: 0.84rem;
|
|
625
|
+
line-height: 1.62;
|
|
626
|
+
background: rgba(16, 14, 12, 0.64);
|
|
627
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
628
|
+
padding: 14px;
|
|
629
|
+
border-radius: 16px;
|
|
510
630
|
overflow-x: auto;
|
|
511
|
-
margin-top: 0.
|
|
512
|
-
color: #
|
|
631
|
+
margin-top: 0.35rem;
|
|
632
|
+
color: #f1e9df;
|
|
513
633
|
white-space: pre-wrap;
|
|
514
634
|
}
|
|
515
635
|
|
|
516
|
-
.messages-container {
|
|
517
|
-
display: flex;
|
|
518
|
-
flex-direction: column;
|
|
519
|
-
gap: 0.75rem;
|
|
520
|
-
}
|
|
521
|
-
|
|
636
|
+
.messages-container { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
|
|
522
637
|
.msg-row {
|
|
523
638
|
display: flex;
|
|
524
639
|
flex-direction: column;
|
|
525
|
-
gap:
|
|
526
|
-
padding:
|
|
527
|
-
background: rgba(255,
|
|
528
|
-
border
|
|
529
|
-
border-
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
.msg-row.
|
|
533
|
-
.msg-row.
|
|
534
|
-
.msg-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
letter-spacing: 0.05em;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
.msg-row.user .msg-role { color: #60a5fa; }
|
|
544
|
-
.msg-row.assistant .msg-role { color: #34d399; }
|
|
545
|
-
.msg-row.system .msg-role { color: #c084fc; }
|
|
546
|
-
|
|
547
|
-
.msg-text {
|
|
548
|
-
font-size: 0.9rem;
|
|
549
|
-
white-space: pre-wrap;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
.duration-pill {
|
|
553
|
-
font-size: 0.75rem;
|
|
554
|
-
background: rgba(255, 255, 255, 0.06);
|
|
555
|
-
padding: 0.1rem 0.4rem;
|
|
556
|
-
border-radius: 4px;
|
|
640
|
+
gap: 4px;
|
|
641
|
+
padding: 12px 14px;
|
|
642
|
+
background: rgba(255,255,255,0.038);
|
|
643
|
+
border: 1px solid rgba(255,255,255,0.06);
|
|
644
|
+
border-radius: 18px;
|
|
645
|
+
}
|
|
646
|
+
.msg-row.user { background: rgba(138, 183, 255, 0.08); }
|
|
647
|
+
.msg-row.assistant { background: rgba(216, 132, 98, 0.08); }
|
|
648
|
+
.msg-row.system { background: rgba(224, 180, 107, 0.08); }
|
|
649
|
+
.msg-role { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); }
|
|
650
|
+
.msg-text { font-size: 0.93rem; color: var(--text-soft); white-space: pre-wrap; }
|
|
651
|
+
|
|
652
|
+
.usage-row {
|
|
653
|
+
margin-top: 12px;
|
|
654
|
+
font-size: 0.84rem;
|
|
557
655
|
color: var(--text-muted);
|
|
656
|
+
display: flex;
|
|
657
|
+
flex-wrap: wrap;
|
|
658
|
+
gap: 12px;
|
|
659
|
+
padding: 10px 12px;
|
|
660
|
+
border-radius: 14px;
|
|
661
|
+
background: rgba(255,255,255,0.03);
|
|
662
|
+
border: 1px solid rgba(255,255,255,0.05);
|
|
663
|
+
}
|
|
664
|
+
.usage-row strong { color: var(--text); font-weight: 620; }
|
|
665
|
+
.duration-pill { font-size: 0.74rem; background: rgba(255,255,255,0.06); padding: 0.2rem 0.46rem; border-radius: 999px; color: var(--text-muted); border: 1px solid rgba(255,255,255,0.06); }
|
|
666
|
+
|
|
667
|
+
@media (min-width: 640px) {
|
|
668
|
+
body { padding: 28px 18px 56px; }
|
|
669
|
+
.hero { border-radius: 28px; padding: 28px 28px 24px; }
|
|
670
|
+
.metrics-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: 18px 0 30px; }
|
|
671
|
+
.card { border-radius: 22px; padding: 18px 18px 16px; }
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
@media (max-width: 720px) {
|
|
675
|
+
.hero, .card, .metric-card { border-radius: 18px; }
|
|
676
|
+
.card-header { flex-direction: column; }
|
|
677
|
+
.card-time { width: 100%; justify-content: space-between; }
|
|
678
|
+
.brand { align-items: flex-start; flex-direction: column; gap: 8px; }
|
|
679
|
+
.brand h1 { font-size: 1.8rem; }
|
|
680
|
+
.hero-meta { flex-direction: column; align-items: stretch; }
|
|
681
|
+
.meta-pill { width: 100%; justify-content: space-between; }
|
|
682
|
+
.usage-row { flex-direction: column; gap: 6px; }
|
|
683
|
+
.code-block { font-size: 0.8rem; padding: 12px; }
|
|
558
684
|
}
|
|
559
685
|
</style>
|
|
560
686
|
</head>
|
|
561
687
|
<body>
|
|
562
688
|
<div class="container">
|
|
563
689
|
<header>
|
|
564
|
-
<div class="
|
|
565
|
-
<
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
<
|
|
570
|
-
<div
|
|
690
|
+
<div class="hero">
|
|
691
|
+
<div class="brand">
|
|
692
|
+
<h1>notrace</h1>
|
|
693
|
+
<span class="brand-tag">retrospective</span>
|
|
694
|
+
</div>
|
|
695
|
+
<p class="hero-subtitle">A local evidence view of the run: messages, tools, model calls, timing, and token cost.</p>
|
|
696
|
+
<div class="hero-meta">
|
|
697
|
+
<span class="meta-pill">Session <strong id="sess-id"></strong></span>
|
|
698
|
+
<span class="meta-pill">Started <strong id="sess-time"></strong></span>
|
|
699
|
+
</div>
|
|
571
700
|
</div>
|
|
572
701
|
</header>
|
|
573
702
|
|
|
@@ -594,7 +723,7 @@ function generateHtmlReport(data) {
|
|
|
594
723
|
</div>
|
|
595
724
|
</div>
|
|
596
725
|
|
|
597
|
-
<h2
|
|
726
|
+
<h2 class="section-title">Activity flow</h2>
|
|
598
727
|
<div class="timeline" id="timeline-container">
|
|
599
728
|
<!-- Injected by JS -->
|
|
600
729
|
</div>
|
|
@@ -714,9 +843,9 @@ function generateHtmlReport(data) {
|
|
|
714
843
|
cardHtml += \`<div class="code-block">\${escapeHtml(ev.body)}</div>\`;
|
|
715
844
|
} else if (ev.type === "tool") {
|
|
716
845
|
cardHtml += \`
|
|
717
|
-
<
|
|
846
|
+
<span class="detail-label">Arguments</span>
|
|
718
847
|
<div class="code-block">\${jsonText(ev.args)}</div>
|
|
719
|
-
<
|
|
848
|
+
<span class="detail-label">Result (\${ev.isError ? "Error" : "Success"})</span>
|
|
720
849
|
<div class="code-block">\${jsonText(ev.result)}</div>
|
|
721
850
|
\`;
|
|
722
851
|
} else if (ev.type === "llm") {
|
|
@@ -750,16 +879,16 @@ function generateHtmlReport(data) {
|
|
|
750
879
|
messagesHtml += '</div>';
|
|
751
880
|
|
|
752
881
|
cardHtml += \`
|
|
753
|
-
<
|
|
882
|
+
<span class="detail-label">Context messages</span>
|
|
754
883
|
\${messagesHtml}
|
|
755
|
-
<
|
|
884
|
+
<span class="detail-label">Generated response</span>
|
|
756
885
|
<div class="code-block">\${jsonText(ev.output)}</div>
|
|
757
886
|
\${ev.usage ? \`
|
|
758
|
-
<div
|
|
759
|
-
<span>Input
|
|
760
|
-
<span>Output
|
|
761
|
-
<span>Total
|
|
762
|
-
<span>Cost
|
|
887
|
+
<div class="usage-row">
|
|
888
|
+
<span>Input tokens <strong>\${escapeHtml(ev.usage.input ?? 0)}</strong></span>
|
|
889
|
+
<span>Output tokens <strong>\${escapeHtml(ev.usage.output ?? 0)}</strong></span>
|
|
890
|
+
<span>Total tokens <strong>\${escapeHtml(ev.usage.totalTokens ?? 0)}</strong></span>
|
|
891
|
+
<span>Cost <strong>\$\${escapeHtml(ev.usage.cost?.total?.toFixed?.(5) || "0.00")}</strong></span>
|
|
763
892
|
</div>
|
|
764
893
|
\` : ""}
|
|
765
894
|
\`;
|