Skip to content

Commit b7b7213

Browse files
committed
ui: add a file that renders the AI agent output
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
1 parent 01fad9f commit b7b7213

1 file changed

Lines changed: 381 additions & 0 deletions

File tree

ui/agent.html

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>CI Agent Reports</title>
7+
<link rel="icon" type="image/png" href="/favicon.png">
8+
<link rel="stylesheet" href="nipa.css">
9+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
10+
<style>
11+
body {
12+
max-width: 1200px;
13+
margin: 20px auto;
14+
padding: 0 20px;
15+
}
16+
17+
h1 {
18+
border-bottom: 2px solid #0366d6;
19+
padding-bottom: 10px;
20+
}
21+
22+
.entry {
23+
border: 1px solid #ddd;
24+
border-radius: 5px;
25+
margin-bottom: 24px;
26+
overflow: hidden;
27+
}
28+
29+
.entry-header {
30+
background-color: #34495e;
31+
color: #fff;
32+
padding: 12px 20px;
33+
display: flex;
34+
justify-content: space-between;
35+
align-items: center;
36+
cursor: pointer;
37+
user-select: none;
38+
}
39+
40+
.entry-header:hover {
41+
opacity: 0.9;
42+
}
43+
44+
.entry-header .branch {
45+
font-weight: bold;
46+
font-size: 1.05em;
47+
}
48+
49+
.entry-header .timestamp {
50+
font-size: 0.85em;
51+
opacity: 0.85;
52+
}
53+
54+
.entry-body {
55+
padding: 20px;
56+
}
57+
58+
.entry.collapsed .entry-body {
59+
display: none;
60+
}
61+
62+
.section-label {
63+
font-weight: bold;
64+
margin: 16px 0 8px 0;
65+
font-size: 0.95em;
66+
color: #555;
67+
}
68+
69+
.section-label:first-child {
70+
margin-top: 0;
71+
}
72+
73+
.failures-table {
74+
width: 100%;
75+
font-size: 0.9em;
76+
}
77+
78+
.failures-table td, .failures-table th {
79+
padding: 5px 10px;
80+
}
81+
82+
.result-fail {
83+
color: #c0392b;
84+
font-weight: bold;
85+
}
86+
87+
.result-skip {
88+
color: #2980b9;
89+
font-weight: bold;
90+
}
91+
92+
.commit-card {
93+
border: 1px solid #ddd;
94+
border-radius: 4px;
95+
padding: 10px 14px;
96+
margin-bottom: 8px;
97+
}
98+
99+
.commit-hash {
100+
font-family: "roboto mono", monospace;
101+
font-size: 0.85em;
102+
color: #888;
103+
}
104+
105+
.commit-desc {
106+
margin: 4px 0;
107+
}
108+
109+
.pw-links {
110+
font-size: 0.85em;
111+
margin-top: 4px;
112+
}
113+
114+
.pw-links a {
115+
margin-right: 12px;
116+
}
117+
118+
.analysis-toggle {
119+
display: inline-block;
120+
cursor: pointer;
121+
padding: 4px 12px;
122+
border-radius: 3px;
123+
font-size: 0.9em;
124+
margin-top: 12px;
125+
}
126+
127+
.analysis-content {
128+
margin-top: 12px;
129+
padding: 16px;
130+
border: 1px solid #ddd;
131+
border-radius: 4px;
132+
background: #f8f9fa;
133+
line-height: 1.6;
134+
overflow-x: auto;
135+
}
136+
137+
.analysis-content h1,
138+
.analysis-content h2,
139+
.analysis-content h3 {
140+
margin-top: 1em;
141+
margin-bottom: 0.4em;
142+
border-bottom: 1px solid #ddd;
143+
padding-bottom: 0.3em;
144+
}
145+
146+
.analysis-content h1:first-child {
147+
margin-top: 0;
148+
}
149+
150+
.analysis-content pre {
151+
background: #eee;
152+
padding: 10px;
153+
border-radius: 4px;
154+
overflow-x: auto;
155+
}
156+
157+
.analysis-content code {
158+
font-family: "roboto mono", monospace;
159+
font-size: 0.9em;
160+
}
161+
162+
.analysis-content p code {
163+
background: #eee;
164+
padding: 1px 5px;
165+
border-radius: 3px;
166+
}
167+
168+
.analysis-content table {
169+
margin: 0.5em 0;
170+
}
171+
172+
.analysis-content blockquote {
173+
border-left: 3px solid #0366d6;
174+
margin: 0.5em 0;
175+
padding: 0.4em 1em;
176+
color: #555;
177+
background: #eee;
178+
border-radius: 0 4px 4px 0;
179+
}
180+
181+
.loading {
182+
text-align: center;
183+
padding: 40px;
184+
font-size: 1.1em;
185+
color: #888;
186+
}
187+
188+
.error-msg {
189+
color: #c0392b;
190+
padding: 20px;
191+
text-align: center;
192+
}
193+
194+
.badge {
195+
display: inline-block;
196+
padding: 2px 8px;
197+
border-radius: 3px;
198+
font-size: 0.8em;
199+
font-weight: bold;
200+
}
201+
202+
.badge-fail { background: #e74c3c; color: #fff; }
203+
.badge-skip { background: #3498db; color: #fff; }
204+
.badge-confidence-high { background: #27ae60; color: #fff; }
205+
.badge-confidence-medium { background: #f39c12; color: #fff; }
206+
.badge-confidence-low { background: #e74c3c; color: #fff; }
207+
.badge-confidence-unknown { background: #95a5a6; color: #fff; }
208+
209+
.failure-summary {
210+
display: flex;
211+
gap: 8px;
212+
align-items: center;
213+
}
214+
215+
@media (prefers-color-scheme: dark) {
216+
.entry {
217+
border-color: #444;
218+
}
219+
.entry-header {
220+
background-color: #2c3e50;
221+
}
222+
.section-label {
223+
color: #999;
224+
}
225+
.commit-card {
226+
border-color: #444;
227+
background: #282828;
228+
}
229+
.commit-hash {
230+
color: #777;
231+
}
232+
.analysis-content {
233+
background: #282828;
234+
border-color: #444;
235+
}
236+
.analysis-content h1,
237+
.analysis-content h2,
238+
.analysis-content h3 {
239+
border-color: #444;
240+
}
241+
.analysis-content pre {
242+
background: #333;
243+
}
244+
.analysis-content p code {
245+
background: #333;
246+
}
247+
.analysis-content blockquote {
248+
background: #333;
249+
color: #999;
250+
}
251+
.result-fail {
252+
color: #e74c3c;
253+
}
254+
.result-skip {
255+
color: #5dade2;
256+
}
257+
}
258+
</style>
259+
</head>
260+
<body>
261+
<h1>CI Agent Reports</h1>
262+
<div id="content"><div class="loading">Loading...</div></div>
263+
264+
<script>
265+
fetch('/agent.json')
266+
.then(r => {
267+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
268+
return r.json();
269+
})
270+
.then(data => {
271+
const entries = data.slice().reverse();
272+
const container = document.getElementById('content');
273+
container.innerHTML = '';
274+
275+
entries.forEach((entry, idx) => {
276+
const div = document.createElement('div');
277+
div.className = 'entry' + (idx > 0 ? ' collapsed' : '');
278+
279+
const ts = new Date(entry.timestamp);
280+
const tsStr = ts.toLocaleString();
281+
282+
const failCount = entry.failures.filter(f => f.result === 'fail').length;
283+
const skipCount = entry.failures.filter(f => f.result === 'skip').length;
284+
285+
let badges = '';
286+
if (failCount) badges += `<span class="badge badge-fail">${failCount} fail</span>`;
287+
if (skipCount) badges += `<span class="badge badge-skip">${skipCount} skip</span>`;
288+
289+
const conf = entry.confidence;
290+
if (conf && conf.level) {
291+
const cls = 'badge-confidence-' + conf.level;
292+
badges += `<span class="badge ${cls}" title="${esc(conf.explanation || '')}">${esc(conf.level)} confidence</span>`;
293+
}
294+
295+
// Header
296+
const header = document.createElement('div');
297+
header.className = 'entry-header';
298+
header.innerHTML =
299+
`<span class="branch">${esc(entry.branch)}</span>` +
300+
`<span class="failure-summary">${badges}<span class="timestamp">${esc(tsStr)}</span></span>`;
301+
header.onclick = () => div.classList.toggle('collapsed');
302+
div.appendChild(header);
303+
304+
// Body
305+
const body = document.createElement('div');
306+
body.className = 'entry-body';
307+
308+
// Failures table
309+
body.innerHTML += '<div class="section-label">Failures</div>';
310+
let tbl = '<table class="failures-table"><tr><th>Executor</th><th>Group</th><th>Test</th><th>Result</th></tr>';
311+
entry.failures.forEach(f => {
312+
const cls = f.result === 'fail' ? 'result-fail' : 'result-skip';
313+
tbl += `<tr><td>${esc(f.executor)}</td><td>${esc(f.group)}</td><td>${esc(f.test)}</td><td class="${cls}">${esc(f.result)}</td></tr>`;
314+
});
315+
tbl += '</table>';
316+
body.innerHTML += tbl;
317+
318+
// Blamed commits
319+
if (entry.blamed_commits && entry.blamed_commits.length) {
320+
body.innerHTML += '<div class="section-label">Blamed Commits</div>';
321+
entry.blamed_commits.forEach(bc => {
322+
let pwHtml = '';
323+
const pws = Array.isArray(bc.patchwork) ? bc.patchwork : (bc.patchwork ? [bc.patchwork] : []);
324+
if (pws.length) {
325+
pwHtml = '<div class="pw-links">' + pws.map(pw =>
326+
`<a href="${esc(pw.url)}" target="_blank" rel="noopener">${esc(pw.name)}</a>` +
327+
(pw.submitter ? ` (${esc(pw.submitter)})` : '')
328+
).join('<br>') + '</div>';
329+
}
330+
body.innerHTML +=
331+
`<div class="commit-card">` +
332+
`<span class="commit-hash">${esc(bc.commit.substring(0, 12))}</span>` +
333+
`<div class="commit-desc">${esc(bc.description)}</div>` +
334+
pwHtml +
335+
`</div>`;
336+
});
337+
}
338+
339+
// Analysis (collapsible)
340+
if (entry.analysis) {
341+
const aid = 'analysis-' + idx;
342+
body.innerHTML +=
343+
`<a class="nipa-button analysis-toggle" onclick="toggleAnalysis('${aid}')">Show Analysis</a>` +
344+
`<div id="${aid}" class="analysis-content" style="display:none"></div>`;
345+
}
346+
347+
div.appendChild(body);
348+
container.appendChild(div);
349+
350+
// Render markdown after DOM insertion
351+
if (entry.analysis) {
352+
const aid = 'analysis-' + idx;
353+
document.getElementById(aid).innerHTML = marked.parse(entry.analysis);
354+
}
355+
});
356+
})
357+
.catch(err => {
358+
document.getElementById('content').innerHTML =
359+
`<div class="error-msg">Failed to load agent.json: ${esc(err.message)}</div>`;
360+
});
361+
362+
function toggleAnalysis(id) {
363+
const el = document.getElementById(id);
364+
const btn = el.previousElementSibling;
365+
if (el.style.display === 'none') {
366+
el.style.display = 'block';
367+
btn.textContent = 'Hide Analysis';
368+
} else {
369+
el.style.display = 'none';
370+
btn.textContent = 'Show Analysis';
371+
}
372+
}
373+
374+
function esc(s) {
375+
const d = document.createElement('div');
376+
d.textContent = s;
377+
return d.innerHTML;
378+
}
379+
</script>
380+
</body>
381+
</html>

0 commit comments

Comments
 (0)