/**
* 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, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
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 & 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>`,
});
}