Rivalcoder commited on
Commit
36e3763
·
1 Parent(s): 4cde4e8
Files changed (5) hide show
  1. Dockerfile +36 -0
  2. __init__.py +3 -0
  3. api.py +330 -0
  4. chatbot_backend.py +605 -0
  5. 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 (&lt;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 (&gt;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")