File size: 9,977 Bytes
e8a6c67 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | // Renders the print-ready auditable report from the agent's last result,
// passed via sessionStorage. Includes original query, planner decision,
// full specialist trail, map snapshot, briefing prose with citations,
// and a Sources section listing every doc_id with its vintage + URL.
(function () {
const raw = sessionStorage.getItem("riprap_report");
if (!raw) return;
let pkg;
try { pkg = JSON.parse(raw); } catch (e) {
document.getElementById("paper").innerHTML =
`<p style="color:#c00">Could not parse stored report payload: ${e.message}</p>`;
return;
}
render(pkg);
})();
function escapeHtml(s) {
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function render(pkg) {
const r = pkg.result || {};
const plan = pkg.plan || r.plan || {};
const trace = pkg.trace || [];
const labels = pkg.sourceLabels || {};
const urls = pkg.sourceUrls || {};
const vintages = pkg.sourceVintages || {};
const stepLabels = pkg.stepLabels || {};
const intent = r.intent || plan.intent || "β";
const intentTitleMap = {
single_address: "Flood-exposure briefing β address",
neighborhood: "Flood-exposure briefing β neighborhood",
development_check: "Active development Γ flood exposure",
live_now: "Current conditions β NYC",
};
const place = (r.target && r.target.nta_name)
|| (r.geocode && r.geocode.address)
|| r.place || "β";
// Build the citation index from the briefing prose so we render a
// numbered Sources section in the SAME order the chips appear in the
// text β same idiom as the agent UI.
const citeIndex = {};
const para = r.paragraph || "";
const para2 = para.replace(/\[([a-z0-9_]+)\]/gi, (_, id) => {
const norm = id.toLowerCase();
if (citeIndex[norm] == null) citeIndex[norm] = Object.keys(citeIndex).length + 1;
return `<span class="cite">${citeIndex[norm]}</span>`;
});
const html = `
<header class="r-head">
<div class="r-brand">Riprap</div>
<div class="r-tagline">Citation-grounded flood-exposure briefing</div>
<dl class="r-meta-grid">
<dt>Subject</dt><dd>${escapeHtml(intentTitleMap[intent] || "Briefing")} Β· <strong>${escapeHtml(place)}</strong></dd>
${r.geocode && r.geocode.borough ? `<dt>Borough</dt><dd>${escapeHtml(r.geocode.borough)}</dd>` : ""}
${r.target && r.target.borough ? `<dt>Borough</dt><dd>${escapeHtml(r.target.borough)}</dd>` : ""}
${r.geocode && r.geocode.bbl ? `<dt>BBL</dt><dd class="mono">${escapeHtml(r.geocode.bbl)}</dd>` : ""}
${r.target && r.target.nta_code ? `<dt>NTA</dt><dd class="mono">${escapeHtml(r.target.nta_code)}</dd>` : ""}
<dt>Generated</dt><dd>${escapeHtml(pkg.finishedAt || new Date().toISOString())}</dd>
<dt>Total runtime</dt><dd>${pkg.wallSeconds ?? r.total_s ?? "β"} s</dd>
</dl>
</header>
<section class="r-section">
<h2>1 Β· Original query</h2>
<div class="r-query">"${escapeHtml(pkg.query)}"</div>
</section>
<section class="r-section">
<h2>2 Β· Agent routing decision</h2>
<dl class="r-plan">
<dt>Intent</dt><dd class="mono">${escapeHtml(plan.intent || intent)}</dd>
<dt>Targets</dt><dd class="mono">${escapeHtml((plan.targets || []).map(t => `${t.type}:${t.text}`).join(", ") || "β")}</dd>
<dt>Specialists requested</dt><dd class="mono">${escapeHtml((plan.specialists || []).join(", ") || "β")}</dd>
${plan.rationale ? `<dd class="r-plan-rationale">"${escapeHtml(plan.rationale)}"</dd>` : ""}
</dl>
</section>
<section class="r-section">
<h2>3 Β· Specialist trail</h2>
<div class="lead">${trace.length} specialists invoked. Each row shows the
step name, status, elapsed time, and the structured result the step
produced. Sources of any data referenced in the briefing appear in
Section 6.</div>
<table class="r-trace">
<thead>
<tr><th>#</th><th>Step</th><th>Status</th><th>Elapsed</th><th>Result / error</th></tr>
</thead>
<tbody>
${trace.map((s, i) => {
const ok = s.ok === true;
const fail = s.ok === false;
const cls = ok ? "ok" : fail ? "err" : "";
const mark = ok ? "β" : fail ? "β" : "β";
const [label] = stepLabels[s.step] || [s.step, ""];
const detail = s.err
? `<span class="err-msg">${escapeHtml(s.err)}</span>`
: `<span class="result">${escapeHtml(JSON.stringify(s.result ?? {}))}</span>`;
return `<tr class="${cls}">
<td class="mono">${i + 1}</td>
<td><strong>${escapeHtml(label)}</strong><br>
<span class="mono" style="color:#888;font-size:7.5pt">${escapeHtml(s.step)}</span></td>
<td><span class="mark">${mark}</span></td>
<td class="mono">${s.elapsed_s != null ? s.elapsed_s + "s" : "β"}</td>
<td>${detail}</td>
</tr>`;
}).join("")}
</tbody>
</table>
</section>
${pkg.mapPng ? `
<section class="r-section">
<h2>4 Β· Map (snapshot)</h2>
<div class="r-map">
<img src="${pkg.mapPng}" alt="Map snapshot at report-generation time">
<div class="legend-cap">Snapshot of the live MapLibre map captured at report-generation time. Layers: per-intent (Sandy 2012 / DEP scenarios / NTA boundary / DOB permit pins / address pin).</div>
</div>
</section>
` : `
<section class="r-section">
<h2>4 Β· Map</h2>
<div class="r-map no-map">No map snapshot was captured (the map may have been hidden or empty for this query type).</div>
</section>
`}
<section class="r-section">
<h2>5 Β· Cited briefing</h2>
<div class="r-briefing">${renderBriefingMarkdown(para2)}</div>
</section>
<section class="r-section">
<h2>6 Β· Sources</h2>
<ol class="r-sources">
${Object.entries(citeIndex).sort((a, b) => a[1] - b[1]).map(([id, n]) => {
const url = urls[id];
return `<li>
<span class="num">[${n}]</span>
<div>
<span class="label">${escapeHtml(labels[id] || id)}</span>
${vintages[id] ? `<span class="vintage">Vintage: ${escapeHtml(vintages[id])}</span>` : ""}
${url ? `<span class="url"><a href="${escapeHtml(url)}">${escapeHtml(url)}</a></span>` : ""}
<span class="vintage" style="font-family:var(--mono);font-size:8pt;color:#888">doc_id: ${escapeHtml(id)}</span>
</div>
</li>`;
}).join("")}
</ol>
</section>
<section class="r-section">
<h2>7 Β· Methodology & honest scope</h2>
<div class="r-method">
<p><strong>This is an exposure briefing, not a damage probability or insurance rating.</strong> Tier and headline statistics are computed from a deterministic, peer-reviewed-grounded rubric (see <em>METHODOLOGY.md</em> in the source repository). The synthesis prose is generated by IBM Granite 4.1 in document-grounded mode; every numeric claim is verified to appear verbatim in a source document before render, and unsupported sentences are dropped.</p>
<p><strong>Stack:</strong> Granite 4.1 (3b planner / 8b reconciler) via Ollama, Granite Embedding 278M for RAG over agency reports, Granite TimeSeries TTM r2 for live surge nowcast, Prithvi-EO 2.0 for satellite-derived flood polygons (offline pre-computed). Apache-2.0 across the stack. Inference runs locally on the deploying machine; no vendor LLM is contacted at runtime.</p>
<p><strong>Out of scope:</strong> engineering vulnerability (foundation/structural fragility), social capacity, financial absorption, sub-surface flooding (basement apartments, subway entrances). Datasets are vintage-bounded as noted per source above.</p>
</div>
</section>
<footer class="r-foot">
<span>Generated by Riprap Β· https://huggingface.co/spaces/msradam/riprap-nyc</span>
<span>${escapeHtml(pkg.finishedAt || "")}</span>
</footer>
`;
document.getElementById("paper").innerHTML = html;
// Update tab title to reflect the subject
document.title = `Riprap β ${place}`;
}
// Subset markdown for the briefing: `**Header.**` lines β <h4>; `- ` lines
// β <ul><li>; inline `**foo**` β <strong>; rest β <p>. Keep parity with
// agent.js's renderMarkdown so reports look like the live UI.
function renderBriefingMarkdown(text) {
const lines = text.split("\n");
const out = [];
let para = []; let bullets = [];
const flushPara = () => {
if (!para.length) return;
const safe = para.join(" ").trim().replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
if (safe) out.push(`<p>${safe}</p>`);
para = [];
};
const flushBullets = () => {
if (!bullets.length) return;
const items = bullets.map(b => {
const safe = b.trim().replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
return `<li>${safe}</li>`;
}).join("");
out.push(`<ul>${items}</ul>`);
bullets = [];
};
// Pre-split inline-bullet runs that Granite occasionally emits as one line
const expanded = [];
for (const line of lines) {
if (line.trim().startsWith("- ") && line.includes(" - ", 2)) {
const parts = line.split(/(?:^|(?<=\.\s))\s*-\s+/g).filter(p => p.trim());
for (const p of parts) expanded.push("- " + p.trim());
} else { expanded.push(line); }
}
for (const line of expanded) {
const m = line.match(/^\s*\*\*([A-Z][A-Za-z\s/]+)\.\*\*\s*$/);
if (m) {
flushPara(); flushBullets();
out.push(`<h4>${m[1]}</h4>`);
} else if (/^\s*[-*]\s+/.test(line)) {
flushPara();
bullets.push(line.replace(/^\s*[-*]\s+/, ""));
} else {
flushBullets();
para.push(line);
}
}
flushPara(); flushBullets();
return out.join("");
}
|