@artemiskit/reports 0.1.6 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -90,6 +90,116 @@ const HTML_TEMPLATE = `
90
90
  margin-left: 0.5rem;
91
91
  }
92
92
  footer { margin-top: 3rem; text-align: center; color: #666; font-size: 0.875rem; }
93
+
94
+ /* Collapsible sections */
95
+ .collapsible-header {
96
+ display: flex;
97
+ align-items: center;
98
+ cursor: pointer;
99
+ user-select: none;
100
+ }
101
+ .collapsible-header h2 {
102
+ margin: 0;
103
+ flex: 1;
104
+ }
105
+ .collapse-icon {
106
+ font-size: 1.25rem;
107
+ transition: transform 0.2s ease;
108
+ margin-left: 0.5rem;
109
+ color: #666;
110
+ }
111
+ .collapsible-header[data-collapsed="true"] .collapse-icon {
112
+ transform: rotate(-90deg);
113
+ }
114
+ .collapsible-content {
115
+ overflow: hidden;
116
+ transition: max-height 0.3s ease;
117
+ }
118
+ .collapsible-content.collapsed {
119
+ max-height: 0 !important;
120
+ padding: 0;
121
+ }
122
+
123
+ /* Filter and Search controls */
124
+ .controls {
125
+ display: flex;
126
+ gap: 1rem;
127
+ margin-bottom: 1rem;
128
+ flex-wrap: wrap;
129
+ align-items: center;
130
+ }
131
+ .filter-group {
132
+ display: flex;
133
+ gap: 0.5rem;
134
+ }
135
+ .filter-btn {
136
+ padding: 0.5rem 1rem;
137
+ border: 1px solid #e0e0e0;
138
+ background: white;
139
+ border-radius: 6px;
140
+ cursor: pointer;
141
+ font-size: 0.875rem;
142
+ font-weight: 500;
143
+ transition: all 0.15s ease;
144
+ }
145
+ .filter-btn:hover {
146
+ background: #f5f5f5;
147
+ }
148
+ .filter-btn.active {
149
+ background: #1a1a1a;
150
+ color: white;
151
+ border-color: #1a1a1a;
152
+ }
153
+ .filter-btn.passed.active {
154
+ background: #166534;
155
+ border-color: #166534;
156
+ }
157
+ .filter-btn.failed.active {
158
+ background: #991b1b;
159
+ border-color: #991b1b;
160
+ }
161
+ .search-box {
162
+ flex: 1;
163
+ min-width: 200px;
164
+ max-width: 400px;
165
+ }
166
+ .search-input {
167
+ width: 100%;
168
+ padding: 0.5rem 1rem;
169
+ border: 1px solid #e0e0e0;
170
+ border-radius: 6px;
171
+ font-size: 0.875rem;
172
+ outline: none;
173
+ transition: border-color 0.15s ease;
174
+ }
175
+ .search-input:focus {
176
+ border-color: #3b82f6;
177
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
178
+ }
179
+ .search-input::placeholder {
180
+ color: #999;
181
+ }
182
+ .results-count {
183
+ font-size: 0.875rem;
184
+ color: #666;
185
+ padding: 0.5rem 0;
186
+ }
187
+
188
+ /* No results message */
189
+ .no-results {
190
+ text-align: center;
191
+ padding: 2rem;
192
+ color: #666;
193
+ background: white;
194
+ border-radius: 8px;
195
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
196
+ }
197
+ .no-results .icon { font-size: 2rem; margin-bottom: 0.5rem; }
198
+
199
+ /* Row highlight for search matches */
200
+ tr.search-highlight td {
201
+ background: #fef9c3;
202
+ }
93
203
  </style>
94
204
  </head>
95
205
  <body>
@@ -104,7 +214,7 @@ const HTML_TEMPLATE = `
104
214
 
105
215
  {{#if manifest.redaction.enabled}}
106
216
  <div class="redaction-banner">
107
- <div class="icon">🔒</div>
217
+ <div class="icon">&#128274;</div>
108
218
  <div class="content">
109
219
  <div class="title">Data Redaction Applied</div>
110
220
  <div class="details">
@@ -116,101 +226,151 @@ const HTML_TEMPLATE = `
116
226
  </div>
117
227
  {{/if}}
118
228
 
119
- <div class="summary">
120
- <div class="card">
121
- <h3>Success Rate</h3>
122
- <div class="value {{successRateClass manifest.metrics.success_rate}}">
123
- {{formatPercent manifest.metrics.success_rate}}
124
- </div>
229
+ <!-- Summary Section (Collapsible) -->
230
+ <div class="collapsible-section" data-section="summary">
231
+ <div class="collapsible-header" onclick="toggleSection('summary')">
232
+ <h2>Summary</h2>
233
+ <span class="collapse-icon">&#9660;</span>
125
234
  </div>
126
- <div class="card">
127
- <h3>Passed / Total</h3>
128
- <div class="value">{{manifest.metrics.passed_cases}} / {{manifest.metrics.total_cases}}</div>
235
+ <div class="collapsible-content" id="section-summary">
236
+ <div class="summary">
237
+ <div class="card">
238
+ <h3>Success Rate</h3>
239
+ <div class="value {{successRateClass manifest.metrics.success_rate}}">
240
+ {{formatPercent manifest.metrics.success_rate}}
241
+ </div>
242
+ </div>
243
+ <div class="card">
244
+ <h3>Passed / Total</h3>
245
+ <div class="value">{{manifest.metrics.passed_cases}} / {{manifest.metrics.total_cases}}</div>
246
+ </div>
247
+ <div class="card">
248
+ <h3>Median Latency</h3>
249
+ <div class="value">{{manifest.metrics.median_latency_ms}}ms</div>
250
+ </div>
251
+ <div class="card">
252
+ <h3>Total Tokens</h3>
253
+ <div class="value">{{formatNumber manifest.metrics.total_tokens}}</div>
254
+ </div>
255
+ </div>
129
256
  </div>
130
- <div class="card">
131
- <h3>Median Latency</h3>
132
- <div class="value">{{manifest.metrics.median_latency_ms}}ms</div>
257
+ </div>
258
+
259
+ <!-- Test Cases Section (Collapsible with Filter & Search) -->
260
+ <div class="collapsible-section" data-section="cases">
261
+ <div class="collapsible-header" onclick="toggleSection('cases')">
262
+ <h2>Test Cases</h2>
263
+ <span class="collapse-icon">&#9660;</span>
133
264
  </div>
134
- <div class="card">
135
- <h3>Total Tokens</h3>
136
- <div class="value">{{formatNumber manifest.metrics.total_tokens}}</div>
265
+ <div class="collapsible-content" id="section-cases">
266
+ <!-- Filter and Search Controls -->
267
+ <div class="controls">
268
+ <div class="filter-group">
269
+ <button class="filter-btn active" data-filter="all" onclick="filterCases('all')">All ({{manifest.metrics.total_cases}})</button>
270
+ <button class="filter-btn passed" data-filter="passed" onclick="filterCases('passed')">Passed ({{manifest.metrics.passed_cases}})</button>
271
+ <button class="filter-btn failed" data-filter="failed" onclick="filterCases('failed')">Failed ({{failedCount manifest}})</button>
272
+ </div>
273
+ <div class="search-box">
274
+ <input type="text" class="search-input" id="search-input" placeholder="Search by ID, name, response..." oninput="searchCases(this.value)">
275
+ </div>
276
+ </div>
277
+ <div class="results-count" id="results-count">Showing all {{manifest.metrics.total_cases}} test cases</div>
278
+
279
+ <table id="cases-table">
280
+ <thead>
281
+ <tr>
282
+ <th>ID</th>
283
+ <th>Status</th>
284
+ <th>Score</th>
285
+ <th>Matcher</th>
286
+ <th>Latency</th>
287
+ <th>Tokens</th>
288
+ </tr>
289
+ </thead>
290
+ <tbody>
291
+ {{#each manifest.cases}}
292
+ <tr class="expandable case-row" data-status="{{#if ok}}passed{{else}}failed{{/if}}" data-id="{{id}}" data-name="{{name}}" data-response="{{response}}" data-reason="{{reason}}" onclick="toggleDetails('{{id}}')">
293
+ <td><strong>{{id}}</strong>{{#if name}}<br><small>{{name}}</small>{{/if}}{{#if redaction.redacted}}<span class="redacted-badge">redacted</span>{{/if}}</td>
294
+ <td><span class="status {{#if ok}}passed{{else}}failed{{/if}}">{{#if ok}}PASSED{{else}}FAILED{{/if}}</span></td>
295
+ <td class="score">{{formatPercent score}}</td>
296
+ <td>{{matcherType}}</td>
297
+ <td>{{latencyMs}}ms</td>
298
+ <td>{{tokens.total}}</td>
299
+ </tr>
300
+ <tr id="details-{{id}}" class="hidden details-row" data-parent="{{id}}">
301
+ <td colspan="6">
302
+ <div class="details">
303
+ <p><strong>Reason:</strong> {{reason}}</p>
304
+ <p><strong>Prompt:</strong>{{#if redaction.promptRedacted}} <span class="redacted-badge">redacted</span>{{/if}}</p>
305
+ <pre>{{formatPrompt prompt}}</pre>
306
+ <p><strong>Response:</strong>{{#if redaction.responseRedacted}} <span class="redacted-badge">redacted</span>{{/if}}</p>
307
+ <pre>{{response}}</pre>
308
+ </div>
309
+ </td>
310
+ </tr>
311
+ {{/each}}
312
+ </tbody>
313
+ </table>
314
+ <div class="no-results hidden" id="no-results">
315
+ <div class="icon">&#128269;</div>
316
+ <p>No test cases match your filter or search criteria.</p>
317
+ </div>
137
318
  </div>
138
319
  </div>
139
320
 
140
- <h2>Test Cases</h2>
141
- <table>
142
- <thead>
143
- <tr>
144
- <th>ID</th>
145
- <th>Status</th>
146
- <th>Score</th>
147
- <th>Matcher</th>
148
- <th>Latency</th>
149
- <th>Tokens</th>
150
- </tr>
151
- </thead>
152
- <tbody>
153
- {{#each manifest.cases}}
154
- <tr class="expandable" onclick="toggleDetails('{{id}}')">
155
- <td><strong>{{id}}</strong>{{#if name}}<br><small>{{name}}</small>{{/if}}{{#if redaction.redacted}}<span class="redacted-badge">redacted</span>{{/if}}</td>
156
- <td><span class="status {{#if ok}}passed{{else}}failed{{/if}}">{{#if ok}}PASSED{{else}}FAILED{{/if}}</span></td>
157
- <td class="score">{{formatPercent score}}</td>
158
- <td>{{matcherType}}</td>
159
- <td>{{latencyMs}}ms</td>
160
- <td>{{tokens.total}}</td>
161
- </tr>
162
- <tr id="details-{{id}}" class="hidden">
163
- <td colspan="6">
164
- <div class="details">
165
- <p><strong>Reason:</strong> {{reason}}</p>
166
- <p><strong>Prompt:</strong>{{#if redaction.promptRedacted}} <span class="redacted-badge">redacted</span>{{/if}}</p>
167
- <pre>{{formatPrompt prompt}}</pre>
168
- <p><strong>Response:</strong>{{#if redaction.responseRedacted}} <span class="redacted-badge">redacted</span>{{/if}}</p>
169
- <pre>{{response}}</pre>
170
- </div>
171
- </td>
172
- </tr>
173
- {{/each}}
174
- </tbody>
175
- </table>
176
-
177
321
  {{#if manifest.resolved_config}}
178
- <h2>Resolved Configuration</h2>
179
- <div class="card">
180
- <p><strong>Provider:</strong> {{manifest.resolved_config.provider}} <span class="source-badge">{{manifest.resolved_config.source.provider}}</span></p>
181
- {{#if manifest.resolved_config.model}}
182
- <p><strong>Model:</strong> {{manifest.resolved_config.model}} <span class="source-badge">{{manifest.resolved_config.source.model}}</span></p>
183
- {{/if}}
184
- {{#if manifest.resolved_config.deployment_name}}
185
- <p><strong>Deployment:</strong> {{manifest.resolved_config.deployment_name}} <span class="source-badge">{{manifest.resolved_config.source.deployment_name}}</span></p>
186
- {{/if}}
187
- {{#if manifest.resolved_config.resource_name}}
188
- <p><strong>Resource:</strong> {{manifest.resolved_config.resource_name}} <span class="source-badge">{{manifest.resolved_config.source.resource_name}}</span></p>
189
- {{/if}}
190
- {{#if manifest.resolved_config.api_version}}
191
- <p><strong>API Version:</strong> {{manifest.resolved_config.api_version}} <span class="source-badge">{{manifest.resolved_config.source.api_version}}</span></p>
192
- {{/if}}
193
- {{#if manifest.resolved_config.base_url}}
194
- <p><strong>Base URL:</strong> {{manifest.resolved_config.base_url}} <span class="source-badge">{{manifest.resolved_config.source.base_url}}</span></p>
195
- {{/if}}
196
- {{#if manifest.resolved_config.underlying_provider}}
197
- <p><strong>Underlying Provider:</strong> {{manifest.resolved_config.underlying_provider}} <span class="source-badge">{{manifest.resolved_config.source.underlying_provider}}</span></p>
198
- {{/if}}
199
- {{#if manifest.resolved_config.temperature}}
200
- <p><strong>Temperature:</strong> {{manifest.resolved_config.temperature}} <span class="source-badge">{{manifest.resolved_config.source.temperature}}</span></p>
201
- {{/if}}
202
- {{#if manifest.resolved_config.max_tokens}}
203
- <p><strong>Max Tokens:</strong> {{manifest.resolved_config.max_tokens}} <span class="source-badge">{{manifest.resolved_config.source.max_tokens}}</span></p>
204
- {{/if}}
322
+ <!-- Resolved Configuration Section (Collapsible) -->
323
+ <div class="collapsible-section" data-section="config">
324
+ <div class="collapsible-header" onclick="toggleSection('config')">
325
+ <h2>Resolved Configuration</h2>
326
+ <span class="collapse-icon">&#9660;</span>
327
+ </div>
328
+ <div class="collapsible-content" id="section-config">
329
+ <div class="card">
330
+ <p><strong>Provider:</strong> {{manifest.resolved_config.provider}} <span class="source-badge">{{manifest.resolved_config.source.provider}}</span></p>
331
+ {{#if manifest.resolved_config.model}}
332
+ <p><strong>Model:</strong> {{manifest.resolved_config.model}} <span class="source-badge">{{manifest.resolved_config.source.model}}</span></p>
333
+ {{/if}}
334
+ {{#if manifest.resolved_config.deployment_name}}
335
+ <p><strong>Deployment:</strong> {{manifest.resolved_config.deployment_name}} <span class="source-badge">{{manifest.resolved_config.source.deployment_name}}</span></p>
336
+ {{/if}}
337
+ {{#if manifest.resolved_config.resource_name}}
338
+ <p><strong>Resource:</strong> {{manifest.resolved_config.resource_name}} <span class="source-badge">{{manifest.resolved_config.source.resource_name}}</span></p>
339
+ {{/if}}
340
+ {{#if manifest.resolved_config.api_version}}
341
+ <p><strong>API Version:</strong> {{manifest.resolved_config.api_version}} <span class="source-badge">{{manifest.resolved_config.source.api_version}}</span></p>
342
+ {{/if}}
343
+ {{#if manifest.resolved_config.base_url}}
344
+ <p><strong>Base URL:</strong> {{manifest.resolved_config.base_url}} <span class="source-badge">{{manifest.resolved_config.source.base_url}}</span></p>
345
+ {{/if}}
346
+ {{#if manifest.resolved_config.underlying_provider}}
347
+ <p><strong>Underlying Provider:</strong> {{manifest.resolved_config.underlying_provider}} <span class="source-badge">{{manifest.resolved_config.source.underlying_provider}}</span></p>
348
+ {{/if}}
349
+ {{#if manifest.resolved_config.temperature}}
350
+ <p><strong>Temperature:</strong> {{manifest.resolved_config.temperature}} <span class="source-badge">{{manifest.resolved_config.source.temperature}}</span></p>
351
+ {{/if}}
352
+ {{#if manifest.resolved_config.max_tokens}}
353
+ <p><strong>Max Tokens:</strong> {{manifest.resolved_config.max_tokens}} <span class="source-badge">{{manifest.resolved_config.source.max_tokens}}</span></p>
354
+ {{/if}}
355
+ </div>
356
+ </div>
205
357
  </div>
206
358
  {{/if}}
207
359
 
208
- <h2>Provenance</h2>
209
- <div class="card">
210
- <p><strong>Git Commit:</strong> {{manifest.git.commit}}</p>
211
- <p><strong>Git Branch:</strong> {{manifest.git.branch}}</p>
212
- <p><strong>Run By:</strong> {{manifest.provenance.run_by}}</p>
213
- <p><strong>Duration:</strong> {{manifest.duration_ms}}ms</p>
360
+ <!-- Provenance Section (Collapsible) -->
361
+ <div class="collapsible-section" data-section="provenance">
362
+ <div class="collapsible-header" onclick="toggleSection('provenance')">
363
+ <h2>Provenance</h2>
364
+ <span class="collapse-icon">&#9660;</span>
365
+ </div>
366
+ <div class="collapsible-content" id="section-provenance">
367
+ <div class="card">
368
+ <p><strong>Git Commit:</strong> {{manifest.git.commit}}</p>
369
+ <p><strong>Git Branch:</strong> {{manifest.git.branch}}</p>
370
+ <p><strong>Run By:</strong> {{manifest.provenance.run_by}}</p>
371
+ <p><strong>Duration:</strong> {{manifest.duration_ms}}ms</p>
372
+ </div>
373
+ </div>
214
374
  </div>
215
375
 
216
376
  <footer>
@@ -219,10 +379,139 @@ const HTML_TEMPLATE = `
219
379
  </div>
220
380
 
221
381
  <script>
382
+ // State
383
+ let currentFilter = 'all';
384
+ let currentSearch = '';
385
+
386
+ // Toggle collapsible sections
387
+ function toggleSection(sectionId) {
388
+ const header = document.querySelector('[data-section="' + sectionId + '"] .collapsible-header');
389
+ const content = document.getElementById('section-' + sectionId);
390
+ const isCollapsed = header.getAttribute('data-collapsed') === 'true';
391
+
392
+ if (isCollapsed) {
393
+ header.setAttribute('data-collapsed', 'false');
394
+ content.classList.remove('collapsed');
395
+ } else {
396
+ header.setAttribute('data-collapsed', 'true');
397
+ content.classList.add('collapsed');
398
+ }
399
+ }
400
+
401
+ // Toggle case details
222
402
  function toggleDetails(id) {
223
403
  const details = document.getElementById('details-' + id);
224
404
  details.classList.toggle('hidden');
225
405
  }
406
+
407
+ // Filter cases by status
408
+ function filterCases(status) {
409
+ currentFilter = status;
410
+
411
+ // Update active button
412
+ document.querySelectorAll('.filter-btn').forEach(btn => {
413
+ btn.classList.remove('active');
414
+ if (btn.getAttribute('data-filter') === status) {
415
+ btn.classList.add('active');
416
+ }
417
+ });
418
+
419
+ applyFilters();
420
+ }
421
+
422
+ // Search cases
423
+ function searchCases(query) {
424
+ currentSearch = query.toLowerCase().trim();
425
+ applyFilters();
426
+ }
427
+
428
+ // Apply both filter and search
429
+ function applyFilters() {
430
+ const rows = document.querySelectorAll('.case-row');
431
+ const table = document.getElementById('cases-table');
432
+ const noResults = document.getElementById('no-results');
433
+ let visibleCount = 0;
434
+
435
+ rows.forEach(row => {
436
+ const status = row.getAttribute('data-status');
437
+ const id = (row.getAttribute('data-id') || '').toLowerCase();
438
+ const name = (row.getAttribute('data-name') || '').toLowerCase();
439
+ const response = (row.getAttribute('data-response') || '').toLowerCase();
440
+ const reason = (row.getAttribute('data-reason') || '').toLowerCase();
441
+ const detailsRow = document.getElementById('details-' + row.getAttribute('data-id'));
442
+
443
+ // Check filter
444
+ const passesFilter = currentFilter === 'all' || status === currentFilter;
445
+
446
+ // Check search
447
+ let passesSearch = true;
448
+ if (currentSearch) {
449
+ passesSearch = id.includes(currentSearch) ||
450
+ name.includes(currentSearch) ||
451
+ response.includes(currentSearch) ||
452
+ reason.includes(currentSearch);
453
+ }
454
+
455
+ // Show/hide row
456
+ const shouldShow = passesFilter && passesSearch;
457
+ row.classList.toggle('hidden', !shouldShow);
458
+
459
+ // Handle search highlighting
460
+ if (shouldShow && currentSearch) {
461
+ row.classList.add('search-highlight');
462
+ } else {
463
+ row.classList.remove('search-highlight');
464
+ }
465
+
466
+ // Keep details row hidden state in sync but don't force it closed
467
+ if (!shouldShow && detailsRow) {
468
+ detailsRow.classList.add('hidden');
469
+ }
470
+
471
+ if (shouldShow) visibleCount++;
472
+ });
473
+
474
+ // Update results count
475
+ const totalCases = rows.length;
476
+ const resultsText = document.getElementById('results-count');
477
+ if (currentFilter === 'all' && !currentSearch) {
478
+ resultsText.textContent = 'Showing all ' + totalCases + ' test cases';
479
+ } else {
480
+ resultsText.textContent = 'Showing ' + visibleCount + ' of ' + totalCases + ' test cases';
481
+ }
482
+
483
+ // Show/hide no results message
484
+ if (visibleCount === 0) {
485
+ table.classList.add('hidden');
486
+ noResults.classList.remove('hidden');
487
+ } else {
488
+ table.classList.remove('hidden');
489
+ noResults.classList.add('hidden');
490
+ }
491
+ }
492
+
493
+ // Keyboard shortcut for search (Ctrl/Cmd + F focuses search)
494
+ document.addEventListener('keydown', function(e) {
495
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
496
+ const searchInput = document.getElementById('search-input');
497
+ // Only if we're on this page (not another browser search)
498
+ if (searchInput) {
499
+ e.preventDefault();
500
+ searchInput.focus();
501
+ searchInput.select();
502
+ }
503
+ }
504
+
505
+ // Escape clears search
506
+ if (e.key === 'Escape') {
507
+ const searchInput = document.getElementById('search-input');
508
+ if (searchInput && document.activeElement === searchInput) {
509
+ searchInput.value = '';
510
+ searchCases('');
511
+ searchInput.blur();
512
+ }
513
+ }
514
+ });
226
515
  </script>
227
516
  </body>
228
517
  </html>
@@ -253,6 +542,10 @@ export function generateHTMLReport(manifest: RunManifest): string {
253
542
  return JSON.stringify(prompt, null, 2);
254
543
  });
255
544
 
545
+ Handlebars.registerHelper('failedCount', (manifest: RunManifest) => {
546
+ return manifest.metrics.total_cases - manifest.metrics.passed_cases;
547
+ });
548
+
256
549
  const template = Handlebars.compile(HTML_TEMPLATE);
257
550
  return template({ manifest });
258
551
  }