Spaces:
Sleeping
Sleeping
Rivalcoder
commited on
Commit
·
36e3763
1
Parent(s):
4cde4e8
Add Fiels
Browse files- Dockerfile +36 -0
- __init__.py +3 -0
- api.py +330 -0
- chatbot_backend.py +605 -0
- dash_app.py +1727 -0
Dockerfile
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# syntax=docker/dockerfile:1
|
| 2 |
+
|
| 3 |
+
FROM python:3.11-slim
|
| 4 |
+
|
| 5 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 6 |
+
PYTHONUNBUFFERED=1 \
|
| 7 |
+
PIP_NO_CACHE_DIR=1
|
| 8 |
+
|
| 9 |
+
WORKDIR /app
|
| 10 |
+
|
| 11 |
+
# System deps (shapely/geos optional; folium used without native deps)
|
| 12 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 13 |
+
build-essential \
|
| 14 |
+
curl \
|
| 15 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 16 |
+
|
| 17 |
+
"""
|
| 18 |
+
We keep the package layout as /app/Backend so imports like `Backend.api:app` work.
|
| 19 |
+
Build context should be the Backend directory.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
# Copy requirements first for better layer caching
|
| 23 |
+
COPY requirements.txt ./requirements.txt
|
| 24 |
+
RUN pip install -r requirements.txt
|
| 25 |
+
|
| 26 |
+
# Copy backend source under /app/Backend to preserve package name
|
| 27 |
+
RUN mkdir -p /app/Backend
|
| 28 |
+
COPY . /app/Backend
|
| 29 |
+
|
| 30 |
+
# Expose default Hugging Face Spaces port
|
| 31 |
+
EXPOSE 7860
|
| 32 |
+
|
| 33 |
+
# Default command for HF Spaces (FastAPI via uvicorn)
|
| 34 |
+
CMD uvicorn Backend.api:app --host 0.0.0.0 --port 7860
|
| 35 |
+
|
| 36 |
+
|
__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Make Backend a package for import stability (e.g., uvicorn Backend.api:app)
|
| 2 |
+
|
| 3 |
+
|
api.py
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 3 |
+
|
| 4 |
+
from fastapi import FastAPI, HTTPException, Response
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from pydantic import BaseModel
|
| 7 |
+
import folium
|
| 8 |
+
import json
|
| 9 |
+
from collections import OrderedDict
|
| 10 |
+
|
| 11 |
+
try:
|
| 12 |
+
# When Backend is treated as a package (e.g., uvicorn Backend.api:app from repo root)
|
| 13 |
+
from .chatbot_backend import GroqRAGChatbot
|
| 14 |
+
except Exception:
|
| 15 |
+
# When running inside Backend directory (e.g., uvicorn api:app)
|
| 16 |
+
from chatbot_backend import GroqRAGChatbot
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# Initialize services
|
| 20 |
+
chatbot = GroqRAGChatbot()
|
| 21 |
+
|
| 22 |
+
app = FastAPI(title="SIH Groundwater API", version="1.0.0")
|
| 23 |
+
|
| 24 |
+
# CORS for Next.js app
|
| 25 |
+
frontend_origin = os.getenv("FRONTEND_ORIGIN", "http://localhost:3000")
|
| 26 |
+
app.add_middleware(
|
| 27 |
+
CORSMiddleware,
|
| 28 |
+
allow_origins=[frontend_origin, "http://localhost:3000", "http://127.0.0.1:3000"],
|
| 29 |
+
allow_credentials=True,
|
| 30 |
+
allow_methods=["*"],
|
| 31 |
+
allow_headers=["*"]
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class ChatRequest(BaseModel):
|
| 36 |
+
query: str
|
| 37 |
+
def _normalize_query(q: Optional[str]) -> str:
|
| 38 |
+
return (q or "").strip().lower()
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
# Simple LRU cache for last chat results by exact query
|
| 42 |
+
_CHAT_CACHE_MAX = 50
|
| 43 |
+
_chat_cache: "OrderedDict[str, List[Dict[str, Any]]]" = OrderedDict()
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _cache_put(query: str, rows: List[Dict[str, Any]]) -> None:
|
| 47 |
+
key = _normalize_query(query)
|
| 48 |
+
if not key:
|
| 49 |
+
return
|
| 50 |
+
if key in _chat_cache:
|
| 51 |
+
del _chat_cache[key]
|
| 52 |
+
_chat_cache[key] = rows or []
|
| 53 |
+
while len(_chat_cache) > _CHAT_CACHE_MAX:
|
| 54 |
+
_chat_cache.popitem(last=False)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _cache_get(query: Optional[str]) -> List[Dict[str, Any]]:
|
| 58 |
+
key = _normalize_query(query)
|
| 59 |
+
if not key:
|
| 60 |
+
return []
|
| 61 |
+
rows = _chat_cache.get(key)
|
| 62 |
+
if rows is None:
|
| 63 |
+
return []
|
| 64 |
+
# move to end (recently used)
|
| 65 |
+
del _chat_cache[key]
|
| 66 |
+
_chat_cache[key] = rows
|
| 67 |
+
return rows
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@app.get("/health")
|
| 72 |
+
def health() -> Dict[str, Any]:
|
| 73 |
+
ok = chatbot.get_db_connection()
|
| 74 |
+
return {"ok": ok}
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@app.post("/chat")
|
| 78 |
+
def chat(req: ChatRequest) -> Dict[str, Any]:
|
| 79 |
+
if not req.query or not req.query.strip():
|
| 80 |
+
raise HTTPException(status_code=400, detail="Query is required")
|
| 81 |
+
|
| 82 |
+
result = chatbot.chat(req.query.strip())
|
| 83 |
+
if not result.get("success"):
|
| 84 |
+
raise HTTPException(status_code=502, detail=result.get("response") or "Failed to process query")
|
| 85 |
+
try:
|
| 86 |
+
_cache_put(req.query, result.get("results") or [])
|
| 87 |
+
except Exception:
|
| 88 |
+
pass
|
| 89 |
+
return result
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
@app.get("/stats")
|
| 93 |
+
def stats() -> Dict[str, Any]:
|
| 94 |
+
return chatbot.get_quick_stats()
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
class MapQuery(BaseModel):
|
| 98 |
+
query: Optional[str] = None
|
| 99 |
+
limit: Optional[int] = 100
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
@app.post("/map-data")
|
| 103 |
+
def map_data(req: MapQuery) -> Dict[str, Any]:
|
| 104 |
+
"""
|
| 105 |
+
Returns lightweight map-ready data from Supabase rows.
|
| 106 |
+
- id: synthetic id
|
| 107 |
+
- name: district (title-cased)
|
| 108 |
+
- state: state (title-cased)
|
| 109 |
+
- area: st_area_shape (float or None)
|
| 110 |
+
- perimeter: st_length_shape (float or None)
|
| 111 |
+
- geometry: WKT/GeoJSON string stored in DB (passed through)
|
| 112 |
+
"""
|
| 113 |
+
user_query = (req.query or "").strip() or "top districts by area"
|
| 114 |
+
intent = chatbot.analyze_user_intent(user_query)
|
| 115 |
+
# Ensure geography focus for better map ranking
|
| 116 |
+
intent["intent_type"] = "geographic"
|
| 117 |
+
|
| 118 |
+
query = chatbot.build_supabase_query(user_query, intent)
|
| 119 |
+
# Override limit if provided
|
| 120 |
+
if req.limit and isinstance(req.limit, int):
|
| 121 |
+
query = query.limit(max(1, min(500, req.limit)))
|
| 122 |
+
|
| 123 |
+
rows = chatbot.execute_supabase_query(query) or []
|
| 124 |
+
|
| 125 |
+
features: List[Dict[str, Any]] = []
|
| 126 |
+
for idx, r in enumerate(rows):
|
| 127 |
+
name = (r.get("district") or "").title() if r.get("district") else None
|
| 128 |
+
state = (r.get("state") or "").title() if r.get("state") else None
|
| 129 |
+
# Best-effort numeric parsing
|
| 130 |
+
def to_float(x: Any) -> Optional[float]:
|
| 131 |
+
try:
|
| 132 |
+
if x in (None, ""):
|
| 133 |
+
return None
|
| 134 |
+
return float(x)
|
| 135 |
+
except Exception:
|
| 136 |
+
return None
|
| 137 |
+
|
| 138 |
+
features.append({
|
| 139 |
+
"id": idx + 1,
|
| 140 |
+
"name": name,
|
| 141 |
+
"state": state,
|
| 142 |
+
"area": to_float(r.get("st_area_shape")),
|
| 143 |
+
"perimeter": to_float(r.get("st_length_shape")),
|
| 144 |
+
"geometry": r.get("geometry")
|
| 145 |
+
})
|
| 146 |
+
|
| 147 |
+
return {
|
| 148 |
+
"count": len(features),
|
| 149 |
+
"features": features
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
# Uvicorn entrypoint: `python -m uvicorn Backend.api:app --reload --host 0.0.0.0 --port 8000`
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
@app.get("/api/map")
|
| 157 |
+
def map_html(query: Optional[str] = None, limit: int = 100) -> Response:
|
| 158 |
+
"""
|
| 159 |
+
Builds an HTML map using folium.
|
| 160 |
+
Uses the SAME rows as chat (chatbot.chat(query)['results']) to ensure identical filtering/ordering.
|
| 161 |
+
Frontend component `MapPlaceholder` expects this endpoint to return HTML.
|
| 162 |
+
"""
|
| 163 |
+
try:
|
| 164 |
+
user_query = (query or "").strip()
|
| 165 |
+
features_data: List[Dict[str, Any]] = []
|
| 166 |
+
|
| 167 |
+
# 1) Primary path: use EXACT results previously produced by /chat for the same query
|
| 168 |
+
rows: List[Dict[str, Any]] = _cache_get(user_query)
|
| 169 |
+
|
| 170 |
+
# 2) Fallback: if no rows from chat, reuse map-data builder
|
| 171 |
+
if not rows:
|
| 172 |
+
payload = MapQuery(query=user_query, limit=limit)
|
| 173 |
+
data = map_data(payload)
|
| 174 |
+
features_data = data.get("features") or []
|
| 175 |
+
|
| 176 |
+
# 3) If still nothing, as a last attempt, run chat now and cache it
|
| 177 |
+
if not rows and not features_data and user_query:
|
| 178 |
+
chat_out = chatbot.chat(user_query)
|
| 179 |
+
rows = chat_out.get("results") or []
|
| 180 |
+
_cache_put(user_query, rows)
|
| 181 |
+
|
| 182 |
+
# Convert rows -> features if we have rows
|
| 183 |
+
if rows and not features_data:
|
| 184 |
+
for idx, r in enumerate(rows[: max(1, min(500, limit))]):
|
| 185 |
+
def to_float(x: Any) -> Optional[float]:
|
| 186 |
+
try:
|
| 187 |
+
if x in (None, ""):
|
| 188 |
+
return None
|
| 189 |
+
return float(x)
|
| 190 |
+
except Exception:
|
| 191 |
+
return None
|
| 192 |
+
|
| 193 |
+
features_data.append({
|
| 194 |
+
"id": idx + 1,
|
| 195 |
+
"name": ((r.get("district") or "").title() if r.get("district") else None),
|
| 196 |
+
"state": ((r.get("state") or "").title() if r.get("state") else None),
|
| 197 |
+
"area": to_float(r.get("st_area_shape")),
|
| 198 |
+
"perimeter": to_float(r.get("st_length_shape")),
|
| 199 |
+
"geometry": r.get("geometry"),
|
| 200 |
+
})
|
| 201 |
+
|
| 202 |
+
# Initialize map centered on India
|
| 203 |
+
fmap = folium.Map(location=[22.9734, 78.6569], zoom_start=5, tiles="OpenStreetMap")
|
| 204 |
+
fg = folium.FeatureGroup(name="Underground Coverage")
|
| 205 |
+
|
| 206 |
+
# Add features; draw GeoJSON when available, otherwise add a label-only marker
|
| 207 |
+
# Geometry parsing helpers
|
| 208 |
+
def parse_geometry(geom: Any) -> Optional[Any]:
|
| 209 |
+
if geom is None:
|
| 210 |
+
return None
|
| 211 |
+
# Already a mapping (GeoJSON-like)
|
| 212 |
+
if isinstance(geom, (dict, list)):
|
| 213 |
+
return geom
|
| 214 |
+
if isinstance(geom, str):
|
| 215 |
+
s = geom.strip()
|
| 216 |
+
# JSON string
|
| 217 |
+
if s.startswith('{') or s.startswith('['):
|
| 218 |
+
try:
|
| 219 |
+
return json.loads(s)
|
| 220 |
+
except Exception:
|
| 221 |
+
pass
|
| 222 |
+
# WKT detection
|
| 223 |
+
wkt_prefixes = ("POLYGON", "MULTIPOLYGON", "LINESTRING", "MULTILINESTRING", "POINT", "MULTIPOINT")
|
| 224 |
+
if any(s.upper().startswith(p) for p in wkt_prefixes):
|
| 225 |
+
try:
|
| 226 |
+
# Try shapely if available
|
| 227 |
+
from shapely import wkt as _wkt
|
| 228 |
+
from shapely.geometry import mapping as _mapping
|
| 229 |
+
shape_obj = _wkt.loads(s)
|
| 230 |
+
return _mapping(shape_obj)
|
| 231 |
+
except Exception:
|
| 232 |
+
return None
|
| 233 |
+
return None
|
| 234 |
+
|
| 235 |
+
for f in features_data:
|
| 236 |
+
name = f.get("name") or "Unknown"
|
| 237 |
+
state = f.get("state") or ""
|
| 238 |
+
area = f.get("area")
|
| 239 |
+
perimeter = f.get("perimeter")
|
| 240 |
+
geometry = f.get("geometry")
|
| 241 |
+
popup = folium.Popup(
|
| 242 |
+
f"<b>{name}</b>, {state}<br/>Area: {area or 'N/A'}<br/>Perimeter: {perimeter or 'N/A'}",
|
| 243 |
+
max_width=300
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
# Try to parse geometry (JSON or WKT -> GeoJSON-like) and render
|
| 247 |
+
parsed = parse_geometry(geometry)
|
| 248 |
+
if parsed is not None:
|
| 249 |
+
try:
|
| 250 |
+
folium.GeoJson(
|
| 251 |
+
parsed,
|
| 252 |
+
name=name,
|
| 253 |
+
tooltip=name,
|
| 254 |
+
popup=popup,
|
| 255 |
+
style_function=lambda _:
|
| 256 |
+
{"fillColor": "#3186cc", "color": "#3186cc", "weight": 1, "fillOpacity": 0.4}
|
| 257 |
+
).add_to(fg)
|
| 258 |
+
continue
|
| 259 |
+
except Exception:
|
| 260 |
+
pass
|
| 261 |
+
|
| 262 |
+
# Fallback: no geometry or not JSON — add a generic marker at India center (avoids failure)
|
| 263 |
+
folium.Marker(
|
| 264 |
+
location=[22.9734, 78.6569],
|
| 265 |
+
tooltip=name,
|
| 266 |
+
popup=popup,
|
| 267 |
+
icon=folium.Icon(color="blue", icon="info-sign")
|
| 268 |
+
).add_to(fg)
|
| 269 |
+
|
| 270 |
+
fg.add_to(fmap)
|
| 271 |
+
folium.LayerControl().add_to(fmap)
|
| 272 |
+
|
| 273 |
+
html = fmap.get_root().render()
|
| 274 |
+
return Response(content=html, media_type="text/html")
|
| 275 |
+
except Exception as e:
|
| 276 |
+
return Response(content=f"<html><body><pre>Failed to render map: {str(e)}</pre></body></html>", media_type="text/html", status_code=500)
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
@app.get("/results")
|
| 280 |
+
def results(query: Optional[str] = None, limit: int = 100) -> Dict[str, Any]:
|
| 281 |
+
"""
|
| 282 |
+
Returns the exact rows used by chat for a given query. If absent, runs chat once.
|
| 283 |
+
Also returns a 'table' projection suited for the Explore page.
|
| 284 |
+
"""
|
| 285 |
+
user_query = (query or "").strip()
|
| 286 |
+
rows: List[Dict[str, Any]] = _cache_get(user_query)
|
| 287 |
+
if not rows and user_query:
|
| 288 |
+
chat_out = chatbot.chat(user_query)
|
| 289 |
+
rows = chat_out.get("results") or []
|
| 290 |
+
_cache_put(user_query, rows)
|
| 291 |
+
|
| 292 |
+
# Clamp and normalize
|
| 293 |
+
rows = (rows or [])[: max(1, min(500, limit))]
|
| 294 |
+
|
| 295 |
+
def to_float(x: Any) -> Optional[float]:
|
| 296 |
+
try:
|
| 297 |
+
if x in (None, ""):
|
| 298 |
+
return None
|
| 299 |
+
return float(x)
|
| 300 |
+
except Exception:
|
| 301 |
+
return None
|
| 302 |
+
|
| 303 |
+
def derive_status(stage: Optional[float]) -> str:
|
| 304 |
+
if stage is None:
|
| 305 |
+
return "safe"
|
| 306 |
+
if stage > 100:
|
| 307 |
+
return "over-exploited"
|
| 308 |
+
if 80 <= stage <= 100:
|
| 309 |
+
return "critical"
|
| 310 |
+
if 60 <= stage < 80:
|
| 311 |
+
return "semi-critical"
|
| 312 |
+
return "safe"
|
| 313 |
+
|
| 314 |
+
table = []
|
| 315 |
+
for r in rows:
|
| 316 |
+
stage = to_float(r.get("stage_of_development"))
|
| 317 |
+
draft_total = to_float(r.get("annual_gw_draft_total"))
|
| 318 |
+
underground_area = to_float(r.get("st_area_shape"))
|
| 319 |
+
table.append({
|
| 320 |
+
"district": (r.get("district") or "").title() if r.get("district") else "",
|
| 321 |
+
"state": (r.get("state") or "").title() if r.get("state") else "",
|
| 322 |
+
"development_stage": round(stage, 1) if isinstance(stage, (int, float)) else None,
|
| 323 |
+
"draft_total": draft_total,
|
| 324 |
+
"availability": to_float(r.get("net_gw_availability")),
|
| 325 |
+
"underground_area": underground_area,
|
| 326 |
+
"status": derive_status(stage),
|
| 327 |
+
})
|
| 328 |
+
|
| 329 |
+
return {"count": len(rows), "rows": rows, "table": table}
|
| 330 |
+
|
chatbot_backend.py
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
import re
|
| 5 |
+
from typing import List, Dict, Any, Optional
|
| 6 |
+
from supabase import create_client, Client
|
| 7 |
+
from groq import Groq
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
|
| 10 |
+
load_dotenv()
|
| 11 |
+
logging.basicConfig(level=logging.INFO)
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class GroqRAGChatbot:
|
| 16 |
+
def __init__(self):
|
| 17 |
+
"""Initialize optimized RAG Chatbot with correct models and Supabase"""
|
| 18 |
+
self.groq_client = Groq(api_key=os.getenv('GROQ_API_KEY'))
|
| 19 |
+
|
| 20 |
+
self.models = {
|
| 21 |
+
'intent_analyzer': 'llama-3.1-8b-instant',
|
| 22 |
+
'query_builder': 'llama-3.3-70b-versatile',
|
| 23 |
+
'response_generator': 'llama-3.3-70b-versatile'
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
self.supabase_url = os.getenv('SUPABASE_URL')
|
| 27 |
+
self.supabase_key = os.getenv('SUPABASE_KEY')
|
| 28 |
+
self.supabase: Client = create_client(self.supabase_url, self.supabase_key)
|
| 29 |
+
|
| 30 |
+
self.schema_info = {
|
| 31 |
+
'table_name': 'groundwater_data',
|
| 32 |
+
'key_columns': {
|
| 33 |
+
'district': 'District name (VARCHAR) - ALWAYS REQUIRED - lowercase',
|
| 34 |
+
'state': 'State name (VARCHAR) - ALWAYS REQUIRED - lowercase',
|
| 35 |
+
'annual_gw_draft_total': 'Total groundwater draft in hectare meters (DECIMAL)',
|
| 36 |
+
'annual_replenishable_gw_resource': 'Replenishable groundwater resource (DECIMAL)',
|
| 37 |
+
'stage_of_development': 'Development stage percentage (DECIMAL)',
|
| 38 |
+
'net_gw_availability': 'Net groundwater availability (DECIMAL)',
|
| 39 |
+
'annual_draft_irrigation': 'Irrigation draft (DECIMAL)',
|
| 40 |
+
'st_area_shape': 'Underground water coverage area in square meters (DOUBLE PRECISION)',
|
| 41 |
+
'st_length_shape': 'Underground water perimeter in meters (DOUBLE PRECISION)',
|
| 42 |
+
'geometry': 'Geographic boundaries for underground water mapping (TEXT)'
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
def get_db_connection(self):
|
| 47 |
+
try:
|
| 48 |
+
result = self.supabase.table(self.schema_info['table_name']).select('*').limit(1).execute()
|
| 49 |
+
return True
|
| 50 |
+
except Exception as e:
|
| 51 |
+
logger.error(f"Supabase connection error: {e}")
|
| 52 |
+
return False
|
| 53 |
+
|
| 54 |
+
def analyze_user_intent(self, user_query: str) -> Dict[str, Any]:
|
| 55 |
+
try:
|
| 56 |
+
prompt = f"""Analyze this user query and respond with JSON only:
|
| 57 |
+
Query: "{user_query}"
|
| 58 |
+
Available columns: {', '.join(self.schema_info['key_columns'].keys())}
|
| 59 |
+
IMPORTANT: For underground water analysis, always consider st_area_shape (coverage area) and st_length_shape (perimeter).
|
| 60 |
+
Response format:
|
| 61 |
+
{{
|
| 62 |
+
"intent_type": "comparison|ranking|statistics|filter|geographic",
|
| 63 |
+
"entities": ["district names mentioned"],
|
| 64 |
+
"target_columns": ["relevant column names"],
|
| 65 |
+
"needs_visualization": true|false,
|
| 66 |
+
"requires_geography": true|false,
|
| 67 |
+
"underground_focus": true|false
|
| 68 |
+
}}"""
|
| 69 |
+
|
| 70 |
+
response = self.groq_client.chat.completions.create(
|
| 71 |
+
messages=[
|
| 72 |
+
{"role": "system", "content": "You are a query analyzer. Respond only with valid JSON. Always include district name and underground water metrics."},
|
| 73 |
+
{"role": "user", "content": prompt}
|
| 74 |
+
],
|
| 75 |
+
model=self.models['intent_analyzer'],
|
| 76 |
+
temperature=0.1,
|
| 77 |
+
max_tokens=200
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
return json.loads(response.choices[0].message.content)
|
| 81 |
+
|
| 82 |
+
except Exception as e:
|
| 83 |
+
logger.error(f"Intent analysis error: {e}")
|
| 84 |
+
return {
|
| 85 |
+
"intent_type": "ranking",
|
| 86 |
+
"entities": [],
|
| 87 |
+
"target_columns": ["annual_gw_draft_total"],
|
| 88 |
+
"needs_visualization": True,
|
| 89 |
+
"requires_geography": False,
|
| 90 |
+
"underground_focus": True
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
def build_supabase_query(self, user_query: str, intent_analysis: Dict[str, Any]) -> Any:
|
| 94 |
+
"""
|
| 95 |
+
Build a Supabase query using the client's query methods instead of generating raw SQL
|
| 96 |
+
"""
|
| 97 |
+
try:
|
| 98 |
+
intent_type = intent_analysis.get('intent_type', 'ranking')
|
| 99 |
+
entities = intent_analysis.get('entities', [])
|
| 100 |
+
target_columns = intent_analysis.get('target_columns', ['annual_gw_draft_total'])
|
| 101 |
+
# Infer top N from free text
|
| 102 |
+
inferred_limit: Optional[int] = None
|
| 103 |
+
try:
|
| 104 |
+
m = re.search(r"\btop\s*(\d+)\b", (user_query or '').lower())
|
| 105 |
+
if m:
|
| 106 |
+
inferred_limit = int(m.group(1))
|
| 107 |
+
except Exception:
|
| 108 |
+
inferred_limit = None
|
| 109 |
+
|
| 110 |
+
# Always include these columns
|
| 111 |
+
mandatory_columns = ['district', 'state', 'st_area_shape', 'st_length_shape', 'geometry']
|
| 112 |
+
selected_columns = list(set(mandatory_columns + target_columns))
|
| 113 |
+
|
| 114 |
+
# Start building the query
|
| 115 |
+
query = self.supabase.table(self.schema_info['table_name']).select(','.join(selected_columns))
|
| 116 |
+
|
| 117 |
+
# Apply filters based on intent
|
| 118 |
+
if entities:
|
| 119 |
+
# Apply OR across district/state for each entity with wildcards
|
| 120 |
+
blacklist = {
|
| 121 |
+
'district', 'districts', 'state', 'states',
|
| 122 |
+
'district names mentioned', 'district names', 'unknown', 'india', 'indian'
|
| 123 |
+
}
|
| 124 |
+
safe_entities = []
|
| 125 |
+
for raw in entities:
|
| 126 |
+
try:
|
| 127 |
+
token = str(raw).strip().lower()
|
| 128 |
+
except Exception:
|
| 129 |
+
continue
|
| 130 |
+
if not token:
|
| 131 |
+
continue
|
| 132 |
+
# ignore placeholders or generic tokens containing admin unit words
|
| 133 |
+
if token in blacklist or ('district' in token) or ('state' in token):
|
| 134 |
+
continue
|
| 135 |
+
# ignore extremely short tokens
|
| 136 |
+
if len(token) < 3:
|
| 137 |
+
continue
|
| 138 |
+
safe_entities.append(token)
|
| 139 |
+
if safe_entities:
|
| 140 |
+
or_clauses = []
|
| 141 |
+
for e in safe_entities:
|
| 142 |
+
# Use PostgREST ilike syntax with *wildcards*
|
| 143 |
+
pattern = f"*{e}*"
|
| 144 |
+
or_clauses.append(f"district.ilike.{pattern}")
|
| 145 |
+
or_clauses.append(f"state.ilike.{pattern}")
|
| 146 |
+
# Combine into a single OR string
|
| 147 |
+
or_str = ','.join(or_clauses)
|
| 148 |
+
try:
|
| 149 |
+
query = query.or_(or_str)
|
| 150 |
+
except Exception:
|
| 151 |
+
# Fallback: chain first entity as ilike filter
|
| 152 |
+
try:
|
| 153 |
+
query = query.ilike('district', pattern)
|
| 154 |
+
except Exception:
|
| 155 |
+
pass
|
| 156 |
+
else:
|
| 157 |
+
# No safe entities; do not constrain by entity at all
|
| 158 |
+
pass
|
| 159 |
+
|
| 160 |
+
elif intent_type == "filter":
|
| 161 |
+
# For filtering queries, we might need to add specific conditions
|
| 162 |
+
# This is a simple implementation - you might want to enhance it
|
| 163 |
+
if "high" in user_query.lower() or "greater" in user_query.lower():
|
| 164 |
+
query = query.gt('stage_of_development', 80)
|
| 165 |
+
elif "low" in user_query.lower() or "less" in user_query.lower():
|
| 166 |
+
query = query.lt('stage_of_development', 40)
|
| 167 |
+
|
| 168 |
+
# Choose metric preference from query keywords
|
| 169 |
+
ql = (user_query or '').lower()
|
| 170 |
+
metric_preference = None
|
| 171 |
+
# Map "water level high" to underground coverage area if available
|
| 172 |
+
if any(k in ql for k in ["water level", "waterlevel", "underground", "groundwater", "coverage"]):
|
| 173 |
+
metric_preference = 'st_area_shape'
|
| 174 |
+
|
| 175 |
+
# Apply ordering based on intent/metric
|
| 176 |
+
if intent_type == "ranking":
|
| 177 |
+
column = metric_preference or (target_columns[0] if target_columns else 'annual_gw_draft_total')
|
| 178 |
+
order = "desc" if any(word in ql for word in ['highest', 'top', 'most', 'maximum', 'high']) else "asc"
|
| 179 |
+
query = query.order(column, desc=(order == "desc"))
|
| 180 |
+
elif intent_type == "geographic":
|
| 181 |
+
query = query.order('st_area_shape', desc=True)
|
| 182 |
+
|
| 183 |
+
# Apply limit (return more rows to power Knowledge/Insights)
|
| 184 |
+
# If specific entities mentioned, default to a tighter limit unless user said otherwise
|
| 185 |
+
if entities:
|
| 186 |
+
default_limit = 50
|
| 187 |
+
else:
|
| 188 |
+
default_limit = 50 if intent_type in ["ranking", "geographic"] else 200
|
| 189 |
+
final_limit = inferred_limit if (isinstance(inferred_limit, int) and inferred_limit > 0) else default_limit
|
| 190 |
+
# Clamp reasonable bounds (1..500)
|
| 191 |
+
final_limit = max(1, min(500, final_limit))
|
| 192 |
+
query = query.limit(final_limit)
|
| 193 |
+
|
| 194 |
+
return query
|
| 195 |
+
|
| 196 |
+
except Exception as e:
|
| 197 |
+
logger.error(f"Supabase query building error: {e}")
|
| 198 |
+
# Fallback to a simple query
|
| 199 |
+
return self.supabase.table(self.schema_info['table_name']).select('*').limit(10)
|
| 200 |
+
|
| 201 |
+
def execute_supabase_query(self, query) -> Optional[List[Dict[str, Any]]]:
|
| 202 |
+
"""
|
| 203 |
+
Execute the Supabase query and return results
|
| 204 |
+
"""
|
| 205 |
+
try:
|
| 206 |
+
result = query.execute()
|
| 207 |
+
# Supabase-py v2 returns a PostgrestResponse with .data
|
| 208 |
+
rows = getattr(result, 'data', None)
|
| 209 |
+
if rows is None:
|
| 210 |
+
rows = []
|
| 211 |
+
|
| 212 |
+
# Normalize string fields to lowercase except geometry
|
| 213 |
+
for row in rows:
|
| 214 |
+
for k, v in list(row.items()):
|
| 215 |
+
if isinstance(v, str) and k != 'geometry':
|
| 216 |
+
row[k] = v.lower()
|
| 217 |
+
# print(rows)
|
| 218 |
+
logger.info(f"Supabase query returned {len(rows)} results")
|
| 219 |
+
return rows
|
| 220 |
+
|
| 221 |
+
except Exception as e:
|
| 222 |
+
logger.error(f"Supabase query execution error: {e}")
|
| 223 |
+
return []
|
| 224 |
+
|
| 225 |
+
def get_quick_stats(self) -> Dict[str, Any]:
|
| 226 |
+
try:
|
| 227 |
+
total_result = self.supabase.table(self.schema_info['table_name']).select("district", count="exact").limit(1).execute()
|
| 228 |
+
total_districts = total_result.count if hasattr(total_result, 'count') else len(total_result.data) if total_result.data else 0
|
| 229 |
+
|
| 230 |
+
all_data = self.supabase.table(self.schema_info['table_name']).select("stage_of_development").execute()
|
| 231 |
+
|
| 232 |
+
if all_data.data:
|
| 233 |
+
developments = []
|
| 234 |
+
for row in all_data.data:
|
| 235 |
+
val = row.get('stage_of_development')
|
| 236 |
+
try:
|
| 237 |
+
if val is not None:
|
| 238 |
+
num_val = float(val) if isinstance(val, str) else val
|
| 239 |
+
if isinstance(num_val, (int, float)):
|
| 240 |
+
# Sanitize: ignore invalid/negative and extreme outliers
|
| 241 |
+
if 0 <= num_val <= 500:
|
| 242 |
+
developments.append(num_val)
|
| 243 |
+
except (ValueError, TypeError):
|
| 244 |
+
continue
|
| 245 |
+
|
| 246 |
+
if developments:
|
| 247 |
+
avg_development = sum(developments) / len(developments)
|
| 248 |
+
# Clamp to sensible range
|
| 249 |
+
avg_development = max(0.0, min(200.0, avg_development))
|
| 250 |
+
over_exploited = len([d for d in developments if d is not None and d > 100])
|
| 251 |
+
critical = len([d for d in developments if d is not None and 80 <= d <= 100])
|
| 252 |
+
else:
|
| 253 |
+
avg_development = 0
|
| 254 |
+
over_exploited = 0
|
| 255 |
+
critical = 0
|
| 256 |
+
else:
|
| 257 |
+
avg_development = 0
|
| 258 |
+
over_exploited = 0
|
| 259 |
+
critical = 0
|
| 260 |
+
|
| 261 |
+
return {
|
| 262 |
+
"total_districts": total_districts,
|
| 263 |
+
"avg_development": round(float(avg_development), 1),
|
| 264 |
+
"over_exploited": over_exploited,
|
| 265 |
+
"critical": critical
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
except Exception as e:
|
| 269 |
+
logger.error(f"Stats query error: {e}")
|
| 270 |
+
return {
|
| 271 |
+
"total_districts": 0,
|
| 272 |
+
"avg_development": 0,
|
| 273 |
+
"over_exploited": 0,
|
| 274 |
+
"critical": 0
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
def generate_response(self, user_query: str, query_results: List[Dict[str, Any]]) -> str:
|
| 278 |
+
try:
|
| 279 |
+
if not query_results:
|
| 280 |
+
return "No data found matching your query. Please try rephrasing your question or check if the district names are correct."
|
| 281 |
+
|
| 282 |
+
results_summary = []
|
| 283 |
+
for result in query_results[:5]:
|
| 284 |
+
result_items = []
|
| 285 |
+
for k, v in result.items():
|
| 286 |
+
if v is not None and k != 'geometry':
|
| 287 |
+
if k == 'st_area_shape':
|
| 288 |
+
try:
|
| 289 |
+
area_val = float(v)
|
| 290 |
+
result_items.append(f"Underground Coverage Area: {area_val:,.0f} sq.m")
|
| 291 |
+
except (ValueError, TypeError):
|
| 292 |
+
result_items.append(f"Underground Coverage Area: {v}")
|
| 293 |
+
elif k == 'st_length_shape':
|
| 294 |
+
try:
|
| 295 |
+
length_val = float(v)
|
| 296 |
+
result_items.append(f"Underground Perimeter: {length_val:,.0f} m")
|
| 297 |
+
except (ValueError, TypeError):
|
| 298 |
+
result_items.append(f"Underground Perimeter: {v}")
|
| 299 |
+
else:
|
| 300 |
+
result_items.append(f"{k}: {v}")
|
| 301 |
+
|
| 302 |
+
results_summary.append(", ".join(result_items))
|
| 303 |
+
|
| 304 |
+
results_text = "\n".join(results_summary)
|
| 305 |
+
|
| 306 |
+
prompt = f"""Analyze Indian groundwater data results with focus on underground water availability.
|
| 307 |
+
|
| 308 |
+
User Question: {user_query}
|
| 309 |
+
|
| 310 |
+
Results ({len(query_results)} total):
|
| 311 |
+
{results_text}
|
| 312 |
+
|
| 313 |
+
IMPORTANT CONTEXT:
|
| 314 |
+
- st_area_shape represents underground water coverage area (larger = more underground water extent)
|
| 315 |
+
- st_length_shape represents underground water perimeter (longer = more complex underground water boundaries)
|
| 316 |
+
- These metrics help assess underground water availability and distribution
|
| 317 |
+
|
| 318 |
+
Provide analysis with:
|
| 319 |
+
1. Direct answer to the user's question
|
| 320 |
+
2. District names with specific numbers
|
| 321 |
+
3. Underground water coverage insights using st_area_shape and st_length_shape
|
| 322 |
+
4. Practical implications for water management
|
| 323 |
+
5. Which districts have better underground water availability based on area/perimeter metrics
|
| 324 |
+
|
| 325 |
+
Keep response informative and highlight underground water aspects."""
|
| 326 |
+
|
| 327 |
+
response = self.groq_client.chat.completions.create(
|
| 328 |
+
messages=[
|
| 329 |
+
{"role": "system", "content": "You are an Indian groundwater expert specializing in underground water analysis. Provide insights using area and perimeter metrics for underground water availability."},
|
| 330 |
+
{"role": "user", "content": prompt}
|
| 331 |
+
],
|
| 332 |
+
model=self.models['response_generator'],
|
| 333 |
+
temperature=0.3,
|
| 334 |
+
max_tokens=500
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
return response.choices[0].message.content
|
| 338 |
+
|
| 339 |
+
except Exception as e:
|
| 340 |
+
logger.error(f"Response generation error: {e}")
|
| 341 |
+
if query_results:
|
| 342 |
+
districts = [r.get('district', 'Unknown') for r in query_results[:3]]
|
| 343 |
+
return f"Found underground water data for {len(query_results)} districts including {', '.join(districts)}. Check the map visualization for underground water coverage areas and detailed results below."
|
| 344 |
+
return "Unable to generate detailed analysis, but query executed successfully."
|
| 345 |
+
|
| 346 |
+
def generate_summary_and_followups(self, user_query: str, query_results: List[Dict[str, Any]]) -> Dict[str, Any]:
|
| 347 |
+
"""Generate a concise summary and 3 follow-up questions to deepen analysis."""
|
| 348 |
+
try:
|
| 349 |
+
# Build compact, token-light context
|
| 350 |
+
top_rows = []
|
| 351 |
+
for r in (query_results or [])[:5]:
|
| 352 |
+
summary_row = {
|
| 353 |
+
k: v for k, v in r.items()
|
| 354 |
+
if k in {
|
| 355 |
+
'district', 'state', 'annual_gw_draft_total', 'stage_of_development',
|
| 356 |
+
'net_gw_availability', 'st_area_shape', 'st_length_shape'
|
| 357 |
+
} and v is not None
|
| 358 |
+
}
|
| 359 |
+
top_rows.append(summary_row)
|
| 360 |
+
|
| 361 |
+
prompt = (
|
| 362 |
+
"You are an assistant that outputs strict JSON. Given a user query and a small set "
|
| 363 |
+
"of Indian groundwater results (with underground coverage metrics), produce: "
|
| 364 |
+
"1) a one-paragraph summary (<= 80 words), 2) three concise follow-up questions.\n\n"
|
| 365 |
+
f"User Query: {user_query}\n\n"
|
| 366 |
+
f"Results Sample: {json.dumps(top_rows) }\n\n"
|
| 367 |
+
"Respond ONLY as JSON with keys 'summary' and 'follow_ups' (array of 3 strings)."
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
response = self.groq_client.chat.completions.create(
|
| 371 |
+
messages=[
|
| 372 |
+
{"role": "system", "content": "Output valid JSON only."},
|
| 373 |
+
{"role": "user", "content": prompt}
|
| 374 |
+
],
|
| 375 |
+
model=self.models['intent_analyzer'],
|
| 376 |
+
temperature=0.2,
|
| 377 |
+
max_tokens=200
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
data = json.loads(response.choices[0].message.content)
|
| 381 |
+
summary = data.get('summary') or ""
|
| 382 |
+
follow_ups = data.get('follow_ups') or []
|
| 383 |
+
# Ensure exactly up to 3
|
| 384 |
+
follow_ups = [str(q) for q in follow_ups][:3]
|
| 385 |
+
return {"summary": summary, "follow_ups": follow_ups}
|
| 386 |
+
except Exception as e:
|
| 387 |
+
logger.warning(f"Summary/follow-ups generation failed: {e}")
|
| 388 |
+
# Sensible fallback
|
| 389 |
+
fallback = [
|
| 390 |
+
"Do you want to compare two or more districts?",
|
| 391 |
+
"Should I filter by over-exploited or critical status?",
|
| 392 |
+
"Would you like a geographic view of underground coverage?"
|
| 393 |
+
]
|
| 394 |
+
return {"summary": "", "follow_ups": fallback}
|
| 395 |
+
|
| 396 |
+
def build_visualization_spec(self, user_query: str, intent_analysis: Dict[str, Any], query_results: List[Dict[str, Any]]) -> Dict[str, Any]:
|
| 397 |
+
"""Derive a lightweight visualization spec without altering existing logic."""
|
| 398 |
+
try:
|
| 399 |
+
if not query_results:
|
| 400 |
+
return {"enabled": False}
|
| 401 |
+
|
| 402 |
+
intent_type = intent_analysis.get("intent_type", "ranking")
|
| 403 |
+
target_columns = intent_analysis.get("target_columns", ["annual_gw_draft_total"]) or ["annual_gw_draft_total"]
|
| 404 |
+
primary = target_columns[0]
|
| 405 |
+
|
| 406 |
+
# Prefer known numeric metrics
|
| 407 |
+
numeric_preferences = [
|
| 408 |
+
"annual_gw_draft_total",
|
| 409 |
+
"stage_of_development",
|
| 410 |
+
"net_gw_availability",
|
| 411 |
+
"annual_replenishable_gw_resource",
|
| 412 |
+
"annual_draft_irrigation",
|
| 413 |
+
"st_area_shape",
|
| 414 |
+
"st_length_shape"
|
| 415 |
+
]
|
| 416 |
+
metric = next((c for c in [primary] + numeric_preferences if any(c in r for r in query_results)), primary)
|
| 417 |
+
|
| 418 |
+
# Fallback metric if not present
|
| 419 |
+
if not any(metric in r for r in query_results):
|
| 420 |
+
metric = "st_area_shape" if any("st_area_shape" in r for r in query_results) else primary
|
| 421 |
+
|
| 422 |
+
spec: Dict[str, Any] = {
|
| 423 |
+
"enabled": True,
|
| 424 |
+
"chart_type": "bar",
|
| 425 |
+
"x": "district" if any("district" in r for r in query_results) else None,
|
| 426 |
+
"y": metric,
|
| 427 |
+
"title": "",
|
| 428 |
+
"top_n": 10,
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
if intent_type == "comparison":
|
| 432 |
+
spec["title"] = f"Comparison of {metric.replace('_',' ').title()}"
|
| 433 |
+
spec["chart_type"] = "bar"
|
| 434 |
+
elif intent_type == "ranking":
|
| 435 |
+
spec["title"] = f"Ranking by {metric.replace('_',' ').title()}"
|
| 436 |
+
spec["chart_type"] = "bar"
|
| 437 |
+
elif intent_type == "statistics":
|
| 438 |
+
spec["title"] = f"Distribution of {metric.replace('_',' ').title()}"
|
| 439 |
+
spec["chart_type"] = "histogram"
|
| 440 |
+
spec["x"] = metric
|
| 441 |
+
spec["y"] = None
|
| 442 |
+
elif intent_type == "geographic":
|
| 443 |
+
spec["title"] = "Underground Coverage by District"
|
| 444 |
+
spec["chart_type"] = "bar"
|
| 445 |
+
spec["y"] = "st_area_shape" if any("st_area_shape" in r for r in query_results) else metric
|
| 446 |
+
|
| 447 |
+
return spec
|
| 448 |
+
except Exception:
|
| 449 |
+
return {"enabled": False}
|
| 450 |
+
|
| 451 |
+
def compute_insights(self, query_results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
| 452 |
+
"""Compute actionable insights from a result set without additional API calls.
|
| 453 |
+
|
| 454 |
+
Returns a list of {title, detail} objects suitable for display.
|
| 455 |
+
"""
|
| 456 |
+
try:
|
| 457 |
+
if not query_results:
|
| 458 |
+
return []
|
| 459 |
+
|
| 460 |
+
# Build a DataFrame-like view without importing pandas here
|
| 461 |
+
rows = []
|
| 462 |
+
for r in query_results:
|
| 463 |
+
try:
|
| 464 |
+
rows.append({
|
| 465 |
+
'district': r.get('district'),
|
| 466 |
+
'stage': float(r.get('stage_of_development')) if r.get('stage_of_development') not in (None, "") else None,
|
| 467 |
+
'draft_total': float(r.get('annual_gw_draft_total')) if r.get('annual_gw_draft_total') not in (None, "") else None,
|
| 468 |
+
'availability': float(r.get('net_gw_availability')) if r.get('net_gw_availability') not in (None, "") else None,
|
| 469 |
+
'replenishable': float(r.get('annual_replenishable_gw_resource')) if r.get('annual_replenishable_gw_resource') not in (None, "") else None,
|
| 470 |
+
'draft_irrigation': float(r.get('annual_draft_irrigation')) if r.get('annual_draft_irrigation') not in (None, "") else None,
|
| 471 |
+
'area': float(r.get('st_area_shape')) if r.get('st_area_shape') not in (None, "") else None,
|
| 472 |
+
'perimeter': float(r.get('st_length_shape')) if r.get('st_length_shape') not in (None, "") else None,
|
| 473 |
+
})
|
| 474 |
+
except Exception:
|
| 475 |
+
continue
|
| 476 |
+
|
| 477 |
+
if not rows:
|
| 478 |
+
return []
|
| 479 |
+
|
| 480 |
+
insights: List[Dict[str, Any]] = []
|
| 481 |
+
|
| 482 |
+
# Over-exploited and critical counts
|
| 483 |
+
over_ex = [r for r in rows if r['stage'] is not None and r['stage'] > 100]
|
| 484 |
+
critical = [r for r in rows if r['stage'] is not None and 80 <= r['stage'] <= 100]
|
| 485 |
+
if over_ex:
|
| 486 |
+
top_over = sorted(over_ex, key=lambda x: x['stage'], reverse=True)[:3]
|
| 487 |
+
names = ", ".join([str(r.get('district', 'unknown')).title() for r in top_over])
|
| 488 |
+
insights.append({
|
| 489 |
+
"title": "Over‑exploited hotspots",
|
| 490 |
+
"detail": f"{len(over_ex)} districts >100% development. Top: {names}."
|
| 491 |
+
})
|
| 492 |
+
if critical:
|
| 493 |
+
insights.append({
|
| 494 |
+
"title": "Critical watchlist",
|
| 495 |
+
"detail": f"{len(critical)} districts between 80–100% development; prioritize monitoring."
|
| 496 |
+
})
|
| 497 |
+
|
| 498 |
+
# Highest draft and availability gaps
|
| 499 |
+
with_draft = [r for r in rows if r['draft_total'] is not None]
|
| 500 |
+
if with_draft:
|
| 501 |
+
top_draft = sorted(with_draft, key=lambda x: x['draft_total'], reverse=True)[:3]
|
| 502 |
+
names = ", ".join([str(r.get('district', 'unknown')).title() for r in top_draft])
|
| 503 |
+
insights.append({
|
| 504 |
+
"title": "Top pressure points",
|
| 505 |
+
"detail": f"Highest total draft in: {names}. Target demand management here first."
|
| 506 |
+
})
|
| 507 |
+
|
| 508 |
+
# Supply-demand gap if both available
|
| 509 |
+
gap_rows = [r for r in rows if r['availability'] is not None and r['draft_total'] is not None]
|
| 510 |
+
if gap_rows:
|
| 511 |
+
gaps = sorted(gap_rows, key=lambda x: (x['draft_total'] - x['availability']), reverse=True)
|
| 512 |
+
worst = gaps[0]
|
| 513 |
+
if worst:
|
| 514 |
+
insights.append({
|
| 515 |
+
"title": "Availability gap",
|
| 516 |
+
"detail": f"Largest draft minus availability gap in {str(worst.get('district', 'unknown')).title()}."
|
| 517 |
+
})
|
| 518 |
+
|
| 519 |
+
# Recharge potential: big underground coverage areas
|
| 520 |
+
with_area = [r for r in rows if r['area'] is not None]
|
| 521 |
+
if with_area:
|
| 522 |
+
top_area = sorted(with_area, key=lambda x: x['area'], reverse=True)[:3]
|
| 523 |
+
names = ", ".join([str(r.get('district', 'unknown')).title() for r in top_area])
|
| 524 |
+
insights.append({
|
| 525 |
+
"title": "Recharge potential",
|
| 526 |
+
"detail": f"Large underground coverage in: {names}. Consider MAR sites."
|
| 527 |
+
})
|
| 528 |
+
|
| 529 |
+
# Complex boundaries: high perimeter relative to area (shape complexity)
|
| 530 |
+
complex_rows = [r for r in rows if r['perimeter'] and r['area'] and r['area'] > 0]
|
| 531 |
+
if complex_rows:
|
| 532 |
+
# Complexity ~ perimeter / sqrt(area)
|
| 533 |
+
ranked = sorted(complex_rows, key=lambda x: x['perimeter'] / max(1.0, x['area'] ** 0.5), reverse=True)[:3]
|
| 534 |
+
names = ", ".join([str(r.get('district', 'unknown')).title() for r in ranked])
|
| 535 |
+
insights.append({
|
| 536 |
+
"title": "Boundary complexity",
|
| 537 |
+
"detail": f"Complex underground boundaries in: {names}. Densify observation wells."
|
| 538 |
+
})
|
| 539 |
+
|
| 540 |
+
# Ensure at least 5 insights by adding generic, data-backed items
|
| 541 |
+
if len(insights) < 5 and with_draft:
|
| 542 |
+
avg_draft = sum([r['draft_total'] for r in with_draft if r['draft_total'] is not None]) / max(1, len(with_draft))
|
| 543 |
+
insights.append({
|
| 544 |
+
"title": "Average draft benchmark",
|
| 545 |
+
"detail": f"Avg annual draft across results is ~{avg_draft:,.0f} HM."
|
| 546 |
+
})
|
| 547 |
+
if len(insights) < 5 and with_area:
|
| 548 |
+
median_area = sorted([r['area'] for r in with_area if r['area'] is not None])
|
| 549 |
+
if median_area:
|
| 550 |
+
mid = median_area[len(median_area)//2]
|
| 551 |
+
insights.append({
|
| 552 |
+
"title": "Coverage benchmark",
|
| 553 |
+
"detail": f"Median underground coverage area is ~{mid:,.0f} sq.m."
|
| 554 |
+
})
|
| 555 |
+
|
| 556 |
+
return insights[:8]
|
| 557 |
+
except Exception:
|
| 558 |
+
return []
|
| 559 |
+
|
| 560 |
+
def chat(self, user_query: str) -> Dict[str, Any]:
|
| 561 |
+
logger.info(f"Processing query: {user_query}")
|
| 562 |
+
|
| 563 |
+
try:
|
| 564 |
+
intent_analysis = self.analyze_user_intent(user_query)
|
| 565 |
+
logger.info(f"Intent analysis: {intent_analysis}")
|
| 566 |
+
|
| 567 |
+
# Build and execute the Supabase query directly
|
| 568 |
+
query = self.build_supabase_query(user_query, intent_analysis)
|
| 569 |
+
query_results = self.execute_supabase_query(query)
|
| 570 |
+
|
| 571 |
+
if not query_results:
|
| 572 |
+
return {
|
| 573 |
+
"response": "Unable to retrieve data. This could be due to incorrect district names or database connectivity issues. Please try rephrasing your query.",
|
| 574 |
+
"intent_analysis": intent_analysis,
|
| 575 |
+
"results": [],
|
| 576 |
+
"results_count": 0,
|
| 577 |
+
"success": False
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
response = self.generate_response(user_query, query_results)
|
| 581 |
+
viz_spec = self.build_visualization_spec(user_query, intent_analysis, query_results)
|
| 582 |
+
aux = self.generate_summary_and_followups(user_query, query_results)
|
| 583 |
+
insights = self.compute_insights(query_results)
|
| 584 |
+
|
| 585 |
+
return {
|
| 586 |
+
"response": response,
|
| 587 |
+
"intent_analysis": intent_analysis,
|
| 588 |
+
"results": query_results,
|
| 589 |
+
"results_count": len(query_results),
|
| 590 |
+
"success": True,
|
| 591 |
+
"visualization": viz_spec,
|
| 592 |
+
"summary": aux.get("summary", ""),
|
| 593 |
+
"follow_ups": aux.get("follow_ups", []),
|
| 594 |
+
"insights": insights
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
except Exception as e:
|
| 598 |
+
logger.error(f"Chat processing error: {e}")
|
| 599 |
+
return {
|
| 600 |
+
"response": f"An error occurred while processing your query: {str(e)}",
|
| 601 |
+
"intent_analysis": {"error": str(e)},
|
| 602 |
+
"results": [],
|
| 603 |
+
"results_count": 0,
|
| 604 |
+
"success": False
|
| 605 |
+
}
|
dash_app.py
ADDED
|
@@ -0,0 +1,1727 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import dash
|
| 2 |
+
from dash import dcc, html, Input, Output, State, callback_context, dash_table
|
| 3 |
+
from dash.dependencies import ALL, MATCH
|
| 4 |
+
import dash_bootstrap_components as dbc
|
| 5 |
+
import pandas as pd
|
| 6 |
+
from chatbot_backend import GroqRAGChatbot
|
| 7 |
+
import json
|
| 8 |
+
import folium
|
| 9 |
+
from folium import plugins
|
| 10 |
+
import geopandas as gpd
|
| 11 |
+
from shapely import wkt
|
| 12 |
+
import base64
|
| 13 |
+
from io import StringIO
|
| 14 |
+
import os
|
| 15 |
+
import plotly.express as px
|
| 16 |
+
import plotly.graph_objects as go
|
| 17 |
+
from flask import Flask
|
| 18 |
+
from dash.exceptions import PreventUpdate
|
| 19 |
+
from datetime import datetime
|
| 20 |
+
|
| 21 |
+
# Initialize Flask server and Dash app
|
| 22 |
+
server = Flask(__name__)
|
| 23 |
+
|
| 24 |
+
# Serve static files (images)
|
| 25 |
+
@server.route('/Assests/<path:filename>')
|
| 26 |
+
def serve_image(filename):
|
| 27 |
+
return server.send_static_file(f'Assests/{filename}')
|
| 28 |
+
|
| 29 |
+
app = dash.Dash(
|
| 30 |
+
__name__,
|
| 31 |
+
external_stylesheets=[dbc.themes.MORPH], # Changed to a more modern theme
|
| 32 |
+
server=server,
|
| 33 |
+
meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1.0"}]
|
| 34 |
+
)
|
| 35 |
+
app.title = "India Groundwater AI Chatbot"
|
| 36 |
+
|
| 37 |
+
# Function to encode image to base64
|
| 38 |
+
def encode_image(image_path):
|
| 39 |
+
if os.path.exists(image_path):
|
| 40 |
+
with open(image_path, "rb") as image_file:
|
| 41 |
+
encoded_string = base64.b64encode(image_file.read()).decode()
|
| 42 |
+
return f"data:image/png;base64,{encoded_string}"
|
| 43 |
+
return None
|
| 44 |
+
|
| 45 |
+
# Encode the water droplet image
|
| 46 |
+
water_droplet_image = encode_image("Assests/image.png")
|
| 47 |
+
|
| 48 |
+
# Custom CSS for additional styling
|
| 49 |
+
app.index_string = '''
|
| 50 |
+
<!DOCTYPE html>
|
| 51 |
+
<html>
|
| 52 |
+
<head>
|
| 53 |
+
{%metas%}
|
| 54 |
+
<title>{%title%}</title>
|
| 55 |
+
{%favicon%}
|
| 56 |
+
{%css%}
|
| 57 |
+
<style>
|
| 58 |
+
:root {
|
| 59 |
+
--primary: #2E86AB;
|
| 60 |
+
--secondary: #A23B72;
|
| 61 |
+
--accent: #F18F01;
|
| 62 |
+
--light: #F7F7FF;
|
| 63 |
+
--dark: #2B2D42;
|
| 64 |
+
--success: #4CB944;
|
| 65 |
+
--warning: #F18F01;
|
| 66 |
+
--danger: #E71D36;
|
| 67 |
+
--bg: #f5f7fb;
|
| 68 |
+
--text: #111827;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* Dark theme overrides */
|
| 72 |
+
#app-root.dark-mode {
|
| 73 |
+
--primary: #60a5fa;
|
| 74 |
+
--secondary: #f472b6;
|
| 75 |
+
--accent: #f59e0b;
|
| 76 |
+
--light: #111827;
|
| 77 |
+
--dark: #e5e7eb;
|
| 78 |
+
--success: #34d399;
|
| 79 |
+
--warning: #f59e0b;
|
| 80 |
+
--danger: #ef4444;
|
| 81 |
+
--bg: #0b1220;
|
| 82 |
+
--text: #e5e7eb;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/* Page background and default text */
|
| 86 |
+
#app-root { background-color: var(--bg); color: var(--text); }
|
| 87 |
+
#app-root > .container-fluid { min-height: 100vh; }
|
| 88 |
+
|
| 89 |
+
.gradient-bg {
|
| 90 |
+
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.card-hover {
|
| 94 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.card-hover:hover {
|
| 98 |
+
transform: translateY(-5px);
|
| 99 |
+
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.feature-icon {
|
| 103 |
+
font-size: 2.5rem;
|
| 104 |
+
margin-bottom: 1rem;
|
| 105 |
+
color: var(--primary);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.stat-number {
|
| 109 |
+
font-size: 2.5rem;
|
| 110 |
+
font-weight: 700;
|
| 111 |
+
color: var(--primary);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.stat-label {
|
| 115 |
+
font-size: 0.9rem;
|
| 116 |
+
color: var(--dark);
|
| 117 |
+
opacity: 0.8;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* Water droplet animations */
|
| 121 |
+
.water-droplet {
|
| 122 |
+
animation: float 3s ease-in-out infinite, pulse 2s ease-in-out infinite;
|
| 123 |
+
width: 250px;
|
| 124 |
+
height: 250px;
|
| 125 |
+
object-fit: cover;
|
| 126 |
+
border-radius: 50%;
|
| 127 |
+
filter: drop-shadow(0 8px 16px rgba(0,0,0,0.3));
|
| 128 |
+
border: 4px solid rgba(255, 255, 255, 0.3);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
@keyframes float {
|
| 132 |
+
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
| 133 |
+
50% { transform: translateY(-15px) rotate(3deg); }
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
@keyframes pulse {
|
| 137 |
+
0%, 100% { transform: scale(1); }
|
| 138 |
+
50% { transform: scale(1.08); }
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.water-droplet-container {
|
| 142 |
+
display: flex;
|
| 143 |
+
align-items: center;
|
| 144 |
+
justify-content: center;
|
| 145 |
+
height: 100%;
|
| 146 |
+
min-height: 300px;
|
| 147 |
+
padding: 1rem;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
.chat-bubble-user {
|
| 151 |
+
background-color: var(--primary);
|
| 152 |
+
color: white;
|
| 153 |
+
border-radius: 18px 18px 0 18px;
|
| 154 |
+
padding: 12px 16px;
|
| 155 |
+
margin: 5px 0;
|
| 156 |
+
max-width: 80%;
|
| 157 |
+
margin-left: auto;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.chat-bubble-bot {
|
| 161 |
+
background-color: #eef2ff;
|
| 162 |
+
color: #111827;
|
| 163 |
+
border-radius: 18px 18px 18px 0;
|
| 164 |
+
padding: 12px 16px;
|
| 165 |
+
margin: 5px 0;
|
| 166 |
+
max-width: 80%;
|
| 167 |
+
margin-right: auto;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/* Modern chat layout */
|
| 171 |
+
.chat-container { display: flex; flex-direction: column; height: 65vh; }
|
| 172 |
+
.chat-messages { flex: 1; overflow-y: auto; padding: 12px; background: var(--light); border-radius: 8px; border: 1px solid #e5e7eb; }
|
| 173 |
+
.message-row { display: flex; gap: 10px; margin: 8px 0; align-items: flex-end; }
|
| 174 |
+
.message-row.user { flex-direction: row-reverse; }
|
| 175 |
+
.avatar { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: #dbeafe; color: #1d4ed8; font-weight: 700; flex: 0 0 36px; }
|
| 176 |
+
.avatar.bot { background: #fef3c7; color: #92400e; }
|
| 177 |
+
.bubble { padding: 10px 14px; border-radius: 14px; max-width: 75%; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
|
| 178 |
+
.bubble.user { background: var(--primary); color: white; border-top-right-radius: 4px; }
|
| 179 |
+
.bubble.bot { background: #ffffff; color: #111827; border-top-left-radius: 4px; }
|
| 180 |
+
.timestamp { font-size: 10px; opacity: 0.7; margin-top: 4px; }
|
| 181 |
+
.input-area { margin-top: 12px; }
|
| 182 |
+
|
| 183 |
+
#app-root.dark-mode .chat-messages { background: #0f172a; border-color: #1f2937; }
|
| 184 |
+
#app-root.dark-mode .bubble.bot { background: #0b1220; color: var(--text); }
|
| 185 |
+
|
| 186 |
+
.navbar-brand {
|
| 187 |
+
font-weight: 700;
|
| 188 |
+
font-size: 1.5rem;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.section-title {
|
| 192 |
+
position: relative;
|
| 193 |
+
padding-bottom: 15px;
|
| 194 |
+
margin-bottom: 25px;
|
| 195 |
+
font-weight: 700;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.section-title:after {
|
| 199 |
+
content: '';
|
| 200 |
+
position: absolute;
|
| 201 |
+
bottom: 0;
|
| 202 |
+
left: 0;
|
| 203 |
+
width: 50px;
|
| 204 |
+
height: 3px;
|
| 205 |
+
background: var(--primary);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.btn-primary {
|
| 209 |
+
background-color: var(--primary);
|
| 210 |
+
border-color: var(--primary);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.btn-primary:hover {
|
| 214 |
+
background-color: #1D6A8F;
|
| 215 |
+
border-color: #1D6A8F;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.btn-accent {
|
| 219 |
+
background-color: var(--accent);
|
| 220 |
+
border-color: var(--accent);
|
| 221 |
+
color: white;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.btn-accent:hover {
|
| 225 |
+
background-color: #D97F00;
|
| 226 |
+
border-color: #D97F00;
|
| 227 |
+
color: white;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
/* General readability and overflow safety */
|
| 231 |
+
body, #app-root, .card, .navbar, .nav-link, .tab-content, .tab-pane, .card-text, .card-title {
|
| 232 |
+
word-wrap: break-word;
|
| 233 |
+
overflow-wrap: anywhere;
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
/* Light theme component backgrounds */
|
| 237 |
+
.card { background-color: #ffffff; }
|
| 238 |
+
.bg-light { background-color: #f1f5f9 !important; }
|
| 239 |
+
.text-muted { color: #6b7280 !important; }
|
| 240 |
+
|
| 241 |
+
/* Dark mode component overrides */
|
| 242 |
+
#app-root.dark-mode .navbar, #app-root.dark-mode .navbar-light { background-color: #111827 !important; }
|
| 243 |
+
#app-root.dark-mode .navbar-brand, #app-root.dark-mode .nav-link { color: var(--text) !important; }
|
| 244 |
+
#app-root.dark-mode .card { background-color: #111827; color: var(--text); border-color: #1f2937; }
|
| 245 |
+
#app-root.dark-mode .bg-light { background-color: #0f172a !important; color: var(--text) !important; }
|
| 246 |
+
#app-root.dark-mode .text-muted { color: #9ca3af !important; }
|
| 247 |
+
#app-root.dark-mode .dropdown-menu { background-color: #0f172a; color: var(--text); }
|
| 248 |
+
#app-root.dark-mode .form-control,
|
| 249 |
+
#app-root.dark-mode .dbc-input,
|
| 250 |
+
#app-root.dark-mode .Select-control,
|
| 251 |
+
#app-root.dark-mode .Select-menu-outer { background-color: #0f172a; color: var(--text); }
|
| 252 |
+
#app-root.dark-mode .chat-bubble-bot { background-color: #0f172a; color: var(--text); }
|
| 253 |
+
#app-root.dark-mode .chat-bubble-user { background-color: var(--primary); color: #0b1220; }
|
| 254 |
+
#app-root.dark-mode .tab-content, #app-root.dark-mode .tab-pane { background-color: transparent; }
|
| 255 |
+
#app-root.dark-mode .table { color: var(--text); }
|
| 256 |
+
|
| 257 |
+
/* Chat history container */
|
| 258 |
+
#chat-history { background-color: #f8f9fa; }
|
| 259 |
+
#app-root.dark-mode #chat-history { background-color: #0b1220; border-color: #1f2937; }
|
| 260 |
+
</style>
|
| 261 |
+
</head>
|
| 262 |
+
<body>
|
| 263 |
+
{%app_entry%}
|
| 264 |
+
<footer>
|
| 265 |
+
{%config%}
|
| 266 |
+
{%scripts%}
|
| 267 |
+
{%renderer%}
|
| 268 |
+
</footer>
|
| 269 |
+
</body>
|
| 270 |
+
</html>
|
| 271 |
+
'''
|
| 272 |
+
|
| 273 |
+
try:
|
| 274 |
+
chatbot = GroqRAGChatbot()
|
| 275 |
+
CHATBOT_READY = True
|
| 276 |
+
except Exception as e:
|
| 277 |
+
print(f"Chatbot initialization error: {e}")
|
| 278 |
+
CHATBOT_READY = False
|
| 279 |
+
|
| 280 |
+
# Default placeholder figure for Forecasts tab so it never renders blank
|
| 281 |
+
def build_placeholder_forecast_figure(title_suffix="No time-series data"):
|
| 282 |
+
try:
|
| 283 |
+
x_vals = list(range(1, 11))
|
| 284 |
+
y_vals = list(range(1, 11))
|
| 285 |
+
fig = go.Figure()
|
| 286 |
+
fig.add_trace(go.Scatter(
|
| 287 |
+
x=x_vals,
|
| 288 |
+
y=y_vals,
|
| 289 |
+
mode='lines+markers',
|
| 290 |
+
name='Placeholder',
|
| 291 |
+
marker=dict(color="#2563eb"),
|
| 292 |
+
line=dict(color="#2563eb")
|
| 293 |
+
))
|
| 294 |
+
fig.update_layout(
|
| 295 |
+
title=f"Forecast Preview ({title_suffix})",
|
| 296 |
+
height=360,
|
| 297 |
+
margin=dict(l=10, r=10, t=50, b=10),
|
| 298 |
+
plot_bgcolor="#ffffff",
|
| 299 |
+
paper_bgcolor="#ffffff"
|
| 300 |
+
)
|
| 301 |
+
return fig
|
| 302 |
+
except Exception:
|
| 303 |
+
return go.Figure()
|
| 304 |
+
|
| 305 |
+
# App Layout
|
| 306 |
+
app.layout = html.Div([
|
| 307 |
+
# Top Navbar - Modern Design
|
| 308 |
+
dbc.Navbar(
|
| 309 |
+
dbc.Container([
|
| 310 |
+
html.A(
|
| 311 |
+
dbc.Row([
|
| 312 |
+
dbc.Col(html.Span("💧", style={"fontSize": "28px"})),
|
| 313 |
+
dbc.Col(dbc.NavbarBrand("India Groundwater AI", className="ms-2")),
|
| 314 |
+
], align="center", className="g-0"),
|
| 315 |
+
href="#",
|
| 316 |
+
style={"textDecoration": "none"}
|
| 317 |
+
),
|
| 318 |
+
dbc.NavbarToggler(id="navbar-toggler"),
|
| 319 |
+
dbc.Collapse(
|
| 320 |
+
dbc.Nav([], className="ms-auto", navbar=True),
|
| 321 |
+
id="navbar-collapse",
|
| 322 |
+
navbar=True,
|
| 323 |
+
),
|
| 324 |
+
dbc.Switch(id="theme-switch", value=False, label="Dark", className="ms-3"),
|
| 325 |
+
], fluid=True),
|
| 326 |
+
color="light",
|
| 327 |
+
dark=False,
|
| 328 |
+
sticky="top",
|
| 329 |
+
className="mb-4 shadow-sm"
|
| 330 |
+
),
|
| 331 |
+
|
| 332 |
+
# Main Content
|
| 333 |
+
html.Div([
|
| 334 |
+
# Top-level Tabs
|
| 335 |
+
dcc.Tabs(id="main-tabs", value="tab-home", children=[
|
| 336 |
+
# Landing Page - Completely Redesigned
|
| 337 |
+
dcc.Tab(label="Home", value="tab-home", children=[
|
| 338 |
+
dbc.Container(fluid=True, children=[
|
| 339 |
+
# Hero Section
|
| 340 |
+
dbc.Row([
|
| 341 |
+
dbc.Col([
|
| 342 |
+
html.Div([
|
| 343 |
+
html.Img(src=water_droplet_image if water_droplet_image else "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjMwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZHJvcGxldCIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdHlsZT0ic3RvcC1jb2xvcjojZmZmZmZmO3N0b3Atb3BhY2l0eToxIiAvPgogICAgICA8c3RvcCBvZmZzZXQ9IjcwJSIgc3R5bGU9InN0b3AtY29sb3I6IzAwYjNjYztzdG9wLW9wYWNpdHk6MSIgLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgPC9kZWZzPgogIDxwYXRoIGQ9Ik0xNTAgMTVMMTgwIDYwTDE1MCA5MEwxMjAgNjBaIiBmaWxsPSJ1cmwoI2Ryb3BsZXQpIi8+CiAgPHRleHQgeD0iMTUwIiB5PSIxMzUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IndoaXRlIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjAiPklOR1JFUzwvdGV4dD4KPC9zdmc+",
|
| 344 |
+
className="water-droplet",
|
| 345 |
+
alt="Water Droplet Icon")
|
| 346 |
+
], className="water-droplet-container")
|
| 347 |
+
], md=5, lg=5, className="d-flex align-items-center justify-content-center"),
|
| 348 |
+
dbc.Col([
|
| 349 |
+
html.Div([
|
| 350 |
+
html.H1("India Groundwater Intelligence Platform", className="display-4 fw-bold mb-4"),
|
| 351 |
+
html.P("Advanced AI-powered insights for sustainable groundwater management across India",
|
| 352 |
+
className="lead mb-4 text-black"),
|
| 353 |
+
dbc.Button("Get Started", id="hero-cta", color="primary", size="lg",
|
| 354 |
+
className="me-2 text-white", href="#tab-explore"),
|
| 355 |
+
dbc.Button("Live Demo", id="hero-demo", color="black", size="lg",
|
| 356 |
+
outline=True, href="#tab-chat"),
|
| 357 |
+
], className="p-5 rounded-3",
|
| 358 |
+
style={"backgroundColor": "rgba(255,255,255,0.9)"})
|
| 359 |
+
], md=7, lg=7)
|
| 360 |
+
], className="gradient-bg py-5 mb-5 text-white align-items-center",
|
| 361 |
+
style={"borderRadius": "0 0 30px 30px", "minHeight": "500px"}),
|
| 362 |
+
|
| 363 |
+
# Stats Section
|
| 364 |
+
dbc.Row([
|
| 365 |
+
dbc.Col([
|
| 366 |
+
html.Div([
|
| 367 |
+
html.Div("🌍", className="feature-icon"),
|
| 368 |
+
html.Div(id="total-districts", children="--", className="stat-number"),
|
| 369 |
+
html.Div("Districts Covered", className="stat-label")
|
| 370 |
+
], className="text-center p-4 bg-white shadow-sm rounded-3 card-hover")
|
| 371 |
+
], md=3, className="mb-3"),
|
| 372 |
+
dbc.Col([
|
| 373 |
+
html.Div([
|
| 374 |
+
html.Div("💧", className="feature-icon"),
|
| 375 |
+
html.Div(id="avg-development", children="--", className="stat-number"),
|
| 376 |
+
html.Div("Avg Development %", className="stat-label")
|
| 377 |
+
], className="text-center p-4 bg-white shadow-sm rounded-3 card-hover")
|
| 378 |
+
], md=3, className="mb-3"),
|
| 379 |
+
dbc.Col([
|
| 380 |
+
html.Div([
|
| 381 |
+
html.Div("⚠️", className="feature-icon"),
|
| 382 |
+
html.Div(id="over-exploited", children="--", className="stat-number"),
|
| 383 |
+
html.Div("Over-exploited Areas", className="stat-label")
|
| 384 |
+
], className="text-center p-4 bg-white shadow-sm rounded-3 card-hover")
|
| 385 |
+
], md=3, className="mb-3"),
|
| 386 |
+
dbc.Col([
|
| 387 |
+
html.Div([
|
| 388 |
+
html.Div("🔍", className="feature-icon"),
|
| 389 |
+
html.Div(id="critical-districts", children="--", className="stat-number"),
|
| 390 |
+
html.Div("Critical Status", className="stat-label")
|
| 391 |
+
], className="text-center p-4 bg-white shadow-sm rounded-3 card-hover")
|
| 392 |
+
], md=3, className="mb-3"),
|
| 393 |
+
], className="mb-5"),
|
| 394 |
+
|
| 395 |
+
# Features Section
|
| 396 |
+
dbc.Row([
|
| 397 |
+
dbc.Col([
|
| 398 |
+
html.H2("Key Features", className="section-title")
|
| 399 |
+
], width=12)
|
| 400 |
+
], className="mb-4"),
|
| 401 |
+
|
| 402 |
+
dbc.Row([
|
| 403 |
+
dbc.Col([
|
| 404 |
+
dbc.Card([
|
| 405 |
+
dbc.CardBody([
|
| 406 |
+
html.Div("🗺️", style={"fontSize": "3rem", "textAlign": "center", "marginBottom": "1rem"}),
|
| 407 |
+
html.H4("Interactive Maps", className="card-title"),
|
| 408 |
+
html.P("Explore groundwater data through interactive visualizations with detailed district-level information.",
|
| 409 |
+
className="card-text text-black")
|
| 410 |
+
])
|
| 411 |
+
], className="h-100 card-hover border-0 shadow-sm")
|
| 412 |
+
], md=4, className="mb-4"),
|
| 413 |
+
dbc.Col([
|
| 414 |
+
dbc.Card([
|
| 415 |
+
dbc.CardBody([
|
| 416 |
+
html.Div("🤖", style={"fontSize": "3rem", "textAlign": "center", "marginBottom": "1rem"}),
|
| 417 |
+
html.H4("AI-Powered Insights", className="card-title"),
|
| 418 |
+
html.P("Get answers to complex groundwater questions using our advanced natural language processing capabilities.",
|
| 419 |
+
className="card-text")
|
| 420 |
+
])
|
| 421 |
+
], className="h-100 card-hover border-0 shadow-sm")
|
| 422 |
+
], md=4, className="mb-4"),
|
| 423 |
+
dbc.Col([
|
| 424 |
+
dbc.Card([
|
| 425 |
+
dbc.CardBody([
|
| 426 |
+
html.Div("📊", style={"fontSize": "3rem", "textAlign": "center", "marginBottom": "1rem"}),
|
| 427 |
+
html.H4("Advanced Analytics", className="card-title"),
|
| 428 |
+
html.P("Dive deep into trends, forecasts, and comprehensive reports on groundwater availability and usage patterns.",
|
| 429 |
+
className="card-text")
|
| 430 |
+
])
|
| 431 |
+
], className="h-100 card-hover border-0 shadow-sm")
|
| 432 |
+
], md=4, className="mb-4"),
|
| 433 |
+
], className="mb-5"),
|
| 434 |
+
|
| 435 |
+
# How It Works Section
|
| 436 |
+
dbc.Row([
|
| 437 |
+
dbc.Col([
|
| 438 |
+
html.H2("How It Works", className="section-title")
|
| 439 |
+
], width=12)
|
| 440 |
+
], className="mb-4"),
|
| 441 |
+
|
| 442 |
+
dbc.Row([
|
| 443 |
+
dbc.Col([
|
| 444 |
+
dbc.Card([
|
| 445 |
+
dbc.CardBody([
|
| 446 |
+
html.Div("1", style={
|
| 447 |
+
"width": "40px",
|
| 448 |
+
"height": "40px",
|
| 449 |
+
"backgroundColor": "var(--primary)",
|
| 450 |
+
"color": "white",
|
| 451 |
+
"borderRadius": "50%",
|
| 452 |
+
"display": "flex",
|
| 453 |
+
"alignItems": "center",
|
| 454 |
+
"justifyContent": "center",
|
| 455 |
+
"marginBottom": "1rem",
|
| 456 |
+
"fontWeight": "bold"
|
| 457 |
+
}),
|
| 458 |
+
html.H4("Ask a Question", className="card-title"),
|
| 459 |
+
html.P("Type your question about groundwater data in natural language - no technical knowledge required.",
|
| 460 |
+
className="card-text")
|
| 461 |
+
])
|
| 462 |
+
], className="h-100 card-hover border-0 shadow-sm")
|
| 463 |
+
], md=4, className="mb-4"),
|
| 464 |
+
dbc.Col([
|
| 465 |
+
dbc.Card([
|
| 466 |
+
dbc.CardBody([
|
| 467 |
+
html.Div("2", style={
|
| 468 |
+
"width": "40px",
|
| 469 |
+
"height": "40px",
|
| 470 |
+
"backgroundColor": "var(--primary)",
|
| 471 |
+
"color": "white",
|
| 472 |
+
"borderRadius": "50%",
|
| 473 |
+
"display": "flex",
|
| 474 |
+
"alignItems": "center",
|
| 475 |
+
"justifyContent": "center",
|
| 476 |
+
"marginBottom": "1rem",
|
| 477 |
+
"fontWeight": "bold"
|
| 478 |
+
}),
|
| 479 |
+
html.H4("Get AI Analysis", className="card-title"),
|
| 480 |
+
html.P("Our AI processes your query, analyzes the groundwater database, and extracts relevant insights.",
|
| 481 |
+
className="card-text")
|
| 482 |
+
])
|
| 483 |
+
], className="h-100 card-hover border-0 shadow-sm")
|
| 484 |
+
], md=4, className="mb-4"),
|
| 485 |
+
dbc.Col([
|
| 486 |
+
dbc.Card([
|
| 487 |
+
dbc.CardBody([
|
| 488 |
+
html.Div("3", style={
|
| 489 |
+
"width": "40px",
|
| 490 |
+
"height": "40px",
|
| 491 |
+
"backgroundColor": "var(--primary)",
|
| 492 |
+
"color": "white",
|
| 493 |
+
"borderRadius": "50%",
|
| 494 |
+
"display": "flex",
|
| 495 |
+
"alignItems": "center",
|
| 496 |
+
"justifyContent": "center",
|
| 497 |
+
"marginBottom": "1rem",
|
| 498 |
+
"fontWeight": "bold"
|
| 499 |
+
}),
|
| 500 |
+
html.H4("Explore Results", className="card-title"),
|
| 501 |
+
html.P("View interactive maps, visualizations, and detailed reports based on the AI's findings.",
|
| 502 |
+
className="card-text")
|
| 503 |
+
])
|
| 504 |
+
], className="h-100 card-hover border-0 shadow-sm")
|
| 505 |
+
], md=4, className="mb-4"),
|
| 506 |
+
], className="mb-5"),
|
| 507 |
+
|
| 508 |
+
# Call to Action
|
| 509 |
+
dbc.Row([
|
| 510 |
+
dbc.Col([
|
| 511 |
+
dbc.Card([
|
| 512 |
+
dbc.CardBody([
|
| 513 |
+
html.H3("Ready to explore India's groundwater data?", className="text-center mb-4"),
|
| 514 |
+
html.Div([
|
| 515 |
+
dbc.Button("Start Exploring", color="primary", size="lg", className="me-3 text-white bg-blue", href="#tab-explore"),
|
| 516 |
+
dbc.Button("Chat with AI", color="", size="lg", outline=True, className="text-gray-600", href="#tab-chat"),
|
| 517 |
+
], className="d-flex justify-content-center")
|
| 518 |
+
])
|
| 519 |
+
], className="border-0 shadow-sm bg-blue")
|
| 520 |
+
], width=12)
|
| 521 |
+
]),
|
| 522 |
+
|
| 523 |
+
# Footer
|
| 524 |
+
dbc.Row([
|
| 525 |
+
dbc.Col([
|
| 526 |
+
html.Hr(),
|
| 527 |
+
html.P("India Groundwater AI Platform - Powered by Advanced Analytics and AI",
|
| 528 |
+
className="text-center text-muted mt-4")
|
| 529 |
+
], width=12)
|
| 530 |
+
], className="mt-5")
|
| 531 |
+
])
|
| 532 |
+
]),
|
| 533 |
+
|
| 534 |
+
# Explore Page (Map, Viz, Results, Details)
|
| 535 |
+
dcc.Tab(label="Explore", value="tab-explore", children=[
|
| 536 |
+
dbc.Container(fluid=True, children=[
|
| 537 |
+
dbc.Row([
|
| 538 |
+
# Left Sidebar (Quick Stats)
|
| 539 |
+
dbc.Col([
|
| 540 |
+
dbc.Card([
|
| 541 |
+
dbc.CardHeader(html.H5("📊 Quick Stats", className="mb-0")),
|
| 542 |
+
dbc.CardBody([
|
| 543 |
+
dbc.Row([
|
| 544 |
+
dbc.Col([
|
| 545 |
+
html.Div([
|
| 546 |
+
html.Div("Total Districts", className="small text-muted"),
|
| 547 |
+
html.H3(id="total-districts-explore", children="--", className="mb-0 text-primary")
|
| 548 |
+
], className="p-3 bg-light rounded-3")
|
| 549 |
+
], width=6, className="mb-3"),
|
| 550 |
+
dbc.Col([
|
| 551 |
+
html.Div([
|
| 552 |
+
html.Div("Avg Development %", className="small text-muted"),
|
| 553 |
+
html.H3(id="avg-development-explore", children="--", className="mb-0 text-warning")
|
| 554 |
+
], className="p-3 bg-light rounded-3")
|
| 555 |
+
], width=6, className="mb-3")
|
| 556 |
+
]),
|
| 557 |
+
html.Hr(className="my-3"),
|
| 558 |
+
dbc.Row([
|
| 559 |
+
dbc.Col([
|
| 560 |
+
html.Div([
|
| 561 |
+
html.Div("Over-exploited", className="small text-muted"),
|
| 562 |
+
html.H4(id="over-exploited-explore", children="--", className="mb-0 text-danger")
|
| 563 |
+
], className="p-3 bg-light rounded-3")
|
| 564 |
+
], width=6, className="mb-3"),
|
| 565 |
+
dbc.Col([
|
| 566 |
+
html.Div([
|
| 567 |
+
html.Div("Critical Status", className="small text-muted"),
|
| 568 |
+
html.H4(id="critical-districts-explore", children="--", className="mb-0 text-warning")
|
| 569 |
+
], className="p-3 bg-light rounded-3")
|
| 570 |
+
], width=6, className="mb-3")
|
| 571 |
+
])
|
| 572 |
+
])
|
| 573 |
+
], className="mb-4 shadow-sm"),
|
| 574 |
+
], width=3, style={"position": "sticky", "top": "20px", "height": "calc(100vh - 120px)", "overflowY": "auto"}),
|
| 575 |
+
|
| 576 |
+
# Right Content Tabs
|
| 577 |
+
dbc.Col([
|
| 578 |
+
dbc.Tabs([
|
| 579 |
+
dbc.Tab(label="Map", tab_id="tab-map", children=[
|
| 580 |
+
dbc.Card([
|
| 581 |
+
dbc.CardHeader([
|
| 582 |
+
html.H5("🗺️ Underground Water Coverage Map", className="mb-0"),
|
| 583 |
+
dbc.Badge(id="map-status", children="No Data", color="secondary", className="float-end")
|
| 584 |
+
]),
|
| 585 |
+
dbc.CardBody([
|
| 586 |
+
dcc.Loading(
|
| 587 |
+
id="map-loading",
|
| 588 |
+
children=[
|
| 589 |
+
html.Div(
|
| 590 |
+
id="groundwater-map",
|
| 591 |
+
style={"height": "500px", "width": "100%", "borderRadius": "8px", "overflow": "hidden"}
|
| 592 |
+
)
|
| 593 |
+
],
|
| 594 |
+
type="circle"
|
| 595 |
+
),
|
| 596 |
+
html.Div([
|
| 597 |
+
dbc.Button("Download Map (HTML)", id="download-map-html-btn", color="primary", size="sm", className="mt-3"),
|
| 598 |
+
dcc.Download(id="download-map-html")
|
| 599 |
+
], className="mt-2")
|
| 600 |
+
])
|
| 601 |
+
], className="mb-4 shadow-sm")
|
| 602 |
+
]),
|
| 603 |
+
dbc.Tab(label="Visualization", tab_id="tab-viz", children=[
|
| 604 |
+
dbc.Card([
|
| 605 |
+
dbc.CardHeader(html.H5("📈 Data Visualization", className="mb-0")),
|
| 606 |
+
dbc.CardBody([
|
| 607 |
+
dcc.Loading(
|
| 608 |
+
id="viz-loading",
|
| 609 |
+
type="circle",
|
| 610 |
+
children=[
|
| 611 |
+
dcc.Graph(id="viz-graph", figure=go.Figure(),
|
| 612 |
+
config={"displaylogo": False, "toImageButtonOptions": {"format": "png", "filename": "groundwater_visualization"}}),
|
| 613 |
+
html.Hr(),
|
| 614 |
+
dcc.Graph(id="viz-graph-2", figure=go.Figure(),
|
| 615 |
+
config={"displaylogo": False, "toImageButtonOptions": {"format": "png", "filename": "groundwater_visualization_2"}})
|
| 616 |
+
]
|
| 617 |
+
),
|
| 618 |
+
html.Div([
|
| 619 |
+
dbc.Button("Download CSV", id="download-csv-btn", color="primary", size="sm", className="me-2"),
|
| 620 |
+
dcc.Download(id="download-csv"),
|
| 621 |
+
dbc.Button("Download PNG", id="download-png-btn", color="primary", size="sm")
|
| 622 |
+
], className="mt-3")
|
| 623 |
+
])
|
| 624 |
+
], className="mb-4 shadow-sm")
|
| 625 |
+
]),
|
| 626 |
+
dbc.Tab(label="Results", tab_id="tab-results", children=[
|
| 627 |
+
dbc.Card([
|
| 628 |
+
dbc.CardHeader(html.H5("📋 Query Results", className="mb-0")),
|
| 629 |
+
dbc.CardBody([
|
| 630 |
+
html.Div(id="results-table")
|
| 631 |
+
])
|
| 632 |
+
], className="mb-4 shadow-sm")
|
| 633 |
+
]),
|
| 634 |
+
dbc.Tab(label="Details", tab_id="tab-details", children=[
|
| 635 |
+
dbc.Card([
|
| 636 |
+
dbc.CardHeader(html.H5("🔍 Query Details", className="mb-0")),
|
| 637 |
+
dbc.CardBody([
|
| 638 |
+
dbc.Collapse([
|
| 639 |
+
dbc.Card([
|
| 640 |
+
dbc.CardBody([
|
| 641 |
+
html.Pre(id="intent-display", style={"fontSize": "12px", "backgroundColor": "#f8f9fa", "padding": "15px", "borderRadius": "5px"})
|
| 642 |
+
])
|
| 643 |
+
])
|
| 644 |
+
], id="details-collapse", is_open=False),
|
| 645 |
+
dbc.Button("Show/Hide Details", id="toggle-details", color="primary", className="mt-3", size="sm")
|
| 646 |
+
])
|
| 647 |
+
], className="mb-4 shadow-sm")
|
| 648 |
+
])
|
| 649 |
+
])
|
| 650 |
+
], width=9)
|
| 651 |
+
])
|
| 652 |
+
])
|
| 653 |
+
]),
|
| 654 |
+
|
| 655 |
+
# Visualizations Page (dedicated)
|
| 656 |
+
dcc.Tab(label="Visualizations", value="tab-visualizations", children=[
|
| 657 |
+
dbc.Container(fluid=True, children=[
|
| 658 |
+
dbc.Row([
|
| 659 |
+
dbc.Col([
|
| 660 |
+
dbc.Card([
|
| 661 |
+
dbc.CardHeader(html.H5("📈 Visual Insights", className="mb-0")),
|
| 662 |
+
dbc.CardBody([
|
| 663 |
+
dcc.Loading(
|
| 664 |
+
id="viz-loading-standalone",
|
| 665 |
+
type="circle",
|
| 666 |
+
children=[
|
| 667 |
+
dcc.Graph(id="viz-graph-standalone", figure=go.Figure(),
|
| 668 |
+
config={"displaylogo": False, "toImageButtonOptions": {"format": "png", "filename": "visualization_standalone"}}),
|
| 669 |
+
html.Hr(),
|
| 670 |
+
dcc.Graph(id="viz-graph-2-standalone", figure=go.Figure(),
|
| 671 |
+
config={"displaylogo": False, "toImageButtonOptions": {"format": "png", "filename": "visualization_standalone_2"}})
|
| 672 |
+
]
|
| 673 |
+
),
|
| 674 |
+
html.Div([
|
| 675 |
+
dbc.Button("Download CSV", id="download-csv-btn-standalone", color="primary", size="sm", className="me-2"),
|
| 676 |
+
dcc.Download(id="download-csv-standalone"),
|
| 677 |
+
dbc.Button("Download PNG", id="download-png-btn-standalone", color="primary", size="sm")
|
| 678 |
+
], className="mt-3")
|
| 679 |
+
])
|
| 680 |
+
], className="mb-4 shadow-sm")
|
| 681 |
+
], width=12)
|
| 682 |
+
])
|
| 683 |
+
])
|
| 684 |
+
]),
|
| 685 |
+
|
| 686 |
+
# Reports Page
|
| 687 |
+
dcc.Tab(label="Reports", value="tab-reports", children=[
|
| 688 |
+
dbc.Container(fluid=True, children=[
|
| 689 |
+
dbc.Row([
|
| 690 |
+
dbc.Col([
|
| 691 |
+
dbc.Card([
|
| 692 |
+
dbc.CardHeader(html.H5("📄 Generate Report", className="mb-0")),
|
| 693 |
+
dbc.CardBody([
|
| 694 |
+
html.P("Download current results as CSV report."),
|
| 695 |
+
dbc.Button("Download CSV Report", id="download-csv-btn-report",className="btn-primary text-white", color="blue"),
|
| 696 |
+
dcc.Download(id="download-csv-report")
|
| 697 |
+
])
|
| 698 |
+
], className="shadow-sm")
|
| 699 |
+
], width=6)
|
| 700 |
+
], className="p-3")
|
| 701 |
+
])
|
| 702 |
+
]),
|
| 703 |
+
|
| 704 |
+
# Forecasts Page
|
| 705 |
+
dcc.Tab(label="Forecasts", value="tab-forecasts", children=[
|
| 706 |
+
dbc.Container(fluid=True, children=[
|
| 707 |
+
dbc.Row([
|
| 708 |
+
dbc.Col([
|
| 709 |
+
dbc.Card([
|
| 710 |
+
dbc.CardHeader(html.H5("🔮 Forecast (Preview)", className="mb-0")),
|
| 711 |
+
dbc.CardBody([
|
| 712 |
+
html.P("Forecasting requires time-series data. Showing a placeholder visualization of the current metric."),
|
| 713 |
+
dcc.Graph(id="viz-graph-forecast", figure=build_placeholder_forecast_figure())
|
| 714 |
+
])
|
| 715 |
+
], className="shadow-sm")
|
| 716 |
+
], width=12)
|
| 717 |
+
], className="p-3")
|
| 718 |
+
])
|
| 719 |
+
]),
|
| 720 |
+
|
| 721 |
+
# Knowledge Cards Page
|
| 722 |
+
dcc.Tab(label="Knowledge", value="tab-knowledge", children=[
|
| 723 |
+
dbc.Container(fluid=True, children=[
|
| 724 |
+
dbc.Row([
|
| 725 |
+
dbc.Col([
|
| 726 |
+
dbc.Card([
|
| 727 |
+
dbc.CardHeader(html.H5("🧠 Insights", className="mb-0 text-black")),
|
| 728 |
+
dbc.CardBody([
|
| 729 |
+
html.Div(id="knowledge-cards", className="d-flex flex-column gap-2 text-black")
|
| 730 |
+
])
|
| 731 |
+
], className="shadow-sm")
|
| 732 |
+
], width=12)
|
| 733 |
+
], className="p-3")
|
| 734 |
+
])
|
| 735 |
+
]),
|
| 736 |
+
|
| 737 |
+
# Chat Page
|
| 738 |
+
dcc.Tab(label="Chat", value="tab-chat", children=[
|
| 739 |
+
dbc.Container(fluid=True, children=[
|
| 740 |
+
dbc.Row([
|
| 741 |
+
dbc.Col([
|
| 742 |
+
dbc.Card([
|
| 743 |
+
dbc.CardHeader([
|
| 744 |
+
html.H5("💬 Ask Your Question", className="mb-0"),
|
| 745 |
+
dbc.Badge(id="status-indicator", children="Ready",
|
| 746 |
+
color="success", className="float-end")
|
| 747 |
+
]),
|
| 748 |
+
dbc.CardBody([
|
| 749 |
+
dbc.Row([
|
| 750 |
+
dbc.Col([
|
| 751 |
+
dbc.Label("Language"),
|
| 752 |
+
dcc.Dropdown(id="language-select", options=[
|
| 753 |
+
{"label": "English", "value": "en"},
|
| 754 |
+
{"label": "Hindi", "value": "hi"},
|
| 755 |
+
{"label": "Telugu", "value": "te"},
|
| 756 |
+
{"label": "Tamil", "value": "ta"},
|
| 757 |
+
], value="en", clearable=False, className="mb-3")
|
| 758 |
+
])
|
| 759 |
+
]),
|
| 760 |
+
dbc.InputGroup([
|
| 761 |
+
dbc.Input(
|
| 762 |
+
id="chat-input",
|
| 763 |
+
placeholder="Ask about groundwater data...",
|
| 764 |
+
type="text",
|
| 765 |
+
className="form-control"
|
| 766 |
+
),
|
| 767 |
+
dbc.Button("Send", id="send-btn", color="primary", n_clicks=0),
|
| 768 |
+
dbc.Button("New Chat", id="new-chat-btn", color="secondary", n_clicks=0, className="ms-2")
|
| 769 |
+
], className="mb-3"),
|
| 770 |
+
html.P("Quick examples:", className="small text-muted mb-2"),
|
| 771 |
+
dbc.ButtonGroup([
|
| 772 |
+
dbc.Button("Highest Draft", id="ex1", color="outline-primary", size="sm"),
|
| 773 |
+
dbc.Button("Over-exploited", id="ex2", color="outline-primary", size="sm"),
|
| 774 |
+
dbc.Button("Compare Districts", id="ex3", color="outline-primary", size="sm"),
|
| 775 |
+
dbc.Button("Underground Coverage", id="ex4", color="outline-primary", size="sm")
|
| 776 |
+
], className="mb-3 flex-wrap"),
|
| 777 |
+
html.Hr(),
|
| 778 |
+
html.Div(id="chat-history",
|
| 779 |
+
style={"height": "60vh", "overflowY": "auto", "border": "1px solid #dee2e6",
|
| 780 |
+
"borderRadius": "0.375rem", "padding": "10px", "backgroundColor": "#f8f9fa"})
|
| 781 |
+
])
|
| 782 |
+
], className="shadow-sm")
|
| 783 |
+
], width=8, className="mx-auto")
|
| 784 |
+
])
|
| 785 |
+
])
|
| 786 |
+
])
|
| 787 |
+
]),
|
| 788 |
+
]),
|
| 789 |
+
|
| 790 |
+
# Toast container
|
| 791 |
+
html.Div(id="toast-container"),
|
| 792 |
+
|
| 793 |
+
# Data Stores
|
| 794 |
+
dcc.Store(id="chat-data", data=[]),
|
| 795 |
+
dcc.Store(id="current-result", data={}),
|
| 796 |
+
dcc.Store(id="followup-store", data=None),
|
| 797 |
+
|
| 798 |
+
# Auto-refresh interval for stats
|
| 799 |
+
dcc.Interval(id="stats-interval", interval=30000, n_intervals=0)
|
| 800 |
+
], id="app-root", style={"minHeight": "100vh"})
|
| 801 |
+
|
| 802 |
+
# Theme toggle callback: apply dark-mode class to root
|
| 803 |
+
@app.callback(
|
| 804 |
+
Output("app-root", "className"),
|
| 805 |
+
Input("theme-switch", "value")
|
| 806 |
+
)
|
| 807 |
+
def toggle_theme(is_dark):
|
| 808 |
+
if is_dark is None:
|
| 809 |
+
raise PreventUpdate
|
| 810 |
+
return "dark-mode" if is_dark else ""
|
| 811 |
+
|
| 812 |
+
# Follow-up buttons handler: write clicked text into followup-store
|
| 813 |
+
@app.callback(
|
| 814 |
+
Output("followup-store", "data"),
|
| 815 |
+
Input({"type": "followup", "index": ALL}, "n_clicks"),
|
| 816 |
+
State({"type": "followup", "index": ALL}, "children"),
|
| 817 |
+
prevent_initial_call=True
|
| 818 |
+
)
|
| 819 |
+
def handle_followup_click(n_clicks_list, labels):
|
| 820 |
+
try:
|
| 821 |
+
if not n_clicks_list:
|
| 822 |
+
raise dash.exceptions.PreventUpdate
|
| 823 |
+
# Find which button was clicked last
|
| 824 |
+
for idx, n in enumerate(n_clicks_list):
|
| 825 |
+
# Any positive click
|
| 826 |
+
if n and n > 0:
|
| 827 |
+
return labels[idx]
|
| 828 |
+
raise dash.exceptions.PreventUpdate
|
| 829 |
+
except Exception:
|
| 830 |
+
raise dash.exceptions.PreventUpdate
|
| 831 |
+
|
| 832 |
+
# Callback for example buttons
|
| 833 |
+
@app.callback(
|
| 834 |
+
Output("chat-input", "value"),
|
| 835 |
+
[Input("ex1", "n_clicks"), Input("ex2", "n_clicks"),
|
| 836 |
+
Input("ex3", "n_clicks"), Input("ex4", "n_clicks")]
|
| 837 |
+
)
|
| 838 |
+
def set_example_query(btn1, btn2, btn3, btn4):
|
| 839 |
+
ctx = callback_context
|
| 840 |
+
if not ctx.triggered:
|
| 841 |
+
return ""
|
| 842 |
+
|
| 843 |
+
button_id = ctx.triggered[0]["prop_id"].split(".")[0]
|
| 844 |
+
examples = {
|
| 845 |
+
"ex1": "Which districts have the highest groundwater draft?",
|
| 846 |
+
"ex2": "Show me over-exploited districts with development over 100%",
|
| 847 |
+
"ex3": "Compare groundwater availability between Chennai and Coimbatore",
|
| 848 |
+
"ex4": "Show districts with largest underground water coverage areas"
|
| 849 |
+
}
|
| 850 |
+
return examples.get(button_id, "")
|
| 851 |
+
|
| 852 |
+
# Main chat processing callback
|
| 853 |
+
@app.callback(
|
| 854 |
+
[Output("chat-history", "children"),
|
| 855 |
+
Output("groundwater-map", "children"),
|
| 856 |
+
Output("results-table", "children"),
|
| 857 |
+
Output("intent-display", "children"),
|
| 858 |
+
Output("status-indicator", "children"),
|
| 859 |
+
Output("status-indicator", "color"),
|
| 860 |
+
Output("map-status", "children"),
|
| 861 |
+
Output("map-status", "color"),
|
| 862 |
+
Output("chat-data", "data"),
|
| 863 |
+
Output("current-result", "data"),
|
| 864 |
+
Output("viz-graph", "figure"),
|
| 865 |
+
Output("viz-graph-2", "figure"),
|
| 866 |
+
Output("toast-container", "children")],
|
| 867 |
+
[Input("send-btn", "n_clicks"), Input("chat-input", "n_submit"), Input("new-chat-btn", "n_clicks"), Input("followup-store", "data")],
|
| 868 |
+
[State("chat-input", "value"), State("chat-data", "data"), State("language-select", "value")]
|
| 869 |
+
)
|
| 870 |
+
def process_chat(n_clicks, n_submit, n_new_chat, followup_data, user_input, chat_data, language_value):
|
| 871 |
+
ctx = callback_context
|
| 872 |
+
# Handle New Chat reset
|
| 873 |
+
if ctx.triggered and ctx.triggered[0]["prop_id"].startswith("new-chat-btn"):
|
| 874 |
+
return ([], create_default_map(), html.P("No data to display"), "",
|
| 875 |
+
"Ready", "success", "No Data", "secondary", [], {}, go.Figure(), go.Figure(), None)
|
| 876 |
+
|
| 877 |
+
# If a follow-up was clicked, treat it as the new user input
|
| 878 |
+
if ctx.triggered and ctx.triggered[0]["prop_id"].startswith("followup-store") and followup_data:
|
| 879 |
+
user_input = followup_data
|
| 880 |
+
|
| 881 |
+
if not user_input or not user_input.strip() or not CHATBOT_READY:
|
| 882 |
+
return ([], create_default_map(), html.P("No data to display"), "",
|
| 883 |
+
"Ready", "success", "No Data", "secondary", [], {}, go.Figure(), go.Figure(), None)
|
| 884 |
+
|
| 885 |
+
try:
|
| 886 |
+
# Process query
|
| 887 |
+
result = chatbot.chat(user_input.strip())
|
| 888 |
+
|
| 889 |
+
# Update chat history
|
| 890 |
+
new_chat = chat_data + [
|
| 891 |
+
{"type": "user", "message": user_input},
|
| 892 |
+
{"type": "bot", "message": result["response"]}
|
| 893 |
+
]
|
| 894 |
+
|
| 895 |
+
# Create chat display including summaries and follow-ups
|
| 896 |
+
chat_display = []
|
| 897 |
+
for i, msg in enumerate(new_chat[-10:]): # Show last 10 messages
|
| 898 |
+
if msg["type"] == "user":
|
| 899 |
+
chat_display.append(
|
| 900 |
+
html.Div([
|
| 901 |
+
html.Strong("You: ", className="text-primary"),
|
| 902 |
+
html.Span(msg["message"])
|
| 903 |
+
], className="chat-bubble-user")
|
| 904 |
+
)
|
| 905 |
+
else:
|
| 906 |
+
chat_display.append(
|
| 907 |
+
html.Div([
|
| 908 |
+
html.Strong("🤖 Assistant: ", className="text-success"),
|
| 909 |
+
dcc.Markdown(msg["message"], dangerously_allow_html=False)
|
| 910 |
+
], className="chat-bubble-bot")
|
| 911 |
+
)
|
| 912 |
+
|
| 913 |
+
# Add optional summary card and follow-ups from backend
|
| 914 |
+
summary_text = result.get("summary")
|
| 915 |
+
follow_ups = result.get("follow_ups", [])
|
| 916 |
+
if summary_text or follow_ups:
|
| 917 |
+
extras = []
|
| 918 |
+
if summary_text:
|
| 919 |
+
extras.append(
|
| 920 |
+
dbc.Alert([html.Strong("Summary: ", className="me-1"), html.Span(summary_text)], color="info", className="mb-2")
|
| 921 |
+
)
|
| 922 |
+
if follow_ups:
|
| 923 |
+
extras.append(
|
| 924 |
+
html.Div([
|
| 925 |
+
html.Div("Try these:", className="small text-muted mb-1"),
|
| 926 |
+
dbc.ButtonGroup([
|
| 927 |
+
*[dbc.Button(q, id={"type": "followup", "index": i}, color="outline-primary", size="sm", className="me-1 mb-1") for i, q in enumerate(follow_ups)]
|
| 928 |
+
], className="flex-wrap")
|
| 929 |
+
], className="mb-3")
|
| 930 |
+
)
|
| 931 |
+
chat_display.extend(extras)
|
| 932 |
+
|
| 933 |
+
# Create enhanced groundwater map
|
| 934 |
+
groundwater_map = create_underground_water_map(result["results"])
|
| 935 |
+
|
| 936 |
+
# Create results table
|
| 937 |
+
results_table = create_results_table(result["results"])
|
| 938 |
+
|
| 939 |
+
# Format intent details
|
| 940 |
+
intent_display = json.dumps(result["intent_analysis"], indent=2)
|
| 941 |
+
|
| 942 |
+
# Status updates
|
| 943 |
+
status = f"Found {result['results_count']} results"
|
| 944 |
+
status_color = "success" if result["success"] else "danger"
|
| 945 |
+
|
| 946 |
+
# Enhanced map status with underground water info
|
| 947 |
+
underground_districts = len([r for r in result['results'] if r.get('geometry') and r.get('st_area_shape')])
|
| 948 |
+
map_status = f"Mapped {underground_districts} districts with underground coverage"
|
| 949 |
+
map_color = "info" if underground_districts > 0 else "warning"
|
| 950 |
+
|
| 951 |
+
# Build visualization figures if spec provided
|
| 952 |
+
viz_fig = build_visualization_figure(result)
|
| 953 |
+
viz_fig_2 = build_secondary_visualization_figure(result)
|
| 954 |
+
|
| 955 |
+
# Toast notification (success)
|
| 956 |
+
toast = dbc.Toast(
|
| 957 |
+
[html.Div(f"{status}")],
|
| 958 |
+
header="Query Processed",
|
| 959 |
+
icon="success",
|
| 960 |
+
duration=4000,
|
| 961 |
+
is_open=True,
|
| 962 |
+
style={"position": "fixed", "top": 10, "right": 10, "zIndex": 2000}
|
| 963 |
+
)
|
| 964 |
+
|
| 965 |
+
return (chat_display, groundwater_map, results_table, intent_display,
|
| 966 |
+
status, status_color, map_status, map_color, new_chat, result, viz_fig, viz_fig_2, toast)
|
| 967 |
+
|
| 968 |
+
except Exception as e:
|
| 969 |
+
error_msg = f"Error processing query: {str(e)}"
|
| 970 |
+
error_chat = chat_data + [
|
| 971 |
+
{"type": "user", "message": user_input},
|
| 972 |
+
{"type": "bot", "message": error_msg}
|
| 973 |
+
]
|
| 974 |
+
|
| 975 |
+
chat_display = [
|
| 976 |
+
html.Div([
|
| 977 |
+
html.Strong("Error: ", className="text-danger"),
|
| 978 |
+
html.Span(error_msg)
|
| 979 |
+
], className="mb-2 p-2 bg-danger text-white rounded")
|
| 980 |
+
]
|
| 981 |
+
|
| 982 |
+
toast_err = dbc.Toast(
|
| 983 |
+
[html.Div(error_msg)],
|
| 984 |
+
header="Error",
|
| 985 |
+
icon="danger",
|
| 986 |
+
duration=6000,
|
| 987 |
+
is_open=True,
|
| 988 |
+
style={"position": "fixed", "top": 10, "right": 10, "zIndex": 2000}
|
| 989 |
+
)
|
| 990 |
+
return (chat_display, create_default_map(), html.P("Error occurred"), error_msg,
|
| 991 |
+
"Error", "danger", "Error", "danger", error_chat, {}, go.Figure(), go.Figure(), toast_err)
|
| 992 |
+
|
| 993 |
+
# Quick stats callback
|
| 994 |
+
@app.callback(
|
| 995 |
+
[Output("total-districts", "children"),
|
| 996 |
+
Output("avg-development", "children"),
|
| 997 |
+
Output("over-exploited", "children"),
|
| 998 |
+
Output("critical-districts", "children"),
|
| 999 |
+
Output("total-districts-explore", "children"),
|
| 1000 |
+
Output("avg-development-explore", "children"),
|
| 1001 |
+
Output("over-exploited-explore", "children"),
|
| 1002 |
+
Output("critical-districts-explore", "children")],
|
| 1003 |
+
[Input("stats-interval", "n_intervals")]
|
| 1004 |
+
)
|
| 1005 |
+
def update_stats(n_intervals):
|
| 1006 |
+
if not CHATBOT_READY:
|
| 1007 |
+
return "--", "--", "--", "--", "--", "--", "--", "--"
|
| 1008 |
+
|
| 1009 |
+
try:
|
| 1010 |
+
stats = chatbot.get_quick_stats()
|
| 1011 |
+
return (
|
| 1012 |
+
str(stats.get("total_districts", "--")),
|
| 1013 |
+
f"{stats.get('avg_development', '--')}%",
|
| 1014 |
+
str(stats.get("over_exploited", "--")),
|
| 1015 |
+
str(stats.get("critical", "--")),
|
| 1016 |
+
str(stats.get("total_districts", "--")),
|
| 1017 |
+
f"{stats.get('avg_development', '--')}%",
|
| 1018 |
+
str(stats.get("over_exploited", "--")),
|
| 1019 |
+
str(stats.get("critical", "--"))
|
| 1020 |
+
)
|
| 1021 |
+
except Exception:
|
| 1022 |
+
return "--", "--", "--", "--", "--", "--", "--", "--"
|
| 1023 |
+
|
| 1024 |
+
# Toggle details callback
|
| 1025 |
+
@app.callback(
|
| 1026 |
+
Output("details-collapse", "is_open"),
|
| 1027 |
+
[Input("toggle-details", "n_clicks")],
|
| 1028 |
+
[State("details-collapse", "is_open")]
|
| 1029 |
+
)
|
| 1030 |
+
def toggle_details(n_clicks, is_open):
|
| 1031 |
+
if n_clicks:
|
| 1032 |
+
return not is_open
|
| 1033 |
+
return is_open
|
| 1034 |
+
|
| 1035 |
+
def create_default_map():
|
| 1036 |
+
"""Create default India map"""
|
| 1037 |
+
try:
|
| 1038 |
+
# India center coordinates
|
| 1039 |
+
india_center = [20.5937, 78.9629]
|
| 1040 |
+
|
| 1041 |
+
m = folium.Map(
|
| 1042 |
+
location=india_center,
|
| 1043 |
+
zoom_start=5, # Zoom out for India view
|
| 1044 |
+
tiles='OpenStreetMap'
|
| 1045 |
+
)
|
| 1046 |
+
|
| 1047 |
+
# Add a marker for India center
|
| 1048 |
+
folium.Marker(
|
| 1049 |
+
india_center,
|
| 1050 |
+
popup="India - Underground Water Data Center",
|
| 1051 |
+
tooltip="Click for more info",
|
| 1052 |
+
icon=folium.Icon(color='blue', icon='tint')
|
| 1053 |
+
).add_to(m)
|
| 1054 |
+
|
| 1055 |
+
# Add title
|
| 1056 |
+
title_html = '''
|
| 1057 |
+
<h3 align="center" style="font-size:16px"><b>India Underground Water Coverage</b></h3>
|
| 1058 |
+
'''
|
| 1059 |
+
m.get_root().html.add_child(folium.Element(title_html))
|
| 1060 |
+
|
| 1061 |
+
return html.Iframe(
|
| 1062 |
+
srcDoc=m._repr_html_(),
|
| 1063 |
+
style={"width": "100%", "height": "450px", "border": "none"}
|
| 1064 |
+
)
|
| 1065 |
+
except Exception as e:
|
| 1066 |
+
return html.Div([
|
| 1067 |
+
html.H5("Map Loading Error", className="text-center text-muted"),
|
| 1068 |
+
html.P(f"Unable to load map: {str(e)}", className="text-center")
|
| 1069 |
+
], style={"height": "450px", "display": "flex", "flexDirection": "column",
|
| 1070 |
+
"justifyContent": "center", "alignItems": "center"})
|
| 1071 |
+
|
| 1072 |
+
def extract_centroid(geometry):
|
| 1073 |
+
"""Extract centroid from WKT geometry with proper coordinate handling"""
|
| 1074 |
+
try:
|
| 1075 |
+
# Parse the WKT geometry
|
| 1076 |
+
geom = wkt.loads(geometry)
|
| 1077 |
+
|
| 1078 |
+
# Get the centroid
|
| 1079 |
+
centroid = geom.centroid
|
| 1080 |
+
|
| 1081 |
+
# Extract coordinates and swap them (lat, lon instead of lon, lat)
|
| 1082 |
+
# Database stores coordinates as (longitude, latitude) but we need (latitude, longitude)
|
| 1083 |
+
coords = list(centroid.coords)[0]
|
| 1084 |
+
|
| 1085 |
+
# Return as (latitude, longitude)
|
| 1086 |
+
return (coords[1], coords[0])
|
| 1087 |
+
|
| 1088 |
+
except Exception as e:
|
| 1089 |
+
print(f"Error parsing geometry: {e}")
|
| 1090 |
+
return None
|
| 1091 |
+
|
| 1092 |
+
def create_underground_water_map(results):
|
| 1093 |
+
"""Create interactive underground water coverage map with proper coordinate handling"""
|
| 1094 |
+
if not results:
|
| 1095 |
+
return create_default_map()
|
| 1096 |
+
|
| 1097 |
+
try:
|
| 1098 |
+
# Convert results to dataframe
|
| 1099 |
+
df = pd.DataFrame(results)
|
| 1100 |
+
|
| 1101 |
+
# Check if we have geometry data
|
| 1102 |
+
if 'geometry' not in df.columns:
|
| 1103 |
+
return create_simple_data_map(df)
|
| 1104 |
+
|
| 1105 |
+
# Filter out rows without geometry or underground water data
|
| 1106 |
+
df_with_geo = df[(df['geometry'].notna()) &
|
| 1107 |
+
(df['st_area_shape'].notna()) &
|
| 1108 |
+
(df['st_length_shape'].notna())].copy()
|
| 1109 |
+
|
| 1110 |
+
if len(df_with_geo) == 0:
|
| 1111 |
+
return create_simple_data_map(df)
|
| 1112 |
+
|
| 1113 |
+
# India center coordinates
|
| 1114 |
+
india_center = [20.5937, 78.9629]
|
| 1115 |
+
|
| 1116 |
+
# Create map
|
| 1117 |
+
m = folium.Map(
|
| 1118 |
+
location=india_center,
|
| 1119 |
+
zoom_start=5,
|
| 1120 |
+
tiles='CartoDB positron'
|
| 1121 |
+
)
|
| 1122 |
+
|
| 1123 |
+
# Calculate underground water coverage intensity
|
| 1124 |
+
if len(df_with_geo) > 0:
|
| 1125 |
+
max_area = df_with_geo['st_area_shape'].max()
|
| 1126 |
+
min_area = df_with_geo['st_area_shape'].min()
|
| 1127 |
+
|
| 1128 |
+
# Add districts with enhanced underground water visualization
|
| 1129 |
+
for idx, row in df_with_geo.iterrows():
|
| 1130 |
+
try:
|
| 1131 |
+
# Parse WKT geometry
|
| 1132 |
+
geom = wkt.loads(row['geometry'])
|
| 1133 |
+
|
| 1134 |
+
# Calculate underground water coverage intensity
|
| 1135 |
+
area_intensity = (row['st_area_shape'] - min_area) / (max_area - min_area) if max_area > min_area else 0.5
|
| 1136 |
+
|
| 1137 |
+
# Determine color based on development stage and underground coverage
|
| 1138 |
+
dev_stage = row.get('stage_of_development', 0)
|
| 1139 |
+
if pd.isna(dev_stage):
|
| 1140 |
+
color = 'gray'
|
| 1141 |
+
elif dev_stage > 100:
|
| 1142 |
+
color = 'red' # Over-exploited
|
| 1143 |
+
elif dev_stage > 80:
|
| 1144 |
+
color = 'orange' # Critical
|
| 1145 |
+
elif dev_stage > 60:
|
| 1146 |
+
color = 'yellow' # Semi-critical
|
| 1147 |
+
else:
|
| 1148 |
+
color = 'green' # Safe
|
| 1149 |
+
|
| 1150 |
+
# Adjust opacity based on underground coverage area
|
| 1151 |
+
fill_opacity = max(0.3, min(0.9, 0.3 + (area_intensity * 0.6)))
|
| 1152 |
+
|
| 1153 |
+
# Create enhanced popup content with underground water info
|
| 1154 |
+
popup_content = create_underground_popup_content(row)
|
| 1155 |
+
|
| 1156 |
+
# Add geometry to map with enhanced styling
|
| 1157 |
+
if geom.geom_type == 'MultiPolygon':
|
| 1158 |
+
for polygon in geom.geoms:
|
| 1159 |
+
# Extract coordinates and swap them (lat, lon instead of lon, lat)
|
| 1160 |
+
coords = [[point[1], point[0]] for point in polygon.exterior.coords]
|
| 1161 |
+
folium.Polygon(
|
| 1162 |
+
locations=coords,
|
| 1163 |
+
color='black',
|
| 1164 |
+
weight=2,
|
| 1165 |
+
fillColor=color,
|
| 1166 |
+
fillOpacity=fill_opacity,
|
| 1167 |
+
popup=folium.Popup(popup_content, max_width=500),
|
| 1168 |
+
tooltip=f"{row.get('district', 'Unknown District')} - Coverage: {row.get('st_area_shape', 0):,.0f} sq.m"
|
| 1169 |
+
).add_to(m)
|
| 1170 |
+
elif geom.geom_type == 'Polygon':
|
| 1171 |
+
# Extract coordinates and swap them (lat, lon instead of lon, lat)
|
| 1172 |
+
coords = [[point[1], point[0]] for point in geom.exterior.coords]
|
| 1173 |
+
folium.Polygon(
|
| 1174 |
+
locations=coords,
|
| 1175 |
+
color='black',
|
| 1176 |
+
weight=2,
|
| 1177 |
+
fillColor=color,
|
| 1178 |
+
fillOpacity=fill_opacity,
|
| 1179 |
+
popup=folium.Popup(popup_content, max_width=500),
|
| 1180 |
+
tooltip=f"{row.get('district', 'Unknown District')} - Coverage: {row.get('st_area_shape', 0):,.0f} sq.m"
|
| 1181 |
+
).add_to(m)
|
| 1182 |
+
|
| 1183 |
+
except Exception as e:
|
| 1184 |
+
print(f"Error processing geometry for {row.get('district', 'unknown')}: {e}")
|
| 1185 |
+
continue
|
| 1186 |
+
|
| 1187 |
+
# Add enhanced legend
|
| 1188 |
+
add_underground_legend_to_map(m)
|
| 1189 |
+
|
| 1190 |
+
# Add title
|
| 1191 |
+
title_html = f'''
|
| 1192 |
+
<h3 align="center" style="font-size:16px"><b>India Underground Water Coverage - {len(df_with_geo)} Districts</b></h3>
|
| 1193 |
+
<p align="center" style="font-size:12px; color: #666;">Opacity indicates underground water coverage area intensity</p>
|
| 1194 |
+
'''
|
| 1195 |
+
m.get_root().html.add_child(folium.Element(title_html))
|
| 1196 |
+
|
| 1197 |
+
return html.Iframe(
|
| 1198 |
+
srcDoc=m._repr_html_(),
|
| 1199 |
+
style={"width": "100%", "height": "450px", "border": "none"}
|
| 1200 |
+
)
|
| 1201 |
+
|
| 1202 |
+
except Exception as e:
|
| 1203 |
+
print(f"Error creating underground water map: {e}")
|
| 1204 |
+
return create_default_map()
|
| 1205 |
+
|
| 1206 |
+
def create_underground_popup_content(row):
|
| 1207 |
+
"""Create enhanced HTML popup content with underground water details"""
|
| 1208 |
+
district = row.get('district', 'Unknown')
|
| 1209 |
+
|
| 1210 |
+
content = f"<b>{district} District</b><br><hr>"
|
| 1211 |
+
|
| 1212 |
+
# Underground Water Coverage Section
|
| 1213 |
+
content += "<b>🏔️ Underground Water Coverage:</b><br>"
|
| 1214 |
+
if 'st_area_shape' in row and not pd.isna(row['st_area_shape']):
|
| 1215 |
+
content += f"Coverage Area: <b>{row['st_area_shape']:,.0f} sq.m</b><br>"
|
| 1216 |
+
if 'st_length_shape' in row and not pd.isna(row['st_length_shape']):
|
| 1217 |
+
content += f"Perimeter: <b>{row['st_length_shape']:,.0f} m</b><br>"
|
| 1218 |
+
|
| 1219 |
+
content += "<br><b>💧 Groundwater Metrics:</b><br>"
|
| 1220 |
+
|
| 1221 |
+
# Add key groundwater metrics
|
| 1222 |
+
metrics = [
|
| 1223 |
+
('Development Stage', 'stage_of_development', '%'),
|
| 1224 |
+
('Total Draft', 'annual_gw_draft_total', ' HM'),
|
| 1225 |
+
('Net Availability', 'net_gw_availability', ' HM'),
|
| 1226 |
+
('Replenishable Resource', 'annual_replenishable_gw_resource', ' HM'),
|
| 1227 |
+
('Irrigation Draft', 'annual_draft_irrigation', ' HM')
|
| 1228 |
+
]
|
| 1229 |
+
|
| 1230 |
+
for label, key, unit in metrics:
|
| 1231 |
+
if key in row and not pd.isna(row[key]):
|
| 1232 |
+
value = row[key]
|
| 1233 |
+
if isinstance(value, (int, float)):
|
| 1234 |
+
if unit == '%':
|
| 1235 |
+
content += f"{label}: <b>{value:.1f}{unit}</b><br>"
|
| 1236 |
+
elif unit == ' HM':
|
| 1237 |
+
content += f"{label}: <b>{value:,.0f}{unit}</b><br>"
|
| 1238 |
+
else:
|
| 1239 |
+
content += f"{label}: <b>{value}{unit}</b><br>"
|
| 1240 |
+
|
| 1241 |
+
# Add underground water assessment
|
| 1242 |
+
if 'st_area_shape' in row and not pd.isna(row['st_area_shape']):
|
| 1243 |
+
area = row['st_area_shape']
|
| 1244 |
+
if area > 3000000000: # > 3 billion sq.m
|
| 1245 |
+
assessment = "🟢 Extensive underground coverage"
|
| 1246 |
+
elif area > 1500000000: # > 1.5 billion sq.m
|
| 1247 |
+
assessment = "🟡 Moderate underground coverage"
|
| 1248 |
+
else:
|
| 1249 |
+
assessment = "🔴 Limited underground coverage"
|
| 1250 |
+
|
| 1251 |
+
content += f"<br><b>Assessment:</b> {assessment}"
|
| 1252 |
+
|
| 1253 |
+
return content
|
| 1254 |
+
|
| 1255 |
+
def add_underground_legend_to_map(m):
|
| 1256 |
+
"""Add enhanced color legend for underground water coverage"""
|
| 1257 |
+
legend_html = '''
|
| 1258 |
+
<div style="position: fixed;
|
| 1259 |
+
bottom: 50px; left: 50px; width: 200px; height: 160px;
|
| 1260 |
+
background-color: white; border:2px solid grey; z-index:9999;
|
| 1261 |
+
font-size:12px; padding: 10px; border-radius: 5px;">
|
| 1262 |
+
<p><b>Development Stage:</b></p>
|
| 1263 |
+
<p><i class="fa fa-square" style="color:green"></i> Safe (<60%)</p>
|
| 1264 |
+
<p><i class="fa fa-square" style="color:yellow"></i> Semi-critical (60-80%)</p>
|
| 1265 |
+
<p><i class="fa fa-square" style="color:orange"></i> Critical (80-100%)</p>
|
| 1266 |
+
<p><i class="fa fa-square" style="color:red"></i> Over-exploited (>100%)</p>
|
| 1267 |
+
<hr>
|
| 1268 |
+
<p><b>Underground Coverage:</b></p>
|
| 1269 |
+
<p>Opacity = Coverage area intensity</p>
|
| 1270 |
+
</div>
|
| 1271 |
+
'''
|
| 1272 |
+
m.get_root().html.add_child(folium.Element(legend_html))
|
| 1273 |
+
|
| 1274 |
+
def create_simple_data_map(df):
|
| 1275 |
+
"""Create simple marker-based map when no geometry available"""
|
| 1276 |
+
try:
|
| 1277 |
+
# India center coordinates
|
| 1278 |
+
india_center = [20.5937, 78.9629]
|
| 1279 |
+
m = folium.Map(location=india_center, zoom_start=5, tiles='OpenStreetMap')
|
| 1280 |
+
|
| 1281 |
+
# Add markers for districts in the data
|
| 1282 |
+
for idx, row in df.iterrows():
|
| 1283 |
+
district = row.get('district', '').strip()
|
| 1284 |
+
|
| 1285 |
+
# Use a simple approach - just place markers at approximate locations
|
| 1286 |
+
# In a real implementation, you'd want to geocode district names
|
| 1287 |
+
# For now, we'll just use random coordinates around India
|
| 1288 |
+
import random
|
| 1289 |
+
lat = 20.5937 + random.uniform(-5, 5)
|
| 1290 |
+
lon = 78.9629 + random.uniform(-5, 5)
|
| 1291 |
+
|
| 1292 |
+
# Determine marker color based on development stage
|
| 1293 |
+
dev_stage = row.get('stage_of_development', 0)
|
| 1294 |
+
if pd.isna(dev_stage):
|
| 1295 |
+
color = 'gray'
|
| 1296 |
+
elif dev_stage > 100:
|
| 1297 |
+
color = 'red'
|
| 1298 |
+
elif dev_stage > 80:
|
| 1299 |
+
color = 'orange'
|
| 1300 |
+
elif dev_stage > 60:
|
| 1301 |
+
color = 'blue'
|
| 1302 |
+
else:
|
| 1303 |
+
color = 'green'
|
| 1304 |
+
|
| 1305 |
+
# Create popup with underground water info
|
| 1306 |
+
popup_content = create_underground_popup_content(row)
|
| 1307 |
+
|
| 1308 |
+
folium.Marker(
|
| 1309 |
+
[lat, lon],
|
| 1310 |
+
popup=folium.Popup(popup_content, max_width=400),
|
| 1311 |
+
tooltip=f"{district} - Underground coverage available",
|
| 1312 |
+
icon=folium.Icon(color=color, icon='tint')
|
| 1313 |
+
).add_to(m)
|
| 1314 |
+
|
| 1315 |
+
# Add title
|
| 1316 |
+
title_html = f'''
|
| 1317 |
+
<h3 align="center" style="font-size:16px"><b>India Districts - {len(df)} Locations</b></h3>
|
| 1318 |
+
'''
|
| 1319 |
+
m.get_root().html.add_child(folium.Element(title_html))
|
| 1320 |
+
|
| 1321 |
+
return html.Iframe(
|
| 1322 |
+
srcDoc=m._repr_html_(),
|
| 1323 |
+
style={"width": "100%", "height": "450px", "border": "none"}
|
| 1324 |
+
)
|
| 1325 |
+
|
| 1326 |
+
except Exception as e:
|
| 1327 |
+
return create_default_map()
|
| 1328 |
+
|
| 1329 |
+
def create_results_table(results):
|
| 1330 |
+
"""Create enhanced results table with underground water columns"""
|
| 1331 |
+
if not results:
|
| 1332 |
+
return html.P("No results to display", className="text-muted")
|
| 1333 |
+
|
| 1334 |
+
df = pd.DataFrame(results)
|
| 1335 |
+
|
| 1336 |
+
# Prioritize columns including underground water metrics
|
| 1337 |
+
display_cols = []
|
| 1338 |
+
priority_cols = ['district', 'st_area_shape', 'st_length_shape', 'annual_gw_draft_total',
|
| 1339 |
+
'stage_of_development', 'net_gw_availability']
|
| 1340 |
+
|
| 1341 |
+
for col in priority_cols:
|
| 1342 |
+
if col in df.columns:
|
| 1343 |
+
display_cols.append(col)
|
| 1344 |
+
|
| 1345 |
+
# Add other columns except geometry
|
| 1346 |
+
other_cols = [col for col in df.columns if col not in priority_cols and col != 'geometry']
|
| 1347 |
+
display_cols.extend(other_cols[:3])
|
| 1348 |
+
|
| 1349 |
+
if display_cols:
|
| 1350 |
+
display_df = df[display_cols].head(10)
|
| 1351 |
+
|
| 1352 |
+
# Format column names for better readability
|
| 1353 |
+
column_names = []
|
| 1354 |
+
for col in display_cols:
|
| 1355 |
+
if col == 'st_area_shape':
|
| 1356 |
+
column_names.append({"name": "Underground Coverage (sq.m)", "id": col, "type": "numeric", "format": {"specifier": ",.0f"}})
|
| 1357 |
+
elif col == 'st_length_shape':
|
| 1358 |
+
column_names.append({"name": "Underground Perimeter (m)", "id": col, "type": "numeric", "format": {"specifier": ",.0f"}})
|
| 1359 |
+
else:
|
| 1360 |
+
column_names.append({"name": col.replace('_', ' ').title(), "id": col})
|
| 1361 |
+
|
| 1362 |
+
return dash_table.DataTable(
|
| 1363 |
+
data=display_df.to_dict('records'),
|
| 1364 |
+
columns=column_names,
|
| 1365 |
+
style_cell={'textAlign': 'left', 'fontSize': '12px', 'padding': '8px', 'whiteSpace': 'normal', 'height': 'auto'},
|
| 1366 |
+
style_header={'backgroundColor': 'rgb(230, 230, 230)', 'fontWeight': 'bold'},
|
| 1367 |
+
style_data_conditional=[
|
| 1368 |
+
{
|
| 1369 |
+
'if': {
|
| 1370 |
+
'filter_query': '{stage_of_development} > 100',
|
| 1371 |
+
'column_id': 'stage_of_development'
|
| 1372 |
+
},
|
| 1373 |
+
'backgroundColor': '#ffebee',
|
| 1374 |
+
'color': 'black',
|
| 1375 |
+
},
|
| 1376 |
+
{
|
| 1377 |
+
'if': {
|
| 1378 |
+
'filter_query': '{stage_of_development} > 80 && {stage_of_development} <= 100',
|
| 1379 |
+
'column_id': 'stage_of_development'
|
| 1380 |
+
},
|
| 1381 |
+
'backgroundColor': '#fff3e0',
|
| 1382 |
+
'color': 'black',
|
| 1383 |
+
},
|
| 1384 |
+
# Highlight large underground coverage areas
|
| 1385 |
+
{
|
| 1386 |
+
'if': {
|
| 1387 |
+
'filter_query': '{st_area_shape} > 3000000000',
|
| 1388 |
+
'column_id': 'st_area_shape'
|
| 1389 |
+
},
|
| 1390 |
+
'backgroundColor': '#e8f5e8',
|
| 1391 |
+
'color': 'black',
|
| 1392 |
+
}
|
| 1393 |
+
],
|
| 1394 |
+
page_size=10,
|
| 1395 |
+
sort_action="native",
|
| 1396 |
+
filter_action="native",
|
| 1397 |
+
tooltip_data=[
|
| 1398 |
+
{
|
| 1399 |
+
column: {'value': 'Underground water coverage area in square meters', 'type': 'markdown'}
|
| 1400 |
+
if column == 'st_area_shape'
|
| 1401 |
+
else {'value': 'Underground water perimeter in meters', 'type': 'markdown'}
|
| 1402 |
+
if column == 'st_length_shape'
|
| 1403 |
+
else {'value': str(value), 'type': 'text'}
|
| 1404 |
+
for column, value in row.items()
|
| 1405 |
+
} for row in display_df.to_dict('records')
|
| 1406 |
+
],
|
| 1407 |
+
tooltip_duration=None
|
| 1408 |
+
)
|
| 1409 |
+
|
| 1410 |
+
return html.P("Unable to display results", className="text-muted")
|
| 1411 |
+
|
| 1412 |
+
def build_visualization_figure(result_dict):
|
| 1413 |
+
"""Create a Plotly figure based on backend-provided visualization spec and results."""
|
| 1414 |
+
try:
|
| 1415 |
+
viz = result_dict.get("visualization", {}) if isinstance(result_dict, dict) else {}
|
| 1416 |
+
results = result_dict.get("results", []) if isinstance(result_dict, dict) else []
|
| 1417 |
+
if not viz or not viz.get("enabled") or not results:
|
| 1418 |
+
return go.Figure()
|
| 1419 |
+
|
| 1420 |
+
df = pd.DataFrame(results)
|
| 1421 |
+
# Coerce numeric columns safely
|
| 1422 |
+
for col in [viz.get("y"), viz.get("x")]:
|
| 1423 |
+
if col and col in df.columns:
|
| 1424 |
+
try:
|
| 1425 |
+
df[col] = pd.to_numeric(df[col], errors='coerce') if col != 'district' else df[col]
|
| 1426 |
+
except Exception:
|
| 1427 |
+
pass
|
| 1428 |
+
|
| 1429 |
+
chart_type = viz.get("chart_type", "bar")
|
| 1430 |
+
x_col = viz.get("x")
|
| 1431 |
+
y_col = viz.get("y")
|
| 1432 |
+
top_n = viz.get("top_n", 10)
|
| 1433 |
+
title = viz.get("title", "Data Visualization")
|
| 1434 |
+
|
| 1435 |
+
# Reduce to top_n by y if possible
|
| 1436 |
+
plot_df = df.copy()
|
| 1437 |
+
if y_col in plot_df.columns and pd.api.types.is_numeric_dtype(plot_df[y_col]):
|
| 1438 |
+
plot_df = plot_df.sort_values(by=y_col, ascending=False).head(top_n)
|
| 1439 |
+
else:
|
| 1440 |
+
plot_df = plot_df.head(top_n)
|
| 1441 |
+
|
| 1442 |
+
if chart_type == "histogram" and x_col and x_col in plot_df.columns:
|
| 1443 |
+
fig = px.histogram(plot_df, x=x_col, nbins=20, title=title)
|
| 1444 |
+
elif chart_type == "scatter" and x_col and y_col and x_col in plot_df.columns and y_col in plot_df.columns:
|
| 1445 |
+
fig = px.scatter(plot_df, x=x_col, y=y_col, hover_data=[c for c in plot_df.columns if c not in ['geometry']], title=title)
|
| 1446 |
+
else:
|
| 1447 |
+
# Default to bar; pick axis intelligently
|
| 1448 |
+
if (not x_col or x_col not in plot_df.columns) and 'district' in plot_df.columns:
|
| 1449 |
+
x_col = 'district'
|
| 1450 |
+
if not y_col or y_col not in plot_df.columns:
|
| 1451 |
+
# choose a numeric column fallback
|
| 1452 |
+
candidates = [c for c in [
|
| 1453 |
+
'annual_gw_draft_total', 'stage_of_development', 'net_gw_availability',
|
| 1454 |
+
'annual_replenishable_gw_resource', 'annual_draft_irrigation',
|
| 1455 |
+
'st_area_shape', 'st_length_shape'
|
| 1456 |
+
] if c in plot_df.columns]
|
| 1457 |
+
y_col = candidates[0] if candidates else None
|
| 1458 |
+
|
| 1459 |
+
if x_col and y_col and y_col in plot_df.columns:
|
| 1460 |
+
fig = px.bar(plot_df, x=x_col, y=y_col, title=title, hover_data=[c for c in plot_df.columns if c not in ['geometry']])
|
| 1461 |
+
else:
|
| 1462 |
+
fig = go.Figure()
|
| 1463 |
+
|
| 1464 |
+
fig.update_layout(margin=dict(l=10, r=10, t=50, b=10), height=400)
|
| 1465 |
+
return fig
|
| 1466 |
+
except Exception:
|
| 1467 |
+
return go.Figure()
|
| 1468 |
+
|
| 1469 |
+
def build_secondary_visualization_figure(result_dict):
|
| 1470 |
+
"""Second complementary chart (e.g., perimeter vs area scatter)."""
|
| 1471 |
+
try:
|
| 1472 |
+
results = result_dict.get("results", []) if isinstance(result_dict, dict) else []
|
| 1473 |
+
if not results:
|
| 1474 |
+
return go.Figure()
|
| 1475 |
+
|
| 1476 |
+
df = pd.DataFrame(results)
|
| 1477 |
+
if not {'st_area_shape', 'st_length_shape'}.issubset(df.columns):
|
| 1478 |
+
return go.Figure()
|
| 1479 |
+
|
| 1480 |
+
# Coerce numeric
|
| 1481 |
+
for col in ['st_area_shape', 'st_length_shape']:
|
| 1482 |
+
if col in df.columns:
|
| 1483 |
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 1484 |
+
|
| 1485 |
+
# Keep top 50 by area
|
| 1486 |
+
df_plot = df.sort_values(by='st_area_shape', ascending=False).head(50)
|
| 1487 |
+
if 'district' not in df_plot.columns:
|
| 1488 |
+
df_plot['district'] = [f"District {i+1}" for i in range(len(df_plot))]
|
| 1489 |
+
|
| 1490 |
+
fig = px.scatter(
|
| 1491 |
+
df_plot,
|
| 1492 |
+
x='st_area_shape',
|
| 1493 |
+
y='st_length_shape',
|
| 1494 |
+
hover_name='district',
|
| 1495 |
+
title='Perimeter vs Area (Underground Coverage)',
|
| 1496 |
+
labels={'st_area_shape': 'Coverage Area (sq.m)', 'st_length_shape': 'Perimeter (m)'}
|
| 1497 |
+
)
|
| 1498 |
+
fig.update_layout(margin=dict(l=10, r=10, t=50, b=10), height=350)
|
| 1499 |
+
return fig
|
| 1500 |
+
except Exception:
|
| 1501 |
+
return go.Figure()
|
| 1502 |
+
|
| 1503 |
+
# Download current map as HTML
|
| 1504 |
+
@app.callback(
|
| 1505 |
+
Output("download-map-html", "data"),
|
| 1506 |
+
Input("download-map-html-btn", "n_clicks"),
|
| 1507 |
+
State("current-result", "data"),
|
| 1508 |
+
prevent_initial_call=True
|
| 1509 |
+
)
|
| 1510 |
+
def download_map_html(n_clicks, result_dict):
|
| 1511 |
+
try:
|
| 1512 |
+
if not n_clicks:
|
| 1513 |
+
return dash.no_update
|
| 1514 |
+
# Rebuild the map HTML from current results for export
|
| 1515 |
+
results = (result_dict or {}).get("results", [])
|
| 1516 |
+
iframe = create_underground_water_map(results)
|
| 1517 |
+
if isinstance(iframe, html.Iframe):
|
| 1518 |
+
html_str = iframe.props.get("srcDoc") or ""
|
| 1519 |
+
else:
|
| 1520 |
+
html_str = ""
|
| 1521 |
+
if not html_str:
|
| 1522 |
+
return dash.no_update
|
| 1523 |
+
return dict(content=html_str, filename="groundwater_map.html")
|
| 1524 |
+
except Exception:
|
| 1525 |
+
return dash.no_update
|
| 1526 |
+
|
| 1527 |
+
# Download CSV of current results
|
| 1528 |
+
@app.callback(
|
| 1529 |
+
Output("download-csv", "data"),
|
| 1530 |
+
Input("download-csv-btn", "n_clicks"),
|
| 1531 |
+
State("current-result", "data"),
|
| 1532 |
+
prevent_initial_call=True
|
| 1533 |
+
)
|
| 1534 |
+
def download_results_csv(n_clicks, result_dict):
|
| 1535 |
+
try:
|
| 1536 |
+
if not n_clicks:
|
| 1537 |
+
return dash.no_update
|
| 1538 |
+
results = (result_dict or {}).get("results", [])
|
| 1539 |
+
df = pd.DataFrame(results)
|
| 1540 |
+
if df.empty:
|
| 1541 |
+
return dash.no_update
|
| 1542 |
+
return dcc.send_data_frame(df.to_csv, "groundwater_results.csv", index=False)
|
| 1543 |
+
except Exception:
|
| 1544 |
+
return dash.no_update
|
| 1545 |
+
|
| 1546 |
+
# Standalone Visualizations page CSV download
|
| 1547 |
+
@app.callback(
|
| 1548 |
+
Output("download-csv-standalone", "data"),
|
| 1549 |
+
Input("download-csv-btn-standalone", "n_clicks"),
|
| 1550 |
+
State("current-result", "data"),
|
| 1551 |
+
prevent_initial_call=True
|
| 1552 |
+
)
|
| 1553 |
+
def download_results_csv_standalone(n_clicks, result_dict):
|
| 1554 |
+
try:
|
| 1555 |
+
if not n_clicks:
|
| 1556 |
+
return dash.no_update
|
| 1557 |
+
results = (result_dict or {}).get("results", [])
|
| 1558 |
+
df = pd.DataFrame(results)
|
| 1559 |
+
if df.empty:
|
| 1560 |
+
return dash.no_update
|
| 1561 |
+
return dcc.send_data_frame(df.to_csv, "groundwater_results.csv", index=False)
|
| 1562 |
+
except Exception:
|
| 1563 |
+
return dash.no_update
|
| 1564 |
+
|
| 1565 |
+
# Sync standalone visualizations with current result
|
| 1566 |
+
@app.callback(
|
| 1567 |
+
[Output("viz-graph-standalone", "figure"), Output("viz-graph-2-standalone", "figure")],
|
| 1568 |
+
[Input("current-result", "data")]
|
| 1569 |
+
)
|
| 1570 |
+
def populate_standalone_viz(result_dict):
|
| 1571 |
+
try:
|
| 1572 |
+
if not result_dict:
|
| 1573 |
+
return go.Figure(), go.Figure()
|
| 1574 |
+
return build_visualization_figure(result_dict), build_secondary_visualization_figure(result_dict)
|
| 1575 |
+
except Exception:
|
| 1576 |
+
return go.Figure(), go.Figure()
|
| 1577 |
+
|
| 1578 |
+
# Reports page CSV download
|
| 1579 |
+
@app.callback(
|
| 1580 |
+
Output("download-csv-report", "data"),
|
| 1581 |
+
Input("download-csv-btn-report", "n_clicks"),
|
| 1582 |
+
State("current-result", "data"),
|
| 1583 |
+
prevent_initial_call=True
|
| 1584 |
+
)
|
| 1585 |
+
def download_results_csv_report(n_clicks, result_dict):
|
| 1586 |
+
try:
|
| 1587 |
+
if not n_clicks:
|
| 1588 |
+
return dash.no_update
|
| 1589 |
+
results = (result_dict or {}).get("results", [])
|
| 1590 |
+
df = pd.DataFrame(results)
|
| 1591 |
+
if df.empty:
|
| 1592 |
+
return dash.no_update
|
| 1593 |
+
return dcc.send_data_frame(df.to_csv, "groundwater_report.csv", index=False)
|
| 1594 |
+
except Exception:
|
| 1595 |
+
return dash.no_update
|
| 1596 |
+
|
| 1597 |
+
# Forecast and Knowledge Cards from current results
|
| 1598 |
+
@app.callback(
|
| 1599 |
+
[Output("viz-graph-forecast", "figure"), Output("knowledge-cards", "children")],
|
| 1600 |
+
[Input("current-result", "data"), Input("main-tabs", "value")]
|
| 1601 |
+
)
|
| 1602 |
+
def build_forecast_and_cards(result_dict, active_tab):
|
| 1603 |
+
try:
|
| 1604 |
+
def _placeholder_figure(title_suffix="No time-series data"):
|
| 1605 |
+
x_vals = list(range(1, 11))
|
| 1606 |
+
y_vals = list(range(1, 11))
|
| 1607 |
+
fig = go.Figure()
|
| 1608 |
+
fig.add_trace(go.Scatter(
|
| 1609 |
+
x=x_vals,
|
| 1610 |
+
y=y_vals,
|
| 1611 |
+
mode='lines+markers',
|
| 1612 |
+
name='Placeholder',
|
| 1613 |
+
marker=dict(color="#2563eb"),
|
| 1614 |
+
line=dict(color="#2563eb")
|
| 1615 |
+
))
|
| 1616 |
+
fig.update_layout(
|
| 1617 |
+
title=f"Forecast Preview ({title_suffix})",
|
| 1618 |
+
height=360,
|
| 1619 |
+
margin=dict(l=10, r=10, t=50, b=10),
|
| 1620 |
+
plot_bgcolor="#ffffff",
|
| 1621 |
+
paper_bgcolor="#ffffff"
|
| 1622 |
+
)
|
| 1623 |
+
return fig
|
| 1624 |
+
|
| 1625 |
+
if not result_dict:
|
| 1626 |
+
# Minimal readable placeholder with hint
|
| 1627 |
+
placeholder = _placeholder_figure()
|
| 1628 |
+
hint = dbc.Alert("Run a query in Chat or Explore to populate insights.", color="secondary", className="mb-2")
|
| 1629 |
+
return placeholder, [hint]
|
| 1630 |
+
results = result_dict.get("results", [])
|
| 1631 |
+
viz = result_dict.get("visualization", {})
|
| 1632 |
+
insights = result_dict.get("insights", []) or []
|
| 1633 |
+
df = pd.DataFrame(results)
|
| 1634 |
+
if df.empty:
|
| 1635 |
+
# Even if results empty, still show insights if any
|
| 1636 |
+
insights_children = []
|
| 1637 |
+
if insights:
|
| 1638 |
+
insights_children.append(
|
| 1639 |
+
dbc.Alert(
|
| 1640 |
+
[html.Strong("Insights", className="me-2")] + [html.Div(f"• {i.get('title', '')}: {i.get('detail', '')}") for i in insights],
|
| 1641 |
+
color="light",
|
| 1642 |
+
className="mb-3"
|
| 1643 |
+
)
|
| 1644 |
+
)
|
| 1645 |
+
placeholder = _placeholder_figure()
|
| 1646 |
+
if not insights_children:
|
| 1647 |
+
insights_children = [dbc.Alert("No insights available for current selection.", color="secondary", className="mb-2")]
|
| 1648 |
+
return placeholder, insights_children
|
| 1649 |
+
|
| 1650 |
+
# Forecast placeholder: line over sorted top N by selected metric
|
| 1651 |
+
metric = viz.get("y") if viz else None
|
| 1652 |
+
if not metric or metric not in df.columns:
|
| 1653 |
+
for c in [
|
| 1654 |
+
'annual_gw_draft_total', 'stage_of_development', 'net_gw_availability',
|
| 1655 |
+
'annual_replenishable_gw_resource', 'annual_draft_irrigation', 'st_area_shape'
|
| 1656 |
+
]:
|
| 1657 |
+
if c in df.columns:
|
| 1658 |
+
metric = c
|
| 1659 |
+
break
|
| 1660 |
+
plot_df = df.copy()
|
| 1661 |
+
if metric in plot_df.columns:
|
| 1662 |
+
with pd.option_context('mode.use_inf_as_na', True):
|
| 1663 |
+
plot_df[metric] = pd.to_numeric(plot_df[metric], errors='coerce')
|
| 1664 |
+
plot_df = plot_df.dropna(subset=[metric]).sort_values(by=metric, ascending=False).head(20)
|
| 1665 |
+
x_vals = list(range(1, len(plot_df) + 1))
|
| 1666 |
+
fig_forecast = go.Figure()
|
| 1667 |
+
fig_forecast.add_trace(go.Scatter(
|
| 1668 |
+
x=x_vals,
|
| 1669 |
+
y=plot_df[metric],
|
| 1670 |
+
mode='lines+markers',
|
| 1671 |
+
name='Metric',
|
| 1672 |
+
marker=dict(color="#2563eb"),
|
| 1673 |
+
line=dict(color="#2563eb")
|
| 1674 |
+
))
|
| 1675 |
+
fig_forecast.update_layout(
|
| 1676 |
+
title=f"Forecast Preview for {metric.replace('_',' ').title()}",
|
| 1677 |
+
height=360,
|
| 1678 |
+
margin=dict(l=10, r=10, t=50, b=10),
|
| 1679 |
+
plot_bgcolor="#ffffff",
|
| 1680 |
+
paper_bgcolor="#ffffff"
|
| 1681 |
+
)
|
| 1682 |
+
else:
|
| 1683 |
+
fig_forecast = _placeholder_figure("No suitable metric found")
|
| 1684 |
+
|
| 1685 |
+
# Knowledge: insights + cards
|
| 1686 |
+
knowledge_children = []
|
| 1687 |
+
if insights:
|
| 1688 |
+
knowledge_children.append(
|
| 1689 |
+
dbc.Alert(
|
| 1690 |
+
[html.Strong("Insights", className="me-2 text-black")] + [html.Div(f"• {i.get('title', '')}: {i.get('detail', '')}") for i in insights],
|
| 1691 |
+
color="light",
|
| 1692 |
+
className="mb-3 text-black"
|
| 1693 |
+
)
|
| 1694 |
+
)
|
| 1695 |
+
|
| 1696 |
+
cards = []
|
| 1697 |
+
top_df = plot_df.head(6) if metric in df.columns else df.head(6)
|
| 1698 |
+
for _, row in top_df.iterrows():
|
| 1699 |
+
title = str(row.get('district', 'District')).title()
|
| 1700 |
+
area = row.get('st_area_shape')
|
| 1701 |
+
perim = row.get('st_length_shape')
|
| 1702 |
+
dev = row.get('stage_of_development')
|
| 1703 |
+
body = [
|
| 1704 |
+
html.Div(f"Area: {area:,.0f} sq.m" if isinstance(area, (int, float)) else f"Area: {area}"),
|
| 1705 |
+
html.Div(f"Perimeter: {perim:,.0f} m" if isinstance(perim, (int, float)) else f"Perimeter: {perim}"),
|
| 1706 |
+
html.Div(f"Development: {dev:.1f}%" if isinstance(dev, (int, float)) else f"Development: {dev}")
|
| 1707 |
+
]
|
| 1708 |
+
cards.append(
|
| 1709 |
+
dbc.Card([
|
| 1710 |
+
dbc.CardHeader(html.Strong(title)),
|
| 1711 |
+
dbc.CardBody(body)
|
| 1712 |
+
], className="me-2 mb-2 shadow-sm", style={"minWidth": "220px"})
|
| 1713 |
+
)
|
| 1714 |
+
|
| 1715 |
+
knowledge_children.extend(cards)
|
| 1716 |
+
return fig_forecast, knowledge_children
|
| 1717 |
+
except Exception:
|
| 1718 |
+
return go.Figure(), []
|
| 1719 |
+
|
| 1720 |
+
if __name__ == "__main__":
|
| 1721 |
+
if CHATBOT_READY:
|
| 1722 |
+
print("🌊 India Underground Water AI Chatbot")
|
| 1723 |
+
print("🚀 Starting server at http://localhost:8050")
|
| 1724 |
+
app.run(debug=True, host="0.0.0.0", port=8050)
|
| 1725 |
+
else:
|
| 1726 |
+
print("❌ Cannot start - Chatbot initialization failed")
|
| 1727 |
+
print("Please check your environment variables and database connection")
|