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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

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 &amp; 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("");
}