BID · Console
Baseline · Intelligence · Decision
web/static/app.js 3,491 bytes · javascript
/* 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. */
    };
  });
})();