Spaces:
Sleeping
Sleeping
Commit
·
0e11366
1
Parent(s):
d3bceca
First commit
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +9 -0
- .gitignore +16 -0
- Dockerfile +37 -0
- backend/app/__init__.py +0 -0
- backend/app/agents/classifier.py +53 -0
- backend/app/agents/graph.py +81 -0
- backend/app/agents/tools.py +40 -0
- backend/app/config/settings.py +38 -0
- backend/app/data/db.py +7 -0
- backend/app/data/geo.py +13 -0
- backend/app/data/store.py +69 -0
- backend/app/main.py +33 -0
- backend/app/routers/chat.py +29 -0
- backend/app/routers/feeds.py +40 -0
- backend/app/routers/reports.py +12 -0
- backend/app/routers/uploads.py +25 -0
- backend/app/services/chat_agent.py +26 -0
- backend/app/services/feeds.py +241 -0
- backend/app/services/fetchers.py +154 -0
- backend/app/services/reports.py +9 -0
- backend/app/types/models.py +27 -0
- pyproject.toml +19 -0
- requirements.txt +14 -0
- web/.gitignore +24 -0
- web/README.md +69 -0
- web/eslint.config.js +23 -0
- web/index.html +12 -0
- web/package-lock.json +1731 -0
- web/package.json +27 -0
- web/public/icons/3d/3d-alert.png +0 -0
- web/public/icons/3d/3d-ambulance.png +0 -0
- web/public/icons/3d/3d-car.png +0 -0
- web/public/icons/3d/3d-construction.png +0 -0
- web/public/icons/3d/3d-flood.png +0 -0
- web/public/icons/3d/3d-gun.png +0 -0
- web/public/icons/3d/3d-help.png +0 -0
- web/public/icons/3d/3d-info.png +0 -0
- web/public/icons/3d/3d-ride.png +0 -0
- web/public/icons/3d/3d-robbery.png +0 -0
- web/public/icons/3d/3d-search.png +0 -0
- web/public/icons/3d/3d-sex.png +0 -0
- web/public/icons/3d/3d-traffic.png +0 -0
- web/public/icons/3d/3d-user_search.png +0 -0
- web/public/vite.svg +1 -0
- web/src/App.css +48 -0
- web/src/App.tsx +159 -0
- web/src/assets/react.svg +1 -0
- web/src/components/ReportIcon.tsx +34 -0
- web/src/components/chat/ChatPanel.tsx +110 -0
- web/src/components/chat/TypingDots.tsx +10 -0
.dockerignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
venv
|
| 3 |
+
.venv
|
| 4 |
+
__pycache__/
|
| 5 |
+
*.pyc
|
| 6 |
+
node_modules/
|
| 7 |
+
web/node_modules/
|
| 8 |
+
web/.vite/
|
| 9 |
+
data/
|
.gitignore
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
|
| 5 |
+
# Env
|
| 6 |
+
.env
|
| 7 |
+
venv/
|
| 8 |
+
.venv/
|
| 9 |
+
|
| 10 |
+
# Data
|
| 11 |
+
data/**
|
| 12 |
+
!data/.gitkeep
|
| 13 |
+
|
| 14 |
+
# Node
|
| 15 |
+
web/node_modules/
|
| 16 |
+
web/dist/
|
Dockerfile
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ---------- Stage 1: build the React app ----------
|
| 2 |
+
FROM node:20-alpine AS webbuilder
|
| 3 |
+
WORKDIR /web
|
| 4 |
+
COPY web/package*.json ./
|
| 5 |
+
RUN npm ci
|
| 6 |
+
COPY web/ .
|
| 7 |
+
RUN npm run build
|
| 8 |
+
|
| 9 |
+
# ---------- Stage 2: Python runtime ----------
|
| 10 |
+
FROM python:3.11-slim
|
| 11 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 12 |
+
PIP_NO_CACHE_DIR=1 \
|
| 13 |
+
PORT=7860 \
|
| 14 |
+
DATA_DIR=/data
|
| 15 |
+
WORKDIR /app
|
| 16 |
+
|
| 17 |
+
# (optional) if you hit build issues with some libs
|
| 18 |
+
RUN apt-get update && apt-get install -y --no-install-recommends build-essential && rm -rf /var/lib/apt/lists/*
|
| 19 |
+
|
| 20 |
+
# Copy backend and install deps
|
| 21 |
+
COPY backend ./backend
|
| 22 |
+
# Use a simple requirements file for predictability
|
| 23 |
+
COPY requirements.txt ./
|
| 24 |
+
RUN pip install --upgrade pip && pip install -r requirements.txt
|
| 25 |
+
|
| 26 |
+
# Copy built frontend into /app/web/dist so FastAPI can serve it
|
| 27 |
+
COPY --from=webbuilder /web/dist ./web/dist
|
| 28 |
+
|
| 29 |
+
# Prepare data dir for sqlite + uploads
|
| 30 |
+
RUN mkdir -p ${DATA_DIR}/uploads
|
| 31 |
+
VOLUME ["/data"]
|
| 32 |
+
|
| 33 |
+
# Spaces require a single port—expose default; they’ll pass $PORT
|
| 34 |
+
EXPOSE 7860
|
| 35 |
+
|
| 36 |
+
# Start FastAPI bound to Spaces' $PORT
|
| 37 |
+
CMD ["bash","-lc","uvicorn backend.app.main:app --host 0.0.0.0 --port ${PORT}"]
|
backend/app/__init__.py
ADDED
|
File without changes
|
backend/app/agents/classifier.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# same content as your current classifier.py, but model name from settings
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
from typing import Optional
|
| 4 |
+
from pydantic import BaseModel, Field
|
| 5 |
+
from langchain_openai import ChatOpenAI
|
| 6 |
+
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
|
| 7 |
+
from ..config.settings import settings
|
| 8 |
+
|
| 9 |
+
class ReportClassification(BaseModel):
|
| 10 |
+
category: str = Field(..., description="taxonomy id like 'crime.gunshot'")
|
| 11 |
+
label: str = Field(..., description="short human title")
|
| 12 |
+
description: Optional[str] = Field(None, description="one sentence, no emojis")
|
| 13 |
+
severity: Optional[str] = None
|
| 14 |
+
confidence: float = Field(..., ge=0, le=1)
|
| 15 |
+
|
| 16 |
+
CATEGORY_TO_ICON = {
|
| 17 |
+
"crime.gunshot": "3d-gun",
|
| 18 |
+
"crime.robbery": "3d-robbery",
|
| 19 |
+
"crime.sex_offender": "3d-sex",
|
| 20 |
+
"crime.suspicious": "3d-alert",
|
| 21 |
+
"incident.missing_person": "3d-user_search",
|
| 22 |
+
"incident.lost_item": "3d-search",
|
| 23 |
+
"incident.medical": "3d-ambulance",
|
| 24 |
+
"incident.car_accident": "3d-car",
|
| 25 |
+
"road.flood": "3d-flood",
|
| 26 |
+
"road.blocked": "3d-traffic",
|
| 27 |
+
"road.construction": "3d-construction",
|
| 28 |
+
"help.general": "3d-help",
|
| 29 |
+
"help.ride": "3d-ride",
|
| 30 |
+
"other.unknown": "3d-info",
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
SYSTEM = ("You classify short community reports into a strict taxonomy. "
|
| 34 |
+
"Return ONLY the schema fields. If unclear, choose other.unknown.")
|
| 35 |
+
|
| 36 |
+
EXAMPLES = [
|
| 37 |
+
{"input": "I heard gunshots near 5th and Pine!",
|
| 38 |
+
"output_json": '{"category":"crime.gunshot","label":"Gunshots reported","description":"Multiple shots heard near 5th and Pine.","severity":"high","confidence":0.9}'},
|
| 39 |
+
{"input": "Car crash blocking the left lane on I-66",
|
| 40 |
+
"output_json": '{"category":"incident.car_accident","label":"Car accident","description":"Crash reported blocking the left lane on I-66.","severity":"medium","confidence":0.85}'},
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
example_block = ChatPromptTemplate.from_messages([("human", "{input}"), ("ai", "{output_json}")])
|
| 44 |
+
prompt = ChatPromptTemplate.from_messages([
|
| 45 |
+
("system", SYSTEM),
|
| 46 |
+
FewShotChatMessagePromptTemplate(example_prompt=example_block, examples=EXAMPLES),
|
| 47 |
+
("human", "{text}"),
|
| 48 |
+
])
|
| 49 |
+
|
| 50 |
+
_model = ChatOpenAI(model=settings.OPENAI_MODEL_CLASSIFIER, temperature=0).with_structured_output(ReportClassification)
|
| 51 |
+
|
| 52 |
+
def classify_report_text(text: str) -> ReportClassification:
|
| 53 |
+
return (prompt | _model).invoke({"text": text})
|
backend/app/agents/graph.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from typing import Annotated, Dict, List, Optional, TypedDict
|
| 3 |
+
from langgraph.graph import StateGraph, START, END
|
| 4 |
+
from langgraph.prebuilt import ToolNode
|
| 5 |
+
from langgraph.graph.message import add_messages
|
| 6 |
+
from langchain_openai import ChatOpenAI
|
| 7 |
+
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage, BaseMessage, ToolMessage
|
| 8 |
+
from langgraph.checkpoint.sqlite.aio import SqliteSaver
|
| 9 |
+
import sqlite3
|
| 10 |
+
|
| 11 |
+
from .tools import TOOLS
|
| 12 |
+
from ..config.settings import settings
|
| 13 |
+
|
| 14 |
+
SYSTEM_PROMPT = """
|
| 15 |
+
You are PulseMap Agent — a calm, friendly assistant inside a live community map.
|
| 16 |
+
You help people add reports and discover what’s happening around them.
|
| 17 |
+
|
| 18 |
+
### What to do
|
| 19 |
+
- If the user reports an incident (e.g. "flooded underpass here"), call `add_report(lat, lon, text, photo_url?)`.
|
| 20 |
+
- If the user asks about nearby updates (e.g. "what’s near me?", "any reports here?"), call `find_reports_near(lat, lon, radius_km=?, limit=?)`.
|
| 21 |
+
• Default radius = 25 miles (~40 km). Default limit = 10.
|
| 22 |
+
- If no coordinates in the message but `user_location` is provided, use that.
|
| 23 |
+
- If a photo URL is available, pass it through.
|
| 24 |
+
|
| 25 |
+
### How to answer
|
| 26 |
+
- Speak like a helpful neighbor, not a robot.
|
| 27 |
+
- Use plain text only. No **bold**, no numbered lists, no markdown tables.
|
| 28 |
+
- After a tool call, start with a quick recap then list items newest first using hyphen bullets.
|
| 29 |
+
*“I checked within 25 miles of your location and found 3 updates.”*
|
| 30 |
+
For each item, one line like:
|
| 31 |
+
- 🔫 Gunshot — Severity: High; Confidence: 0.9; Time: 2h ago; Source: User; Photo: yes
|
| 32 |
+
- If nothing found:
|
| 33 |
+
- “I didn’t find anything within 25 miles in the last 48 hours. Want me to widen the search?”
|
| 34 |
+
|
| 35 |
+
### Safety
|
| 36 |
+
- Keep a supportive tone. Do not dramatize.
|
| 37 |
+
- End with situational advice when it makes sense (e.g. “Avoid driving through floodwater”).
|
| 38 |
+
- Only mention calling 911 if the report itself clearly describes an urgent danger.
|
| 39 |
+
- Never invent reports — summarize only what tools/feed data provide.
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
# Long-lived sessions DB (same filename as before)
|
| 43 |
+
conn = sqlite3.connect(str(settings.SESSIONS_DB), check_same_thread=False)
|
| 44 |
+
|
| 45 |
+
model = ChatOpenAI(
|
| 46 |
+
model=settings.OPENAI_MODEL_AGENT,
|
| 47 |
+
temperature=0.2,
|
| 48 |
+
openai_api_key=settings.OPENAI_API_KEY,
|
| 49 |
+
streaming=True,
|
| 50 |
+
).bind_tools(TOOLS)
|
| 51 |
+
|
| 52 |
+
class AgentState(TypedDict):
|
| 53 |
+
messages: Annotated[List[BaseMessage], add_messages]
|
| 54 |
+
user_location: Optional[Dict[str, float]]
|
| 55 |
+
photo_url: Optional[str]
|
| 56 |
+
|
| 57 |
+
def model_call(state: AgentState, config=None) -> AgentState:
|
| 58 |
+
loc = state.get("user_location")
|
| 59 |
+
loc_hint = f"User location (fallback): lat={loc['lat']}, lon={loc['lon']}" if (loc and 'lat' in loc and 'lon' in loc) else "User location: unknown"
|
| 60 |
+
photo = state.get("photo_url") or ""
|
| 61 |
+
photo_hint = f"Photo URL available: {photo}" if photo else "No photo URL in context."
|
| 62 |
+
system = SystemMessage(content=SYSTEM_PROMPT + "\n" + loc_hint + "\n" + photo_hint + "\nOnly call another tool if the user asks for more.")
|
| 63 |
+
msgs = [system, *state["messages"]]
|
| 64 |
+
ai_msg: AIMessage = model.invoke(msgs)
|
| 65 |
+
return {"messages": [ai_msg]}
|
| 66 |
+
|
| 67 |
+
def should_continue(state: AgentState) -> str:
|
| 68 |
+
last = state["messages"][-1]
|
| 69 |
+
if getattr(last, "tool_calls", None):
|
| 70 |
+
return "continue"
|
| 71 |
+
return "end"
|
| 72 |
+
|
| 73 |
+
graph = StateGraph(AgentState)
|
| 74 |
+
graph.add_node("agent", model_call)
|
| 75 |
+
graph.add_node("tools", ToolNode(tools=TOOLS))
|
| 76 |
+
graph.add_edge(START, "agent")
|
| 77 |
+
graph.add_conditional_edges("agent", should_continue, {"continue": "tools", "end": END})
|
| 78 |
+
graph.add_edge("tools", "agent")
|
| 79 |
+
|
| 80 |
+
checkpointer = SqliteSaver(conn)
|
| 81 |
+
APP = graph.compile(checkpointer=checkpointer)
|
backend/app/agents/tools.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from datetime import datetime, timezone
|
| 3 |
+
from typing import Optional
|
| 4 |
+
from langchain.tools import tool
|
| 5 |
+
from .classifier import classify_report_text, CATEGORY_TO_ICON
|
| 6 |
+
from ..services.reports import add_report, find_reports_near
|
| 7 |
+
|
| 8 |
+
@tool("add_report")
|
| 9 |
+
def add_report_tool(lat: float, lon: float, text: str = "User report", photo_url: Optional[str] = None) -> str:
|
| 10 |
+
"""
|
| 11 |
+
Add a user report as a map point (GeoJSON Feature).
|
| 12 |
+
Returns a JSON string: {"ok": true, "feature": ...}
|
| 13 |
+
"""
|
| 14 |
+
cls = classify_report_text(text or "User report")
|
| 15 |
+
icon_name = CATEGORY_TO_ICON.get(cls.category, "3d-info")
|
| 16 |
+
props = {
|
| 17 |
+
"title": cls.label,
|
| 18 |
+
"text": cls.description or (text.strip() if text else "User report"),
|
| 19 |
+
"category": cls.category,
|
| 20 |
+
"emoji": icon_name,
|
| 21 |
+
"severity": cls.severity,
|
| 22 |
+
"confidence": cls.confidence,
|
| 23 |
+
"source": "user",
|
| 24 |
+
"reported_at": datetime.now(timezone.utc).isoformat(),
|
| 25 |
+
}
|
| 26 |
+
if photo_url:
|
| 27 |
+
props["photo_url"] = photo_url
|
| 28 |
+
feat = add_report(float(lat), float(lon), text or cls.label, props=props)
|
| 29 |
+
return json.dumps({"ok": True, "feature": feat})
|
| 30 |
+
|
| 31 |
+
@tool("find_reports_near")
|
| 32 |
+
def find_reports_near_tool(lat: float, lon: float, radius_km: float = 10.0, limit: int = 20) -> str:
|
| 33 |
+
"""
|
| 34 |
+
Find user reports near a location.
|
| 35 |
+
Returns a JSON string: {"ok": true, "count": N, "results": [Feature,...]}
|
| 36 |
+
"""
|
| 37 |
+
res = find_reports_near(float(lat), float(lon), float(radius_km), int(limit))
|
| 38 |
+
return json.dumps({"ok": True, "count": len(res), "results": res})
|
| 39 |
+
|
| 40 |
+
TOOLS = [add_report_tool, find_reports_near_tool]
|
backend/app/config/settings.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from pydantic import Field
|
| 4 |
+
|
| 5 |
+
class Settings(BaseSettings):
|
| 6 |
+
|
| 7 |
+
model_config = SettingsConfigDict(
|
| 8 |
+
env_file=".env",
|
| 9 |
+
extra="ignore",
|
| 10 |
+
case_sensitive=False,
|
| 11 |
+
populate_by_name=True,
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
FRONTEND_DIST: Path = Path("web") / "dist"
|
| 15 |
+
|
| 16 |
+
# Models
|
| 17 |
+
OPENAI_API_KEY: str | None = None
|
| 18 |
+
OPENAI_MODEL_AGENT: str = "gpt-4o"
|
| 19 |
+
OPENAI_MODEL_CLASSIFIER: str = "gpt-4o-mini"
|
| 20 |
+
|
| 21 |
+
# Data paths
|
| 22 |
+
DATA_DIR: Path = Path("data")
|
| 23 |
+
REPORTS_DB: Path = DATA_DIR / "pulsemaps_reports.db"
|
| 24 |
+
SESSIONS_DB: Path = DATA_DIR / "pulsemap_sessions.db"
|
| 25 |
+
UPLOADS_DIR: Path = DATA_DIR / "uploads"
|
| 26 |
+
|
| 27 |
+
# Defaults
|
| 28 |
+
DEFAULT_RADIUS_KM: float = 40.0 # ~25 miles
|
| 29 |
+
DEFAULT_LIMIT: int = 10
|
| 30 |
+
MAX_AGE_HOURS: int = 48
|
| 31 |
+
|
| 32 |
+
firms_map_key: str | None = Field(default=None, alias="FIRMS_MAP_KEY")
|
| 33 |
+
gdacs_rss_url: str | None = Field(default="https://www.gdacs.org/xml/rss.xml", alias="GDACS_RSS_URL")
|
| 34 |
+
nvidia_api_key: str | None = Field(default=None, alias="NVIDIA_API_KEY")
|
| 35 |
+
|
| 36 |
+
settings = Settings()
|
| 37 |
+
settings.DATA_DIR.mkdir(exist_ok=True)
|
| 38 |
+
settings.UPLOADS_DIR.mkdir(parents=True, exist_ok=True)
|
backend/app/data/db.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
from . import store # ensure tables are created on import (store does CREATE TABLE)
|
| 3 |
+
|
| 4 |
+
def get_reports_conn() -> sqlite3.Connection:
|
| 5 |
+
# store.py already keeps a module-level connection; this is a placeholder
|
| 6 |
+
from .store import _CONN as REPORTS_CONN # type: ignore
|
| 7 |
+
return REPORTS_CONN
|
backend/app/data/geo.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from math import radians, sin, cos, asin, sqrt
|
| 2 |
+
from typing import Tuple
|
| 3 |
+
|
| 4 |
+
def haversine_km(a: Tuple[float, float], b: Tuple[float, float]) -> float:
|
| 5 |
+
"""Distance in km between (lat,lon) points a, b."""
|
| 6 |
+
lat1, lon1 = a
|
| 7 |
+
lat2, lon2 = b
|
| 8 |
+
R = 6371.0
|
| 9 |
+
dlat = radians(lat2 - lat1)
|
| 10 |
+
dlon = radians(lon2 - lon1)
|
| 11 |
+
lat1r, lat2r = radians(lat1), radians(lat2)
|
| 12 |
+
h = sin(dlat/2)**2 + cos(lat1r)*cos(lat2r)*sin(dlon/2)**2
|
| 13 |
+
return 2 * R * asin(sqrt(h))
|
backend/app/data/store.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# same content as your current store.py, just moved here
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
import json, sqlite3
|
| 4 |
+
from datetime import datetime, timezone, timedelta
|
| 5 |
+
from typing import Dict, Any, List, Optional
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from ..data.geo import haversine_km
|
| 8 |
+
|
| 9 |
+
Path("data").mkdir(exist_ok=True)
|
| 10 |
+
_CONN = sqlite3.connect("data/pulsemaps_reports.db", check_same_thread=False)
|
| 11 |
+
_CONN.execute("""
|
| 12 |
+
CREATE TABLE IF NOT EXISTS reports (
|
| 13 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 14 |
+
lat REAL NOT NULL,
|
| 15 |
+
lon REAL NOT NULL,
|
| 16 |
+
text TEXT NOT NULL,
|
| 17 |
+
props_json TEXT,
|
| 18 |
+
created_at TEXT NOT NULL
|
| 19 |
+
)
|
| 20 |
+
""")
|
| 21 |
+
_CONN.commit()
|
| 22 |
+
|
| 23 |
+
def _row_to_feature(row: tuple) -> Dict[str, Any]:
|
| 24 |
+
_id, lat, lon, text, props_json, created_at = row
|
| 25 |
+
props = {"type": "user_report", "text": text, "reported_at": created_at}
|
| 26 |
+
if props_json:
|
| 27 |
+
try: props.update(json.loads(props_json))
|
| 28 |
+
except Exception: props["raw_props"] = props_json
|
| 29 |
+
return {"type": "Feature", "geometry": {"type": "Point", "coordinates": [lon, lat]}, "properties": props}
|
| 30 |
+
|
| 31 |
+
def add_report(lat: float, lon: float, text: str = "User report", props: dict | None = None):
|
| 32 |
+
created_at = datetime.now(timezone.utc).isoformat()
|
| 33 |
+
props_json = json.dumps(props or {})
|
| 34 |
+
_CONN.execute("INSERT INTO reports (lat, lon, text, props_json, created_at) VALUES (?,?,?,?,?)",
|
| 35 |
+
(float(lat), float(lon), text, props_json, created_at))
|
| 36 |
+
_CONN.commit()
|
| 37 |
+
return {"type": "Feature", "geometry": {"type": "Point", "coordinates": [float(lon), float(lat)]},
|
| 38 |
+
"properties": {"type": "user_report", "text": text, "reported_at": created_at, **(props or {})}}
|
| 39 |
+
|
| 40 |
+
def get_feature_collection() -> Dict[str, Any]:
|
| 41 |
+
cur = _CONN.execute("SELECT id, lat, lon, text, props_json, created_at FROM reports ORDER BY id DESC")
|
| 42 |
+
feats = [_row_to_feature(r) for r in cur.fetchall()]
|
| 43 |
+
return {"type": "FeatureCollection", "features": feats}
|
| 44 |
+
|
| 45 |
+
def find_reports_near(lat: float, lon: float, radius_km: float = 10.0, limit: int = 20, max_age_hours: Optional[int] = None) -> List[Dict[str, Any]]:
|
| 46 |
+
params: list[Any] = []
|
| 47 |
+
sql = "SELECT id, lat, lon, text, props_json, created_at FROM reports"
|
| 48 |
+
if max_age_hours is not None:
|
| 49 |
+
cutoff = datetime.now(timezone.utc) - timedelta(hours=int(max_age_hours))
|
| 50 |
+
sql += " WHERE datetime(created_at) >= datetime(?)"
|
| 51 |
+
params.append(cutoff.isoformat())
|
| 52 |
+
sql += " ORDER BY id DESC LIMIT 2000"
|
| 53 |
+
cur = _CONN.execute(sql, params)
|
| 54 |
+
|
| 55 |
+
center = (lat, lon)
|
| 56 |
+
cand = []
|
| 57 |
+
for r in cur.fetchall():
|
| 58 |
+
_, lat2, lon2, *_ = r
|
| 59 |
+
d = haversine_km(center, (lat2, lon2))
|
| 60 |
+
if d <= radius_km:
|
| 61 |
+
cand.append((d, r))
|
| 62 |
+
cand.sort(key=lambda x: x[0])
|
| 63 |
+
out = [_row_to_feature(r) for _, r in cand[:max(1, limit)]]
|
| 64 |
+
return out
|
| 65 |
+
|
| 66 |
+
def clear_reports() -> dict[str, any]:
|
| 67 |
+
_CONN.execute("DELETE FROM reports")
|
| 68 |
+
_CONN.commit()
|
| 69 |
+
return {"ok": True, "message": "All reports cleared."}
|
backend/app/main.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
from .config.settings import settings
|
| 7 |
+
|
| 8 |
+
app = FastAPI(title="PulseMap Agent – API", version="0.2.0")
|
| 9 |
+
|
| 10 |
+
app.add_middleware(
|
| 11 |
+
CORSMiddleware,
|
| 12 |
+
allow_origins=["*"], allow_methods=["*"], allow_headers=["*"],
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
# Static uploads
|
| 16 |
+
app.mount("/uploads", StaticFiles(directory=str(settings.UPLOADS_DIR)), name="uploads")
|
| 17 |
+
|
| 18 |
+
# Routers
|
| 19 |
+
from .routers import chat, reports, feeds, uploads # noqa
|
| 20 |
+
from .routers.feeds import updates as updates_router
|
| 21 |
+
app.include_router(chat.router)
|
| 22 |
+
app.include_router(reports.router)
|
| 23 |
+
app.include_router(feeds.router)
|
| 24 |
+
app.include_router(updates_router)
|
| 25 |
+
app.include_router(uploads.router)
|
| 26 |
+
|
| 27 |
+
if settings.FRONTEND_DIST.exists():
|
| 28 |
+
app.mount("/", StaticFiles(directory=str(settings.FRONTEND_DIST), html=True), name="spa")
|
| 29 |
+
|
| 30 |
+
@app.get("/health")
|
| 31 |
+
def health():
|
| 32 |
+
from datetime import datetime, timezone
|
| 33 |
+
return {"ok": True, "time": datetime.now(timezone.utc).isoformat()}
|
backend/app/routers/chat.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Body
|
| 2 |
+
from typing import Dict, Any, Optional
|
| 3 |
+
|
| 4 |
+
from ..services.chat_agent import run_chat
|
| 5 |
+
|
| 6 |
+
router = APIRouter(prefix="/chat", tags=["chat"])
|
| 7 |
+
|
| 8 |
+
@router.post("")
|
| 9 |
+
def chat(payload: Dict[str, Any] = Body(...)):
|
| 10 |
+
"""
|
| 11 |
+
Body: { "message": str, "user_location": {lat,lon}?, "session_id"?: str, "photo_url"?: str }
|
| 12 |
+
"""
|
| 13 |
+
msg = payload.get("message", "")
|
| 14 |
+
if not isinstance(msg, str) or not msg.strip():
|
| 15 |
+
return {"reply": "Please type something.", "tool_used": None}
|
| 16 |
+
return run_chat(
|
| 17 |
+
message=msg.strip(),
|
| 18 |
+
user_location=payload.get("user_location"),
|
| 19 |
+
session_id=payload.get("session_id"),
|
| 20 |
+
photo_url=payload.get("photo_url"),
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
@router.post("/reset")
|
| 24 |
+
def reset_chat(payload: Dict[str, Any] = Body(...)):
|
| 25 |
+
sid = payload.get("session_id")
|
| 26 |
+
if not sid:
|
| 27 |
+
return {"ok": False, "error": "session_id required"}
|
| 28 |
+
# Same guidance as before—client can rotate session_id for SqliteSaver threads.
|
| 29 |
+
return {"ok": True}
|
backend/app/routers/feeds.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException
|
| 2 |
+
from typing import Any, Dict, Optional
|
| 3 |
+
from ..services.feeds import (
|
| 4 |
+
fetch_usgs_quakes_geojson, fetch_nws_alerts_geojson,
|
| 5 |
+
eonet_geojson_points, firms_geojson_points, # <-- use normalized point outputs
|
| 6 |
+
local_updates as _local_updates, global_updates as _global_updates
|
| 7 |
+
)
|
| 8 |
+
|
| 9 |
+
router = APIRouter(prefix="/feeds", tags=["feeds"])
|
| 10 |
+
|
| 11 |
+
@router.get("/usgs")
|
| 12 |
+
async def usgs():
|
| 13 |
+
return {"data": await fetch_usgs_quakes_geojson()}
|
| 14 |
+
|
| 15 |
+
@router.get("/nws")
|
| 16 |
+
async def nws():
|
| 17 |
+
return {"data": await fetch_nws_alerts_geojson()}
|
| 18 |
+
|
| 19 |
+
@router.get("/eonet")
|
| 20 |
+
async def eonet():
|
| 21 |
+
return {"data": await eonet_geojson_points()}
|
| 22 |
+
|
| 23 |
+
@router.get("/firms")
|
| 24 |
+
async def firms():
|
| 25 |
+
# Return pointified features for map markers
|
| 26 |
+
return {"data": await firms_geojson_points()}
|
| 27 |
+
|
| 28 |
+
# Convenience endpoints parallel to your previous design
|
| 29 |
+
updates = APIRouter(prefix="/updates", tags=["updates"])
|
| 30 |
+
|
| 31 |
+
@updates.get("/local")
|
| 32 |
+
async def local_updates(lat: float, lon: float, radius_miles: float = 25.0,
|
| 33 |
+
max_age_hours: int = 48, limit: int = 100):
|
| 34 |
+
return await _local_updates(lat, lon, radius_miles, max_age_hours, limit)
|
| 35 |
+
|
| 36 |
+
@updates.get("/global")
|
| 37 |
+
async def global_updates(limit: int = 200, max_age_hours: Optional[int] = None):
|
| 38 |
+
return await _global_updates(limit, max_age_hours)
|
| 39 |
+
|
| 40 |
+
router.include_router(updates)
|
backend/app/routers/reports.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from ..data.store import get_feature_collection, clear_reports
|
| 3 |
+
|
| 4 |
+
router = APIRouter(prefix="/reports", tags=["reports"])
|
| 5 |
+
|
| 6 |
+
@router.get("")
|
| 7 |
+
def reports():
|
| 8 |
+
return get_feature_collection()
|
| 9 |
+
|
| 10 |
+
@router.post("/clear")
|
| 11 |
+
def clear_reports_api():
|
| 12 |
+
return clear_reports()
|
backend/app/routers/uploads.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, UploadFile, File, Request, HTTPException
|
| 2 |
+
import os
|
| 3 |
+
from uuid import uuid4
|
| 4 |
+
from ..config.settings import settings
|
| 5 |
+
|
| 6 |
+
router = APIRouter(prefix="/upload", tags=["uploads"])
|
| 7 |
+
|
| 8 |
+
@router.post("/photo")
|
| 9 |
+
async def upload_photo(request: Request, file: UploadFile = File(...)):
|
| 10 |
+
if not file.content_type or not file.content_type.startswith("image/"):
|
| 11 |
+
raise HTTPException(status_code=400, detail="Only image files are allowed.")
|
| 12 |
+
ext = os.path.splitext(file.filename or "")[1].lower() or ".jpg"
|
| 13 |
+
if ext not in [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp"]:
|
| 14 |
+
ext = ".jpg"
|
| 15 |
+
|
| 16 |
+
data = await file.read()
|
| 17 |
+
if len(data) > 5 * 1024 * 1024:
|
| 18 |
+
raise HTTPException(status_code=413, detail="Image too large (max 5MB).")
|
| 19 |
+
|
| 20 |
+
name = f"{uuid4().hex}{ext}"
|
| 21 |
+
(settings.UPLOADS_DIR / name).write_bytes(data)
|
| 22 |
+
|
| 23 |
+
base = str(request.base_url).rstrip("/")
|
| 24 |
+
url = f"{base}/uploads/{name}"
|
| 25 |
+
return {"ok": True, "url": url, "path": f"/uploads/{name}"}
|
backend/app/services/chat_agent.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Any, Optional
|
| 2 |
+
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
|
| 3 |
+
from ..agents.graph import APP
|
| 4 |
+
|
| 5 |
+
def run_chat(message: str,
|
| 6 |
+
user_location: Optional[Dict[str, float]] = None,
|
| 7 |
+
session_id: Optional[str] = None,
|
| 8 |
+
photo_url: Optional[str] = None) -> Dict[str, Any]:
|
| 9 |
+
from uuid import uuid4
|
| 10 |
+
sid = session_id or str(uuid4())
|
| 11 |
+
init = {"messages": [HumanMessage(content=message)], "user_location": user_location, "photo_url": photo_url}
|
| 12 |
+
cfg = {"configurable": {"thread_id": sid}}
|
| 13 |
+
final = APP.invoke(init, config=cfg)
|
| 14 |
+
|
| 15 |
+
reply, tool_used, tool_result = "", None, None
|
| 16 |
+
for m in final["messages"]:
|
| 17 |
+
if isinstance(m, AIMessage):
|
| 18 |
+
reply = m.content or reply
|
| 19 |
+
elif isinstance(m, ToolMessage) and getattr(m, "name", None) in {"add_report", "find_reports_near"}:
|
| 20 |
+
import json
|
| 21 |
+
try:
|
| 22 |
+
tool_used = m.name
|
| 23 |
+
tool_result = json.loads(m.content) if isinstance(m.content, str) else m.content
|
| 24 |
+
except Exception:
|
| 25 |
+
tool_result = {"raw": m.content}
|
| 26 |
+
return {"reply": reply, "tool_used": tool_used, "tool_result": tool_result, "session_id": sid}
|
backend/app/services/feeds.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from datetime import datetime, timezone
|
| 3 |
+
from typing import Any, Dict, Optional, List, Iterable, Tuple
|
| 4 |
+
from dateutil import parser as dtparser
|
| 5 |
+
|
| 6 |
+
from ..data.geo import haversine_km
|
| 7 |
+
from .fetchers import (
|
| 8 |
+
fetch_usgs_quakes_geojson, fetch_nws_alerts_geojson,
|
| 9 |
+
fetch_eonet_events_geojson, fetch_firms_hotspots_geojson
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
def _flatten_lonlats(coords: Any) -> List[Tuple[float, float]]:
|
| 13 |
+
"""Collect (lon, lat) pairs from nested coordinate arrays."""
|
| 14 |
+
out: List[Tuple[float, float]] = []
|
| 15 |
+
if not isinstance(coords, (list, tuple)):
|
| 16 |
+
return out
|
| 17 |
+
if len(coords) >= 2 and isinstance(coords[0], (int, float)) and isinstance(coords[1], (int, float)):
|
| 18 |
+
# Single coordinate pair [lon, lat, ...]
|
| 19 |
+
out.append((float(coords[0]), float(coords[1])))
|
| 20 |
+
else:
|
| 21 |
+
for c in coords:
|
| 22 |
+
out.extend(_flatten_lonlats(c))
|
| 23 |
+
return out
|
| 24 |
+
|
| 25 |
+
def _centroid_from_geom(geom: Dict[str, Any]) -> Optional[Tuple[float, float]]:
|
| 26 |
+
"""Return (lon, lat) for any geometry by taking a simple average of all coords."""
|
| 27 |
+
if not geom or "type" not in geom:
|
| 28 |
+
return None
|
| 29 |
+
gtype = geom.get("type")
|
| 30 |
+
coords = geom.get("coordinates")
|
| 31 |
+
|
| 32 |
+
# Fast path for Point
|
| 33 |
+
if gtype == "Point" and isinstance(coords, (list, tuple)) and len(coords) >= 2:
|
| 34 |
+
return (float(coords[0]), float(coords[1]))
|
| 35 |
+
|
| 36 |
+
# Generic centroid for Polygon/MultiPolygon/LineString/etc.
|
| 37 |
+
pts = _flatten_lonlats(coords)
|
| 38 |
+
if not pts:
|
| 39 |
+
return None
|
| 40 |
+
xs = [p[0] for p in pts]
|
| 41 |
+
ys = [p[1] for p in pts]
|
| 42 |
+
return (sum(xs) / len(xs), sum(ys) / len(ys))
|
| 43 |
+
|
| 44 |
+
def _mk_point_feature(lon: float, lat: float, props: Dict[str, Any]) -> Dict[str, Any]:
|
| 45 |
+
return {
|
| 46 |
+
"type": "Feature",
|
| 47 |
+
"geometry": {"type": "Point", "coordinates": [float(lon), float(lat)]},
|
| 48 |
+
"properties": props or {},
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
def _report_to_update(f: Dict[str, Any]) -> Dict[str, Any]:
|
| 52 |
+
p = f.get("properties", {}) or {}
|
| 53 |
+
lat = f["geometry"]["coordinates"][1]
|
| 54 |
+
lon = f["geometry"]["coordinates"][0]
|
| 55 |
+
return {
|
| 56 |
+
"kind": "report",
|
| 57 |
+
"title": p.get("title") or p.get("text") or "User report",
|
| 58 |
+
"emoji": p.get("emoji") or "📝",
|
| 59 |
+
"time": p.get("reported_at"),
|
| 60 |
+
"lat": float(lat), "lon": float(lon),
|
| 61 |
+
"severity": p.get("severity"),
|
| 62 |
+
"sourceUrl": None,
|
| 63 |
+
"raw": p,
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
def _quake_to_update(f: Dict[str, Any]) -> Dict[str, Any] | None:
|
| 67 |
+
p = f.get("properties", {}) or {}
|
| 68 |
+
g = f.get("geometry", {}) or {}
|
| 69 |
+
if g.get("type") != "Point": return None
|
| 70 |
+
lon, lat = g["coordinates"][:2]
|
| 71 |
+
title = p.get("place") or p.get("title") or "Earthquake"
|
| 72 |
+
mag = p.get("mag") or p.get("Magnitude") or p.get("m")
|
| 73 |
+
ts = p.get("time")
|
| 74 |
+
if isinstance(ts, (int, float)):
|
| 75 |
+
time_iso = datetime.fromtimestamp(ts/1000, tz=timezone.utc).isoformat()
|
| 76 |
+
else:
|
| 77 |
+
time_iso = p.get("updated") if isinstance(p.get("updated"), str) else datetime.now(timezone.utc).isoformat()
|
| 78 |
+
return {"kind": "quake", "title": title, "emoji": "💥", "time": time_iso,
|
| 79 |
+
"lat": float(lat), "lon": float(lon), "severity": f"M{mag}" if mag is not None else None,
|
| 80 |
+
"sourceUrl": p.get("url") or p.get("detail"), "raw": p}
|
| 81 |
+
|
| 82 |
+
def _eonet_to_update(f: Dict[str, Any]) -> Dict[str, Any] | None:
|
| 83 |
+
p = f.get("properties", {}) or {}
|
| 84 |
+
g = f.get("geometry", {}) or {}
|
| 85 |
+
if g.get("type") != "Point": return None
|
| 86 |
+
lon, lat = g["coordinates"][:2]
|
| 87 |
+
title = p.get("title") or p.get("category") or "Event"
|
| 88 |
+
cat = (p.get("category") or (p.get("categories") or [{}])[0].get("title") or "").lower()
|
| 89 |
+
if "wildfire" in cat: emoji = "🔥"
|
| 90 |
+
elif "volcano" in cat: emoji = "🌋"
|
| 91 |
+
elif "earthquake" in cat or "seismic" in cat: emoji = "💥"
|
| 92 |
+
elif any(k in cat for k in ["storm","cyclone","hurricane","typhoon"]): emoji = "🌀"
|
| 93 |
+
elif "flood" in cat: emoji = "🌊"
|
| 94 |
+
elif "landslide" in cat: emoji = "🏔️"
|
| 95 |
+
elif any(k in cat for k in ["ice","snow","blizzard"]): emoji = "❄️"
|
| 96 |
+
elif any(k in cat for k in ["dust","smoke","haze"]): emoji = "🌫️"
|
| 97 |
+
else: emoji = "⚠️"
|
| 98 |
+
time_iso = p.get("time") or p.get("updated") or datetime.now(timezone.utc).isoformat()
|
| 99 |
+
return {"kind": "eonet", "title": title, "emoji": emoji, "time": time_iso,
|
| 100 |
+
"lat": float(lat), "lon": float(lon), "sourceUrl": p.get("link") or p.get("url"), "raw": p}
|
| 101 |
+
|
| 102 |
+
def _firms_to_update(f: Dict[str, Any]) -> Dict[str, Any] | None:
|
| 103 |
+
p = f.get("properties", {}) or {}
|
| 104 |
+
g = f.get("geometry", {}) or {}
|
| 105 |
+
if g.get("type") != "Point": return None
|
| 106 |
+
lon, lat = g["coordinates"][:2]
|
| 107 |
+
time_iso = p.get("acq_datetime") or p.get("acq_date") or datetime.now(timezone.utc).isoformat()
|
| 108 |
+
sev = p.get("confidence") or p.get("brightness") or p.get("frp")
|
| 109 |
+
return {"kind": "fire", "title": "Fire hotspot", "emoji": "🔥", "time": time_iso,
|
| 110 |
+
"lat": float(lat), "lon": float(lon), "severity": sev, "sourceUrl": None, "raw": p}
|
| 111 |
+
|
| 112 |
+
def _within(lat: float, lon: float, u: Dict[str, Any], radius_km: float) -> bool:
|
| 113 |
+
return haversine_km((lat, lon), (u["lat"], u["lon"])) <= radius_km
|
| 114 |
+
|
| 115 |
+
def _is_recent(iso: str | None, max_age_hours: int) -> bool:
|
| 116 |
+
if not iso: return False
|
| 117 |
+
try:
|
| 118 |
+
t = dtparser.isoparse(iso)
|
| 119 |
+
if not t.tzinfo: t = t.replace(tzinfo=timezone.utc)
|
| 120 |
+
except Exception:
|
| 121 |
+
return False
|
| 122 |
+
return (datetime.now(timezone.utc) - t).total_seconds() <= max_age_hours * 3600
|
| 123 |
+
|
| 124 |
+
async def _gather_feeds():
|
| 125 |
+
results = await asyncio.gather(
|
| 126 |
+
fetch_usgs_quakes_geojson(), fetch_nws_alerts_geojson(),
|
| 127 |
+
fetch_eonet_events_geojson(), fetch_firms_hotspots_geojson(),
|
| 128 |
+
return_exceptions=True
|
| 129 |
+
)
|
| 130 |
+
def ok(x): return {"features": []} if isinstance(x, Exception) or not x else x
|
| 131 |
+
return {"usgs": ok(results[0]), "nws": ok(results[1]), "eonet": ok(results[2]), "firms": ok(results[3])}
|
| 132 |
+
|
| 133 |
+
async def local_updates(lat: float, lon: float, radius_miles: float, max_age_hours: int, limit: int):
|
| 134 |
+
from ..data.store import find_reports_near
|
| 135 |
+
km = float(radius_miles) * 1.609344
|
| 136 |
+
near_reports = find_reports_near(lat, lon, radius_km=km, limit=limit, max_age_hours=max_age_hours)
|
| 137 |
+
updates: List[Dict[str, Any]] = [_report_to_update(f) for f in near_reports]
|
| 138 |
+
feeds = await _gather_feeds()
|
| 139 |
+
|
| 140 |
+
for f in (feeds["usgs"].get("features") or []):
|
| 141 |
+
u = _quake_to_update(f)
|
| 142 |
+
if u and _is_recent(u["time"], max_age_hours) and _within(lat, lon, u, km):
|
| 143 |
+
updates.append(u)
|
| 144 |
+
for u in _nws_to_updates(feeds["nws"]):
|
| 145 |
+
if _is_recent(u["time"], max_age_hours) and _within(lat, lon, u, km):
|
| 146 |
+
updates.append(u)
|
| 147 |
+
for f in (feeds["eonet"].get("features") or []):
|
| 148 |
+
u = _eonet_to_update(f)
|
| 149 |
+
if u and _is_recent(u["time"], max_age_hours) and _within(lat, lon, u, km):
|
| 150 |
+
updates.append(u)
|
| 151 |
+
for f in (feeds["firms"].get("features") or []):
|
| 152 |
+
u = _firms_to_update(f)
|
| 153 |
+
if u and _is_recent(u["time"], max_age_hours) and _within(lat, lon, u, km):
|
| 154 |
+
updates.append(u)
|
| 155 |
+
|
| 156 |
+
updates.sort(key=lambda x: x["time"] or "", reverse=True)
|
| 157 |
+
return {"count": min(len(updates), limit), "updates": updates[:limit]}
|
| 158 |
+
|
| 159 |
+
def _nws_to_updates(fc: Dict[str, Any]) -> list[Dict[str, Any]]:
|
| 160 |
+
out: list[Dict[str, Any]] = []
|
| 161 |
+
for f in (fc.get("features") or []):
|
| 162 |
+
p = f.get("properties", {}) or {}
|
| 163 |
+
g = f.get("geometry", {}) or {}
|
| 164 |
+
coords = None
|
| 165 |
+
if g.get("type") == "Polygon":
|
| 166 |
+
poly = g["coordinates"][0]
|
| 167 |
+
if poly:
|
| 168 |
+
lats = [c[1] for c in poly]; lons = [c[0] for c in poly]
|
| 169 |
+
coords = (sum(lats)/len(lats), sum(lons)/len(lons))
|
| 170 |
+
elif g.get("type") == "Point":
|
| 171 |
+
coords = (g["coordinates"][1], g["coordinates"][0])
|
| 172 |
+
if not coords:
|
| 173 |
+
continue
|
| 174 |
+
sev = p.get("severity") or "Unknown"
|
| 175 |
+
issued = p.get("effective") or p.get("onset") or p.get("sent") or datetime.now(timezone.utc).isoformat()
|
| 176 |
+
out.append({"kind": "nws", "title": p.get("event") or "NWS Alert", "emoji": "⚠️",
|
| 177 |
+
"time": issued, "lat": float(coords[0]), "lon": float(coords[1]),
|
| 178 |
+
"severity": sev, "sourceUrl": p.get("@id") or p.get("id"), "raw": p})
|
| 179 |
+
return out
|
| 180 |
+
|
| 181 |
+
async def global_updates(limit: int, max_age_hours: Optional[int]):
|
| 182 |
+
from ..data.store import get_feature_collection
|
| 183 |
+
fc = get_feature_collection()
|
| 184 |
+
reports = fc.get("features") or []
|
| 185 |
+
rep_updates = [_report_to_update(f) for f in reports]
|
| 186 |
+
feeds = await _gather_feeds()
|
| 187 |
+
nws_updates = _nws_to_updates(feeds["nws"])
|
| 188 |
+
quake_updates = [_ for f in (feeds["usgs"].get("features") or []) if (_ := _quake_to_update(f))]
|
| 189 |
+
eonet_updates = [_ for f in (feeds["eonet"].get("features") or []) if (_ := _eonet_to_update(f))]
|
| 190 |
+
firms_updates = [_ for f in (feeds["firms"].get("features") or []) if (_ := _firms_to_update(f))]
|
| 191 |
+
|
| 192 |
+
updates = rep_updates + nws_updates + quake_updates + eonet_updates + firms_updates
|
| 193 |
+
if max_age_hours is not None:
|
| 194 |
+
updates = [u for u in updates if _is_recent(u["time"], max_age_hours)]
|
| 195 |
+
updates.sort(key=lambda x: x["time"] or "", reverse=True)
|
| 196 |
+
return {"count": min(len(updates), limit), "updates": updates[:limit]}
|
| 197 |
+
|
| 198 |
+
async def eonet_geojson_points() -> Dict[str, Any]:
|
| 199 |
+
"""Always return Point features for EONET (polygon events -> centroid)."""
|
| 200 |
+
fc = await fetch_eonet_events_geojson() or {}
|
| 201 |
+
features = []
|
| 202 |
+
for f in (fc.get("features") or []):
|
| 203 |
+
g = f.get("geometry") or {}
|
| 204 |
+
p = f.get("properties") or {}
|
| 205 |
+
cen = _centroid_from_geom(g)
|
| 206 |
+
if not cen:
|
| 207 |
+
continue
|
| 208 |
+
lon, lat = cen
|
| 209 |
+
# Keep a stable, small prop set the map can style
|
| 210 |
+
props = {
|
| 211 |
+
"source": "eonet",
|
| 212 |
+
"title": p.get("title") or p.get("category") or "Event",
|
| 213 |
+
"emoji": "⚠️", # the map can replace based on category if it wants
|
| 214 |
+
"raw": p,
|
| 215 |
+
}
|
| 216 |
+
features.append(_mk_point_feature(lon, lat, props))
|
| 217 |
+
return {"type": "FeatureCollection", "features": features}
|
| 218 |
+
|
| 219 |
+
async def firms_geojson_points() -> Dict[str, Any]:
|
| 220 |
+
"""Always return Point features for FIRMS (skip invalid rows)."""
|
| 221 |
+
fc = await fetch_firms_hotspots_geojson() or {}
|
| 222 |
+
features = []
|
| 223 |
+
for f in (fc.get("features") or []):
|
| 224 |
+
g = f.get("geometry") or {}
|
| 225 |
+
p = f.get("properties") or {}
|
| 226 |
+
cen = _centroid_from_geom(g)
|
| 227 |
+
if not cen:
|
| 228 |
+
# Some rows can be malformed; skip them
|
| 229 |
+
continue
|
| 230 |
+
lon, lat = cen
|
| 231 |
+
props = {
|
| 232 |
+
"source": "firms",
|
| 233 |
+
"title": "Fire hotspot",
|
| 234 |
+
"emoji": "🔥",
|
| 235 |
+
"confidence": p.get("confidence"),
|
| 236 |
+
"brightness": p.get("brightness"),
|
| 237 |
+
"time": p.get("acq_datetime") or p.get("acq_date"),
|
| 238 |
+
"raw": p,
|
| 239 |
+
}
|
| 240 |
+
features.append(_mk_point_feature(lon, lat, props))
|
| 241 |
+
return {"type": "FeatureCollection", "features": features}
|
backend/app/services/fetchers.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import httpx
|
| 2 |
+
import os, io, csv
|
| 3 |
+
import asyncio
|
| 4 |
+
import random
|
| 5 |
+
import httpx
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
# Keep URLs simple & stable; you can lift to config/env later.
|
| 9 |
+
USGS_ALL_HOUR = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson"
|
| 10 |
+
NWS_ALERTS_ACTIVE = "https://api.weather.gov/alerts/active"
|
| 11 |
+
EONET_EVENTS_GEOJSON = "https://eonet.gsfc.nasa.gov/api/v3/events/geojson?status=open&days=7"
|
| 12 |
+
DATASETS = ["VIIRS_NOAA20_NRT", "VIIRS_SNPP_NRT"]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
import httpx
|
| 16 |
+
|
| 17 |
+
def _in_usa(lat: float, lon: float) -> bool:
|
| 18 |
+
# CONUS
|
| 19 |
+
if 24.5 <= lat <= 49.5 and -125.0 <= lon <= -66.0:
|
| 20 |
+
return True
|
| 21 |
+
# Alaska (rough)
|
| 22 |
+
if 51.0 <= lat <= 71.0 and -170.0 <= lon <= -129.0:
|
| 23 |
+
return True
|
| 24 |
+
# Hawaii
|
| 25 |
+
if 18.5 <= lat <= 22.5 and -161.0 <= lon <= -154.0:
|
| 26 |
+
return True
|
| 27 |
+
return False
|
| 28 |
+
|
| 29 |
+
async def fetch_json_once(
|
| 30 |
+
url: str,
|
| 31 |
+
headers: dict,
|
| 32 |
+
*,
|
| 33 |
+
connect_timeout: float = 3,
|
| 34 |
+
read_timeout: float = 12,
|
| 35 |
+
):
|
| 36 |
+
"""
|
| 37 |
+
Single attempt fetch; no retries, no delay.
|
| 38 |
+
"""
|
| 39 |
+
timeout = httpx.Timeout(
|
| 40 |
+
connect=connect_timeout,
|
| 41 |
+
read=read_timeout,
|
| 42 |
+
write=read_timeout,
|
| 43 |
+
pool=connect_timeout,
|
| 44 |
+
)
|
| 45 |
+
async with httpx.AsyncClient(timeout=timeout, follow_redirects=True) as client:
|
| 46 |
+
r = await client.get(url, headers=headers)
|
| 47 |
+
r.raise_for_status()
|
| 48 |
+
return r.json()
|
| 49 |
+
|
| 50 |
+
async def fetch_usgs_quakes_geojson():
|
| 51 |
+
async with httpx.AsyncClient(timeout=10) as client:
|
| 52 |
+
r = await client.get(USGS_ALL_HOUR, headers={"Accept":"application/geo+json"})
|
| 53 |
+
r.raise_for_status()
|
| 54 |
+
return r.json()
|
| 55 |
+
|
| 56 |
+
async def fetch_nws_alerts_geojson():
|
| 57 |
+
async with httpx.AsyncClient(timeout=10) as client:
|
| 58 |
+
r = await client.get(NWS_ALERTS_ACTIVE, headers={"Accept":"application/geo+json"})
|
| 59 |
+
r.raise_for_status()
|
| 60 |
+
return r.json()
|
| 61 |
+
|
| 62 |
+
async def fetch_eonet_events_geojson():
|
| 63 |
+
return await fetch_json_once(
|
| 64 |
+
EONET_EVENTS_GEOJSON,
|
| 65 |
+
headers={"Accept": "application/geo+json"},
|
| 66 |
+
connect_timeout=3,
|
| 67 |
+
read_timeout=12,
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
def _get_num(d: dict, *keys):
|
| 71 |
+
for k in keys:
|
| 72 |
+
if k in d and d[k] not in (None, ""):
|
| 73 |
+
try:
|
| 74 |
+
return float(d[k])
|
| 75 |
+
except Exception:
|
| 76 |
+
pass
|
| 77 |
+
raise KeyError("no numeric value")
|
| 78 |
+
|
| 79 |
+
async def _fetch_firms_csv_rows(key: str, dataset: str, hours: int = 1) -> list[dict]:
|
| 80 |
+
url = f"https://firms.modaps.eosdis.nasa.gov/api/area/csv/{key}/{dataset}/world/{hours}"
|
| 81 |
+
async with httpx.AsyncClient(timeout=20) as client:
|
| 82 |
+
r = await client.get(url, headers={"Accept": "text/csv", "User-Agent": "PulseMap/1.0"})
|
| 83 |
+
text = r.text or ""
|
| 84 |
+
|
| 85 |
+
# Some FIRMS edges return text/plain or octet-stream; parse anyway
|
| 86 |
+
# Strip BOM if present
|
| 87 |
+
if text and text[:1] == "\ufeff":
|
| 88 |
+
text = text[1:]
|
| 89 |
+
|
| 90 |
+
# Try CSV parse
|
| 91 |
+
try:
|
| 92 |
+
reader = csv.DictReader(io.StringIO(text))
|
| 93 |
+
rows = [row for row in reader]
|
| 94 |
+
except Exception:
|
| 95 |
+
rows = []
|
| 96 |
+
|
| 97 |
+
# If we got nothing, surface first 200 chars to the caller for logging
|
| 98 |
+
if not rows:
|
| 99 |
+
return [{"__error__": (text[:200] if text else "empty response")}]
|
| 100 |
+
|
| 101 |
+
return rows
|
| 102 |
+
|
| 103 |
+
async def fetch_firms_hotspots_geojson():
|
| 104 |
+
"""
|
| 105 |
+
NASA FIRMS: returns GeoJSON FeatureCollection (Points).
|
| 106 |
+
Requires env FIRMS_MAP_KEY. Tries NOAA-20 first, then SNPP. World, last 24h (1 day segment).
|
| 107 |
+
"""
|
| 108 |
+
key = "95fa2dac8d20024aa6a17229dbf5ce74"
|
| 109 |
+
if not key:
|
| 110 |
+
return {"type": "FeatureCollection", "features": [], "_note": "Set FIRMS_MAP_KEY to enable."}
|
| 111 |
+
|
| 112 |
+
errors = []
|
| 113 |
+
for dataset in DATASETS:
|
| 114 |
+
rows = await _fetch_firms_csv_rows(key, dataset, hours=1)
|
| 115 |
+
if rows and "__error__" in rows[0]:
|
| 116 |
+
errors.append(f"{dataset}: {rows[0]['__error__']}")
|
| 117 |
+
continue
|
| 118 |
+
|
| 119 |
+
feats = []
|
| 120 |
+
for i, row in enumerate(rows):
|
| 121 |
+
if i >= 1500:
|
| 122 |
+
break
|
| 123 |
+
try:
|
| 124 |
+
lat = _get_num(row, "latitude", "LATITUDE", "lat", "LAT")
|
| 125 |
+
lon = _get_num(row, "longitude", "LONGITUDE", "lon", "LON")
|
| 126 |
+
except Exception:
|
| 127 |
+
continue
|
| 128 |
+
|
| 129 |
+
props = {
|
| 130 |
+
"source": "FIRMS",
|
| 131 |
+
"dataset": dataset,
|
| 132 |
+
"acq_date": row.get("acq_date") or row.get("ACQ_DATE"),
|
| 133 |
+
"acq_time": row.get("acq_time") or row.get("ACQ_TIME"),
|
| 134 |
+
"instrument": row.get("instrument") or row.get("INSTRUMENT"),
|
| 135 |
+
"confidence": row.get("confidence") or row.get("CONFIDENCE"),
|
| 136 |
+
"frp": row.get("frp") or row.get("FRP"),
|
| 137 |
+
"daynight": row.get("daynight") or row.get("DAYNIGHT"),
|
| 138 |
+
}
|
| 139 |
+
feats.append({
|
| 140 |
+
"type": "Feature",
|
| 141 |
+
"geometry": {"type": "Point", "coordinates": [lon, lat]},
|
| 142 |
+
"properties": props,
|
| 143 |
+
})
|
| 144 |
+
|
| 145 |
+
feats = [f for f in feats
|
| 146 |
+
if _in_usa(f["geometry"]["coordinates"][1], f["geometry"]["coordinates"][0])]
|
| 147 |
+
if feats:
|
| 148 |
+
return {"type": "FeatureCollection", "features": feats, "_note": f"{dataset} ok, {len(feats)} points (USA only)"}
|
| 149 |
+
|
| 150 |
+
# Try next dataset if this one returned 0 points
|
| 151 |
+
errors.append(f"{dataset}: 0 rows or no valid coordinates")
|
| 152 |
+
|
| 153 |
+
# If we got here, nothing worked
|
| 154 |
+
return {"type": "FeatureCollection", "features": [], "_note": f"FIRMS empty. Details: {' | '.join(errors[:2])}"}
|
backend/app/services/reports.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Any, List, Optional
|
| 2 |
+
from ..data.store import add_report as _add, find_reports_near as _find
|
| 3 |
+
|
| 4 |
+
def add_report(lat: float, lon: float, text: str, props: dict | None = None) -> Dict[str, Any]:
|
| 5 |
+
return _add(lat, lon, text, props)
|
| 6 |
+
|
| 7 |
+
def find_reports_near(lat: float, lon: float, radius_km: float, limit: int,
|
| 8 |
+
max_age_hours: Optional[int] = None) -> List[Dict[str, Any]]:
|
| 9 |
+
return _find(lat, lon, radius_km, limit, max_age_hours=max_age_hours)
|
backend/app/types/models.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional, List, Any
|
| 3 |
+
|
| 4 |
+
class UserLocation(BaseModel):
|
| 5 |
+
lat: float
|
| 6 |
+
lon: float
|
| 7 |
+
|
| 8 |
+
class ChatRequest(BaseModel):
|
| 9 |
+
message: str
|
| 10 |
+
user_location: Optional[UserLocation] = None
|
| 11 |
+
session_id: Optional[str] = None
|
| 12 |
+
photo_url: Optional[str] = None
|
| 13 |
+
|
| 14 |
+
class Update(BaseModel):
|
| 15 |
+
kind: str
|
| 16 |
+
title: str
|
| 17 |
+
emoji: str
|
| 18 |
+
time: Optional[str]
|
| 19 |
+
lat: float
|
| 20 |
+
lon: float
|
| 21 |
+
severity: Optional[str] = None
|
| 22 |
+
sourceUrl: Optional[str] = None
|
| 23 |
+
raw: Any
|
| 24 |
+
|
| 25 |
+
class UpdatesResponse(BaseModel):
|
| 26 |
+
count: int
|
| 27 |
+
updates: List[Update]
|
pyproject.toml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "pulsemaps-backend"
|
| 3 |
+
version = "0.2.0"
|
| 4 |
+
requires-python = ">=3.10"
|
| 5 |
+
dependencies = [
|
| 6 |
+
"fastapi",
|
| 7 |
+
"uvicorn[standard]",
|
| 8 |
+
"pydantic",
|
| 9 |
+
"pydantic-settings",
|
| 10 |
+
"python-dateutil",
|
| 11 |
+
"httpx",
|
| 12 |
+
"langchain",
|
| 13 |
+
"langchain-openai",
|
| 14 |
+
"langgraph",
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
[tool.ruff]
|
| 18 |
+
line-length = 100
|
| 19 |
+
select = ["E","F","I","UP"]
|
requirements.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.4
|
| 2 |
+
uvicorn[standard]==0.31.0
|
| 3 |
+
pydantic==2.9.2
|
| 4 |
+
pydantic-settings==2.5.2
|
| 5 |
+
python-multipart==0.0.9
|
| 6 |
+
python-dateutil==2.9.0.post0
|
| 7 |
+
httpx==0.27.2
|
| 8 |
+
|
| 9 |
+
# LangChain stack
|
| 10 |
+
langchain==0.2.16
|
| 11 |
+
langchain-openai==0.1.26
|
| 12 |
+
openai==1.40.6
|
| 13 |
+
langgraph==0.2.34
|
| 14 |
+
aiosqlite==0.20.0
|
web/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
web/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# React + TypeScript + Vite
|
| 2 |
+
|
| 3 |
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
| 4 |
+
|
| 5 |
+
Currently, two official plugins are available:
|
| 6 |
+
|
| 7 |
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
| 8 |
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
| 9 |
+
|
| 10 |
+
## Expanding the ESLint configuration
|
| 11 |
+
|
| 12 |
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
| 13 |
+
|
| 14 |
+
```js
|
| 15 |
+
export default tseslint.config([
|
| 16 |
+
globalIgnores(['dist']),
|
| 17 |
+
{
|
| 18 |
+
files: ['**/*.{ts,tsx}'],
|
| 19 |
+
extends: [
|
| 20 |
+
// Other configs...
|
| 21 |
+
|
| 22 |
+
// Remove tseslint.configs.recommended and replace with this
|
| 23 |
+
...tseslint.configs.recommendedTypeChecked,
|
| 24 |
+
// Alternatively, use this for stricter rules
|
| 25 |
+
...tseslint.configs.strictTypeChecked,
|
| 26 |
+
// Optionally, add this for stylistic rules
|
| 27 |
+
...tseslint.configs.stylisticTypeChecked,
|
| 28 |
+
|
| 29 |
+
// Other configs...
|
| 30 |
+
],
|
| 31 |
+
languageOptions: {
|
| 32 |
+
parserOptions: {
|
| 33 |
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
| 34 |
+
tsconfigRootDir: import.meta.dirname,
|
| 35 |
+
},
|
| 36 |
+
// other options...
|
| 37 |
+
},
|
| 38 |
+
},
|
| 39 |
+
])
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
| 43 |
+
|
| 44 |
+
```js
|
| 45 |
+
// eslint.config.js
|
| 46 |
+
import reactX from 'eslint-plugin-react-x'
|
| 47 |
+
import reactDom from 'eslint-plugin-react-dom'
|
| 48 |
+
|
| 49 |
+
export default tseslint.config([
|
| 50 |
+
globalIgnores(['dist']),
|
| 51 |
+
{
|
| 52 |
+
files: ['**/*.{ts,tsx}'],
|
| 53 |
+
extends: [
|
| 54 |
+
// Other configs...
|
| 55 |
+
// Enable lint rules for React
|
| 56 |
+
reactX.configs['recommended-typescript'],
|
| 57 |
+
// Enable lint rules for React DOM
|
| 58 |
+
reactDom.configs.recommended,
|
| 59 |
+
],
|
| 60 |
+
languageOptions: {
|
| 61 |
+
parserOptions: {
|
| 62 |
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
| 63 |
+
tsconfigRootDir: import.meta.dirname,
|
| 64 |
+
},
|
| 65 |
+
// other options...
|
| 66 |
+
},
|
| 67 |
+
},
|
| 68 |
+
])
|
| 69 |
+
```
|
web/eslint.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import tseslint from 'typescript-eslint'
|
| 6 |
+
import { globalIgnores } from 'eslint/config'
|
| 7 |
+
|
| 8 |
+
export default tseslint.config([
|
| 9 |
+
globalIgnores(['dist']),
|
| 10 |
+
{
|
| 11 |
+
files: ['**/*.{ts,tsx}'],
|
| 12 |
+
extends: [
|
| 13 |
+
js.configs.recommended,
|
| 14 |
+
tseslint.configs.recommended,
|
| 15 |
+
reactHooks.configs['recommended-latest'],
|
| 16 |
+
reactRefresh.configs.vite,
|
| 17 |
+
],
|
| 18 |
+
languageOptions: {
|
| 19 |
+
ecmaVersion: 2020,
|
| 20 |
+
globals: globals.browser,
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
])
|
web/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>PulseMap Agent</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<div id="root"></div>
|
| 10 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 11 |
+
</body>
|
| 12 |
+
</html>
|
web/package-lock.json
ADDED
|
@@ -0,0 +1,1731 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "cta-web",
|
| 3 |
+
"version": "0.0.1",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "cta-web",
|
| 9 |
+
"version": "0.0.1",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"@vis.gl/react-google-maps": "^1.5.5",
|
| 12 |
+
"leaflet": "^1.9.4",
|
| 13 |
+
"lucide-react": "^0.542.0",
|
| 14 |
+
"react": "^18.3.1",
|
| 15 |
+
"react-dom": "^18.3.1",
|
| 16 |
+
"react-leaflet": "^4.2.1"
|
| 17 |
+
},
|
| 18 |
+
"devDependencies": {
|
| 19 |
+
"@types/leaflet": "^1.9.20",
|
| 20 |
+
"@types/react": "^18.3.24",
|
| 21 |
+
"@types/react-dom": "^18.3.7",
|
| 22 |
+
"@vitejs/plugin-react": "^5.0.1",
|
| 23 |
+
"typescript": "^5.9.2",
|
| 24 |
+
"vite": "^5.4.19"
|
| 25 |
+
}
|
| 26 |
+
},
|
| 27 |
+
"node_modules/@ampproject/remapping": {
|
| 28 |
+
"version": "2.3.0",
|
| 29 |
+
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
| 30 |
+
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
| 31 |
+
"dev": true,
|
| 32 |
+
"license": "Apache-2.0",
|
| 33 |
+
"dependencies": {
|
| 34 |
+
"@jridgewell/gen-mapping": "^0.3.5",
|
| 35 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 36 |
+
},
|
| 37 |
+
"engines": {
|
| 38 |
+
"node": ">=6.0.0"
|
| 39 |
+
}
|
| 40 |
+
},
|
| 41 |
+
"node_modules/@babel/code-frame": {
|
| 42 |
+
"version": "7.27.1",
|
| 43 |
+
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
| 44 |
+
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
| 45 |
+
"dev": true,
|
| 46 |
+
"license": "MIT",
|
| 47 |
+
"dependencies": {
|
| 48 |
+
"@babel/helper-validator-identifier": "^7.27.1",
|
| 49 |
+
"js-tokens": "^4.0.0",
|
| 50 |
+
"picocolors": "^1.1.1"
|
| 51 |
+
},
|
| 52 |
+
"engines": {
|
| 53 |
+
"node": ">=6.9.0"
|
| 54 |
+
}
|
| 55 |
+
},
|
| 56 |
+
"node_modules/@babel/compat-data": {
|
| 57 |
+
"version": "7.28.0",
|
| 58 |
+
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
|
| 59 |
+
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
|
| 60 |
+
"dev": true,
|
| 61 |
+
"license": "MIT",
|
| 62 |
+
"engines": {
|
| 63 |
+
"node": ">=6.9.0"
|
| 64 |
+
}
|
| 65 |
+
},
|
| 66 |
+
"node_modules/@babel/core": {
|
| 67 |
+
"version": "7.28.3",
|
| 68 |
+
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz",
|
| 69 |
+
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
|
| 70 |
+
"dev": true,
|
| 71 |
+
"license": "MIT",
|
| 72 |
+
"dependencies": {
|
| 73 |
+
"@ampproject/remapping": "^2.2.0",
|
| 74 |
+
"@babel/code-frame": "^7.27.1",
|
| 75 |
+
"@babel/generator": "^7.28.3",
|
| 76 |
+
"@babel/helper-compilation-targets": "^7.27.2",
|
| 77 |
+
"@babel/helper-module-transforms": "^7.28.3",
|
| 78 |
+
"@babel/helpers": "^7.28.3",
|
| 79 |
+
"@babel/parser": "^7.28.3",
|
| 80 |
+
"@babel/template": "^7.27.2",
|
| 81 |
+
"@babel/traverse": "^7.28.3",
|
| 82 |
+
"@babel/types": "^7.28.2",
|
| 83 |
+
"convert-source-map": "^2.0.0",
|
| 84 |
+
"debug": "^4.1.0",
|
| 85 |
+
"gensync": "^1.0.0-beta.2",
|
| 86 |
+
"json5": "^2.2.3",
|
| 87 |
+
"semver": "^6.3.1"
|
| 88 |
+
},
|
| 89 |
+
"engines": {
|
| 90 |
+
"node": ">=6.9.0"
|
| 91 |
+
},
|
| 92 |
+
"funding": {
|
| 93 |
+
"type": "opencollective",
|
| 94 |
+
"url": "https://opencollective.com/babel"
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
"node_modules/@babel/generator": {
|
| 98 |
+
"version": "7.28.3",
|
| 99 |
+
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
|
| 100 |
+
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
|
| 101 |
+
"dev": true,
|
| 102 |
+
"license": "MIT",
|
| 103 |
+
"dependencies": {
|
| 104 |
+
"@babel/parser": "^7.28.3",
|
| 105 |
+
"@babel/types": "^7.28.2",
|
| 106 |
+
"@jridgewell/gen-mapping": "^0.3.12",
|
| 107 |
+
"@jridgewell/trace-mapping": "^0.3.28",
|
| 108 |
+
"jsesc": "^3.0.2"
|
| 109 |
+
},
|
| 110 |
+
"engines": {
|
| 111 |
+
"node": ">=6.9.0"
|
| 112 |
+
}
|
| 113 |
+
},
|
| 114 |
+
"node_modules/@babel/helper-compilation-targets": {
|
| 115 |
+
"version": "7.27.2",
|
| 116 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
|
| 117 |
+
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
|
| 118 |
+
"dev": true,
|
| 119 |
+
"license": "MIT",
|
| 120 |
+
"dependencies": {
|
| 121 |
+
"@babel/compat-data": "^7.27.2",
|
| 122 |
+
"@babel/helper-validator-option": "^7.27.1",
|
| 123 |
+
"browserslist": "^4.24.0",
|
| 124 |
+
"lru-cache": "^5.1.1",
|
| 125 |
+
"semver": "^6.3.1"
|
| 126 |
+
},
|
| 127 |
+
"engines": {
|
| 128 |
+
"node": ">=6.9.0"
|
| 129 |
+
}
|
| 130 |
+
},
|
| 131 |
+
"node_modules/@babel/helper-globals": {
|
| 132 |
+
"version": "7.28.0",
|
| 133 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
| 134 |
+
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
| 135 |
+
"dev": true,
|
| 136 |
+
"license": "MIT",
|
| 137 |
+
"engines": {
|
| 138 |
+
"node": ">=6.9.0"
|
| 139 |
+
}
|
| 140 |
+
},
|
| 141 |
+
"node_modules/@babel/helper-module-imports": {
|
| 142 |
+
"version": "7.27.1",
|
| 143 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
| 144 |
+
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
| 145 |
+
"dev": true,
|
| 146 |
+
"license": "MIT",
|
| 147 |
+
"dependencies": {
|
| 148 |
+
"@babel/traverse": "^7.27.1",
|
| 149 |
+
"@babel/types": "^7.27.1"
|
| 150 |
+
},
|
| 151 |
+
"engines": {
|
| 152 |
+
"node": ">=6.9.0"
|
| 153 |
+
}
|
| 154 |
+
},
|
| 155 |
+
"node_modules/@babel/helper-module-transforms": {
|
| 156 |
+
"version": "7.28.3",
|
| 157 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
|
| 158 |
+
"integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
|
| 159 |
+
"dev": true,
|
| 160 |
+
"license": "MIT",
|
| 161 |
+
"dependencies": {
|
| 162 |
+
"@babel/helper-module-imports": "^7.27.1",
|
| 163 |
+
"@babel/helper-validator-identifier": "^7.27.1",
|
| 164 |
+
"@babel/traverse": "^7.28.3"
|
| 165 |
+
},
|
| 166 |
+
"engines": {
|
| 167 |
+
"node": ">=6.9.0"
|
| 168 |
+
},
|
| 169 |
+
"peerDependencies": {
|
| 170 |
+
"@babel/core": "^7.0.0"
|
| 171 |
+
}
|
| 172 |
+
},
|
| 173 |
+
"node_modules/@babel/helper-plugin-utils": {
|
| 174 |
+
"version": "7.27.1",
|
| 175 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
|
| 176 |
+
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
|
| 177 |
+
"dev": true,
|
| 178 |
+
"license": "MIT",
|
| 179 |
+
"engines": {
|
| 180 |
+
"node": ">=6.9.0"
|
| 181 |
+
}
|
| 182 |
+
},
|
| 183 |
+
"node_modules/@babel/helper-string-parser": {
|
| 184 |
+
"version": "7.27.1",
|
| 185 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
| 186 |
+
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
| 187 |
+
"dev": true,
|
| 188 |
+
"license": "MIT",
|
| 189 |
+
"engines": {
|
| 190 |
+
"node": ">=6.9.0"
|
| 191 |
+
}
|
| 192 |
+
},
|
| 193 |
+
"node_modules/@babel/helper-validator-identifier": {
|
| 194 |
+
"version": "7.27.1",
|
| 195 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
| 196 |
+
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
| 197 |
+
"dev": true,
|
| 198 |
+
"license": "MIT",
|
| 199 |
+
"engines": {
|
| 200 |
+
"node": ">=6.9.0"
|
| 201 |
+
}
|
| 202 |
+
},
|
| 203 |
+
"node_modules/@babel/helper-validator-option": {
|
| 204 |
+
"version": "7.27.1",
|
| 205 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
| 206 |
+
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
| 207 |
+
"dev": true,
|
| 208 |
+
"license": "MIT",
|
| 209 |
+
"engines": {
|
| 210 |
+
"node": ">=6.9.0"
|
| 211 |
+
}
|
| 212 |
+
},
|
| 213 |
+
"node_modules/@babel/helpers": {
|
| 214 |
+
"version": "7.28.3",
|
| 215 |
+
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz",
|
| 216 |
+
"integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==",
|
| 217 |
+
"dev": true,
|
| 218 |
+
"license": "MIT",
|
| 219 |
+
"dependencies": {
|
| 220 |
+
"@babel/template": "^7.27.2",
|
| 221 |
+
"@babel/types": "^7.28.2"
|
| 222 |
+
},
|
| 223 |
+
"engines": {
|
| 224 |
+
"node": ">=6.9.0"
|
| 225 |
+
}
|
| 226 |
+
},
|
| 227 |
+
"node_modules/@babel/parser": {
|
| 228 |
+
"version": "7.28.3",
|
| 229 |
+
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
|
| 230 |
+
"integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
|
| 231 |
+
"dev": true,
|
| 232 |
+
"license": "MIT",
|
| 233 |
+
"dependencies": {
|
| 234 |
+
"@babel/types": "^7.28.2"
|
| 235 |
+
},
|
| 236 |
+
"bin": {
|
| 237 |
+
"parser": "bin/babel-parser.js"
|
| 238 |
+
},
|
| 239 |
+
"engines": {
|
| 240 |
+
"node": ">=6.0.0"
|
| 241 |
+
}
|
| 242 |
+
},
|
| 243 |
+
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
| 244 |
+
"version": "7.27.1",
|
| 245 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
|
| 246 |
+
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
|
| 247 |
+
"dev": true,
|
| 248 |
+
"license": "MIT",
|
| 249 |
+
"dependencies": {
|
| 250 |
+
"@babel/helper-plugin-utils": "^7.27.1"
|
| 251 |
+
},
|
| 252 |
+
"engines": {
|
| 253 |
+
"node": ">=6.9.0"
|
| 254 |
+
},
|
| 255 |
+
"peerDependencies": {
|
| 256 |
+
"@babel/core": "^7.0.0-0"
|
| 257 |
+
}
|
| 258 |
+
},
|
| 259 |
+
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
| 260 |
+
"version": "7.27.1",
|
| 261 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
|
| 262 |
+
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
|
| 263 |
+
"dev": true,
|
| 264 |
+
"license": "MIT",
|
| 265 |
+
"dependencies": {
|
| 266 |
+
"@babel/helper-plugin-utils": "^7.27.1"
|
| 267 |
+
},
|
| 268 |
+
"engines": {
|
| 269 |
+
"node": ">=6.9.0"
|
| 270 |
+
},
|
| 271 |
+
"peerDependencies": {
|
| 272 |
+
"@babel/core": "^7.0.0-0"
|
| 273 |
+
}
|
| 274 |
+
},
|
| 275 |
+
"node_modules/@babel/template": {
|
| 276 |
+
"version": "7.27.2",
|
| 277 |
+
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
| 278 |
+
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
| 279 |
+
"dev": true,
|
| 280 |
+
"license": "MIT",
|
| 281 |
+
"dependencies": {
|
| 282 |
+
"@babel/code-frame": "^7.27.1",
|
| 283 |
+
"@babel/parser": "^7.27.2",
|
| 284 |
+
"@babel/types": "^7.27.1"
|
| 285 |
+
},
|
| 286 |
+
"engines": {
|
| 287 |
+
"node": ">=6.9.0"
|
| 288 |
+
}
|
| 289 |
+
},
|
| 290 |
+
"node_modules/@babel/traverse": {
|
| 291 |
+
"version": "7.28.3",
|
| 292 |
+
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz",
|
| 293 |
+
"integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==",
|
| 294 |
+
"dev": true,
|
| 295 |
+
"license": "MIT",
|
| 296 |
+
"dependencies": {
|
| 297 |
+
"@babel/code-frame": "^7.27.1",
|
| 298 |
+
"@babel/generator": "^7.28.3",
|
| 299 |
+
"@babel/helper-globals": "^7.28.0",
|
| 300 |
+
"@babel/parser": "^7.28.3",
|
| 301 |
+
"@babel/template": "^7.27.2",
|
| 302 |
+
"@babel/types": "^7.28.2",
|
| 303 |
+
"debug": "^4.3.1"
|
| 304 |
+
},
|
| 305 |
+
"engines": {
|
| 306 |
+
"node": ">=6.9.0"
|
| 307 |
+
}
|
| 308 |
+
},
|
| 309 |
+
"node_modules/@babel/types": {
|
| 310 |
+
"version": "7.28.2",
|
| 311 |
+
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
|
| 312 |
+
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
|
| 313 |
+
"dev": true,
|
| 314 |
+
"license": "MIT",
|
| 315 |
+
"dependencies": {
|
| 316 |
+
"@babel/helper-string-parser": "^7.27.1",
|
| 317 |
+
"@babel/helper-validator-identifier": "^7.27.1"
|
| 318 |
+
},
|
| 319 |
+
"engines": {
|
| 320 |
+
"node": ">=6.9.0"
|
| 321 |
+
}
|
| 322 |
+
},
|
| 323 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 324 |
+
"version": "0.21.5",
|
| 325 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
| 326 |
+
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
| 327 |
+
"cpu": [
|
| 328 |
+
"ppc64"
|
| 329 |
+
],
|
| 330 |
+
"dev": true,
|
| 331 |
+
"license": "MIT",
|
| 332 |
+
"optional": true,
|
| 333 |
+
"os": [
|
| 334 |
+
"aix"
|
| 335 |
+
],
|
| 336 |
+
"engines": {
|
| 337 |
+
"node": ">=12"
|
| 338 |
+
}
|
| 339 |
+
},
|
| 340 |
+
"node_modules/@esbuild/android-arm": {
|
| 341 |
+
"version": "0.21.5",
|
| 342 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
| 343 |
+
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
| 344 |
+
"cpu": [
|
| 345 |
+
"arm"
|
| 346 |
+
],
|
| 347 |
+
"dev": true,
|
| 348 |
+
"license": "MIT",
|
| 349 |
+
"optional": true,
|
| 350 |
+
"os": [
|
| 351 |
+
"android"
|
| 352 |
+
],
|
| 353 |
+
"engines": {
|
| 354 |
+
"node": ">=12"
|
| 355 |
+
}
|
| 356 |
+
},
|
| 357 |
+
"node_modules/@esbuild/android-arm64": {
|
| 358 |
+
"version": "0.21.5",
|
| 359 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
| 360 |
+
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
| 361 |
+
"cpu": [
|
| 362 |
+
"arm64"
|
| 363 |
+
],
|
| 364 |
+
"dev": true,
|
| 365 |
+
"license": "MIT",
|
| 366 |
+
"optional": true,
|
| 367 |
+
"os": [
|
| 368 |
+
"android"
|
| 369 |
+
],
|
| 370 |
+
"engines": {
|
| 371 |
+
"node": ">=12"
|
| 372 |
+
}
|
| 373 |
+
},
|
| 374 |
+
"node_modules/@esbuild/android-x64": {
|
| 375 |
+
"version": "0.21.5",
|
| 376 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
| 377 |
+
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
| 378 |
+
"cpu": [
|
| 379 |
+
"x64"
|
| 380 |
+
],
|
| 381 |
+
"dev": true,
|
| 382 |
+
"license": "MIT",
|
| 383 |
+
"optional": true,
|
| 384 |
+
"os": [
|
| 385 |
+
"android"
|
| 386 |
+
],
|
| 387 |
+
"engines": {
|
| 388 |
+
"node": ">=12"
|
| 389 |
+
}
|
| 390 |
+
},
|
| 391 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 392 |
+
"version": "0.21.5",
|
| 393 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
| 394 |
+
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
| 395 |
+
"cpu": [
|
| 396 |
+
"arm64"
|
| 397 |
+
],
|
| 398 |
+
"dev": true,
|
| 399 |
+
"license": "MIT",
|
| 400 |
+
"optional": true,
|
| 401 |
+
"os": [
|
| 402 |
+
"darwin"
|
| 403 |
+
],
|
| 404 |
+
"engines": {
|
| 405 |
+
"node": ">=12"
|
| 406 |
+
}
|
| 407 |
+
},
|
| 408 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 409 |
+
"version": "0.21.5",
|
| 410 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
| 411 |
+
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
| 412 |
+
"cpu": [
|
| 413 |
+
"x64"
|
| 414 |
+
],
|
| 415 |
+
"dev": true,
|
| 416 |
+
"license": "MIT",
|
| 417 |
+
"optional": true,
|
| 418 |
+
"os": [
|
| 419 |
+
"darwin"
|
| 420 |
+
],
|
| 421 |
+
"engines": {
|
| 422 |
+
"node": ">=12"
|
| 423 |
+
}
|
| 424 |
+
},
|
| 425 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 426 |
+
"version": "0.21.5",
|
| 427 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
| 428 |
+
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
| 429 |
+
"cpu": [
|
| 430 |
+
"arm64"
|
| 431 |
+
],
|
| 432 |
+
"dev": true,
|
| 433 |
+
"license": "MIT",
|
| 434 |
+
"optional": true,
|
| 435 |
+
"os": [
|
| 436 |
+
"freebsd"
|
| 437 |
+
],
|
| 438 |
+
"engines": {
|
| 439 |
+
"node": ">=12"
|
| 440 |
+
}
|
| 441 |
+
},
|
| 442 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 443 |
+
"version": "0.21.5",
|
| 444 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
| 445 |
+
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
| 446 |
+
"cpu": [
|
| 447 |
+
"x64"
|
| 448 |
+
],
|
| 449 |
+
"dev": true,
|
| 450 |
+
"license": "MIT",
|
| 451 |
+
"optional": true,
|
| 452 |
+
"os": [
|
| 453 |
+
"freebsd"
|
| 454 |
+
],
|
| 455 |
+
"engines": {
|
| 456 |
+
"node": ">=12"
|
| 457 |
+
}
|
| 458 |
+
},
|
| 459 |
+
"node_modules/@esbuild/linux-arm": {
|
| 460 |
+
"version": "0.21.5",
|
| 461 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
| 462 |
+
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
| 463 |
+
"cpu": [
|
| 464 |
+
"arm"
|
| 465 |
+
],
|
| 466 |
+
"dev": true,
|
| 467 |
+
"license": "MIT",
|
| 468 |
+
"optional": true,
|
| 469 |
+
"os": [
|
| 470 |
+
"linux"
|
| 471 |
+
],
|
| 472 |
+
"engines": {
|
| 473 |
+
"node": ">=12"
|
| 474 |
+
}
|
| 475 |
+
},
|
| 476 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 477 |
+
"version": "0.21.5",
|
| 478 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
| 479 |
+
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
| 480 |
+
"cpu": [
|
| 481 |
+
"arm64"
|
| 482 |
+
],
|
| 483 |
+
"dev": true,
|
| 484 |
+
"license": "MIT",
|
| 485 |
+
"optional": true,
|
| 486 |
+
"os": [
|
| 487 |
+
"linux"
|
| 488 |
+
],
|
| 489 |
+
"engines": {
|
| 490 |
+
"node": ">=12"
|
| 491 |
+
}
|
| 492 |
+
},
|
| 493 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 494 |
+
"version": "0.21.5",
|
| 495 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
| 496 |
+
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
| 497 |
+
"cpu": [
|
| 498 |
+
"ia32"
|
| 499 |
+
],
|
| 500 |
+
"dev": true,
|
| 501 |
+
"license": "MIT",
|
| 502 |
+
"optional": true,
|
| 503 |
+
"os": [
|
| 504 |
+
"linux"
|
| 505 |
+
],
|
| 506 |
+
"engines": {
|
| 507 |
+
"node": ">=12"
|
| 508 |
+
}
|
| 509 |
+
},
|
| 510 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 511 |
+
"version": "0.21.5",
|
| 512 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
| 513 |
+
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
| 514 |
+
"cpu": [
|
| 515 |
+
"loong64"
|
| 516 |
+
],
|
| 517 |
+
"dev": true,
|
| 518 |
+
"license": "MIT",
|
| 519 |
+
"optional": true,
|
| 520 |
+
"os": [
|
| 521 |
+
"linux"
|
| 522 |
+
],
|
| 523 |
+
"engines": {
|
| 524 |
+
"node": ">=12"
|
| 525 |
+
}
|
| 526 |
+
},
|
| 527 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 528 |
+
"version": "0.21.5",
|
| 529 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
| 530 |
+
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
| 531 |
+
"cpu": [
|
| 532 |
+
"mips64el"
|
| 533 |
+
],
|
| 534 |
+
"dev": true,
|
| 535 |
+
"license": "MIT",
|
| 536 |
+
"optional": true,
|
| 537 |
+
"os": [
|
| 538 |
+
"linux"
|
| 539 |
+
],
|
| 540 |
+
"engines": {
|
| 541 |
+
"node": ">=12"
|
| 542 |
+
}
|
| 543 |
+
},
|
| 544 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 545 |
+
"version": "0.21.5",
|
| 546 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
| 547 |
+
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
| 548 |
+
"cpu": [
|
| 549 |
+
"ppc64"
|
| 550 |
+
],
|
| 551 |
+
"dev": true,
|
| 552 |
+
"license": "MIT",
|
| 553 |
+
"optional": true,
|
| 554 |
+
"os": [
|
| 555 |
+
"linux"
|
| 556 |
+
],
|
| 557 |
+
"engines": {
|
| 558 |
+
"node": ">=12"
|
| 559 |
+
}
|
| 560 |
+
},
|
| 561 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 562 |
+
"version": "0.21.5",
|
| 563 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
| 564 |
+
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
| 565 |
+
"cpu": [
|
| 566 |
+
"riscv64"
|
| 567 |
+
],
|
| 568 |
+
"dev": true,
|
| 569 |
+
"license": "MIT",
|
| 570 |
+
"optional": true,
|
| 571 |
+
"os": [
|
| 572 |
+
"linux"
|
| 573 |
+
],
|
| 574 |
+
"engines": {
|
| 575 |
+
"node": ">=12"
|
| 576 |
+
}
|
| 577 |
+
},
|
| 578 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 579 |
+
"version": "0.21.5",
|
| 580 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
| 581 |
+
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
| 582 |
+
"cpu": [
|
| 583 |
+
"s390x"
|
| 584 |
+
],
|
| 585 |
+
"dev": true,
|
| 586 |
+
"license": "MIT",
|
| 587 |
+
"optional": true,
|
| 588 |
+
"os": [
|
| 589 |
+
"linux"
|
| 590 |
+
],
|
| 591 |
+
"engines": {
|
| 592 |
+
"node": ">=12"
|
| 593 |
+
}
|
| 594 |
+
},
|
| 595 |
+
"node_modules/@esbuild/linux-x64": {
|
| 596 |
+
"version": "0.21.5",
|
| 597 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
| 598 |
+
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
| 599 |
+
"cpu": [
|
| 600 |
+
"x64"
|
| 601 |
+
],
|
| 602 |
+
"dev": true,
|
| 603 |
+
"license": "MIT",
|
| 604 |
+
"optional": true,
|
| 605 |
+
"os": [
|
| 606 |
+
"linux"
|
| 607 |
+
],
|
| 608 |
+
"engines": {
|
| 609 |
+
"node": ">=12"
|
| 610 |
+
}
|
| 611 |
+
},
|
| 612 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 613 |
+
"version": "0.21.5",
|
| 614 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
| 615 |
+
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
| 616 |
+
"cpu": [
|
| 617 |
+
"x64"
|
| 618 |
+
],
|
| 619 |
+
"dev": true,
|
| 620 |
+
"license": "MIT",
|
| 621 |
+
"optional": true,
|
| 622 |
+
"os": [
|
| 623 |
+
"netbsd"
|
| 624 |
+
],
|
| 625 |
+
"engines": {
|
| 626 |
+
"node": ">=12"
|
| 627 |
+
}
|
| 628 |
+
},
|
| 629 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 630 |
+
"version": "0.21.5",
|
| 631 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
| 632 |
+
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
| 633 |
+
"cpu": [
|
| 634 |
+
"x64"
|
| 635 |
+
],
|
| 636 |
+
"dev": true,
|
| 637 |
+
"license": "MIT",
|
| 638 |
+
"optional": true,
|
| 639 |
+
"os": [
|
| 640 |
+
"openbsd"
|
| 641 |
+
],
|
| 642 |
+
"engines": {
|
| 643 |
+
"node": ">=12"
|
| 644 |
+
}
|
| 645 |
+
},
|
| 646 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 647 |
+
"version": "0.21.5",
|
| 648 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
| 649 |
+
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
| 650 |
+
"cpu": [
|
| 651 |
+
"x64"
|
| 652 |
+
],
|
| 653 |
+
"dev": true,
|
| 654 |
+
"license": "MIT",
|
| 655 |
+
"optional": true,
|
| 656 |
+
"os": [
|
| 657 |
+
"sunos"
|
| 658 |
+
],
|
| 659 |
+
"engines": {
|
| 660 |
+
"node": ">=12"
|
| 661 |
+
}
|
| 662 |
+
},
|
| 663 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 664 |
+
"version": "0.21.5",
|
| 665 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
| 666 |
+
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
| 667 |
+
"cpu": [
|
| 668 |
+
"arm64"
|
| 669 |
+
],
|
| 670 |
+
"dev": true,
|
| 671 |
+
"license": "MIT",
|
| 672 |
+
"optional": true,
|
| 673 |
+
"os": [
|
| 674 |
+
"win32"
|
| 675 |
+
],
|
| 676 |
+
"engines": {
|
| 677 |
+
"node": ">=12"
|
| 678 |
+
}
|
| 679 |
+
},
|
| 680 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 681 |
+
"version": "0.21.5",
|
| 682 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
| 683 |
+
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
| 684 |
+
"cpu": [
|
| 685 |
+
"ia32"
|
| 686 |
+
],
|
| 687 |
+
"dev": true,
|
| 688 |
+
"license": "MIT",
|
| 689 |
+
"optional": true,
|
| 690 |
+
"os": [
|
| 691 |
+
"win32"
|
| 692 |
+
],
|
| 693 |
+
"engines": {
|
| 694 |
+
"node": ">=12"
|
| 695 |
+
}
|
| 696 |
+
},
|
| 697 |
+
"node_modules/@esbuild/win32-x64": {
|
| 698 |
+
"version": "0.21.5",
|
| 699 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
| 700 |
+
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
| 701 |
+
"cpu": [
|
| 702 |
+
"x64"
|
| 703 |
+
],
|
| 704 |
+
"dev": true,
|
| 705 |
+
"license": "MIT",
|
| 706 |
+
"optional": true,
|
| 707 |
+
"os": [
|
| 708 |
+
"win32"
|
| 709 |
+
],
|
| 710 |
+
"engines": {
|
| 711 |
+
"node": ">=12"
|
| 712 |
+
}
|
| 713 |
+
},
|
| 714 |
+
"node_modules/@jridgewell/gen-mapping": {
|
| 715 |
+
"version": "0.3.13",
|
| 716 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
| 717 |
+
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
| 718 |
+
"dev": true,
|
| 719 |
+
"license": "MIT",
|
| 720 |
+
"dependencies": {
|
| 721 |
+
"@jridgewell/sourcemap-codec": "^1.5.0",
|
| 722 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 723 |
+
}
|
| 724 |
+
},
|
| 725 |
+
"node_modules/@jridgewell/resolve-uri": {
|
| 726 |
+
"version": "3.1.2",
|
| 727 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 728 |
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
| 729 |
+
"dev": true,
|
| 730 |
+
"license": "MIT",
|
| 731 |
+
"engines": {
|
| 732 |
+
"node": ">=6.0.0"
|
| 733 |
+
}
|
| 734 |
+
},
|
| 735 |
+
"node_modules/@jridgewell/sourcemap-codec": {
|
| 736 |
+
"version": "1.5.5",
|
| 737 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 738 |
+
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
| 739 |
+
"dev": true,
|
| 740 |
+
"license": "MIT"
|
| 741 |
+
},
|
| 742 |
+
"node_modules/@jridgewell/trace-mapping": {
|
| 743 |
+
"version": "0.3.30",
|
| 744 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
|
| 745 |
+
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
|
| 746 |
+
"dev": true,
|
| 747 |
+
"license": "MIT",
|
| 748 |
+
"dependencies": {
|
| 749 |
+
"@jridgewell/resolve-uri": "^3.1.0",
|
| 750 |
+
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 751 |
+
}
|
| 752 |
+
},
|
| 753 |
+
"node_modules/@react-leaflet/core": {
|
| 754 |
+
"version": "2.1.0",
|
| 755 |
+
"resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
|
| 756 |
+
"integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
|
| 757 |
+
"license": "Hippocratic-2.1",
|
| 758 |
+
"peerDependencies": {
|
| 759 |
+
"leaflet": "^1.9.0",
|
| 760 |
+
"react": "^18.0.0",
|
| 761 |
+
"react-dom": "^18.0.0"
|
| 762 |
+
}
|
| 763 |
+
},
|
| 764 |
+
"node_modules/@rolldown/pluginutils": {
|
| 765 |
+
"version": "1.0.0-beta.32",
|
| 766 |
+
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.32.tgz",
|
| 767 |
+
"integrity": "sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g==",
|
| 768 |
+
"dev": true,
|
| 769 |
+
"license": "MIT"
|
| 770 |
+
},
|
| 771 |
+
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 772 |
+
"version": "4.48.0",
|
| 773 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.48.0.tgz",
|
| 774 |
+
"integrity": "sha512-aVzKH922ogVAWkKiyKXorjYymz2084zrhrZRXtLrA5eEx5SO8Dj0c/4FpCHZyn7MKzhW2pW4tK28vVr+5oQ2xw==",
|
| 775 |
+
"cpu": [
|
| 776 |
+
"arm"
|
| 777 |
+
],
|
| 778 |
+
"dev": true,
|
| 779 |
+
"license": "MIT",
|
| 780 |
+
"optional": true,
|
| 781 |
+
"os": [
|
| 782 |
+
"android"
|
| 783 |
+
]
|
| 784 |
+
},
|
| 785 |
+
"node_modules/@rollup/rollup-android-arm64": {
|
| 786 |
+
"version": "4.48.0",
|
| 787 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.48.0.tgz",
|
| 788 |
+
"integrity": "sha512-diOdQuw43xTa1RddAFbhIA8toirSzFMcnIg8kvlzRbK26xqEnKJ/vqQnghTAajy2Dcy42v+GMPMo6jq67od+Dw==",
|
| 789 |
+
"cpu": [
|
| 790 |
+
"arm64"
|
| 791 |
+
],
|
| 792 |
+
"dev": true,
|
| 793 |
+
"license": "MIT",
|
| 794 |
+
"optional": true,
|
| 795 |
+
"os": [
|
| 796 |
+
"android"
|
| 797 |
+
]
|
| 798 |
+
},
|
| 799 |
+
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 800 |
+
"version": "4.48.0",
|
| 801 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.48.0.tgz",
|
| 802 |
+
"integrity": "sha512-QhR2KA18fPlJWFefySJPDYZELaVqIUVnYgAOdtJ+B/uH96CFg2l1TQpX19XpUMWUqMyIiyY45wje8K6F4w4/CA==",
|
| 803 |
+
"cpu": [
|
| 804 |
+
"arm64"
|
| 805 |
+
],
|
| 806 |
+
"dev": true,
|
| 807 |
+
"license": "MIT",
|
| 808 |
+
"optional": true,
|
| 809 |
+
"os": [
|
| 810 |
+
"darwin"
|
| 811 |
+
]
|
| 812 |
+
},
|
| 813 |
+
"node_modules/@rollup/rollup-darwin-x64": {
|
| 814 |
+
"version": "4.48.0",
|
| 815 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.48.0.tgz",
|
| 816 |
+
"integrity": "sha512-Q9RMXnQVJ5S1SYpNSTwXDpoQLgJ/fbInWOyjbCnnqTElEyeNvLAB3QvG5xmMQMhFN74bB5ZZJYkKaFPcOG8sGg==",
|
| 817 |
+
"cpu": [
|
| 818 |
+
"x64"
|
| 819 |
+
],
|
| 820 |
+
"dev": true,
|
| 821 |
+
"license": "MIT",
|
| 822 |
+
"optional": true,
|
| 823 |
+
"os": [
|
| 824 |
+
"darwin"
|
| 825 |
+
]
|
| 826 |
+
},
|
| 827 |
+
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 828 |
+
"version": "4.48.0",
|
| 829 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.48.0.tgz",
|
| 830 |
+
"integrity": "sha512-3jzOhHWM8O8PSfyft+ghXZfBkZawQA0PUGtadKYxFqpcYlOYjTi06WsnYBsbMHLawr+4uWirLlbhcYLHDXR16w==",
|
| 831 |
+
"cpu": [
|
| 832 |
+
"arm64"
|
| 833 |
+
],
|
| 834 |
+
"dev": true,
|
| 835 |
+
"license": "MIT",
|
| 836 |
+
"optional": true,
|
| 837 |
+
"os": [
|
| 838 |
+
"freebsd"
|
| 839 |
+
]
|
| 840 |
+
},
|
| 841 |
+
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 842 |
+
"version": "4.48.0",
|
| 843 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.48.0.tgz",
|
| 844 |
+
"integrity": "sha512-NcD5uVUmE73C/TPJqf78hInZmiSBsDpz3iD5MF/BuB+qzm4ooF2S1HfeTChj5K4AV3y19FFPgxonsxiEpy8v/A==",
|
| 845 |
+
"cpu": [
|
| 846 |
+
"x64"
|
| 847 |
+
],
|
| 848 |
+
"dev": true,
|
| 849 |
+
"license": "MIT",
|
| 850 |
+
"optional": true,
|
| 851 |
+
"os": [
|
| 852 |
+
"freebsd"
|
| 853 |
+
]
|
| 854 |
+
},
|
| 855 |
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 856 |
+
"version": "4.48.0",
|
| 857 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.48.0.tgz",
|
| 858 |
+
"integrity": "sha512-JWnrj8qZgLWRNHr7NbpdnrQ8kcg09EBBq8jVOjmtlB3c8C6IrynAJSMhMVGME4YfTJzIkJqvSUSVJRqkDnu/aA==",
|
| 859 |
+
"cpu": [
|
| 860 |
+
"arm"
|
| 861 |
+
],
|
| 862 |
+
"dev": true,
|
| 863 |
+
"license": "MIT",
|
| 864 |
+
"optional": true,
|
| 865 |
+
"os": [
|
| 866 |
+
"linux"
|
| 867 |
+
]
|
| 868 |
+
},
|
| 869 |
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 870 |
+
"version": "4.48.0",
|
| 871 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.48.0.tgz",
|
| 872 |
+
"integrity": "sha512-9xu92F0TxuMH0tD6tG3+GtngwdgSf8Bnz+YcsPG91/r5Vgh5LNofO48jV55priA95p3c92FLmPM7CvsVlnSbGQ==",
|
| 873 |
+
"cpu": [
|
| 874 |
+
"arm"
|
| 875 |
+
],
|
| 876 |
+
"dev": true,
|
| 877 |
+
"license": "MIT",
|
| 878 |
+
"optional": true,
|
| 879 |
+
"os": [
|
| 880 |
+
"linux"
|
| 881 |
+
]
|
| 882 |
+
},
|
| 883 |
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 884 |
+
"version": "4.48.0",
|
| 885 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.48.0.tgz",
|
| 886 |
+
"integrity": "sha512-NLtvJB5YpWn7jlp1rJiY0s+G1Z1IVmkDuiywiqUhh96MIraC0n7XQc2SZ1CZz14shqkM+XN2UrfIo7JB6UufOA==",
|
| 887 |
+
"cpu": [
|
| 888 |
+
"arm64"
|
| 889 |
+
],
|
| 890 |
+
"dev": true,
|
| 891 |
+
"license": "MIT",
|
| 892 |
+
"optional": true,
|
| 893 |
+
"os": [
|
| 894 |
+
"linux"
|
| 895 |
+
]
|
| 896 |
+
},
|
| 897 |
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 898 |
+
"version": "4.48.0",
|
| 899 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.48.0.tgz",
|
| 900 |
+
"integrity": "sha512-QJ4hCOnz2SXgCh+HmpvZkM+0NSGcZACyYS8DGbWn2PbmA0e5xUk4bIP8eqJyNXLtyB4gZ3/XyvKtQ1IFH671vQ==",
|
| 901 |
+
"cpu": [
|
| 902 |
+
"arm64"
|
| 903 |
+
],
|
| 904 |
+
"dev": true,
|
| 905 |
+
"license": "MIT",
|
| 906 |
+
"optional": true,
|
| 907 |
+
"os": [
|
| 908 |
+
"linux"
|
| 909 |
+
]
|
| 910 |
+
},
|
| 911 |
+
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
| 912 |
+
"version": "4.48.0",
|
| 913 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.48.0.tgz",
|
| 914 |
+
"integrity": "sha512-Pk0qlGJnhILdIC5zSKQnprFjrGmjfDM7TPZ0FKJxRkoo+kgMRAg4ps1VlTZf8u2vohSicLg7NP+cA5qE96PaFg==",
|
| 915 |
+
"cpu": [
|
| 916 |
+
"loong64"
|
| 917 |
+
],
|
| 918 |
+
"dev": true,
|
| 919 |
+
"license": "MIT",
|
| 920 |
+
"optional": true,
|
| 921 |
+
"os": [
|
| 922 |
+
"linux"
|
| 923 |
+
]
|
| 924 |
+
},
|
| 925 |
+
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
| 926 |
+
"version": "4.48.0",
|
| 927 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.48.0.tgz",
|
| 928 |
+
"integrity": "sha512-/dNFc6rTpoOzgp5GKoYjT6uLo8okR/Chi2ECOmCZiS4oqh3mc95pThWma7Bgyk6/WTEvjDINpiBCuecPLOgBLQ==",
|
| 929 |
+
"cpu": [
|
| 930 |
+
"ppc64"
|
| 931 |
+
],
|
| 932 |
+
"dev": true,
|
| 933 |
+
"license": "MIT",
|
| 934 |
+
"optional": true,
|
| 935 |
+
"os": [
|
| 936 |
+
"linux"
|
| 937 |
+
]
|
| 938 |
+
},
|
| 939 |
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 940 |
+
"version": "4.48.0",
|
| 941 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.48.0.tgz",
|
| 942 |
+
"integrity": "sha512-YBwXsvsFI8CVA4ej+bJF2d9uAeIiSkqKSPQNn0Wyh4eMDY4wxuSp71BauPjQNCKK2tD2/ksJ7uhJ8X/PVY9bHQ==",
|
| 943 |
+
"cpu": [
|
| 944 |
+
"riscv64"
|
| 945 |
+
],
|
| 946 |
+
"dev": true,
|
| 947 |
+
"license": "MIT",
|
| 948 |
+
"optional": true,
|
| 949 |
+
"os": [
|
| 950 |
+
"linux"
|
| 951 |
+
]
|
| 952 |
+
},
|
| 953 |
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 954 |
+
"version": "4.48.0",
|
| 955 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.48.0.tgz",
|
| 956 |
+
"integrity": "sha512-FI3Rr2aGAtl1aHzbkBIamsQyuauYtTF9SDUJ8n2wMXuuxwchC3QkumZa1TEXYIv/1AUp1a25Kwy6ONArvnyeVQ==",
|
| 957 |
+
"cpu": [
|
| 958 |
+
"riscv64"
|
| 959 |
+
],
|
| 960 |
+
"dev": true,
|
| 961 |
+
"license": "MIT",
|
| 962 |
+
"optional": true,
|
| 963 |
+
"os": [
|
| 964 |
+
"linux"
|
| 965 |
+
]
|
| 966 |
+
},
|
| 967 |
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 968 |
+
"version": "4.48.0",
|
| 969 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.48.0.tgz",
|
| 970 |
+
"integrity": "sha512-Dx7qH0/rvNNFmCcIRe1pyQ9/H0XO4v/f0SDoafwRYwc2J7bJZ5N4CHL/cdjamISZ5Cgnon6iazAVRFlxSoHQnQ==",
|
| 971 |
+
"cpu": [
|
| 972 |
+
"s390x"
|
| 973 |
+
],
|
| 974 |
+
"dev": true,
|
| 975 |
+
"license": "MIT",
|
| 976 |
+
"optional": true,
|
| 977 |
+
"os": [
|
| 978 |
+
"linux"
|
| 979 |
+
]
|
| 980 |
+
},
|
| 981 |
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 982 |
+
"version": "4.48.0",
|
| 983 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.48.0.tgz",
|
| 984 |
+
"integrity": "sha512-GUdZKTeKBq9WmEBzvFYuC88yk26vT66lQV8D5+9TgkfbewhLaTHRNATyzpQwwbHIfJvDJ3N9WJ90wK/uR3cy3Q==",
|
| 985 |
+
"cpu": [
|
| 986 |
+
"x64"
|
| 987 |
+
],
|
| 988 |
+
"dev": true,
|
| 989 |
+
"license": "MIT",
|
| 990 |
+
"optional": true,
|
| 991 |
+
"os": [
|
| 992 |
+
"linux"
|
| 993 |
+
]
|
| 994 |
+
},
|
| 995 |
+
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 996 |
+
"version": "4.48.0",
|
| 997 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.48.0.tgz",
|
| 998 |
+
"integrity": "sha512-ao58Adz/v14MWpQgYAb4a4h3fdw73DrDGtaiF7Opds5wNyEQwtO6M9dBh89nke0yoZzzaegq6J/EXs7eBebG8A==",
|
| 999 |
+
"cpu": [
|
| 1000 |
+
"x64"
|
| 1001 |
+
],
|
| 1002 |
+
"dev": true,
|
| 1003 |
+
"license": "MIT",
|
| 1004 |
+
"optional": true,
|
| 1005 |
+
"os": [
|
| 1006 |
+
"linux"
|
| 1007 |
+
]
|
| 1008 |
+
},
|
| 1009 |
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 1010 |
+
"version": "4.48.0",
|
| 1011 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.48.0.tgz",
|
| 1012 |
+
"integrity": "sha512-kpFno46bHtjZVdRIOxqaGeiABiToo2J+st7Yce+aiAoo1H0xPi2keyQIP04n2JjDVuxBN6bSz9R6RdTK5hIppw==",
|
| 1013 |
+
"cpu": [
|
| 1014 |
+
"arm64"
|
| 1015 |
+
],
|
| 1016 |
+
"dev": true,
|
| 1017 |
+
"license": "MIT",
|
| 1018 |
+
"optional": true,
|
| 1019 |
+
"os": [
|
| 1020 |
+
"win32"
|
| 1021 |
+
]
|
| 1022 |
+
},
|
| 1023 |
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 1024 |
+
"version": "4.48.0",
|
| 1025 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.48.0.tgz",
|
| 1026 |
+
"integrity": "sha512-rFYrk4lLk9YUTIeihnQMiwMr6gDhGGSbWThPEDfBoU/HdAtOzPXeexKi7yU8jO+LWRKnmqPN9NviHQf6GDwBcQ==",
|
| 1027 |
+
"cpu": [
|
| 1028 |
+
"ia32"
|
| 1029 |
+
],
|
| 1030 |
+
"dev": true,
|
| 1031 |
+
"license": "MIT",
|
| 1032 |
+
"optional": true,
|
| 1033 |
+
"os": [
|
| 1034 |
+
"win32"
|
| 1035 |
+
]
|
| 1036 |
+
},
|
| 1037 |
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 1038 |
+
"version": "4.48.0",
|
| 1039 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.48.0.tgz",
|
| 1040 |
+
"integrity": "sha512-sq0hHLTgdtwOPDB5SJOuaoHyiP1qSwg+71TQWk8iDS04bW1wIE0oQ6otPiRj2ZvLYNASLMaTp8QRGUVZ+5OL5A==",
|
| 1041 |
+
"cpu": [
|
| 1042 |
+
"x64"
|
| 1043 |
+
],
|
| 1044 |
+
"dev": true,
|
| 1045 |
+
"license": "MIT",
|
| 1046 |
+
"optional": true,
|
| 1047 |
+
"os": [
|
| 1048 |
+
"win32"
|
| 1049 |
+
]
|
| 1050 |
+
},
|
| 1051 |
+
"node_modules/@types/babel__core": {
|
| 1052 |
+
"version": "7.20.5",
|
| 1053 |
+
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
| 1054 |
+
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
| 1055 |
+
"dev": true,
|
| 1056 |
+
"license": "MIT",
|
| 1057 |
+
"dependencies": {
|
| 1058 |
+
"@babel/parser": "^7.20.7",
|
| 1059 |
+
"@babel/types": "^7.20.7",
|
| 1060 |
+
"@types/babel__generator": "*",
|
| 1061 |
+
"@types/babel__template": "*",
|
| 1062 |
+
"@types/babel__traverse": "*"
|
| 1063 |
+
}
|
| 1064 |
+
},
|
| 1065 |
+
"node_modules/@types/babel__generator": {
|
| 1066 |
+
"version": "7.27.0",
|
| 1067 |
+
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
| 1068 |
+
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
| 1069 |
+
"dev": true,
|
| 1070 |
+
"license": "MIT",
|
| 1071 |
+
"dependencies": {
|
| 1072 |
+
"@babel/types": "^7.0.0"
|
| 1073 |
+
}
|
| 1074 |
+
},
|
| 1075 |
+
"node_modules/@types/babel__template": {
|
| 1076 |
+
"version": "7.4.4",
|
| 1077 |
+
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
| 1078 |
+
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
| 1079 |
+
"dev": true,
|
| 1080 |
+
"license": "MIT",
|
| 1081 |
+
"dependencies": {
|
| 1082 |
+
"@babel/parser": "^7.1.0",
|
| 1083 |
+
"@babel/types": "^7.0.0"
|
| 1084 |
+
}
|
| 1085 |
+
},
|
| 1086 |
+
"node_modules/@types/babel__traverse": {
|
| 1087 |
+
"version": "7.28.0",
|
| 1088 |
+
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
| 1089 |
+
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
| 1090 |
+
"dev": true,
|
| 1091 |
+
"license": "MIT",
|
| 1092 |
+
"dependencies": {
|
| 1093 |
+
"@babel/types": "^7.28.2"
|
| 1094 |
+
}
|
| 1095 |
+
},
|
| 1096 |
+
"node_modules/@types/estree": {
|
| 1097 |
+
"version": "1.0.8",
|
| 1098 |
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 1099 |
+
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 1100 |
+
"dev": true,
|
| 1101 |
+
"license": "MIT"
|
| 1102 |
+
},
|
| 1103 |
+
"node_modules/@types/geojson": {
|
| 1104 |
+
"version": "7946.0.16",
|
| 1105 |
+
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
| 1106 |
+
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
| 1107 |
+
"dev": true,
|
| 1108 |
+
"license": "MIT"
|
| 1109 |
+
},
|
| 1110 |
+
"node_modules/@types/google.maps": {
|
| 1111 |
+
"version": "3.58.1",
|
| 1112 |
+
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz",
|
| 1113 |
+
"integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==",
|
| 1114 |
+
"license": "MIT"
|
| 1115 |
+
},
|
| 1116 |
+
"node_modules/@types/leaflet": {
|
| 1117 |
+
"version": "1.9.20",
|
| 1118 |
+
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.20.tgz",
|
| 1119 |
+
"integrity": "sha512-rooalPMlk61LCaLOvBF2VIf9M47HgMQqi5xQ9QRi7c8PkdIe0WrIi5IxXUXQjAdL0c+vcQ01mYWbthzmp9GHWw==",
|
| 1120 |
+
"dev": true,
|
| 1121 |
+
"license": "MIT",
|
| 1122 |
+
"dependencies": {
|
| 1123 |
+
"@types/geojson": "*"
|
| 1124 |
+
}
|
| 1125 |
+
},
|
| 1126 |
+
"node_modules/@types/prop-types": {
|
| 1127 |
+
"version": "15.7.15",
|
| 1128 |
+
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
| 1129 |
+
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
| 1130 |
+
"dev": true,
|
| 1131 |
+
"license": "MIT"
|
| 1132 |
+
},
|
| 1133 |
+
"node_modules/@types/react": {
|
| 1134 |
+
"version": "18.3.24",
|
| 1135 |
+
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
|
| 1136 |
+
"integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==",
|
| 1137 |
+
"dev": true,
|
| 1138 |
+
"license": "MIT",
|
| 1139 |
+
"dependencies": {
|
| 1140 |
+
"@types/prop-types": "*",
|
| 1141 |
+
"csstype": "^3.0.2"
|
| 1142 |
+
}
|
| 1143 |
+
},
|
| 1144 |
+
"node_modules/@types/react-dom": {
|
| 1145 |
+
"version": "18.3.7",
|
| 1146 |
+
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
| 1147 |
+
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
| 1148 |
+
"dev": true,
|
| 1149 |
+
"license": "MIT",
|
| 1150 |
+
"peerDependencies": {
|
| 1151 |
+
"@types/react": "^18.0.0"
|
| 1152 |
+
}
|
| 1153 |
+
},
|
| 1154 |
+
"node_modules/@vis.gl/react-google-maps": {
|
| 1155 |
+
"version": "1.5.5",
|
| 1156 |
+
"resolved": "https://registry.npmjs.org/@vis.gl/react-google-maps/-/react-google-maps-1.5.5.tgz",
|
| 1157 |
+
"integrity": "sha512-LgHtK1AtE2/BN4dPoK05oWu0jWmeDdyX0Ffqi+mZc+M4apaHn2sUxxKXAxhPF90O9vcsiou/ntm6/XBWX+gpqw==",
|
| 1158 |
+
"license": "MIT",
|
| 1159 |
+
"dependencies": {
|
| 1160 |
+
"@types/google.maps": "^3.54.10",
|
| 1161 |
+
"fast-deep-equal": "^3.1.3"
|
| 1162 |
+
},
|
| 1163 |
+
"peerDependencies": {
|
| 1164 |
+
"react": ">=16.8.0 || ^19.0 || ^19.0.0-rc",
|
| 1165 |
+
"react-dom": ">=16.8.0 || ^19.0 || ^19.0.0-rc"
|
| 1166 |
+
}
|
| 1167 |
+
},
|
| 1168 |
+
"node_modules/@vitejs/plugin-react": {
|
| 1169 |
+
"version": "5.0.1",
|
| 1170 |
+
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.1.tgz",
|
| 1171 |
+
"integrity": "sha512-DE4UNaBXwtVoDJ0ccBdLVjFTWL70NRuWNCxEieTI3lrq9ORB9aOCQEKstwDXBl87NvFdbqh/p7eINGyj0BthJA==",
|
| 1172 |
+
"dev": true,
|
| 1173 |
+
"license": "MIT",
|
| 1174 |
+
"dependencies": {
|
| 1175 |
+
"@babel/core": "^7.28.3",
|
| 1176 |
+
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
| 1177 |
+
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
| 1178 |
+
"@rolldown/pluginutils": "1.0.0-beta.32",
|
| 1179 |
+
"@types/babel__core": "^7.20.5",
|
| 1180 |
+
"react-refresh": "^0.17.0"
|
| 1181 |
+
},
|
| 1182 |
+
"engines": {
|
| 1183 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1184 |
+
},
|
| 1185 |
+
"peerDependencies": {
|
| 1186 |
+
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
| 1187 |
+
}
|
| 1188 |
+
},
|
| 1189 |
+
"node_modules/browserslist": {
|
| 1190 |
+
"version": "4.25.3",
|
| 1191 |
+
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz",
|
| 1192 |
+
"integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==",
|
| 1193 |
+
"dev": true,
|
| 1194 |
+
"funding": [
|
| 1195 |
+
{
|
| 1196 |
+
"type": "opencollective",
|
| 1197 |
+
"url": "https://opencollective.com/browserslist"
|
| 1198 |
+
},
|
| 1199 |
+
{
|
| 1200 |
+
"type": "tidelift",
|
| 1201 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1202 |
+
},
|
| 1203 |
+
{
|
| 1204 |
+
"type": "github",
|
| 1205 |
+
"url": "https://github.com/sponsors/ai"
|
| 1206 |
+
}
|
| 1207 |
+
],
|
| 1208 |
+
"license": "MIT",
|
| 1209 |
+
"dependencies": {
|
| 1210 |
+
"caniuse-lite": "^1.0.30001735",
|
| 1211 |
+
"electron-to-chromium": "^1.5.204",
|
| 1212 |
+
"node-releases": "^2.0.19",
|
| 1213 |
+
"update-browserslist-db": "^1.1.3"
|
| 1214 |
+
},
|
| 1215 |
+
"bin": {
|
| 1216 |
+
"browserslist": "cli.js"
|
| 1217 |
+
},
|
| 1218 |
+
"engines": {
|
| 1219 |
+
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 1220 |
+
}
|
| 1221 |
+
},
|
| 1222 |
+
"node_modules/caniuse-lite": {
|
| 1223 |
+
"version": "1.0.30001737",
|
| 1224 |
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz",
|
| 1225 |
+
"integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==",
|
| 1226 |
+
"dev": true,
|
| 1227 |
+
"funding": [
|
| 1228 |
+
{
|
| 1229 |
+
"type": "opencollective",
|
| 1230 |
+
"url": "https://opencollective.com/browserslist"
|
| 1231 |
+
},
|
| 1232 |
+
{
|
| 1233 |
+
"type": "tidelift",
|
| 1234 |
+
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
| 1235 |
+
},
|
| 1236 |
+
{
|
| 1237 |
+
"type": "github",
|
| 1238 |
+
"url": "https://github.com/sponsors/ai"
|
| 1239 |
+
}
|
| 1240 |
+
],
|
| 1241 |
+
"license": "CC-BY-4.0"
|
| 1242 |
+
},
|
| 1243 |
+
"node_modules/convert-source-map": {
|
| 1244 |
+
"version": "2.0.0",
|
| 1245 |
+
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
| 1246 |
+
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
| 1247 |
+
"dev": true,
|
| 1248 |
+
"license": "MIT"
|
| 1249 |
+
},
|
| 1250 |
+
"node_modules/csstype": {
|
| 1251 |
+
"version": "3.1.3",
|
| 1252 |
+
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
| 1253 |
+
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
| 1254 |
+
"dev": true,
|
| 1255 |
+
"license": "MIT"
|
| 1256 |
+
},
|
| 1257 |
+
"node_modules/debug": {
|
| 1258 |
+
"version": "4.4.1",
|
| 1259 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
| 1260 |
+
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
| 1261 |
+
"dev": true,
|
| 1262 |
+
"license": "MIT",
|
| 1263 |
+
"dependencies": {
|
| 1264 |
+
"ms": "^2.1.3"
|
| 1265 |
+
},
|
| 1266 |
+
"engines": {
|
| 1267 |
+
"node": ">=6.0"
|
| 1268 |
+
},
|
| 1269 |
+
"peerDependenciesMeta": {
|
| 1270 |
+
"supports-color": {
|
| 1271 |
+
"optional": true
|
| 1272 |
+
}
|
| 1273 |
+
}
|
| 1274 |
+
},
|
| 1275 |
+
"node_modules/electron-to-chromium": {
|
| 1276 |
+
"version": "1.5.208",
|
| 1277 |
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.208.tgz",
|
| 1278 |
+
"integrity": "sha512-ozZyibehoe7tOhNaf16lKmljVf+3npZcJIEbJRVftVsmAg5TeA1mGS9dVCZzOwr2xT7xK15V0p7+GZqSPgkuPg==",
|
| 1279 |
+
"dev": true,
|
| 1280 |
+
"license": "ISC"
|
| 1281 |
+
},
|
| 1282 |
+
"node_modules/esbuild": {
|
| 1283 |
+
"version": "0.21.5",
|
| 1284 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
| 1285 |
+
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
| 1286 |
+
"dev": true,
|
| 1287 |
+
"hasInstallScript": true,
|
| 1288 |
+
"license": "MIT",
|
| 1289 |
+
"bin": {
|
| 1290 |
+
"esbuild": "bin/esbuild"
|
| 1291 |
+
},
|
| 1292 |
+
"engines": {
|
| 1293 |
+
"node": ">=12"
|
| 1294 |
+
},
|
| 1295 |
+
"optionalDependencies": {
|
| 1296 |
+
"@esbuild/aix-ppc64": "0.21.5",
|
| 1297 |
+
"@esbuild/android-arm": "0.21.5",
|
| 1298 |
+
"@esbuild/android-arm64": "0.21.5",
|
| 1299 |
+
"@esbuild/android-x64": "0.21.5",
|
| 1300 |
+
"@esbuild/darwin-arm64": "0.21.5",
|
| 1301 |
+
"@esbuild/darwin-x64": "0.21.5",
|
| 1302 |
+
"@esbuild/freebsd-arm64": "0.21.5",
|
| 1303 |
+
"@esbuild/freebsd-x64": "0.21.5",
|
| 1304 |
+
"@esbuild/linux-arm": "0.21.5",
|
| 1305 |
+
"@esbuild/linux-arm64": "0.21.5",
|
| 1306 |
+
"@esbuild/linux-ia32": "0.21.5",
|
| 1307 |
+
"@esbuild/linux-loong64": "0.21.5",
|
| 1308 |
+
"@esbuild/linux-mips64el": "0.21.5",
|
| 1309 |
+
"@esbuild/linux-ppc64": "0.21.5",
|
| 1310 |
+
"@esbuild/linux-riscv64": "0.21.5",
|
| 1311 |
+
"@esbuild/linux-s390x": "0.21.5",
|
| 1312 |
+
"@esbuild/linux-x64": "0.21.5",
|
| 1313 |
+
"@esbuild/netbsd-x64": "0.21.5",
|
| 1314 |
+
"@esbuild/openbsd-x64": "0.21.5",
|
| 1315 |
+
"@esbuild/sunos-x64": "0.21.5",
|
| 1316 |
+
"@esbuild/win32-arm64": "0.21.5",
|
| 1317 |
+
"@esbuild/win32-ia32": "0.21.5",
|
| 1318 |
+
"@esbuild/win32-x64": "0.21.5"
|
| 1319 |
+
}
|
| 1320 |
+
},
|
| 1321 |
+
"node_modules/escalade": {
|
| 1322 |
+
"version": "3.2.0",
|
| 1323 |
+
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
| 1324 |
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
| 1325 |
+
"dev": true,
|
| 1326 |
+
"license": "MIT",
|
| 1327 |
+
"engines": {
|
| 1328 |
+
"node": ">=6"
|
| 1329 |
+
}
|
| 1330 |
+
},
|
| 1331 |
+
"node_modules/fast-deep-equal": {
|
| 1332 |
+
"version": "3.1.3",
|
| 1333 |
+
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
| 1334 |
+
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
| 1335 |
+
"license": "MIT"
|
| 1336 |
+
},
|
| 1337 |
+
"node_modules/fsevents": {
|
| 1338 |
+
"version": "2.3.3",
|
| 1339 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 1340 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1341 |
+
"dev": true,
|
| 1342 |
+
"hasInstallScript": true,
|
| 1343 |
+
"license": "MIT",
|
| 1344 |
+
"optional": true,
|
| 1345 |
+
"os": [
|
| 1346 |
+
"darwin"
|
| 1347 |
+
],
|
| 1348 |
+
"engines": {
|
| 1349 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1350 |
+
}
|
| 1351 |
+
},
|
| 1352 |
+
"node_modules/gensync": {
|
| 1353 |
+
"version": "1.0.0-beta.2",
|
| 1354 |
+
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
| 1355 |
+
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
| 1356 |
+
"dev": true,
|
| 1357 |
+
"license": "MIT",
|
| 1358 |
+
"engines": {
|
| 1359 |
+
"node": ">=6.9.0"
|
| 1360 |
+
}
|
| 1361 |
+
},
|
| 1362 |
+
"node_modules/js-tokens": {
|
| 1363 |
+
"version": "4.0.0",
|
| 1364 |
+
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
| 1365 |
+
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
| 1366 |
+
"license": "MIT"
|
| 1367 |
+
},
|
| 1368 |
+
"node_modules/jsesc": {
|
| 1369 |
+
"version": "3.1.0",
|
| 1370 |
+
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
| 1371 |
+
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
| 1372 |
+
"dev": true,
|
| 1373 |
+
"license": "MIT",
|
| 1374 |
+
"bin": {
|
| 1375 |
+
"jsesc": "bin/jsesc"
|
| 1376 |
+
},
|
| 1377 |
+
"engines": {
|
| 1378 |
+
"node": ">=6"
|
| 1379 |
+
}
|
| 1380 |
+
},
|
| 1381 |
+
"node_modules/json5": {
|
| 1382 |
+
"version": "2.2.3",
|
| 1383 |
+
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
| 1384 |
+
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
| 1385 |
+
"dev": true,
|
| 1386 |
+
"license": "MIT",
|
| 1387 |
+
"bin": {
|
| 1388 |
+
"json5": "lib/cli.js"
|
| 1389 |
+
},
|
| 1390 |
+
"engines": {
|
| 1391 |
+
"node": ">=6"
|
| 1392 |
+
}
|
| 1393 |
+
},
|
| 1394 |
+
"node_modules/leaflet": {
|
| 1395 |
+
"version": "1.9.4",
|
| 1396 |
+
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
| 1397 |
+
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
| 1398 |
+
"license": "BSD-2-Clause"
|
| 1399 |
+
},
|
| 1400 |
+
"node_modules/loose-envify": {
|
| 1401 |
+
"version": "1.4.0",
|
| 1402 |
+
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
| 1403 |
+
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
| 1404 |
+
"license": "MIT",
|
| 1405 |
+
"dependencies": {
|
| 1406 |
+
"js-tokens": "^3.0.0 || ^4.0.0"
|
| 1407 |
+
},
|
| 1408 |
+
"bin": {
|
| 1409 |
+
"loose-envify": "cli.js"
|
| 1410 |
+
}
|
| 1411 |
+
},
|
| 1412 |
+
"node_modules/lru-cache": {
|
| 1413 |
+
"version": "5.1.1",
|
| 1414 |
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
| 1415 |
+
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
| 1416 |
+
"dev": true,
|
| 1417 |
+
"license": "ISC",
|
| 1418 |
+
"dependencies": {
|
| 1419 |
+
"yallist": "^3.0.2"
|
| 1420 |
+
}
|
| 1421 |
+
},
|
| 1422 |
+
"node_modules/lucide-react": {
|
| 1423 |
+
"version": "0.542.0",
|
| 1424 |
+
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.542.0.tgz",
|
| 1425 |
+
"integrity": "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==",
|
| 1426 |
+
"license": "ISC",
|
| 1427 |
+
"peerDependencies": {
|
| 1428 |
+
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 1429 |
+
}
|
| 1430 |
+
},
|
| 1431 |
+
"node_modules/ms": {
|
| 1432 |
+
"version": "2.1.3",
|
| 1433 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 1434 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 1435 |
+
"dev": true,
|
| 1436 |
+
"license": "MIT"
|
| 1437 |
+
},
|
| 1438 |
+
"node_modules/nanoid": {
|
| 1439 |
+
"version": "3.3.11",
|
| 1440 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
| 1441 |
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
| 1442 |
+
"dev": true,
|
| 1443 |
+
"funding": [
|
| 1444 |
+
{
|
| 1445 |
+
"type": "github",
|
| 1446 |
+
"url": "https://github.com/sponsors/ai"
|
| 1447 |
+
}
|
| 1448 |
+
],
|
| 1449 |
+
"license": "MIT",
|
| 1450 |
+
"bin": {
|
| 1451 |
+
"nanoid": "bin/nanoid.cjs"
|
| 1452 |
+
},
|
| 1453 |
+
"engines": {
|
| 1454 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 1455 |
+
}
|
| 1456 |
+
},
|
| 1457 |
+
"node_modules/node-releases": {
|
| 1458 |
+
"version": "2.0.19",
|
| 1459 |
+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
| 1460 |
+
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
| 1461 |
+
"dev": true,
|
| 1462 |
+
"license": "MIT"
|
| 1463 |
+
},
|
| 1464 |
+
"node_modules/picocolors": {
|
| 1465 |
+
"version": "1.1.1",
|
| 1466 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 1467 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 1468 |
+
"dev": true,
|
| 1469 |
+
"license": "ISC"
|
| 1470 |
+
},
|
| 1471 |
+
"node_modules/postcss": {
|
| 1472 |
+
"version": "8.5.6",
|
| 1473 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
| 1474 |
+
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
| 1475 |
+
"dev": true,
|
| 1476 |
+
"funding": [
|
| 1477 |
+
{
|
| 1478 |
+
"type": "opencollective",
|
| 1479 |
+
"url": "https://opencollective.com/postcss/"
|
| 1480 |
+
},
|
| 1481 |
+
{
|
| 1482 |
+
"type": "tidelift",
|
| 1483 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 1484 |
+
},
|
| 1485 |
+
{
|
| 1486 |
+
"type": "github",
|
| 1487 |
+
"url": "https://github.com/sponsors/ai"
|
| 1488 |
+
}
|
| 1489 |
+
],
|
| 1490 |
+
"license": "MIT",
|
| 1491 |
+
"dependencies": {
|
| 1492 |
+
"nanoid": "^3.3.11",
|
| 1493 |
+
"picocolors": "^1.1.1",
|
| 1494 |
+
"source-map-js": "^1.2.1"
|
| 1495 |
+
},
|
| 1496 |
+
"engines": {
|
| 1497 |
+
"node": "^10 || ^12 || >=14"
|
| 1498 |
+
}
|
| 1499 |
+
},
|
| 1500 |
+
"node_modules/react": {
|
| 1501 |
+
"version": "18.3.1",
|
| 1502 |
+
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
| 1503 |
+
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
| 1504 |
+
"license": "MIT",
|
| 1505 |
+
"dependencies": {
|
| 1506 |
+
"loose-envify": "^1.1.0"
|
| 1507 |
+
},
|
| 1508 |
+
"engines": {
|
| 1509 |
+
"node": ">=0.10.0"
|
| 1510 |
+
}
|
| 1511 |
+
},
|
| 1512 |
+
"node_modules/react-dom": {
|
| 1513 |
+
"version": "18.3.1",
|
| 1514 |
+
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
| 1515 |
+
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
| 1516 |
+
"license": "MIT",
|
| 1517 |
+
"dependencies": {
|
| 1518 |
+
"loose-envify": "^1.1.0",
|
| 1519 |
+
"scheduler": "^0.23.2"
|
| 1520 |
+
},
|
| 1521 |
+
"peerDependencies": {
|
| 1522 |
+
"react": "^18.3.1"
|
| 1523 |
+
}
|
| 1524 |
+
},
|
| 1525 |
+
"node_modules/react-leaflet": {
|
| 1526 |
+
"version": "4.2.1",
|
| 1527 |
+
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz",
|
| 1528 |
+
"integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==",
|
| 1529 |
+
"license": "Hippocratic-2.1",
|
| 1530 |
+
"dependencies": {
|
| 1531 |
+
"@react-leaflet/core": "^2.1.0"
|
| 1532 |
+
},
|
| 1533 |
+
"peerDependencies": {
|
| 1534 |
+
"leaflet": "^1.9.0",
|
| 1535 |
+
"react": "^18.0.0",
|
| 1536 |
+
"react-dom": "^18.0.0"
|
| 1537 |
+
}
|
| 1538 |
+
},
|
| 1539 |
+
"node_modules/react-refresh": {
|
| 1540 |
+
"version": "0.17.0",
|
| 1541 |
+
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
| 1542 |
+
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
| 1543 |
+
"dev": true,
|
| 1544 |
+
"license": "MIT",
|
| 1545 |
+
"engines": {
|
| 1546 |
+
"node": ">=0.10.0"
|
| 1547 |
+
}
|
| 1548 |
+
},
|
| 1549 |
+
"node_modules/rollup": {
|
| 1550 |
+
"version": "4.48.0",
|
| 1551 |
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.48.0.tgz",
|
| 1552 |
+
"integrity": "sha512-BXHRqK1vyt9XVSEHZ9y7xdYtuYbwVod2mLwOMFP7t/Eqoc1pHRlG/WdV2qNeNvZHRQdLedaFycljaYYM96RqJQ==",
|
| 1553 |
+
"dev": true,
|
| 1554 |
+
"license": "MIT",
|
| 1555 |
+
"dependencies": {
|
| 1556 |
+
"@types/estree": "1.0.8"
|
| 1557 |
+
},
|
| 1558 |
+
"bin": {
|
| 1559 |
+
"rollup": "dist/bin/rollup"
|
| 1560 |
+
},
|
| 1561 |
+
"engines": {
|
| 1562 |
+
"node": ">=18.0.0",
|
| 1563 |
+
"npm": ">=8.0.0"
|
| 1564 |
+
},
|
| 1565 |
+
"optionalDependencies": {
|
| 1566 |
+
"@rollup/rollup-android-arm-eabi": "4.48.0",
|
| 1567 |
+
"@rollup/rollup-android-arm64": "4.48.0",
|
| 1568 |
+
"@rollup/rollup-darwin-arm64": "4.48.0",
|
| 1569 |
+
"@rollup/rollup-darwin-x64": "4.48.0",
|
| 1570 |
+
"@rollup/rollup-freebsd-arm64": "4.48.0",
|
| 1571 |
+
"@rollup/rollup-freebsd-x64": "4.48.0",
|
| 1572 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.48.0",
|
| 1573 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.48.0",
|
| 1574 |
+
"@rollup/rollup-linux-arm64-gnu": "4.48.0",
|
| 1575 |
+
"@rollup/rollup-linux-arm64-musl": "4.48.0",
|
| 1576 |
+
"@rollup/rollup-linux-loongarch64-gnu": "4.48.0",
|
| 1577 |
+
"@rollup/rollup-linux-ppc64-gnu": "4.48.0",
|
| 1578 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.48.0",
|
| 1579 |
+
"@rollup/rollup-linux-riscv64-musl": "4.48.0",
|
| 1580 |
+
"@rollup/rollup-linux-s390x-gnu": "4.48.0",
|
| 1581 |
+
"@rollup/rollup-linux-x64-gnu": "4.48.0",
|
| 1582 |
+
"@rollup/rollup-linux-x64-musl": "4.48.0",
|
| 1583 |
+
"@rollup/rollup-win32-arm64-msvc": "4.48.0",
|
| 1584 |
+
"@rollup/rollup-win32-ia32-msvc": "4.48.0",
|
| 1585 |
+
"@rollup/rollup-win32-x64-msvc": "4.48.0",
|
| 1586 |
+
"fsevents": "~2.3.2"
|
| 1587 |
+
}
|
| 1588 |
+
},
|
| 1589 |
+
"node_modules/scheduler": {
|
| 1590 |
+
"version": "0.23.2",
|
| 1591 |
+
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
|
| 1592 |
+
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
| 1593 |
+
"license": "MIT",
|
| 1594 |
+
"dependencies": {
|
| 1595 |
+
"loose-envify": "^1.1.0"
|
| 1596 |
+
}
|
| 1597 |
+
},
|
| 1598 |
+
"node_modules/semver": {
|
| 1599 |
+
"version": "6.3.1",
|
| 1600 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
| 1601 |
+
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
| 1602 |
+
"dev": true,
|
| 1603 |
+
"license": "ISC",
|
| 1604 |
+
"bin": {
|
| 1605 |
+
"semver": "bin/semver.js"
|
| 1606 |
+
}
|
| 1607 |
+
},
|
| 1608 |
+
"node_modules/source-map-js": {
|
| 1609 |
+
"version": "1.2.1",
|
| 1610 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 1611 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 1612 |
+
"dev": true,
|
| 1613 |
+
"license": "BSD-3-Clause",
|
| 1614 |
+
"engines": {
|
| 1615 |
+
"node": ">=0.10.0"
|
| 1616 |
+
}
|
| 1617 |
+
},
|
| 1618 |
+
"node_modules/typescript": {
|
| 1619 |
+
"version": "5.9.2",
|
| 1620 |
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
| 1621 |
+
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
| 1622 |
+
"dev": true,
|
| 1623 |
+
"license": "Apache-2.0",
|
| 1624 |
+
"bin": {
|
| 1625 |
+
"tsc": "bin/tsc",
|
| 1626 |
+
"tsserver": "bin/tsserver"
|
| 1627 |
+
},
|
| 1628 |
+
"engines": {
|
| 1629 |
+
"node": ">=14.17"
|
| 1630 |
+
}
|
| 1631 |
+
},
|
| 1632 |
+
"node_modules/update-browserslist-db": {
|
| 1633 |
+
"version": "1.1.3",
|
| 1634 |
+
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
| 1635 |
+
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
| 1636 |
+
"dev": true,
|
| 1637 |
+
"funding": [
|
| 1638 |
+
{
|
| 1639 |
+
"type": "opencollective",
|
| 1640 |
+
"url": "https://opencollective.com/browserslist"
|
| 1641 |
+
},
|
| 1642 |
+
{
|
| 1643 |
+
"type": "tidelift",
|
| 1644 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1645 |
+
},
|
| 1646 |
+
{
|
| 1647 |
+
"type": "github",
|
| 1648 |
+
"url": "https://github.com/sponsors/ai"
|
| 1649 |
+
}
|
| 1650 |
+
],
|
| 1651 |
+
"license": "MIT",
|
| 1652 |
+
"dependencies": {
|
| 1653 |
+
"escalade": "^3.2.0",
|
| 1654 |
+
"picocolors": "^1.1.1"
|
| 1655 |
+
},
|
| 1656 |
+
"bin": {
|
| 1657 |
+
"update-browserslist-db": "cli.js"
|
| 1658 |
+
},
|
| 1659 |
+
"peerDependencies": {
|
| 1660 |
+
"browserslist": ">= 4.21.0"
|
| 1661 |
+
}
|
| 1662 |
+
},
|
| 1663 |
+
"node_modules/vite": {
|
| 1664 |
+
"version": "5.4.19",
|
| 1665 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
|
| 1666 |
+
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
|
| 1667 |
+
"dev": true,
|
| 1668 |
+
"license": "MIT",
|
| 1669 |
+
"dependencies": {
|
| 1670 |
+
"esbuild": "^0.21.3",
|
| 1671 |
+
"postcss": "^8.4.43",
|
| 1672 |
+
"rollup": "^4.20.0"
|
| 1673 |
+
},
|
| 1674 |
+
"bin": {
|
| 1675 |
+
"vite": "bin/vite.js"
|
| 1676 |
+
},
|
| 1677 |
+
"engines": {
|
| 1678 |
+
"node": "^18.0.0 || >=20.0.0"
|
| 1679 |
+
},
|
| 1680 |
+
"funding": {
|
| 1681 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 1682 |
+
},
|
| 1683 |
+
"optionalDependencies": {
|
| 1684 |
+
"fsevents": "~2.3.3"
|
| 1685 |
+
},
|
| 1686 |
+
"peerDependencies": {
|
| 1687 |
+
"@types/node": "^18.0.0 || >=20.0.0",
|
| 1688 |
+
"less": "*",
|
| 1689 |
+
"lightningcss": "^1.21.0",
|
| 1690 |
+
"sass": "*",
|
| 1691 |
+
"sass-embedded": "*",
|
| 1692 |
+
"stylus": "*",
|
| 1693 |
+
"sugarss": "*",
|
| 1694 |
+
"terser": "^5.4.0"
|
| 1695 |
+
},
|
| 1696 |
+
"peerDependenciesMeta": {
|
| 1697 |
+
"@types/node": {
|
| 1698 |
+
"optional": true
|
| 1699 |
+
},
|
| 1700 |
+
"less": {
|
| 1701 |
+
"optional": true
|
| 1702 |
+
},
|
| 1703 |
+
"lightningcss": {
|
| 1704 |
+
"optional": true
|
| 1705 |
+
},
|
| 1706 |
+
"sass": {
|
| 1707 |
+
"optional": true
|
| 1708 |
+
},
|
| 1709 |
+
"sass-embedded": {
|
| 1710 |
+
"optional": true
|
| 1711 |
+
},
|
| 1712 |
+
"stylus": {
|
| 1713 |
+
"optional": true
|
| 1714 |
+
},
|
| 1715 |
+
"sugarss": {
|
| 1716 |
+
"optional": true
|
| 1717 |
+
},
|
| 1718 |
+
"terser": {
|
| 1719 |
+
"optional": true
|
| 1720 |
+
}
|
| 1721 |
+
}
|
| 1722 |
+
},
|
| 1723 |
+
"node_modules/yallist": {
|
| 1724 |
+
"version": "3.1.1",
|
| 1725 |
+
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
| 1726 |
+
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
| 1727 |
+
"dev": true,
|
| 1728 |
+
"license": "ISC"
|
| 1729 |
+
}
|
| 1730 |
+
}
|
| 1731 |
+
}
|
web/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "cta-web",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.1",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview --port 5174"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"@vis.gl/react-google-maps": "^1.5.5",
|
| 13 |
+
"leaflet": "^1.9.4",
|
| 14 |
+
"lucide-react": "^0.542.0",
|
| 15 |
+
"react": "^18.3.1",
|
| 16 |
+
"react-dom": "^18.3.1",
|
| 17 |
+
"react-leaflet": "^4.2.1"
|
| 18 |
+
},
|
| 19 |
+
"devDependencies": {
|
| 20 |
+
"@types/leaflet": "^1.9.20",
|
| 21 |
+
"@types/react": "^18.3.24",
|
| 22 |
+
"@types/react-dom": "^18.3.7",
|
| 23 |
+
"@vitejs/plugin-react": "^5.0.1",
|
| 24 |
+
"typescript": "^5.9.2",
|
| 25 |
+
"vite": "^5.4.19"
|
| 26 |
+
}
|
| 27 |
+
}
|
web/public/icons/3d/3d-alert.png
ADDED
|
|
web/public/icons/3d/3d-ambulance.png
ADDED
|
|
web/public/icons/3d/3d-car.png
ADDED
|
|
web/public/icons/3d/3d-construction.png
ADDED
|
|
web/public/icons/3d/3d-flood.png
ADDED
|
|
web/public/icons/3d/3d-gun.png
ADDED
|
|
web/public/icons/3d/3d-help.png
ADDED
|
|
web/public/icons/3d/3d-info.png
ADDED
|
|
web/public/icons/3d/3d-ride.png
ADDED
|
|
web/public/icons/3d/3d-robbery.png
ADDED
|
|
web/public/icons/3d/3d-search.png
ADDED
|
|
web/public/icons/3d/3d-sex.png
ADDED
|
|
web/public/icons/3d/3d-traffic.png
ADDED
|
|
web/public/icons/3d/3d-user_search.png
ADDED
|
|
web/public/vite.svg
ADDED
|
|
web/src/App.css
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#root {
|
| 2 |
+
max-width: 1280px;
|
| 3 |
+
margin: 0 auto;
|
| 4 |
+
padding: 2rem;
|
| 5 |
+
text-align: center;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
.logo {
|
| 9 |
+
height: 6em;
|
| 10 |
+
padding: 1.5em;
|
| 11 |
+
will-change: filter;
|
| 12 |
+
transition: filter 300ms;
|
| 13 |
+
}
|
| 14 |
+
.logo:hover {
|
| 15 |
+
filter: drop-shadow(0 0 2em #646cffaa);
|
| 16 |
+
}
|
| 17 |
+
.logo.react:hover {
|
| 18 |
+
filter: drop-shadow(0 0 2em #61dafbaa);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
@keyframes logo-spin {
|
| 22 |
+
from {
|
| 23 |
+
transform: rotate(0deg);
|
| 24 |
+
}
|
| 25 |
+
to {
|
| 26 |
+
transform: rotate(360deg);
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
@media (prefers-reduced-motion: no-preference) {
|
| 31 |
+
a:nth-of-type(2) .logo {
|
| 32 |
+
animation: logo-spin infinite 20s linear;
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.card {
|
| 37 |
+
padding: 2em;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.read-the-docs {
|
| 41 |
+
color: #888;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.sidebar {
|
| 45 |
+
display: flex;
|
| 46 |
+
flex-direction: column;
|
| 47 |
+
min-height: 0;
|
| 48 |
+
}
|
web/src/App.tsx
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
import "./style.css";
|
| 3 |
+
import type { FC, SelectMeta } from "./lib/types";
|
| 4 |
+
import { REPORTS_URL } from "./lib/constants";
|
| 5 |
+
import { useFeeds } from "./hooks/useFeeds";
|
| 6 |
+
import { useSessionId } from "./hooks/useSessionId";
|
| 7 |
+
import { useUpdates } from "./hooks/useUpdates";
|
| 8 |
+
import { useChat } from "./hooks/useChat";
|
| 9 |
+
import MapCanvas from "./components/map/MapCanvas";
|
| 10 |
+
import SelectedLocationCard from "./components/sidebar/SelectedLocationCard";
|
| 11 |
+
import UpdatesPanel from "./components/sidebar/UpdatesPanel";
|
| 12 |
+
import ChatPanel from "./components/chat/ChatPanel";
|
| 13 |
+
|
| 14 |
+
export default function App() {
|
| 15 |
+
const [selectedLL, setSelectedLL] = React.useState<[number, number] | null>(
|
| 16 |
+
null
|
| 17 |
+
);
|
| 18 |
+
const [selectedMeta, setSelectedMeta] = React.useState<SelectMeta | null>(
|
| 19 |
+
null
|
| 20 |
+
);
|
| 21 |
+
|
| 22 |
+
const [reports, setReports] = React.useState<FC>({
|
| 23 |
+
type: "FeatureCollection",
|
| 24 |
+
features: [],
|
| 25 |
+
});
|
| 26 |
+
const { nws, quakes, eonet, firms } = useFeeds();
|
| 27 |
+
|
| 28 |
+
const sessionId = useSessionId();
|
| 29 |
+
const {
|
| 30 |
+
activeTab,
|
| 31 |
+
setActiveTab,
|
| 32 |
+
localUpdates,
|
| 33 |
+
globalUpdates,
|
| 34 |
+
loadingLocal,
|
| 35 |
+
loadingGlobal,
|
| 36 |
+
} = useUpdates(selectedLL);
|
| 37 |
+
|
| 38 |
+
const {
|
| 39 |
+
messages,
|
| 40 |
+
draft,
|
| 41 |
+
setDraft,
|
| 42 |
+
isStreaming,
|
| 43 |
+
hasFirstToken,
|
| 44 |
+
chatBodyRef,
|
| 45 |
+
send,
|
| 46 |
+
pendingPhotoUrl,
|
| 47 |
+
setPendingPhotoUrl,
|
| 48 |
+
isUploading,
|
| 49 |
+
onFileChosen,
|
| 50 |
+
} = useChat(sessionId, selectedLL);
|
| 51 |
+
|
| 52 |
+
const fileInputRef = React.useRef<HTMLInputElement | null>(null);
|
| 53 |
+
|
| 54 |
+
const loadReports = React.useCallback(async () => {
|
| 55 |
+
const fc = await fetch(REPORTS_URL)
|
| 56 |
+
.then((r) => r.json())
|
| 57 |
+
.catch(() => ({ type: "FeatureCollection", features: [] }));
|
| 58 |
+
setReports(fc);
|
| 59 |
+
}, []);
|
| 60 |
+
|
| 61 |
+
React.useEffect(() => {
|
| 62 |
+
loadReports();
|
| 63 |
+
}, [loadReports]);
|
| 64 |
+
|
| 65 |
+
const selectPoint = React.useCallback(
|
| 66 |
+
(ll: [number, number], meta: SelectMeta) => {
|
| 67 |
+
setSelectedLL(ll);
|
| 68 |
+
setSelectedMeta(meta);
|
| 69 |
+
},
|
| 70 |
+
[]
|
| 71 |
+
);
|
| 72 |
+
|
| 73 |
+
const pickPhoto = React.useCallback(() => fileInputRef.current?.click(), []);
|
| 74 |
+
const onSend = React.useCallback(async () => {
|
| 75 |
+
const res = await send();
|
| 76 |
+
if (res?.tool_used === "add_report") await loadReports();
|
| 77 |
+
}, [send, loadReports]);
|
| 78 |
+
|
| 79 |
+
return (
|
| 80 |
+
<div className="shell">
|
| 81 |
+
<aside className="sidebar">
|
| 82 |
+
<div className="brand">
|
| 83 |
+
<div className="logo">PM</div>
|
| 84 |
+
<div className="title">PulseMap Agent</div>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<SelectedLocationCard
|
| 88 |
+
selectedLL={selectedLL}
|
| 89 |
+
selectedMeta={selectedMeta}
|
| 90 |
+
onClear={() => {
|
| 91 |
+
setSelectedLL(null);
|
| 92 |
+
setSelectedMeta(null);
|
| 93 |
+
}}
|
| 94 |
+
/>
|
| 95 |
+
|
| 96 |
+
<UpdatesPanel
|
| 97 |
+
activeTab={activeTab}
|
| 98 |
+
setActiveTab={setActiveTab}
|
| 99 |
+
localUpdates={localUpdates}
|
| 100 |
+
globalUpdates={globalUpdates}
|
| 101 |
+
loadingLocal={loadingLocal}
|
| 102 |
+
loadingGlobal={loadingGlobal}
|
| 103 |
+
selectedLL={selectedLL}
|
| 104 |
+
onView={(u) =>
|
| 105 |
+
selectPoint([u.lat, u.lon], {
|
| 106 |
+
kind: u.kind as any,
|
| 107 |
+
title: u.title,
|
| 108 |
+
subtitle: (u as any).raw?.text || "",
|
| 109 |
+
severity:
|
| 110 |
+
typeof u.severity === "undefined" ? "" : String(u.severity),
|
| 111 |
+
sourceUrl: u.sourceUrl,
|
| 112 |
+
})
|
| 113 |
+
}
|
| 114 |
+
/>
|
| 115 |
+
</aside>
|
| 116 |
+
|
| 117 |
+
<main className="main">
|
| 118 |
+
<section className="mapWrap" style={{ position: "relative" }}>
|
| 119 |
+
<MapCanvas
|
| 120 |
+
selectedLL={selectedLL}
|
| 121 |
+
selectedMeta={selectedMeta}
|
| 122 |
+
setSelected={selectPoint}
|
| 123 |
+
nws={nws}
|
| 124 |
+
quakes={quakes}
|
| 125 |
+
eonet={eonet}
|
| 126 |
+
firms={firms}
|
| 127 |
+
reports={reports}
|
| 128 |
+
/>
|
| 129 |
+
</section>
|
| 130 |
+
|
| 131 |
+
<ChatPanel
|
| 132 |
+
messages={messages}
|
| 133 |
+
draft={draft}
|
| 134 |
+
setDraft={setDraft}
|
| 135 |
+
isStreaming={isStreaming}
|
| 136 |
+
hasFirstToken={hasFirstToken}
|
| 137 |
+
chatBodyRef={chatBodyRef}
|
| 138 |
+
onSend={onSend}
|
| 139 |
+
pendingThumb={pendingPhotoUrl}
|
| 140 |
+
onAttachClick={pickPhoto}
|
| 141 |
+
onClearAttach={() => setPendingPhotoUrl(null)}
|
| 142 |
+
isUploading={isUploading}
|
| 143 |
+
/>
|
| 144 |
+
|
| 145 |
+
{/* hidden file input lives here */}
|
| 146 |
+
<input
|
| 147 |
+
ref={fileInputRef}
|
| 148 |
+
type="file"
|
| 149 |
+
accept="image/*"
|
| 150 |
+
style={{ display: "none" }}
|
| 151 |
+
onChange={(e) => {
|
| 152 |
+
const f = e.target.files?.[0];
|
| 153 |
+
if (f) onFileChosen(f);
|
| 154 |
+
}}
|
| 155 |
+
/>
|
| 156 |
+
</main>
|
| 157 |
+
</div>
|
| 158 |
+
);
|
| 159 |
+
}
|
web/src/assets/react.svg
ADDED
|
|
web/src/components/ReportIcon.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// components/ReportIcon.tsx
|
| 2 |
+
import * as Lucide from "lucide-react";
|
| 3 |
+
|
| 4 |
+
const pascal = (s: string) =>
|
| 5 |
+
s.replace(/(^\w|-\w)/g, (m) => m.replace("-", "").toUpperCase());
|
| 6 |
+
|
| 7 |
+
export function ReportIcon({
|
| 8 |
+
name = "info",
|
| 9 |
+
size = 32,
|
| 10 |
+
}: {
|
| 11 |
+
name?: string;
|
| 12 |
+
size?: number;
|
| 13 |
+
}) {
|
| 14 |
+
const key = (name || "info").toLowerCase();
|
| 15 |
+
|
| 16 |
+
// 3D local PNGs: name like "3d:police-light"
|
| 17 |
+
if (key.startsWith("3d-")) {
|
| 18 |
+
const file = key; // "police-light"
|
| 19 |
+
return (
|
| 20 |
+
<img
|
| 21 |
+
src={`/icons/3d/${file}.png`}
|
| 22 |
+
alt={file}
|
| 23 |
+
width={size}
|
| 24 |
+
height={size}
|
| 25 |
+
loading="lazy"
|
| 26 |
+
style={{ display: "block" }}
|
| 27 |
+
/>
|
| 28 |
+
);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// fallback to your existing Lucide logic
|
| 32 |
+
const Comp = (Lucide as any)[pascal(key)] ?? (Lucide as any).Info;
|
| 33 |
+
return <Comp size={size} />;
|
| 34 |
+
}
|
web/src/components/chat/ChatPanel.tsx
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import TypingDots from "./TypingDots";
|
| 2 |
+
import type { Message } from "../../lib/types";
|
| 3 |
+
|
| 4 |
+
export default function ChatPanel({
|
| 5 |
+
messages,
|
| 6 |
+
draft,
|
| 7 |
+
setDraft,
|
| 8 |
+
isStreaming,
|
| 9 |
+
hasFirstToken,
|
| 10 |
+
chatBodyRef,
|
| 11 |
+
onSend,
|
| 12 |
+
pendingThumb,
|
| 13 |
+
onAttachClick,
|
| 14 |
+
onClearAttach,
|
| 15 |
+
isUploading,
|
| 16 |
+
}: {
|
| 17 |
+
messages: Message[];
|
| 18 |
+
draft: string;
|
| 19 |
+
setDraft: (s: string) => void;
|
| 20 |
+
isStreaming: boolean;
|
| 21 |
+
hasFirstToken: boolean;
|
| 22 |
+
chatBodyRef: React.RefObject<HTMLDivElement>;
|
| 23 |
+
onSend: () => void;
|
| 24 |
+
pendingThumb?: string | null;
|
| 25 |
+
onAttachClick: () => void;
|
| 26 |
+
onClearAttach: () => void;
|
| 27 |
+
isUploading: boolean;
|
| 28 |
+
}) {
|
| 29 |
+
return (
|
| 30 |
+
<section className="chat">
|
| 31 |
+
<div className="chatHdr">Assistant</div>
|
| 32 |
+
<div className="chatBody" ref={chatBodyRef}>
|
| 33 |
+
{messages.length === 0 ? (
|
| 34 |
+
<div className="muted">
|
| 35 |
+
Try: “Flooded underpass here”, or “List reports near me”.
|
| 36 |
+
</div>
|
| 37 |
+
) : (
|
| 38 |
+
messages.map((m, idx) => (
|
| 39 |
+
<div key={idx} className={`msg ${m.role}`}>
|
| 40 |
+
{isStreaming &&
|
| 41 |
+
!hasFirstToken &&
|
| 42 |
+
idx === messages.length - 1 &&
|
| 43 |
+
m.role === "assistant" ? (
|
| 44 |
+
<div className="pointer-events-none relative top-1 translate-y-1 z-20">
|
| 45 |
+
<TypingDots />
|
| 46 |
+
</div>
|
| 47 |
+
) : (
|
| 48 |
+
<>
|
| 49 |
+
{m.text}
|
| 50 |
+
{m.image && (
|
| 51 |
+
<div style={{ marginTop: 8 }}>
|
| 52 |
+
<img
|
| 53 |
+
src={m.image}
|
| 54 |
+
alt="attachment"
|
| 55 |
+
style={{
|
| 56 |
+
maxWidth: 220,
|
| 57 |
+
maxHeight: 220,
|
| 58 |
+
borderRadius: 8,
|
| 59 |
+
objectFit: "cover",
|
| 60 |
+
display: "block",
|
| 61 |
+
}}
|
| 62 |
+
/>
|
| 63 |
+
</div>
|
| 64 |
+
)}
|
| 65 |
+
</>
|
| 66 |
+
)}
|
| 67 |
+
</div>
|
| 68 |
+
))
|
| 69 |
+
)}
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<div className="chatInputRow">
|
| 73 |
+
<input
|
| 74 |
+
className="input-chat"
|
| 75 |
+
placeholder="Type a message…"
|
| 76 |
+
value={draft}
|
| 77 |
+
onChange={(e) => setDraft(e.target.value)}
|
| 78 |
+
onKeyDown={(e) => e.key === "Enter" && onSend()}
|
| 79 |
+
/>
|
| 80 |
+
<button
|
| 81 |
+
className="btn btn-ghost"
|
| 82 |
+
onClick={onAttachClick}
|
| 83 |
+
disabled={isUploading}
|
| 84 |
+
>
|
| 85 |
+
{isUploading ? "Uploading…" : "Attach"}
|
| 86 |
+
</button>
|
| 87 |
+
{pendingThumb && (
|
| 88 |
+
<div className="flex items-center gap-2 px-2">
|
| 89 |
+
<img
|
| 90 |
+
src={pendingThumb}
|
| 91 |
+
alt="attachment"
|
| 92 |
+
style={{
|
| 93 |
+
width: 36,
|
| 94 |
+
height: 36,
|
| 95 |
+
objectFit: "cover",
|
| 96 |
+
borderRadius: 6,
|
| 97 |
+
}}
|
| 98 |
+
/>
|
| 99 |
+
<button className="btn btn-ghost" onClick={onClearAttach}>
|
| 100 |
+
✕
|
| 101 |
+
</button>
|
| 102 |
+
</div>
|
| 103 |
+
)}
|
| 104 |
+
<button className="btn" onClick={onSend}>
|
| 105 |
+
Send
|
| 106 |
+
</button>
|
| 107 |
+
</div>
|
| 108 |
+
</section>
|
| 109 |
+
);
|
| 110 |
+
}
|
web/src/components/chat/TypingDots.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default function TypingDots() {
|
| 2 |
+
return (
|
| 3 |
+
<span className="inline-flex items-center gap-1 align-middle">
|
| 4 |
+
<span className="sr-only">…</span>
|
| 5 |
+
<span className="h-1.5 w-1.5 rounded-full bg-zinc-300 animate-bounce [animation-delay:-0.2s]" />
|
| 6 |
+
<span className="h-1.5 w-1.5 rounded-full bg-zinc-300 animate-bounce" />
|
| 7 |
+
<span className="h-1.5 w-1.5 rounded-full bg-zinc-300 animate-bounce [animation-delay:0.2s]" />
|
| 8 |
+
</span>
|
| 9 |
+
);
|
| 10 |
+
}
|