@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.
- package/CHANGELOG.md +93 -0
- package/dist/html/compare-generator.d.ts +52 -0
- package/dist/html/compare-generator.d.ts.map +1 -0
- package/dist/html/generator.d.ts.map +1 -1
- package/dist/html/redteam-generator.d.ts.map +1 -1
- package/dist/html/stress-generator.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1739 -352
- package/package.json +2 -2
- package/src/html/compare-generator.ts +783 -0
- package/src/html/generator.ts +379 -86
- package/src/html/redteam-generator.ts +437 -134
- package/src/html/stress-generator.ts +215 -128
- package/src/index.ts +8 -0
package/src/html/generator.ts
CHANGED
|
@@ -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"
|
|
217
|
+
<div class="icon">🔒</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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
|
|
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">▼</span>
|
|
125
234
|
</div>
|
|
126
|
-
<div class="
|
|
127
|
-
<
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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">▼</span>
|
|
133
264
|
</div>
|
|
134
|
-
<div class="
|
|
135
|
-
|
|
136
|
-
<div class="
|
|
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">🔍</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
|
-
|
|
179
|
-
<div class="
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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">▼</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
|
-
|
|
209
|
-
<div class="
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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">▼</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
|
}
|