live panel: pretty rendering (per-event rows, turn blocks, color-coded)
This commit is contained in:
parent
70af56e050
commit
9716f20f81
1 changed files with 124 additions and 60 deletions
|
|
@ -114,81 +114,109 @@ fn render_online(label: &str) -> String {
|
|||
/// reload, so the login flow and other forms aren't clobbered.
|
||||
const LIVE_PANEL: &str = r#"
|
||||
<h3>live</h3>
|
||||
<pre id="live" class="diff"><span class="meta">connecting…</span></pre>
|
||||
<div id="live" class="live"><div class="meta">connecting…</div></div>
|
||||
<script>
|
||||
(function() {
|
||||
const log = document.getElementById('live');
|
||||
let placeholder = log.firstChild;
|
||||
function setPlaceholder(text, cls) {
|
||||
function setPlaceholder(text) {
|
||||
log.innerHTML = '';
|
||||
const span = document.createElement('span');
|
||||
span.className = cls || 'meta';
|
||||
const span = document.createElement('div');
|
||||
span.className = 'meta';
|
||||
span.textContent = text;
|
||||
log.appendChild(span);
|
||||
placeholder = span;
|
||||
}
|
||||
function appendLine(text, cls) {
|
||||
function clearPlaceholder() {
|
||||
if (placeholder) { log.innerHTML = ''; placeholder = null; }
|
||||
const row = document.createElement('span');
|
||||
if (cls) row.className = cls;
|
||||
row.textContent = text + '\n';
|
||||
log.appendChild(row);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
function fmt(ev) {
|
||||
if (ev.kind === 'turn_start') return '◆ TURN ← ' + ev.from + ': ' + ev.body;
|
||||
if (ev.kind === 'turn_end') return '◇ TURN END ' + (ev.ok ? 'ok' : 'fail') + (ev.note ? ' — ' + ev.note : '');
|
||||
if (ev.kind === 'note') return '· ' + ev.text;
|
||||
if (ev.kind === 'stream') {
|
||||
// serde internal tagging flattens the inner json next to `kind`,
|
||||
// so the original stream-json event sits under `ev` minus `kind`.
|
||||
const v = Object.assign({}, ev); delete v.kind;
|
||||
if (v.type === 'system' && v.subtype === 'init') return '[init] tools=' + (v.tools||[]).length;
|
||||
function row(cls, text) {
|
||||
clearPlaceholder();
|
||||
const el = document.createElement('div');
|
||||
el.className = 'row ' + (cls || '');
|
||||
el.textContent = text;
|
||||
log.appendChild(el);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
return el;
|
||||
}
|
||||
function trim(s, n) {
|
||||
return s.length > n ? s.slice(0, n) + '…' : s;
|
||||
}
|
||||
function renderStream(v) {
|
||||
if (v.type === 'system' && v.subtype === 'init') {
|
||||
row('sys', '· session init · tools=' + (v.tools||[]).length + ' model=' + (v.model || '?'));
|
||||
return;
|
||||
}
|
||||
if (v.type === 'rate_limit_event') {
|
||||
const u = Math.round((v.rate_limit_info?.utilization || 0) * 100);
|
||||
const s = v.rate_limit_info?.status || '';
|
||||
row('sys', '· rate-limit util=' + u + '% (' + s + ')');
|
||||
return;
|
||||
}
|
||||
if (v.type === 'assistant' && v.message && v.message.content) {
|
||||
const parts = v.message.content.map(c => {
|
||||
if (c.type === 'text') return c.text;
|
||||
if (c.type === 'tool_use') return '→ ' + c.name + '(' + JSON.stringify(c.input) + ')';
|
||||
return c.type;
|
||||
});
|
||||
return parts.join('\n');
|
||||
for (const c of v.message.content) {
|
||||
if (c.type === 'text' && c.text && c.text.trim())
|
||||
row('text', c.text);
|
||||
else if (c.type === 'thinking')
|
||||
row('thinking', '· thinking …');
|
||||
else if (c.type === 'tool_use')
|
||||
row('tool-use', '→ ' + c.name + ' ' + trim(JSON.stringify(c.input || {}), 240));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (v.type === 'user' && v.message && v.message.content) {
|
||||
const parts = v.message.content.map(c => {
|
||||
for (const c of v.message.content) {
|
||||
if (c.type === 'tool_result') {
|
||||
const txt = Array.isArray(c.content) ? c.content.map(p => p.text || '').join(' ') : (c.content || '');
|
||||
return '← ' + (txt.length > 200 ? txt.slice(0,200) + '…' : txt);
|
||||
const txt = Array.isArray(c.content)
|
||||
? c.content.map(p => p.text || '').join(' ')
|
||||
: (c.content || '');
|
||||
row('tool-result', '← ' + trim(txt, 300));
|
||||
}
|
||||
return c.type;
|
||||
});
|
||||
return parts.join('\n');
|
||||
}
|
||||
if (v.type === 'result') return '[done] ' + (v.subtype || '') + (v.is_error ? ' error' : '');
|
||||
return JSON.stringify(v);
|
||||
return;
|
||||
}
|
||||
return JSON.stringify(ev);
|
||||
if (v.type === 'result') {
|
||||
row('result', '✓ done · ' + (v.subtype || '') + (v.is_error ? ' [error]' : ''));
|
||||
return;
|
||||
}
|
||||
function cls(ev) {
|
||||
if (ev.kind === 'turn_start') return 'turnstart';
|
||||
if (ev.kind === 'turn_end') return ev.ok ? 'turnok' : 'turnfail';
|
||||
if (ev.kind === 'note') return 'meta';
|
||||
return '';
|
||||
// Fallback: small one-liner for unknown events; don't spam.
|
||||
row('sys', '· ' + trim(JSON.stringify(v), 200));
|
||||
}
|
||||
function handle(ev) {
|
||||
if (ev.kind === 'turn_start') {
|
||||
const block = row('turn-start', '◆ TURN ← ' + ev.from);
|
||||
const body = document.createElement('div');
|
||||
body.className = 'turn-body';
|
||||
body.textContent = ev.body;
|
||||
block.appendChild(body);
|
||||
return;
|
||||
}
|
||||
if (ev.kind === 'turn_end') {
|
||||
const cls = ev.ok ? 'turn-end-ok' : 'turn-end-fail';
|
||||
const sym = ev.ok ? '✓' : '✗';
|
||||
row(cls, sym + ' turn ' + (ev.ok ? 'ok' : 'fail') + (ev.note ? ' — ' + ev.note : ''));
|
||||
return;
|
||||
}
|
||||
if (ev.kind === 'note') {
|
||||
row('note', '· ' + ev.text);
|
||||
return;
|
||||
}
|
||||
if (ev.kind === 'stream') {
|
||||
const v = Object.assign({}, ev); delete v.kind;
|
||||
renderStream(v);
|
||||
return;
|
||||
}
|
||||
row('note', JSON.stringify(ev));
|
||||
}
|
||||
const es = new EventSource('/events/stream');
|
||||
es.onopen = function() { setPlaceholder('(connected — waiting for events)'); };
|
||||
es.onmessage = function(e) {
|
||||
try {
|
||||
const ev = JSON.parse(e.data);
|
||||
appendLine(fmt(ev), cls(ev));
|
||||
} catch (err) {
|
||||
appendLine('[parse err] ' + e.data, 'meta');
|
||||
}
|
||||
try { handle(JSON.parse(e.data)); }
|
||||
catch (err) { row('note', '[parse err] ' + e.data); }
|
||||
};
|
||||
es.onerror = function() {
|
||||
if (es.readyState === EventSource.CONNECTING) {
|
||||
setPlaceholder('(reconnecting…)');
|
||||
} else {
|
||||
appendLine('[disconnected]', 'meta');
|
||||
}
|
||||
if (es.readyState === EventSource.CONNECTING) setPlaceholder('(reconnecting…)');
|
||||
else row('note', '[disconnected]');
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -437,10 +465,46 @@ const STYLE: &str = r#"
|
|||
word-break: break-all;
|
||||
max-height: 30em;
|
||||
}
|
||||
#live { max-height: 24em; overflow-y: auto; }
|
||||
#live span { display: block; }
|
||||
#live .turnstart { color: var(--amber); }
|
||||
#live .turnok { color: var(--green); }
|
||||
#live .turnfail { color: #ff6b6b; }
|
||||
.live {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--purple-dim);
|
||||
padding: 0.4em 0.6em;
|
||||
overflow-y: auto;
|
||||
max-height: 32em;
|
||||
font-family: inherit;
|
||||
}
|
||||
.live .row {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
padding: 0.05em 0;
|
||||
line-height: 1.45;
|
||||
border-left: 2px solid transparent;
|
||||
padding-left: 0.5em;
|
||||
margin: 0.1em 0;
|
||||
}
|
||||
.live .row + .row { border-top: 0; }
|
||||
.live .turn-start {
|
||||
color: var(--amber);
|
||||
font-weight: bold;
|
||||
margin-top: 1em;
|
||||
border-left-color: var(--amber);
|
||||
padding-top: 0.3em;
|
||||
}
|
||||
.live .turn-start:first-child { margin-top: 0; }
|
||||
.live .turn-body {
|
||||
color: var(--fg);
|
||||
font-weight: normal;
|
||||
margin-top: 0.15em;
|
||||
padding-left: 1.2em;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.live .turn-end-ok { color: #66ff99; border-left-color: #66ff99; margin-bottom: 0.4em; }
|
||||
.live .turn-end-fail { color: #ff6b6b; border-left-color: #ff6b6b; margin-bottom: 0.4em; }
|
||||
.live .text { color: var(--fg); padding-left: 1.2em; }
|
||||
.live .thinking { color: var(--muted); font-style: italic; padding-left: 1.2em; }
|
||||
.live .tool-use { color: #66e0ff; padding-left: 1.2em; }
|
||||
.live .tool-result { color: var(--muted); padding-left: 1.2em; }
|
||||
.live .result { color: var(--green); padding-left: 0.5em; }
|
||||
.live .sys, .live .note { color: var(--muted); }
|
||||
</style>
|
||||
"#;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue