Frontend overhaul: Lit kickoff → Svelte 5 custom elements → SvelteKit design-system
Browse filesTelescoped from ~25 spine commits into the three UI generations:
Lit (kept on disk for reference, not loaded):
web/static/components/{briefing,trace,signals,sources-footer}.js
Component-per-tag custom elements with reactive props.
Svelte 5 custom-element bundle (legacy primary, served at /legacy):
web/svelte/src/lib/{Briefing,Trace,SourcesFooter}.svelte + main.js
Shared store for cross-component highlight (citeIndex,
highlightedDocId). Built artifact ships at web/static/dist/riprap.js.
SvelteKit design-system v0.4.2 (the new primary):
web/sveltekit/src/* — adapter-static build, IBM Plex, four-tier
glyphs, MapLibre map with custom basemap, server-side render of
/q/sample + /q/<query>. Built artifact ships at web/sveltekit/build/.
web/main.py wired up the new endpoint surface:
/, /q/sample, /q/<query> → SvelteKit build
/legacy, /single, /compare → Svelte custom-element bundle
/api/agent/stream → SSE pipeline (plan/step/token/mellea_attempt
/final/error/done events)
/api/backend → live LLM-backend descriptor for the pill
/api/layers/* → MapLibre tile/polygon endpoints
/api/floodnet_near, /api/register/{class}, /print/{query_id}, etc.
HF Spaces ships pre-built artefacts (no Node at deploy time); rebuild
locally with cd web/sveltekit && npm run build, or cd web/svelte &&
npm run build for the legacy bundle.
- web/main.py +382 -6
- web/static/agent.html +523 -0
- web/static/agent.js +1391 -0
- web/static/app.js +270 -23
- web/static/components/briefing.js +133 -0
- web/static/components/signals.js +21 -0
- web/static/components/sources-footer.js +144 -0
- web/static/components/trace.js +87 -0
- web/static/dist/riprap.js +0 -0
- web/static/dist/riprap.js.map +0 -0
- web/static/index.html +1 -1
- web/static/report.html +244 -0
- web/static/report.js +218 -0
- web/static/style.css +50 -3
- web/svelte/package-lock.json +1337 -0
- web/svelte/package.json +15 -0
- web/svelte/src/lib/Briefing.svelte +124 -0
- web/svelte/src/lib/SourcesFooter.svelte +105 -0
- web/svelte/src/lib/Trace.svelte +109 -0
- web/svelte/src/lib/stores.js +10 -0
- web/svelte/src/main.js +11 -0
- web/svelte/vite.config.js +33 -0
- web/sveltekit/.gitignore +11 -0
- web/sveltekit/.npmrc +1 -0
- web/sveltekit/build/200.html +43 -0
- web/sveltekit/build/_app/env.js +1 -0
- web/sveltekit/build/_app/immutable/assets/0.KpTzaSsX.css +0 -0
- web/sveltekit/build/_app/immutable/assets/3.BZfqQRM0.css +1 -0
- web/sveltekit/build/_app/immutable/assets/4.CPUwsEjs.css +1 -0
- web/sveltekit/build/_app/immutable/assets/Briefing.Cg0TTl7h.css +1 -0
- web/sveltekit/build/_app/immutable/assets/MapLegend.DvDgr167.css +1 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-cyrillic-400-normal.BSMlKf0J.woff2 +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-cyrillic-400-normal.CEL4l2ZJ.woff +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-cyrillic-500-normal.Ael50iVv.woff +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-cyrillic-500-normal.Bq9vWWag.woff2 +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-cyrillic-600-normal.CTOM6hUh.woff2 +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-cyrillic-600-normal.fLZuRloM.woff +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-cyrillic-ext-400-normal.DMdlQ8Kv.woff +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-cyrillic-ext-400-normal.xuaO2J-f.woff2 +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-cyrillic-ext-500-normal.BIfNGwUT.woff +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-cyrillic-ext-500-normal.BqneJy0T.woff2 +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-cyrillic-ext-600-normal.9HEixskS.woff +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-cyrillic-ext-600-normal.V-xxqcpd.woff2 +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-latin-400-normal.CvHOgSBP.woff +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-latin-400-normal.DMJ8VG8y.woff2 +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-latin-500-normal.CB9ihrfo.woff +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-latin-500-normal.DSY6xOcd.woff2 +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-latin-600-normal.BgSNZQsw.woff2 +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-latin-600-normal.DWFSQ4vo.woff +0 -0
- web/sveltekit/build/_app/immutable/assets/ibm-plex-mono-latin-ext-400-normal.BmRBH3aV.woff2 +0 -0
|
@@ -1,10 +1,11 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
Run: uvicorn web.main:app --reload --port 8000
|
| 4 |
"""
|
| 5 |
from __future__ import annotations
|
| 6 |
|
| 7 |
import json
|
|
|
|
| 8 |
import warnings
|
| 9 |
from pathlib import Path
|
| 10 |
|
|
@@ -20,14 +21,21 @@ from app.fsm import iter_steps # noqa: E402
|
|
| 20 |
|
| 21 |
ROOT = Path(__file__).resolve().parent
|
| 22 |
STATIC = ROOT / "static"
|
|
|
|
| 23 |
|
| 24 |
app = FastAPI(title="Riprap")
|
| 25 |
app.mount("/static", StaticFiles(directory=STATIC), name="static")
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
import json as _json # noqa: E402
|
| 28 |
|
| 29 |
import geopandas as _gpd # noqa: E402
|
| 30 |
-
from fastapi.responses import JSONResponse
|
| 31 |
|
| 32 |
_LAYER_CACHE: dict = {}
|
| 33 |
|
|
@@ -74,13 +82,221 @@ def _warm_caches():
|
|
| 74 |
dep_stormwater.load(scen)
|
| 75 |
print("[startup] flood layers ready", flush=True)
|
| 76 |
print("[startup] warming RAG (Granite Embedding 278M + 5 PDFs)...", flush=True)
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
|
| 82 |
@app.get("/")
|
| 83 |
def index():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
return FileResponse(STATIC / "index.html")
|
| 85 |
|
| 86 |
|
|
@@ -89,6 +305,18 @@ def compare_page():
|
|
| 89 |
return FileResponse(STATIC / "compare.html")
|
| 90 |
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
@app.get("/register/{asset_class}")
|
| 93 |
def register_page(asset_class: str):
|
| 94 |
if asset_class not in ("schools", "nycha", "mta_entrances"):
|
|
@@ -121,6 +349,7 @@ async def compare_stream(a: str, b: str, request: Request):
|
|
| 121 |
route updates to the correct panel."""
|
| 122 |
import asyncio
|
| 123 |
import queue
|
|
|
|
| 124 |
from app.fsm import iter_steps
|
| 125 |
|
| 126 |
def gen_for_side(side: str, q_text: str, out_q):
|
|
@@ -132,7 +361,7 @@ async def compare_stream(a: str, b: str, request: Request):
|
|
| 132 |
out_q.put({"side": side, "kind": "error", "err": str(e)})
|
| 133 |
out_q.put({"side": side, "kind": "_done"})
|
| 134 |
|
| 135 |
-
out_q:
|
| 136 |
|
| 137 |
def kick():
|
| 138 |
# run both sides in parallel threads — each Burr Application owns
|
|
@@ -187,6 +416,153 @@ async def stream(q: str, request: Request):
|
|
| 187 |
"X-Accel-Buffering": "no"})
|
| 188 |
|
| 189 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
@app.get("/api/layers/sandy")
|
| 191 |
def layer_sandy(lat: float, lon: float, r: float = 1500):
|
| 192 |
key = ("sandy", round(lat, 4), round(lon, 4), int(r))
|
|
|
|
| 1 |
+
"""Riprap web UI — FastAPI + SSE streaming of the Burr FSM trace.
|
| 2 |
|
| 3 |
Run: uvicorn web.main:app --reload --port 8000
|
| 4 |
"""
|
| 5 |
from __future__ import annotations
|
| 6 |
|
| 7 |
import json
|
| 8 |
+
import os
|
| 9 |
import warnings
|
| 10 |
from pathlib import Path
|
| 11 |
|
|
|
|
| 21 |
|
| 22 |
ROOT = Path(__file__).resolve().parent
|
| 23 |
STATIC = ROOT / "static"
|
| 24 |
+
SVELTEKIT_BUILD = ROOT / "sveltekit" / "build"
|
| 25 |
|
| 26 |
app = FastAPI(title="Riprap")
|
| 27 |
app.mount("/static", StaticFiles(directory=STATIC), name="static")
|
| 28 |
|
| 29 |
+
# SvelteKit static build (adapter-static). Serves the new design-system UI
|
| 30 |
+
# from /, /q/sample, /q/<query>. The legacy custom-element pages remain at
|
| 31 |
+
# /legacy, /single, /compare, /register/* for as long as they're useful.
|
| 32 |
+
if SVELTEKIT_BUILD.exists():
|
| 33 |
+
app.mount("/_app", StaticFiles(directory=SVELTEKIT_BUILD / "_app"), name="sveltekit_assets")
|
| 34 |
+
|
| 35 |
import json as _json # noqa: E402
|
| 36 |
|
| 37 |
import geopandas as _gpd # noqa: E402
|
| 38 |
+
from fastapi.responses import JSONResponse # noqa: E402
|
| 39 |
|
| 40 |
_LAYER_CACHE: dict = {}
|
| 41 |
|
|
|
|
| 82 |
dep_stormwater.load(scen)
|
| 83 |
print("[startup] flood layers ready", flush=True)
|
| 84 |
print("[startup] warming RAG (Granite Embedding 278M + 5 PDFs)...", flush=True)
|
| 85 |
+
# RAG warm loads sentence-transformers, which on some HF Space rebuilds
|
| 86 |
+
# has hit transformers-lazy-import edge cases (CodeCarbonCallback). The
|
| 87 |
+
# Space *must* start even if RAG fails — the FSM still works without
|
| 88 |
+
# RAG citations (specialists deliver their own grounded data, and the
|
| 89 |
+
# rag step in fsm.py already handles `rag=[]` gracefully). Surface the
|
| 90 |
+
# failure loudly in logs but don't kill the app.
|
| 91 |
+
try:
|
| 92 |
+
from app import rag
|
| 93 |
+
rag.warm()
|
| 94 |
+
print("[startup] RAG ready", flush=True)
|
| 95 |
+
except Exception as e: # noqa: BLE001
|
| 96 |
+
print(f"[startup] RAG warm FAILED — continuing without RAG: "
|
| 97 |
+
f"{type(e).__name__}: {e}", flush=True)
|
| 98 |
+
import traceback
|
| 99 |
+
traceback.print_exc()
|
| 100 |
+
# Pre-import the heavy EO/ML stacks on the main thread so the
|
| 101 |
+
# parallel-fanout workers don't race each other on first
|
| 102 |
+
# import (sklearn's "partially initialized module" surfaces as a
|
| 103 |
+
# spurious ImportError when terratorch / tsfm_public both pull
|
| 104 |
+
# sklearn concurrently from worker threads).
|
| 105 |
+
# Warm the Ollama LLM models so the first user query doesn't pay a
|
| 106 |
+
# cold-load penalty (~70 s for the 3B planner, ~12 s for the 8B
|
| 107 |
+
# reconciler at Q4_K_M). Sets keep_alive to 24 h so they stay
|
| 108 |
+
# resident across queries. Both calls use num_ctx that matches the
|
| 109 |
+
# production call sites (Mellea's 4096), so Ollama's KV cache is
|
| 110 |
+
# pre-allocated at the right size and the first reconcile doesn't
|
| 111 |
+
# pay an extra grow-and-reinit cost.
|
| 112 |
+
if os.environ.get("RIPRAP_SKIP_LLM_WARM", "").lower() not in ("1", "true", "yes"):
|
| 113 |
+
print("[startup] warming Ollama models (granite4.1:3b + 8b)...",
|
| 114 |
+
flush=True)
|
| 115 |
+
try:
|
| 116 |
+
import httpx as _httpx
|
| 117 |
+
base = os.environ.get(
|
| 118 |
+
"OLLAMA_BASE_URL",
|
| 119 |
+
os.environ.get("OLLAMA_HOST", "http://localhost:11434"),
|
| 120 |
+
)
|
| 121 |
+
if not base.startswith("http"):
|
| 122 |
+
base = "http://" + base
|
| 123 |
+
keep_alive = os.environ.get("OLLAMA_KEEP_ALIVE", "24h")
|
| 124 |
+
num_ctx = int(os.environ.get("RIPRAP_MELLEA_NUM_CTX", "4096"))
|
| 125 |
+
for tag in (os.environ.get("RIPRAP_OLLAMA_3B_TAG", "granite4.1:3b"),
|
| 126 |
+
os.environ.get("RIPRAP_OLLAMA_8B_TAG", "granite4.1:8b")):
|
| 127 |
+
try:
|
| 128 |
+
r = _httpx.post(
|
| 129 |
+
base.rstrip("/") + "/api/generate",
|
| 130 |
+
json={
|
| 131 |
+
"model": tag,
|
| 132 |
+
"prompt": "hi",
|
| 133 |
+
"stream": False,
|
| 134 |
+
"keep_alive": keep_alive,
|
| 135 |
+
"options": {"num_ctx": num_ctx, "num_predict": 1},
|
| 136 |
+
},
|
| 137 |
+
timeout=180,
|
| 138 |
+
)
|
| 139 |
+
if r.status_code == 200:
|
| 140 |
+
load_s = r.json().get("load_duration", 0) / 1e9
|
| 141 |
+
print(f"[startup] {tag} loaded "
|
| 142 |
+
f"(load_duration={load_s:.1f}s, "
|
| 143 |
+
f"keep_alive={keep_alive}, num_ctx={num_ctx})",
|
| 144 |
+
flush=True)
|
| 145 |
+
else:
|
| 146 |
+
print(f"[startup] {tag} warm failed "
|
| 147 |
+
f"({r.status_code})", flush=True)
|
| 148 |
+
except Exception as warm_err:
|
| 149 |
+
print(f"[startup] {tag} warm failed: {warm_err}",
|
| 150 |
+
flush=True)
|
| 151 |
+
except Exception as e:
|
| 152 |
+
print(f"[startup] LLM warm skipped: {e}", flush=True)
|
| 153 |
+
print("[startup] pre-importing terratorch + tsfm_public...", flush=True)
|
| 154 |
+
try:
|
| 155 |
+
import sklearn # noqa: F401 prime sklearn first
|
| 156 |
+
import terratorch # noqa: F401
|
| 157 |
+
import tsfm_public # noqa: F401
|
| 158 |
+
except Exception as e:
|
| 159 |
+
print(f"[startup] heavy-EO pre-import skipped: {e}", flush=True)
|
| 160 |
+
# Warm the TerraMind specialist so first per-query call is just
|
| 161 |
+
# the diffusion (~3 s), not model load (~30 s). No-ops if deps
|
| 162 |
+
# are missing on this deployment.
|
| 163 |
+
try:
|
| 164 |
+
from app.context import terramind_synthesis
|
| 165 |
+
terramind_synthesis.warm()
|
| 166 |
+
print("[startup] TerraMind ready", flush=True)
|
| 167 |
+
except Exception as e:
|
| 168 |
+
print(f"[startup] TerraMind warm skipped: {e}", flush=True)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
@app.get("/api/debug/eo")
|
| 172 |
+
def api_debug_eo():
|
| 173 |
+
"""Diagnostic for the EO toolchain (Phase 1 + Phase 4) on HF Spaces.
|
| 174 |
+
|
| 175 |
+
Surfaces sys.path, PYTHONPATH, and per-module import status so we
|
| 176 |
+
can tell whether terratorch is actually findable from inside the
|
| 177 |
+
uvicorn process. Used to debug why the runtime --target install
|
| 178 |
+
appears to succeed in the entrypoint but isn't visible to the
|
| 179 |
+
FSM specialists at request time.
|
| 180 |
+
"""
|
| 181 |
+
import os
|
| 182 |
+
import sys
|
| 183 |
+
import traceback
|
| 184 |
+
from pathlib import Path
|
| 185 |
+
|
| 186 |
+
out = {
|
| 187 |
+
"python_executable": sys.executable,
|
| 188 |
+
"python_version": sys.version,
|
| 189 |
+
"PYTHONPATH": os.environ.get("PYTHONPATH"),
|
| 190 |
+
"PYTHONNOUSERSITE": os.environ.get("PYTHONNOUSERSITE"),
|
| 191 |
+
"HOME": os.environ.get("HOME"),
|
| 192 |
+
"sys.path": sys.path,
|
| 193 |
+
}
|
| 194 |
+
eo_dir = Path(os.environ.get("HOME", "/home/user")) / ".eo-pkgs"
|
| 195 |
+
out["eo_dir"] = str(eo_dir)
|
| 196 |
+
out["eo_dir_exists"] = eo_dir.exists()
|
| 197 |
+
if eo_dir.exists():
|
| 198 |
+
out["eo_dir_contents"] = sorted(p.name for p in eo_dir.iterdir())[:50]
|
| 199 |
+
out["modules"] = {}
|
| 200 |
+
for name in ("terratorch", "einops", "diffusers", "timm",
|
| 201 |
+
"rasterio", "planetary_computer", "pystac_client"):
|
| 202 |
+
try:
|
| 203 |
+
mod = __import__(name)
|
| 204 |
+
out["modules"][name] = {"ok": True,
|
| 205 |
+
"file": getattr(mod, "__file__", "?")}
|
| 206 |
+
except Exception as e:
|
| 207 |
+
out["modules"][name] = {"ok": False,
|
| 208 |
+
"err": f"{type(e).__name__}: {e}",
|
| 209 |
+
"tb": traceback.format_exc().splitlines()[-3:]}
|
| 210 |
+
return JSONResponse(out)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
@app.get("/api/backend")
|
| 214 |
+
async def api_backend():
|
| 215 |
+
"""Live LLM-backend descriptor for the UI's hardware badge.
|
| 216 |
+
|
| 217 |
+
Returns the configured primary (vLLM/AMD or Ollama/local), plus a
|
| 218 |
+
quick reachability ping so the badge can show whether the primary is
|
| 219 |
+
actually answering or whether the Router is on the fallback path.
|
| 220 |
+
"""
|
| 221 |
+
import httpx
|
| 222 |
+
|
| 223 |
+
from app import llm
|
| 224 |
+
info = llm.backend_info()
|
| 225 |
+
reachable = None
|
| 226 |
+
try:
|
| 227 |
+
if info["primary"] in ("vllm", "mlx") and info["vllm_base_url"]:
|
| 228 |
+
url = info["vllm_base_url"].rstrip("/") + "/models"
|
| 229 |
+
async with httpx.AsyncClient(timeout=2.5) as client:
|
| 230 |
+
r = await client.get(url, headers={"Authorization": "Bearer ping"})
|
| 231 |
+
# vLLM and mlx_lm.server both return 200 on /v1/models when
|
| 232 |
+
# reachable; vLLM may return 401 with --api-key set. Either
|
| 233 |
+
# proves the server is up. Anything else = unreachable.
|
| 234 |
+
reachable = r.status_code in (200, 401)
|
| 235 |
+
else:
|
| 236 |
+
url = info["ollama_base_url"].rstrip("/") + "/api/tags"
|
| 237 |
+
async with httpx.AsyncClient(timeout=2.5) as client:
|
| 238 |
+
r = await client.get(url)
|
| 239 |
+
reachable = r.status_code == 200
|
| 240 |
+
except Exception:
|
| 241 |
+
reachable = False
|
| 242 |
+
info["reachable"] = reachable
|
| 243 |
+
info["effective_engine"] = (
|
| 244 |
+
info["engine"] if reachable
|
| 245 |
+
else (info.get("fallback_engine") or "offline")
|
| 246 |
+
)
|
| 247 |
+
return JSONResponse(info)
|
| 248 |
|
| 249 |
|
| 250 |
@app.get("/")
|
| 251 |
def index():
|
| 252 |
+
"""SvelteKit cold-start page (the new design-system UI). Falls back to
|
| 253 |
+
the legacy custom-element agent.html if the SvelteKit build hasn't been
|
| 254 |
+
compiled yet — that lets `uvicorn` boot in a fresh checkout without a
|
| 255 |
+
Node toolchain present."""
|
| 256 |
+
sk = SVELTEKIT_BUILD / "index.html"
|
| 257 |
+
if sk.exists():
|
| 258 |
+
return FileResponse(sk)
|
| 259 |
+
return FileResponse(STATIC / "agent.html")
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
@app.get("/q/sample")
|
| 263 |
+
def q_sample_page():
|
| 264 |
+
"""The prerendered Red Hook demo briefing (no SSE)."""
|
| 265 |
+
sk = SVELTEKIT_BUILD / "q" / "sample.html"
|
| 266 |
+
if sk.exists():
|
| 267 |
+
return FileResponse(sk)
|
| 268 |
+
return JSONResponse({"error": "sveltekit build not present"}, status_code=503)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
@app.get("/q/{query_id}")
|
| 272 |
+
def q_query_page(query_id: str): # noqa: ARG001 — captured for the SPA router
|
| 273 |
+
"""Live briefing route. Served by the SvelteKit SPA fallback (200.html);
|
| 274 |
+
the client opens an EventSource to /api/agent/stream."""
|
| 275 |
+
sk = SVELTEKIT_BUILD / "200.html"
|
| 276 |
+
if sk.exists():
|
| 277 |
+
return FileResponse(sk)
|
| 278 |
+
return JSONResponse({"error": "sveltekit build not present"}, status_code=503)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
@app.get("/print/{query_id}")
|
| 282 |
+
def print_page(query_id: str): # noqa: ARG001 — captured by the SPA router
|
| 283 |
+
"""Curated print artifact for a completed briefing. The client
|
| 284 |
+
hydrates from localStorage (key riprap:print:<query_id>) and
|
| 285 |
+
auto-fires window.print() — no backend round-trip."""
|
| 286 |
+
sk = SVELTEKIT_BUILD / "200.html"
|
| 287 |
+
if sk.exists():
|
| 288 |
+
return FileResponse(sk)
|
| 289 |
+
return JSONResponse({"error": "sveltekit build not present"}, status_code=503)
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
@app.get("/legacy")
|
| 293 |
+
def legacy_index():
|
| 294 |
+
"""Original custom-element agent page, preserved for fallback / debugging."""
|
| 295 |
+
return FileResponse(STATIC / "agent.html")
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
@app.get("/single")
|
| 299 |
+
def single_address_page():
|
| 300 |
return FileResponse(STATIC / "index.html")
|
| 301 |
|
| 302 |
|
|
|
|
| 305 |
return FileResponse(STATIC / "compare.html")
|
| 306 |
|
| 307 |
|
| 308 |
+
@app.get("/agent")
|
| 309 |
+
def agent_page():
|
| 310 |
+
return FileResponse(STATIC / "agent.html")
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
@app.get("/report")
|
| 314 |
+
def report_page():
|
| 315 |
+
"""Print-ready auditable report. Reads the prior agent run from
|
| 316 |
+
the browser's sessionStorage; fully client-side render."""
|
| 317 |
+
return FileResponse(STATIC / "report.html")
|
| 318 |
+
|
| 319 |
+
|
| 320 |
@app.get("/register/{asset_class}")
|
| 321 |
def register_page(asset_class: str):
|
| 322 |
if asset_class not in ("schools", "nycha", "mta_entrances"):
|
|
|
|
| 349 |
route updates to the correct panel."""
|
| 350 |
import asyncio
|
| 351 |
import queue
|
| 352 |
+
|
| 353 |
from app.fsm import iter_steps
|
| 354 |
|
| 355 |
def gen_for_side(side: str, q_text: str, out_q):
|
|
|
|
| 361 |
out_q.put({"side": side, "kind": "error", "err": str(e)})
|
| 362 |
out_q.put({"side": side, "kind": "_done"})
|
| 363 |
|
| 364 |
+
out_q: queue.Queue[dict] = queue.Queue()
|
| 365 |
|
| 366 |
def kick():
|
| 367 |
# run both sides in parallel threads — each Burr Application owns
|
|
|
|
| 416 |
"X-Accel-Buffering": "no"})
|
| 417 |
|
| 418 |
|
| 419 |
+
@app.get("/api/agent")
|
| 420 |
+
def api_agent(q: str):
|
| 421 |
+
"""Agentic endpoint: take a natural-language query, plan it via
|
| 422 |
+
Granite 4.1, dispatch to the appropriate intent module, return the
|
| 423 |
+
full result as JSON. The Plan is included so callers can see the
|
| 424 |
+
agent's routing decision.
|
| 425 |
+
|
| 426 |
+
All non-trivial reconciliation (single_address / neighborhood /
|
| 427 |
+
development_check) routes through Mellea-validated rejection
|
| 428 |
+
sampling against four grounding requirements. live_now stays on
|
| 429 |
+
streaming reconcile because outputs are short and the live signals
|
| 430 |
+
have low hallucination surface."""
|
| 431 |
+
from app.intents import development_check as i_dev
|
| 432 |
+
from app.intents import live_now as i_live
|
| 433 |
+
from app.intents import neighborhood as i_nbhd
|
| 434 |
+
from app.intents import single_address as i_addr
|
| 435 |
+
from app.planner import plan as run_planner
|
| 436 |
+
p = run_planner(q)
|
| 437 |
+
if p.intent == "development_check":
|
| 438 |
+
out = i_dev.run(p, q, strict=True)
|
| 439 |
+
elif p.intent == "neighborhood":
|
| 440 |
+
out = i_nbhd.run(p, q, strict=True)
|
| 441 |
+
elif p.intent == "live_now":
|
| 442 |
+
out = i_live.run(p, q)
|
| 443 |
+
else:
|
| 444 |
+
out = i_addr.run(p, q, strict=True)
|
| 445 |
+
return JSONResponse(out)
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
@app.get("/api/agent/stream")
|
| 449 |
+
async def api_agent_stream(q: str):
|
| 450 |
+
"""SSE: emit `plan` once the planner finishes, then a `step` event per
|
| 451 |
+
finalized specialist, then `final` with the full result. The intent
|
| 452 |
+
runs in a thread; we marshal events through a queue."""
|
| 453 |
+
import asyncio
|
| 454 |
+
import queue
|
| 455 |
+
out_q: queue.Queue[dict] = queue.Queue()
|
| 456 |
+
|
| 457 |
+
def runner():
|
| 458 |
+
try:
|
| 459 |
+
from app.intents import development_check as i_dev
|
| 460 |
+
from app.intents import live_now as i_live
|
| 461 |
+
from app.intents import neighborhood as i_nbhd
|
| 462 |
+
from app.intents import single_address as i_addr
|
| 463 |
+
from app.planner import plan as run_planner
|
| 464 |
+
|
| 465 |
+
def _on_plan_token(delta: str):
|
| 466 |
+
out_q.put({"kind": "plan_token", "delta": delta})
|
| 467 |
+
p = run_planner(q, on_token=_on_plan_token)
|
| 468 |
+
out_q.put({"kind": "plan",
|
| 469 |
+
"intent": p.intent,
|
| 470 |
+
"targets": p.targets,
|
| 471 |
+
"specialists": p.specialists,
|
| 472 |
+
"rationale": p.rationale})
|
| 473 |
+
if p.intent == "development_check":
|
| 474 |
+
final = i_dev.run(p, q, progress_q=out_q, strict=True)
|
| 475 |
+
elif p.intent == "neighborhood":
|
| 476 |
+
final = i_nbhd.run(p, q, progress_q=out_q, strict=True)
|
| 477 |
+
elif p.intent == "live_now":
|
| 478 |
+
final = i_live.run(p, q, progress_q=out_q)
|
| 479 |
+
else:
|
| 480 |
+
final = i_addr.run(p, q, progress_q=out_q, strict=True)
|
| 481 |
+
out_q.put({"kind": "final", **final})
|
| 482 |
+
except Exception as e:
|
| 483 |
+
out_q.put({"kind": "error", "err": str(e)})
|
| 484 |
+
finally:
|
| 485 |
+
out_q.put({"kind": "_done"})
|
| 486 |
+
|
| 487 |
+
async def event_stream():
|
| 488 |
+
loop = asyncio.get_event_loop()
|
| 489 |
+
loop.run_in_executor(None, runner)
|
| 490 |
+
yield f"event: hello\ndata: {json.dumps({'query': q})}\n\n"
|
| 491 |
+
while True:
|
| 492 |
+
try:
|
| 493 |
+
ev = await asyncio.to_thread(out_q.get, True, 1.0)
|
| 494 |
+
except Exception:
|
| 495 |
+
continue
|
| 496 |
+
kind = ev.get("kind")
|
| 497 |
+
if kind == "_done":
|
| 498 |
+
break
|
| 499 |
+
yield f"event: {kind}\ndata: {json.dumps(ev, default=str)}\n\n"
|
| 500 |
+
yield "event: done\ndata: {}\n\n"
|
| 501 |
+
|
| 502 |
+
return StreamingResponse(event_stream(), media_type="text/event-stream",
|
| 503 |
+
headers={"Cache-Control": "no-cache",
|
| 504 |
+
"X-Accel-Buffering": "no"})
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
@app.get("/api/agent/plan")
|
| 508 |
+
def api_agent_plan(q: str):
|
| 509 |
+
"""Just the plan, no execution. Useful for showing the agent's routing
|
| 510 |
+
decision before running specialists."""
|
| 511 |
+
from app.planner import plan as run_planner
|
| 512 |
+
p = run_planner(q)
|
| 513 |
+
return JSONResponse({
|
| 514 |
+
"intent": p.intent,
|
| 515 |
+
"targets": p.targets,
|
| 516 |
+
"specialists": p.specialists,
|
| 517 |
+
"rationale": p.rationale,
|
| 518 |
+
})
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
@app.get("/api/layers/nta")
|
| 522 |
+
def layer_nta(code: str):
|
| 523 |
+
"""Return the NTA polygon for a given NTA code as GeoJSON (EPSG:4326)."""
|
| 524 |
+
from app.areas import nta as nta_mod
|
| 525 |
+
g = nta_mod.load()
|
| 526 |
+
sub = g[g["nta2020"] == code][["nta2020", "ntaname", "boroname", "geometry"]]
|
| 527 |
+
if sub.empty:
|
| 528 |
+
return JSONResponse({"type": "FeatureCollection", "features": []}, status_code=404)
|
| 529 |
+
return JSONResponse(_json.loads(sub.to_json()),
|
| 530 |
+
headers={"Cache-Control": "public, max-age=3600"})
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
@app.get("/api/layers/sandy_clipped")
|
| 534 |
+
def layer_sandy_clipped(code: str):
|
| 535 |
+
"""Sandy inundation polygons clipped to an NTA bbox + simplified.
|
| 536 |
+
Used by the agent map for neighborhood / development_check intents."""
|
| 537 |
+
from app.areas import nta as nta_mod
|
| 538 |
+
from app.flood_layers import sandy_inundation
|
| 539 |
+
poly = nta_mod.polygon_for(code)
|
| 540 |
+
if poly is None:
|
| 541 |
+
return JSONResponse({"type": "FeatureCollection", "features": []})
|
| 542 |
+
bounds = poly.bounds
|
| 543 |
+
cx, cy = (bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2
|
| 544 |
+
# bbox half-extent in metres ~ half the polygon span × 111 km/deg
|
| 545 |
+
half_m = max((bounds[2] - bounds[0]), (bounds[3] - bounds[1])) / 2 * 111_000
|
| 546 |
+
return JSONResponse(_clip_simplify(sandy_inundation.load(), cy, cx, half_m * 1.2),
|
| 547 |
+
headers={"Cache-Control": "public, max-age=600"})
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
@app.get("/api/layers/dep_clipped")
|
| 551 |
+
def layer_dep_clipped(code: str, scenario: str = "dep_extreme_2080"):
|
| 552 |
+
"""DEP scenario polygons clipped to an NTA bbox + simplified."""
|
| 553 |
+
from app.areas import nta as nta_mod
|
| 554 |
+
from app.flood_layers import dep_stormwater
|
| 555 |
+
poly = nta_mod.polygon_for(code)
|
| 556 |
+
if poly is None:
|
| 557 |
+
return JSONResponse({"type": "FeatureCollection", "features": []})
|
| 558 |
+
bounds = poly.bounds
|
| 559 |
+
cx, cy = (bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2
|
| 560 |
+
half_m = max((bounds[2] - bounds[0]), (bounds[3] - bounds[1])) / 2 * 111_000
|
| 561 |
+
return JSONResponse(_clip_simplify(dep_stormwater.load(scenario), cy, cx, half_m * 1.2,
|
| 562 |
+
props_keep={"Flooding_Category"}),
|
| 563 |
+
headers={"Cache-Control": "public, max-age=600"})
|
| 564 |
+
|
| 565 |
+
|
| 566 |
@app.get("/api/layers/sandy")
|
| 567 |
def layer_sandy(lat: float, lon: float, r: float = 1500):
|
| 568 |
key = ("sandy", round(lat, 4), round(lon, 4), int(r))
|
|
@@ -0,0 +1,523 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>Riprap — agent</title>
|
| 7 |
+
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css">
|
| 8 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 9 |
+
<style>
|
| 10 |
+
.agent-topbar-bar {
|
| 11 |
+
max-width: 1640px; margin: 14px auto 8px; padding: 0 20px;
|
| 12 |
+
display: flex; gap: 8px; align-items: center;
|
| 13 |
+
}
|
| 14 |
+
.agent-input-form {
|
| 15 |
+
flex: 1; display: flex; gap: 8px;
|
| 16 |
+
border: 1px solid var(--line); border-radius: 4px;
|
| 17 |
+
background: var(--panel); padding: 6px;
|
| 18 |
+
}
|
| 19 |
+
.agent-input-form input {
|
| 20 |
+
flex: 1; border: 0; outline: 0; padding: 10px 12px;
|
| 21 |
+
font-size: 14.5px; background: transparent; color: var(--text);
|
| 22 |
+
font-family: inherit;
|
| 23 |
+
}
|
| 24 |
+
.agent-input-form button {
|
| 25 |
+
padding: 8px 18px; border: 0; border-radius: 3px;
|
| 26 |
+
background: var(--nyc-blue); color: #fff; font-weight: 600;
|
| 27 |
+
cursor: pointer; font-size: 13px; font-family: inherit;
|
| 28 |
+
}
|
| 29 |
+
.agent-input-form button:disabled { opacity: 0.6; cursor: wait; }
|
| 30 |
+
|
| 31 |
+
/* Mellea compliance badge in the briefing header */
|
| 32 |
+
.mellea-badge {
|
| 33 |
+
display: inline-block; margin-left: 8px;
|
| 34 |
+
padding: 2px 9px; border-radius: 999px;
|
| 35 |
+
font-size: 10.5px; font-weight: 700;
|
| 36 |
+
font-family: var(--mono); letter-spacing: 0.03em;
|
| 37 |
+
vertical-align: middle;
|
| 38 |
+
color: white;
|
| 39 |
+
/* Bloom in once when the badge is rendered. transform-origin keeps the
|
| 40 |
+
scale anchored at the left edge so it doesn't push neighbors. */
|
| 41 |
+
animation: mellea-bloom 380ms cubic-bezier(.2,.7,.3,1.4);
|
| 42 |
+
transform-origin: left center;
|
| 43 |
+
}
|
| 44 |
+
@keyframes mellea-bloom {
|
| 45 |
+
0% { transform: scale(0.5); opacity: 0; }
|
| 46 |
+
60% { transform: scale(1.08); opacity: 1; }
|
| 47 |
+
100% { transform: scale(1); opacity: 1; }
|
| 48 |
+
}
|
| 49 |
+
/* Inline banner that appears between the briefing header and the prose
|
| 50 |
+
when Mellea is about to reroll (or when it confirms first-try pass). */
|
| 51 |
+
.mellea-banner {
|
| 52 |
+
margin: 0 16px 8px; padding: 8px 12px;
|
| 53 |
+
border-radius: 4px; font-size: 11.5px;
|
| 54 |
+
font-family: var(--mono);
|
| 55 |
+
border: 1px solid transparent;
|
| 56 |
+
animation: mellea-bloom 280ms cubic-bezier(.2,.7,.3,1.1);
|
| 57 |
+
transform-origin: left center;
|
| 58 |
+
}
|
| 59 |
+
.mellea-banner.reroll {
|
| 60 |
+
background: rgba(217, 119, 6, 0.10);
|
| 61 |
+
border-color: rgba(217, 119, 6, 0.35);
|
| 62 |
+
color: #92400e;
|
| 63 |
+
}
|
| 64 |
+
.mellea-banner.pass {
|
| 65 |
+
background: rgba(26, 135, 84, 0.10);
|
| 66 |
+
border-color: rgba(26, 135, 84, 0.35);
|
| 67 |
+
color: #1a5e3a;
|
| 68 |
+
}
|
| 69 |
+
.mellea-banner code {
|
| 70 |
+
background: rgba(0,0,0,0.06); padding: 1px 5px; border-radius: 3px;
|
| 71 |
+
font-size: 10.5px;
|
| 72 |
+
}
|
| 73 |
+
.mellea-badge.full { background: #1a8754; } /* 4/4 */
|
| 74 |
+
.mellea-badge.partial { background: #d97706; } /* 1-3/4 */
|
| 75 |
+
.mellea-badge.none { background: var(--nyc-scarlet); } /* 0/4 */
|
| 76 |
+
.mellea-badge .ico { font-size: 9px; margin-right: 3px; }
|
| 77 |
+
.agent-samples {
|
| 78 |
+
max-width: 1640px; margin: 4px auto 12px; padding: 0 20px;
|
| 79 |
+
display: flex; flex-wrap: wrap; gap: 8px;
|
| 80 |
+
}
|
| 81 |
+
.agent-samples .label {
|
| 82 |
+
font-size: 11px; color: var(--text-muted);
|
| 83 |
+
letter-spacing: 0.05em; text-transform: uppercase;
|
| 84 |
+
align-self: center; margin-right: 4px;
|
| 85 |
+
}
|
| 86 |
+
.sample-btn {
|
| 87 |
+
display: inline-flex; align-items: center; gap: 6px;
|
| 88 |
+
padding: 6px 11px; border: 1px solid var(--line);
|
| 89 |
+
background: var(--panel); border-radius: 999px;
|
| 90 |
+
font-size: 12px; color: var(--text); cursor: pointer;
|
| 91 |
+
font-family: inherit;
|
| 92 |
+
transition: background 0.12s, border-color 0.12s;
|
| 93 |
+
}
|
| 94 |
+
.sample-btn:hover { background: var(--bg-soft); border-color: var(--nyc-blue); }
|
| 95 |
+
.sample-btn .pill {
|
| 96 |
+
padding: 1px 7px; border-radius: 999px;
|
| 97 |
+
font-size: 9.5px; font-weight: 700;
|
| 98 |
+
letter-spacing: 0.05em; text-transform: uppercase;
|
| 99 |
+
}
|
| 100 |
+
.sample-btn .pill.live { background: #1a8754; color: white; }
|
| 101 |
+
.sample-btn .pill.addr { background: #6b7280; color: white; }
|
| 102 |
+
.sample-btn .pill.nbhd { background: #1642DF; color: white; }
|
| 103 |
+
.sample-btn .pill.dev { background: #af3a03; color: white; }
|
| 104 |
+
.sample-btn .qtxt {
|
| 105 |
+
white-space: nowrap; overflow: hidden;
|
| 106 |
+
text-overflow: ellipsis; max-width: 280px;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
/* ---- planner box (full-width above the 3 panels) ---- */
|
| 110 |
+
.planner-row {
|
| 111 |
+
max-width: 1640px; margin: 0 auto 12px; padding: 0 20px;
|
| 112 |
+
}
|
| 113 |
+
.planner-box {
|
| 114 |
+
background: var(--bg-soft); border: 1px solid var(--line);
|
| 115 |
+
border-radius: 4px; padding: 10px 14px;
|
| 116 |
+
display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px;
|
| 117 |
+
font-size: 12px;
|
| 118 |
+
}
|
| 119 |
+
.planner-key {
|
| 120 |
+
color: var(--text-muted); font-weight: 700;
|
| 121 |
+
text-transform: uppercase; font-size: 10px; letter-spacing: 0.06em;
|
| 122 |
+
}
|
| 123 |
+
.planner-val { font-family: var(--mono); font-size: 11.5px; }
|
| 124 |
+
.planner-rationale {
|
| 125 |
+
grid-column: 1 / -1;
|
| 126 |
+
color: var(--text-muted); font-style: italic; margin-top: 4px; font-size: 11.5px;
|
| 127 |
+
}
|
| 128 |
+
.intent-pill {
|
| 129 |
+
display: inline-block; padding: 1px 9px; border-radius: 999px;
|
| 130 |
+
background: var(--nyc-blue); color: white; font-size: 10px; font-weight: 700;
|
| 131 |
+
text-transform: uppercase; letter-spacing: 0.05em;
|
| 132 |
+
}
|
| 133 |
+
.intent-pill.dev { background: #af3a03; }
|
| 134 |
+
.intent-pill.live { background: #1a8754; }
|
| 135 |
+
.intent-pill.nbhd { background: #1642DF; }
|
| 136 |
+
.intent-pill.addr { background: #6b7280; }
|
| 137 |
+
|
| 138 |
+
/* ---- loading skeletons ---- */
|
| 139 |
+
@keyframes pulse {
|
| 140 |
+
0%, 100% { background-color: var(--bg-soft); }
|
| 141 |
+
50% { background-color: rgba(22, 66, 223, 0.08); }
|
| 142 |
+
}
|
| 143 |
+
.skel {
|
| 144 |
+
background: var(--bg-soft); border-radius: 3px;
|
| 145 |
+
animation: pulse 1.6s ease-in-out infinite;
|
| 146 |
+
}
|
| 147 |
+
.skel-line { height: 12px; margin: 6px 0; }
|
| 148 |
+
.skel-line.w-100 { width: 100%; }
|
| 149 |
+
.skel-line.w-80 { width: 80%; }
|
| 150 |
+
.skel-line.w-60 { width: 60%; }
|
| 151 |
+
.skel-line.w-40 { width: 40%; }
|
| 152 |
+
.skel-pad { padding: 14px 16px; }
|
| 153 |
+
|
| 154 |
+
.loading-overlay {
|
| 155 |
+
position: relative;
|
| 156 |
+
pointer-events: none;
|
| 157 |
+
}
|
| 158 |
+
.loading-overlay::after {
|
| 159 |
+
content: ""; position: absolute; inset: 0;
|
| 160 |
+
background: rgba(255,255,255,0.55);
|
| 161 |
+
backdrop-filter: blur(0.5px);
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.map-loading {
|
| 165 |
+
position: absolute; left: 50%; top: 50%;
|
| 166 |
+
transform: translate(-50%, -50%);
|
| 167 |
+
background: var(--panel); border: 1px solid var(--line);
|
| 168 |
+
border-radius: 4px; padding: 8px 14px;
|
| 169 |
+
font-size: 11.5px; color: var(--text-muted);
|
| 170 |
+
z-index: 10; pointer-events: none;
|
| 171 |
+
display: flex; align-items: center; gap: 8px;
|
| 172 |
+
}
|
| 173 |
+
.map-loading .dot {
|
| 174 |
+
width: 6px; height: 6px; border-radius: 50%;
|
| 175 |
+
background: var(--nyc-blue);
|
| 176 |
+
animation: dotpulse 1.2s ease-in-out infinite;
|
| 177 |
+
}
|
| 178 |
+
@keyframes dotpulse {
|
| 179 |
+
0%, 100% { opacity: 0.3; transform: scale(0.85); }
|
| 180 |
+
50% { opacity: 1; transform: scale(1.1); }
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
/* ---- map legend (intent-aware) ---- */
|
| 184 |
+
.map-legend {
|
| 185 |
+
position: absolute; left: 10px; bottom: 10px;
|
| 186 |
+
background: rgba(255,255,255,0.95);
|
| 187 |
+
border: 1px solid var(--line); border-radius: 4px;
|
| 188 |
+
padding: 8px 12px; font-size: 11px; color: var(--text);
|
| 189 |
+
box-shadow: 0 2px 6px rgba(0,0,0,0.06);
|
| 190 |
+
pointer-events: none;
|
| 191 |
+
z-index: 5;
|
| 192 |
+
}
|
| 193 |
+
.map-legend .legend-row { display: flex; align-items: center; gap: 6px; margin: 2px 0; }
|
| 194 |
+
.legend-swatch {
|
| 195 |
+
width: 12px; height: 12px; border-radius: 50%;
|
| 196 |
+
border: 1.5px solid #fff; box-shadow: 0 0 0 1px rgba(0,0,0,0.08);
|
| 197 |
+
}
|
| 198 |
+
.legend-swatch.fill {
|
| 199 |
+
width: 14px; height: 10px; border-radius: 2px; box-shadow: none; border: 0;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/* ---- briefing header (matches the report-head idiom from /) ---- */
|
| 203 |
+
.brief-head {
|
| 204 |
+
padding: 14px 16px;
|
| 205 |
+
border-bottom: 1px solid var(--line);
|
| 206 |
+
background: linear-gradient(180deg, var(--bg-soft) 0%, #fff 100%);
|
| 207 |
+
}
|
| 208 |
+
.brief-eyebrow {
|
| 209 |
+
font-size: 10px; font-weight: 700;
|
| 210 |
+
letter-spacing: 0.10em; text-transform: uppercase;
|
| 211 |
+
color: var(--nyc-blue);
|
| 212 |
+
}
|
| 213 |
+
.brief-title {
|
| 214 |
+
margin-top: 4px;
|
| 215 |
+
font-size: 16px; font-weight: 600;
|
| 216 |
+
line-height: 1.25; color: var(--text);
|
| 217 |
+
}
|
| 218 |
+
.brief-meta {
|
| 219 |
+
margin-top: 6px;
|
| 220 |
+
font-family: var(--mono); font-size: 11px;
|
| 221 |
+
color: var(--text-muted);
|
| 222 |
+
display: flex; flex-wrap: wrap; gap: 4px 10px;
|
| 223 |
+
}
|
| 224 |
+
.brief-meta-k {
|
| 225 |
+
text-transform: uppercase; font-size: 9.5px;
|
| 226 |
+
letter-spacing: 0.05em; color: var(--text-faint);
|
| 227 |
+
}
|
| 228 |
+
.brief-meta-v { color: var(--text); }
|
| 229 |
+
|
| 230 |
+
.report-btn {
|
| 231 |
+
display: none; /* shown by JS once a query completes */
|
| 232 |
+
margin-top: 10px; padding: 6px 12px;
|
| 233 |
+
border: 1px solid var(--nyc-blue);
|
| 234 |
+
background: var(--panel); color: var(--nyc-blue);
|
| 235 |
+
border-radius: 3px; cursor: pointer; font-size: 12px;
|
| 236 |
+
font-weight: 600; font-family: inherit;
|
| 237 |
+
transition: background 0.12s, color 0.12s;
|
| 238 |
+
}
|
| 239 |
+
.report-btn:hover { background: var(--nyc-blue); color: white; }
|
| 240 |
+
.report-btn.ready { display: inline-block; }
|
| 241 |
+
|
| 242 |
+
/* tier badge inline with the title — single_address intent only.
|
| 243 |
+
Mirrors the colour idiom from the legacy /single page tier-badge. */
|
| 244 |
+
.tier-chip {
|
| 245 |
+
display: inline-block;
|
| 246 |
+
margin-left: 8px;
|
| 247 |
+
padding: 2px 10px;
|
| 248 |
+
border-radius: 999px;
|
| 249 |
+
font-size: 11px; font-weight: 700;
|
| 250 |
+
font-family: var(--mono); letter-spacing: 0.04em;
|
| 251 |
+
vertical-align: middle;
|
| 252 |
+
color: white;
|
| 253 |
+
}
|
| 254 |
+
.tier-chip.t-0 { background: var(--good); }
|
| 255 |
+
.tier-chip.t-1 { background: var(--nyc-scarlet); }
|
| 256 |
+
.tier-chip.t-2 { background: #d97706; }
|
| 257 |
+
.tier-chip.t-3 { background: #ca8a04; }
|
| 258 |
+
.tier-chip.t-4 { background: var(--nyc-blue); }
|
| 259 |
+
.tier-floor {
|
| 260 |
+
font-size: 9.5px; font-weight: 600;
|
| 261 |
+
background: rgba(255,255,255,0.22);
|
| 262 |
+
padding: 1px 5px; border-radius: 6px; margin-left: 4px;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
/* ---- streaming caret on the briefing while tokens flow ---- */
|
| 266 |
+
.streaming::after {
|
| 267 |
+
content: "▋";
|
| 268 |
+
display: inline-block; color: var(--nyc-blue);
|
| 269 |
+
margin-left: 2px;
|
| 270 |
+
animation: caret 0.9s steps(1) infinite;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
/* ---- citation chips + Sources footer ---- */
|
| 274 |
+
.report-pane #paragraph .cite {
|
| 275 |
+
cursor: pointer;
|
| 276 |
+
transition: background 0.15s, color 0.15s;
|
| 277 |
+
}
|
| 278 |
+
.report-pane #paragraph .cite:hover,
|
| 279 |
+
.report-pane #paragraph .cite.hl {
|
| 280 |
+
background: var(--nyc-blue) !important;
|
| 281 |
+
color: white !important;
|
| 282 |
+
}
|
| 283 |
+
#sourcesSection {
|
| 284 |
+
border-top: 1px solid var(--line);
|
| 285 |
+
background: var(--bg-soft);
|
| 286 |
+
padding: 12px 16px 14px;
|
| 287 |
+
}
|
| 288 |
+
#sourcesSection .src-h {
|
| 289 |
+
font-size: 10px; font-weight: 700;
|
| 290 |
+
text-transform: uppercase; letter-spacing: 0.10em;
|
| 291 |
+
color: var(--text-muted);
|
| 292 |
+
margin: 0 0 8px;
|
| 293 |
+
}
|
| 294 |
+
#sourcesSection ol {
|
| 295 |
+
margin: 0; padding: 0; list-style: none;
|
| 296 |
+
display: grid; gap: 6px;
|
| 297 |
+
font-size: 11.5px; line-height: 1.45;
|
| 298 |
+
}
|
| 299 |
+
#sourcesSection ol li {
|
| 300 |
+
display: grid; grid-template-columns: 22px 1fr;
|
| 301 |
+
gap: 8px; align-items: baseline;
|
| 302 |
+
padding: 4px 6px; border-radius: 3px;
|
| 303 |
+
transition: background 0.15s;
|
| 304 |
+
}
|
| 305 |
+
#sourcesSection ol li.hl { background: rgba(22, 66, 223, 0.10); }
|
| 306 |
+
#sourcesSection .src-num {
|
| 307 |
+
font-family: var(--mono); font-size: 10.5px;
|
| 308 |
+
font-weight: 700; color: var(--nyc-blue);
|
| 309 |
+
text-align: right;
|
| 310 |
+
}
|
| 311 |
+
#sourcesSection .src-label { color: var(--text); }
|
| 312 |
+
#sourcesSection .src-link {
|
| 313 |
+
color: var(--text); text-decoration: none;
|
| 314 |
+
border-bottom: 1px dotted var(--text-muted);
|
| 315 |
+
transition: color 0.12s, border-color 0.12s;
|
| 316 |
+
}
|
| 317 |
+
#sourcesSection .src-link:hover {
|
| 318 |
+
color: var(--nyc-blue);
|
| 319 |
+
border-bottom-color: var(--nyc-blue);
|
| 320 |
+
}
|
| 321 |
+
#sourcesSection .src-ext {
|
| 322 |
+
font-size: 9.5px; color: var(--text-faint);
|
| 323 |
+
margin-left: 2px; vertical-align: super;
|
| 324 |
+
}
|
| 325 |
+
#sourcesSection .src-id {
|
| 326 |
+
font-family: var(--mono); font-size: 10px;
|
| 327 |
+
color: var(--text-faint); margin-left: 6px;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
/* live-streaming planner output (raw JSON forming character-by-character) */
|
| 331 |
+
.planner-streaming {
|
| 332 |
+
background: var(--bg-soft); border: 1px solid var(--line);
|
| 333 |
+
border-radius: 4px; padding: 10px 14px;
|
| 334 |
+
font-family: var(--mono); font-size: 11px; color: var(--text-muted);
|
| 335 |
+
line-height: 1.5; white-space: pre-wrap; word-break: break-word;
|
| 336 |
+
max-height: 160px; overflow: auto;
|
| 337 |
+
position: relative;
|
| 338 |
+
}
|
| 339 |
+
.planner-streaming::before {
|
| 340 |
+
content: "Planner thinking…";
|
| 341 |
+
position: absolute; top: 6px; right: 10px;
|
| 342 |
+
font-family: inherit; font-size: 9.5px;
|
| 343 |
+
color: var(--text-faint); letter-spacing: 0.06em;
|
| 344 |
+
text-transform: uppercase;
|
| 345 |
+
}
|
| 346 |
+
.planner-streaming::after {
|
| 347 |
+
content: "▋";
|
| 348 |
+
display: inline-block; color: var(--nyc-blue);
|
| 349 |
+
animation: caret 0.9s steps(1) infinite;
|
| 350 |
+
}
|
| 351 |
+
@keyframes caret { 50% { opacity: 0; } }
|
| 352 |
+
|
| 353 |
+
#map { width: 100%; height: 600px; border: 1px solid var(--line); border-radius: 4px; }
|
| 354 |
+
|
| 355 |
+
/* ---- structured report ---- */
|
| 356 |
+
.report-pane #paragraph .rsum-h {
|
| 357 |
+
margin: 12px 0 6px; font-size: 10.5px; font-weight: 700;
|
| 358 |
+
text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted);
|
| 359 |
+
}
|
| 360 |
+
.report-pane #paragraph .rsum-h:first-child { margin-top: 0; }
|
| 361 |
+
.report-pane #paragraph .rsum-p { margin: 0 0 6px; line-height: 1.55; font-size: 13px; }
|
| 362 |
+
.report-pane #paragraph .rsum-list { margin: 4px 0 8px 0; padding: 0; list-style: none; }
|
| 363 |
+
.report-pane #paragraph .rsum-list li {
|
| 364 |
+
display: block; padding: 8px 10px; margin: 4px 0;
|
| 365 |
+
background: var(--bg-soft); border-left: 3px solid var(--nyc-blue);
|
| 366 |
+
border-radius: 0 3px 3px 0; font-size: 12.5px; line-height: 1.5;
|
| 367 |
+
}
|
| 368 |
+
.report-pane #paragraph strong {
|
| 369 |
+
font-weight: 600;
|
| 370 |
+
background: linear-gradient(transparent 60%, var(--nyc-blue-soft) 60%);
|
| 371 |
+
padding: 0 2px;
|
| 372 |
+
}
|
| 373 |
+
.report-pane #paragraph .cite {
|
| 374 |
+
display: inline-block; vertical-align: super; font-size: 9.5px;
|
| 375 |
+
font-family: var(--mono); padding: 0 5px; margin-left: 2px;
|
| 376 |
+
background: var(--bg-soft); border-radius: 8px;
|
| 377 |
+
color: var(--text-muted);
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
/* ---- intent-specific facts panel ---- */
|
| 381 |
+
.facts-grid {
|
| 382 |
+
display: grid; grid-template-columns: 1fr 1fr; gap: 6px 14px;
|
| 383 |
+
margin: 8px 0; font-size: 12px;
|
| 384 |
+
}
|
| 385 |
+
.facts-grid dt {
|
| 386 |
+
color: var(--text-muted); font-size: 10.5px;
|
| 387 |
+
text-transform: uppercase; letter-spacing: 0.05em; font-weight: 700;
|
| 388 |
+
}
|
| 389 |
+
.facts-grid dd { margin: 0; font-family: var(--mono); }
|
| 390 |
+
.headline-stat {
|
| 391 |
+
font-size: 26px; font-weight: 700; color: var(--text);
|
| 392 |
+
margin: 6px 0 2px; line-height: 1.1;
|
| 393 |
+
}
|
| 394 |
+
.headline-sub {
|
| 395 |
+
color: var(--text-muted); font-size: 12px; margin-bottom: 8px;
|
| 396 |
+
}
|
| 397 |
+
</style>
|
| 398 |
+
</head>
|
| 399 |
+
<body>
|
| 400 |
+
<header class="topbar">
|
| 401 |
+
<div class="topbar-inner">
|
| 402 |
+
<div class="brand">
|
| 403 |
+
<span class="brand-name">Riprap</span>
|
| 404 |
+
<span class="brand-sep">·</span>
|
| 405 |
+
<span class="brand-tag">citation-grounded flood-exposure briefings for NYC</span>
|
| 406 |
+
</div>
|
| 407 |
+
<div class="topbar-right">
|
| 408 |
+
<span id="backendPill" class="local-pill" data-state="loading"
|
| 409 |
+
title="Granite 4.1 inference. No vendor LLM is contacted.">
|
| 410 |
+
<span class="dot"></span><span id="backendPillText">checking…</span>
|
| 411 |
+
</span>
|
| 412 |
+
</div>
|
| 413 |
+
</div>
|
| 414 |
+
</header>
|
| 415 |
+
|
| 416 |
+
<div class="agent-topbar-bar">
|
| 417 |
+
<form id="agentForm" class="agent-input-form" autocomplete="off">
|
| 418 |
+
<input id="q" type="text" placeholder="Ask anything: an address, a neighborhood, 'what are they building in Gowanus', 'is there flooding right now'…" autofocus />
|
| 419 |
+
<button type="submit" id="goBtn">Ask</button>
|
| 420 |
+
</form>
|
| 421 |
+
</div>
|
| 422 |
+
|
| 423 |
+
<div class="agent-samples">
|
| 424 |
+
<span class="label">Try:</span>
|
| 425 |
+
<!-- Defaults chosen by a 16-query sweep across NYC; ranked by
|
| 426 |
+
(map-layers populated, unique-citations, latency).
|
| 427 |
+
See /tmp/sweep-out.log for the full ranking. -->
|
| 428 |
+
<button class="sample-btn" data-q="2940 Brighton 3rd St, Brooklyn"
|
| 429 |
+
title="single_address — 5 map layers + 8 cites; coastal Sandy + DEP + 311 + FloodNet + Ida HWMs + NOAA gauge + TerraMind LULC">
|
| 430 |
+
<span class="pill addr">address</span><span class="qtxt">2940 Brighton 3rd St (coastal)</span>
|
| 431 |
+
</button>
|
| 432 |
+
<button class="sample-btn" data-q="180-08 Hillside Ave, Jamaica, NY"
|
| 433 |
+
title="single_address — 5 layers + 7 cites; Jamaica/Hollis pluvial-inland pattern">
|
| 434 |
+
<span class="pill addr">address</span><span class="qtxt">Hillside Ave, Jamaica (pluvial)</span>
|
| 435 |
+
</button>
|
| 436 |
+
<button class="sample-btn" data-q="100 Gold St Manhattan"
|
| 437 |
+
title="single_address — 4 layers + 7 cites; Lower Manhattan dense urban">
|
| 438 |
+
<span class="pill addr">address</span><span class="qtxt">100 Gold St (Manhattan)</span>
|
| 439 |
+
</button>
|
| 440 |
+
<button class="sample-btn" data-q="Far Rockaway"
|
| 441 |
+
title="neighborhood — 7 unique cites; coastal Queens NTA polygon scope">
|
| 442 |
+
<span class="pill nbhd">neighborhood</span><span class="qtxt">Far Rockaway</span>
|
| 443 |
+
</button>
|
| 444 |
+
<button class="sample-btn" data-q="Gowanus"
|
| 445 |
+
title="neighborhood — 6 cites; combined-sewer / pluvial Brooklyn">
|
| 446 |
+
<span class="pill nbhd">neighborhood</span><span class="qtxt">Gowanus</span>
|
| 447 |
+
</button>
|
| 448 |
+
<button class="sample-btn" data-q="is there flooding right now in NYC"
|
| 449 |
+
title="live_now — fast (~13 s); NWS alerts + NOAA tides + TTM surge nowcast">
|
| 450 |
+
<span class="pill live">live</span><span class="qtxt">flooding right now in NYC</span>
|
| 451 |
+
</button>
|
| 452 |
+
</div>
|
| 453 |
+
|
| 454 |
+
<div class="planner-row" id="plannerRow"></div>
|
| 455 |
+
|
| 456 |
+
<div class="workbench">
|
| 457 |
+
<aside class="col-left">
|
| 458 |
+
<section class="panel">
|
| 459 |
+
<h2>Specialist trace <span class="hint" id="traceMeta"></span></h2>
|
| 460 |
+
<r-trace id="steps"></r-trace>
|
| 461 |
+
<div id="traceSkel" class="skel-pad" style="display:none">
|
| 462 |
+
<div class="skel skel-line w-80"></div>
|
| 463 |
+
<div class="skel skel-line w-60"></div>
|
| 464 |
+
<div class="skel skel-line w-100"></div>
|
| 465 |
+
<div class="skel skel-line w-40"></div>
|
| 466 |
+
</div>
|
| 467 |
+
</section>
|
| 468 |
+
</aside>
|
| 469 |
+
|
| 470 |
+
<main class="col-mid">
|
| 471 |
+
<div id="map-card" class="panel panel-map" style="position:relative">
|
| 472 |
+
<div id="map"></div>
|
| 473 |
+
<div id="mapLoading" class="map-loading" style="display:none">
|
| 474 |
+
<span class="dot"></span><span id="mapLoadingText">Resolving location…</span>
|
| 475 |
+
</div>
|
| 476 |
+
<div id="mapLegend" class="map-legend" style="display:none"></div>
|
| 477 |
+
</div>
|
| 478 |
+
<section class="panel" id="factsPanel" style="display:none">
|
| 479 |
+
<h2 id="factsTitle">Findings</h2>
|
| 480 |
+
<div id="factsBody" style="padding: 12px 16px;"></div>
|
| 481 |
+
</section>
|
| 482 |
+
</main>
|
| 483 |
+
|
| 484 |
+
<aside class="col-right">
|
| 485 |
+
<section class="panel report-pane" id="reportPanel" style="display:none">
|
| 486 |
+
<header class="brief-head" id="briefHead">
|
| 487 |
+
<div class="brief-eyebrow" id="briefEyebrow">Briefing</div>
|
| 488 |
+
<div class="brief-title" id="briefTitle">—</div>
|
| 489 |
+
<div class="brief-meta" id="briefMeta"></div>
|
| 490 |
+
<button id="reportBtn" class="report-btn" title="Open a print-ready PDF-formatted report of this query in a new tab">
|
| 491 |
+
↗ Generate auditable report
|
| 492 |
+
</button>
|
| 493 |
+
</header>
|
| 494 |
+
<div id="melleaBanner" class="mellea-banner" style="display:none"></div>
|
| 495 |
+
<r-briefing id="paragraph" style="display:block; padding: 14px 16px 18px;"></r-briefing>
|
| 496 |
+
<r-sources-footer id="sourcesFooter" hidden></r-sources-footer>
|
| 497 |
+
</section>
|
| 498 |
+
<section class="panel" id="reportSkel" style="display:none">
|
| 499 |
+
<header class="brief-head">
|
| 500 |
+
<div class="brief-eyebrow">Mellea is validating the briefing</div>
|
| 501 |
+
<div class="brief-title" style="color:var(--text-muted)">Granite drafts → 4 grounding requirements → reroll if any fail…</div>
|
| 502 |
+
</header>
|
| 503 |
+
<div class="skel-pad">
|
| 504 |
+
<div class="skel skel-line w-40" style="height:10px"></div>
|
| 505 |
+
<div class="skel skel-line w-100"></div>
|
| 506 |
+
<div class="skel skel-line w-100"></div>
|
| 507 |
+
<div class="skel skel-line w-80"></div>
|
| 508 |
+
<div class="skel skel-line w-40" style="height:10px; margin-top:14px"></div>
|
| 509 |
+
<div class="skel skel-line w-100"></div>
|
| 510 |
+
<div class="skel skel-line w-60"></div>
|
| 511 |
+
</div>
|
| 512 |
+
</section>
|
| 513 |
+
</aside>
|
| 514 |
+
</div>
|
| 515 |
+
|
| 516 |
+
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
|
| 517 |
+
<!-- Svelte custom-element bundle — registers <r-briefing>, <r-trace>,
|
| 518 |
+
<r-sources-footer>. agent.js sets properties on these tags exactly
|
| 519 |
+
as before; Svelte just owns the implementation now. -->
|
| 520 |
+
<script type="module" src="/static/dist/riprap.js"></script>
|
| 521 |
+
<script src="/static/agent.js"></script>
|
| 522 |
+
</body>
|
| 523 |
+
</html>
|
|
@@ -0,0 +1,1391 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Riprap agent client — three-panel UI with live SSE streaming, intent-
|
| 2 |
+
// dispatched map, and structured report rendering.
|
| 3 |
+
|
| 4 |
+
const $ = (s) => document.querySelector(s);
|
| 5 |
+
|
| 6 |
+
const STEP_LABELS = {
|
| 7 |
+
// single_address chain (linear FSM)
|
| 8 |
+
geocode: ["Geocode (DCP Geosearch)", "address → lat/lon, BBL"],
|
| 9 |
+
sandy_inundation: ["Sandy Inundation (NYC OD)", "empirical 2012 extent"],
|
| 10 |
+
dep_stormwater: ["DEP Stormwater Maps", "pluvial scenarios + 2080 SLR"],
|
| 11 |
+
floodnet: ["FloodNet sensor network", "live ultrasonic depth sensors"],
|
| 12 |
+
nyc311: ["NYC 311 archive", "flood complaints in 200m"],
|
| 13 |
+
noaa_tides: ["NOAA Tides & Currents (live)", "Battery / Kings Pt / Sandy Hook"],
|
| 14 |
+
nws_alerts: ["NWS Public Alerts (live)", "active flood-relevant alerts"],
|
| 15 |
+
nws_obs: ["NWS METAR observation (live)", "nearest ASOS recent precipitation"],
|
| 16 |
+
ttm_forecast: ["Granite TTM r2 — surge nowcast", "9.6h forecast at the closest of Battery / Kings Pt / Sandy Hook"],
|
| 17 |
+
ttm_311_forecast: ["Granite TTM r2 — 311 forecast", "4-week per-address flood-complaint forecast (52w history)"],
|
| 18 |
+
floodnet_forecast: ["Granite TTM r2 — FloodNet forecast", "flood-event recurrence forecast at nearest FloodNet sensor"],
|
| 19 |
+
mta_entrance_exposure: ["MTA subway entrances", "subway-entrance exposure (point-in-polygon Sandy + DEP)"],
|
| 20 |
+
nycha_development_exposure: ["NYCHA developments", "NYCHA campus footprint × Sandy + DEP overlap %"],
|
| 21 |
+
doe_school_exposure: ["NYC DOE schools", "school-point exposure (Sandy + DEP)"],
|
| 22 |
+
doh_hospital_exposure: ["NYS DOH hospitals", "Article-28 hospital exposure (Sandy + DEP)"],
|
| 23 |
+
microtopo_lidar: ["LiDAR terrain (DEM + TWI + HAND)", "USGS 3DEP DEM + whitebox-workflows"],
|
| 24 |
+
ida_hwm_2021: ["Ida 2021 high-water marks", "USGS empirical post-event extent"],
|
| 25 |
+
prithvi_eo_v2: ["Prithvi-EO 2.0 (NASA/IBM)", "Sen1Floods11 satellite segmentation"],
|
| 26 |
+
prithvi_eo_live: ["Prithvi-EO 2.0 — live segmentation","fresh Sentinel-2 water mask at this address"],
|
| 27 |
+
terramind_synthesis: ["TerraMind 1.0 base — synthetic LULC", "DEM → ESRI Land Cover, any-to-any generative synthesis (IBM/ESA)"],
|
| 28 |
+
rag_granite_embedding: ["Granite Embedding 278M (RAG)", "policy corpus retrieval (+ Granite Reranker R2 if enabled)"],
|
| 29 |
+
gliner_extract: ["GLiNER typed extraction", "agencies, dollar amounts, projects, locations"],
|
| 30 |
+
reconcile_granite41: ["Granite 4.1 reconcile (local)", "document-grounded synthesis"],
|
| 31 |
+
// neighborhood + dev_check
|
| 32 |
+
nta_resolve: ["NTA polygon resolve", "name → NYC NTA 2020 polygon"],
|
| 33 |
+
sandy_nta: ["Sandy 2012, polygon-aggregated", "% of NTA inside 2012 inundation"],
|
| 34 |
+
dep_extreme_2080_nta: ["DEP Extreme-2080, polygon", "% of NTA in modeled flooding"],
|
| 35 |
+
dep_moderate_2050_nta: ["DEP Moderate-2050, polygon", "% of NTA in modeled flooding"],
|
| 36 |
+
dep_moderate_current_nta:["DEP Moderate-current, polygon", "% of NTA in modeled flooding"],
|
| 37 |
+
nyc311_nta: ["NYC 311, polygon-aggregated", "complaints inside polygon"],
|
| 38 |
+
microtopo_nta: ["LiDAR terrain, polygon", "median HAND/TWI + flood bands"],
|
| 39 |
+
rag_nta: ["Granite Embedding RAG (NTA)", "policy retrieval for the place"],
|
| 40 |
+
reconcile_neighborhood: ["Granite 4.1 reconcile (NTA)", "polygon-flavored briefing"],
|
| 41 |
+
// dev_check
|
| 42 |
+
dob_permits_nta: ["NYC DOB permits in polygon", "active NB / A1 / DM jobs ↔ flood layers"],
|
| 43 |
+
rag_dev: ["Granite Embedding RAG (dev)", "policy on new construction in flood zones"],
|
| 44 |
+
reconcile_development: ["Granite 4.1 reconcile (dev)", "flagged-projects briefing"],
|
| 45 |
+
// live_now
|
| 46 |
+
reconcile_live_now: ["Granite 4.1 reconcile (live)", "current-conditions briefing"],
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
const SOURCE_LABELS = {
|
| 50 |
+
geocode: "NYC DCP Geosearch",
|
| 51 |
+
nta_resolve: "NYC DCP Neighborhood Tabulation Areas 2020",
|
| 52 |
+
sandy: "NYC OD 5xsi-dfpx — Sandy 2012 inundation",
|
| 53 |
+
sandy_nta: "Sandy 2012 inundation, polygon-aggregated",
|
| 54 |
+
dep_extreme_2080: "NYC DEP Stormwater — Extreme-2080",
|
| 55 |
+
dep_moderate_2050: "NYC DEP Stormwater — Moderate-2050",
|
| 56 |
+
dep_moderate_current: "NYC DEP Stormwater — Moderate-current",
|
| 57 |
+
dep_extreme_2080_nta: "NYC DEP Extreme-2080, polygon-aggregated",
|
| 58 |
+
dep_moderate_2050_nta: "NYC DEP Moderate-2050, polygon-aggregated",
|
| 59 |
+
dep_moderate_current_nta: "NYC DEP Moderate-current, polygon-aggregated",
|
| 60 |
+
floodnet: "FloodNet NYC",
|
| 61 |
+
nyc311: "NYC 311 (erm2-nwe9)",
|
| 62 |
+
nyc311_nta: "NYC 311, polygon-aggregated",
|
| 63 |
+
microtopo: "USGS 3DEP DEM",
|
| 64 |
+
microtopo_nta: "USGS 3DEP DEM, polygon-aggregated",
|
| 65 |
+
ida_hwm: "USGS Hurricane Ida 2021 HWMs",
|
| 66 |
+
prithvi_water: "Prithvi-EO 2.0 — Hurricane Ida 2021 polygons",
|
| 67 |
+
prithvi_live: "Prithvi-EO 2.0 ��� live Sentinel-2 water segmentation",
|
| 68 |
+
terramind_synthetic: "TerraMind 1.0 base — synthetic LULC (DEM→ESRI Land Cover)",
|
| 69 |
+
gliner_comptroller: "GLiNER over Comptroller report",
|
| 70 |
+
gliner_dep_2013: "GLiNER over DEP wastewater plan",
|
| 71 |
+
gliner_nycha: "GLiNER over NYCHA Lessons Learned",
|
| 72 |
+
gliner_mta: "GLiNER over MTA Climate Resilience Roadmap",
|
| 73 |
+
gliner_coned: "GLiNER over Con Edison Climate Resilience",
|
| 74 |
+
noaa_tides: "NOAA CO-OPS Tides & Currents",
|
| 75 |
+
nws_alerts: "NWS Public Alerts",
|
| 76 |
+
nws_obs: "NWS Station Observations",
|
| 77 |
+
ttm_forecast: "Granite TimeSeries TTM r2 — surge residual nowcast",
|
| 78 |
+
ttm_311_forecast: "Granite TimeSeries TTM r2 — per-address 311 weekly forecast",
|
| 79 |
+
floodnet_forecast: "Granite TimeSeries TTM r2 — FloodNet sensor recurrence forecast",
|
| 80 |
+
dob_permits: "NYC DOB Permit Issuance (Socrata ipu4-2q9a)",
|
| 81 |
+
live_target: "Riprap planner — live target",
|
| 82 |
+
rag_comptroller: 'NYC Comptroller — "Is NYC Ready for Rain?" (2024)',
|
| 83 |
+
rag_npcc4: "NPCC4 (2024)",
|
| 84 |
+
rag_mta: "MTA Climate Resilience Roadmap",
|
| 85 |
+
rag_nycha: "NYCHA Flood Resilience: Lessons Learned",
|
| 86 |
+
rag_coned: "Con Edison Climate Resilience Plan",
|
| 87 |
+
// Register-specialist family labels — chip lookups for dynamic
|
| 88 |
+
// doc_ids (mta_entrance_<id>, nycha_dev_<tds>, doe_school_<loc>,
|
| 89 |
+
// nyc_hospital_<fac>) fall through to these via family-prefix match.
|
| 90 |
+
mta_entrance: "MTA subway-entrance exposure (Open Data)",
|
| 91 |
+
nycha_dev: "NYCHA development exposure (NYC OD phvi-damg)",
|
| 92 |
+
doe_school: "NYC DOE school exposure",
|
| 93 |
+
nyc_hospital: "NYS DOH hospital exposure (vn5v-hh5r)",
|
| 94 |
+
};
|
| 95 |
+
|
| 96 |
+
// Canonical URL per doc_id — clicking a source row opens the underlying
|
| 97 |
+
// dataset / API / report in a new tab so users can verify provenance.
|
| 98 |
+
const SOURCE_URLS = {
|
| 99 |
+
geocode: "https://geosearch.planninglabs.nyc/",
|
| 100 |
+
nta_resolve: "https://www.nyc.gov/site/planning/data-maps/open-data/dwn-nynta.page",
|
| 101 |
+
sandy: "https://data.cityofnewyork.us/Environment/Sandy-Inundation-Zone/uyj8-7rv5",
|
| 102 |
+
sandy_nta: "https://data.cityofnewyork.us/Environment/Sandy-Inundation-Zone/uyj8-7rv5",
|
| 103 |
+
dep_extreme_2080: "https://data.cityofnewyork.us/Environment/NYC-Stormwater-Flood-Map-Extreme-Flood-with-Curren/w8eg-8ha6",
|
| 104 |
+
dep_moderate_2050: "https://data.cityofnewyork.us/Environment/NYC-Stormwater-Flood-Map-Moderate-Flood-with-Curre/9i7c-xyvv",
|
| 105 |
+
dep_moderate_current: "https://data.cityofnewyork.us/Environment/NYC-Stormwater-Flood-Map-Moderate-Flood/5rzh-cyqd",
|
| 106 |
+
dep_extreme_2080_nta: "https://data.cityofnewyork.us/Environment/NYC-Stormwater-Flood-Map-Extreme-Flood-with-Curren/w8eg-8ha6",
|
| 107 |
+
dep_moderate_2050_nta: "https://data.cityofnewyork.us/Environment/NYC-Stormwater-Flood-Map-Moderate-Flood-with-Curre/9i7c-xyvv",
|
| 108 |
+
dep_moderate_current_nta: "https://data.cityofnewyork.us/Environment/NYC-Stormwater-Flood-Map-Moderate-Flood/5rzh-cyqd",
|
| 109 |
+
floodnet: "https://www.floodnet.nyc/",
|
| 110 |
+
nyc311: "https://data.cityofnewyork.us/Social-Services/311-Service-Requests-from-2010-to-Present/erm2-nwe9",
|
| 111 |
+
nyc311_nta: "https://data.cityofnewyork.us/Social-Services/311-Service-Requests-from-2010-to-Present/erm2-nwe9",
|
| 112 |
+
microtopo: "https://www.usgs.gov/3d-elevation-program",
|
| 113 |
+
microtopo_nta: "https://www.usgs.gov/3d-elevation-program",
|
| 114 |
+
ida_hwm: "https://stn.wim.usgs.gov/STNDataPortal/",
|
| 115 |
+
prithvi_water: "https://huggingface.co/ibm-nasa-geospatial/Prithvi-EO-2.0-300M-TL-Sen1Floods11",
|
| 116 |
+
prithvi_live: "https://huggingface.co/ibm-nasa-geospatial/Prithvi-EO-2.0-300M-TL-Sen1Floods11",
|
| 117 |
+
terramind_synthetic: "https://huggingface.co/ibm-esa-geospatial/TerraMind-1.0-base",
|
| 118 |
+
gliner_comptroller: "https://huggingface.co/urchade/gliner_medium-v2.1",
|
| 119 |
+
gliner_dep_2013: "https://huggingface.co/urchade/gliner_medium-v2.1",
|
| 120 |
+
gliner_nycha: "https://huggingface.co/urchade/gliner_medium-v2.1",
|
| 121 |
+
gliner_mta: "https://huggingface.co/urchade/gliner_medium-v2.1",
|
| 122 |
+
gliner_coned: "https://huggingface.co/urchade/gliner_medium-v2.1",
|
| 123 |
+
noaa_tides: "https://tidesandcurrents.noaa.gov/",
|
| 124 |
+
nws_alerts: "https://www.weather.gov/documentation/services-web-api",
|
| 125 |
+
nws_obs: "https://www.weather.gov/documentation/services-web-api",
|
| 126 |
+
ttm_forecast: "https://huggingface.co/ibm-granite/granite-timeseries-ttm-r2",
|
| 127 |
+
ttm_311_forecast: "https://huggingface.co/ibm-granite/granite-timeseries-ttm-r2",
|
| 128 |
+
floodnet_forecast: "https://huggingface.co/ibm-granite/granite-timeseries-ttm-r2",
|
| 129 |
+
dob_permits: "https://data.cityofnewyork.us/Housing-Development/DOB-Permit-Issuance/ipu4-2q9a",
|
| 130 |
+
rag_comptroller: "https://comptroller.nyc.gov/reports/is-new-york-city-ready-for-rain/",
|
| 131 |
+
rag_npcc4: "https://nyaspubs.onlinelibrary.wiley.com/toc/17496632/2024/1539/1",
|
| 132 |
+
rag_mta: "https://new.mta.info/sustainability/climate-resilience",
|
| 133 |
+
rag_nycha: "https://www.nyc.gov/site/nycha/about/sustainability.page",
|
| 134 |
+
rag_coned: "https://www.coned.com/en/our-energy-future/climate-change-resilience",
|
| 135 |
+
mta_entrance: "https://data.ny.gov/Transportation/MTA-Subway-Entrances-and-Exits-2024/i9wp-a4ja",
|
| 136 |
+
nycha_dev: "https://data.cityofnewyork.us/Housing-Development/Map-of-NYCHA-Developments/i9rv-hdr5",
|
| 137 |
+
doe_school: "https://data.cityofnewyork.us/Education/School-Locations/jfju-ynrr",
|
| 138 |
+
nyc_hospital: "https://health.data.ny.gov/Health/Health-Facility-Certification-Information/2g9y-7kqm",
|
| 139 |
+
};
|
| 140 |
+
|
| 141 |
+
// Per-source vintage / "as of" — what date the underlying data represents.
|
| 142 |
+
// For live sources, the answer is "live; observation timestamps in payload".
|
| 143 |
+
// For archival sources, this is the dataset publication or extent date.
|
| 144 |
+
const SOURCE_VINTAGES = {
|
| 145 |
+
geocode: "live (NYC DCP Geosearch v2)",
|
| 146 |
+
nta_resolve: "NYC NTA 2020 boundaries (DCP, Sept 2022 release)",
|
| 147 |
+
sandy: "Sandy 2012 inundation extent (NYC OEM survey, dataset published 2013)",
|
| 148 |
+
sandy_nta: "Sandy 2012 inundation extent (polygon-aggregated)",
|
| 149 |
+
dep_extreme_2080: "NYC DEP Stormwater Flood Map — Extreme + 2080 SLR (2021 release)",
|
| 150 |
+
dep_moderate_2050: "NYC DEP Stormwater Flood Map — Moderate + 2050 SLR (2021 release)",
|
| 151 |
+
dep_moderate_current: "NYC DEP Stormwater Flood Map — Moderate, current SLR (2021 release)",
|
| 152 |
+
dep_extreme_2080_nta: "NYC DEP Extreme-2080 (2021 release; polygon-aggregated)",
|
| 153 |
+
dep_moderate_2050_nta: "NYC DEP Moderate-2050 (2021 release; polygon-aggregated)",
|
| 154 |
+
dep_moderate_current_nta: "NYC DEP Moderate-current (2021 release; polygon-aggregated)",
|
| 155 |
+
floodnet: "live FloodNet sensor stream (per-event timestamps in payload)",
|
| 156 |
+
nyc311: "live NYC 311 archive, trailing 5-year window (latest record in payload)",
|
| 157 |
+
nyc311_nta: "live NYC 311 archive, trailing 3-year window (polygon-aggregated)",
|
| 158 |
+
microtopo: "USGS 3DEP DEM (NYC LiDAR collect, ~2018) + derived HAND/TWI",
|
| 159 |
+
microtopo_nta: "USGS 3DEP DEM (NYC ~2018) — polygon-aggregated stats",
|
| 160 |
+
ida_hwm: "USGS Short-Term Network Event 312 — Hurricane Ida 2021 high-water marks (Sept 1-2 2021 survey)",
|
| 161 |
+
prithvi_water: "Prithvi-EO 2.0 satellite segmentation, scenes 2021-08-25 (pre) & 2021-09-02 (post Ida)",
|
| 162 |
+
prithvi_live: "live Sentinel-2 L2A scene from Microsoft Planetary Computer (acquisition timestamp in payload)",
|
| 163 |
+
terramind_synthetic: "synthetic prior — TerraMind 1.0 base generated a plausible categorical land-cover map from the LiDAR terrain at this point (deterministic seed, 10 diffusion steps; class fractions cite-able; not a measurement)",
|
| 164 |
+
gliner_comptroller: "GLiNER typed extraction over the Comptroller PDF (per-paragraph)",
|
| 165 |
+
gliner_dep_2013: "GLiNER typed extraction over the DEP wastewater plan",
|
| 166 |
+
gliner_nycha: "GLiNER typed extraction over the NYCHA Lessons Learned PDF",
|
| 167 |
+
gliner_mta: "GLiNER typed extraction over the MTA Resilience Roadmap",
|
| 168 |
+
gliner_coned: "GLiNER typed extraction over the Con Edison Climate Resilience plan",
|
| 169 |
+
noaa_tides: "live NOAA CO-OPS, 6-min cadence (observation time in payload)",
|
| 170 |
+
nws_alerts: "live NWS Public Alerts API (effective/expires in payload)",
|
| 171 |
+
nws_obs: "live NWS hourly METAR observation (observation time in payload)",
|
| 172 |
+
ttm_forecast: "live TTM forecast based on trailing 51 h at the closest NOAA gauge to this address (Battery / Kings Pt / Sandy Hook)",
|
| 173 |
+
ttm_311_forecast: "live TTM forecast based on trailing 52 weeks of NYC 311 flood complaints within 200 m of this address",
|
| 174 |
+
floodnet_forecast: "live TTM forecast based on the 512-day daily flood-event series at the nearest FloodNet sensor",
|
| 175 |
+
dob_permits: "live NYC DOB Permit Issuance, trailing 18-month window (per-permit issuance dates in payload)",
|
| 176 |
+
rag_comptroller: "NYC Comptroller report 'Is NYC Ready for Rain?' (2024)",
|
| 177 |
+
rag_npcc4: "NPCC4 — NYC Climate Assessment 4th edition, Annals NYAS vol. 1539 (2024)",
|
| 178 |
+
rag_mta: "MTA Climate Resilience Roadmap, October 2025 update",
|
| 179 |
+
rag_nycha: "NYCHA Flood Resilience: Lessons Learned (post-Sandy)",
|
| 180 |
+
rag_coned: "Con Edison Climate Change Resilience Plan, NY PSC Case 22-E-0222 (2023)",
|
| 181 |
+
scope_note: "Riprap planner — geographic scope guard (this query)",
|
| 182 |
+
live_target: "Riprap planner — live target (this query)",
|
| 183 |
+
mta_entrance: "MTA Open Data subway-entrance geometry (refreshed monthly) joined to Sandy 2012 + DEP scenarios + USGS 3DEP DEM",
|
| 184 |
+
nycha_dev: "NYC Open Data NYCHA Developments (phvi-damg) joined to Sandy 2012 + DEP scenarios + USGS 3DEP DEM",
|
| 185 |
+
doe_school: "NYC DOE Locations Points (1992 schools) joined to Sandy 2012 + DEP scenarios + USGS 3DEP DEM",
|
| 186 |
+
nyc_hospital: "NYS DOH Health Facility Certification (vn5v-hh5r, NYC counties + fac_desc_short=HOSP) joined to Sandy 2012 + DEP scenarios + USGS 3DEP DEM",
|
| 187 |
+
};
|
| 188 |
+
|
| 189 |
+
const INTENT_PILL_CLASS = {
|
| 190 |
+
development_check: "dev",
|
| 191 |
+
live_now: "live",
|
| 192 |
+
neighborhood: "nbhd",
|
| 193 |
+
single_address: "addr",
|
| 194 |
+
};
|
| 195 |
+
|
| 196 |
+
// ---------------------------------------------------------------------------
|
| 197 |
+
// MAP
|
| 198 |
+
// ---------------------------------------------------------------------------
|
| 199 |
+
|
| 200 |
+
let map = null;
|
| 201 |
+
let mapInit = false;
|
| 202 |
+
|
| 203 |
+
function ensureMap() {
|
| 204 |
+
if (mapInit) return;
|
| 205 |
+
mapInit = true;
|
| 206 |
+
map = new maplibregl.Map({
|
| 207 |
+
container: "map",
|
| 208 |
+
style: {
|
| 209 |
+
version: 8,
|
| 210 |
+
// CARTO Voyager — more editorial typography + softer palette than
|
| 211 |
+
// Positron, no API key required. Retina (@2x) tiles for crisp type.
|
| 212 |
+
sources: {
|
| 213 |
+
basemap: {
|
| 214 |
+
type: "raster",
|
| 215 |
+
tiles: [
|
| 216 |
+
"https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png",
|
| 217 |
+
"https://b.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png",
|
| 218 |
+
"https://c.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png",
|
| 219 |
+
"https://d.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png",
|
| 220 |
+
],
|
| 221 |
+
tileSize: 256,
|
| 222 |
+
attribution: "© OpenStreetMap contributors © CARTO",
|
| 223 |
+
},
|
| 224 |
+
},
|
| 225 |
+
layers: [
|
| 226 |
+
{ id: "bg", type: "background", paint: { "background-color": "#f3f5f8" } },
|
| 227 |
+
{ id: "basemap", type: "raster", source: "basemap" },
|
| 228 |
+
],
|
| 229 |
+
},
|
| 230 |
+
center: [-74.0, 40.72],
|
| 231 |
+
zoom: 10,
|
| 232 |
+
attributionControl: { compact: true },
|
| 233 |
+
// Required for map.getCanvas().toDataURL() to work on the report-export
|
| 234 |
+
// path. Otherwise the WebGL drawing buffer is cleared after each frame
|
| 235 |
+
// and snapshots come back blank.
|
| 236 |
+
preserveDrawingBuffer: true,
|
| 237 |
+
});
|
| 238 |
+
map.addControl(new maplibregl.NavigationControl({ visualizePitch: false }), "top-right");
|
| 239 |
+
map.on("load", initMapSources);
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
function initMapSources() {
|
| 243 |
+
// Sandy + DEP overlays (used for nbhd / dev_check)
|
| 244 |
+
map.addSource("sandy", { type: "geojson", data: empty() });
|
| 245 |
+
map.addLayer({ id: "sandy-fill", type: "fill", source: "sandy",
|
| 246 |
+
paint: { "fill-color": "#fc5d52", "fill-opacity": 0.25 } });
|
| 247 |
+
map.addLayer({ id: "sandy-line", type: "line", source: "sandy",
|
| 248 |
+
paint: { "line-color": "#fc5d52", "line-width": 0.5, "line-opacity": 0.7 } });
|
| 249 |
+
|
| 250 |
+
map.addSource("dep", { type: "geojson", data: empty() });
|
| 251 |
+
map.addLayer({ id: "dep-fill", type: "fill", source: "dep",
|
| 252 |
+
paint: {
|
| 253 |
+
"fill-color": ["match", ["get", "Flooding_Category"],
|
| 254 |
+
1, "#568adf", 2, "#1642DF", 3, "#031553", "#568adf"],
|
| 255 |
+
"fill-opacity": 0.32 } });
|
| 256 |
+
|
| 257 |
+
// Prithvi-EO 2.0 live water-segmentation polygons. Cyan to differ
|
| 258 |
+
// visually from Sandy (red) and DEP (blue) — this is *observed today*
|
| 259 |
+
// water from the latest cloud-free Sentinel-2 scene, not a modeled
|
| 260 |
+
// scenario. We outline + fill so even sliver geometries (river edges,
|
| 261 |
+
// canal banks) show up at street zoom.
|
| 262 |
+
map.addSource("prithvi_live", { type: "geojson", data: empty() });
|
| 263 |
+
map.addLayer({ id: "prithvi-live-fill", type: "fill", source: "prithvi_live",
|
| 264 |
+
paint: { "fill-color": "#48c6eb", "fill-opacity": 0.45 } });
|
| 265 |
+
map.addLayer({ id: "prithvi-live-line", type: "line", source: "prithvi_live",
|
| 266 |
+
paint: { "line-color": "#1aa3c8", "line-width": 1.2, "line-opacity": 0.85 } });
|
| 267 |
+
|
| 268 |
+
// TerraMind synthesised LULC polygons — *synthetic-prior* tier
|
| 269 |
+
// (4th epistemic class). Per-feature fill_color carried from the
|
| 270 |
+
// server side so the legend stays in one place. Dashed outline so
|
| 271 |
+
// it visually reads as "synthesized, not observed".
|
| 272 |
+
map.addSource("terramind_lulc", { type: "geojson", data: empty() });
|
| 273 |
+
map.addLayer({ id: "terramind-lulc-fill", type: "fill",
|
| 274 |
+
source: "terramind_lulc",
|
| 275 |
+
paint: {
|
| 276 |
+
"fill-color": ["coalesce", ["get", "fill_color"], "#9ca3af"],
|
| 277 |
+
"fill-opacity": 0.30,
|
| 278 |
+
},
|
| 279 |
+
});
|
| 280 |
+
map.addLayer({ id: "terramind-lulc-line", type: "line",
|
| 281 |
+
source: "terramind_lulc",
|
| 282 |
+
paint: {
|
| 283 |
+
"line-color": ["coalesce", ["get", "fill_color"], "#9ca3af"],
|
| 284 |
+
"line-width": 1.0,
|
| 285 |
+
"line-dasharray": [2, 2],
|
| 286 |
+
"line-opacity": 0.65,
|
| 287 |
+
},
|
| 288 |
+
});
|
| 289 |
+
map.on("click", "terramind-lulc-fill", (e) => {
|
| 290 |
+
const f = e.features[0]; const p = f.properties;
|
| 291 |
+
new maplibregl.Popup().setLngLat(e.lngLat)
|
| 292 |
+
.setHTML(`<b>TerraMind synthetic land-cover</b><br>` +
|
| 293 |
+
`Class: ${escapeHtml(p.label || "")} (tentative)<br>` +
|
| 294 |
+
`<i>Synthesised from LiDAR DEM, not observed.</i>`)
|
| 295 |
+
.addTo(map);
|
| 296 |
+
});
|
| 297 |
+
|
| 298 |
+
// NTA polygon outline
|
| 299 |
+
map.addSource("nta", { type: "geojson", data: empty() });
|
| 300 |
+
map.addLayer({ id: "nta-line", type: "line", source: "nta",
|
| 301 |
+
paint: { "line-color": "#0b3b6b", "line-width": 2.4, "line-opacity": 0.9 } });
|
| 302 |
+
map.addLayer({ id: "nta-fill", type: "fill", source: "nta",
|
| 303 |
+
paint: { "fill-color": "#0b3b6b", "fill-opacity": 0.04 } });
|
| 304 |
+
|
| 305 |
+
// DOB permit pins
|
| 306 |
+
map.addSource("permits", { type: "geojson", data: empty() });
|
| 307 |
+
map.addLayer({ id: "permits-circles", type: "circle", source: "permits",
|
| 308 |
+
paint: {
|
| 309 |
+
"circle-radius": ["case", ["get", "any_flood"], 6, 4],
|
| 310 |
+
"circle-color": [
|
| 311 |
+
"case",
|
| 312 |
+
["get", "in_sandy"], "#fc5d52",
|
| 313 |
+
[">=", ["get", "dep_max_class"], 2], "#1642DF",
|
| 314 |
+
[">", ["get", "dep_max_class"], 0], "#568adf",
|
| 315 |
+
"#1a8754",
|
| 316 |
+
],
|
| 317 |
+
"circle-stroke-color": "#ffffff",
|
| 318 |
+
"circle-stroke-width": 1.4,
|
| 319 |
+
"circle-opacity": 0.95,
|
| 320 |
+
} });
|
| 321 |
+
map.on("click", "permits-circles", (e) => {
|
| 322 |
+
const f = e.features[0]; const p = f.properties;
|
| 323 |
+
new maplibregl.Popup()
|
| 324 |
+
.setLngLat(f.geometry.coordinates)
|
| 325 |
+
.setHTML(
|
| 326 |
+
`<b>${escapeHtml(p.address || "(unknown)")}</b><br>` +
|
| 327 |
+
`${p.job_type} · ${p.in_sandy === 'true' ? 'Sandy zone' : 'outside Sandy'}<br>` +
|
| 328 |
+
`DEP class: ${p.dep_max_class}`)
|
| 329 |
+
.addTo(map);
|
| 330 |
+
});
|
| 331 |
+
|
| 332 |
+
// Address pin (single_address intent)
|
| 333 |
+
map.addSource("addr", { type: "geojson", data: empty() });
|
| 334 |
+
map.addLayer({ id: "addr-pin", type: "circle", source: "addr",
|
| 335 |
+
paint: { "circle-radius": 10, "circle-color": "#0b3b6b",
|
| 336 |
+
"circle-stroke-color": "#fff", "circle-stroke-width": 3 } });
|
| 337 |
+
|
| 338 |
+
// Search-radius circles (200 m / 600 m / 800 m). Visualizes the
|
| 339 |
+
// spatial scope each specialist is reading from. Drawn as a thin
|
| 340 |
+
// line so the underlying point data is readable through them.
|
| 341 |
+
map.addSource("scope", { type: "geojson", data: empty() });
|
| 342 |
+
map.addLayer({ id: "scope-line", type: "line", source: "scope",
|
| 343 |
+
paint: { "line-color": "#0b3b6b", "line-width": 1.0,
|
| 344 |
+
"line-opacity": 0.55, "line-dasharray": [3, 3] } });
|
| 345 |
+
|
| 346 |
+
// NYC 311 flood complaint pins — coloured by descriptor.
|
| 347 |
+
map.addSource("nyc311_pts", { type: "geojson", data: empty() });
|
| 348 |
+
map.addLayer({ id: "nyc311-circles", type: "circle", source: "nyc311_pts",
|
| 349 |
+
paint: {
|
| 350 |
+
"circle-radius": 4.5,
|
| 351 |
+
"circle-color": ["match", ["get", "descriptor"],
|
| 352 |
+
"Sewer Backup (Use Comments) (SA)", "#fc5d52",
|
| 353 |
+
"Catch Basin Clogged/Flooding (Use Comments) (SC)", "#f59e0b",
|
| 354 |
+
"Street Flooding (SJ)", "#1642DF",
|
| 355 |
+
"Manhole Overflow (Use Comments) (SA1)", "#8b5cf6",
|
| 356 |
+
"#6b7280",
|
| 357 |
+
],
|
| 358 |
+
"circle-stroke-color": "#ffffff",
|
| 359 |
+
"circle-stroke-width": 1.0,
|
| 360 |
+
"circle-opacity": 0.85,
|
| 361 |
+
},
|
| 362 |
+
});
|
| 363 |
+
map.on("click", "nyc311-circles", (e) => {
|
| 364 |
+
const f = e.features[0]; const p = f.properties;
|
| 365 |
+
new maplibregl.Popup().setLngLat(f.geometry.coordinates)
|
| 366 |
+
.setHTML(`<b>311 complaint</b><br>${escapeHtml(p.descriptor || "")}<br>` +
|
| 367 |
+
`${escapeHtml(p.date || "")}<br>${escapeHtml(p.address || "")}`)
|
| 368 |
+
.addTo(map);
|
| 369 |
+
});
|
| 370 |
+
|
| 371 |
+
// FloodNet sensors — triangles via SDF circle stand-in (cyan,
|
| 372 |
+
// larger if the sensor has triggered events).
|
| 373 |
+
map.addSource("floodnet_pts", { type: "geojson", data: empty() });
|
| 374 |
+
map.addLayer({ id: "floodnet-circles", type: "circle", source: "floodnet_pts",
|
| 375 |
+
paint: {
|
| 376 |
+
"circle-radius": 7,
|
| 377 |
+
"circle-color": "#48c6eb",
|
| 378 |
+
"circle-stroke-color": "#1aa3c8",
|
| 379 |
+
"circle-stroke-width": 2.0,
|
| 380 |
+
"circle-opacity": 0.95,
|
| 381 |
+
},
|
| 382 |
+
});
|
| 383 |
+
map.on("click", "floodnet-circles", (e) => {
|
| 384 |
+
const f = e.features[0]; const p = f.properties;
|
| 385 |
+
new maplibregl.Popup().setLngLat(f.geometry.coordinates)
|
| 386 |
+
.setHTML(`<b>FloodNet sensor</b><br>${escapeHtml(p.name || p.deployment_id || "")}`)
|
| 387 |
+
.addTo(map);
|
| 388 |
+
});
|
| 389 |
+
|
| 390 |
+
// USGS Hurricane Ida 2021 high-water marks — hot orange, sized by height.
|
| 391 |
+
map.addSource("ida_hwm_pts", { type: "geojson", data: empty() });
|
| 392 |
+
map.addLayer({ id: "ida-hwm-circles", type: "circle", source: "ida_hwm_pts",
|
| 393 |
+
paint: {
|
| 394 |
+
"circle-radius": ["interpolate", ["linear"],
|
| 395 |
+
["coalesce", ["get", "height_above_gnd_ft"], 0],
|
| 396 |
+
0, 4, 3, 7, 6, 11],
|
| 397 |
+
"circle-color": "#ea580c",
|
| 398 |
+
"circle-stroke-color": "#7c2d12",
|
| 399 |
+
"circle-stroke-width": 1.4,
|
| 400 |
+
"circle-opacity": 0.92,
|
| 401 |
+
},
|
| 402 |
+
});
|
| 403 |
+
map.on("click", "ida-hwm-circles", (e) => {
|
| 404 |
+
const f = e.features[0]; const p = f.properties;
|
| 405 |
+
new maplibregl.Popup().setLngLat(f.geometry.coordinates)
|
| 406 |
+
.setHTML(`<b>USGS Ida 2021 high-water mark</b><br>` +
|
| 407 |
+
`${escapeHtml(p.site || "(unnamed)")}<br>` +
|
| 408 |
+
`Elevation: ${p.elev_ft ?? "?"} ft<br>` +
|
| 409 |
+
`Height above ground: ${p.height_above_gnd_ft ?? "?"} ft`)
|
| 410 |
+
.addTo(map);
|
| 411 |
+
});
|
| 412 |
+
|
| 413 |
+
// NOAA tide gauge marker — shows which of the 3 gauges is active.
|
| 414 |
+
map.addSource("noaa_gauge", { type: "geojson", data: empty() });
|
| 415 |
+
map.addLayer({ id: "noaa-gauge-marker", type: "circle", source: "noaa_gauge",
|
| 416 |
+
paint: {
|
| 417 |
+
"circle-radius": 9,
|
| 418 |
+
"circle-color": "#0ea5e9",
|
| 419 |
+
"circle-stroke-color": "#fff",
|
| 420 |
+
"circle-stroke-width": 2.5,
|
| 421 |
+
},
|
| 422 |
+
});
|
| 423 |
+
map.on("click", "noaa-gauge-marker", (e) => {
|
| 424 |
+
const f = e.features[0]; const p = f.properties;
|
| 425 |
+
new maplibregl.Popup().setLngLat(f.geometry.coordinates)
|
| 426 |
+
.setHTML(`<b>NOAA tide gauge</b><br>${escapeHtml(p.name || "")}<br>` +
|
| 427 |
+
`Observed water level: ${p.observed_ft ?? "?"} ft MLLW<br>` +
|
| 428 |
+
`Residual (≈ surge): ${p.residual_ft ?? "?"} ft`)
|
| 429 |
+
.addTo(map);
|
| 430 |
+
});
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
// ~3 m/° latitude × cos(lat) for longitude. Build a circle polygon
|
| 434 |
+
// approximating a fixed-radius (meters) buffer around (lat, lon).
|
| 435 |
+
function metersBuffer(lat, lon, meters, steps = 64) {
|
| 436 |
+
const dLat = meters / 111_000.0;
|
| 437 |
+
const dLon = meters / (111_000.0 * Math.cos(lat * Math.PI / 180));
|
| 438 |
+
const ring = [];
|
| 439 |
+
for (let i = 0; i <= steps; i++) {
|
| 440 |
+
const a = (i / steps) * 2 * Math.PI;
|
| 441 |
+
ring.push([lon + dLon * Math.cos(a), lat + dLat * Math.sin(a)]);
|
| 442 |
+
}
|
| 443 |
+
return { type: "Polygon", coordinates: [ring] };
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
function empty() { return { type: "FeatureCollection", features: [] }; }
|
| 447 |
+
|
| 448 |
+
function clearMap() {
|
| 449 |
+
if (!map || !map.getSource) return;
|
| 450 |
+
for (const id of ["sandy", "dep", "nta", "permits", "addr", "prithvi_live",
|
| 451 |
+
"terramind_lulc",
|
| 452 |
+
"scope", "nyc311_pts", "floodnet_pts", "ida_hwm_pts",
|
| 453 |
+
"noaa_gauge"]) {
|
| 454 |
+
const s = map.getSource(id);
|
| 455 |
+
if (s) s.setData(empty());
|
| 456 |
+
}
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
async function fillMapForFinal(d) {
|
| 460 |
+
if (!map || !map.loaded()) {
|
| 461 |
+
map.once("load", () => fillMapForFinal(d));
|
| 462 |
+
return;
|
| 463 |
+
}
|
| 464 |
+
clearMap();
|
| 465 |
+
const intent = d.intent;
|
| 466 |
+
if (intent === "single_address") return fillMapAddress(d);
|
| 467 |
+
if (intent === "neighborhood") return fillMapNeighborhood(d);
|
| 468 |
+
if (intent === "development_check") return fillMapDevelopment(d);
|
| 469 |
+
if (intent === "live_now") return fillMapLive(d);
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
async function fillMapAddress(d) {
|
| 473 |
+
const geo = d.geocode;
|
| 474 |
+
if (!geo || !geo.lat) return;
|
| 475 |
+
map.flyTo({ center: [geo.lon, geo.lat], zoom: 15.5, duration: 700 });
|
| 476 |
+
map.getSource("addr").setData({ type: "FeatureCollection",
|
| 477 |
+
features: [{ type: "Feature",
|
| 478 |
+
geometry: { type: "Point", coordinates: [geo.lon, geo.lat] }, properties: {} }] });
|
| 479 |
+
// Fetch Sandy + DEP layers clipped to address
|
| 480 |
+
try {
|
| 481 |
+
const r = await fetch(`/api/layers/sandy?lat=${geo.lat}&lon=${geo.lon}&r=1500`);
|
| 482 |
+
map.getSource("sandy").setData(await r.json());
|
| 483 |
+
} catch {}
|
| 484 |
+
try {
|
| 485 |
+
const r = await fetch(`/api/layers/dep_extreme_2080?lat=${geo.lat}&lon=${geo.lon}&r=1500`);
|
| 486 |
+
map.getSource("dep").setData(await r.json());
|
| 487 |
+
} catch {}
|
| 488 |
+
// Prithvi-EO live water mask comes inlined in the SSE final event,
|
| 489 |
+
// not via a separate /api/layers fetch — it's per-query, not corpus.
|
| 490 |
+
const live = d.prithvi_live;
|
| 491 |
+
if (live && live.ok && live.polygons_geojson && map.getSource("prithvi_live")) {
|
| 492 |
+
map.getSource("prithvi_live").setData(live.polygons_geojson);
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
// TerraMind synthesised LULC polygons — same per-query pattern.
|
| 496 |
+
const tm = d.terramind;
|
| 497 |
+
if (tm && tm.ok && tm.polygons_geojson && map.getSource("terramind_lulc")) {
|
| 498 |
+
map.getSource("terramind_lulc").setData(tm.polygons_geojson);
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
// ---- search-radius scope rings (200 m / 600 m / 800 m) ----
|
| 502 |
+
// Three rings matching the buffers each specialist actually reads:
|
| 503 |
+
// 200 m for 311, 600 m for FloodNet sensors, 800 m for Ida HWMs.
|
| 504 |
+
if (map.getSource("scope")) {
|
| 505 |
+
map.getSource("scope").setData({
|
| 506 |
+
type: "FeatureCollection",
|
| 507 |
+
features: [200, 600, 800].map(r => ({
|
| 508 |
+
type: "Feature",
|
| 509 |
+
geometry: metersBuffer(geo.lat, geo.lon, r),
|
| 510 |
+
properties: { radius_m: r },
|
| 511 |
+
})),
|
| 512 |
+
});
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
// ---- NYC 311 flood complaint pins ----
|
| 516 |
+
const c311 = d.nyc311 || {};
|
| 517 |
+
const c311Pts = c311.points || [];
|
| 518 |
+
if (map.getSource("nyc311_pts")) {
|
| 519 |
+
map.getSource("nyc311_pts").setData({
|
| 520 |
+
type: "FeatureCollection",
|
| 521 |
+
features: c311Pts.filter(p => p.lat && p.lon).map(p => ({
|
| 522 |
+
type: "Feature",
|
| 523 |
+
geometry: { type: "Point", coordinates: [p.lon, p.lat] },
|
| 524 |
+
properties: {
|
| 525 |
+
descriptor: p.descriptor || "",
|
| 526 |
+
date: p.date || "",
|
| 527 |
+
address: p.address || "",
|
| 528 |
+
},
|
| 529 |
+
})),
|
| 530 |
+
});
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
// ---- FloodNet sensors ----
|
| 534 |
+
const fn = d.floodnet || {};
|
| 535 |
+
const fnSensors = fn.sensors || [];
|
| 536 |
+
if (map.getSource("floodnet_pts")) {
|
| 537 |
+
map.getSource("floodnet_pts").setData({
|
| 538 |
+
type: "FeatureCollection",
|
| 539 |
+
features: fnSensors.filter(s => s.lat && s.lon).map(s => ({
|
| 540 |
+
type: "Feature",
|
| 541 |
+
geometry: { type: "Point", coordinates: [s.lon, s.lat] },
|
| 542 |
+
properties: {
|
| 543 |
+
name: s.name || s.deployment_id || "",
|
| 544 |
+
deployment_id: s.deployment_id || "",
|
| 545 |
+
},
|
| 546 |
+
})),
|
| 547 |
+
});
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
// ---- USGS Ida 2021 HWMs ----
|
| 551 |
+
const hwm = d.ida_hwm || {};
|
| 552 |
+
const hwmPts = hwm.points || [];
|
| 553 |
+
if (map.getSource("ida_hwm_pts")) {
|
| 554 |
+
map.getSource("ida_hwm_pts").setData({
|
| 555 |
+
type: "FeatureCollection",
|
| 556 |
+
features: hwmPts.filter(p => p.lat && p.lon).map(p => ({
|
| 557 |
+
type: "Feature",
|
| 558 |
+
geometry: { type: "Point", coordinates: [p.lon, p.lat] },
|
| 559 |
+
properties: {
|
| 560 |
+
site: p.site || "",
|
| 561 |
+
elev_ft: p.elev_ft,
|
| 562 |
+
height_above_gnd_ft: p.height_above_gnd_ft,
|
| 563 |
+
},
|
| 564 |
+
})),
|
| 565 |
+
});
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
// ---- NOAA tide gauge marker ----
|
| 569 |
+
const tides = d.noaa_tides || {};
|
| 570 |
+
if (tides.station_id && tides.station_lat && tides.station_lon &&
|
| 571 |
+
map.getSource("noaa_gauge")) {
|
| 572 |
+
map.getSource("noaa_gauge").setData({
|
| 573 |
+
type: "FeatureCollection",
|
| 574 |
+
features: [{
|
| 575 |
+
type: "Feature",
|
| 576 |
+
geometry: { type: "Point",
|
| 577 |
+
coordinates: [tides.station_lon, tides.station_lat] },
|
| 578 |
+
properties: {
|
| 579 |
+
name: tides.station_name || tides.station_id,
|
| 580 |
+
observed_ft: tides.observed_ft_mllw,
|
| 581 |
+
residual_ft: tides.residual_ft,
|
| 582 |
+
},
|
| 583 |
+
}],
|
| 584 |
+
});
|
| 585 |
+
}
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
async function fillMapNeighborhood(d) {
|
| 589 |
+
const t = d.target;
|
| 590 |
+
if (!t || !t.bbox || !t.nta_code) return;
|
| 591 |
+
const [minx, miny, maxx, maxy] = t.bbox;
|
| 592 |
+
map.fitBounds([[minx, miny], [maxx, maxy]], { padding: 32, duration: 700 });
|
| 593 |
+
const [r1, r2, r3] = await Promise.all([
|
| 594 |
+
fetch(`/api/layers/nta?code=${t.nta_code}`).then(r => r.json()),
|
| 595 |
+
fetch(`/api/layers/sandy_clipped?code=${t.nta_code}`).then(r => r.json()).catch(() => empty()),
|
| 596 |
+
fetch(`/api/layers/dep_clipped?code=${t.nta_code}&scenario=dep_extreme_2080`).then(r => r.json()).catch(() => empty()),
|
| 597 |
+
]);
|
| 598 |
+
map.getSource("nta").setData(r1);
|
| 599 |
+
map.getSource("sandy").setData(r2);
|
| 600 |
+
map.getSource("dep").setData(r3);
|
| 601 |
+
// Prithvi-EO live water mask (NTA centroid) — same per-query GeoJSON
|
| 602 |
+
// as the single_address path; clipped visually to the NTA polygon by
|
| 603 |
+
// the basemap zoom.
|
| 604 |
+
const live = d.prithvi_live;
|
| 605 |
+
if (live && live.ok && live.polygons_geojson && map.getSource("prithvi_live")) {
|
| 606 |
+
map.getSource("prithvi_live").setData(live.polygons_geojson);
|
| 607 |
+
}
|
| 608 |
+
// TerraMind synthesised LULC at NTA centroid.
|
| 609 |
+
const tm = d.terramind;
|
| 610 |
+
if (tm && tm.ok && tm.polygons_geojson && map.getSource("terramind_lulc")) {
|
| 611 |
+
map.getSource("terramind_lulc").setData(tm.polygons_geojson);
|
| 612 |
+
}
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
async function fillMapDevelopment(d) {
|
| 616 |
+
await fillMapNeighborhood(d); // same NTA + Sandy + DEP overlays
|
| 617 |
+
const pins = ((d.dob_summary || {}).all_pins) || [];
|
| 618 |
+
const fc = {
|
| 619 |
+
type: "FeatureCollection",
|
| 620 |
+
features: pins.filter(p => p.lat && p.lon).map(p => ({
|
| 621 |
+
type: "Feature",
|
| 622 |
+
geometry: { type: "Point", coordinates: [p.lon, p.lat] },
|
| 623 |
+
properties: {
|
| 624 |
+
address: p.address, job_type: p.job_type,
|
| 625 |
+
in_sandy: !!p.in_sandy, any_flood: !!p.any_flood,
|
| 626 |
+
dep_max_class: p.dep_max_class || 0,
|
| 627 |
+
},
|
| 628 |
+
})),
|
| 629 |
+
};
|
| 630 |
+
map.getSource("permits").setData(fc);
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
function fillMapLive(d) {
|
| 634 |
+
// NYC overview with the 3 NOAA gauges
|
| 635 |
+
map.flyTo({ center: [-74.0, 40.7], zoom: 10, duration: 700 });
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
// Fire as each FSM step completes, so the map updates progressively
|
| 639 |
+
// instead of waiting for the `final` event. Each branch is idempotent —
|
| 640 |
+
// it's safe if `final` later overwrites with the same data.
|
| 641 |
+
async function incrementallyFillMap(step) {
|
| 642 |
+
if (!map || !map.loaded()) {
|
| 643 |
+
map.once("load", () => incrementallyFillMap(step));
|
| 644 |
+
return;
|
| 645 |
+
}
|
| 646 |
+
const r = step.result || {};
|
| 647 |
+
// Address mode — geocode just resolved
|
| 648 |
+
if (step.step === "geocode" && r.lat != null && r.lon != null) {
|
| 649 |
+
map.flyTo({ center: [r.lon, r.lat], zoom: 15.5, duration: 700 });
|
| 650 |
+
map.getSource("addr").setData({ type: "FeatureCollection",
|
| 651 |
+
features: [{ type: "Feature",
|
| 652 |
+
geometry: { type: "Point", coordinates: [r.lon, r.lat] }, properties: {} }] });
|
| 653 |
+
Promise.all([
|
| 654 |
+
fetch(`/api/layers/sandy?lat=${r.lat}&lon=${r.lon}&r=1500`).then(x => x.json()).catch(() => empty()),
|
| 655 |
+
fetch(`/api/layers/dep_extreme_2080?lat=${r.lat}&lon=${r.lon}&r=1500`).then(x => x.json()).catch(() => empty()),
|
| 656 |
+
]).then(([s, d]) => {
|
| 657 |
+
map.getSource("sandy").setData(s);
|
| 658 |
+
map.getSource("dep").setData(d);
|
| 659 |
+
});
|
| 660 |
+
return;
|
| 661 |
+
}
|
| 662 |
+
// Neighborhood / dev_check — NTA polygon resolved
|
| 663 |
+
if (step.step === "nta_resolve" && r.nta_code && r.bbox) {
|
| 664 |
+
const [minx, miny, maxx, maxy] = r.bbox;
|
| 665 |
+
map.fitBounds([[minx, miny], [maxx, maxy]], { padding: 32, duration: 700 });
|
| 666 |
+
Promise.all([
|
| 667 |
+
fetch(`/api/layers/nta?code=${r.nta_code}`).then(x => x.json()).catch(() => empty()),
|
| 668 |
+
fetch(`/api/layers/sandy_clipped?code=${r.nta_code}`).then(x => x.json()).catch(() => empty()),
|
| 669 |
+
fetch(`/api/layers/dep_clipped?code=${r.nta_code}&scenario=dep_extreme_2080`).then(x => x.json()).catch(() => empty()),
|
| 670 |
+
]).then(([n, s, d]) => {
|
| 671 |
+
map.getSource("nta").setData(n);
|
| 672 |
+
map.getSource("sandy").setData(s);
|
| 673 |
+
map.getSource("dep").setData(d);
|
| 674 |
+
});
|
| 675 |
+
return;
|
| 676 |
+
}
|
| 677 |
+
// Dev_check — DOB permits arrived; pin them now
|
| 678 |
+
if (step.step === "dob_permits_nta" && Array.isArray(r.all_pins)) {
|
| 679 |
+
const fc = { type: "FeatureCollection",
|
| 680 |
+
features: r.all_pins.filter(p => p.lat && p.lon).map(p => ({
|
| 681 |
+
type: "Feature",
|
| 682 |
+
geometry: { type: "Point", coordinates: [p.lon, p.lat] },
|
| 683 |
+
properties: {
|
| 684 |
+
address: p.address, job_type: p.job_type,
|
| 685 |
+
in_sandy: !!p.in_sandy, any_flood: !!p.any_flood,
|
| 686 |
+
dep_max_class: p.dep_max_class || 0,
|
| 687 |
+
},
|
| 688 |
+
})) };
|
| 689 |
+
map.getSource("permits").setData(fc);
|
| 690 |
+
return;
|
| 691 |
+
}
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
// ---------------------------------------------------------------------------
|
| 695 |
+
// REPORT (paragraph) RENDERING
|
| 696 |
+
// ---------------------------------------------------------------------------
|
| 697 |
+
|
| 698 |
+
function escapeHtml(s) {
|
| 699 |
+
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
let CITE_INDEX = {};
|
| 703 |
+
// Resolve a doc_id to its source-label family. Register specialists emit
|
| 704 |
+
// per-asset doc_ids like `mta_entrance_54` / `nycha_dev_004` — for those
|
| 705 |
+
// we strip the trailing `_<id>` and look up the family key.
|
| 706 |
+
const _FAMILY_PREFIXES = ["mta_entrance", "nycha_dev", "doe_school", "nyc_hospital"];
|
| 707 |
+
function _docIdFamily(norm) {
|
| 708 |
+
for (const fam of _FAMILY_PREFIXES) {
|
| 709 |
+
if (norm.startsWith(fam + "_")) return fam;
|
| 710 |
+
}
|
| 711 |
+
return null;
|
| 712 |
+
}
|
| 713 |
+
function _resolveSourceLabel(norm) {
|
| 714 |
+
if (SOURCE_LABELS[norm]) return SOURCE_LABELS[norm];
|
| 715 |
+
const fam = _docIdFamily(norm);
|
| 716 |
+
return fam ? SOURCE_LABELS[fam] : norm;
|
| 717 |
+
}
|
| 718 |
+
function rewriteCitations(html) {
|
| 719 |
+
return html.replace(/\[([a-z0-9_]+)\]/gi, (_, id) => {
|
| 720 |
+
const norm = id.toLowerCase();
|
| 721 |
+
if (CITE_INDEX[norm] == null) CITE_INDEX[norm] = Object.keys(CITE_INDEX).length + 1;
|
| 722 |
+
const n = CITE_INDEX[norm];
|
| 723 |
+
return `<span class="cite" data-src-id="${norm}" data-src-n="${n}" title="${_resolveSourceLabel(norm)} — click to highlight in Sources">${n}</span>`;
|
| 724 |
+
});
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
// Sources footer is a Lit web component (<r-sources-footer>) — driven
|
| 728 |
+
// by the citeIndex signal in /static/components/signals.js. We feed
|
| 729 |
+
// it the labels/urls/vintages once at boot and update the signal as
|
| 730 |
+
// the briefing markdown is rendered.
|
| 731 |
+
async function renderSources() {
|
| 732 |
+
const el = document.getElementById("sourcesFooter");
|
| 733 |
+
if (!el) return;
|
| 734 |
+
// Module is loaded async; wait for define() then push fresh data.
|
| 735 |
+
await customElements.whenDefined("r-sources-footer");
|
| 736 |
+
el.labels = SOURCE_LABELS;
|
| 737 |
+
el.urls = SOURCE_URLS;
|
| 738 |
+
el.vintages = SOURCE_VINTAGES;
|
| 739 |
+
// Push the citation index into the shared signal — the component
|
| 740 |
+
// re-renders reactively.
|
| 741 |
+
const { citeIndex } = await import("/static/components/signals.js");
|
| 742 |
+
citeIndex.set({ ...CITE_INDEX });
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
function renderMarkdown(text) {
|
| 746 |
+
// Block recognizer:
|
| 747 |
+
// `**Header.**` (own line) → <h4>
|
| 748 |
+
// lines starting `- ` or `* ` → bullet items collected into <ul>
|
| 749 |
+
// anything else → <p>
|
| 750 |
+
// Inline `**foo**` → <strong>
|
| 751 |
+
const lines = text.split("\n");
|
| 752 |
+
const out = [];
|
| 753 |
+
let para = []; let bullets = [];
|
| 754 |
+
const flushPara = () => {
|
| 755 |
+
if (!para.length) return;
|
| 756 |
+
const safe = escapeHtml(para.join(" ").trim()).replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
| 757 |
+
if (safe) out.push(`<p class="rsum-p">${safe}</p>`);
|
| 758 |
+
para = [];
|
| 759 |
+
};
|
| 760 |
+
const flushBullets = () => {
|
| 761 |
+
if (!bullets.length) return;
|
| 762 |
+
const items = bullets.map(b => {
|
| 763 |
+
const safe = escapeHtml(b.trim()).replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
| 764 |
+
return `<li>${safe}</li>`;
|
| 765 |
+
}).join("");
|
| 766 |
+
out.push(`<ul class="rsum-list">${items}</ul>`);
|
| 767 |
+
bullets = [];
|
| 768 |
+
};
|
| 769 |
+
// Granite sometimes runs all bullets onto one line separated by " - ";
|
| 770 |
+
// pre-split those so each becomes its own bullet.
|
| 771 |
+
const expanded = [];
|
| 772 |
+
for (const line of lines) {
|
| 773 |
+
if (line.trim().startsWith("- ") && line.includes(" - ", 2)) {
|
| 774 |
+
// split into bullets
|
| 775 |
+
const parts = line.split(/(?:^|(?<=\.\s))\s*-\s+/g).filter(p => p.trim());
|
| 776 |
+
for (const p of parts) expanded.push("- " + p.trim());
|
| 777 |
+
} else {
|
| 778 |
+
expanded.push(line);
|
| 779 |
+
}
|
| 780 |
+
}
|
| 781 |
+
for (const line of expanded) {
|
| 782 |
+
const m = line.match(/^\s*\*\*([A-Z][A-Za-z\s/]+)\.\*\*\s*$/);
|
| 783 |
+
if (m) {
|
| 784 |
+
flushPara(); flushBullets();
|
| 785 |
+
out.push(`<h4 class="rsum-h">${escapeHtml(m[1])}</h4>`);
|
| 786 |
+
continue;
|
| 787 |
+
}
|
| 788 |
+
if (/^\s*[-*]\s+/.test(line)) {
|
| 789 |
+
flushPara();
|
| 790 |
+
bullets.push(line.replace(/^\s*[-*]\s+/, ""));
|
| 791 |
+
} else {
|
| 792 |
+
flushBullets();
|
| 793 |
+
para.push(line);
|
| 794 |
+
}
|
| 795 |
+
}
|
| 796 |
+
flushPara(); flushBullets();
|
| 797 |
+
return out.join("");
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
// Briefing is now the Lit <r-briefing> web component. It owns markdown
|
| 801 |
+
// rendering, citation chip binding, and pushing CITE_INDEX into the
|
| 802 |
+
// shared signal — agent.js just feeds it `.text` + `.sourceLabels`.
|
| 803 |
+
async function setBriefingText(text) {
|
| 804 |
+
const el = document.getElementById("paragraph");
|
| 805 |
+
if (!el) return;
|
| 806 |
+
await customElements.whenDefined("r-briefing");
|
| 807 |
+
el.sourceLabels = SOURCE_LABELS;
|
| 808 |
+
el.text = text || "";
|
| 809 |
+
}
|
| 810 |
+
function renderParagraph(text) { setBriefingText(text); }
|
| 811 |
+
|
| 812 |
+
// ---------------------------------------------------------------------------
|
| 813 |
+
// FACTS PANEL — intent-specific quick-look stats below the map
|
| 814 |
+
// ---------------------------------------------------------------------------
|
| 815 |
+
|
| 816 |
+
function renderFacts(d) {
|
| 817 |
+
const intent = d.intent;
|
| 818 |
+
const panel = $("#factsPanel");
|
| 819 |
+
const body = $("#factsBody");
|
| 820 |
+
panel.style.display = "";
|
| 821 |
+
if (intent === "neighborhood") renderNbhdFacts(d, body);
|
| 822 |
+
else if (intent === "development_check") renderDevFacts(d, body);
|
| 823 |
+
else if (intent === "live_now") renderLiveFacts(d, body);
|
| 824 |
+
else if (intent === "single_address") renderAddressFacts(d, body);
|
| 825 |
+
}
|
| 826 |
+
|
| 827 |
+
function renderNbhdFacts(d, body) {
|
| 828 |
+
$("#factsTitle").textContent = `Findings — ${d.target?.nta_name || ""}`;
|
| 829 |
+
const s = d.sandy_nta || {}; const dep = d.dep_nta || {};
|
| 830 |
+
const m = d.microtopo_nta || {}; const c = d.nyc311_nta || {};
|
| 831 |
+
const sandyPct = s.fraction != null ? (s.fraction * 100).toFixed(1) + "%" : "—";
|
| 832 |
+
const dep80 = (dep.dep_extreme_2080 || {}).fraction_any;
|
| 833 |
+
const dep50 = (dep.dep_moderate_2050 || {}).fraction_any;
|
| 834 |
+
body.innerHTML = `
|
| 835 |
+
<div class="headline-stat">${sandyPct}</div>
|
| 836 |
+
<div class="headline-sub">of the neighborhood is inside the 2012 Sandy Inundation Zone</div>
|
| 837 |
+
<dl class="facts-grid">
|
| 838 |
+
<dt>DEP Extreme 2080</dt><dd>${dep80!=null ? (dep80*100).toFixed(1)+"%" : "—"}</dd>
|
| 839 |
+
<dt>DEP Moderate 2050</dt><dd>${dep50!=null ? (dep50*100).toFixed(1)+"%" : "—"}</dd>
|
| 840 |
+
<dt>311 (3 yr)</dt><dd>${c.n ?? "—"} flood complaints</dd>
|
| 841 |
+
<dt>HAND median</dt><dd>${m.hand_median_m != null ? m.hand_median_m+" m" : "—"}</dd>
|
| 842 |
+
<dt>HAND < 1 m fraction</dt><dd>${m.frac_hand_lt1 != null ? (m.frac_hand_lt1*100).toFixed(0)+"%" : "—"}</dd>
|
| 843 |
+
<dt>TWI median</dt><dd>${m.twi_median ?? "—"}</dd>
|
| 844 |
+
</dl>`;
|
| 845 |
+
}
|
| 846 |
+
|
| 847 |
+
function renderDevFacts(d, body) {
|
| 848 |
+
$("#factsTitle").textContent = `Active construction — ${d.target?.nta_name || ""}`;
|
| 849 |
+
const ds = d.dob_summary || {};
|
| 850 |
+
body.innerHTML = `
|
| 851 |
+
<div class="headline-stat">${ds.n_in_sandy ?? 0} <span style="color:var(--text-muted); font-size:18px; font-weight:400;">/ ${ds.n_total ?? 0}</span></div>
|
| 852 |
+
<div class="headline-sub">active projects inside the Sandy zone</div>
|
| 853 |
+
<dl class="facts-grid">
|
| 854 |
+
<dt>Total active</dt><dd>${ds.n_total ?? 0}</dd>
|
| 855 |
+
<dt>In any DEP scenario</dt><dd>${ds.n_in_dep_any ?? 0}</dd>
|
| 856 |
+
<dt>In severe DEP (≥1 ft)</dt><dd>${ds.n_in_dep_severe ?? 0}</dd>
|
| 857 |
+
<dt>By job type</dt><dd>${Object.entries(ds.by_job_type || {}).map(([k,v]) => `${v} ${k}`).join(", ")}</dd>
|
| 858 |
+
</dl>`;
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
function renderLiveFacts(d, body) {
|
| 862 |
+
$("#factsTitle").textContent = `Live conditions — ${d.place || "NYC"}`;
|
| 863 |
+
const t = d.noaa_tides || {}; const a = d.nws_alerts || {}; const o = d.nws_obs || {};
|
| 864 |
+
const ttm = d.ttm_forecast || {};
|
| 865 |
+
const r = t.residual_ft;
|
| 866 |
+
body.innerHTML = `
|
| 867 |
+
<div class="headline-stat">${a.n_active ?? 0} alerts</div>
|
| 868 |
+
<div class="headline-sub">active flood-relevant NWS alerts at this point</div>
|
| 869 |
+
<dl class="facts-grid">
|
| 870 |
+
<dt>Tide gauge</dt><dd>${t.station_name || "—"}</dd>
|
| 871 |
+
<dt>Observed</dt><dd>${t.observed_ft_mllw != null ? t.observed_ft_mllw+" ft MLLW" : "—"}</dd>
|
| 872 |
+
<dt>Residual</dt><dd>${r != null ? (r >= 0 ? "+" : "")+r+" ft" : "—"}</dd>
|
| 873 |
+
<dt>Nearest ASOS</dt><dd>${o.station_id || "—"}</dd>
|
| 874 |
+
<dt>Precip 1h</dt><dd>${o.precip_last_hour_mm != null ? o.precip_last_hour_mm+" mm" : "—"}</dd>
|
| 875 |
+
<dt>TTM peak (next 9.6h)</dt><dd>${ttm.forecast_peak_ft != null ? ttm.forecast_peak_ft+" ft" : "—"}</dd>
|
| 876 |
+
</dl>`;
|
| 877 |
+
}
|
| 878 |
+
|
| 879 |
+
function renderAddressFacts(d, body) {
|
| 880 |
+
$("#factsTitle").textContent = "Findings";
|
| 881 |
+
const geo = d.geocode || {};
|
| 882 |
+
const dep = d.dep || {}; const e80 = (dep.dep_extreme_2080 || {});
|
| 883 |
+
const m = d.microtopo || {};
|
| 884 |
+
body.innerHTML = `
|
| 885 |
+
<div class="headline-sub">${geo.address || "—"}</div>
|
| 886 |
+
<dl class="facts-grid">
|
| 887 |
+
<dt>Sandy zone</dt><dd>${d.sandy ? "INSIDE" : "outside"}</dd>
|
| 888 |
+
<dt>DEP Extreme 2080</dt><dd>${e80.depth_label || "—"}</dd>
|
| 889 |
+
<dt>HAND</dt><dd>${m.hand_m != null ? m.hand_m+" m" : "—"}</dd>
|
| 890 |
+
<dt>TWI</dt><dd>${m.twi ?? "—"}</dd>
|
| 891 |
+
<dt>Elev pct (200m)</dt><dd>${m.rel_elev_pct_200m ?? "—"}</dd>
|
| 892 |
+
<dt>311 (5y, 200m)</dt><dd>${(d.nyc311 || {}).n ?? "—"}</dd>
|
| 893 |
+
</dl>`;
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
// ---------------------------------------------------------------------------
|
| 897 |
+
// TRACE PANEL
|
| 898 |
+
// ---------------------------------------------------------------------------
|
| 899 |
+
|
| 900 |
+
// Trace list is a Lit web component (<r-trace>); pushTraceStep delegates
|
| 901 |
+
// once the component is registered. STEP_LABELS is set on the element
|
| 902 |
+
// at boot.
|
| 903 |
+
async function pushTraceStep(step) {
|
| 904 |
+
const el = document.getElementById("steps");
|
| 905 |
+
if (!el) return;
|
| 906 |
+
await customElements.whenDefined("r-trace");
|
| 907 |
+
if (!el.stepLabels || !Object.keys(el.stepLabels).length) {
|
| 908 |
+
el.stepLabels = STEP_LABELS;
|
| 909 |
+
}
|
| 910 |
+
el.pushStep(step);
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
async function clearTrace() {
|
| 914 |
+
const el = document.getElementById("steps");
|
| 915 |
+
if (el) {
|
| 916 |
+
await customElements.whenDefined("r-trace");
|
| 917 |
+
el.clear();
|
| 918 |
+
}
|
| 919 |
+
$("#traceMeta").textContent = "";
|
| 920 |
+
}
|
| 921 |
+
|
| 922 |
+
// --------------------------------------------------------------------------
|
| 923 |
+
// Loading-state and chrome helpers
|
| 924 |
+
// --------------------------------------------------------------------------
|
| 925 |
+
|
| 926 |
+
function setMapLoading(text) {
|
| 927 |
+
const el = $("#mapLoading");
|
| 928 |
+
if (!el) return;
|
| 929 |
+
if (text) {
|
| 930 |
+
el.style.display = "";
|
| 931 |
+
$("#mapLoadingText").textContent = text;
|
| 932 |
+
} else {
|
| 933 |
+
el.style.display = "none";
|
| 934 |
+
}
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
function setLegend(intent) {
|
| 938 |
+
const el = $("#mapLegend");
|
| 939 |
+
if (!el) return;
|
| 940 |
+
// Reusable legend rows shared across intents.
|
| 941 |
+
const empirical = `
|
| 942 |
+
<div class="legend-row"><span class="legend-swatch fill" style="background:#fc5d52; opacity:0.4"></span>Sandy 2012 extent</div>
|
| 943 |
+
<div class="legend-row"><span class="legend-swatch fill" style="background:#1642DF; opacity:0.4"></span>DEP Extreme-2080</div>`;
|
| 944 |
+
const points = `
|
| 945 |
+
<div class="legend-row"><span class="legend-swatch" style="background:#fc5d52; border-radius:50%"></span>311 — sewer backup</div>
|
| 946 |
+
<div class="legend-row"><span class="legend-swatch" style="background:#f59e0b; border-radius:50%"></span>311 — catch basin</div>
|
| 947 |
+
<div class="legend-row"><span class="legend-swatch" style="background:#1642DF; border-radius:50%"></span>311 — street flooding</div>
|
| 948 |
+
<div class="legend-row"><span class="legend-swatch" style="background:#48c6eb; border:2px solid #1aa3c8; border-radius:50%"></span>FloodNet sensor</div>
|
| 949 |
+
<div class="legend-row"><span class="legend-swatch" style="background:#ea580c; border:1px solid #7c2d12; border-radius:50%"></span>Ida 2021 high-water mark</div>
|
| 950 |
+
<div class="legend-row"><span class="legend-swatch" style="background:#0ea5e9; border:2px solid #fff; border-radius:50%; outline:1px solid #ccc"></span>NOAA tide gauge</div>`;
|
| 951 |
+
// Synthetic-prior tier — distinct visual idiom (dashed) so users
|
| 952 |
+
// immediately read it as "generated, not observed".
|
| 953 |
+
const synthetic = `
|
| 954 |
+
<div style="font-weight:700; font-size:9.5px; text-transform:uppercase; letter-spacing:0.06em; color:var(--text-muted); margin-top:6px; margin-bottom:2px">Synthetic priors (not observed)</div>
|
| 955 |
+
<div class="legend-row"><span class="legend-swatch fill" style="background:#48c6eb; opacity:0.45"></span>Prithvi-EO 2.0 — live water mask</div>
|
| 956 |
+
<div class="legend-row"><span class="legend-swatch fill" style="background:#16a34a; opacity:0.30; border:1px dashed #16a34a"></span>TerraMind — synthetic LULC (DEM→ESRI Land Cover, dashed = generated)</div>`;
|
| 957 |
+
|
| 958 |
+
if (intent === "development_check") {
|
| 959 |
+
el.innerHTML = `
|
| 960 |
+
<div style="font-weight:700; font-size:9.5px; text-transform:uppercase; letter-spacing:0.06em; color:var(--text-muted); margin-bottom:4px">Active permits</div>
|
| 961 |
+
<div class="legend-row"><span class="legend-swatch" style="background:#fc5d52"></span>Inside Sandy zone</div>
|
| 962 |
+
<div class="legend-row"><span class="legend-swatch" style="background:#1642DF"></span>DEP deep band (≥1 ft)</div>
|
| 963 |
+
<div class="legend-row"><span class="legend-swatch" style="background:#568adf"></span>DEP nuisance band</div>
|
| 964 |
+
<div class="legend-row"><span class="legend-swatch" style="background:#1a8754"></span>No flood layer</div>
|
| 965 |
+
<div class="legend-row" style="margin-top:6px">${empirical}</div>${synthetic}`;
|
| 966 |
+
el.style.display = "";
|
| 967 |
+
} else if (intent === "neighborhood") {
|
| 968 |
+
el.innerHTML = `${empirical}
|
| 969 |
+
<div class="legend-row"><span class="legend-swatch fill" style="background:transparent; border:2px solid #0b3b6b"></span>NTA boundary</div>${synthetic}`;
|
| 970 |
+
el.style.display = "";
|
| 971 |
+
} else if (intent === "single_address") {
|
| 972 |
+
el.innerHTML = `
|
| 973 |
+
<div class="legend-row"><span class="legend-swatch" style="background:#0b3b6b"></span>Address</div>${empirical}${points}${synthetic}`;
|
| 974 |
+
el.style.display = "";
|
| 975 |
+
} else {
|
| 976 |
+
el.style.display = "none";
|
| 977 |
+
}
|
| 978 |
+
}
|
| 979 |
+
|
| 980 |
+
// Mirrors app/score.py.composite() — see ARCHITECTURE.md / METHODOLOGY.md.
|
| 981 |
+
// Used only for the single_address intent badge; neighborhood and
|
| 982 |
+
// development_check have their own headline stats in the facts panel.
|
| 983 |
+
const REG_W = { fema_1pct: 1.0, fema_02pct: 0.5,
|
| 984 |
+
dep_moderate_2050: 0.75, dep_extreme_2080: 0.50, dep_tidal_2050: 0.75 };
|
| 985 |
+
const HYD_W = { hand_band: 1.0, twi_quartile: 0.5,
|
| 986 |
+
elev_pct_200m_inv: 0.5, elev_pct_750m_inv: 0.5, basin_relief_band: 0.25 };
|
| 987 |
+
const EMP_W = { sandy: 1.0, ida_hwm_within_100m: 1.0, ida_hwm_within_800m: 0.5,
|
| 988 |
+
prithvi_polygon: 0.75, complaints_band: 0.75, floodnet_trigger: 0.75 };
|
| 989 |
+
const handBand = h => h == null ? 0 : (h < 1 ? 1 : h < 3 ? 0.66 : h < 10 ? 0.33 : 0);
|
| 990 |
+
const pctInvBand = p => p == null ? 0 : (p < 10 ? 1 : p < 25 ? 0.66 : p < 50 ? 0.33 : 0);
|
| 991 |
+
const twiBand = t => t == null ? 0 : (t >= 12 ? 1 : t >= 10 ? 0.66 : t >= 8 ? 0.33 : 0);
|
| 992 |
+
const reliefBand = r => r == null ? 0 : (r >= 8 ? 1 : r >= 4 ? 0.66 : r >= 2 ? 0.33 : 0);
|
| 993 |
+
const complBand = n => !n ? 0 : (n >= 10 ? 1 : n >= 3 ? 0.66 : 0.33);
|
| 994 |
+
const sumW = w => Object.values(w).reduce((a, b) => a + b, 0);
|
| 995 |
+
|
| 996 |
+
function computeComposite(ev) {
|
| 997 |
+
const dep = ev.dep || {}, mt = ev.microtopo || {}, ida = ev.ida_hwm || {}, pw = ev.prithvi_water || {};
|
| 998 |
+
const s = {
|
| 999 |
+
fema_1pct: false, fema_02pct: false,
|
| 1000 |
+
dep_moderate_2050: (dep.dep_moderate_2050?.depth_class || 0) > 0,
|
| 1001 |
+
dep_extreme_2080: (dep.dep_extreme_2080?.depth_class || 0) > 0,
|
| 1002 |
+
dep_tidal_2050: false,
|
| 1003 |
+
hand_m: mt.hand_m, twi: mt.twi,
|
| 1004 |
+
rel_elev_pct_200m: mt.rel_elev_pct_200m,
|
| 1005 |
+
rel_elev_pct_750m: mt.rel_elev_pct_750m,
|
| 1006 |
+
basin_relief_m: mt.basin_relief_m,
|
| 1007 |
+
sandy: !!ev.sandy,
|
| 1008 |
+
ida_hwm_within_100m: (ida.nearest_dist_m != null && ida.nearest_dist_m < 100),
|
| 1009 |
+
ida_hwm_within_800m: (ida.n_within_radius || 0) > 0,
|
| 1010 |
+
prithvi_polygon: !!pw.inside_water_polygon,
|
| 1011 |
+
complaints_count: ev.nyc311?.n || 0,
|
| 1012 |
+
floodnet_trigger: (ev.floodnet?.n_flood_events_3y || 0) > 0,
|
| 1013 |
+
};
|
| 1014 |
+
let regRaw = 0; for (const [k, w] of Object.entries(REG_W)) regRaw += s[k] ? w : 0;
|
| 1015 |
+
const reg = regRaw / sumW(REG_W);
|
| 1016 |
+
const hb = { hand_band: handBand(s.hand_m), twi_quartile: twiBand(s.twi),
|
| 1017 |
+
elev_pct_200m_inv: pctInvBand(s.rel_elev_pct_200m),
|
| 1018 |
+
elev_pct_750m_inv: pctInvBand(s.rel_elev_pct_750m),
|
| 1019 |
+
basin_relief_band: reliefBand(s.basin_relief_m) };
|
| 1020 |
+
let hydRaw = 0; for (const [k, w] of Object.entries(HYD_W)) hydRaw += w * hb[k];
|
| 1021 |
+
const hyd = hydRaw / sumW(HYD_W);
|
| 1022 |
+
const ev2 = { sandy: s.sandy ? 1 : 0,
|
| 1023 |
+
ida_hwm_within_100m: s.ida_hwm_within_100m ? 1 : 0,
|
| 1024 |
+
ida_hwm_within_800m: s.ida_hwm_within_800m ? 1 : 0,
|
| 1025 |
+
prithvi_polygon: s.prithvi_polygon ? 1 : 0,
|
| 1026 |
+
complaints_band: complBand(s.complaints_count),
|
| 1027 |
+
floodnet_trigger: s.floodnet_trigger ? 1 : 0 };
|
| 1028 |
+
let empRaw = 0; for (const [k, w] of Object.entries(EMP_W)) empRaw += w * ev2[k];
|
| 1029 |
+
const emp = empRaw / sumW(EMP_W);
|
| 1030 |
+
const composite = reg + hyd + emp;
|
| 1031 |
+
let tier = 0;
|
| 1032 |
+
if (composite >= 1.50) tier = 1;
|
| 1033 |
+
else if (composite >= 1.00) tier = 2;
|
| 1034 |
+
else if (composite >= 0.50) tier = 3;
|
| 1035 |
+
else if (composite >= 0.01) tier = 4;
|
| 1036 |
+
const floorApplied = !!(s.sandy || s.ida_hwm_within_100m);
|
| 1037 |
+
if (floorApplied && (tier === 0 || tier > 2)) tier = 2;
|
| 1038 |
+
return { tier, composite, floorApplied,
|
| 1039 |
+
sub: { regulatory: reg, hydrological: hyd, empirical: emp } };
|
| 1040 |
+
}
|
| 1041 |
+
|
| 1042 |
+
function tierMeta(tier) {
|
| 1043 |
+
if (tier === 1) return { tier, label: "High exposure",
|
| 1044 |
+
help: "Multiple sub-indices saturated. Not a damage probability." };
|
| 1045 |
+
if (tier === 2) return { tier, label: "Elevated exposure",
|
| 1046 |
+
help: "At least one sub-index near saturation. Not a damage probability." };
|
| 1047 |
+
if (tier === 3) return { tier, label: "Moderate exposure",
|
| 1048 |
+
help: "Partial signals across categories. Not a damage probability." };
|
| 1049 |
+
if (tier === 4) return { tier, label: "Limited exposure",
|
| 1050 |
+
help: "A single contextual signal." };
|
| 1051 |
+
return { tier: 0, label: "No flagged exposure",
|
| 1052 |
+
help: "No positive flood signal across the assessed sources." };
|
| 1053 |
+
}
|
| 1054 |
+
|
| 1055 |
+
function renderBriefHead(d) {
|
| 1056 |
+
const intent = d.intent;
|
| 1057 |
+
const place = (d.target && d.target.nta_name)
|
| 1058 |
+
|| (d.geocode && d.geocode.address)
|
| 1059 |
+
|| d.place || "—";
|
| 1060 |
+
const meta = [];
|
| 1061 |
+
const eyebrowMap = {
|
| 1062 |
+
single_address: "Flood-exposure briefing — address",
|
| 1063 |
+
neighborhood: "Flood-exposure briefing — neighborhood",
|
| 1064 |
+
development_check: "Active development × flood exposure",
|
| 1065 |
+
live_now: "Current conditions — NYC",
|
| 1066 |
+
};
|
| 1067 |
+
$("#briefEyebrow").textContent = eyebrowMap[intent] || "Briefing";
|
| 1068 |
+
$("#briefTitle").innerHTML = escapeHtml(place);
|
| 1069 |
+
|
| 1070 |
+
// For single_address intent, append the tier badge inline with the title
|
| 1071 |
+
// — same idiom as the legacy /single page.
|
| 1072 |
+
if (intent === "single_address") {
|
| 1073 |
+
const c = computeComposite(d);
|
| 1074 |
+
const m = tierMeta(c.tier);
|
| 1075 |
+
const titleEl = $("#briefTitle");
|
| 1076 |
+
const floor = c.floorApplied ? ' <span class="tier-floor">empirical floor</span>' : "";
|
| 1077 |
+
titleEl.innerHTML += ` <span class="tier-chip t-${m.tier}" title="${escapeHtml(m.help)}">
|
| 1078 |
+
Tier ${m.tier} · ${escapeHtml(m.label)}${floor}
|
| 1079 |
+
</span>`;
|
| 1080 |
+
}
|
| 1081 |
+
|
| 1082 |
+
// Mellea compliance badge — present iff strict mode ran and produced
|
| 1083 |
+
// metadata. Color reflects pass ratio: green for full, amber partial,
|
| 1084 |
+
// red none.
|
| 1085 |
+
if (d.mellea) {
|
| 1086 |
+
const m = d.mellea;
|
| 1087 |
+
const passed = (m.requirements_passed || []).length;
|
| 1088 |
+
const total = m.requirements_total || 0;
|
| 1089 |
+
const cls = passed === total ? "full"
|
| 1090 |
+
: passed > 0 ? "partial"
|
| 1091 |
+
: "none";
|
| 1092 |
+
const tip = `Mellea (IBM Research) ran ${m.n_attempts} attempt${m.n_attempts === 1 ? "" : "s"}` +
|
| 1093 |
+
` (${m.rerolls} reroll${m.rerolls === 1 ? "" : "s"}). ` +
|
| 1094 |
+
`Requirements passed: ${(m.requirements_passed || []).join(", ") || "none"}. ` +
|
| 1095 |
+
(m.requirements_failed?.length
|
| 1096 |
+
? `Failed: ${m.requirements_failed.join(", ")}.` : "");
|
| 1097 |
+
$("#briefTitle").innerHTML +=
|
| 1098 |
+
` <span class="mellea-badge ${cls}" title="${escapeHtml(tip)}">` +
|
| 1099 |
+
`<span class="ico">✓</span>Mellea ${passed}/${total}` +
|
| 1100 |
+
(m.rerolls > 0 ? ` · ${m.rerolls} reroll${m.rerolls === 1 ? "" : "s"}` : "") +
|
| 1101 |
+
`</span>`;
|
| 1102 |
+
}
|
| 1103 |
+
if (intent === "single_address" && d.geocode) {
|
| 1104 |
+
if (d.geocode.borough) meta.push(`<span class="brief-meta-k">borough</span> <span class="brief-meta-v">${escapeHtml(d.geocode.borough)}</span>`);
|
| 1105 |
+
if (d.geocode.bbl) meta.push(`<span class="brief-meta-k">bbl</span> <span class="brief-meta-v">${escapeHtml(d.geocode.bbl)}</span>`);
|
| 1106 |
+
} else if (d.target && d.target.borough) {
|
| 1107 |
+
meta.push(`<span class="brief-meta-k">borough</span> <span class="brief-meta-v">${escapeHtml(d.target.borough)}</span>`);
|
| 1108 |
+
if (d.target.nta_code) meta.push(`<span class="brief-meta-k">nta</span> <span class="brief-meta-v">${escapeHtml(d.target.nta_code)}</span>`);
|
| 1109 |
+
}
|
| 1110 |
+
if (d.total_s != null) meta.push(`<span class="brief-meta-k">runtime</span> <span class="brief-meta-v">${d.total_s}s</span>`);
|
| 1111 |
+
meta.push(`<span class="brief-meta-k">assessed</span> <span class="brief-meta-v">${new Date().toISOString().slice(0,16).replace("T"," ")}</span>`);
|
| 1112 |
+
$("#briefMeta").innerHTML = meta.join('<span style="color:var(--text-faint)">·</span>');
|
| 1113 |
+
}
|
| 1114 |
+
|
| 1115 |
+
// ---------------------------------------------------------------------------
|
| 1116 |
+
// PLANNER ROW
|
| 1117 |
+
// ---------------------------------------------------------------------------
|
| 1118 |
+
|
| 1119 |
+
function renderPlan(p) {
|
| 1120 |
+
const pillCls = INTENT_PILL_CLASS[p.intent] || "";
|
| 1121 |
+
$("#plannerRow").innerHTML = `
|
| 1122 |
+
<div class="planner-box">
|
| 1123 |
+
<div class="planner-key">Planner</div>
|
| 1124 |
+
<div><span class="intent-pill ${pillCls}">${escapeHtml(p.intent)}</span></div>
|
| 1125 |
+
<div class="planner-key">Targets</div>
|
| 1126 |
+
<div class="planner-val">${(p.targets || []).map(t => escapeHtml(t.type) + ":" + escapeHtml(t.text)).join(", ") || "(none)"}</div>
|
| 1127 |
+
<div class="planner-key">Specialists</div>
|
| 1128 |
+
<div class="planner-val">${(p.specialists || []).join(", ")}</div>
|
| 1129 |
+
<div class="planner-rationale">"${escapeHtml(p.rationale || "")}"</div>
|
| 1130 |
+
</div>`;
|
| 1131 |
+
}
|
| 1132 |
+
|
| 1133 |
+
// ---------------------------------------------------------------------------
|
| 1134 |
+
// SSE driver
|
| 1135 |
+
// ---------------------------------------------------------------------------
|
| 1136 |
+
|
| 1137 |
+
let currentEs = null;
|
| 1138 |
+
// Buffers for the report-export feature — capture the full plan, trace,
|
| 1139 |
+
// and final result during streaming so the report page can render the
|
| 1140 |
+
// complete evidence package without re-running the agent.
|
| 1141 |
+
let LAST_RESULT = null;
|
| 1142 |
+
let LAST_TRACE = [];
|
| 1143 |
+
let LAST_PLAN = null;
|
| 1144 |
+
let LAST_PLAN_OBJ = null;
|
| 1145 |
+
let TRACE_BUF = [];
|
| 1146 |
+
|
| 1147 |
+
function ask(q) {
|
| 1148 |
+
ensureMap();
|
| 1149 |
+
clearTrace(); clearMap();
|
| 1150 |
+
$("#plannerRow").innerHTML = "";
|
| 1151 |
+
setBriefingText("");
|
| 1152 |
+
$("#paragraph").classList.remove("streaming");
|
| 1153 |
+
const banner = $("#melleaBanner");
|
| 1154 |
+
if (banner) { banner.style.display = "none"; banner.innerHTML = ""; }
|
| 1155 |
+
$("#reportPanel").style.display = "none";
|
| 1156 |
+
$("#factsPanel").style.display = "none";
|
| 1157 |
+
$("#reportSkel").style.display = "";
|
| 1158 |
+
$("#traceSkel").style.display = "";
|
| 1159 |
+
$("#mapLegend").style.display = "none";
|
| 1160 |
+
setMapLoading("Granite is planning the query…");
|
| 1161 |
+
$("#goBtn").disabled = true;
|
| 1162 |
+
$("#traceMeta").textContent = "…";
|
| 1163 |
+
|
| 1164 |
+
if (currentEs) currentEs.close();
|
| 1165 |
+
const es = new EventSource("/api/agent/stream?q=" + encodeURIComponent(q));
|
| 1166 |
+
currentEs = es;
|
| 1167 |
+
const t0 = Date.now();
|
| 1168 |
+
let streamBuf = "";
|
| 1169 |
+
let streamTimer = null;
|
| 1170 |
+
let planStreamBuf = "";
|
| 1171 |
+
let planStreamTimer = null;
|
| 1172 |
+
const ensurePlannerStream = () => {
|
| 1173 |
+
let el = $("#plannerRow .planner-streaming");
|
| 1174 |
+
if (!el) {
|
| 1175 |
+
$("#plannerRow").innerHTML = `<div class="planner-streaming"></div>`;
|
| 1176 |
+
el = $("#plannerRow .planner-streaming");
|
| 1177 |
+
}
|
| 1178 |
+
return el;
|
| 1179 |
+
};
|
| 1180 |
+
const repaintPlanner = () => {
|
| 1181 |
+
const el = $("#plannerRow .planner-streaming");
|
| 1182 |
+
if (el) el.textContent = planStreamBuf;
|
| 1183 |
+
};
|
| 1184 |
+
const schedulePlannerRepaint = () => {
|
| 1185 |
+
if (planStreamTimer) return;
|
| 1186 |
+
planStreamTimer = setTimeout(() => { planStreamTimer = null; repaintPlanner(); }, 60);
|
| 1187 |
+
};
|
| 1188 |
+
// Re-render the partial markdown on every token, but at most every 80 ms
|
| 1189 |
+
// so the browser isn't murdered by a token-stream that arrives in bursts.
|
| 1190 |
+
// Build the Sources footer alongside so it grows as new doc_ids appear.
|
| 1191 |
+
// Briefing component owns citation indexing + chip binding via shared
|
| 1192 |
+
// signals; we just feed it the latest text. Sources footer reacts to
|
| 1193 |
+
// the citeIndex signal that <r-briefing> updates each render.
|
| 1194 |
+
const repaint = () => {
|
| 1195 |
+
setBriefingText(streamBuf);
|
| 1196 |
+
renderSources();
|
| 1197 |
+
};
|
| 1198 |
+
const scheduleRepaint = () => {
|
| 1199 |
+
if (streamTimer) return;
|
| 1200 |
+
streamTimer = setTimeout(() => { streamTimer = null; repaint(); }, 80);
|
| 1201 |
+
};
|
| 1202 |
+
|
| 1203 |
+
es.addEventListener("plan_token", (e) => {
|
| 1204 |
+
ensurePlannerStream();
|
| 1205 |
+
const d = JSON.parse(e.data);
|
| 1206 |
+
planStreamBuf += d.delta || "";
|
| 1207 |
+
schedulePlannerRepaint();
|
| 1208 |
+
});
|
| 1209 |
+
es.addEventListener("plan", (e) => {
|
| 1210 |
+
if (planStreamTimer) { clearTimeout(planStreamTimer); planStreamTimer = null; }
|
| 1211 |
+
const planObj = JSON.parse(e.data);
|
| 1212 |
+
LAST_PLAN_OBJ = planObj;
|
| 1213 |
+
renderPlan(planObj);
|
| 1214 |
+
setLegend(planObj.intent);
|
| 1215 |
+
setMapLoading(planObj.intent === "live_now" ? null : "Resolving location…");
|
| 1216 |
+
$("#traceSkel").style.display = "none";
|
| 1217 |
+
TRACE_BUF = [];
|
| 1218 |
+
$("#reportBtn").classList.remove("ready");
|
| 1219 |
+
});
|
| 1220 |
+
es.addEventListener("step", (e) => {
|
| 1221 |
+
const step = JSON.parse(e.data);
|
| 1222 |
+
TRACE_BUF.push(step);
|
| 1223 |
+
incrementallyFillMap(step);
|
| 1224 |
+
if (step.step === "geocode" || step.step === "nta_resolve") setMapLoading(null);
|
| 1225 |
+
});
|
| 1226 |
+
es.addEventListener("step", (e) => { pushTraceStep(JSON.parse(e.data)); });
|
| 1227 |
+
let currentAttempt = 0;
|
| 1228 |
+
es.addEventListener("token", (e) => {
|
| 1229 |
+
const d = JSON.parse(e.data);
|
| 1230 |
+
if (!streamBuf || (d.attempt != null && d.attempt !== currentAttempt)) {
|
| 1231 |
+
// First token of a (possibly new) attempt → reveal panel, reset
|
| 1232 |
+
// buffer if Mellea moved to a reroll.
|
| 1233 |
+
if (d.attempt != null && d.attempt !== currentAttempt) {
|
| 1234 |
+
currentAttempt = d.attempt;
|
| 1235 |
+
streamBuf = "";
|
| 1236 |
+
}
|
| 1237 |
+
$("#reportSkel").style.display = "none";
|
| 1238 |
+
$("#reportPanel").style.display = "";
|
| 1239 |
+
$("#paragraph").classList.add("streaming");
|
| 1240 |
+
}
|
| 1241 |
+
streamBuf += d.delta || "";
|
| 1242 |
+
scheduleRepaint();
|
| 1243 |
+
});
|
| 1244 |
+
// Mellea per-attempt outcome — render a small banner above the briefing
|
| 1245 |
+
// when a reroll is about to start so the user knows the model is
|
| 1246 |
+
// self-correcting (and what failed).
|
| 1247 |
+
es.addEventListener("mellea_attempt", (e) => {
|
| 1248 |
+
const d = JSON.parse(e.data);
|
| 1249 |
+
const banner = $("#melleaBanner");
|
| 1250 |
+
if (!banner) return;
|
| 1251 |
+
if (d.failed && d.failed.length) {
|
| 1252 |
+
banner.className = "mellea-banner reroll";
|
| 1253 |
+
banner.innerHTML = `<strong>↻ Mellea reroll</strong> — attempt ${(d.attempt|0)+1} failed: <code>${d.failed.join(", ")}</code>. Re-drafting…`;
|
| 1254 |
+
banner.style.display = "";
|
| 1255 |
+
} else {
|
| 1256 |
+
banner.className = "mellea-banner pass";
|
| 1257 |
+
banner.innerHTML = `<strong>✓ Mellea</strong> — all 4 grounding requirements satisfied`;
|
| 1258 |
+
banner.style.display = "";
|
| 1259 |
+
}
|
| 1260 |
+
});
|
| 1261 |
+
es.addEventListener("final", (e) => {
|
| 1262 |
+
const d = JSON.parse(e.data);
|
| 1263 |
+
const dt = ((Date.now() - t0) / 1000).toFixed(1);
|
| 1264 |
+
$("#traceMeta").textContent = `${dt}s`;
|
| 1265 |
+
setMapLoading(null);
|
| 1266 |
+
$("#reportSkel").style.display = "none";
|
| 1267 |
+
$("#paragraph").classList.remove("streaming");
|
| 1268 |
+
if (d.paragraph) {
|
| 1269 |
+
$("#reportPanel").style.display = "";
|
| 1270 |
+
streamBuf = d.paragraph;
|
| 1271 |
+
if (streamTimer) { clearTimeout(streamTimer); streamTimer = null; }
|
| 1272 |
+
repaint();
|
| 1273 |
+
renderBriefHead(d);
|
| 1274 |
+
}
|
| 1275 |
+
renderFacts(d);
|
| 1276 |
+
fillMapForFinal(d);
|
| 1277 |
+
// Stash everything needed for the auditable-report page.
|
| 1278 |
+
LAST_RESULT = { query: q, finishedAt: new Date().toISOString(),
|
| 1279 |
+
wallSeconds: Number(dt), result: d };
|
| 1280 |
+
LAST_TRACE = TRACE_BUF.slice();
|
| 1281 |
+
LAST_PLAN = LAST_PLAN_OBJ;
|
| 1282 |
+
$("#reportBtn").classList.add("ready");
|
| 1283 |
+
});
|
| 1284 |
+
es.addEventListener("error", () => {});
|
| 1285 |
+
es.addEventListener("done", () => { es.close(); $("#goBtn").disabled = false; });
|
| 1286 |
+
}
|
| 1287 |
+
|
| 1288 |
+
// ---------------------------------------------------------------------------
|
| 1289 |
+
// wire
|
| 1290 |
+
// ---------------------------------------------------------------------------
|
| 1291 |
+
|
| 1292 |
+
// Bind form/sample handlers FIRST so a throw in ensureMap() (e.g. a
|
| 1293 |
+
// WebGL init failure) can't strand the user with a dead "Ask" button.
|
| 1294 |
+
$("#agentForm").addEventListener("submit", (e) => {
|
| 1295 |
+
e.preventDefault();
|
| 1296 |
+
const q = $("#q").value.trim();
|
| 1297 |
+
if (q) ask(q);
|
| 1298 |
+
});
|
| 1299 |
+
document.querySelectorAll(".sample-btn").forEach(b => {
|
| 1300 |
+
b.addEventListener("click", () => { $("#q").value = b.dataset.q; ask(b.dataset.q); });
|
| 1301 |
+
});
|
| 1302 |
+
try { ensureMap(); } catch (e) { console.error("ensureMap failed:", e); }
|
| 1303 |
+
|
| 1304 |
+
// Backend hardware pill: fetches /api/backend, renders "<HW> · <ENGINE>"
|
| 1305 |
+
// and a state color (green=primary up, amber=fallback active, red=down).
|
| 1306 |
+
// Refreshes every 60s so a flipped droplet shows up without a page reload.
|
| 1307 |
+
async function refreshBackendPill() {
|
| 1308 |
+
const pill = document.getElementById("backendPill");
|
| 1309 |
+
const text = document.getElementById("backendPillText");
|
| 1310 |
+
if (!pill || !text) return;
|
| 1311 |
+
try {
|
| 1312 |
+
const r = await fetch("/api/backend", { cache: "no-store" });
|
| 1313 |
+
if (!r.ok) throw new Error("status " + r.status);
|
| 1314 |
+
const info = await r.json();
|
| 1315 |
+
const onFallback = info.reachable === false && !!info.fallback_engine;
|
| 1316 |
+
const engine = onFallback ? info.fallback_engine : info.engine;
|
| 1317 |
+
const hw = onFallback ? "fallback" : info.hardware;
|
| 1318 |
+
text.textContent = `${hw} · Granite 4.1 / ${engine}`;
|
| 1319 |
+
pill.dataset.state =
|
| 1320 |
+
info.reachable ? "ok" :
|
| 1321 |
+
onFallback ? "fallback" : "down";
|
| 1322 |
+
const detail = info.vllm_base_url
|
| 1323 |
+
? `Primary: ${info.engine} @ ${info.vllm_base_url}`
|
| 1324 |
+
: `Engine: ${info.engine}`;
|
| 1325 |
+
pill.title = info.reachable
|
| 1326 |
+
? `${detail} — reachable. No vendor LLM is contacted.`
|
| 1327 |
+
: onFallback
|
| 1328 |
+
? `${detail} unreachable; running on ${info.fallback_engine} fallback.`
|
| 1329 |
+
: `${detail} — UNREACHABLE.`;
|
| 1330 |
+
} catch (e) {
|
| 1331 |
+
text.textContent = "backend unknown";
|
| 1332 |
+
pill.dataset.state = "down";
|
| 1333 |
+
pill.title = "Could not query /api/backend: " + e.message;
|
| 1334 |
+
}
|
| 1335 |
+
}
|
| 1336 |
+
refreshBackendPill();
|
| 1337 |
+
setInterval(refreshBackendPill, 60000);
|
| 1338 |
+
|
| 1339 |
+
// Subscribe to the shared highlight signal so vanilla-rendered citation
|
| 1340 |
+
// chips in the briefing prose mirror the highlight state driven by the
|
| 1341 |
+
// Lit <r-sources-footer> (and vice versa).
|
| 1342 |
+
(async () => {
|
| 1343 |
+
const { highlightedDocId } = await import("/static/components/signals.js");
|
| 1344 |
+
const apply = () => {
|
| 1345 |
+
const id = highlightedDocId.get();
|
| 1346 |
+
document.querySelectorAll("#paragraph .cite").forEach(c => {
|
| 1347 |
+
c.classList.toggle("hl", c.dataset.srcId === id);
|
| 1348 |
+
});
|
| 1349 |
+
};
|
| 1350 |
+
// Lit-labs/signals exposes a subscribe / effect — try both shapes.
|
| 1351 |
+
if (typeof highlightedDocId.subscribe === "function") {
|
| 1352 |
+
highlightedDocId.subscribe(apply);
|
| 1353 |
+
} else {
|
| 1354 |
+
// Polyfill: poll on mutation. Cheap; signal updates are rare.
|
| 1355 |
+
const orig = highlightedDocId.set.bind(highlightedDocId);
|
| 1356 |
+
highlightedDocId.set = (v) => { orig(v); apply(); };
|
| 1357 |
+
}
|
| 1358 |
+
})();
|
| 1359 |
+
|
| 1360 |
+
// "Generate auditable report" — snapshots the live map, packs the full
|
| 1361 |
+
// evidence (query / plan / per-specialist trace / final result / per-source
|
| 1362 |
+
// vintages / labels / urls), parks it in sessionStorage, opens /report.
|
| 1363 |
+
$("#reportBtn").addEventListener("click", () => {
|
| 1364 |
+
if (!LAST_RESULT) return;
|
| 1365 |
+
let mapPng = null;
|
| 1366 |
+
try {
|
| 1367 |
+
if (map && map.loaded()) {
|
| 1368 |
+
// preserveDrawingBuffer:false would force a one-frame render here
|
| 1369 |
+
map.triggerRepaint();
|
| 1370 |
+
mapPng = map.getCanvas().toDataURL("image/png");
|
| 1371 |
+
}
|
| 1372 |
+
} catch (e) {
|
| 1373 |
+
console.warn("map snapshot failed", e);
|
| 1374 |
+
}
|
| 1375 |
+
const pkg = {
|
| 1376 |
+
...LAST_RESULT,
|
| 1377 |
+
plan: LAST_PLAN,
|
| 1378 |
+
trace: LAST_TRACE,
|
| 1379 |
+
mapPng,
|
| 1380 |
+
sourceLabels: SOURCE_LABELS,
|
| 1381 |
+
sourceUrls: SOURCE_URLS,
|
| 1382 |
+
sourceVintages: SOURCE_VINTAGES,
|
| 1383 |
+
stepLabels: STEP_LABELS,
|
| 1384 |
+
};
|
| 1385 |
+
try {
|
| 1386 |
+
sessionStorage.setItem("riprap_report", JSON.stringify(pkg));
|
| 1387 |
+
window.open("/report", "_blank");
|
| 1388 |
+
} catch (e) {
|
| 1389 |
+
alert("Could not stash report payload (storage may be full): " + e.message);
|
| 1390 |
+
}
|
| 1391 |
+
});
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
//
|
| 2 |
|
| 3 |
const STEP_LABELS = {
|
| 4 |
geocode: ["Geocode (DCP Geosearch)", "address → lat/lon, BBL"],
|
|
@@ -6,6 +6,10 @@ const STEP_LABELS = {
|
|
| 6 |
dep_stormwater: ["DEP Stormwater Maps", "pluvial scenarios + 2080 SLR"],
|
| 7 |
floodnet: ["FloodNet sensor network", "live ultrasonic depth sensors"],
|
| 8 |
nyc311: ["NYC 311 archive", "flood complaints in buffer"],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
microtopo_lidar: ["LiDAR terrain (DEM + TWI + HAND)", "USGS 3DEP DEM + whitebox-workflows hydrology"],
|
| 10 |
ida_hwm_2021: ["Ida 2021 high-water marks", "USGS empirical post-event extent"],
|
| 11 |
prithvi_eo_v2: ["Prithvi-EO 2.0 (300M, NASA/IBM)", "Sen1Floods11 satellite water segmentation"],
|
|
@@ -15,6 +19,7 @@ const STEP_LABELS = {
|
|
| 15 |
|
| 16 |
const STEPS_ORDER = [
|
| 17 |
"geocode", "sandy_inundation", "dep_stormwater", "floodnet", "nyc311",
|
|
|
|
| 18 |
"microtopo_lidar", "ida_hwm_2021", "prithvi_eo_v2",
|
| 19 |
"rag_granite_embedding", "reconcile_granite41",
|
| 20 |
];
|
|
@@ -255,8 +260,45 @@ function rewriteCitations(text) {
|
|
| 255 |
});
|
| 256 |
}
|
| 257 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
function renderParagraph(text) {
|
| 259 |
-
|
|
|
|
|
|
|
| 260 |
}
|
| 261 |
|
| 262 |
const SOURCE_LABELS = {
|
|
@@ -275,6 +317,10 @@ const SOURCE_LABELS = {
|
|
| 275 |
rag_coned: "Con Edison Climate Change Resilience Plan (Case 22-E-0222)",
|
| 276 |
rag_mta: "MTA Climate Resilience Roadmap (Oct 2025)",
|
| 277 |
rag_comptroller: "NYC Comptroller — \"Is NYC Ready for Rain?\" (2024)",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
};
|
| 279 |
|
| 280 |
// ----------------------------------------------------------------------
|
|
@@ -282,29 +328,129 @@ const SOURCE_LABELS = {
|
|
| 282 |
// evidence cards, policy quotes, methodology footer.
|
| 283 |
// ----------------------------------------------------------------------
|
| 284 |
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
if (
|
| 289 |
-
|
| 290 |
-
if (
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
}
|
| 293 |
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
const dep = ev.dep || {};
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
}
|
| 307 |
|
|
|
|
|
|
|
|
|
|
| 308 |
function renderHeader(ev) {
|
| 309 |
const geo = ev.geocode || {};
|
| 310 |
$("#reportAddr").textContent = geo.address || "(unresolved)";
|
|
@@ -314,12 +460,13 @@ function renderHeader(ev) {
|
|
| 314 |
}
|
| 315 |
|
| 316 |
function renderTier(ev) {
|
| 317 |
-
const
|
| 318 |
-
const m = tierMeta(
|
| 319 |
const badge = $("#tierBadge");
|
| 320 |
badge.className = "tier-badge t-" + m.tier;
|
| 321 |
$("#tierNum").textContent = m.tier;
|
| 322 |
-
|
|
|
|
| 323 |
$("#tierHelp").textContent = m.help;
|
| 324 |
}
|
| 325 |
|
|
@@ -557,6 +704,106 @@ function renderEvidence(ev) {
|
|
| 557 |
}));
|
| 558 |
}
|
| 559 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
$("#evidenceCards").innerHTML = cards.join("");
|
| 561 |
}
|
| 562 |
|
|
|
|
| 1 |
+
// Riprap web client — subscribes to SSE, lights up FSM steps, renders the report.
|
| 2 |
|
| 3 |
const STEP_LABELS = {
|
| 4 |
geocode: ["Geocode (DCP Geosearch)", "address → lat/lon, BBL"],
|
|
|
|
| 6 |
dep_stormwater: ["DEP Stormwater Maps", "pluvial scenarios + 2080 SLR"],
|
| 7 |
floodnet: ["FloodNet sensor network", "live ultrasonic depth sensors"],
|
| 8 |
nyc311: ["NYC 311 archive", "flood complaints in buffer"],
|
| 9 |
+
noaa_tides: ["NOAA Tides & Currents (live)", "Battery / Kings Pt / Sandy Hook water level"],
|
| 10 |
+
nws_alerts: ["NWS Public Alerts (live)", "active flood-relevant alerts at point"],
|
| 11 |
+
nws_obs: ["NWS METAR observation (live)", "nearest ASOS recent precipitation"],
|
| 12 |
+
ttm_forecast: ["Granite TTM r2 (TimeSeries)", "9.6h surge-residual nowcast at the Battery"],
|
| 13 |
microtopo_lidar: ["LiDAR terrain (DEM + TWI + HAND)", "USGS 3DEP DEM + whitebox-workflows hydrology"],
|
| 14 |
ida_hwm_2021: ["Ida 2021 high-water marks", "USGS empirical post-event extent"],
|
| 15 |
prithvi_eo_v2: ["Prithvi-EO 2.0 (300M, NASA/IBM)", "Sen1Floods11 satellite water segmentation"],
|
|
|
|
| 19 |
|
| 20 |
const STEPS_ORDER = [
|
| 21 |
"geocode", "sandy_inundation", "dep_stormwater", "floodnet", "nyc311",
|
| 22 |
+
"noaa_tides", "nws_alerts", "nws_obs", "ttm_forecast",
|
| 23 |
"microtopo_lidar", "ida_hwm_2021", "prithvi_eo_v2",
|
| 24 |
"rag_granite_embedding", "reconcile_granite41",
|
| 25 |
];
|
|
|
|
| 260 |
});
|
| 261 |
}
|
| 262 |
|
| 263 |
+
function escapeHtml(s) {
|
| 264 |
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
function renderMarkdown(text) {
|
| 268 |
+
// Tiny safe markdown subset:
|
| 269 |
+
// **Header.** (on its own line) -> <h4 class="rsum-h">Header</h4>
|
| 270 |
+
// **inline bold** (mid-sentence) -> <strong>...</strong>
|
| 271 |
+
// We escape HTML first to defang any injection in model output.
|
| 272 |
+
const lines = text.split("\n");
|
| 273 |
+
const out = [];
|
| 274 |
+
let bodyBuf = [];
|
| 275 |
+
const flushBody = () => {
|
| 276 |
+
if (!bodyBuf.length) return;
|
| 277 |
+
const body = bodyBuf.join(" ").trim();
|
| 278 |
+
bodyBuf = [];
|
| 279 |
+
if (!body) return;
|
| 280 |
+
const safe = escapeHtml(body)
|
| 281 |
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
| 282 |
+
out.push(`<p class="rsum-p">${safe}</p>`);
|
| 283 |
+
};
|
| 284 |
+
const headerRe = /^\s*\*\*([A-Z][A-Za-z\s/]+)\.\*\*\s*$/;
|
| 285 |
+
for (const line of lines) {
|
| 286 |
+
const m = line.match(headerRe);
|
| 287 |
+
if (m) {
|
| 288 |
+
flushBody();
|
| 289 |
+
out.push(`<h4 class="rsum-h">${escapeHtml(m[1])}</h4>`);
|
| 290 |
+
} else {
|
| 291 |
+
bodyBuf.push(line);
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
flushBody();
|
| 295 |
+
return out.join("");
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
function renderParagraph(text) {
|
| 299 |
+
// Build markdown structure FIRST, then rewrite citations inside. Citations
|
| 300 |
+
// are bracketed tokens like [sandy] which don't conflict with our markdown.
|
| 301 |
+
$("#paragraph").innerHTML = rewriteCitations(renderMarkdown(text));
|
| 302 |
}
|
| 303 |
|
| 304 |
const SOURCE_LABELS = {
|
|
|
|
| 317 |
rag_coned: "Con Edison Climate Change Resilience Plan (Case 22-E-0222)",
|
| 318 |
rag_mta: "MTA Climate Resilience Roadmap (Oct 2025)",
|
| 319 |
rag_comptroller: "NYC Comptroller — \"Is NYC Ready for Rain?\" (2024)",
|
| 320 |
+
noaa_tides: "NOAA CO-OPS Tides & Currents — live water level (6-min)",
|
| 321 |
+
nws_alerts: "NWS Public Alerts API — active flood-relevant alerts",
|
| 322 |
+
nws_obs: "NWS Station Observations — nearest ASOS hourly METAR",
|
| 323 |
+
ttm_forecast: "Granite TimeSeries TTM r2 — surge-residual nowcast (Ekambaram et al. 2024, NeurIPS)",
|
| 324 |
};
|
| 325 |
|
| 326 |
// ----------------------------------------------------------------------
|
|
|
|
| 328 |
// evidence cards, policy quotes, methodology footer.
|
| 329 |
// ----------------------------------------------------------------------
|
| 330 |
|
| 331 |
+
// Tier meta — uses the new composite breakpoints, mirrors app/score.py.
|
| 332 |
+
// Tooltip copy explicitly states scope: exposure, not damage probability.
|
| 333 |
+
function tierMeta(tier) {
|
| 334 |
+
if (tier === 1) return {tier: 1, label: "High exposure",
|
| 335 |
+
help: "Multiple sub-indices saturated; empirical and/or modeled scenarios both indicate substantial exposure. Not a damage probability."};
|
| 336 |
+
if (tier === 2) return {tier: 2, label: "Elevated exposure",
|
| 337 |
+
help: "At least one sub-index near saturation; significant overlap with empirical or modeled scenarios. Not a damage probability."};
|
| 338 |
+
if (tier === 3) return {tier: 3, label: "Moderate exposure",
|
| 339 |
+
help: "Partial signals across categories; scenario- or neighborhood-specific exposure. Not a damage probability."};
|
| 340 |
+
if (tier === 4) return {tier: 4, label: "Limited exposure",
|
| 341 |
+
help: "A single contextual signal; no positive scenario hits."};
|
| 342 |
+
return {tier: 0, label: "No flagged exposure",
|
| 343 |
+
help: "No positive flood signal across the assessed sources."};
|
| 344 |
}
|
| 345 |
|
| 346 |
+
// ---- Score computation: mirrors app/score.py.composite() exactly ---------
|
| 347 |
+
// Three thematic sub-indices, equal weights within each, max-empirical
|
| 348 |
+
// floor. Live signals (NWS alerts, surge, precip) are NOT in this score
|
| 349 |
+
// per IPCC AR6 WG II's distinction between exposure (static) and event
|
| 350 |
+
// occurrence (live).
|
| 351 |
+
const REG_W = {
|
| 352 |
+
fema_1pct: 1.0, fema_02pct: 0.5,
|
| 353 |
+
dep_moderate_2050: 0.75, dep_extreme_2080: 0.50, dep_tidal_2050: 0.75,
|
| 354 |
+
};
|
| 355 |
+
const HYD_W = {
|
| 356 |
+
hand_band: 1.0, twi_quartile: 0.5,
|
| 357 |
+
elev_pct_200m_inv: 0.5, elev_pct_750m_inv: 0.5, basin_relief_band: 0.25,
|
| 358 |
+
};
|
| 359 |
+
const EMP_W = {
|
| 360 |
+
sandy: 1.0,
|
| 361 |
+
ida_hwm_within_100m: 1.0, ida_hwm_within_800m: 0.5,
|
| 362 |
+
prithvi_polygon: 0.75, complaints_band: 0.75, floodnet_trigger: 0.75,
|
| 363 |
+
};
|
| 364 |
+
|
| 365 |
+
const handBand = (h) => h == null ? 0 : (h < 1 ? 1 : h < 3 ? 0.66 : h < 10 ? 0.33 : 0);
|
| 366 |
+
const pctInvBand = (p) => p == null ? 0 : (p < 10 ? 1 : p < 25 ? 0.66 : p < 50 ? 0.33 : 0);
|
| 367 |
+
const twiBand = (t) => t == null ? 0 : (t >= 12 ? 1 : t >= 10 ? 0.66 : t >= 8 ? 0.33 : 0);
|
| 368 |
+
const reliefBand = (r) => r == null ? 0 : (r >= 8 ? 1 : r >= 4 ? 0.66 : r >= 2 ? 0.33 : 0);
|
| 369 |
+
const complBand = (n) => !n ? 0 : (n >= 10 ? 1 : n >= 3 ? 0.66 : 0.33);
|
| 370 |
+
const sumW = (w) => Object.values(w).reduce((a, b) => a + b, 0);
|
| 371 |
+
|
| 372 |
+
function computeComposite(ev) {
|
| 373 |
const dep = ev.dep || {};
|
| 374 |
+
const mt = ev.microtopo || {};
|
| 375 |
+
const ida = ev.ida_hwm || {};
|
| 376 |
+
const pw = ev.prithvi_water || {};
|
| 377 |
+
|
| 378 |
+
// Build the signal dict in the shape app/score.py expects.
|
| 379 |
+
const s = {
|
| 380 |
+
// Regulatory
|
| 381 |
+
fema_1pct: false, // not yet wired in this build
|
| 382 |
+
fema_02pct: false,
|
| 383 |
+
dep_moderate_2050: (dep.dep_moderate_2050?.depth_class || 0) > 0,
|
| 384 |
+
dep_extreme_2080: (dep.dep_extreme_2080?.depth_class || 0) > 0,
|
| 385 |
+
dep_tidal_2050: false, // tidal scenario not in current FSM
|
| 386 |
+
// Hydrological
|
| 387 |
+
hand_m: mt.hand_m,
|
| 388 |
+
twi: mt.twi,
|
| 389 |
+
rel_elev_pct_200m: mt.rel_elev_pct_200m,
|
| 390 |
+
rel_elev_pct_750m: mt.rel_elev_pct_750m,
|
| 391 |
+
basin_relief_m: mt.basin_relief_m,
|
| 392 |
+
// Empirical
|
| 393 |
+
sandy: !!ev.sandy,
|
| 394 |
+
ida_hwm_within_100m: (ida.nearest_dist_m != null && ida.nearest_dist_m < 100) ||
|
| 395 |
+
(ida.n_within_radius || 0) > 0 && (ida.nearest_dist_m || 9999) < 100,
|
| 396 |
+
ida_hwm_within_800m: (ida.n_within_radius || 0) > 0,
|
| 397 |
+
prithvi_polygon: !!pw.inside_water_polygon,
|
| 398 |
+
complaints_count: ev.nyc311?.n || 0,
|
| 399 |
+
floodnet_trigger: (ev.floodnet?.n_flood_events_3y || 0) > 0,
|
| 400 |
+
};
|
| 401 |
+
|
| 402 |
+
// Regulatory sub-index (binary signals)
|
| 403 |
+
let regRaw = 0;
|
| 404 |
+
for (const [k, w] of Object.entries(REG_W)) regRaw += s[k] ? w : 0;
|
| 405 |
+
const reg = regRaw / sumW(REG_W);
|
| 406 |
+
|
| 407 |
+
// Hydrological sub-index (banded continuous)
|
| 408 |
+
const hydBands = {
|
| 409 |
+
hand_band: handBand(s.hand_m),
|
| 410 |
+
twi_quartile: twiBand(s.twi),
|
| 411 |
+
elev_pct_200m_inv: pctInvBand(s.rel_elev_pct_200m),
|
| 412 |
+
elev_pct_750m_inv: pctInvBand(s.rel_elev_pct_750m),
|
| 413 |
+
basin_relief_band: reliefBand(s.basin_relief_m),
|
| 414 |
+
};
|
| 415 |
+
let hydRaw = 0;
|
| 416 |
+
for (const [k, w] of Object.entries(HYD_W)) hydRaw += w * hydBands[k];
|
| 417 |
+
const hyd = hydRaw / sumW(HYD_W);
|
| 418 |
+
|
| 419 |
+
// Empirical sub-index
|
| 420 |
+
const empVals = {
|
| 421 |
+
sandy: s.sandy ? 1 : 0,
|
| 422 |
+
ida_hwm_within_100m: s.ida_hwm_within_100m ? 1 : 0,
|
| 423 |
+
ida_hwm_within_800m: s.ida_hwm_within_800m ? 1 : 0,
|
| 424 |
+
prithvi_polygon: s.prithvi_polygon ? 1 : 0,
|
| 425 |
+
complaints_band: complBand(s.complaints_count),
|
| 426 |
+
floodnet_trigger: s.floodnet_trigger ? 1 : 0,
|
| 427 |
+
};
|
| 428 |
+
let empRaw = 0;
|
| 429 |
+
for (const [k, w] of Object.entries(EMP_W)) empRaw += w * empVals[k];
|
| 430 |
+
const emp = empRaw / sumW(EMP_W);
|
| 431 |
+
|
| 432 |
+
const composite = reg + hyd + emp;
|
| 433 |
+
|
| 434 |
+
// Tier breakpoints (mirror score.py)
|
| 435 |
+
let tier = 0;
|
| 436 |
+
if (composite >= 1.50) tier = 1;
|
| 437 |
+
else if (composite >= 1.00) tier = 2;
|
| 438 |
+
else if (composite >= 0.50) tier = 3;
|
| 439 |
+
else if (composite >= 0.01) tier = 4;
|
| 440 |
+
|
| 441 |
+
// Max-empirical floor: Sandy or HWM-within-100m → tier ≤ 2
|
| 442 |
+
const floorApplied = !!(s.sandy || s.ida_hwm_within_100m);
|
| 443 |
+
if (floorApplied && (tier === 0 || tier > 2)) tier = 2;
|
| 444 |
+
|
| 445 |
+
return {
|
| 446 |
+
subindices: {regulatory: reg, hydrological: hyd, empirical: emp},
|
| 447 |
+
composite, tier, floorApplied,
|
| 448 |
+
};
|
| 449 |
}
|
| 450 |
|
| 451 |
+
// Backward-compat shim: places that called computeScore() now read .tier.
|
| 452 |
+
function computeScore(ev) { return computeComposite(ev).tier; }
|
| 453 |
+
|
| 454 |
function renderHeader(ev) {
|
| 455 |
const geo = ev.geocode || {};
|
| 456 |
$("#reportAddr").textContent = geo.address || "(unresolved)";
|
|
|
|
| 460 |
}
|
| 461 |
|
| 462 |
function renderTier(ev) {
|
| 463 |
+
const c = computeComposite(ev);
|
| 464 |
+
const m = tierMeta(c.tier);
|
| 465 |
const badge = $("#tierBadge");
|
| 466 |
badge.className = "tier-badge t-" + m.tier;
|
| 467 |
$("#tierNum").textContent = m.tier;
|
| 468 |
+
const floor = c.floorApplied ? " · empirical floor" : "";
|
| 469 |
+
$("#tierLabel").textContent = `Tier ${m.tier} — ${m.label}${floor}`;
|
| 470 |
$("#tierHelp").textContent = m.help;
|
| 471 |
}
|
| 472 |
|
|
|
|
| 704 |
}));
|
| 705 |
}
|
| 706 |
|
| 707 |
+
// Live signals — refresh every query, may produce nothing on a calm day.
|
| 708 |
+
const tides = ev.noaa_tides;
|
| 709 |
+
if (tides && tides.observed_ft_mllw != null) {
|
| 710 |
+
const rows = [
|
| 711 |
+
["Gauge", `${tides.station_name} (${tides.station_id})`],
|
| 712 |
+
["Distance to gauge", `${tides.distance_km} km`],
|
| 713 |
+
["Observed", `${tides.observed_ft_mllw} ft above MLLW`],
|
| 714 |
+
];
|
| 715 |
+
if (tides.predicted_ft_mllw != null)
|
| 716 |
+
rows.push(["Predicted (astro tide)", `${tides.predicted_ft_mllw} ft`]);
|
| 717 |
+
if (tides.residual_ft != null)
|
| 718 |
+
rows.push(["Residual (obs − pred)", `${tides.residual_ft >= 0 ? "+" : ""}${tides.residual_ft} ft`]);
|
| 719 |
+
if (tides.obs_time)
|
| 720 |
+
rows.push(["Observation time", tides.obs_time]);
|
| 721 |
+
const flag = (tides.residual_ft != null && tides.residual_ft >= 1.0) ? "hit" : "note";
|
| 722 |
+
cards.push(evCard({
|
| 723 |
+
key: "noaa_tides",
|
| 724 |
+
title: "NOAA Tides & Currents — live coastal water level",
|
| 725 |
+
flag, rows,
|
| 726 |
+
sourceText: "NOAA CO-OPS API (api.tidesandcurrents.noaa.gov)",
|
| 727 |
+
sourceUrl: `https://tidesandcurrents.noaa.gov/stationhome.html?id=${tides.station_id}`,
|
| 728 |
+
vintage: "live, 6-min cadence; residual ≈ surge",
|
| 729 |
+
collapsed: false,
|
| 730 |
+
}));
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
const al = ev.nws_alerts;
|
| 734 |
+
if (al && al.n_active > 0) {
|
| 735 |
+
const rows = [["Active flood-relevant alerts", String(al.n_active)]];
|
| 736 |
+
(al.alerts || []).slice(0, 3).forEach((a, i) => {
|
| 737 |
+
rows.push([
|
| 738 |
+
`Alert ${i + 1}`,
|
| 739 |
+
`${a.event} (${a.severity || "?"} / ${a.urgency || "?"}) — expires ${
|
| 740 |
+
(a.expires || "").slice(0, 16)
|
| 741 |
+
}`,
|
| 742 |
+
]);
|
| 743 |
+
});
|
| 744 |
+
cards.push(evCard({
|
| 745 |
+
key: "nws_alerts",
|
| 746 |
+
title: "NWS — active flood alerts at this point",
|
| 747 |
+
flag: "hit", rows,
|
| 748 |
+
sourceText: "NWS Public Alerts API (api.weather.gov)",
|
| 749 |
+
sourceUrl: "https://www.weather.gov/documentation/services-web-api",
|
| 750 |
+
vintage: "live, push-cadence (refresh on event)",
|
| 751 |
+
collapsed: false,
|
| 752 |
+
}));
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
const obs = ev.nws_obs;
|
| 756 |
+
if (obs && obs.station_id && !obs.error && (
|
| 757 |
+
obs.precip_last_hour_mm != null ||
|
| 758 |
+
obs.precip_last_6h_mm != null)) {
|
| 759 |
+
const rows = [
|
| 760 |
+
["Nearest ASOS station", `${obs.station_name} (${obs.station_id})`],
|
| 761 |
+
["Distance", `${obs.distance_km} km`],
|
| 762 |
+
];
|
| 763 |
+
if (obs.precip_last_hour_mm != null)
|
| 764 |
+
rows.push(["Precip last 1 h", `${obs.precip_last_hour_mm} mm`]);
|
| 765 |
+
if (obs.precip_last_3h_mm != null)
|
| 766 |
+
rows.push(["Precip last 3 h", `${obs.precip_last_3h_mm} mm`]);
|
| 767 |
+
if (obs.precip_last_6h_mm != null)
|
| 768 |
+
rows.push(["Precip last 6 h", `${obs.precip_last_6h_mm} mm`]);
|
| 769 |
+
if (obs.obs_time)
|
| 770 |
+
rows.push(["Observation time", obs.obs_time]);
|
| 771 |
+
const heavy = (obs.precip_last_hour_mm || 0) >= 10 ||
|
| 772 |
+
(obs.precip_last_6h_mm || 0) >= 25;
|
| 773 |
+
cards.push(evCard({
|
| 774 |
+
key: "nws_obs",
|
| 775 |
+
title: "NWS hourly METAR — recent precipitation",
|
| 776 |
+
flag: heavy ? "hit" : "note", rows,
|
| 777 |
+
sourceText: "NWS station observations API",
|
| 778 |
+
sourceUrl: `https://www.weather.gov/wrh/timeseries?site=${obs.station_id}`,
|
| 779 |
+
vintage: "live, ~hourly",
|
| 780 |
+
collapsed: false,
|
| 781 |
+
}));
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
const ttm = ev.ttm_forecast;
|
| 785 |
+
if (ttm && ttm.available) {
|
| 786 |
+
const peak = ttm.forecast_peak_ft;
|
| 787 |
+
const rows = [
|
| 788 |
+
["Gauge", `${ttm.station_name} (NOAA ${ttm.station_id})`],
|
| 789 |
+
["Recent residual", `${ttm.history_recent_ft} ft`],
|
| 790 |
+
["Recent peak |residual|", `${ttm.history_peak_abs_ft} ft (last ~51 h)`],
|
| 791 |
+
["Forecast peak residual", `${peak >= 0 ? "+" : ""}${peak} ft`],
|
| 792 |
+
["Forecast peak time", `~${ttm.forecast_peak_minutes_ahead} min ahead (${(ttm.forecast_peak_time_utc || "").slice(11, 16)} UTC)`],
|
| 793 |
+
["Threshold", `±${ttm.threshold_ft} ft (gate for emission)`],
|
| 794 |
+
];
|
| 795 |
+
const flag = ttm.interesting ? (Math.abs(peak) >= 0.5 ? "hit" : "note") : "miss";
|
| 796 |
+
cards.push(evCard({
|
| 797 |
+
key: "ttm_forecast",
|
| 798 |
+
title: "Granite TimeSeries TTM r2 — surge nowcast",
|
| 799 |
+
flag, rows,
|
| 800 |
+
sourceText: "IBM Granite TimeSeries TTM r2 (Ekambaram et al. 2024, NeurIPS)",
|
| 801 |
+
sourceUrl: "https://huggingface.co/ibm-granite/granite-timeseries-ttm-r2",
|
| 802 |
+
vintage: "zero-shot multivariate forecaster, ~1.5M params; runs on CPU",
|
| 803 |
+
collapsed: !ttm.interesting,
|
| 804 |
+
}));
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
$("#evidenceCards").innerHTML = cards.join("");
|
| 808 |
}
|
| 809 |
|
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// <r-briefing> — the streaming-token, citation-chipped briefing panel.
|
| 2 |
+
//
|
| 3 |
+
// Replaces the agent.js renderMarkdown + rewriteCitations + paint
|
| 4 |
+
// scheduler. Token streaming becomes "append to a signal, re-render."
|
| 5 |
+
//
|
| 6 |
+
// Properties:
|
| 7 |
+
// text — full markdown text (set by parent on token / final events)
|
| 8 |
+
// streaming — bool; shows the blinking caret
|
| 9 |
+
// citeIndex — { doc_id: number } shared with <r-sources-footer>
|
| 10 |
+
// sourceLabels — passed through for chip tooltips
|
| 11 |
+
//
|
| 12 |
+
// Signals consumed:
|
| 13 |
+
// highlightedDocId — toggles `.hl` on chips reactively (set by
|
| 14 |
+
// <r-sources-footer> on hover)
|
| 15 |
+
// Signals updated:
|
| 16 |
+
// citeIndex — populated as citations are encountered in the text
|
| 17 |
+
// highlightedDocId — set on chip hover/click
|
| 18 |
+
|
| 19 |
+
import { html, LitElement } from "https://esm.sh/lit@3";
|
| 20 |
+
import { unsafeHTML } from "https://esm.sh/lit@3/directives/unsafe-html.js";
|
| 21 |
+
import { SignalWatcher } from "https://esm.sh/@lit-labs/signals@0.1.x";
|
| 22 |
+
import { citeIndex, highlightedDocId } from "./signals.js";
|
| 23 |
+
|
| 24 |
+
// Same minimal markdown subset as agent.js renderMarkdown — kept
|
| 25 |
+
// duplicated for now; will collapse when agent.js stops calling
|
| 26 |
+
// renderMarkdown. After full port this is the only impl.
|
| 27 |
+
function renderMarkdownPure(text) {
|
| 28 |
+
const lines = text.split("\n");
|
| 29 |
+
const out = [];
|
| 30 |
+
let para = []; let bullets = [];
|
| 31 |
+
const escapeHtml = (s) =>
|
| 32 |
+
String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
| 33 |
+
const flushPara = () => {
|
| 34 |
+
if (!para.length) return;
|
| 35 |
+
const safe = escapeHtml(para.join(" ").trim())
|
| 36 |
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
| 37 |
+
if (safe) out.push(`<p class="rsum-p">${safe}</p>`);
|
| 38 |
+
para = [];
|
| 39 |
+
};
|
| 40 |
+
const flushBullets = () => {
|
| 41 |
+
if (!bullets.length) return;
|
| 42 |
+
const items = bullets.map(b => {
|
| 43 |
+
const safe = escapeHtml(b.trim()).replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
| 44 |
+
return `<li>${safe}</li>`;
|
| 45 |
+
}).join("");
|
| 46 |
+
out.push(`<ul class="rsum-list">${items}</ul>`);
|
| 47 |
+
bullets = [];
|
| 48 |
+
};
|
| 49 |
+
// Granite sometimes runs all bullets onto one line.
|
| 50 |
+
const expanded = [];
|
| 51 |
+
for (const line of lines) {
|
| 52 |
+
if (line.trim().startsWith("- ") && line.includes(" - ", 2)) {
|
| 53 |
+
const parts = line.split(/(?:^|(?<=\.\s))\s*-\s+/g).filter(p => p.trim());
|
| 54 |
+
for (const p of parts) expanded.push("- " + p.trim());
|
| 55 |
+
} else { expanded.push(line); }
|
| 56 |
+
}
|
| 57 |
+
for (const line of expanded) {
|
| 58 |
+
const m = line.match(/^\s*\*\*([A-Z][A-Za-z\s/]+)\.\*\*\s*$/);
|
| 59 |
+
if (m) { flushPara(); flushBullets(); out.push(`<h4 class="rsum-h">${escapeHtml(m[1])}</h4>`); }
|
| 60 |
+
else if (/^\s*[-*]\s+/.test(line)) { flushPara(); bullets.push(line.replace(/^\s*[-*]\s+/, "")); }
|
| 61 |
+
else { flushBullets(); para.push(line); }
|
| 62 |
+
}
|
| 63 |
+
flushPara(); flushBullets();
|
| 64 |
+
return out.join("");
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function rewriteCitations(html, sourceLabels, indexMap) {
|
| 68 |
+
return html.replace(/\[([a-z0-9_]+)\]/gi, (_, id) => {
|
| 69 |
+
const norm = id.toLowerCase();
|
| 70 |
+
if (indexMap[norm] == null) indexMap[norm] = Object.keys(indexMap).length + 1;
|
| 71 |
+
const n = indexMap[norm];
|
| 72 |
+
const lab = sourceLabels[norm] || norm;
|
| 73 |
+
return `<span class="cite" data-src-id="${norm}" data-src-n="${n}" title="${lab.replace(/"/g, """)} — click to highlight in Sources">${n}</span>`;
|
| 74 |
+
});
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
export class Briefing extends SignalWatcher(LitElement) {
|
| 78 |
+
static properties = {
|
| 79 |
+
text: { type: String },
|
| 80 |
+
streaming: { type: Boolean, reflect: true },
|
| 81 |
+
sourceLabels: { type: Object },
|
| 82 |
+
};
|
| 83 |
+
|
| 84 |
+
// No shadow DOM — we use the parent's `.report-pane #paragraph` styles
|
| 85 |
+
// directly so the markdown renders match the legacy/print idiom.
|
| 86 |
+
createRenderRoot() { return this; }
|
| 87 |
+
|
| 88 |
+
constructor() {
|
| 89 |
+
super();
|
| 90 |
+
this.text = "";
|
| 91 |
+
this.streaming = false;
|
| 92 |
+
this.sourceLabels = {};
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
updated(changed) {
|
| 96 |
+
if (changed.has("text") && this.text) {
|
| 97 |
+
// Bind chip hover/click to the highlight signal post-render.
|
| 98 |
+
this._bindChips();
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
_bindChips() {
|
| 103 |
+
this.querySelectorAll(".cite").forEach(c => {
|
| 104 |
+
const id = c.dataset.srcId;
|
| 105 |
+
if (!id || c.dataset.signalBound) return;
|
| 106 |
+
c.dataset.signalBound = "1";
|
| 107 |
+
c.addEventListener("mouseenter", () => highlightedDocId.set(id));
|
| 108 |
+
c.addEventListener("click", (e) => {
|
| 109 |
+
e.stopPropagation();
|
| 110 |
+
const cur = highlightedDocId.get();
|
| 111 |
+
highlightedDocId.set(cur === id ? null : id);
|
| 112 |
+
});
|
| 113 |
+
});
|
| 114 |
+
// Apply highlight class reactively from current signal value.
|
| 115 |
+
const hl = highlightedDocId.get();
|
| 116 |
+
this.querySelectorAll(".cite").forEach(c => {
|
| 117 |
+
c.classList.toggle("hl", c.dataset.srcId === hl);
|
| 118 |
+
});
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
render() {
|
| 122 |
+
if (!this.text) return html`<div class="rsum-p" style="color:var(--text-muted)">Waiting for content…</div>`;
|
| 123 |
+
const indexMap = {};
|
| 124 |
+
const md = renderMarkdownPure(this.text);
|
| 125 |
+
const withCites = rewriteCitations(md, this.sourceLabels, indexMap);
|
| 126 |
+
// Push the citation index up to the shared signal so SourcesFooter
|
| 127 |
+
// re-renders. Done in render() because indexMap is computed here.
|
| 128 |
+
queueMicrotask(() => citeIndex.set({ ...indexMap }));
|
| 129 |
+
return html`${unsafeHTML(withCites)}`;
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
customElements.define("r-briefing", Briefing);
|
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Shared reactive state for Riprap web components.
|
| 2 |
+
//
|
| 3 |
+
// Lit components import these signals; updating one signal re-renders
|
| 4 |
+
// every subscribed component. Replaces the hand-wired DOM-querying
|
| 5 |
+
// cross-linking we used to do in vanilla JS.
|
| 6 |
+
|
| 7 |
+
import { signal } from "https://esm.sh/@lit-labs/signals@0.1.x";
|
| 8 |
+
|
| 9 |
+
// Currently-highlighted citation doc_id. When a Briefing chip is hovered
|
| 10 |
+
// or clicked, this gets set; SourcesFooter observes it and highlights
|
| 11 |
+
// the matching row, and vice versa.
|
| 12 |
+
export const highlightedDocId = signal(null);
|
| 13 |
+
|
| 14 |
+
// The full agent run output (from /api/agent/stream `final` event).
|
| 15 |
+
// Components that need the result post-render read from this.
|
| 16 |
+
export const lastResult = signal(null);
|
| 17 |
+
|
| 18 |
+
// The cite-index map { doc_id: number } populated by Briefing as it
|
| 19 |
+
// renders the streamed markdown. SourcesFooter reads it to know which
|
| 20 |
+
// numbered rows to render.
|
| 21 |
+
export const citeIndex = signal({});
|
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// <r-sources-footer> — numbered, hyperlinked, vintage-aware Sources
|
| 2 |
+
// section that appears below the Briefing in the agent UI and inside
|
| 3 |
+
// the printable /report.
|
| 4 |
+
//
|
| 5 |
+
// Shared signals power the cross-linking with <r-briefing>: hovering
|
| 6 |
+
// a [N] chip in the prose highlights the matching <li> here, and
|
| 7 |
+
// clicking either side persists the highlight + scrolls into view.
|
| 8 |
+
//
|
| 9 |
+
// Mounts via <r-sources-footer></r-sources-footer>. Reads:
|
| 10 |
+
// - citeIndex — { doc_id: number } from Briefing
|
| 11 |
+
// - highlightedDocId — current highlight target (in/out)
|
| 12 |
+
// Plus three label/url/vintage maps passed in as properties.
|
| 13 |
+
|
| 14 |
+
import { html, css, LitElement } from "https://esm.sh/lit@3";
|
| 15 |
+
import { SignalWatcher } from "https://esm.sh/@lit-labs/signals@0.1.x";
|
| 16 |
+
import { citeIndex, highlightedDocId } from "./signals.js";
|
| 17 |
+
|
| 18 |
+
export class SourcesFooter extends SignalWatcher(LitElement) {
|
| 19 |
+
static properties = {
|
| 20 |
+
labels: { type: Object },
|
| 21 |
+
urls: { type: Object },
|
| 22 |
+
vintages: { type: Object },
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
static styles = css`
|
| 26 |
+
:host {
|
| 27 |
+
display: block;
|
| 28 |
+
border-top: 1px solid var(--line, #e5e7eb);
|
| 29 |
+
background: var(--bg-soft, #f5f7fb);
|
| 30 |
+
padding: 12px 16px 14px;
|
| 31 |
+
}
|
| 32 |
+
:host([hidden]) { display: none; }
|
| 33 |
+
.src-h {
|
| 34 |
+
font-size: 10px; font-weight: 700;
|
| 35 |
+
text-transform: uppercase; letter-spacing: 0.10em;
|
| 36 |
+
color: var(--text-muted, #6b7280);
|
| 37 |
+
margin: 0 0 8px;
|
| 38 |
+
}
|
| 39 |
+
ol {
|
| 40 |
+
margin: 0; padding: 0; list-style: none;
|
| 41 |
+
display: grid; gap: 6px;
|
| 42 |
+
font-size: 11.5px; line-height: 1.45;
|
| 43 |
+
}
|
| 44 |
+
li {
|
| 45 |
+
display: grid; grid-template-columns: 22px 1fr;
|
| 46 |
+
gap: 8px; align-items: baseline;
|
| 47 |
+
padding: 4px 6px; border-radius: 3px;
|
| 48 |
+
cursor: pointer;
|
| 49 |
+
transition: background 0.15s;
|
| 50 |
+
}
|
| 51 |
+
li:hover, li.hl {
|
| 52 |
+
background: rgba(22, 66, 223, 0.10);
|
| 53 |
+
}
|
| 54 |
+
.src-num {
|
| 55 |
+
font-family: var(--mono, monospace); font-size: 10.5px;
|
| 56 |
+
font-weight: 700; color: var(--nyc-blue, #1642DF);
|
| 57 |
+
text-align: right;
|
| 58 |
+
}
|
| 59 |
+
.src-link {
|
| 60 |
+
color: var(--text, #111); text-decoration: none;
|
| 61 |
+
border-bottom: 1px dotted var(--text-muted, #6b7280);
|
| 62 |
+
transition: color 0.12s, border-color 0.12s;
|
| 63 |
+
}
|
| 64 |
+
.src-link:hover {
|
| 65 |
+
color: var(--nyc-blue, #1642DF);
|
| 66 |
+
border-bottom-color: var(--nyc-blue, #1642DF);
|
| 67 |
+
}
|
| 68 |
+
.src-ext {
|
| 69 |
+
font-size: 9.5px; color: var(--text-faint, #9ca3af);
|
| 70 |
+
margin-left: 2px; vertical-align: super;
|
| 71 |
+
}
|
| 72 |
+
.src-vintage {
|
| 73 |
+
display: block; color: var(--text-muted, #6b7280);
|
| 74 |
+
font-size: 9.5px; margin-top: 2px;
|
| 75 |
+
}
|
| 76 |
+
.src-id {
|
| 77 |
+
display: inline-block;
|
| 78 |
+
font-family: var(--mono, monospace); font-size: 9.5px;
|
| 79 |
+
color: var(--text-faint, #9ca3af); margin-left: 6px;
|
| 80 |
+
}
|
| 81 |
+
`;
|
| 82 |
+
|
| 83 |
+
constructor() {
|
| 84 |
+
super();
|
| 85 |
+
this.labels = {};
|
| 86 |
+
this.urls = {};
|
| 87 |
+
this.vintages = {};
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
_entries() {
|
| 91 |
+
return Object.entries(citeIndex.get() || {}).sort((a, b) => a[1] - b[1]);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
_onHover(id) {
|
| 95 |
+
highlightedDocId.set(id);
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
_onLeave() {
|
| 99 |
+
// Only clear if not pinned by click — keep highlight on click.
|
| 100 |
+
// For now, hover-only highlight clears on leave.
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
_onClick(id) {
|
| 104 |
+
const cur = highlightedDocId.get();
|
| 105 |
+
highlightedDocId.set(cur === id ? null : id);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
render() {
|
| 109 |
+
const entries = this._entries();
|
| 110 |
+
if (!entries.length) {
|
| 111 |
+
this.setAttribute("hidden", "");
|
| 112 |
+
return html``;
|
| 113 |
+
}
|
| 114 |
+
this.removeAttribute("hidden");
|
| 115 |
+
const hl = highlightedDocId.get();
|
| 116 |
+
return html`
|
| 117 |
+
<div class="src-h">Sources</div>
|
| 118 |
+
<ol>
|
| 119 |
+
${entries.map(([id, n]) => {
|
| 120 |
+
const url = this.urls[id];
|
| 121 |
+
const label = this.labels[id] || id;
|
| 122 |
+
const vintage = this.vintages[id];
|
| 123 |
+
const cls = id === hl ? "hl" : "";
|
| 124 |
+
return html`
|
| 125 |
+
<li class="${cls}"
|
| 126 |
+
@mouseenter=${() => this._onHover(id)}
|
| 127 |
+
@click=${() => this._onClick(id)}>
|
| 128 |
+
<span class="src-num">[${n}]</span>
|
| 129 |
+
<div>
|
| 130 |
+
${url
|
| 131 |
+
? html`<a class="src-link" href="${url}" target="_blank" rel="noopener noreferrer" @click=${(e) => e.stopPropagation()}>${label} <span class="src-ext">↗</span></a>`
|
| 132 |
+
: html`<span>${label}</span>`}
|
| 133 |
+
<span class="src-id">${id}</span>
|
| 134 |
+
${vintage ? html`<span class="src-vintage">${vintage}</span>` : ""}
|
| 135 |
+
</div>
|
| 136 |
+
</li>
|
| 137 |
+
`;
|
| 138 |
+
})}
|
| 139 |
+
</ol>
|
| 140 |
+
`;
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
customElements.define("r-sources-footer", SourcesFooter);
|
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// <r-trace> — specialist trail. Reactive list of pipeline steps.
|
| 2 |
+
//
|
| 3 |
+
// API:
|
| 4 |
+
// .pushStep(step) — append a {step, ok, elapsed_s, result, err} record
|
| 5 |
+
// .clear() — reset
|
| 6 |
+
// .meta = "1.4s" — text shown in the header
|
| 7 |
+
// .stepLabels = {...} — { stepName: [label, hint] } map (set once at boot)
|
| 8 |
+
//
|
| 9 |
+
// Light DOM (no shadow) so the existing `#steps li.ok / .err / .running`
|
| 10 |
+
// CSS in agent.html keeps applying without rewrites.
|
| 11 |
+
|
| 12 |
+
import { html, css, LitElement } from "https://esm.sh/lit@3";
|
| 13 |
+
|
| 14 |
+
const escapeHtml = (s) =>
|
| 15 |
+
String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
| 16 |
+
|
| 17 |
+
export class Trace extends LitElement {
|
| 18 |
+
static properties = {
|
| 19 |
+
steps: { type: Array, state: true },
|
| 20 |
+
meta: { type: String, reflect: true },
|
| 21 |
+
stepLabels: { type: Object },
|
| 22 |
+
};
|
| 23 |
+
|
| 24 |
+
createRenderRoot() { return this; }
|
| 25 |
+
|
| 26 |
+
constructor() {
|
| 27 |
+
super();
|
| 28 |
+
this.steps = [];
|
| 29 |
+
this.meta = "";
|
| 30 |
+
this.stepLabels = {};
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
pushStep(step) {
|
| 34 |
+
this.steps = [...this.steps, step];
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
clear() {
|
| 38 |
+
this.steps = [];
|
| 39 |
+
this.meta = "";
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
_renderStep(step) {
|
| 43 |
+
const [label, hint] = this.stepLabels[step.step] || [step.step, ""];
|
| 44 |
+
const ok = step.ok === true;
|
| 45 |
+
const fail = step.ok === false;
|
| 46 |
+
const cls = ok ? "ok" : fail ? "err" : "running";
|
| 47 |
+
const mark = ok ? "✓" : fail ? "✗" : "○";
|
| 48 |
+
const time = step.elapsed_s != null
|
| 49 |
+
? `<span class="time">${step.elapsed_s}s</span>` : "";
|
| 50 |
+
const result = step.result
|
| 51 |
+
? `<div class="result">${escapeHtml(JSON.stringify(step.result))}</div>` : "";
|
| 52 |
+
const err = step.err
|
| 53 |
+
? `<div class="result" style="color:var(--nyc-scarlet)">${escapeHtml(step.err)}</div>` : "";
|
| 54 |
+
// Inner HTML is hand-built so the existing list CSS targets the same
|
| 55 |
+
// structure as the legacy renderer; we keep .innerHTML rather than
|
| 56 |
+
// Lit's html`` for byte-for-byte parity here.
|
| 57 |
+
const li = document.createElement("li");
|
| 58 |
+
li.className = cls;
|
| 59 |
+
li.innerHTML = `
|
| 60 |
+
<span class="icon">${mark}</span>
|
| 61 |
+
<div>
|
| 62 |
+
<div class="label">${escapeHtml(label)}</div>
|
| 63 |
+
<div class="meta">${escapeHtml(hint)}</div>
|
| 64 |
+
</div>
|
| 65 |
+
${time}
|
| 66 |
+
${result}
|
| 67 |
+
${err}
|
| 68 |
+
`;
|
| 69 |
+
return li;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
render() {
|
| 73 |
+
// Render the <ol> as innerHTML on update so we don't fight Lit's
|
| 74 |
+
// template diffing for raw HTML lists.
|
| 75 |
+
queueMicrotask(() => {
|
| 76 |
+
const ol = this.querySelector("ol#steps-list");
|
| 77 |
+
if (!ol) return;
|
| 78 |
+
ol.innerHTML = "";
|
| 79 |
+
for (const s of this.steps) ol.appendChild(this._renderStep(s));
|
| 80 |
+
});
|
| 81 |
+
// Inline reset so the legacy `#steps { list-style: none; ... }` rules
|
| 82 |
+
// (which now target the host element, not the <ol>) keep applying.
|
| 83 |
+
return html`<ol id="steps-list" style="list-style:none; margin:0; padding:4px 0; font-size:12.5px;"></ol>`;
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
customElements.define("r-trace", Trace);
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -14,7 +14,7 @@
|
|
| 14 |
<div class="brand">
|
| 15 |
<span class="brand-name">Riprap</span>
|
| 16 |
<span class="brand-sep">·</span>
|
| 17 |
-
<span class="brand-tag">citation-grounded
|
| 18 |
</div>
|
| 19 |
<div class="topbar-right">
|
| 20 |
<a href="/compare" class="modelink">compare</a>
|
|
|
|
| 14 |
<div class="brand">
|
| 15 |
<span class="brand-name">Riprap</span>
|
| 16 |
<span class="brand-sep">·</span>
|
| 17 |
+
<span class="brand-tag">citation-grounded flood-exposure briefings for NYC — not a risk score</span>
|
| 18 |
</div>
|
| 19 |
<div class="topbar-right">
|
| 20 |
<a href="/compare" class="modelink">compare</a>
|
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<title>Riprap — auditable flood-exposure report</title>
|
| 6 |
+
<link rel="stylesheet" href="/static/style.css">
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--paper-w: 8.5in;
|
| 10 |
+
--paper-pad: 0.55in;
|
| 11 |
+
}
|
| 12 |
+
* { box-sizing: border-box; }
|
| 13 |
+
body {
|
| 14 |
+
background: #e9eef5; margin: 0; padding: 24px 0;
|
| 15 |
+
font-family: var(--font-sans), system-ui, sans-serif;
|
| 16 |
+
color: var(--text);
|
| 17 |
+
}
|
| 18 |
+
.report-controls {
|
| 19 |
+
max-width: var(--paper-w); margin: 0 auto 16px; padding: 0 12px;
|
| 20 |
+
display: flex; gap: 12px; align-items: center;
|
| 21 |
+
}
|
| 22 |
+
.report-controls .lhs {
|
| 23 |
+
font-size: 12px; color: var(--text-muted);
|
| 24 |
+
}
|
| 25 |
+
.report-controls .actions {
|
| 26 |
+
margin-left: auto; display: flex; gap: 8px;
|
| 27 |
+
}
|
| 28 |
+
.btn-primary {
|
| 29 |
+
padding: 8px 16px; border: 0; border-radius: 4px;
|
| 30 |
+
background: var(--nyc-blue); color: white;
|
| 31 |
+
font-weight: 600; font-size: 13px; cursor: pointer;
|
| 32 |
+
font-family: inherit;
|
| 33 |
+
}
|
| 34 |
+
.btn-secondary {
|
| 35 |
+
padding: 8px 16px; border: 1px solid var(--line);
|
| 36 |
+
background: var(--panel); color: var(--text);
|
| 37 |
+
border-radius: 4px; font-size: 13px; cursor: pointer;
|
| 38 |
+
font-family: inherit; font-weight: 500;
|
| 39 |
+
}
|
| 40 |
+
.btn-primary:hover { background: #0f329d; }
|
| 41 |
+
.btn-secondary:hover { background: var(--bg-soft); }
|
| 42 |
+
|
| 43 |
+
.paper {
|
| 44 |
+
background: white;
|
| 45 |
+
width: var(--paper-w);
|
| 46 |
+
margin: 0 auto;
|
| 47 |
+
padding: var(--paper-pad);
|
| 48 |
+
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
|
| 49 |
+
font-size: 11pt;
|
| 50 |
+
line-height: 1.45;
|
| 51 |
+
color: #111;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/* report-specific typography */
|
| 55 |
+
.r-head {
|
| 56 |
+
border-bottom: 2px solid #111;
|
| 57 |
+
padding-bottom: 12px; margin-bottom: 18px;
|
| 58 |
+
}
|
| 59 |
+
.r-brand {
|
| 60 |
+
font-weight: 700; font-size: 18pt; color: var(--nyc-blue);
|
| 61 |
+
letter-spacing: -0.01em;
|
| 62 |
+
}
|
| 63 |
+
.r-tagline {
|
| 64 |
+
font-size: 9pt; color: #555; margin-top: 2px;
|
| 65 |
+
text-transform: uppercase; letter-spacing: 0.06em;
|
| 66 |
+
}
|
| 67 |
+
.r-meta-grid {
|
| 68 |
+
display: grid; grid-template-columns: max-content 1fr;
|
| 69 |
+
gap: 4px 12px; font-size: 9.5pt; margin-top: 12px;
|
| 70 |
+
}
|
| 71 |
+
.r-meta-grid dt {
|
| 72 |
+
color: #666; font-weight: 600;
|
| 73 |
+
text-transform: uppercase; letter-spacing: 0.05em;
|
| 74 |
+
font-size: 8.5pt;
|
| 75 |
+
}
|
| 76 |
+
.r-meta-grid dd { margin: 0; color: #111; }
|
| 77 |
+
.r-meta-grid dd.mono { font-family: var(--mono); font-size: 9pt; }
|
| 78 |
+
|
| 79 |
+
.r-section {
|
| 80 |
+
margin-top: 22px;
|
| 81 |
+
page-break-inside: avoid;
|
| 82 |
+
}
|
| 83 |
+
.r-section h2 {
|
| 84 |
+
font-size: 9pt; font-weight: 700;
|
| 85 |
+
text-transform: uppercase; letter-spacing: 0.10em;
|
| 86 |
+
color: #444; margin: 0 0 8px;
|
| 87 |
+
border-bottom: 1px solid #ccc; padding-bottom: 4px;
|
| 88 |
+
}
|
| 89 |
+
.r-section .lead { font-size: 10pt; color: #333; margin-bottom: 8px; }
|
| 90 |
+
|
| 91 |
+
.r-query {
|
| 92 |
+
background: #f5f7fb; padding: 10px 14px;
|
| 93 |
+
border-left: 3px solid var(--nyc-blue);
|
| 94 |
+
font-style: italic; font-size: 11pt; color: #222;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.r-plan {
|
| 98 |
+
display: grid; grid-template-columns: max-content 1fr;
|
| 99 |
+
gap: 4px 12px; font-size: 9.5pt;
|
| 100 |
+
}
|
| 101 |
+
.r-plan dt {
|
| 102 |
+
color: #666; font-weight: 600;
|
| 103 |
+
text-transform: uppercase; letter-spacing: 0.05em; font-size: 8pt;
|
| 104 |
+
}
|
| 105 |
+
.r-plan dd { margin: 0; }
|
| 106 |
+
.r-plan dd.mono { font-family: var(--mono); font-size: 9pt; }
|
| 107 |
+
.r-plan-rationale {
|
| 108 |
+
grid-column: 1 / -1;
|
| 109 |
+
margin-top: 6px; padding-top: 6px;
|
| 110 |
+
border-top: 1px dotted #ccc;
|
| 111 |
+
color: #444; font-style: italic; font-size: 9.5pt;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.r-trace {
|
| 115 |
+
width: 100%; border-collapse: collapse; font-size: 8.5pt;
|
| 116 |
+
}
|
| 117 |
+
.r-trace th, .r-trace td {
|
| 118 |
+
text-align: left; padding: 5px 8px;
|
| 119 |
+
border-bottom: 1px solid #eee;
|
| 120 |
+
vertical-align: top;
|
| 121 |
+
}
|
| 122 |
+
.r-trace th {
|
| 123 |
+
background: #f5f7fb; color: #444;
|
| 124 |
+
font-weight: 700; text-transform: uppercase;
|
| 125 |
+
letter-spacing: 0.05em; font-size: 7.5pt;
|
| 126 |
+
}
|
| 127 |
+
.r-trace tr.ok .mark { color: #1a8754; }
|
| 128 |
+
.r-trace tr.err .mark { color: #c0392b; }
|
| 129 |
+
.r-trace .mono { font-family: var(--mono); }
|
| 130 |
+
.r-trace .mark { font-weight: 700; }
|
| 131 |
+
.r-trace .result {
|
| 132 |
+
font-family: var(--mono); font-size: 7.5pt;
|
| 133 |
+
color: #555; word-break: break-word;
|
| 134 |
+
max-width: 280px;
|
| 135 |
+
}
|
| 136 |
+
.r-trace .err-msg {
|
| 137 |
+
color: #c0392b; font-style: italic; font-size: 8pt;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.r-map { margin-top: 6px; }
|
| 141 |
+
.r-map img {
|
| 142 |
+
width: 100%; height: auto; display: block;
|
| 143 |
+
border: 1px solid #ddd; border-radius: 2px;
|
| 144 |
+
}
|
| 145 |
+
.r-map .legend-cap {
|
| 146 |
+
font-size: 8pt; color: #666; margin-top: 4px;
|
| 147 |
+
text-align: center; font-style: italic;
|
| 148 |
+
}
|
| 149 |
+
.r-map.no-map {
|
| 150 |
+
padding: 24px; background: #f5f7fb; border: 1px dashed #ccc;
|
| 151 |
+
color: #888; text-align: center; font-size: 9pt;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.r-briefing { font-size: 10pt; }
|
| 155 |
+
.r-briefing h4 {
|
| 156 |
+
font-size: 9pt; font-weight: 700;
|
| 157 |
+
text-transform: uppercase; letter-spacing: 0.06em;
|
| 158 |
+
color: #555; margin: 14px 0 4px;
|
| 159 |
+
}
|
| 160 |
+
.r-briefing h4:first-child { margin-top: 0; }
|
| 161 |
+
.r-briefing p { margin: 0 0 6px; line-height: 1.55; }
|
| 162 |
+
.r-briefing ul { margin: 4px 0 8px 0; padding-left: 20px; }
|
| 163 |
+
.r-briefing ul li { margin: 3px 0; line-height: 1.5; }
|
| 164 |
+
.r-briefing strong { font-weight: 700; background: #fff7d6; padding: 0 1px; }
|
| 165 |
+
.r-briefing .cite {
|
| 166 |
+
display: inline-block; vertical-align: super;
|
| 167 |
+
font-size: 7pt; font-family: var(--mono);
|
| 168 |
+
padding: 0 4px; margin-left: 1px;
|
| 169 |
+
background: #f5f7fb; color: var(--nyc-blue);
|
| 170 |
+
border-radius: 6px; font-weight: 700;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.r-sources {
|
| 174 |
+
font-size: 9pt;
|
| 175 |
+
}
|
| 176 |
+
.r-sources ol {
|
| 177 |
+
list-style: none; padding: 0; margin: 0;
|
| 178 |
+
}
|
| 179 |
+
.r-sources li {
|
| 180 |
+
display: grid; grid-template-columns: 26px 1fr;
|
| 181 |
+
gap: 8px; padding: 6px 0;
|
| 182 |
+
border-bottom: 1px dotted #ddd;
|
| 183 |
+
page-break-inside: avoid;
|
| 184 |
+
}
|
| 185 |
+
.r-sources li:last-child { border-bottom: 0; }
|
| 186 |
+
.r-sources .num {
|
| 187 |
+
font-family: var(--mono); font-weight: 700;
|
| 188 |
+
color: var(--nyc-blue); font-size: 9pt; text-align: right;
|
| 189 |
+
}
|
| 190 |
+
.r-sources .label { font-weight: 600; color: #111; }
|
| 191 |
+
.r-sources .vintage {
|
| 192 |
+
display: block; color: #666; font-size: 8.5pt; margin-top: 2px;
|
| 193 |
+
}
|
| 194 |
+
.r-sources .url {
|
| 195 |
+
display: block; font-family: var(--mono);
|
| 196 |
+
font-size: 8pt; color: #444; margin-top: 2px;
|
| 197 |
+
word-break: break-all;
|
| 198 |
+
}
|
| 199 |
+
.r-sources .url a { color: var(--nyc-blue); text-decoration: none; }
|
| 200 |
+
|
| 201 |
+
.r-method {
|
| 202 |
+
font-size: 8.5pt; color: #555; line-height: 1.5;
|
| 203 |
+
}
|
| 204 |
+
.r-method p { margin: 0 0 6px; }
|
| 205 |
+
|
| 206 |
+
.r-foot {
|
| 207 |
+
margin-top: 22px; padding-top: 10px;
|
| 208 |
+
border-top: 1px solid #ccc;
|
| 209 |
+
font-size: 7.5pt; color: #888;
|
| 210 |
+
display: flex; justify-content: space-between;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
/* PRINT — strip chrome, force letter, page-break-aware */
|
| 214 |
+
@media print {
|
| 215 |
+
body { background: white; padding: 0; }
|
| 216 |
+
.report-controls { display: none; }
|
| 217 |
+
.paper {
|
| 218 |
+
box-shadow: none; margin: 0;
|
| 219 |
+
width: auto; padding: 0.5in;
|
| 220 |
+
}
|
| 221 |
+
@page { size: letter; margin: 0; }
|
| 222 |
+
}
|
| 223 |
+
</style>
|
| 224 |
+
</head>
|
| 225 |
+
<body>
|
| 226 |
+
<div class="report-controls">
|
| 227 |
+
<div class="lhs"><strong>Auditable report</strong> · ready to print or save as PDF</div>
|
| 228 |
+
<div class="actions">
|
| 229 |
+
<button class="btn-secondary" onclick="window.close()">Close</button>
|
| 230 |
+
<button class="btn-primary" onclick="window.print()">Print / Save PDF</button>
|
| 231 |
+
</div>
|
| 232 |
+
</div>
|
| 233 |
+
|
| 234 |
+
<article class="paper" id="paper">
|
| 235 |
+
<div style="padding: 60px 0; text-align: center; color: #888;">
|
| 236 |
+
Loading report from agent session…
|
| 237 |
+
<br><br>
|
| 238 |
+
<small>If this hangs, the report payload wasn't found in sessionStorage. Re-run the query and click <em>Generate auditable report</em> from there.</small>
|
| 239 |
+
</div>
|
| 240 |
+
</article>
|
| 241 |
+
|
| 242 |
+
<script src="/static/report.js"></script>
|
| 243 |
+
</body>
|
| 244 |
+
</html>
|
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Renders the print-ready auditable report from the agent's last result,
|
| 2 |
+
// passed via sessionStorage. Includes original query, planner decision,
|
| 3 |
+
// full specialist trail, map snapshot, briefing prose with citations,
|
| 4 |
+
// and a Sources section listing every doc_id with its vintage + URL.
|
| 5 |
+
|
| 6 |
+
(function () {
|
| 7 |
+
const raw = sessionStorage.getItem("riprap_report");
|
| 8 |
+
if (!raw) return;
|
| 9 |
+
let pkg;
|
| 10 |
+
try { pkg = JSON.parse(raw); } catch (e) {
|
| 11 |
+
document.getElementById("paper").innerHTML =
|
| 12 |
+
`<p style="color:#c00">Could not parse stored report payload: ${e.message}</p>`;
|
| 13 |
+
return;
|
| 14 |
+
}
|
| 15 |
+
render(pkg);
|
| 16 |
+
})();
|
| 17 |
+
|
| 18 |
+
function escapeHtml(s) {
|
| 19 |
+
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
function render(pkg) {
|
| 23 |
+
const r = pkg.result || {};
|
| 24 |
+
const plan = pkg.plan || r.plan || {};
|
| 25 |
+
const trace = pkg.trace || [];
|
| 26 |
+
const labels = pkg.sourceLabels || {};
|
| 27 |
+
const urls = pkg.sourceUrls || {};
|
| 28 |
+
const vintages = pkg.sourceVintages || {};
|
| 29 |
+
const stepLabels = pkg.stepLabels || {};
|
| 30 |
+
|
| 31 |
+
const intent = r.intent || plan.intent || "—";
|
| 32 |
+
const intentTitleMap = {
|
| 33 |
+
single_address: "Flood-exposure briefing — address",
|
| 34 |
+
neighborhood: "Flood-exposure briefing — neighborhood",
|
| 35 |
+
development_check: "Active development × flood exposure",
|
| 36 |
+
live_now: "Current conditions — NYC",
|
| 37 |
+
};
|
| 38 |
+
const place = (r.target && r.target.nta_name)
|
| 39 |
+
|| (r.geocode && r.geocode.address)
|
| 40 |
+
|| r.place || "—";
|
| 41 |
+
|
| 42 |
+
// Build the citation index from the briefing prose so we render a
|
| 43 |
+
// numbered Sources section in the SAME order the chips appear in the
|
| 44 |
+
// text — same idiom as the agent UI.
|
| 45 |
+
const citeIndex = {};
|
| 46 |
+
const para = r.paragraph || "";
|
| 47 |
+
const para2 = para.replace(/\[([a-z0-9_]+)\]/gi, (_, id) => {
|
| 48 |
+
const norm = id.toLowerCase();
|
| 49 |
+
if (citeIndex[norm] == null) citeIndex[norm] = Object.keys(citeIndex).length + 1;
|
| 50 |
+
return `<span class="cite">${citeIndex[norm]}</span>`;
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
const html = `
|
| 54 |
+
<header class="r-head">
|
| 55 |
+
<div class="r-brand">Riprap</div>
|
| 56 |
+
<div class="r-tagline">Citation-grounded flood-exposure briefing</div>
|
| 57 |
+
<dl class="r-meta-grid">
|
| 58 |
+
<dt>Subject</dt><dd>${escapeHtml(intentTitleMap[intent] || "Briefing")} · <strong>${escapeHtml(place)}</strong></dd>
|
| 59 |
+
${r.geocode && r.geocode.borough ? `<dt>Borough</dt><dd>${escapeHtml(r.geocode.borough)}</dd>` : ""}
|
| 60 |
+
${r.target && r.target.borough ? `<dt>Borough</dt><dd>${escapeHtml(r.target.borough)}</dd>` : ""}
|
| 61 |
+
${r.geocode && r.geocode.bbl ? `<dt>BBL</dt><dd class="mono">${escapeHtml(r.geocode.bbl)}</dd>` : ""}
|
| 62 |
+
${r.target && r.target.nta_code ? `<dt>NTA</dt><dd class="mono">${escapeHtml(r.target.nta_code)}</dd>` : ""}
|
| 63 |
+
<dt>Generated</dt><dd>${escapeHtml(pkg.finishedAt || new Date().toISOString())}</dd>
|
| 64 |
+
<dt>Total runtime</dt><dd>${pkg.wallSeconds ?? r.total_s ?? "—"} s</dd>
|
| 65 |
+
</dl>
|
| 66 |
+
</header>
|
| 67 |
+
|
| 68 |
+
<section class="r-section">
|
| 69 |
+
<h2>1 · Original query</h2>
|
| 70 |
+
<div class="r-query">"${escapeHtml(pkg.query)}"</div>
|
| 71 |
+
</section>
|
| 72 |
+
|
| 73 |
+
<section class="r-section">
|
| 74 |
+
<h2>2 · Agent routing decision</h2>
|
| 75 |
+
<dl class="r-plan">
|
| 76 |
+
<dt>Intent</dt><dd class="mono">${escapeHtml(plan.intent || intent)}</dd>
|
| 77 |
+
<dt>Targets</dt><dd class="mono">${escapeHtml((plan.targets || []).map(t => `${t.type}:${t.text}`).join(", ") || "—")}</dd>
|
| 78 |
+
<dt>Specialists requested</dt><dd class="mono">${escapeHtml((plan.specialists || []).join(", ") || "—")}</dd>
|
| 79 |
+
${plan.rationale ? `<dd class="r-plan-rationale">"${escapeHtml(plan.rationale)}"</dd>` : ""}
|
| 80 |
+
</dl>
|
| 81 |
+
</section>
|
| 82 |
+
|
| 83 |
+
<section class="r-section">
|
| 84 |
+
<h2>3 · Specialist trail</h2>
|
| 85 |
+
<div class="lead">${trace.length} specialists invoked. Each row shows the
|
| 86 |
+
step name, status, elapsed time, and the structured result the step
|
| 87 |
+
produced. Sources of any data referenced in the briefing appear in
|
| 88 |
+
Section 6.</div>
|
| 89 |
+
<table class="r-trace">
|
| 90 |
+
<thead>
|
| 91 |
+
<tr><th>#</th><th>Step</th><th>Status</th><th>Elapsed</th><th>Result / error</th></tr>
|
| 92 |
+
</thead>
|
| 93 |
+
<tbody>
|
| 94 |
+
${trace.map((s, i) => {
|
| 95 |
+
const ok = s.ok === true;
|
| 96 |
+
const fail = s.ok === false;
|
| 97 |
+
const cls = ok ? "ok" : fail ? "err" : "";
|
| 98 |
+
const mark = ok ? "✓" : fail ? "✗" : "○";
|
| 99 |
+
const [label] = stepLabels[s.step] || [s.step, ""];
|
| 100 |
+
const detail = s.err
|
| 101 |
+
? `<span class="err-msg">${escapeHtml(s.err)}</span>`
|
| 102 |
+
: `<span class="result">${escapeHtml(JSON.stringify(s.result ?? {}))}</span>`;
|
| 103 |
+
return `<tr class="${cls}">
|
| 104 |
+
<td class="mono">${i + 1}</td>
|
| 105 |
+
<td><strong>${escapeHtml(label)}</strong><br>
|
| 106 |
+
<span class="mono" style="color:#888;font-size:7.5pt">${escapeHtml(s.step)}</span></td>
|
| 107 |
+
<td><span class="mark">${mark}</span></td>
|
| 108 |
+
<td class="mono">${s.elapsed_s != null ? s.elapsed_s + "s" : "—"}</td>
|
| 109 |
+
<td>${detail}</td>
|
| 110 |
+
</tr>`;
|
| 111 |
+
}).join("")}
|
| 112 |
+
</tbody>
|
| 113 |
+
</table>
|
| 114 |
+
</section>
|
| 115 |
+
|
| 116 |
+
${pkg.mapPng ? `
|
| 117 |
+
<section class="r-section">
|
| 118 |
+
<h2>4 · Map (snapshot)</h2>
|
| 119 |
+
<div class="r-map">
|
| 120 |
+
<img src="${pkg.mapPng}" alt="Map snapshot at report-generation time">
|
| 121 |
+
<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>
|
| 122 |
+
</div>
|
| 123 |
+
</section>
|
| 124 |
+
` : `
|
| 125 |
+
<section class="r-section">
|
| 126 |
+
<h2>4 · Map</h2>
|
| 127 |
+
<div class="r-map no-map">No map snapshot was captured (the map may have been hidden or empty for this query type).</div>
|
| 128 |
+
</section>
|
| 129 |
+
`}
|
| 130 |
+
|
| 131 |
+
<section class="r-section">
|
| 132 |
+
<h2>5 · Cited briefing</h2>
|
| 133 |
+
<div class="r-briefing">${renderBriefingMarkdown(para2)}</div>
|
| 134 |
+
</section>
|
| 135 |
+
|
| 136 |
+
<section class="r-section">
|
| 137 |
+
<h2>6 · Sources</h2>
|
| 138 |
+
<ol class="r-sources">
|
| 139 |
+
${Object.entries(citeIndex).sort((a, b) => a[1] - b[1]).map(([id, n]) => {
|
| 140 |
+
const url = urls[id];
|
| 141 |
+
return `<li>
|
| 142 |
+
<span class="num">[${n}]</span>
|
| 143 |
+
<div>
|
| 144 |
+
<span class="label">${escapeHtml(labels[id] || id)}</span>
|
| 145 |
+
${vintages[id] ? `<span class="vintage">Vintage: ${escapeHtml(vintages[id])}</span>` : ""}
|
| 146 |
+
${url ? `<span class="url"><a href="${escapeHtml(url)}">${escapeHtml(url)}</a></span>` : ""}
|
| 147 |
+
<span class="vintage" style="font-family:var(--mono);font-size:8pt;color:#888">doc_id: ${escapeHtml(id)}</span>
|
| 148 |
+
</div>
|
| 149 |
+
</li>`;
|
| 150 |
+
}).join("")}
|
| 151 |
+
</ol>
|
| 152 |
+
</section>
|
| 153 |
+
|
| 154 |
+
<section class="r-section">
|
| 155 |
+
<h2>7 · Methodology & honest scope</h2>
|
| 156 |
+
<div class="r-method">
|
| 157 |
+
<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>
|
| 158 |
+
<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>
|
| 159 |
+
<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>
|
| 160 |
+
</div>
|
| 161 |
+
</section>
|
| 162 |
+
|
| 163 |
+
<footer class="r-foot">
|
| 164 |
+
<span>Generated by Riprap · https://huggingface.co/spaces/msradam/riprap-nyc</span>
|
| 165 |
+
<span>${escapeHtml(pkg.finishedAt || "")}</span>
|
| 166 |
+
</footer>
|
| 167 |
+
`;
|
| 168 |
+
document.getElementById("paper").innerHTML = html;
|
| 169 |
+
// Update tab title to reflect the subject
|
| 170 |
+
document.title = `Riprap — ${place}`;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// Subset markdown for the briefing: `**Header.**` lines → <h4>; `- ` lines
|
| 174 |
+
// → <ul><li>; inline `**foo**` → <strong>; rest → <p>. Keep parity with
|
| 175 |
+
// agent.js's renderMarkdown so reports look like the live UI.
|
| 176 |
+
function renderBriefingMarkdown(text) {
|
| 177 |
+
const lines = text.split("\n");
|
| 178 |
+
const out = [];
|
| 179 |
+
let para = []; let bullets = [];
|
| 180 |
+
const flushPara = () => {
|
| 181 |
+
if (!para.length) return;
|
| 182 |
+
const safe = para.join(" ").trim().replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
| 183 |
+
if (safe) out.push(`<p>${safe}</p>`);
|
| 184 |
+
para = [];
|
| 185 |
+
};
|
| 186 |
+
const flushBullets = () => {
|
| 187 |
+
if (!bullets.length) return;
|
| 188 |
+
const items = bullets.map(b => {
|
| 189 |
+
const safe = b.trim().replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
| 190 |
+
return `<li>${safe}</li>`;
|
| 191 |
+
}).join("");
|
| 192 |
+
out.push(`<ul>${items}</ul>`);
|
| 193 |
+
bullets = [];
|
| 194 |
+
};
|
| 195 |
+
// Pre-split inline-bullet runs that Granite occasionally emits as one line
|
| 196 |
+
const expanded = [];
|
| 197 |
+
for (const line of lines) {
|
| 198 |
+
if (line.trim().startsWith("- ") && line.includes(" - ", 2)) {
|
| 199 |
+
const parts = line.split(/(?:^|(?<=\.\s))\s*-\s+/g).filter(p => p.trim());
|
| 200 |
+
for (const p of parts) expanded.push("- " + p.trim());
|
| 201 |
+
} else { expanded.push(line); }
|
| 202 |
+
}
|
| 203 |
+
for (const line of expanded) {
|
| 204 |
+
const m = line.match(/^\s*\*\*([A-Z][A-Za-z\s/]+)\.\*\*\s*$/);
|
| 205 |
+
if (m) {
|
| 206 |
+
flushPara(); flushBullets();
|
| 207 |
+
out.push(`<h4>${m[1]}</h4>`);
|
| 208 |
+
} else if (/^\s*[-*]\s+/.test(line)) {
|
| 209 |
+
flushPara();
|
| 210 |
+
bullets.push(line.replace(/^\s*[-*]\s+/, ""));
|
| 211 |
+
} else {
|
| 212 |
+
flushBullets();
|
| 213 |
+
para.push(line);
|
| 214 |
+
}
|
| 215 |
+
}
|
| 216 |
+
flushPara(); flushBullets();
|
| 217 |
+
return out.join("");
|
| 218 |
+
}
|
|
@@ -116,6 +116,32 @@ a:hover { text-decoration: underline; }
|
|
| 116 |
|
| 117 |
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.45} }
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
/* ----- Form bar ----- */
|
| 120 |
|
| 121 |
.form-bar {
|
|
@@ -218,9 +244,10 @@ button.chip:hover {
|
|
| 218 |
}
|
| 219 |
.col-right {
|
| 220 |
display: flex; flex-direction: column; gap: 12px;
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
|
|
|
| 224 |
}
|
| 225 |
|
| 226 |
/* ----- Panels ----- */
|
|
@@ -501,6 +528,26 @@ button.chip:hover {
|
|
| 501 |
line-height: 1.55;
|
| 502 |
color: var(--text);
|
| 503 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
.summary-box #paragraph .cite {
|
| 505 |
display: inline-block;
|
| 506 |
vertical-align: super;
|
|
|
|
| 116 |
|
| 117 |
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.45} }
|
| 118 |
|
| 119 |
+
/* Backend pill state colors. Green = primary reachable; amber =
|
| 120 |
+
running on the configured fallback; gray = checking; red = nothing
|
| 121 |
+
reachable. */
|
| 122 |
+
.local-pill[data-state="loading"] {
|
| 123 |
+
color: #6c757d;
|
| 124 |
+
background: rgba(108, 117, 125, 0.08);
|
| 125 |
+
border-color: rgba(108, 117, 125, 0.30);
|
| 126 |
+
}
|
| 127 |
+
.local-pill[data-state="loading"] .dot { background: #6c757d; box-shadow: none; }
|
| 128 |
+
.local-pill[data-state="fallback"] {
|
| 129 |
+
color: #b06b00;
|
| 130 |
+
background: rgba(176, 107, 0, 0.10);
|
| 131 |
+
border-color: rgba(176, 107, 0, 0.35);
|
| 132 |
+
}
|
| 133 |
+
.local-pill[data-state="fallback"] .dot {
|
| 134 |
+
background: #b06b00; box-shadow: 0 0 6px rgba(176, 107, 0, 0.7);
|
| 135 |
+
}
|
| 136 |
+
.local-pill[data-state="down"] {
|
| 137 |
+
color: #b00020;
|
| 138 |
+
background: rgba(176, 0, 32, 0.08);
|
| 139 |
+
border-color: rgba(176, 0, 32, 0.35);
|
| 140 |
+
}
|
| 141 |
+
.local-pill[data-state="down"] .dot {
|
| 142 |
+
background: #b00020; box-shadow: 0 0 6px rgba(176, 0, 32, 0.7);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
/* ----- Form bar ----- */
|
| 146 |
|
| 147 |
.form-bar {
|
|
|
|
| 244 |
}
|
| 245 |
.col-right {
|
| 246 |
display: flex; flex-direction: column; gap: 12px;
|
| 247 |
+
/* No viewport clamp: report flows to its natural height. The page
|
| 248 |
+
scrolls vertically when content exceeds viewport — with an internal
|
| 249 |
+
scrollbar (the previous behavior) the bottom citations were
|
| 250 |
+
unreachable for users who didn't notice the thin inner scrollbar. */
|
| 251 |
}
|
| 252 |
|
| 253 |
/* ----- Panels ----- */
|
|
|
|
| 528 |
line-height: 1.55;
|
| 529 |
color: var(--text);
|
| 530 |
}
|
| 531 |
+
/* Section structure inside the summary (Status / Empirical / Modeled / Policy).
|
| 532 |
+
The reconciler emits markdown headers like `**Status.**` on their own line;
|
| 533 |
+
renderMarkdown() converts them to <h4 class="rsum-h">. */
|
| 534 |
+
.summary-box #paragraph .rsum-h {
|
| 535 |
+
margin: 12px 0 4px;
|
| 536 |
+
font-size: 10.5px;
|
| 537 |
+
font-weight: 700;
|
| 538 |
+
text-transform: uppercase;
|
| 539 |
+
letter-spacing: 0.08em;
|
| 540 |
+
color: var(--text-muted);
|
| 541 |
+
}
|
| 542 |
+
.summary-box #paragraph .rsum-h:first-child { margin-top: 0; }
|
| 543 |
+
.summary-box #paragraph .rsum-p {
|
| 544 |
+
margin: 0 0 4px;
|
| 545 |
+
}
|
| 546 |
+
.summary-box #paragraph .rsum-p strong {
|
| 547 |
+
font-weight: 600;
|
| 548 |
+
background: linear-gradient(transparent 60%, var(--nyc-blue-soft) 60%);
|
| 549 |
+
padding: 0 2px;
|
| 550 |
+
}
|
| 551 |
.summary-box #paragraph .cite {
|
| 552 |
display: inline-block;
|
| 553 |
vertical-align: super;
|
|
@@ -0,0 +1,1337 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "riprap-svelte",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "riprap-svelte",
|
| 9 |
+
"version": "0.1.0",
|
| 10 |
+
"devDependencies": {
|
| 11 |
+
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
| 12 |
+
"svelte": "^5.0.0",
|
| 13 |
+
"vite": "^5.4.0"
|
| 14 |
+
}
|
| 15 |
+
},
|
| 16 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 17 |
+
"version": "0.21.5",
|
| 18 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
| 19 |
+
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
| 20 |
+
"cpu": [
|
| 21 |
+
"ppc64"
|
| 22 |
+
],
|
| 23 |
+
"dev": true,
|
| 24 |
+
"license": "MIT",
|
| 25 |
+
"optional": true,
|
| 26 |
+
"os": [
|
| 27 |
+
"aix"
|
| 28 |
+
],
|
| 29 |
+
"engines": {
|
| 30 |
+
"node": ">=12"
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
"node_modules/@esbuild/android-arm": {
|
| 34 |
+
"version": "0.21.5",
|
| 35 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
| 36 |
+
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
| 37 |
+
"cpu": [
|
| 38 |
+
"arm"
|
| 39 |
+
],
|
| 40 |
+
"dev": true,
|
| 41 |
+
"license": "MIT",
|
| 42 |
+
"optional": true,
|
| 43 |
+
"os": [
|
| 44 |
+
"android"
|
| 45 |
+
],
|
| 46 |
+
"engines": {
|
| 47 |
+
"node": ">=12"
|
| 48 |
+
}
|
| 49 |
+
},
|
| 50 |
+
"node_modules/@esbuild/android-arm64": {
|
| 51 |
+
"version": "0.21.5",
|
| 52 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
| 53 |
+
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
| 54 |
+
"cpu": [
|
| 55 |
+
"arm64"
|
| 56 |
+
],
|
| 57 |
+
"dev": true,
|
| 58 |
+
"license": "MIT",
|
| 59 |
+
"optional": true,
|
| 60 |
+
"os": [
|
| 61 |
+
"android"
|
| 62 |
+
],
|
| 63 |
+
"engines": {
|
| 64 |
+
"node": ">=12"
|
| 65 |
+
}
|
| 66 |
+
},
|
| 67 |
+
"node_modules/@esbuild/android-x64": {
|
| 68 |
+
"version": "0.21.5",
|
| 69 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
| 70 |
+
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
| 71 |
+
"cpu": [
|
| 72 |
+
"x64"
|
| 73 |
+
],
|
| 74 |
+
"dev": true,
|
| 75 |
+
"license": "MIT",
|
| 76 |
+
"optional": true,
|
| 77 |
+
"os": [
|
| 78 |
+
"android"
|
| 79 |
+
],
|
| 80 |
+
"engines": {
|
| 81 |
+
"node": ">=12"
|
| 82 |
+
}
|
| 83 |
+
},
|
| 84 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 85 |
+
"version": "0.21.5",
|
| 86 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
| 87 |
+
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
| 88 |
+
"cpu": [
|
| 89 |
+
"arm64"
|
| 90 |
+
],
|
| 91 |
+
"dev": true,
|
| 92 |
+
"license": "MIT",
|
| 93 |
+
"optional": true,
|
| 94 |
+
"os": [
|
| 95 |
+
"darwin"
|
| 96 |
+
],
|
| 97 |
+
"engines": {
|
| 98 |
+
"node": ">=12"
|
| 99 |
+
}
|
| 100 |
+
},
|
| 101 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 102 |
+
"version": "0.21.5",
|
| 103 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
| 104 |
+
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
| 105 |
+
"cpu": [
|
| 106 |
+
"x64"
|
| 107 |
+
],
|
| 108 |
+
"dev": true,
|
| 109 |
+
"license": "MIT",
|
| 110 |
+
"optional": true,
|
| 111 |
+
"os": [
|
| 112 |
+
"darwin"
|
| 113 |
+
],
|
| 114 |
+
"engines": {
|
| 115 |
+
"node": ">=12"
|
| 116 |
+
}
|
| 117 |
+
},
|
| 118 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 119 |
+
"version": "0.21.5",
|
| 120 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
| 121 |
+
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
| 122 |
+
"cpu": [
|
| 123 |
+
"arm64"
|
| 124 |
+
],
|
| 125 |
+
"dev": true,
|
| 126 |
+
"license": "MIT",
|
| 127 |
+
"optional": true,
|
| 128 |
+
"os": [
|
| 129 |
+
"freebsd"
|
| 130 |
+
],
|
| 131 |
+
"engines": {
|
| 132 |
+
"node": ">=12"
|
| 133 |
+
}
|
| 134 |
+
},
|
| 135 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 136 |
+
"version": "0.21.5",
|
| 137 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
| 138 |
+
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
| 139 |
+
"cpu": [
|
| 140 |
+
"x64"
|
| 141 |
+
],
|
| 142 |
+
"dev": true,
|
| 143 |
+
"license": "MIT",
|
| 144 |
+
"optional": true,
|
| 145 |
+
"os": [
|
| 146 |
+
"freebsd"
|
| 147 |
+
],
|
| 148 |
+
"engines": {
|
| 149 |
+
"node": ">=12"
|
| 150 |
+
}
|
| 151 |
+
},
|
| 152 |
+
"node_modules/@esbuild/linux-arm": {
|
| 153 |
+
"version": "0.21.5",
|
| 154 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
| 155 |
+
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
| 156 |
+
"cpu": [
|
| 157 |
+
"arm"
|
| 158 |
+
],
|
| 159 |
+
"dev": true,
|
| 160 |
+
"license": "MIT",
|
| 161 |
+
"optional": true,
|
| 162 |
+
"os": [
|
| 163 |
+
"linux"
|
| 164 |
+
],
|
| 165 |
+
"engines": {
|
| 166 |
+
"node": ">=12"
|
| 167 |
+
}
|
| 168 |
+
},
|
| 169 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 170 |
+
"version": "0.21.5",
|
| 171 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
| 172 |
+
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
| 173 |
+
"cpu": [
|
| 174 |
+
"arm64"
|
| 175 |
+
],
|
| 176 |
+
"dev": true,
|
| 177 |
+
"license": "MIT",
|
| 178 |
+
"optional": true,
|
| 179 |
+
"os": [
|
| 180 |
+
"linux"
|
| 181 |
+
],
|
| 182 |
+
"engines": {
|
| 183 |
+
"node": ">=12"
|
| 184 |
+
}
|
| 185 |
+
},
|
| 186 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 187 |
+
"version": "0.21.5",
|
| 188 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
| 189 |
+
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
| 190 |
+
"cpu": [
|
| 191 |
+
"ia32"
|
| 192 |
+
],
|
| 193 |
+
"dev": true,
|
| 194 |
+
"license": "MIT",
|
| 195 |
+
"optional": true,
|
| 196 |
+
"os": [
|
| 197 |
+
"linux"
|
| 198 |
+
],
|
| 199 |
+
"engines": {
|
| 200 |
+
"node": ">=12"
|
| 201 |
+
}
|
| 202 |
+
},
|
| 203 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 204 |
+
"version": "0.21.5",
|
| 205 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
| 206 |
+
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
| 207 |
+
"cpu": [
|
| 208 |
+
"loong64"
|
| 209 |
+
],
|
| 210 |
+
"dev": true,
|
| 211 |
+
"license": "MIT",
|
| 212 |
+
"optional": true,
|
| 213 |
+
"os": [
|
| 214 |
+
"linux"
|
| 215 |
+
],
|
| 216 |
+
"engines": {
|
| 217 |
+
"node": ">=12"
|
| 218 |
+
}
|
| 219 |
+
},
|
| 220 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 221 |
+
"version": "0.21.5",
|
| 222 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
| 223 |
+
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
| 224 |
+
"cpu": [
|
| 225 |
+
"mips64el"
|
| 226 |
+
],
|
| 227 |
+
"dev": true,
|
| 228 |
+
"license": "MIT",
|
| 229 |
+
"optional": true,
|
| 230 |
+
"os": [
|
| 231 |
+
"linux"
|
| 232 |
+
],
|
| 233 |
+
"engines": {
|
| 234 |
+
"node": ">=12"
|
| 235 |
+
}
|
| 236 |
+
},
|
| 237 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 238 |
+
"version": "0.21.5",
|
| 239 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
| 240 |
+
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
| 241 |
+
"cpu": [
|
| 242 |
+
"ppc64"
|
| 243 |
+
],
|
| 244 |
+
"dev": true,
|
| 245 |
+
"license": "MIT",
|
| 246 |
+
"optional": true,
|
| 247 |
+
"os": [
|
| 248 |
+
"linux"
|
| 249 |
+
],
|
| 250 |
+
"engines": {
|
| 251 |
+
"node": ">=12"
|
| 252 |
+
}
|
| 253 |
+
},
|
| 254 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 255 |
+
"version": "0.21.5",
|
| 256 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
| 257 |
+
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
| 258 |
+
"cpu": [
|
| 259 |
+
"riscv64"
|
| 260 |
+
],
|
| 261 |
+
"dev": true,
|
| 262 |
+
"license": "MIT",
|
| 263 |
+
"optional": true,
|
| 264 |
+
"os": [
|
| 265 |
+
"linux"
|
| 266 |
+
],
|
| 267 |
+
"engines": {
|
| 268 |
+
"node": ">=12"
|
| 269 |
+
}
|
| 270 |
+
},
|
| 271 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 272 |
+
"version": "0.21.5",
|
| 273 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
| 274 |
+
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
| 275 |
+
"cpu": [
|
| 276 |
+
"s390x"
|
| 277 |
+
],
|
| 278 |
+
"dev": true,
|
| 279 |
+
"license": "MIT",
|
| 280 |
+
"optional": true,
|
| 281 |
+
"os": [
|
| 282 |
+
"linux"
|
| 283 |
+
],
|
| 284 |
+
"engines": {
|
| 285 |
+
"node": ">=12"
|
| 286 |
+
}
|
| 287 |
+
},
|
| 288 |
+
"node_modules/@esbuild/linux-x64": {
|
| 289 |
+
"version": "0.21.5",
|
| 290 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
| 291 |
+
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
| 292 |
+
"cpu": [
|
| 293 |
+
"x64"
|
| 294 |
+
],
|
| 295 |
+
"dev": true,
|
| 296 |
+
"license": "MIT",
|
| 297 |
+
"optional": true,
|
| 298 |
+
"os": [
|
| 299 |
+
"linux"
|
| 300 |
+
],
|
| 301 |
+
"engines": {
|
| 302 |
+
"node": ">=12"
|
| 303 |
+
}
|
| 304 |
+
},
|
| 305 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 306 |
+
"version": "0.21.5",
|
| 307 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
| 308 |
+
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
| 309 |
+
"cpu": [
|
| 310 |
+
"x64"
|
| 311 |
+
],
|
| 312 |
+
"dev": true,
|
| 313 |
+
"license": "MIT",
|
| 314 |
+
"optional": true,
|
| 315 |
+
"os": [
|
| 316 |
+
"netbsd"
|
| 317 |
+
],
|
| 318 |
+
"engines": {
|
| 319 |
+
"node": ">=12"
|
| 320 |
+
}
|
| 321 |
+
},
|
| 322 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 323 |
+
"version": "0.21.5",
|
| 324 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
| 325 |
+
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
| 326 |
+
"cpu": [
|
| 327 |
+
"x64"
|
| 328 |
+
],
|
| 329 |
+
"dev": true,
|
| 330 |
+
"license": "MIT",
|
| 331 |
+
"optional": true,
|
| 332 |
+
"os": [
|
| 333 |
+
"openbsd"
|
| 334 |
+
],
|
| 335 |
+
"engines": {
|
| 336 |
+
"node": ">=12"
|
| 337 |
+
}
|
| 338 |
+
},
|
| 339 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 340 |
+
"version": "0.21.5",
|
| 341 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
| 342 |
+
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
| 343 |
+
"cpu": [
|
| 344 |
+
"x64"
|
| 345 |
+
],
|
| 346 |
+
"dev": true,
|
| 347 |
+
"license": "MIT",
|
| 348 |
+
"optional": true,
|
| 349 |
+
"os": [
|
| 350 |
+
"sunos"
|
| 351 |
+
],
|
| 352 |
+
"engines": {
|
| 353 |
+
"node": ">=12"
|
| 354 |
+
}
|
| 355 |
+
},
|
| 356 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 357 |
+
"version": "0.21.5",
|
| 358 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
| 359 |
+
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
| 360 |
+
"cpu": [
|
| 361 |
+
"arm64"
|
| 362 |
+
],
|
| 363 |
+
"dev": true,
|
| 364 |
+
"license": "MIT",
|
| 365 |
+
"optional": true,
|
| 366 |
+
"os": [
|
| 367 |
+
"win32"
|
| 368 |
+
],
|
| 369 |
+
"engines": {
|
| 370 |
+
"node": ">=12"
|
| 371 |
+
}
|
| 372 |
+
},
|
| 373 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 374 |
+
"version": "0.21.5",
|
| 375 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
| 376 |
+
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
| 377 |
+
"cpu": [
|
| 378 |
+
"ia32"
|
| 379 |
+
],
|
| 380 |
+
"dev": true,
|
| 381 |
+
"license": "MIT",
|
| 382 |
+
"optional": true,
|
| 383 |
+
"os": [
|
| 384 |
+
"win32"
|
| 385 |
+
],
|
| 386 |
+
"engines": {
|
| 387 |
+
"node": ">=12"
|
| 388 |
+
}
|
| 389 |
+
},
|
| 390 |
+
"node_modules/@esbuild/win32-x64": {
|
| 391 |
+
"version": "0.21.5",
|
| 392 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
| 393 |
+
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
| 394 |
+
"cpu": [
|
| 395 |
+
"x64"
|
| 396 |
+
],
|
| 397 |
+
"dev": true,
|
| 398 |
+
"license": "MIT",
|
| 399 |
+
"optional": true,
|
| 400 |
+
"os": [
|
| 401 |
+
"win32"
|
| 402 |
+
],
|
| 403 |
+
"engines": {
|
| 404 |
+
"node": ">=12"
|
| 405 |
+
}
|
| 406 |
+
},
|
| 407 |
+
"node_modules/@jridgewell/gen-mapping": {
|
| 408 |
+
"version": "0.3.13",
|
| 409 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
| 410 |
+
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
| 411 |
+
"dev": true,
|
| 412 |
+
"license": "MIT",
|
| 413 |
+
"dependencies": {
|
| 414 |
+
"@jridgewell/sourcemap-codec": "^1.5.0",
|
| 415 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 416 |
+
}
|
| 417 |
+
},
|
| 418 |
+
"node_modules/@jridgewell/remapping": {
|
| 419 |
+
"version": "2.3.5",
|
| 420 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
| 421 |
+
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
| 422 |
+
"dev": true,
|
| 423 |
+
"license": "MIT",
|
| 424 |
+
"dependencies": {
|
| 425 |
+
"@jridgewell/gen-mapping": "^0.3.5",
|
| 426 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 427 |
+
}
|
| 428 |
+
},
|
| 429 |
+
"node_modules/@jridgewell/resolve-uri": {
|
| 430 |
+
"version": "3.1.2",
|
| 431 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 432 |
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
| 433 |
+
"dev": true,
|
| 434 |
+
"license": "MIT",
|
| 435 |
+
"engines": {
|
| 436 |
+
"node": ">=6.0.0"
|
| 437 |
+
}
|
| 438 |
+
},
|
| 439 |
+
"node_modules/@jridgewell/sourcemap-codec": {
|
| 440 |
+
"version": "1.5.5",
|
| 441 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 442 |
+
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
| 443 |
+
"dev": true,
|
| 444 |
+
"license": "MIT"
|
| 445 |
+
},
|
| 446 |
+
"node_modules/@jridgewell/trace-mapping": {
|
| 447 |
+
"version": "0.3.31",
|
| 448 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
| 449 |
+
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
| 450 |
+
"dev": true,
|
| 451 |
+
"license": "MIT",
|
| 452 |
+
"dependencies": {
|
| 453 |
+
"@jridgewell/resolve-uri": "^3.1.0",
|
| 454 |
+
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 455 |
+
}
|
| 456 |
+
},
|
| 457 |
+
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 458 |
+
"version": "4.60.2",
|
| 459 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
|
| 460 |
+
"integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
|
| 461 |
+
"cpu": [
|
| 462 |
+
"arm"
|
| 463 |
+
],
|
| 464 |
+
"dev": true,
|
| 465 |
+
"license": "MIT",
|
| 466 |
+
"optional": true,
|
| 467 |
+
"os": [
|
| 468 |
+
"android"
|
| 469 |
+
]
|
| 470 |
+
},
|
| 471 |
+
"node_modules/@rollup/rollup-android-arm64": {
|
| 472 |
+
"version": "4.60.2",
|
| 473 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
|
| 474 |
+
"integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
|
| 475 |
+
"cpu": [
|
| 476 |
+
"arm64"
|
| 477 |
+
],
|
| 478 |
+
"dev": true,
|
| 479 |
+
"license": "MIT",
|
| 480 |
+
"optional": true,
|
| 481 |
+
"os": [
|
| 482 |
+
"android"
|
| 483 |
+
]
|
| 484 |
+
},
|
| 485 |
+
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 486 |
+
"version": "4.60.2",
|
| 487 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
|
| 488 |
+
"integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
|
| 489 |
+
"cpu": [
|
| 490 |
+
"arm64"
|
| 491 |
+
],
|
| 492 |
+
"dev": true,
|
| 493 |
+
"license": "MIT",
|
| 494 |
+
"optional": true,
|
| 495 |
+
"os": [
|
| 496 |
+
"darwin"
|
| 497 |
+
]
|
| 498 |
+
},
|
| 499 |
+
"node_modules/@rollup/rollup-darwin-x64": {
|
| 500 |
+
"version": "4.60.2",
|
| 501 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
|
| 502 |
+
"integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
|
| 503 |
+
"cpu": [
|
| 504 |
+
"x64"
|
| 505 |
+
],
|
| 506 |
+
"dev": true,
|
| 507 |
+
"license": "MIT",
|
| 508 |
+
"optional": true,
|
| 509 |
+
"os": [
|
| 510 |
+
"darwin"
|
| 511 |
+
]
|
| 512 |
+
},
|
| 513 |
+
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 514 |
+
"version": "4.60.2",
|
| 515 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
|
| 516 |
+
"integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
|
| 517 |
+
"cpu": [
|
| 518 |
+
"arm64"
|
| 519 |
+
],
|
| 520 |
+
"dev": true,
|
| 521 |
+
"license": "MIT",
|
| 522 |
+
"optional": true,
|
| 523 |
+
"os": [
|
| 524 |
+
"freebsd"
|
| 525 |
+
]
|
| 526 |
+
},
|
| 527 |
+
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 528 |
+
"version": "4.60.2",
|
| 529 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
|
| 530 |
+
"integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
|
| 531 |
+
"cpu": [
|
| 532 |
+
"x64"
|
| 533 |
+
],
|
| 534 |
+
"dev": true,
|
| 535 |
+
"license": "MIT",
|
| 536 |
+
"optional": true,
|
| 537 |
+
"os": [
|
| 538 |
+
"freebsd"
|
| 539 |
+
]
|
| 540 |
+
},
|
| 541 |
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 542 |
+
"version": "4.60.2",
|
| 543 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
|
| 544 |
+
"integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
|
| 545 |
+
"cpu": [
|
| 546 |
+
"arm"
|
| 547 |
+
],
|
| 548 |
+
"dev": true,
|
| 549 |
+
"libc": [
|
| 550 |
+
"glibc"
|
| 551 |
+
],
|
| 552 |
+
"license": "MIT",
|
| 553 |
+
"optional": true,
|
| 554 |
+
"os": [
|
| 555 |
+
"linux"
|
| 556 |
+
]
|
| 557 |
+
},
|
| 558 |
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 559 |
+
"version": "4.60.2",
|
| 560 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
|
| 561 |
+
"integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
|
| 562 |
+
"cpu": [
|
| 563 |
+
"arm"
|
| 564 |
+
],
|
| 565 |
+
"dev": true,
|
| 566 |
+
"libc": [
|
| 567 |
+
"musl"
|
| 568 |
+
],
|
| 569 |
+
"license": "MIT",
|
| 570 |
+
"optional": true,
|
| 571 |
+
"os": [
|
| 572 |
+
"linux"
|
| 573 |
+
]
|
| 574 |
+
},
|
| 575 |
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 576 |
+
"version": "4.60.2",
|
| 577 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
|
| 578 |
+
"integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
|
| 579 |
+
"cpu": [
|
| 580 |
+
"arm64"
|
| 581 |
+
],
|
| 582 |
+
"dev": true,
|
| 583 |
+
"libc": [
|
| 584 |
+
"glibc"
|
| 585 |
+
],
|
| 586 |
+
"license": "MIT",
|
| 587 |
+
"optional": true,
|
| 588 |
+
"os": [
|
| 589 |
+
"linux"
|
| 590 |
+
]
|
| 591 |
+
},
|
| 592 |
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 593 |
+
"version": "4.60.2",
|
| 594 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
|
| 595 |
+
"integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
|
| 596 |
+
"cpu": [
|
| 597 |
+
"arm64"
|
| 598 |
+
],
|
| 599 |
+
"dev": true,
|
| 600 |
+
"libc": [
|
| 601 |
+
"musl"
|
| 602 |
+
],
|
| 603 |
+
"license": "MIT",
|
| 604 |
+
"optional": true,
|
| 605 |
+
"os": [
|
| 606 |
+
"linux"
|
| 607 |
+
]
|
| 608 |
+
},
|
| 609 |
+
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
| 610 |
+
"version": "4.60.2",
|
| 611 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
|
| 612 |
+
"integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
|
| 613 |
+
"cpu": [
|
| 614 |
+
"loong64"
|
| 615 |
+
],
|
| 616 |
+
"dev": true,
|
| 617 |
+
"libc": [
|
| 618 |
+
"glibc"
|
| 619 |
+
],
|
| 620 |
+
"license": "MIT",
|
| 621 |
+
"optional": true,
|
| 622 |
+
"os": [
|
| 623 |
+
"linux"
|
| 624 |
+
]
|
| 625 |
+
},
|
| 626 |
+
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
| 627 |
+
"version": "4.60.2",
|
| 628 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
|
| 629 |
+
"integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
|
| 630 |
+
"cpu": [
|
| 631 |
+
"loong64"
|
| 632 |
+
],
|
| 633 |
+
"dev": true,
|
| 634 |
+
"libc": [
|
| 635 |
+
"musl"
|
| 636 |
+
],
|
| 637 |
+
"license": "MIT",
|
| 638 |
+
"optional": true,
|
| 639 |
+
"os": [
|
| 640 |
+
"linux"
|
| 641 |
+
]
|
| 642 |
+
},
|
| 643 |
+
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
| 644 |
+
"version": "4.60.2",
|
| 645 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
|
| 646 |
+
"integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
|
| 647 |
+
"cpu": [
|
| 648 |
+
"ppc64"
|
| 649 |
+
],
|
| 650 |
+
"dev": true,
|
| 651 |
+
"libc": [
|
| 652 |
+
"glibc"
|
| 653 |
+
],
|
| 654 |
+
"license": "MIT",
|
| 655 |
+
"optional": true,
|
| 656 |
+
"os": [
|
| 657 |
+
"linux"
|
| 658 |
+
]
|
| 659 |
+
},
|
| 660 |
+
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
| 661 |
+
"version": "4.60.2",
|
| 662 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
|
| 663 |
+
"integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
|
| 664 |
+
"cpu": [
|
| 665 |
+
"ppc64"
|
| 666 |
+
],
|
| 667 |
+
"dev": true,
|
| 668 |
+
"libc": [
|
| 669 |
+
"musl"
|
| 670 |
+
],
|
| 671 |
+
"license": "MIT",
|
| 672 |
+
"optional": true,
|
| 673 |
+
"os": [
|
| 674 |
+
"linux"
|
| 675 |
+
]
|
| 676 |
+
},
|
| 677 |
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 678 |
+
"version": "4.60.2",
|
| 679 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
|
| 680 |
+
"integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
|
| 681 |
+
"cpu": [
|
| 682 |
+
"riscv64"
|
| 683 |
+
],
|
| 684 |
+
"dev": true,
|
| 685 |
+
"libc": [
|
| 686 |
+
"glibc"
|
| 687 |
+
],
|
| 688 |
+
"license": "MIT",
|
| 689 |
+
"optional": true,
|
| 690 |
+
"os": [
|
| 691 |
+
"linux"
|
| 692 |
+
]
|
| 693 |
+
},
|
| 694 |
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 695 |
+
"version": "4.60.2",
|
| 696 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
|
| 697 |
+
"integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
|
| 698 |
+
"cpu": [
|
| 699 |
+
"riscv64"
|
| 700 |
+
],
|
| 701 |
+
"dev": true,
|
| 702 |
+
"libc": [
|
| 703 |
+
"musl"
|
| 704 |
+
],
|
| 705 |
+
"license": "MIT",
|
| 706 |
+
"optional": true,
|
| 707 |
+
"os": [
|
| 708 |
+
"linux"
|
| 709 |
+
]
|
| 710 |
+
},
|
| 711 |
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 712 |
+
"version": "4.60.2",
|
| 713 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
|
| 714 |
+
"integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
|
| 715 |
+
"cpu": [
|
| 716 |
+
"s390x"
|
| 717 |
+
],
|
| 718 |
+
"dev": true,
|
| 719 |
+
"libc": [
|
| 720 |
+
"glibc"
|
| 721 |
+
],
|
| 722 |
+
"license": "MIT",
|
| 723 |
+
"optional": true,
|
| 724 |
+
"os": [
|
| 725 |
+
"linux"
|
| 726 |
+
]
|
| 727 |
+
},
|
| 728 |
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 729 |
+
"version": "4.60.2",
|
| 730 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
|
| 731 |
+
"integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
|
| 732 |
+
"cpu": [
|
| 733 |
+
"x64"
|
| 734 |
+
],
|
| 735 |
+
"dev": true,
|
| 736 |
+
"libc": [
|
| 737 |
+
"glibc"
|
| 738 |
+
],
|
| 739 |
+
"license": "MIT",
|
| 740 |
+
"optional": true,
|
| 741 |
+
"os": [
|
| 742 |
+
"linux"
|
| 743 |
+
]
|
| 744 |
+
},
|
| 745 |
+
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 746 |
+
"version": "4.60.2",
|
| 747 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
|
| 748 |
+
"integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
|
| 749 |
+
"cpu": [
|
| 750 |
+
"x64"
|
| 751 |
+
],
|
| 752 |
+
"dev": true,
|
| 753 |
+
"libc": [
|
| 754 |
+
"musl"
|
| 755 |
+
],
|
| 756 |
+
"license": "MIT",
|
| 757 |
+
"optional": true,
|
| 758 |
+
"os": [
|
| 759 |
+
"linux"
|
| 760 |
+
]
|
| 761 |
+
},
|
| 762 |
+
"node_modules/@rollup/rollup-openbsd-x64": {
|
| 763 |
+
"version": "4.60.2",
|
| 764 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
|
| 765 |
+
"integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
|
| 766 |
+
"cpu": [
|
| 767 |
+
"x64"
|
| 768 |
+
],
|
| 769 |
+
"dev": true,
|
| 770 |
+
"license": "MIT",
|
| 771 |
+
"optional": true,
|
| 772 |
+
"os": [
|
| 773 |
+
"openbsd"
|
| 774 |
+
]
|
| 775 |
+
},
|
| 776 |
+
"node_modules/@rollup/rollup-openharmony-arm64": {
|
| 777 |
+
"version": "4.60.2",
|
| 778 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
|
| 779 |
+
"integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
|
| 780 |
+
"cpu": [
|
| 781 |
+
"arm64"
|
| 782 |
+
],
|
| 783 |
+
"dev": true,
|
| 784 |
+
"license": "MIT",
|
| 785 |
+
"optional": true,
|
| 786 |
+
"os": [
|
| 787 |
+
"openharmony"
|
| 788 |
+
]
|
| 789 |
+
},
|
| 790 |
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 791 |
+
"version": "4.60.2",
|
| 792 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
|
| 793 |
+
"integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
|
| 794 |
+
"cpu": [
|
| 795 |
+
"arm64"
|
| 796 |
+
],
|
| 797 |
+
"dev": true,
|
| 798 |
+
"license": "MIT",
|
| 799 |
+
"optional": true,
|
| 800 |
+
"os": [
|
| 801 |
+
"win32"
|
| 802 |
+
]
|
| 803 |
+
},
|
| 804 |
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 805 |
+
"version": "4.60.2",
|
| 806 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
|
| 807 |
+
"integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
|
| 808 |
+
"cpu": [
|
| 809 |
+
"ia32"
|
| 810 |
+
],
|
| 811 |
+
"dev": true,
|
| 812 |
+
"license": "MIT",
|
| 813 |
+
"optional": true,
|
| 814 |
+
"os": [
|
| 815 |
+
"win32"
|
| 816 |
+
]
|
| 817 |
+
},
|
| 818 |
+
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
| 819 |
+
"version": "4.60.2",
|
| 820 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
|
| 821 |
+
"integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
|
| 822 |
+
"cpu": [
|
| 823 |
+
"x64"
|
| 824 |
+
],
|
| 825 |
+
"dev": true,
|
| 826 |
+
"license": "MIT",
|
| 827 |
+
"optional": true,
|
| 828 |
+
"os": [
|
| 829 |
+
"win32"
|
| 830 |
+
]
|
| 831 |
+
},
|
| 832 |
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 833 |
+
"version": "4.60.2",
|
| 834 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
|
| 835 |
+
"integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
|
| 836 |
+
"cpu": [
|
| 837 |
+
"x64"
|
| 838 |
+
],
|
| 839 |
+
"dev": true,
|
| 840 |
+
"license": "MIT",
|
| 841 |
+
"optional": true,
|
| 842 |
+
"os": [
|
| 843 |
+
"win32"
|
| 844 |
+
]
|
| 845 |
+
},
|
| 846 |
+
"node_modules/@sveltejs/acorn-typescript": {
|
| 847 |
+
"version": "1.0.9",
|
| 848 |
+
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz",
|
| 849 |
+
"integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==",
|
| 850 |
+
"dev": true,
|
| 851 |
+
"license": "MIT",
|
| 852 |
+
"peerDependencies": {
|
| 853 |
+
"acorn": "^8.9.0"
|
| 854 |
+
}
|
| 855 |
+
},
|
| 856 |
+
"node_modules/@sveltejs/vite-plugin-svelte": {
|
| 857 |
+
"version": "4.0.4",
|
| 858 |
+
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz",
|
| 859 |
+
"integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==",
|
| 860 |
+
"dev": true,
|
| 861 |
+
"license": "MIT",
|
| 862 |
+
"dependencies": {
|
| 863 |
+
"@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0",
|
| 864 |
+
"debug": "^4.3.7",
|
| 865 |
+
"deepmerge": "^4.3.1",
|
| 866 |
+
"kleur": "^4.1.5",
|
| 867 |
+
"magic-string": "^0.30.12",
|
| 868 |
+
"vitefu": "^1.0.3"
|
| 869 |
+
},
|
| 870 |
+
"engines": {
|
| 871 |
+
"node": "^18.0.0 || ^20.0.0 || >=22"
|
| 872 |
+
},
|
| 873 |
+
"peerDependencies": {
|
| 874 |
+
"svelte": "^5.0.0-next.96 || ^5.0.0",
|
| 875 |
+
"vite": "^5.0.0"
|
| 876 |
+
}
|
| 877 |
+
},
|
| 878 |
+
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
|
| 879 |
+
"version": "3.0.1",
|
| 880 |
+
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz",
|
| 881 |
+
"integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==",
|
| 882 |
+
"dev": true,
|
| 883 |
+
"license": "MIT",
|
| 884 |
+
"dependencies": {
|
| 885 |
+
"debug": "^4.3.7"
|
| 886 |
+
},
|
| 887 |
+
"engines": {
|
| 888 |
+
"node": "^18.0.0 || ^20.0.0 || >=22"
|
| 889 |
+
},
|
| 890 |
+
"peerDependencies": {
|
| 891 |
+
"@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0",
|
| 892 |
+
"svelte": "^5.0.0-next.96 || ^5.0.0",
|
| 893 |
+
"vite": "^5.0.0"
|
| 894 |
+
}
|
| 895 |
+
},
|
| 896 |
+
"node_modules/@types/estree": {
|
| 897 |
+
"version": "1.0.8",
|
| 898 |
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 899 |
+
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 900 |
+
"dev": true,
|
| 901 |
+
"license": "MIT"
|
| 902 |
+
},
|
| 903 |
+
"node_modules/@types/trusted-types": {
|
| 904 |
+
"version": "2.0.7",
|
| 905 |
+
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
| 906 |
+
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
| 907 |
+
"dev": true,
|
| 908 |
+
"license": "MIT"
|
| 909 |
+
},
|
| 910 |
+
"node_modules/acorn": {
|
| 911 |
+
"version": "8.16.0",
|
| 912 |
+
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
| 913 |
+
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
| 914 |
+
"dev": true,
|
| 915 |
+
"license": "MIT",
|
| 916 |
+
"bin": {
|
| 917 |
+
"acorn": "bin/acorn"
|
| 918 |
+
},
|
| 919 |
+
"engines": {
|
| 920 |
+
"node": ">=0.4.0"
|
| 921 |
+
}
|
| 922 |
+
},
|
| 923 |
+
"node_modules/aria-query": {
|
| 924 |
+
"version": "5.3.1",
|
| 925 |
+
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
| 926 |
+
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
| 927 |
+
"dev": true,
|
| 928 |
+
"license": "Apache-2.0",
|
| 929 |
+
"engines": {
|
| 930 |
+
"node": ">= 0.4"
|
| 931 |
+
}
|
| 932 |
+
},
|
| 933 |
+
"node_modules/axobject-query": {
|
| 934 |
+
"version": "4.1.0",
|
| 935 |
+
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
| 936 |
+
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
| 937 |
+
"dev": true,
|
| 938 |
+
"license": "Apache-2.0",
|
| 939 |
+
"engines": {
|
| 940 |
+
"node": ">= 0.4"
|
| 941 |
+
}
|
| 942 |
+
},
|
| 943 |
+
"node_modules/clsx": {
|
| 944 |
+
"version": "2.1.1",
|
| 945 |
+
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
| 946 |
+
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
| 947 |
+
"dev": true,
|
| 948 |
+
"license": "MIT",
|
| 949 |
+
"engines": {
|
| 950 |
+
"node": ">=6"
|
| 951 |
+
}
|
| 952 |
+
},
|
| 953 |
+
"node_modules/debug": {
|
| 954 |
+
"version": "4.4.3",
|
| 955 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 956 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 957 |
+
"dev": true,
|
| 958 |
+
"license": "MIT",
|
| 959 |
+
"dependencies": {
|
| 960 |
+
"ms": "^2.1.3"
|
| 961 |
+
},
|
| 962 |
+
"engines": {
|
| 963 |
+
"node": ">=6.0"
|
| 964 |
+
},
|
| 965 |
+
"peerDependenciesMeta": {
|
| 966 |
+
"supports-color": {
|
| 967 |
+
"optional": true
|
| 968 |
+
}
|
| 969 |
+
}
|
| 970 |
+
},
|
| 971 |
+
"node_modules/deepmerge": {
|
| 972 |
+
"version": "4.3.1",
|
| 973 |
+
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
| 974 |
+
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
| 975 |
+
"dev": true,
|
| 976 |
+
"license": "MIT",
|
| 977 |
+
"engines": {
|
| 978 |
+
"node": ">=0.10.0"
|
| 979 |
+
}
|
| 980 |
+
},
|
| 981 |
+
"node_modules/devalue": {
|
| 982 |
+
"version": "5.8.0",
|
| 983 |
+
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.0.tgz",
|
| 984 |
+
"integrity": "sha512-2zA9pFEsnp7vWBZbXF5JAgAq0fsUIt/1XPbRiAmRV3lp/2C3upzH+sADiyy66aFCihoLEsrQHxNM5w1gIDfsBg==",
|
| 985 |
+
"dev": true,
|
| 986 |
+
"license": "MIT"
|
| 987 |
+
},
|
| 988 |
+
"node_modules/esbuild": {
|
| 989 |
+
"version": "0.21.5",
|
| 990 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
| 991 |
+
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
| 992 |
+
"dev": true,
|
| 993 |
+
"hasInstallScript": true,
|
| 994 |
+
"license": "MIT",
|
| 995 |
+
"bin": {
|
| 996 |
+
"esbuild": "bin/esbuild"
|
| 997 |
+
},
|
| 998 |
+
"engines": {
|
| 999 |
+
"node": ">=12"
|
| 1000 |
+
},
|
| 1001 |
+
"optionalDependencies": {
|
| 1002 |
+
"@esbuild/aix-ppc64": "0.21.5",
|
| 1003 |
+
"@esbuild/android-arm": "0.21.5",
|
| 1004 |
+
"@esbuild/android-arm64": "0.21.5",
|
| 1005 |
+
"@esbuild/android-x64": "0.21.5",
|
| 1006 |
+
"@esbuild/darwin-arm64": "0.21.5",
|
| 1007 |
+
"@esbuild/darwin-x64": "0.21.5",
|
| 1008 |
+
"@esbuild/freebsd-arm64": "0.21.5",
|
| 1009 |
+
"@esbuild/freebsd-x64": "0.21.5",
|
| 1010 |
+
"@esbuild/linux-arm": "0.21.5",
|
| 1011 |
+
"@esbuild/linux-arm64": "0.21.5",
|
| 1012 |
+
"@esbuild/linux-ia32": "0.21.5",
|
| 1013 |
+
"@esbuild/linux-loong64": "0.21.5",
|
| 1014 |
+
"@esbuild/linux-mips64el": "0.21.5",
|
| 1015 |
+
"@esbuild/linux-ppc64": "0.21.5",
|
| 1016 |
+
"@esbuild/linux-riscv64": "0.21.5",
|
| 1017 |
+
"@esbuild/linux-s390x": "0.21.5",
|
| 1018 |
+
"@esbuild/linux-x64": "0.21.5",
|
| 1019 |
+
"@esbuild/netbsd-x64": "0.21.5",
|
| 1020 |
+
"@esbuild/openbsd-x64": "0.21.5",
|
| 1021 |
+
"@esbuild/sunos-x64": "0.21.5",
|
| 1022 |
+
"@esbuild/win32-arm64": "0.21.5",
|
| 1023 |
+
"@esbuild/win32-ia32": "0.21.5",
|
| 1024 |
+
"@esbuild/win32-x64": "0.21.5"
|
| 1025 |
+
}
|
| 1026 |
+
},
|
| 1027 |
+
"node_modules/esm-env": {
|
| 1028 |
+
"version": "1.2.2",
|
| 1029 |
+
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
| 1030 |
+
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
| 1031 |
+
"dev": true,
|
| 1032 |
+
"license": "MIT"
|
| 1033 |
+
},
|
| 1034 |
+
"node_modules/esrap": {
|
| 1035 |
+
"version": "2.2.5",
|
| 1036 |
+
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.5.tgz",
|
| 1037 |
+
"integrity": "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==",
|
| 1038 |
+
"dev": true,
|
| 1039 |
+
"license": "MIT",
|
| 1040 |
+
"dependencies": {
|
| 1041 |
+
"@jridgewell/sourcemap-codec": "^1.4.15"
|
| 1042 |
+
},
|
| 1043 |
+
"peerDependencies": {
|
| 1044 |
+
"@typescript-eslint/types": "^8.2.0"
|
| 1045 |
+
},
|
| 1046 |
+
"peerDependenciesMeta": {
|
| 1047 |
+
"@typescript-eslint/types": {
|
| 1048 |
+
"optional": true
|
| 1049 |
+
}
|
| 1050 |
+
}
|
| 1051 |
+
},
|
| 1052 |
+
"node_modules/fsevents": {
|
| 1053 |
+
"version": "2.3.3",
|
| 1054 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 1055 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1056 |
+
"dev": true,
|
| 1057 |
+
"hasInstallScript": true,
|
| 1058 |
+
"license": "MIT",
|
| 1059 |
+
"optional": true,
|
| 1060 |
+
"os": [
|
| 1061 |
+
"darwin"
|
| 1062 |
+
],
|
| 1063 |
+
"engines": {
|
| 1064 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1065 |
+
}
|
| 1066 |
+
},
|
| 1067 |
+
"node_modules/is-reference": {
|
| 1068 |
+
"version": "3.0.3",
|
| 1069 |
+
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
| 1070 |
+
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
| 1071 |
+
"dev": true,
|
| 1072 |
+
"license": "MIT",
|
| 1073 |
+
"dependencies": {
|
| 1074 |
+
"@types/estree": "^1.0.6"
|
| 1075 |
+
}
|
| 1076 |
+
},
|
| 1077 |
+
"node_modules/kleur": {
|
| 1078 |
+
"version": "4.1.5",
|
| 1079 |
+
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
| 1080 |
+
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
|
| 1081 |
+
"dev": true,
|
| 1082 |
+
"license": "MIT",
|
| 1083 |
+
"engines": {
|
| 1084 |
+
"node": ">=6"
|
| 1085 |
+
}
|
| 1086 |
+
},
|
| 1087 |
+
"node_modules/locate-character": {
|
| 1088 |
+
"version": "3.0.0",
|
| 1089 |
+
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
| 1090 |
+
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
| 1091 |
+
"dev": true,
|
| 1092 |
+
"license": "MIT"
|
| 1093 |
+
},
|
| 1094 |
+
"node_modules/magic-string": {
|
| 1095 |
+
"version": "0.30.21",
|
| 1096 |
+
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
| 1097 |
+
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
| 1098 |
+
"dev": true,
|
| 1099 |
+
"license": "MIT",
|
| 1100 |
+
"dependencies": {
|
| 1101 |
+
"@jridgewell/sourcemap-codec": "^1.5.5"
|
| 1102 |
+
}
|
| 1103 |
+
},
|
| 1104 |
+
"node_modules/ms": {
|
| 1105 |
+
"version": "2.1.3",
|
| 1106 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 1107 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 1108 |
+
"dev": true,
|
| 1109 |
+
"license": "MIT"
|
| 1110 |
+
},
|
| 1111 |
+
"node_modules/nanoid": {
|
| 1112 |
+
"version": "3.3.12",
|
| 1113 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
| 1114 |
+
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
| 1115 |
+
"dev": true,
|
| 1116 |
+
"funding": [
|
| 1117 |
+
{
|
| 1118 |
+
"type": "github",
|
| 1119 |
+
"url": "https://github.com/sponsors/ai"
|
| 1120 |
+
}
|
| 1121 |
+
],
|
| 1122 |
+
"license": "MIT",
|
| 1123 |
+
"bin": {
|
| 1124 |
+
"nanoid": "bin/nanoid.cjs"
|
| 1125 |
+
},
|
| 1126 |
+
"engines": {
|
| 1127 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 1128 |
+
}
|
| 1129 |
+
},
|
| 1130 |
+
"node_modules/picocolors": {
|
| 1131 |
+
"version": "1.1.1",
|
| 1132 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 1133 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 1134 |
+
"dev": true,
|
| 1135 |
+
"license": "ISC"
|
| 1136 |
+
},
|
| 1137 |
+
"node_modules/postcss": {
|
| 1138 |
+
"version": "8.5.13",
|
| 1139 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz",
|
| 1140 |
+
"integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==",
|
| 1141 |
+
"dev": true,
|
| 1142 |
+
"funding": [
|
| 1143 |
+
{
|
| 1144 |
+
"type": "opencollective",
|
| 1145 |
+
"url": "https://opencollective.com/postcss/"
|
| 1146 |
+
},
|
| 1147 |
+
{
|
| 1148 |
+
"type": "tidelift",
|
| 1149 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 1150 |
+
},
|
| 1151 |
+
{
|
| 1152 |
+
"type": "github",
|
| 1153 |
+
"url": "https://github.com/sponsors/ai"
|
| 1154 |
+
}
|
| 1155 |
+
],
|
| 1156 |
+
"license": "MIT",
|
| 1157 |
+
"dependencies": {
|
| 1158 |
+
"nanoid": "^3.3.11",
|
| 1159 |
+
"picocolors": "^1.1.1",
|
| 1160 |
+
"source-map-js": "^1.2.1"
|
| 1161 |
+
},
|
| 1162 |
+
"engines": {
|
| 1163 |
+
"node": "^10 || ^12 || >=14"
|
| 1164 |
+
}
|
| 1165 |
+
},
|
| 1166 |
+
"node_modules/rollup": {
|
| 1167 |
+
"version": "4.60.2",
|
| 1168 |
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
|
| 1169 |
+
"integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
|
| 1170 |
+
"dev": true,
|
| 1171 |
+
"license": "MIT",
|
| 1172 |
+
"dependencies": {
|
| 1173 |
+
"@types/estree": "1.0.8"
|
| 1174 |
+
},
|
| 1175 |
+
"bin": {
|
| 1176 |
+
"rollup": "dist/bin/rollup"
|
| 1177 |
+
},
|
| 1178 |
+
"engines": {
|
| 1179 |
+
"node": ">=18.0.0",
|
| 1180 |
+
"npm": ">=8.0.0"
|
| 1181 |
+
},
|
| 1182 |
+
"optionalDependencies": {
|
| 1183 |
+
"@rollup/rollup-android-arm-eabi": "4.60.2",
|
| 1184 |
+
"@rollup/rollup-android-arm64": "4.60.2",
|
| 1185 |
+
"@rollup/rollup-darwin-arm64": "4.60.2",
|
| 1186 |
+
"@rollup/rollup-darwin-x64": "4.60.2",
|
| 1187 |
+
"@rollup/rollup-freebsd-arm64": "4.60.2",
|
| 1188 |
+
"@rollup/rollup-freebsd-x64": "4.60.2",
|
| 1189 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
|
| 1190 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.60.2",
|
| 1191 |
+
"@rollup/rollup-linux-arm64-gnu": "4.60.2",
|
| 1192 |
+
"@rollup/rollup-linux-arm64-musl": "4.60.2",
|
| 1193 |
+
"@rollup/rollup-linux-loong64-gnu": "4.60.2",
|
| 1194 |
+
"@rollup/rollup-linux-loong64-musl": "4.60.2",
|
| 1195 |
+
"@rollup/rollup-linux-ppc64-gnu": "4.60.2",
|
| 1196 |
+
"@rollup/rollup-linux-ppc64-musl": "4.60.2",
|
| 1197 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.60.2",
|
| 1198 |
+
"@rollup/rollup-linux-riscv64-musl": "4.60.2",
|
| 1199 |
+
"@rollup/rollup-linux-s390x-gnu": "4.60.2",
|
| 1200 |
+
"@rollup/rollup-linux-x64-gnu": "4.60.2",
|
| 1201 |
+
"@rollup/rollup-linux-x64-musl": "4.60.2",
|
| 1202 |
+
"@rollup/rollup-openbsd-x64": "4.60.2",
|
| 1203 |
+
"@rollup/rollup-openharmony-arm64": "4.60.2",
|
| 1204 |
+
"@rollup/rollup-win32-arm64-msvc": "4.60.2",
|
| 1205 |
+
"@rollup/rollup-win32-ia32-msvc": "4.60.2",
|
| 1206 |
+
"@rollup/rollup-win32-x64-gnu": "4.60.2",
|
| 1207 |
+
"@rollup/rollup-win32-x64-msvc": "4.60.2",
|
| 1208 |
+
"fsevents": "~2.3.2"
|
| 1209 |
+
}
|
| 1210 |
+
},
|
| 1211 |
+
"node_modules/source-map-js": {
|
| 1212 |
+
"version": "1.2.1",
|
| 1213 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 1214 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 1215 |
+
"dev": true,
|
| 1216 |
+
"license": "BSD-3-Clause",
|
| 1217 |
+
"engines": {
|
| 1218 |
+
"node": ">=0.10.0"
|
| 1219 |
+
}
|
| 1220 |
+
},
|
| 1221 |
+
"node_modules/svelte": {
|
| 1222 |
+
"version": "5.55.5",
|
| 1223 |
+
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.5.tgz",
|
| 1224 |
+
"integrity": "sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==",
|
| 1225 |
+
"dev": true,
|
| 1226 |
+
"license": "MIT",
|
| 1227 |
+
"dependencies": {
|
| 1228 |
+
"@jridgewell/remapping": "^2.3.4",
|
| 1229 |
+
"@jridgewell/sourcemap-codec": "^1.5.0",
|
| 1230 |
+
"@sveltejs/acorn-typescript": "^1.0.5",
|
| 1231 |
+
"@types/estree": "^1.0.5",
|
| 1232 |
+
"@types/trusted-types": "^2.0.7",
|
| 1233 |
+
"acorn": "^8.12.1",
|
| 1234 |
+
"aria-query": "5.3.1",
|
| 1235 |
+
"axobject-query": "^4.1.0",
|
| 1236 |
+
"clsx": "^2.1.1",
|
| 1237 |
+
"devalue": "^5.6.4",
|
| 1238 |
+
"esm-env": "^1.2.1",
|
| 1239 |
+
"esrap": "^2.2.4",
|
| 1240 |
+
"is-reference": "^3.0.3",
|
| 1241 |
+
"locate-character": "^3.0.0",
|
| 1242 |
+
"magic-string": "^0.30.11",
|
| 1243 |
+
"zimmerframe": "^1.1.2"
|
| 1244 |
+
},
|
| 1245 |
+
"engines": {
|
| 1246 |
+
"node": ">=18"
|
| 1247 |
+
}
|
| 1248 |
+
},
|
| 1249 |
+
"node_modules/vite": {
|
| 1250 |
+
"version": "5.4.21",
|
| 1251 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
| 1252 |
+
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
| 1253 |
+
"dev": true,
|
| 1254 |
+
"license": "MIT",
|
| 1255 |
+
"dependencies": {
|
| 1256 |
+
"esbuild": "^0.21.3",
|
| 1257 |
+
"postcss": "^8.4.43",
|
| 1258 |
+
"rollup": "^4.20.0"
|
| 1259 |
+
},
|
| 1260 |
+
"bin": {
|
| 1261 |
+
"vite": "bin/vite.js"
|
| 1262 |
+
},
|
| 1263 |
+
"engines": {
|
| 1264 |
+
"node": "^18.0.0 || >=20.0.0"
|
| 1265 |
+
},
|
| 1266 |
+
"funding": {
|
| 1267 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 1268 |
+
},
|
| 1269 |
+
"optionalDependencies": {
|
| 1270 |
+
"fsevents": "~2.3.3"
|
| 1271 |
+
},
|
| 1272 |
+
"peerDependencies": {
|
| 1273 |
+
"@types/node": "^18.0.0 || >=20.0.0",
|
| 1274 |
+
"less": "*",
|
| 1275 |
+
"lightningcss": "^1.21.0",
|
| 1276 |
+
"sass": "*",
|
| 1277 |
+
"sass-embedded": "*",
|
| 1278 |
+
"stylus": "*",
|
| 1279 |
+
"sugarss": "*",
|
| 1280 |
+
"terser": "^5.4.0"
|
| 1281 |
+
},
|
| 1282 |
+
"peerDependenciesMeta": {
|
| 1283 |
+
"@types/node": {
|
| 1284 |
+
"optional": true
|
| 1285 |
+
},
|
| 1286 |
+
"less": {
|
| 1287 |
+
"optional": true
|
| 1288 |
+
},
|
| 1289 |
+
"lightningcss": {
|
| 1290 |
+
"optional": true
|
| 1291 |
+
},
|
| 1292 |
+
"sass": {
|
| 1293 |
+
"optional": true
|
| 1294 |
+
},
|
| 1295 |
+
"sass-embedded": {
|
| 1296 |
+
"optional": true
|
| 1297 |
+
},
|
| 1298 |
+
"stylus": {
|
| 1299 |
+
"optional": true
|
| 1300 |
+
},
|
| 1301 |
+
"sugarss": {
|
| 1302 |
+
"optional": true
|
| 1303 |
+
},
|
| 1304 |
+
"terser": {
|
| 1305 |
+
"optional": true
|
| 1306 |
+
}
|
| 1307 |
+
}
|
| 1308 |
+
},
|
| 1309 |
+
"node_modules/vitefu": {
|
| 1310 |
+
"version": "1.1.3",
|
| 1311 |
+
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz",
|
| 1312 |
+
"integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==",
|
| 1313 |
+
"dev": true,
|
| 1314 |
+
"license": "MIT",
|
| 1315 |
+
"workspaces": [
|
| 1316 |
+
"tests/deps/*",
|
| 1317 |
+
"tests/projects/*",
|
| 1318 |
+
"tests/projects/workspace/packages/*"
|
| 1319 |
+
],
|
| 1320 |
+
"peerDependencies": {
|
| 1321 |
+
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
| 1322 |
+
},
|
| 1323 |
+
"peerDependenciesMeta": {
|
| 1324 |
+
"vite": {
|
| 1325 |
+
"optional": true
|
| 1326 |
+
}
|
| 1327 |
+
}
|
| 1328 |
+
},
|
| 1329 |
+
"node_modules/zimmerframe": {
|
| 1330 |
+
"version": "1.1.4",
|
| 1331 |
+
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
| 1332 |
+
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
| 1333 |
+
"dev": true,
|
| 1334 |
+
"license": "MIT"
|
| 1335 |
+
}
|
| 1336 |
+
}
|
| 1337 |
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "riprap-svelte",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.1.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"build": "vite build",
|
| 8 |
+
"dev": "vite build --watch"
|
| 9 |
+
},
|
| 10 |
+
"devDependencies": {
|
| 11 |
+
"svelte": "^5.0.0",
|
| 12 |
+
"vite": "^5.4.0",
|
| 13 |
+
"@sveltejs/vite-plugin-svelte": "^4.0.0"
|
| 14 |
+
}
|
| 15 |
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<svelte:options
|
| 2 |
+
customElement={{
|
| 3 |
+
tag: "r-briefing",
|
| 4 |
+
props: {
|
| 5 |
+
text: { type: "String" },
|
| 6 |
+
streaming: { type: "Boolean", reflect: true },
|
| 7 |
+
sourceLabels: { type: "Object" },
|
| 8 |
+
},
|
| 9 |
+
}} />
|
| 10 |
+
|
| 11 |
+
<script>
|
| 12 |
+
import { onMount, tick } from "svelte";
|
| 13 |
+
import { citeIndex, highlightedDocId } from "./stores.js";
|
| 14 |
+
|
| 15 |
+
let { text = "", streaming = false, sourceLabels = {} } = $props();
|
| 16 |
+
|
| 17 |
+
const escapeHtml = (s) =>
|
| 18 |
+
String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
| 19 |
+
|
| 20 |
+
function renderMarkdown(input) {
|
| 21 |
+
const lines = input.split("\n");
|
| 22 |
+
const out = [];
|
| 23 |
+
let para = []; let bullets = [];
|
| 24 |
+
const flushPara = () => {
|
| 25 |
+
if (!para.length) return;
|
| 26 |
+
const safe = escapeHtml(para.join(" ").trim())
|
| 27 |
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
| 28 |
+
if (safe) out.push(`<p class="rsum-p">${safe}</p>`);
|
| 29 |
+
para = [];
|
| 30 |
+
};
|
| 31 |
+
const flushBullets = () => {
|
| 32 |
+
if (!bullets.length) return;
|
| 33 |
+
const items = bullets.map(b => {
|
| 34 |
+
const safe = escapeHtml(b.trim()).replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
| 35 |
+
return `<li>${safe}</li>`;
|
| 36 |
+
}).join("");
|
| 37 |
+
out.push(`<ul class="rsum-list">${items}</ul>`);
|
| 38 |
+
bullets = [];
|
| 39 |
+
};
|
| 40 |
+
// Granite sometimes runs all bullets onto one line.
|
| 41 |
+
const expanded = [];
|
| 42 |
+
for (const line of lines) {
|
| 43 |
+
if (line.trim().startsWith("- ") && line.includes(" - ", 2)) {
|
| 44 |
+
const parts = line.split(/(?:^|(?<=\.\s))\s*-\s+/g).filter(p => p.trim());
|
| 45 |
+
for (const p of parts) expanded.push("- " + p.trim());
|
| 46 |
+
} else { expanded.push(line); }
|
| 47 |
+
}
|
| 48 |
+
for (const line of expanded) {
|
| 49 |
+
const m = line.match(/^\s*\*\*([A-Z][A-Za-z\s/]+)\.\*\*\s*$/);
|
| 50 |
+
if (m) { flushPara(); flushBullets(); out.push(`<h4 class="rsum-h">${escapeHtml(m[1])}</h4>`); }
|
| 51 |
+
else if (/^\s*[-*]\s+/.test(line)) { flushPara(); bullets.push(line.replace(/^\s*[-*]\s+/, "")); }
|
| 52 |
+
else { flushBullets(); para.push(line); }
|
| 53 |
+
}
|
| 54 |
+
flushPara(); flushBullets();
|
| 55 |
+
return out.join("");
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
function rewriteCitations(html, indexMap) {
|
| 59 |
+
return html.replace(/\[([a-z0-9_]+)\]/gi, (_, id) => {
|
| 60 |
+
const norm = id.toLowerCase();
|
| 61 |
+
if (indexMap[norm] == null) indexMap[norm] = Object.keys(indexMap).length + 1;
|
| 62 |
+
const n = indexMap[norm];
|
| 63 |
+
const lab = sourceLabels[norm] || norm;
|
| 64 |
+
return `<span class="cite" data-src-id="${norm}" data-src-n="${n}" title="${lab.replace(/"/g, """)} — click to highlight">${n}</span>`;
|
| 65 |
+
});
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
let bodyHtml = $derived.by(() => {
|
| 69 |
+
if (!text) return "";
|
| 70 |
+
const indexMap = {};
|
| 71 |
+
const md = renderMarkdown(text);
|
| 72 |
+
const html = rewriteCitations(md, indexMap);
|
| 73 |
+
queueMicrotask(() => citeIndex.set({ ...indexMap }));
|
| 74 |
+
return html;
|
| 75 |
+
});
|
| 76 |
+
|
| 77 |
+
let container;
|
| 78 |
+
let hl = $derived($highlightedDocId);
|
| 79 |
+
|
| 80 |
+
// Re-bind chip listeners + hl class whenever the body or hl changes.
|
| 81 |
+
$effect(() => {
|
| 82 |
+
void bodyHtml; void hl;
|
| 83 |
+
if (!container) return;
|
| 84 |
+
tick().then(() => {
|
| 85 |
+
const chips = container.querySelectorAll(".cite");
|
| 86 |
+
chips.forEach(c => {
|
| 87 |
+
const id = c.dataset.srcId;
|
| 88 |
+
if (!id) return;
|
| 89 |
+
c.classList.toggle("hl", id === hl);
|
| 90 |
+
if (c.dataset.bound) return;
|
| 91 |
+
c.dataset.bound = "1";
|
| 92 |
+
c.addEventListener("mouseenter", () => highlightedDocId.set(id));
|
| 93 |
+
c.addEventListener("click", (e) => {
|
| 94 |
+
e.stopPropagation();
|
| 95 |
+
highlightedDocId.update(cur => cur === id ? null : id);
|
| 96 |
+
});
|
| 97 |
+
});
|
| 98 |
+
});
|
| 99 |
+
});
|
| 100 |
+
</script>
|
| 101 |
+
|
| 102 |
+
{#if !text}
|
| 103 |
+
<div class="rsum-p" style="color:var(--text-muted, #6b7280)">Waiting for content…</div>
|
| 104 |
+
{:else}
|
| 105 |
+
<div bind:this={container}>
|
| 106 |
+
{@html bodyHtml}
|
| 107 |
+
</div>
|
| 108 |
+
{/if}
|
| 109 |
+
|
| 110 |
+
<style>
|
| 111 |
+
:host { display: block; }
|
| 112 |
+
/* The host-level styles for typography, .cite, etc. live in the parent
|
| 113 |
+
stylesheet and target #paragraph descendants — they pierce shadow DOM
|
| 114 |
+
for inline-styled markup we don't ship here. The .rsum-* classes are
|
| 115 |
+
wired in the global stylesheet. We intentionally don't restate them. */
|
| 116 |
+
:host(.streaming)::after,
|
| 117 |
+
:host([streaming])::after {
|
| 118 |
+
content: "▋";
|
| 119 |
+
display: inline-block; color: var(--nyc-blue, #1642DF);
|
| 120 |
+
margin-left: 2px;
|
| 121 |
+
animation: caret 0.9s steps(1) infinite;
|
| 122 |
+
}
|
| 123 |
+
@keyframes caret { 50% { opacity: 0; } }
|
| 124 |
+
</style>
|
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<svelte:options
|
| 2 |
+
customElement={{
|
| 3 |
+
tag: "r-sources-footer",
|
| 4 |
+
props: {
|
| 5 |
+
labels: { type: "Object" },
|
| 6 |
+
urls: { type: "Object" },
|
| 7 |
+
vintages: { type: "Object" },
|
| 8 |
+
},
|
| 9 |
+
}} />
|
| 10 |
+
|
| 11 |
+
<script>
|
| 12 |
+
import { citeIndex, highlightedDocId } from "./stores.js";
|
| 13 |
+
import { fade, scale } from "svelte/transition";
|
| 14 |
+
import { cubicOut } from "svelte/easing";
|
| 15 |
+
|
| 16 |
+
let { labels = {}, urls = {}, vintages = {} } = $props();
|
| 17 |
+
|
| 18 |
+
let entries = $derived(
|
| 19 |
+
Object.entries($citeIndex || {}).sort((a, b) => a[1] - b[1])
|
| 20 |
+
);
|
| 21 |
+
let hl = $derived($highlightedDocId);
|
| 22 |
+
</script>
|
| 23 |
+
|
| 24 |
+
{#if entries.length}
|
| 25 |
+
<div class="src-h" in:fade={{ duration: 200 }}>Sources</div>
|
| 26 |
+
<ol>
|
| 27 |
+
{#each entries as [id, n] (id)}
|
| 28 |
+
{@const url = urls[id]}
|
| 29 |
+
{@const label = labels[id] || id}
|
| 30 |
+
{@const vintage = vintages[id]}
|
| 31 |
+
<li class:hl={id === hl}
|
| 32 |
+
in:scale={{ start: 0.96, duration: 220, easing: cubicOut }}
|
| 33 |
+
onmouseenter={() => highlightedDocId.set(id)}
|
| 34 |
+
onclick={() => highlightedDocId.set(hl === id ? null : id)}>
|
| 35 |
+
<span class="src-num">[{n}]</span>
|
| 36 |
+
<div>
|
| 37 |
+
{#if url}
|
| 38 |
+
<a class="src-link" href={url} target="_blank" rel="noopener noreferrer"
|
| 39 |
+
onclick={(e) => e.stopPropagation()}>
|
| 40 |
+
{label} <span class="src-ext">↗</span>
|
| 41 |
+
</a>
|
| 42 |
+
{:else}
|
| 43 |
+
<span>{label}</span>
|
| 44 |
+
{/if}
|
| 45 |
+
<span class="src-id">{id}</span>
|
| 46 |
+
{#if vintage}<span class="src-vintage">{vintage}</span>{/if}
|
| 47 |
+
</div>
|
| 48 |
+
</li>
|
| 49 |
+
{/each}
|
| 50 |
+
</ol>
|
| 51 |
+
{/if}
|
| 52 |
+
|
| 53 |
+
<style>
|
| 54 |
+
:host {
|
| 55 |
+
display: block;
|
| 56 |
+
border-top: 1px solid var(--line, #e5e7eb);
|
| 57 |
+
background: var(--bg-soft, #f5f7fb);
|
| 58 |
+
padding: 12px 16px 14px;
|
| 59 |
+
}
|
| 60 |
+
:host(:not(:has(ol))) { display: none; }
|
| 61 |
+
.src-h {
|
| 62 |
+
font-size: 10px; font-weight: 700;
|
| 63 |
+
text-transform: uppercase; letter-spacing: 0.10em;
|
| 64 |
+
color: var(--text-muted, #6b7280);
|
| 65 |
+
margin: 0 0 8px;
|
| 66 |
+
}
|
| 67 |
+
ol {
|
| 68 |
+
margin: 0; padding: 0; list-style: none;
|
| 69 |
+
display: grid; gap: 6px;
|
| 70 |
+
font-size: 11.5px; line-height: 1.45;
|
| 71 |
+
}
|
| 72 |
+
li {
|
| 73 |
+
display: grid; grid-template-columns: 22px 1fr;
|
| 74 |
+
gap: 8px; align-items: baseline;
|
| 75 |
+
padding: 4px 6px; border-radius: 3px;
|
| 76 |
+
cursor: pointer;
|
| 77 |
+
transition: background 0.15s;
|
| 78 |
+
}
|
| 79 |
+
li:hover, li.hl { background: rgba(22, 66, 223, 0.10); }
|
| 80 |
+
li.hl {
|
| 81 |
+
/* Brief pulse each time a chip selects this row. */
|
| 82 |
+
animation: pulse 360ms cubic-bezier(.2,.7,.3,1);
|
| 83 |
+
}
|
| 84 |
+
@keyframes pulse {
|
| 85 |
+
0% { box-shadow: 0 0 0 0 rgba(22, 66, 223, 0.35); }
|
| 86 |
+
60% { box-shadow: 0 0 0 6px rgba(22, 66, 223, 0.00); }
|
| 87 |
+
100% { box-shadow: 0 0 0 0 rgba(22, 66, 223, 0.00); }
|
| 88 |
+
}
|
| 89 |
+
.src-num {
|
| 90 |
+
font-family: var(--mono, monospace); font-size: 10.5px;
|
| 91 |
+
font-weight: 700; color: var(--nyc-blue, #1642DF);
|
| 92 |
+
text-align: right;
|
| 93 |
+
}
|
| 94 |
+
.src-link {
|
| 95 |
+
color: var(--text, #111); text-decoration: none;
|
| 96 |
+
border-bottom: 1px dotted var(--text-muted, #6b7280);
|
| 97 |
+
}
|
| 98 |
+
.src-link:hover {
|
| 99 |
+
color: var(--nyc-blue, #1642DF);
|
| 100 |
+
border-bottom-color: var(--nyc-blue, #1642DF);
|
| 101 |
+
}
|
| 102 |
+
.src-ext { font-size: 9.5px; color: var(--text-faint, #9ca3af); margin-left: 2px; vertical-align: super; }
|
| 103 |
+
.src-vintage { display: block; color: var(--text-muted, #6b7280); font-size: 9.5px; margin-top: 2px; }
|
| 104 |
+
.src-id { display: inline-block; font-family: var(--mono, monospace); font-size: 9.5px; color: var(--text-faint, #9ca3af); margin-left: 6px; }
|
| 105 |
+
</style>
|
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<svelte:options
|
| 2 |
+
customElement={{
|
| 3 |
+
tag: "r-trace",
|
| 4 |
+
props: {
|
| 5 |
+
stepLabels: { type: "Object" },
|
| 6 |
+
},
|
| 7 |
+
}} />
|
| 8 |
+
|
| 9 |
+
<script>
|
| 10 |
+
import { onMount } from "svelte";
|
| 11 |
+
import { fly, fade } from "svelte/transition";
|
| 12 |
+
import { cubicOut } from "svelte/easing";
|
| 13 |
+
|
| 14 |
+
let { stepLabels = {} } = $props();
|
| 15 |
+
let steps = $state([]);
|
| 16 |
+
|
| 17 |
+
// Imperative API consumed by legacy agent.js. Exposed via the host
|
| 18 |
+
// element through onMount once the custom element is upgraded.
|
| 19 |
+
onMount(() => {
|
| 20 |
+
// `this` would be the wrapper context; rely on the component's
|
| 21 |
+
// host element discovery via document.currentScript trickery is
|
| 22 |
+
// brittle — Svelte exposes the host via the element instance.
|
| 23 |
+
// We expose pushStep / clear via a custom event-driven API:
|
| 24 |
+
// el.dispatchEvent(new CustomEvent('riprap-trace-push', { detail: step }))
|
| 25 |
+
// el.dispatchEvent(new CustomEvent('riprap-trace-clear'))
|
| 26 |
+
// But agent.js currently calls el.pushStep() / el.clear(); to keep
|
| 27 |
+
// that ergonomic we attach methods to the host in onMount.
|
| 28 |
+
// The host is the parent of the shadow root.
|
| 29 |
+
const host = container?.getRootNode()?.host;
|
| 30 |
+
if (host) {
|
| 31 |
+
host.pushStep = (step) => { steps = [...steps, step]; };
|
| 32 |
+
host.clear = () => { steps = []; };
|
| 33 |
+
}
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
let container;
|
| 37 |
+
|
| 38 |
+
const escapeHtml = (s) =>
|
| 39 |
+
String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
| 40 |
+
|
| 41 |
+
function classFor(step) {
|
| 42 |
+
return step.ok === true ? "ok" : step.ok === false ? "err" : "running";
|
| 43 |
+
}
|
| 44 |
+
function markFor(step) {
|
| 45 |
+
return step.ok === true ? "✓" : step.ok === false ? "✗" : "○";
|
| 46 |
+
}
|
| 47 |
+
function labelFor(step) {
|
| 48 |
+
return (stepLabels[step.step] && stepLabels[step.step][0]) || step.step;
|
| 49 |
+
}
|
| 50 |
+
function hintFor(step) {
|
| 51 |
+
return (stepLabels[step.step] && stepLabels[step.step][1]) || "";
|
| 52 |
+
}
|
| 53 |
+
</script>
|
| 54 |
+
|
| 55 |
+
<ol bind:this={container} id="steps-list">
|
| 56 |
+
{#each steps as step, i (i)}
|
| 57 |
+
<li class={classFor(step)}
|
| 58 |
+
in:fly={{ y: -8, duration: 220, easing: cubicOut }}>
|
| 59 |
+
<span class="icon">{markFor(step)}</span>
|
| 60 |
+
<div>
|
| 61 |
+
<div class="label">{labelFor(step)}</div>
|
| 62 |
+
<div class="meta">{hintFor(step)}</div>
|
| 63 |
+
</div>
|
| 64 |
+
{#if step.elapsed_s != null}
|
| 65 |
+
<span class="time">{step.elapsed_s}s</span>
|
| 66 |
+
{/if}
|
| 67 |
+
{#if step.result}
|
| 68 |
+
<div class="result">{JSON.stringify(step.result)}</div>
|
| 69 |
+
{/if}
|
| 70 |
+
{#if step.err}
|
| 71 |
+
<div class="result" style="color:var(--nyc-scarlet, #b80000)">{step.err}</div>
|
| 72 |
+
{/if}
|
| 73 |
+
</li>
|
| 74 |
+
{/each}
|
| 75 |
+
</ol>
|
| 76 |
+
|
| 77 |
+
<style>
|
| 78 |
+
:host { display: block; }
|
| 79 |
+
ol {
|
| 80 |
+
list-style: none; margin: 0; padding: 4px 0;
|
| 81 |
+
font-size: 12.5px;
|
| 82 |
+
}
|
| 83 |
+
li {
|
| 84 |
+
display: grid;
|
| 85 |
+
grid-template-columns: 18px 1fr auto;
|
| 86 |
+
gap: 10px;
|
| 87 |
+
padding: 7px 14px;
|
| 88 |
+
border-bottom: 1px solid var(--line, #e5e7eb);
|
| 89 |
+
align-items: baseline;
|
| 90 |
+
}
|
| 91 |
+
li:last-child { border-bottom: 0; }
|
| 92 |
+
.icon { font-weight: 700; font-size: 14px; line-height: 1; }
|
| 93 |
+
.running .icon { color: var(--nyc-blue, #1642DF); }
|
| 94 |
+
.ok .icon { color: var(--good, #1a8754); }
|
| 95 |
+
.err .icon { color: var(--nyc-scarlet, #b80000); }
|
| 96 |
+
.label { color: var(--text, #111); font-weight: 500; }
|
| 97 |
+
.meta { color: var(--text-muted, #6b7280); font-size: 11px; }
|
| 98 |
+
.time { font-family: var(--mono, monospace); color: var(--text-faint, #9ca3af); font-size: 11.5px; }
|
| 99 |
+
.running { background: rgba(22, 66, 223, 0.04); }
|
| 100 |
+
.result {
|
| 101 |
+
grid-column: 2 / -1;
|
| 102 |
+
color: var(--text-muted, #6b7280);
|
| 103 |
+
font-size: 11px;
|
| 104 |
+
font-family: var(--mono, monospace);
|
| 105 |
+
margin-top: 3px;
|
| 106 |
+
word-break: break-word;
|
| 107 |
+
line-height: 1.4;
|
| 108 |
+
}
|
| 109 |
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Shared state across all three custom elements. Svelte stores have
|
| 2 |
+
// .subscribe (Svelte runtime uses it) AND a manual subscribe is exposed
|
| 3 |
+
// here so legacy agent.js can wire vanilla DOM to the same signal that
|
| 4 |
+
// Svelte components react to.
|
| 5 |
+
import { writable } from "svelte/store";
|
| 6 |
+
|
| 7 |
+
export const highlightedDocId = writable(null);
|
| 8 |
+
// { doc_id: number } — Briefing populates as it encounters citations,
|
| 9 |
+
// SourcesFooter renders the numbered list from this.
|
| 10 |
+
export const citeIndex = writable({});
|
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Side-effect imports register the custom elements; agent.html mounts
|
| 2 |
+
// the same <r-briefing>, <r-trace>, <r-sources-footer> tags it always
|
| 3 |
+
// did — only the implementation changed.
|
| 4 |
+
import "./lib/SourcesFooter.svelte";
|
| 5 |
+
import "./lib/Briefing.svelte";
|
| 6 |
+
import "./lib/Trace.svelte";
|
| 7 |
+
|
| 8 |
+
// Re-export shared stores so non-Svelte code (legacy agent.js, the
|
| 9 |
+
// briefing chip-binding subscriber) can reach them. agent.js does:
|
| 10 |
+
// import("/static/dist/riprap.js").then(m => m.highlightedDocId.set(id))
|
| 11 |
+
export { highlightedDocId, citeIndex } from "./lib/stores.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from "vite";
|
| 2 |
+
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
| 3 |
+
import { resolve } from "path";
|
| 4 |
+
|
| 5 |
+
// Svelte 5 custom-element build → drop-in replacement for the Lit
|
| 6 |
+
// components in /web/static/components. agent.html keeps using
|
| 7 |
+
// <r-briefing>, <r-trace>, <r-sources-footer>; this bundle just
|
| 8 |
+
// supplies their implementation.
|
| 9 |
+
export default defineConfig({
|
| 10 |
+
plugins: [
|
| 11 |
+
svelte({
|
| 12 |
+
compilerOptions: {
|
| 13 |
+
// Globally compile every .svelte file as a custom element so we
|
| 14 |
+
// get one bundle per page, no per-file flag needed.
|
| 15 |
+
customElement: true,
|
| 16 |
+
},
|
| 17 |
+
}),
|
| 18 |
+
],
|
| 19 |
+
build: {
|
| 20 |
+
outDir: resolve(__dirname, "../static/dist"),
|
| 21 |
+
emptyOutDir: true,
|
| 22 |
+
lib: {
|
| 23 |
+
entry: resolve(__dirname, "src/main.js"),
|
| 24 |
+
formats: ["es"],
|
| 25 |
+
fileName: () => "riprap.js",
|
| 26 |
+
},
|
| 27 |
+
rollupOptions: {
|
| 28 |
+
output: { inlineDynamicImports: true },
|
| 29 |
+
},
|
| 30 |
+
target: "es2022",
|
| 31 |
+
sourcemap: true,
|
| 32 |
+
},
|
| 33 |
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
.svelte-kit
|
| 3 |
+
# build/ is committed — HF Spaces does not run Node, so we ship the
|
| 4 |
+
# adapter-static output the same way web/static/dist/riprap.js is committed
|
| 5 |
+
# for the legacy custom-element bundle.
|
| 6 |
+
.DS_Store
|
| 7 |
+
.env*
|
| 8 |
+
!.env.example
|
| 9 |
+
vite.config.ts.timestamp-*
|
| 10 |
+
test-results/
|
| 11 |
+
playwright-report/
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
engine-strict=true
|
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' fill='%23FAFAF7'/%3E%3Crect x='2' y='2' width='5' height='12' fill='%23D17C00'/%3E%3C/svg%3E" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 7 |
+
<meta name="description" content="Riprap — citation-grounded NYC flood-exposure briefings." />
|
| 8 |
+
<title>Riprap — flood-exposure briefing</title>
|
| 9 |
+
<link href="/_app/immutable/entry/start.CmPnsLDb.js" rel="modulepreload">
|
| 10 |
+
<link href="/_app/immutable/chunks/DOnKrFbX.js" rel="modulepreload">
|
| 11 |
+
<link href="/_app/immutable/chunks/CYuHyzh3.js" rel="modulepreload">
|
| 12 |
+
<link href="/_app/immutable/entry/app.NAzo06Kr.js" rel="modulepreload">
|
| 13 |
+
<link href="/_app/immutable/chunks/DwPgZwgo.js" rel="modulepreload">
|
| 14 |
+
<link href="/_app/immutable/chunks/BLULdth_.js" rel="modulepreload">
|
| 15 |
+
<link href="/_app/immutable/chunks/vWNuMvXT.js" rel="modulepreload">
|
| 16 |
+
<link href="/_app/immutable/nodes/0.B4c3XvjB.js" rel="modulepreload">
|
| 17 |
+
<link href="/_app/immutable/chunks/CdmpEGnB.js" rel="modulepreload">
|
| 18 |
+
<link href="/_app/immutable/chunks/DO0D806X.js" rel="modulepreload">
|
| 19 |
+
<link href="/_app/immutable/chunks/CpEmpa3I.js" rel="modulepreload">
|
| 20 |
+
|
| 21 |
+
<link href="/_app/immutable/assets/0.KpTzaSsX.css" rel="stylesheet">
|
| 22 |
+
</head>
|
| 23 |
+
<body data-sveltekit-preload-data="hover">
|
| 24 |
+
<div style="display: contents">
|
| 25 |
+
<script>
|
| 26 |
+
{
|
| 27 |
+
__sveltekit_yhur1t = {
|
| 28 |
+
base: ""
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
const element = document.currentScript.parentElement;
|
| 32 |
+
|
| 33 |
+
Promise.all([
|
| 34 |
+
import("/_app/immutable/entry/start.CmPnsLDb.js"),
|
| 35 |
+
import("/_app/immutable/entry/app.NAzo06Kr.js")
|
| 36 |
+
]).then(([kit, app]) => {
|
| 37 |
+
kit.start(app, element);
|
| 38 |
+
});
|
| 39 |
+
}
|
| 40 |
+
</script>
|
| 41 |
+
</div>
|
| 42 |
+
</body>
|
| 43 |
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
export const env={}
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.print-doc.svelte-uialbm{max-width:7.5in;margin:.5in auto;padding:0 .5in;font-family:var(--font-serif),Georgia,serif;color:#111;background:#fff}.print-head.svelte-uialbm{border-bottom:1pt solid #111;padding-bottom:8pt;margin-bottom:14pt}.print-head-top.svelte-uialbm{display:flex;justify-content:space-between;align-items:baseline;font:9pt var(--font-mono, "IBM Plex Mono");color:#4a4a4a;text-transform:uppercase;letter-spacing:.04em}.wordmark.svelte-uialbm{font-weight:600;color:#111}.print-title.svelte-uialbm{font:600 22pt var(--font-sans, "IBM Plex Sans");margin:8pt 0 4pt;line-height:1.15}.print-sub.svelte-uialbm{font:10pt var(--font-mono, "IBM Plex Mono");color:#4a4a4a}.print-controls.svelte-uialbm{display:flex;gap:12px;align-items:center;margin:12pt 0;padding:8pt 10pt;background:#f5f5f3;border:1px solid #d8d6d2;border-radius:4px;font:10pt var(--font-sans, "IBM Plex Sans")}.print-controls.svelte-uialbm button:where(.svelte-uialbm){font:10pt var(--font-sans, "IBM Plex Sans");padding:4pt 10pt;background:#111;color:#fff;border:0;border-radius:3px;cursor:pointer}.hint.svelte-uialbm{color:#4a4a4a;font-size:9pt}.print-citations.svelte-uialbm{margin-top:18pt;padding-top:8pt;border-top:1pt solid #111;page-break-before:always}.print-citations.svelte-uialbm h2:where(.svelte-uialbm){font:600 13pt var(--font-sans, "IBM Plex Sans");margin:0 0 8pt}.print-citations.svelte-uialbm ol:where(.svelte-uialbm){list-style:none;padding:0;margin:0}.print-citations.svelte-uialbm li:where(.svelte-uialbm){margin-bottom:8pt;padding-left:28pt;position:relative;font-size:10pt;line-height:1.4;break-inside:avoid}.cn.svelte-uialbm{position:absolute;left:0;top:0;font:600 10pt var(--font-mono, "IBM Plex Mono");color:#0b5394}.cglyph.svelte-uialbm{display:inline-block;vertical-align:middle;margin-right:4pt}.csrc.svelte-uialbm{font-weight:600}.cvint.svelte-uialbm{color:#4a4a4a;margin-left:6pt;font-size:9pt}.ctitle.svelte-uialbm{color:#1a1a1a}.curl.svelte-uialbm{font:8.5pt var(--font-mono, "IBM Plex Mono");color:#0b5394;word-break:break-all}.cdocid.svelte-uialbm{font:8.5pt var(--font-mono, "IBM Plex Mono");color:#6b6b6b}.print-foot.svelte-uialbm{margin-top:18pt;padding-top:6pt;border-top:1pt solid #c8c6c2;font:8.5pt var(--font-mono, "IBM Plex Mono");color:#6b6b6b;line-height:1.5}.empty.svelte-uialbm{max-width:600px;margin:100px auto;padding:24px;font-family:var(--font-sans, "IBM Plex Sans");color:#1a1a1a}.empty.svelte-uialbm h1:where(.svelte-uialbm){font-size:20pt;margin-bottom:8pt}.empty.svelte-uialbm a:where(.svelte-uialbm){color:#0b5394}@media print{.no-print.svelte-uialbm{display:none!important}.print-doc.svelte-uialbm{margin:0;padding:0;max-width:none}@page{size:letter;margin:.85in .85in .85in 1in;@bottom-right{content:"page " counter(page) " of " counter(pages);font:9pt IBM Plex Mono;color:#4a4a4a}@bottom-left{content:"riprap.nyc";font:9pt IBM Plex Mono;color:#4a4a4a}}.print-citations.svelte-uialbm{page-break-before:always}}
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.register-grid.svelte-1q8jizq{display:grid;gap:16px;grid-template-columns:1fr}@media(min-width:1100px){.register-grid.svelte-1q8jizq{grid-template-columns:1fr 1fr}}.plan-details.svelte-1q8jizq{border:1px solid var(--rule-soft);background:var(--paper-deep);margin-bottom:16px}.plan-details.svelte-1q8jizq summary:where(.svelte-1q8jizq){padding:10px 14px;cursor:pointer;font-family:var(--font-mono);font-size:12px;color:var(--ink-secondary)}.plan-stream.svelte-1q8jizq{font-family:var(--font-mono);font-size:11px;color:var(--ink-tertiary);white-space:pre-wrap;padding:0 14px 12px;margin:0;max-height:240px;overflow:auto}.generating-status.svelte-1q8jizq{display:flex;align-items:center;gap:12px;padding:12px 0;font-family:var(--font-mono);font-size:13px;color:var(--ink-secondary);flex-wrap:wrap}.pulse.svelte-1q8jizq{width:8px;height:8px;border-radius:50%;background:var(--accent-graphical);animation:svelte-1q8jizq-pulse 1.4s ease-in-out infinite}@keyframes svelte-1q8jizq-pulse{0%,to{opacity:.3;transform:scale(.85)}50%{opacity:1;transform:scale(1.1)}}@media(prefers-reduced-motion:reduce){.pulse.svelte-1q8jizq{animation:none;opacity:.7}}
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.tier-badge.svelte-1acpjpp{display:inline-flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;font-weight:500}
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.citation-drawer.svelte-1p339fd a{color:inherit;border-bottom:1px solid var(--rule-soft);text-decoration:none}.citation-drawer.svelte-1p339fd a:hover{border-bottom-color:var(--accent);color:var(--accent)}.rip-map-container.svelte-wk2bu4{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%}.map-frame.svelte-wk2bu4{aspect-ratio:8 / 5.6;position:relative}
|
|
Binary file (8.36 kB). View file
|
|
|
|
Binary file (7.2 kB). View file
|
|
|
|
Binary file (7.21 kB). View file
|
|
|
|
Binary file (8.46 kB). View file
|
|
|
|
Binary file (9.35 kB). View file
|
|
|
|
Binary file (7.27 kB). View file
|
|
|
|
Binary file (5.82 kB). View file
|
|
|
|
Binary file (6.91 kB). View file
|
|
|
|
Binary file (5.77 kB). View file
|
|
|
|
Binary file (6.97 kB). View file
|
|
|
|
Binary file (5.79 kB). View file
|
|
|
|
Binary file (7.69 kB). View file
|
|
|
|
Binary file (13.1 kB). View file
|
|
|
|
Binary file (14.7 kB). View file
|
|
|
|
Binary file (13.2 kB). View file
|
|
|
|
Binary file (14.9 kB). View file
|
|
|
|
Binary file (15.6 kB). View file
|
|
|
|
Binary file (13.2 kB). View file
|
|
|
|
Binary file (13.3 kB). View file
|
|
|