/* Small client-side glue for the BID Console.
* - Live runner: POST a question, attach EventSource, append stdout
* to the log box, navigate to /run/:id when the pipeline finishes. */
(function () {
const form = document.getElementById('runForm');
if (!form) return;
const input = document.getElementById('question');
const btn = document.getElementById('runBtn');
const box = document.getElementById('liveBox');
const status = document.getElementById('liveStatus');
const log = document.getElementById('liveLog');
function append(line) {
const node = document.createTextNode(line + '\n');
log.appendChild(node);
log.scrollTop = log.scrollHeight;
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const question = (input.value || '').trim();
if (!question) return;
btn.disabled = true;
btn.textContent = 'Running…';
box.classList.remove('hidden');
status.className = 'liveStatus';
status.textContent = 'starting…';
log.textContent = '';
let res;
try {
res = await fetch('/api/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question }),
});
} catch (err) {
append('[client] network error: ' + err);
status.textContent = 'error';
status.classList.add('fail');
btn.disabled = false;
btn.textContent = 'Run';
return;
}
if (!res.ok) {
const body = await res.json().catch(() => ({}));
append('[server] ' + (body.error || res.statusText));
status.textContent = 'rejected';
status.classList.add('fail');
btn.disabled = false;
btn.textContent = 'Run';
return;
}
const { runId } = await res.json();
status.textContent = 'streaming…';
const es = new EventSource('/api/run/' + encodeURIComponent(runId) + '/stream');
const startedAt = Date.now();
let tick = null;
function updateElapsed() {
const s = Math.floor((Date.now() - startedAt) / 1000);
if (status.classList.contains('ok') || status.classList.contains('fail')) return;
status.textContent = 'streaming · ' + s + 's';
}
tick = setInterval(updateElapsed, 1000);
es.addEventListener('log', (ev) => {
try {
const { line } = JSON.parse(ev.data);
append(line);
} catch { /* noop */ }
});
es.addEventListener('done', (ev) => {
if (tick) clearInterval(tick);
try {
const data = JSON.parse(ev.data);
if (data.state === 'completed' && data.resultRunId) {
status.classList.add('ok');
status.textContent = 'done · opening report…';
setTimeout(() => { window.location.href = '/run/' + encodeURIComponent(data.resultRunId); }, 800);
} else if (data.resultRunId) {
status.classList.add('fail');
status.textContent = 'failed · opening audit…';
setTimeout(() => { window.location.href = '/run/' + encodeURIComponent(data.resultRunId); }, 800);
} else {
status.classList.add('fail');
status.textContent = 'failed (exit ' + data.exitCode + ')';
}
} catch {
status.classList.add('fail');
status.textContent = 'finished (parse error)';
}
es.close();
btn.disabled = false;
btn.textContent = 'Run';
});
es.onerror = () => {
/* EventSource auto-retries; let it. */
};
});
})();