BID · Console
Baseline · Intelligence · Decision
web/views.ts 21,846 bytes · typescript
/**
 * HTML views — no template engine, just escaped template strings.
 *
 * Every interpolated value passes through `esc()` unless it's
 * explicitly pre-rendered HTML produced by another view function.
 *
 * The same views power two outputs:
 *   - mode "live"   — the dev server in web/server.ts, with the
 *                     question-box form and SSE-driven runner
 *   - mode "static" — the build in scripts/build-static.ts, which
 *                     drops the form and rewrites URLs to a
 *                     directory-per-page layout suitable for
 *                     Cloudflare Pages
 *
 * Callers may omit `opts`; the default is live for backward
 * compatibility with the existing server.
 */

import type { RunSummary, Run } from './runs.js';
import type { TreeNode, CodeFile } from './code-tree.js';

export interface RenderOpts {
  readonly mode: 'live' | 'static';
}
const DEFAULT_OPTS: RenderOpts = { mode: 'live' };

function runUrl(id: string, opts: RenderOpts): string {
  return opts.mode === 'static' ? `/run/${id}/` : `/run/${id}`;
}
function codeLandingUrl(opts: RenderOpts): string {
  return opts.mode === 'static' ? '/code/' : '/code';
}
function codeFileUrl(filePath: string, opts: RenderOpts): string {
  return opts.mode === 'static' ? `/code/${filePath}/` : `/code?path=${encodeURIComponent(filePath)}`;
}

function esc(v: unknown): string {
  return String(v)
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

function fmtMs(ms: number): string {
  if (!ms) return '—';
  if (ms < 1000) return `${ms}ms`;
  if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
  return `${(ms / 60_000).toFixed(1)}m`;
}

function fmtCost(usd: number): string {
  if (!usd) return '$0';
  if (usd < 0.01) return `$${usd.toFixed(5)}`;
  return `$${usd.toFixed(3)}`;
}

function fmtTokens(n: number): string {
  if (n < 1000) return String(n);
  if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`;
  return `${(n / 1_000_000).toFixed(2)}M`;
}

function fmtDate(iso: string): string {
  const d = new Date(iso);
  return d.toLocaleString();
}

function layout(args: {
  title: string;
  nav: 'home' | 'code' | 'run';
  body: string;
  head?: string;
  renderOpts: RenderOpts;
}): string {
  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>${esc(args.title)}</title>
  <link rel="stylesheet" href="/static/style.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
  ${args.head ?? ''}
</head>
<body>
  <header class="topbar">
    <a class="brand" href="/">BID <span class="dot">·</span> Console</a>
    <nav>
      <a href="/" class="${args.nav === 'home' ? 'active' : ''}">Runs</a>
      <a href="${codeLandingUrl(args.renderOpts)}" class="${args.nav === 'code' ? 'active' : ''}">Code</a>
    </nav>
    <div class="tag">Baseline · Intelligence · Decision</div>
  </header>
  <main>${args.body}</main>
  <script src="/static/app.js" defer></script>
</body>
</html>`;
}

/* ---------- Homepage: live runner + run list ---------- */

export function homepage(runs: readonly RunSummary[], opts: RenderOpts = DEFAULT_OPTS): string {
  const isStatic = opts.mode === 'static';
  const emptyRow = isStatic
    ? `<tr><td colspan="7" class="muted center">No runs in this deployment.</td></tr>`
    : `<tr><td colspan="7" class="muted center">No runs yet. Type a question above to create the first one.</td></tr>`;
  const rows = runs.length === 0
    ? emptyRow
    : runs.map(r => `
      <tr>
        <td><a href="${runUrl(r.id, opts)}">${esc(r.question)}</a></td>
        <td>${r.ok
          ? `<span class="status ok">ok</span>`
          : `<span class="status fail" title="${esc(r.failureCategory ?? '')}">failed</span>`}</td>
        <td class="num">${r.stages}</td>
        <td class="num">${r.insightCount}</td>
        <td class="num">${fmtMs(r.elapsedMs)}</td>
        <td class="num">${fmtCost(r.estCostUsd)}</td>
        <td class="muted small">${esc(fmtDate(r.mtime))}</td>
      </tr>`).join('');

  /* In static (Cloudflare Pages) deployments we drop the live runner —
   * there's no server to spawn the pipeline. The note below tells
   * viewers where to get the live experience. */
  const interactive = isStatic
    ? `
      <div class="staticNotice">
        <strong>Historical runs only.</strong>
        This deployment shows past pipeline runs from the development environment.
        To type a fresh question and watch the 7-agent pipeline execute end-to-end,
        run the local development version (<code>npm run web</code> with an
        <code>ANTHROPIC_API_KEY</code> set).
      </div>`
    : `
        <form id="runForm" class="runForm">
          <input type="text" id="question" name="question" autocomplete="off" placeholder='e.g. "Compare technology spending efficiency for JPMorgan Chase and Bank of America for FY-2024"' />
          <button type="submit" id="runBtn">Run</button>
        </form>
        <div id="liveBox" class="liveBox hidden">
          <div class="liveHeader">
            <span class="liveLabel">Pipeline output</span>
            <span class="liveStatus" id="liveStatus">starting…</span>
          </div>
          <pre id="liveLog"></pre>
        </div>`;

  return layout({
    title: 'BID Console — Runs',
    nav: 'home',
    renderOpts: opts,
    body: `
      <section class="hero">
        <h1>From a question to evidence-cited insights</h1>
        <p class="lead">A financial question goes in. The 7-agent BID pipeline turns it into a structured JobRequest, fetches the source data from SEC EDGAR, normalises it, computes the metric, runs the comparison, and synthesises insights — every claim cited back to its source filing.</p>
        ${interactive}
      </section>

      <section class="runs">
        <h2>Past runs</h2>
        <table class="runsTable">
          <thead>
            <tr>
              <th>Question</th>
              <th>Status</th>
              <th class="num">Stages</th>
              <th class="num">Insights</th>
              <th class="num">Elapsed</th>
              <th class="num">~Cost</th>
              <th>When</th>
            </tr>
          </thead>
          <tbody>${rows}</tbody>
        </table>
      </section>
    `,
  });
}

/* ---------- Run detail page ---------- */

interface TraceEntry {
  agent: string;
  standard: number;
  step: string;
  detail: string;
  at: string;
}

interface UsageRow {
  agent: string;
  model: string;
  calls: number;
  inputTokens: number;
  outputTokens: number;
}

function renderJobRequest(job: Record<string, unknown>): string {
  const entities = (job.entities ?? []) as { id: string; aliases: string[] }[];
  const targetMetrics = (job.targetMetrics ?? []) as { key: string; definition: string; unit?: string }[];
  const derivedMetrics = (job.derivedMetrics ?? []) as
    | { key: string; definition: string; methodology: string | null; unit?: string }[]
    | undefined;
  return `
    <div class="card">
      <h3>JobRequest <span class="muted small">(constructed by intake)</span></h3>
      <dl class="kv">
        <dt>Question</dt><dd>${esc(job.question ?? '')}</dd>
        <dt>analysisId</dt><dd><code>${esc(job.analysisId ?? '')}</code></dd>
        <dt>Period</dt><dd><code>${esc(job.period ?? '')}</code></dd>
        <dt>Sources</dt><dd>${((job.sources ?? []) as string[]).map(s => `<code>${esc(s)}</code>`).join(' ')}</dd>
      </dl>
      <div class="grid">
        <div>
          <h4>Entities</h4>
          <ul class="bare">
            ${entities.map(e => `<li><strong>${esc(e.id)}</strong>${e.aliases?.length ? ` <span class="muted small">(${e.aliases.map(esc).join(', ')})</span>` : ''}</li>`).join('')}
          </ul>
        </div>
        <div>
          <h4>Target metrics <span class="muted small">(source concepts)</span></h4>
          <ul class="bare">
            ${targetMetrics.map(m => `<li><code>${esc(m.key)}</code>${m.unit ? ` <span class="tagSmall">${esc(m.unit)}</span>` : ''}<br><span class="muted small">${esc(m.definition)}</span></li>`).join('')}
          </ul>
        </div>
        ${derivedMetrics && derivedMetrics.length > 0 ? `
          <div>
            <h4>Derived metrics <span class="muted small">(computed)</span></h4>
            <ul class="bare">
              ${derivedMetrics.map(m => `<li><code>${esc(m.key)}</code>${m.unit ? ` <span class="tagSmall">${esc(m.unit)}</span>` : ''}<br><span class="muted small">${esc(m.definition)}</span><br>methodology: ${m.methodology ? `<code>${esc(m.methodology)}</code>` : '<span class="muted small">none (will escalate per Std 9)</span>'}</li>`).join('')}
            </ul>
          </div>
        ` : ''}
      </div>
    </div>
  `;
}

function renderPipeline(trace: TraceEntry[], pipeline: { agent: string; pillar: string }[], failure: { agent?: string } | null): string {
  /* Group trace by agent in pipeline order. */
  const byAgent = new Map<string, TraceEntry[]>();
  for (const t of trace) {
    const list = byAgent.get(t.agent);
    if (list) list.push(t); else byAgent.set(t.agent, [t]);
  }
  const stages = pipeline.map(p => {
    const fullName = `${p.pillar}.${p.agent}`;
    const events = byAgent.get(fullName) ?? [];
    const completed = events.some(e => e.step.includes('handoff') || e.step.includes('package-handoff') || e.step.includes('package-outcome'));
    const isFailure = failure?.agent === fullName;
    const status = isFailure ? 'fail' : (events.length === 0 ? 'idle' : (completed ? 'ok' : 'partial'));
    const eventList = events.map(e => `
      <li>
        <span class="stdBadge">Std ${e.standard}</span>
        <strong>${esc(e.step)}</strong>
        <span class="muted">${esc(e.detail)}</span>
      </li>`).join('');
    return `
      <div class="stage ${status}">
        <div class="stageHead">
          <span class="stageIdx">${esc(p.pillar)}</span>
          <span class="stageName">${esc(p.agent)}</span>
          <span class="stageStatus">${status}</span>
        </div>
        <ul class="traceList">${eventList || '<li class="muted small">(no trace; never reached)</li>'}</ul>
      </div>`;
  }).join('');
  return `<div class="card"><h3>Pipeline</h3><div class="stages">${stages}</div></div>`;
}

function renderInsights(payload: Record<string, unknown>): string {
  const insights = (payload.insights ?? []) as {
    insightId: string;
    claim: string;
    frameworkUsed?: string;
    isInference?: boolean;
    confidence: number;
    supportingEvidence?: { kind: string; ref: string; detail?: string }[];
    reasoningLineage?: string[];
    flags?: string[];
  }[];
  const removed = (payload.unsupportedClaimsRemoved ?? []) as { claim: string; reason: string }[];
  const notes = (payload.notes ?? []) as string[];

  const insightHtml = insights.map(i => {
    const evidence = (i.supportingEvidence ?? []).map(e => `
      <li><span class="tagSmall">${esc(e.kind)}</span> <code>${esc(e.ref)}</code>${e.detail ? `<br><span class="muted small">${esc(e.detail)}</span>` : ''}</li>`).join('');
    const lineage = (i.reasoningLineage ?? []).map(l => {
      const isUrl = /^https?:\/\//.test(l);
      return `<li class="muted small">${isUrl ? `<a href="${esc(l)}" target="_blank" rel="noreferrer">${esc(l)}</a>` : esc(l)}</li>`;
    }).join('');
    const flags = (i.flags ?? []).map(f => `<li class="flag">${esc(f)}</li>`).join('');
    return `
      <article class="insight ${i.isInference ? 'inference' : ''}">
        <header>
          <h4>${esc(i.claim)}</h4>
          <div class="meta">
            <span class="tagSmall">conf ${i.confidence.toFixed(2)}</span>
            ${i.frameworkUsed ? `<span class="tagSmall">${esc(i.frameworkUsed)}</span>` : ''}
            ${i.isInference ? `<span class="tagSmall inferenceTag">inference</span>` : ''}
          </div>
        </header>
        <details ${insights.length <= 4 ? 'open' : ''}>
          <summary>Evidence &amp; lineage</summary>
          ${evidence ? `<h5>Supporting evidence</h5><ul class="bare">${evidence}</ul>` : ''}
          ${lineage ? `<h5>Reasoning lineage</h5><ul class="bare">${lineage}</ul>` : ''}
          ${flags ? `<h5>Flags</h5><ul class="bare">${flags}</ul>` : ''}
        </details>
      </article>`;
  }).join('');

  const removedHtml = removed.length === 0 ? '' : `
    <div class="card warnCard">
      <h3>Unsupported claims removed <span class="muted small">(Std 4 discipline visible)</span></h3>
      <ul class="bare">
        ${removed.map(r => `<li><strong>${esc(r.claim)}</strong><br><span class="muted small">${esc(r.reason)}</span></li>`).join('')}
      </ul>
    </div>`;

  const notesHtml = notes.length === 0 ? '' : `
    <div class="card">
      <h3>Notes</h3>
      <ul class="bare">${notes.map(n => `<li class="muted small">${esc(n)}</li>`).join('')}</ul>
    </div>`;

  return `
    <div class="card">
      <h3>Insights <span class="muted small">(${insights.length} cited, evidence preserved)</span></h3>
      <div class="insights">${insightHtml || '<p class="muted">No insights in payload.</p>'}</div>
    </div>
    ${removedHtml}
    ${notesHtml}
  `;
}

function renderFailure(failure: Record<string, unknown>): string {
  return `
    <div class="card failCard">
      <h3>Run failed</h3>
      <dl class="kv">
        <dt>Agent</dt><dd><code>${esc(failure.agent ?? '')}</code></dd>
        <dt>Category</dt><dd><code>${esc(failure.category ?? '')}</code></dd>
        <dt>Reason</dt><dd>${esc(failure.reason ?? '')}</dd>
      </dl>
    </div>`;
}

function renderUsage(usage: Record<string, unknown>): string {
  const byAgent = (usage.byAgent ?? []) as UsageRow[];
  const totals = (usage.totals ?? { calls: 0, inputTokens: 0, outputTokens: 0 }) as { calls: number; inputTokens: number; outputTokens: number };
  let totalCost = 0;
  const rows = byAgent.map(r => {
    const cost = r.model.startsWith('claude-haiku-4-5')
      ? (r.inputTokens / 1_000_000) * 1.0 + (r.outputTokens / 1_000_000) * 5.0
      : 0;
    totalCost += cost;
    return `
      <tr>
        <td><code>${esc(r.agent)}</code></td>
        <td class="muted small">${esc(r.model)}</td>
        <td class="num">${r.calls}</td>
        <td class="num">${fmtTokens(r.inputTokens)}</td>
        <td class="num">${fmtTokens(r.outputTokens)}</td>
        <td class="num">${fmtCost(cost)}</td>
      </tr>`;
  }).join('');
  return `
    <div class="card">
      <h3>Anthropic usage <span class="muted small">(Haiku 4.5 list pricing)</span></h3>
      <table class="usageTable">
        <thead>
          <tr><th>Agent</th><th>Model</th><th class="num">Calls</th><th class="num">In tok</th><th class="num">Out tok</th><th class="num">~Cost</th></tr>
        </thead>
        <tbody>
          ${rows || '<tr><td colspan="6" class="muted center">No LLM calls in this run.</td></tr>'}
          <tr class="totalRow">
            <td><strong>TOTAL</strong></td><td></td>
            <td class="num"><strong>${totals.calls}</strong></td>
            <td class="num"><strong>${fmtTokens(totals.inputTokens)}</strong></td>
            <td class="num"><strong>${fmtTokens(totals.outputTokens)}</strong></td>
            <td class="num"><strong>${fmtCost(totalCost)}</strong></td>
          </tr>
        </tbody>
      </table>
    </div>`;
}

export function runDetail(run: Run, opts: RenderOpts = DEFAULT_OPTS): string {
  const audit = run.audit;
  const job = (audit.jobRequest ?? {}) as Record<string, unknown>;
  const trace = (audit.trace ?? []) as TraceEntry[];
  const pipeline = (audit.pipeline ?? []) as { agent: string; pillar: string }[];
  const usage = (audit.usage ?? {}) as Record<string, unknown>;
  const failure = (audit.failure ?? null) as Record<string, unknown> | null;
  const finalHandoff = (audit.finalHandoff ?? null) as { payload?: Record<string, unknown> } | null;
  const auditJson = JSON.stringify(audit, null, 2);

  return layout({
    title: `Run · ${run.question.slice(0, 60)}…`,
    nav: 'run',
    renderOpts: opts,
    body: `
      <section class="runHeader">
        <div class="crumbs"><a href="/">← All runs</a></div>
        <h1>${esc(run.question)}</h1>
        <div class="runMeta">
          <span>${run.ok ? `<span class="status ok">ok</span>` : `<span class="status fail">failed</span>`}</span>
          <span class="muted small"><code>${esc(run.analysisId)}</code></span>
          <span class="muted small">${fmtMs(run.elapsedMs)}</span>
          <span class="muted small">${run.totalCalls} LLM calls</span>
          <span class="muted small">${fmtTokens(run.totalInputTokens)} in / ${fmtTokens(run.totalOutputTokens)} out</span>
          <span class="muted small">~${fmtCost(run.estCostUsd)}</span>
        </div>
      </section>

      ${renderJobRequest(job)}
      ${renderPipeline(trace, pipeline, failure)}
      ${failure ? renderFailure(failure) : finalHandoff?.payload ? renderInsights(finalHandoff.payload) : ''}
      ${renderUsage(usage)}

      <details class="card">
        <summary><h3 style="display:inline">Full audit JSON</h3> <span class="muted small">(${auditJson.length.toLocaleString()} chars)</span></summary>
        <pre class="codeBlock"><code class="language-json">${esc(auditJson)}</code></pre>
      </details>

      <script>
        if (window.hljs) window.hljs.highlightAll();
      </script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
      <script>document.querySelectorAll('pre code').forEach(b => window.hljs && window.hljs.highlightElement(b));</script>
    `,
  });
}

/* ---------- Code browser ---------- */

function renderTree(nodes: readonly TreeNode[], selectedPath: string, opts: RenderOpts): string {
  return `<ul class="treeList">${nodes.map(n => {
    if (n.kind === 'dir') {
      const isSelectedDescendant = selectedPath.startsWith(n.path + '/');
      return `<li class="treeDir">
        <details ${isSelectedDescendant ? 'open' : ''}>
          <summary>${esc(n.name)}/</summary>
          ${renderTree(n.children ?? [], selectedPath, opts)}
        </details>
      </li>`;
    }
    const isSelected = n.path === selectedPath;
    return `<li class="treeFile ${isSelected ? 'sel' : ''}">
      <a href="${codeFileUrl(n.path, opts)}">${esc(n.name)}</a>
    </li>`;
  }).join('')}</ul>`;
}

/** Curated landing-page shortcuts — the "show this to the partner first"
 *  entry points. Also drives which files the static build always emits. */
export const CURATED_FILE_SHORTCUTS: readonly { path: string; gloss: string }[] = [
  { path: 'src/standards.ts',                                                        gloss: 'the 12 universal standards (canonical)' },
  { path: 'src/intelligence/methodologies/banking/tech_opex_efficiency_banking.yaml', gloss: 'SME methodology library entry (banking)' },
  { path: 'src/intelligence/methodologies/cross_domain/peer_benchmark_three_year_growth.yaml', gloss: 'SME methodology library entry (cross-domain)' },
  { path: 'src/agents/baseline/source-extraction/prompt.ts',                         gloss: 'agent prompt with narrow-first guidance' },
  { path: 'src/tools/retrieval/connectors/sec-edgar-xbrl.ts',                        gloss: 'SEC connector with period scoping' },
  { path: 'src/tools/retrieval/connectors/sec-edgar.ts',                             gloss: 'SEC connector with summary-mode sec_financials' },
  { path: 'src/agents/baseline/normalization/index.ts',                              gloss: 'cost-appropriate execution short-circuit (Std 6/7)' },
  { path: 'scripts/intake.ts',                                                       gloss: 'English-to-JobRequest intake' },
  { path: 'src/orchestrator.ts',                                                     gloss: 'pipeline orchestrator' },
  { path: 'src/observability/usage.ts',                                              gloss: 'token + cost meter' },
];

export function codePage(tree: readonly TreeNode[], file: CodeFile | null, opts: RenderOpts = DEFAULT_OPTS): string {
  const selectedPath = file?.path ?? '';
  const right = file ? `
    <div class="codeHeader">
      <code>${esc(file.path)}</code>
      <span class="muted small">${file.bytes.toLocaleString()} bytes · ${esc(file.language)}</span>
    </div>
    <pre class="codeBlock"><code class="language-${esc(file.language)}">${esc(file.content)}</code></pre>
  ` : `
    <div class="codeEmpty">
      <h2>Select a file</h2>
      <p class="muted">Browse the BID source on the left. Highlights to show the partner first:</p>
      <ul class="bare">
        ${CURATED_FILE_SHORTCUTS.map(s => `<li><a href="${codeFileUrl(s.path, opts)}"><code>${esc(s.path)}</code></a> — ${esc(s.gloss)}</li>`).join('')}
      </ul>
    </div>
  `;
  return layout({
    title: file ? `Code · ${file.path}` : 'Code · Browse',
    nav: 'code',
    renderOpts: opts,
    body: `
      <section class="codeLayout">
        <aside class="codeTree">
          <h3>Files</h3>
          ${renderTree(tree, selectedPath, opts)}
        </aside>
        <div class="codePane">${right}</div>
      </section>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
      <script>document.querySelectorAll('pre code').forEach(b => window.hljs && window.hljs.highlightElement(b));</script>
    `,
  });
}

/* ---------- 404 ---------- */

export function notFoundPage(detail: string, opts: RenderOpts = DEFAULT_OPTS): string {
  return layout({
    title: 'Not found',
    nav: 'home',
    renderOpts: opts,
    body: `<section class="hero"><h1>404</h1><p class="lead">${esc(detail)}</p><p><a href="/">Back to runs</a></p></section>`,
  });
}