Spaces:
Sleeping
Sleeping
Upload 30 files
Browse files- .env.example +9 -0
- .gitignore +11 -0
- Dockerfile +21 -0
- index.html +13 -0
- metadata.json +5 -0
- openmemory.md +27 -0
- package-lock.json +0 -0
- package.json +43 -0
- public/data/README.txt +6 -0
- public/data/maharashtra-districts.geojson +0 -0
- scripts/cache_maharashtra_districts_geojson.py +151 -0
- src/App.tsx +31 -0
- src/components/Layout.tsx +140 -0
- src/components/ui/Badge.tsx +32 -0
- src/components/ui/Button.tsx +37 -0
- src/components/ui/Card.tsx +34 -0
- src/components/ui/Input.tsx +23 -0
- src/data/mockFAQs.ts +233 -0
- src/data/mockGRs.ts +244 -0
- src/index.css +1 -0
- src/main.tsx +10 -0
- src/pages/Chatbot.tsx +781 -0
- src/pages/Dashboard.tsx +320 -0
- src/pages/Faq.tsx +290 -0
- src/pages/GisMap.tsx +969 -0
- src/pages/GrFinder.tsx +355 -0
- src/pages/Repository.tsx +192 -0
- src/utils/cn.ts +6 -0
- tsconfig.json +26 -0
- vite.config.ts +29 -0
.env.example
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
| 2 |
+
# AI Studio automatically injects this at runtime from user secrets.
|
| 3 |
+
# Users configure this via the Secrets panel in the AI Studio UI.
|
| 4 |
+
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
| 5 |
+
|
| 6 |
+
# APP_URL: The URL where this applet is hosted.
|
| 7 |
+
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
| 8 |
+
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
| 9 |
+
APP_URL="MY_APP_URL"
|
.gitignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
build/
|
| 3 |
+
dist/
|
| 4 |
+
coverage/
|
| 5 |
+
.DS_Store
|
| 6 |
+
*.log
|
| 7 |
+
.env*
|
| 8 |
+
!.env.example
|
| 9 |
+
|
| 10 |
+
# OpenMemory - IDE/Assistant specific rules
|
| 11 |
+
.windsurf\rules\openmemory.md
|
Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:18-alpine
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Copy package files
|
| 6 |
+
COPY package*.json ./
|
| 7 |
+
|
| 8 |
+
# Install dependencies
|
| 9 |
+
RUN npm install --legacy-peer-deps
|
| 10 |
+
|
| 11 |
+
# Copy all files
|
| 12 |
+
COPY . .
|
| 13 |
+
|
| 14 |
+
# Build the app
|
| 15 |
+
RUN npm run build
|
| 16 |
+
|
| 17 |
+
# Expose port 7860 (Hugging Face Spaces default)
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
# Start the preview server
|
| 21 |
+
CMD ["npm", "run", "preview", "--", "--port", "7860", "--host", "0.0.0.0"]
|
index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 6 |
+
<title>WRD JALDRISHTI</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<div id="root"></div>
|
| 10 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 11 |
+
</body>
|
| 12 |
+
</html>
|
| 13 |
+
|
metadata.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "WRD Insight Dashboard",
|
| 3 |
+
"description": "Centralized repository system for Government GRs, Acts, circulars, and flood prediction dashboard for the Water Resources Department.",
|
| 4 |
+
"requestFramePermissions": []
|
| 5 |
+
}
|
openmemory.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# WRD Insight Dashboard (JalDrishti)
|
| 3 |
+
|
| 4 |
+
## Overview
|
| 5 |
+
React + TypeScript (Vite) dashboard for Maharashtra WRD concept demo. Key modules:
|
| 6 |
+
- Dashboard (KPIs/analytics)
|
| 7 |
+
- GIS Map (Leaflet)
|
| 8 |
+
- Repository (documents)
|
| 9 |
+
- AI Assistant (chat-style document assistant)
|
| 10 |
+
|
| 11 |
+
## Architecture
|
| 12 |
+
- Top-level layout: `src/components/Layout.tsx` with sidebar + header + routed page content via `react-router-dom` `Outlet`.
|
| 13 |
+
- Routes: `src/App.tsx` mounts `Chatbot` at `/ai-assistant`.
|
| 14 |
+
|
| 15 |
+
## Components
|
| 16 |
+
- `src/pages/Chatbot.tsx`
|
| 17 |
+
- Desktop chat renders within the routed page content area.
|
| 18 |
+
- Mobile chat uses a floating button and a `fixed inset-0` full-screen overlay.
|
| 19 |
+
- `src/pages/GisMap.tsx`
|
| 20 |
+
- Leaflet-based GIS map with district boundaries and dam markers.
|
| 21 |
+
- Loads cached GeoJSON from `public/data/maharashtra-districts.geojson`.
|
| 22 |
+
- Live Data button toggles simulated rainfall/flood pulse animation overlay.
|
| 23 |
+
|
| 24 |
+
## Patterns
|
| 25 |
+
- **Nested flex scroll fix**: for chat UIs inside dashboards, ensure the scrollable region is in a `flex-1 min-h-0 overflow-y-auto` container, and parents also include `min-h-0` when needed. This prevents the input/footer from being pushed off-screen.
|
| 26 |
+
- **Route-specific page wrapper**: for `/ai-assistant` and `/map`, `Layout.tsx` uses `overflow-hidden` and removes outer padding so each page manages its internal scrolling and spacing.
|
| 27 |
+
- **GeoJSON caching for GIS**: District boundaries are cached locally to avoid Nominatim rate-limiting. Run `python scripts/cache_maharashtra_districts_geojson.py` to refresh. Script uses bbox fallback when Nominatim is unavailable.
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "react-example",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite --port=3000 --host=0.0.0.0",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview",
|
| 10 |
+
"clean": "rm -rf dist",
|
| 11 |
+
"lint": "tsc --noEmit"
|
| 12 |
+
},
|
| 13 |
+
"dependencies": {
|
| 14 |
+
"@google/genai": "^1.29.0",
|
| 15 |
+
"@tailwindcss/vite": "^4.1.14",
|
| 16 |
+
"@types/leaflet": "^1.9.21",
|
| 17 |
+
"@vitejs/plugin-react": "^5.0.4",
|
| 18 |
+
"better-sqlite3": "^12.4.1",
|
| 19 |
+
"clsx": "^2.1.1",
|
| 20 |
+
"date-fns": "^4.1.0",
|
| 21 |
+
"dotenv": "^17.2.3",
|
| 22 |
+
"express": "^4.21.2",
|
| 23 |
+
"leaflet": "^1.9.4",
|
| 24 |
+
"lucide-react": "^0.546.0",
|
| 25 |
+
"motion": "^12.23.24",
|
| 26 |
+
"react": "^19.0.0",
|
| 27 |
+
"react-dom": "^19.0.0",
|
| 28 |
+
"react-leaflet": "^5.0.0",
|
| 29 |
+
"react-router-dom": "^7.13.1",
|
| 30 |
+
"recharts": "^3.7.0",
|
| 31 |
+
"tailwind-merge": "^3.5.0",
|
| 32 |
+
"vite": "^6.2.0"
|
| 33 |
+
},
|
| 34 |
+
"devDependencies": {
|
| 35 |
+
"@types/express": "^4.17.21",
|
| 36 |
+
"@types/node": "^22.14.0",
|
| 37 |
+
"autoprefixer": "^10.4.21",
|
| 38 |
+
"tailwindcss": "^4.1.14",
|
| 39 |
+
"tsx": "^4.21.0",
|
| 40 |
+
"typescript": "~5.8.2",
|
| 41 |
+
"vite": "^6.2.0"
|
| 42 |
+
}
|
| 43 |
+
}
|
public/data/README.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Place cached GeoJSON/JSON files here to avoid hitting Nominatim/OSRM rate limits.
|
| 2 |
+
|
| 3 |
+
Expected by GisMap.tsx:
|
| 4 |
+
- public/data/maharashtra-districts.geojson (FeatureCollection; each feature has properties.name matching district)
|
| 5 |
+
|
| 6 |
+
If this file is missing/empty, district boundaries will not render, but the app will still run.
|
public/data/maharashtra-districts.geojson
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
scripts/cache_maharashtra_districts_geojson.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import random
|
| 4 |
+
import sys
|
| 5 |
+
import time
|
| 6 |
+
import urllib.error
|
| 7 |
+
import urllib.parse
|
| 8 |
+
import urllib.request
|
| 9 |
+
|
| 10 |
+
DISTRICTS = [
|
| 11 |
+
"Kolhapur",
|
| 12 |
+
"Sangli",
|
| 13 |
+
"Satara",
|
| 14 |
+
"Pune",
|
| 15 |
+
"Ahmednagar",
|
| 16 |
+
"Nashik",
|
| 17 |
+
"Aurangabad",
|
| 18 |
+
"Thane",
|
| 19 |
+
"Raigad",
|
| 20 |
+
"Ratnagiri",
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
# Approximate bounding boxes for Maharashtra districts (lon_min, lat_min, lon_max, lat_max)
|
| 24 |
+
# Used as fallback when Nominatim is rate-limited
|
| 25 |
+
DISTRICT_BBOXES = {
|
| 26 |
+
"Kolhapur": (73.6, 16.0, 74.8, 17.2),
|
| 27 |
+
"Sangli": (73.8, 16.4, 75.0, 17.2),
|
| 28 |
+
"Satara": (73.3, 17.0, 74.5, 18.0),
|
| 29 |
+
"Pune": (73.2, 18.0, 74.5, 19.0),
|
| 30 |
+
"Ahmednagar": (74.0, 18.5, 75.5, 19.5),
|
| 31 |
+
"Nashik": (73.0, 19.5, 74.5, 20.5),
|
| 32 |
+
"Aurangabad": (74.5, 19.0, 76.0, 20.0),
|
| 33 |
+
"Thane": (72.8, 19.0, 73.6, 20.0),
|
| 34 |
+
"Raigad": (72.8, 18.0, 73.5, 18.8),
|
| 35 |
+
"Ratnagiri": (72.8, 16.5, 73.8, 17.5),
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _request_json(url: str, timeout: int = 10) -> object:
|
| 40 |
+
req = urllib.request.Request(
|
| 41 |
+
url,
|
| 42 |
+
headers={
|
| 43 |
+
"Accept": "application/json",
|
| 44 |
+
"User-Agent": "wrd-insight-dashboard/1.0 (cache script; contact: local-demo)",
|
| 45 |
+
},
|
| 46 |
+
)
|
| 47 |
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
| 48 |
+
body = resp.read().decode("utf-8")
|
| 49 |
+
return json.loads(body)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def fetch_district_geojson_from_nominatim(district: str) -> dict | None:
|
| 53 |
+
"""Try to fetch district boundary from Nominatim. Returns None on any error."""
|
| 54 |
+
params = {
|
| 55 |
+
"county": district,
|
| 56 |
+
"state": "Maharashtra",
|
| 57 |
+
"country": "India",
|
| 58 |
+
"polygon_geojson": 1,
|
| 59 |
+
"format": "json",
|
| 60 |
+
}
|
| 61 |
+
url = "https://nominatim.openstreetmap.org/search?" + urllib.parse.urlencode(params)
|
| 62 |
+
try:
|
| 63 |
+
data = _request_json(url, timeout=10)
|
| 64 |
+
except Exception:
|
| 65 |
+
return None
|
| 66 |
+
|
| 67 |
+
if isinstance(data, list) and data and isinstance(data[0], dict) and data[0].get("geojson"):
|
| 68 |
+
geo = data[0]["geojson"]
|
| 69 |
+
return {
|
| 70 |
+
"type": "Feature",
|
| 71 |
+
"properties": {"name": district, "source": "nominatim"},
|
| 72 |
+
"geometry": geo,
|
| 73 |
+
}
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def create_bbox_polygon(district: str) -> dict:
|
| 78 |
+
"""Create a simple rectangular polygon from bounding box as fallback."""
|
| 79 |
+
bbox = DISTRICT_BBOXES.get(district)
|
| 80 |
+
if not bbox:
|
| 81 |
+
# Default to a placeholder in central Maharashtra
|
| 82 |
+
bbox = (73.5, 18.0, 75.0, 19.5)
|
| 83 |
+
|
| 84 |
+
lon_min, lat_min, lon_max, lat_max = bbox
|
| 85 |
+
# Create a simple rectangular polygon (5 points to close the ring)
|
| 86 |
+
coords = [
|
| 87 |
+
[lon_min, lat_min],
|
| 88 |
+
[lon_max, lat_min],
|
| 89 |
+
[lon_max, lat_max],
|
| 90 |
+
[lon_min, lat_max],
|
| 91 |
+
[lon_min, lat_min], # Close the ring
|
| 92 |
+
]
|
| 93 |
+
return {
|
| 94 |
+
"type": "Feature",
|
| 95 |
+
"properties": {"name": district, "source": "bbox_fallback"},
|
| 96 |
+
"geometry": {"type": "Polygon", "coordinates": [coords]},
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def main() -> int:
|
| 101 |
+
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
| 102 |
+
out_path = os.path.join(repo_root, "public", "data", "maharashtra-districts.geojson")
|
| 103 |
+
|
| 104 |
+
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
| 105 |
+
|
| 106 |
+
features: list[dict] = []
|
| 107 |
+
nominatim_success = 0
|
| 108 |
+
fallback_used = 0
|
| 109 |
+
|
| 110 |
+
print("Fetching Maharashtra district boundaries...")
|
| 111 |
+
print("Strategy: Try Nominatim first (quick timeout), use bbox fallback if rate-limited.\n")
|
| 112 |
+
|
| 113 |
+
for district in DISTRICTS:
|
| 114 |
+
# Try Nominatim with just 1 attempt (quick)
|
| 115 |
+
feature = fetch_district_geojson_from_nominatim(district)
|
| 116 |
+
|
| 117 |
+
if feature:
|
| 118 |
+
features.append(feature)
|
| 119 |
+
nominatim_success += 1
|
| 120 |
+
print(f" ✓ {district}: fetched from Nominatim")
|
| 121 |
+
else:
|
| 122 |
+
# Use bounding box fallback immediately
|
| 123 |
+
feature = create_bbox_polygon(district)
|
| 124 |
+
features.append(feature)
|
| 125 |
+
fallback_used += 1
|
| 126 |
+
print(f" ○ {district}: using bbox fallback (Nominatim unavailable)")
|
| 127 |
+
|
| 128 |
+
# Minimal delay to be polite (Nominatim asks for 1 req/sec)
|
| 129 |
+
time.sleep(1.0)
|
| 130 |
+
|
| 131 |
+
# Write the FeatureCollection
|
| 132 |
+
fc = {"type": "FeatureCollection", "features": features}
|
| 133 |
+
with open(out_path, "w", encoding="utf-8") as f:
|
| 134 |
+
json.dump(fc, f, ensure_ascii=False, indent=2)
|
| 135 |
+
|
| 136 |
+
print(f"\n✓ Wrote {len(features)} district features to: {out_path}")
|
| 137 |
+
print(f" - From Nominatim: {nominatim_success}")
|
| 138 |
+
print(f" - Bbox fallback: {fallback_used}")
|
| 139 |
+
|
| 140 |
+
if fallback_used > 0:
|
| 141 |
+
print(
|
| 142 |
+
"\nNote: Some districts use approximate bounding boxes. "
|
| 143 |
+
"For precise boundaries, re-run when Nominatim is available "
|
| 144 |
+
"or download a proper Maharashtra district GeoJSON from a trusted source."
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
return 0
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
if __name__ == "__main__":
|
| 151 |
+
raise SystemExit(main())
|
src/App.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @license
|
| 3 |
+
* SPDX-License-Identifier: Apache-2.0
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import React from 'react';
|
| 7 |
+
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
| 8 |
+
import { Layout } from './components/Layout';
|
| 9 |
+
import { Dashboard } from './pages/Dashboard';
|
| 10 |
+
import { GisMap } from './pages/GisMap';
|
| 11 |
+
import { Repository } from './pages/Repository';
|
| 12 |
+
import { Chatbot } from './pages/Chatbot';
|
| 13 |
+
import { GrFinder } from './pages/GrFinder';
|
| 14 |
+
import { Faq } from './pages/Faq';
|
| 15 |
+
|
| 16 |
+
export default function App() {
|
| 17 |
+
return (
|
| 18 |
+
<BrowserRouter>
|
| 19 |
+
<Routes>
|
| 20 |
+
<Route path="/" element={<Layout />}>
|
| 21 |
+
<Route index element={<Dashboard />} />
|
| 22 |
+
<Route path="map" element={<GisMap />} />
|
| 23 |
+
<Route path="gr-finder" element={<GrFinder />} />
|
| 24 |
+
<Route path="faq" element={<Faq />} />
|
| 25 |
+
<Route path="repository" element={<Repository />} />
|
| 26 |
+
<Route path="ai-assistant" element={<Chatbot />} />
|
| 27 |
+
</Route>
|
| 28 |
+
</Routes>
|
| 29 |
+
</BrowserRouter>
|
| 30 |
+
);
|
| 31 |
+
}
|
src/components/Layout.tsx
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Outlet, NavLink, useLocation } from 'react-router-dom';
|
| 3 |
+
import {
|
| 4 |
+
LayoutDashboard,
|
| 5 |
+
Map,
|
| 6 |
+
FileText,
|
| 7 |
+
MessageSquare,
|
| 8 |
+
Settings,
|
| 9 |
+
Bell,
|
| 10 |
+
Search,
|
| 11 |
+
Droplets,
|
| 12 |
+
HelpCircle,
|
| 13 |
+
FileSearch
|
| 14 |
+
} from 'lucide-react';
|
| 15 |
+
import { cn } from '@/src/utils/cn';
|
| 16 |
+
import { Button } from './ui/Button';
|
| 17 |
+
import { Input } from './ui/Input';
|
| 18 |
+
|
| 19 |
+
export function Layout() {
|
| 20 |
+
const location = useLocation();
|
| 21 |
+
const isAIAssistant = location.pathname === '/ai-assistant';
|
| 22 |
+
const isGisMap = location.pathname === '/map';
|
| 23 |
+
|
| 24 |
+
const navItems = [
|
| 25 |
+
{ name: 'Dashboard', path: '/', icon: LayoutDashboard },
|
| 26 |
+
{ name: 'GIS Map', path: '/map', icon: Map },
|
| 27 |
+
{ name: 'GR Finder', path: '/gr-finder', icon: FileSearch },
|
| 28 |
+
{ name: 'GR Helper', path: '/ai-assistant', icon: MessageSquare },
|
| 29 |
+
{ name: 'FAQ', path: '/faq', icon: HelpCircle },
|
| 30 |
+
{ name: 'Repository', path: '/repository', icon: FileText },
|
| 31 |
+
];
|
| 32 |
+
|
| 33 |
+
return (
|
| 34 |
+
<div className="flex h-screen w-full bg-slate-50 overflow-hidden">
|
| 35 |
+
{/* Desktop Sidebar */}
|
| 36 |
+
<aside className="hidden md:flex w-64 flex-col border-r border-slate-200 bg-white shrink-0">
|
| 37 |
+
<div className="flex h-16 items-center px-6 border-b border-slate-200">
|
| 38 |
+
<Droplets className="h-6 w-6 text-blue-600 mr-2" />
|
| 39 |
+
<span className="text-xl font-bold text-slate-900 tracking-tight">JalDrishti</span>
|
| 40 |
+
</div>
|
| 41 |
+
<nav className="flex-1 space-y-1 px-3 py-4 overflow-y-auto">
|
| 42 |
+
{navItems.map((item) => {
|
| 43 |
+
const Icon = item.icon;
|
| 44 |
+
const isActive = location.pathname === item.path;
|
| 45 |
+
return (
|
| 46 |
+
<NavLink
|
| 47 |
+
key={item.name}
|
| 48 |
+
to={item.path}
|
| 49 |
+
className={cn(
|
| 50 |
+
"flex items-center px-3 py-2.5 text-sm font-medium rounded-md transition-colors",
|
| 51 |
+
isActive
|
| 52 |
+
? "bg-blue-50 text-blue-700"
|
| 53 |
+
: "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
|
| 54 |
+
)}
|
| 55 |
+
>
|
| 56 |
+
<Icon className={cn("mr-3 h-5 w-5", isActive ? "text-blue-700" : "text-slate-400")} />
|
| 57 |
+
{item.name}
|
| 58 |
+
</NavLink>
|
| 59 |
+
);
|
| 60 |
+
})}
|
| 61 |
+
</nav>
|
| 62 |
+
<div className="p-4 border-t border-slate-200">
|
| 63 |
+
<div className="flex items-center">
|
| 64 |
+
<div className="h-8 w-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-700 font-bold text-xs">
|
| 65 |
+
SD
|
| 66 |
+
</div>
|
| 67 |
+
<div className="ml-3">
|
| 68 |
+
<p className="text-sm font-medium text-slate-900">Swapnali Deshpande</p>
|
| 69 |
+
<p className="text-xs text-slate-500">WRD Main Incharge</p>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
</aside>
|
| 74 |
+
|
| 75 |
+
{/* Main Content */}
|
| 76 |
+
<main className="flex-1 flex flex-col min-w-0 overflow-hidden pb-16 md:pb-0">
|
| 77 |
+
{/* Header */}
|
| 78 |
+
<header className="flex h-14 md:h-16 items-center justify-between border-b border-slate-200 bg-white px-4 sm:px-6 lg:px-8 shrink-0">
|
| 79 |
+
<div className="flex items-center flex-1">
|
| 80 |
+
<div className="md:hidden flex items-center mr-4">
|
| 81 |
+
<Droplets className="h-6 w-6 text-blue-600 mr-2" />
|
| 82 |
+
<span className="text-lg font-bold text-slate-900 tracking-tight">JalDrishti</span>
|
| 83 |
+
</div>
|
| 84 |
+
<div className="w-full max-w-lg hidden sm:flex items-center relative">
|
| 85 |
+
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-slate-400" />
|
| 86 |
+
<Input
|
| 87 |
+
type="search"
|
| 88 |
+
placeholder="Search GRs, Acts, Circulars..."
|
| 89 |
+
className="w-full bg-slate-50 pl-9 border-none focus-visible:ring-1 focus-visible:ring-blue-500"
|
| 90 |
+
/>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
<div className="flex items-center space-x-2 md:space-x-4">
|
| 94 |
+
<Button variant="ghost" size="icon" className="relative text-slate-500 h-9 w-9">
|
| 95 |
+
<Bell className="h-5 w-5" />
|
| 96 |
+
<span className="absolute top-1.5 right-1.5 h-2 w-2 rounded-full bg-red-500 ring-2 ring-white"></span>
|
| 97 |
+
</Button>
|
| 98 |
+
<Button variant="ghost" size="icon" className="text-slate-500 h-9 w-9 hidden sm:flex">
|
| 99 |
+
<Settings className="h-5 w-5" />
|
| 100 |
+
</Button>
|
| 101 |
+
<div className="h-8 w-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-700 font-bold text-xs md:hidden ml-2">
|
| 102 |
+
SD
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
</header>
|
| 106 |
+
|
| 107 |
+
{/* Page Content */}
|
| 108 |
+
<div
|
| 109 |
+
className={cn(
|
| 110 |
+
'flex-1 bg-slate-50/50',
|
| 111 |
+
(isAIAssistant || isGisMap) ? 'overflow-hidden p-0 sm:p-0 lg:p-0' : 'overflow-auto p-3 sm:p-6 lg:p-8'
|
| 112 |
+
)}
|
| 113 |
+
>
|
| 114 |
+
<Outlet />
|
| 115 |
+
</div>
|
| 116 |
+
</main>
|
| 117 |
+
|
| 118 |
+
{/* Mobile Bottom Navigation */}
|
| 119 |
+
<nav className="md:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-slate-200 flex justify-around items-center h-16 z-50 pb-safe">
|
| 120 |
+
{navItems.map((item) => {
|
| 121 |
+
const Icon = item.icon;
|
| 122 |
+
const isActive = location.pathname === item.path;
|
| 123 |
+
return (
|
| 124 |
+
<NavLink
|
| 125 |
+
key={item.name}
|
| 126 |
+
to={item.path}
|
| 127 |
+
className={cn(
|
| 128 |
+
"flex flex-col items-center justify-center w-full h-full space-y-1 transition-colors",
|
| 129 |
+
isActive ? "text-blue-600" : "text-slate-500 hover:text-slate-900"
|
| 130 |
+
)}
|
| 131 |
+
>
|
| 132 |
+
<Icon className={cn("h-5 w-5", isActive ? "fill-blue-50/50" : "")} />
|
| 133 |
+
<span className="text-[10px] font-medium">{item.name}</span>
|
| 134 |
+
</NavLink>
|
| 135 |
+
);
|
| 136 |
+
})}
|
| 137 |
+
</nav>
|
| 138 |
+
</div>
|
| 139 |
+
);
|
| 140 |
+
}
|
src/components/ui/Badge.tsx
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { cn } from "@/src/utils/cn"
|
| 3 |
+
|
| 4 |
+
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
|
| 5 |
+
variant?: "default" | "secondary" | "destructive" | "outline";
|
| 6 |
+
className?: string;
|
| 7 |
+
children?: React.ReactNode;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
| 11 |
+
({ className, variant = "default", ...props }, ref) => {
|
| 12 |
+
return (
|
| 13 |
+
<div
|
| 14 |
+
ref={ref}
|
| 15 |
+
className={cn(
|
| 16 |
+
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2",
|
| 17 |
+
{
|
| 18 |
+
"border-transparent bg-blue-600 text-white shadow hover:bg-blue-700": variant === "default",
|
| 19 |
+
"border-transparent bg-slate-100 text-slate-900 hover:bg-slate-100/80": variant === "secondary",
|
| 20 |
+
"border-transparent bg-red-500 text-slate-50 shadow hover:bg-red-500/80": variant === "destructive",
|
| 21 |
+
"text-slate-950": variant === "outline",
|
| 22 |
+
},
|
| 23 |
+
className
|
| 24 |
+
)}
|
| 25 |
+
{...props}
|
| 26 |
+
/>
|
| 27 |
+
)
|
| 28 |
+
}
|
| 29 |
+
)
|
| 30 |
+
Badge.displayName = "Badge"
|
| 31 |
+
|
| 32 |
+
export { Badge }
|
src/components/ui/Button.tsx
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { cn } from "@/src/utils/cn"
|
| 3 |
+
|
| 4 |
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
| 5 |
+
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
|
| 6 |
+
size?: "default" | "sm" | "lg" | "icon"
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
| 10 |
+
({ className, variant = "default", size = "default", ...props }, ref) => {
|
| 11 |
+
return (
|
| 12 |
+
<button
|
| 13 |
+
className={cn(
|
| 14 |
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-slate-950 disabled:pointer-events-none disabled:opacity-50",
|
| 15 |
+
{
|
| 16 |
+
"bg-blue-600 text-white shadow hover:bg-blue-700": variant === "default",
|
| 17 |
+
"bg-red-500 text-slate-50 shadow-sm hover:bg-red-500/90": variant === "destructive",
|
| 18 |
+
"border border-slate-200 bg-white shadow-sm hover:bg-slate-100 hover:text-slate-900": variant === "outline",
|
| 19 |
+
"bg-slate-100 text-slate-900 shadow-sm hover:bg-slate-100/80": variant === "secondary",
|
| 20 |
+
"hover:bg-slate-100 hover:text-slate-900": variant === "ghost",
|
| 21 |
+
"text-slate-900 underline-offset-4 hover:underline": variant === "link",
|
| 22 |
+
"h-9 px-4 py-2": size === "default",
|
| 23 |
+
"h-8 rounded-md px-3 text-xs": size === "sm",
|
| 24 |
+
"h-10 rounded-md px-8": size === "lg",
|
| 25 |
+
"h-9 w-9": size === "icon",
|
| 26 |
+
},
|
| 27 |
+
className
|
| 28 |
+
)}
|
| 29 |
+
ref={ref}
|
| 30 |
+
{...props}
|
| 31 |
+
/>
|
| 32 |
+
)
|
| 33 |
+
}
|
| 34 |
+
)
|
| 35 |
+
Button.displayName = "Button"
|
| 36 |
+
|
| 37 |
+
export { Button }
|
src/components/ui/Card.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { cn } from "@/src/utils/cn"
|
| 3 |
+
|
| 4 |
+
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
| 5 |
+
<div ref={ref} className={cn("rounded-xl border border-slate-200 bg-white text-slate-950 shadow-sm", className)} {...props} />
|
| 6 |
+
))
|
| 7 |
+
Card.displayName = "Card"
|
| 8 |
+
|
| 9 |
+
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
| 10 |
+
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
| 11 |
+
))
|
| 12 |
+
CardHeader.displayName = "CardHeader"
|
| 13 |
+
|
| 14 |
+
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(({ className, ...props }, ref) => (
|
| 15 |
+
<h3 ref={ref} className={cn("font-semibold leading-none tracking-tight", className)} {...props} />
|
| 16 |
+
))
|
| 17 |
+
CardTitle.displayName = "CardTitle"
|
| 18 |
+
|
| 19 |
+
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(({ className, ...props }, ref) => (
|
| 20 |
+
<p ref={ref} className={cn("text-sm text-slate-500", className)} {...props} />
|
| 21 |
+
))
|
| 22 |
+
CardDescription.displayName = "CardDescription"
|
| 23 |
+
|
| 24 |
+
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
| 25 |
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
| 26 |
+
))
|
| 27 |
+
CardContent.displayName = "CardContent"
|
| 28 |
+
|
| 29 |
+
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
|
| 30 |
+
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
| 31 |
+
))
|
| 32 |
+
CardFooter.displayName = "CardFooter"
|
| 33 |
+
|
| 34 |
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
src/components/ui/Input.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as React from "react"
|
| 2 |
+
import { cn } from "@/src/utils/cn"
|
| 3 |
+
|
| 4 |
+
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
| 5 |
+
|
| 6 |
+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
| 7 |
+
({ className, type, ...props }, ref) => {
|
| 8 |
+
return (
|
| 9 |
+
<input
|
| 10 |
+
type={type}
|
| 11 |
+
className={cn(
|
| 12 |
+
"flex h-9 w-full rounded-md border border-slate-200 bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-600 disabled:cursor-not-allowed disabled:opacity-50",
|
| 13 |
+
className
|
| 14 |
+
)}
|
| 15 |
+
ref={ref}
|
| 16 |
+
{...props}
|
| 17 |
+
/>
|
| 18 |
+
)
|
| 19 |
+
}
|
| 20 |
+
)
|
| 21 |
+
Input.displayName = "Input"
|
| 22 |
+
|
| 23 |
+
export { Input }
|
src/data/mockFAQs.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Mock FAQ data for WRD demo
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
export interface FAQItem {
|
| 6 |
+
id: string;
|
| 7 |
+
question: string;
|
| 8 |
+
answer: string;
|
| 9 |
+
category: 'Flood Management' | 'Dam Operations' | 'Irrigation & Water Rights' | 'Compensation & Relief' | 'GRs & Acts';
|
| 10 |
+
relatedGRs?: string[];
|
| 11 |
+
keywords: string[];
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export const faqCategories = [
|
| 15 |
+
{ id: 'flood', name: 'Flood Management', icon: 'Waves' },
|
| 16 |
+
{ id: 'dam', name: 'Dam Operations', icon: 'Database' },
|
| 17 |
+
{ id: 'irrigation', name: 'Irrigation & Water Rights', icon: 'Droplets' },
|
| 18 |
+
{ id: 'compensation', name: 'Compensation & Relief', icon: 'IndianRupee' },
|
| 19 |
+
{ id: 'grs', name: 'GRs & Acts', icon: 'FileText' },
|
| 20 |
+
] as const;
|
| 21 |
+
|
| 22 |
+
export const faqData: FAQItem[] = [
|
| 23 |
+
// Flood Management
|
| 24 |
+
{
|
| 25 |
+
id: 'faq-flood-1',
|
| 26 |
+
question: 'What should I do when a flood warning is issued for my area?',
|
| 27 |
+
answer: 'When a flood warning is issued:\n\n1. **Immediate Actions:** Move to higher ground immediately if told to evacuate. Do not wait for instructions.\n\n2. **Essential Items:** Carry important documents (ID proof, property papers), medicines, and emergency supplies.\n\n3. **Avoid:** Do not walk through moving water (6 inches can make you fall). Do not drive through flooded roads.\n\n4. **Stay Informed:** Listen to local radio/TV for updates. Follow WRD official alerts.\n\n5. **After Flood:** Do not return home until authorities declare it safe. Watch for contaminated water and electrical hazards.\n\nRefer to GR-2023-WRD-156 for complete evacuation protocols.',
|
| 28 |
+
category: 'Flood Management',
|
| 29 |
+
relatedGRs: ['GR-2023-WRD-156', 'GR-2024-WRD-056'],
|
| 30 |
+
keywords: ['flood warning', 'evacuation', 'safety', 'emergency']
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
id: 'faq-flood-2',
|
| 34 |
+
question: 'How are flood warning levels classified?',
|
| 35 |
+
answer: 'Flood warning levels in Maharashtra are classified as:\n\n**Level 1 - Normal:** River flowing within normal limits. No action required.\n\n**Level 2 - Alert:** River rising but below danger level. Stay informed, prepare for evacuation.\n\n**Level 3 - Warning:** River at danger level. Evacuation may be ordered. Move valuables to higher floors.\n\n**Level 4 - Critical:** River above danger level. Mandatory evacuation. Emergency response activated.\n\nThese levels are monitored by WRD control rooms and communicated via SMS alerts, sirens, and local officials.',
|
| 36 |
+
category: 'Flood Management',
|
| 37 |
+
relatedGRs: ['GR-2023-WRD-156'],
|
| 38 |
+
keywords: ['warning levels', 'classification', 'danger level', 'alert']
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
id: 'faq-flood-3',
|
| 42 |
+
question: 'Who issues flood evacuation orders?',
|
| 43 |
+
answer: 'Flood evacuation orders are issued by:\n\n1. **District Collector:** Primary authority for evacuation orders in the district.\n\n2. **Tahsildar:** Issues orders at taluka level on Collector\'s direction.\n\n3. **WRD Chief Engineer:** Can recommend evacuation when dam discharge exceeds safe levels.\n\n4. **State Disaster Management Authority:** Issues orders for large-scale emergencies.\n\nThe order is communicated through:\n- SMS alerts to registered mobile numbers\n- Sirens and public announcements\n- Local police and revenue officials\n- Radio and TV broadcasts',
|
| 44 |
+
category: 'Flood Management',
|
| 45 |
+
relatedGRs: ['GR-2023-WRD-156'],
|
| 46 |
+
keywords: ['evacuation order', 'collector', 'authority', 'disaster management']
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
id: 'faq-flood-4',
|
| 50 |
+
question: 'What is the early warning system for floods in Konkan region?',
|
| 51 |
+
answer: 'The Konkan Flood Early Warning System includes:\n\n**Monitoring Network:**\n- 45 automated rainfall monitoring stations\n- 28 river level sensors\n- 12 tidal gauges along the coast\n\n**Alert Mechanism:**\n- SMS alerts sent 6-12 hours in advance\n- Color-coded warnings (Yellow/Orange/Red)\n- Community radio announcements\n\n**Communication:**\n- Village-level volunteers with megaphones\n- Temple/mosque loudspeakers\n- WhatsApp groups managed by Gram Panchayat\n\nThis system was implemented under GR-2024-WRD-056 and covers all 7 coastal districts.',
|
| 52 |
+
category: 'Flood Management',
|
| 53 |
+
relatedGRs: ['GR-2024-WRD-056'],
|
| 54 |
+
keywords: ['early warning', 'Konkan', 'SMS', 'monitoring', 'alert']
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
id: 'faq-flood-5',
|
| 58 |
+
question: 'How can I check current flood status in my district?',
|
| 59 |
+
answer: 'You can check flood status through:\n\n**Online:**\n- JalDrishti Dashboard (this portal) - Real-time dam levels and flood zones\n- wrd.maharashtra.gov.in - Official WRD portal\n- IMD website - Rainfall predictions\n\n**Mobile:**\n- Download "MahaFlood" app (Android/iOS)\n- SMS "FLOOD <District>" to 9223456789\n\n**Offline:**\n- Call District Control Room (available 24x7)\n- Contact local Tahsildar office\n- Listen to All India Radio regional broadcasts\n\nThe GIS Map on this portal shows live flood zones and affected areas.',
|
| 60 |
+
category: 'Flood Management',
|
| 61 |
+
keywords: ['flood status', 'check', 'online', 'mobile app', 'SMS']
|
| 62 |
+
},
|
| 63 |
+
|
| 64 |
+
// Dam Operations
|
| 65 |
+
{
|
| 66 |
+
id: 'faq-dam-1',
|
| 67 |
+
question: 'What happens when a dam reaches 90% capacity?',
|
| 68 |
+
answer: 'When a dam reaches 90% capacity:\n\n**Immediate Actions:**\n1. Control room staff increased to 24x7 monitoring\n2. Downstream districts notified (minimum 2 hours advance)\n3. Gate operation team placed on standby\n\n**Gate Operations:**\n- Gradual release begins at 10% gate opening\n- Incremental increase every 15 minutes\n- Maximum safe discharge calculated based on downstream capacity\n\n**Public Communication:**\n- Media briefing by Chief Engineer\n- SMS alerts to downstream villages\n- Siren activation in danger zones\n\n**Emergency Protocol:** If level exceeds 95%, Chief Engineer can authorize immediate release without waiting for full notification period.\n\nSee GR-2024-WRD-056 and GR-2023-WRD-124 for complete protocols.',
|
| 69 |
+
category: 'Dam Operations',
|
| 70 |
+
relatedGRs: ['GR-2024-WRD-056', 'GR-2023-WRD-124'],
|
| 71 |
+
keywords: ['dam capacity', '90%', 'gate operation', 'discharge', 'emergency']
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
id: 'faq-dam-2',
|
| 75 |
+
question: 'How often are dams inspected?',
|
| 76 |
+
answer: 'Dam inspection frequency as per GR-2022-WRD-112:\n\n**Daily Inspection (Monsoon Season - June to October):**\n- Visual inspection of dam structure\n- Water level and inflow measurement\n- Seepage monitoring\n- Gate mechanism check\n\n**Weekly Inspection (Non-Monsoon):**\n- Structural assessment\n- Instrumentation reading\n- Downstream condition check\n\n**Annual Comprehensive Inspection (Before Monsoon - May):**\n- Full structural integrity assessment\n- Seismic risk evaluation\n- Emergency action plan review\n- Gate testing under load\n\n**Special Inspections:**\n- After any seismic event >4.0 magnitude\n- After extreme flood event\n- When ordered by Dam Safety Review Committee\n\nAll inspections must be logged in the DAMS (Dam Asset Management System) within 24 hours.',
|
| 77 |
+
category: 'Dam Operations',
|
| 78 |
+
relatedGRs: ['GR-2022-WRD-112'],
|
| 79 |
+
keywords: ['inspection', 'frequency', 'monsoon', 'annual', 'seismic']
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
id: 'faq-dam-3',
|
| 83 |
+
question: 'What is the procedure for dam gate opening?',
|
| 84 |
+
answer: 'Dam gate opening procedure as per GR-2023-WRD-124:\n\n**Pre-Opening Checklist:**\n✓ Seismic sensors calibrated\n✓ Downstream flow sensors active\n✓ Alert systems tested\n✓ Control room staffed (minimum 2 engineers)\n✓ Emergency backup power available\n\n**Notification Timeline:**\n- T-2 hours: Alert downstream district collectors\n- T-1 hour: Public announcement via media\n- T-30 min: Siren activation\n\n**Opening Sequence:**\n1. Start with 10% gate opening\n2. Wait 15 minutes, observe downstream flow\n3. Increase by 10% increments\n4. Maximum 50% opening per hour\n\n**Emergency Override:**\nChief Engineer can authorize immediate release if dam level exceeds 95% or structural risk detected.\n\n**Documentation:** All operations logged in DAMS within 1 hour of completion.',
|
| 85 |
+
category: 'Dam Operations',
|
| 86 |
+
relatedGRs: ['GR-2023-WRD-124'],
|
| 87 |
+
keywords: ['gate opening', 'procedure', 'notification', 'SOP', 'discharge']
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
id: 'faq-dam-4',
|
| 91 |
+
question: 'Which dams in Maharashtra are classified as critical?',
|
| 92 |
+
answer: 'Currently, dams are classified as critical based on:\n\n**Critical Status Criteria:**\n- Storage level >90% AND\n- High inflow rate (>10,000 cusecs) OR\n- Downstream areas at flood risk\n\n**Currently Critical Dams (as of latest update):**\n1. **Koyna Dam, Satara** - 92.4% capacity, 45,000 cusecs inflow\n2. **Radhanagari Dam, Kolhapur** - 95% capacity, 25,000 cusecs inflow\n\n**Warning Status Dams:**\n1. Ujani Dam, Solapur - 85%\n2. Ghatghar Dam, Thane - 88%\n\n**Historically Critical Dams:**\n- Jayakwadi (Aurangabad) - Flood 2019\n- Tillari (Sindhudurg) - Flood 2021\n\nStatus changes are updated every 3 hours during monsoon. Check the GIS Map for real-time status.',
|
| 93 |
+
category: 'Dam Operations',
|
| 94 |
+
keywords: ['critical dams', 'Koyna', 'Radhanagari', 'status', 'monitoring']
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
id: 'faq-dam-5',
|
| 98 |
+
question: 'What safety measures are in place for dam failure prevention?',
|
| 99 |
+
answer: 'Dam safety measures include:\n\n**Structural Monitoring:**\n- Instrumentation: Piezometers, inclinometers, settlement gauges\n- Seismic sensors: Real-time earthquake detection\n- Drone surveys: Annual structural imaging\n\n**Operational Safety:**\n- Rule curve compliance: Mandatory storage limits by date\n- Regular gate testing: Monthly during non-monsoon\n- Emergency action plans: Updated annually\n\n**Regulatory Framework:**\n- Dam Safety Bill 2019 compliance\n- Annual safety audit (GR-2022-WRD-067)\n- Dam Safety Review Committee inspections\n\n**Emergency Preparedness:**\n- Downstream evacuation plans\n- Early warning systems\n- Mock drills every 2 years\n\n**Insurance:** All major dams covered under Dam Safety Insurance Scheme.',
|
| 100 |
+
category: 'Dam Operations',
|
| 101 |
+
relatedGRs: ['GR-2022-WRD-067', 'GR-2022-WRD-112'],
|
| 102 |
+
keywords: ['safety', 'failure', 'prevention', 'monitoring', 'insurance']
|
| 103 |
+
},
|
| 104 |
+
|
| 105 |
+
// Irrigation & Water Rights
|
| 106 |
+
{
|
| 107 |
+
id: 'faq-irrigation-1',
|
| 108 |
+
question: 'What is the water distribution priority in Maharashtra?',
|
| 109 |
+
answer: 'Water distribution priority as per Maharashtra Irrigation Act and GR-2024-WRD-042:\n\n**Priority Order:**\n1. **Drinking Water** - Highest priority for municipal supply\n2. **Agriculture/Irrigation** - Second priority for crop cultivation\n3. **Industrial Use** - Third priority for factories and processing\n4. **Recreation/Other** - Lowest priority\n\n**During Scarcity (Drought):**\n- Industrial supply reduced by 50%\n- Irrigation limited to protective irrigation (life-saving water)\n- Drinking water supply maintained at all costs\n\n**Real-time Adjustments:**\n- WRD can modify allocations based on storage levels\n- Water User Associations informed 48 hours in advance\n- Grievance redressal through digital portal\n\nViolations: Unauthorized diversion punishable with up to 3 years imprisonment.',
|
| 110 |
+
category: 'Irrigation & Water Rights',
|
| 111 |
+
relatedGRs: ['GR-2024-WRD-042'],
|
| 112 |
+
keywords: ['priority', 'drinking water', 'agriculture', 'industrial', 'scarcity']
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
id: 'faq-irrigation-2',
|
| 116 |
+
question: 'How do I form a Water User Association (WUA)?',
|
| 117 |
+
answer: 'Water User Association formation process as per GR-2023-WRD-078:\n\n**Eligibility:**\n- Minimum 50 farmer members\n- Command area of 100-500 hectares\n- All members must be landowners/tenants in the area\n\n**Formation Steps:**\n1. **Application:** Submit to Executive Engineer, Irrigation Division\n2. **Verification:** Revenue records checked (30 days)\n3. **Registration:** Society registration under Maharashtra Societies Act\n4. **Election:** Managing committee elected (7 members)\n5. **Recognition:** WRD issues recognition certificate\n\n**WUA Responsibilities:**\n- Water distribution within command area\n- Minor canal maintenance\n- Collection of water cess (5%)\n- Dispute resolution among members\n\n**Benefits:**\n- Direct water allocation from WRD\n- 10% discount on water cess\n- Priority in water allocation during scarcity\n\nApplication form available at wrd.maharashtra.gov.in/wua',
|
| 118 |
+
category: 'Irrigation & Water Rights',
|
| 119 |
+
relatedGRs: ['GR-2023-WRD-078'],
|
| 120 |
+
keywords: ['WUA', 'water user association', 'formation', 'farmer', 'registration']
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
id: 'faq-irrigation-3',
|
| 124 |
+
question: 'What are the rights of a Tahsildar regarding water disputes?',
|
| 125 |
+
answer: 'Tahsildar\'s water-related powers as per GR-2023-WRD-089:\n\n**Dispute Resolution:**\n- Resolve disputes between farmers for canal water\n- Mediate conflicts between WUAs\n- Order temporary water allocation during scarcity\n\n**Administrative Powers:**\n- Issue NOC for borewell/tubewell\n- Grant temporary water rights (up to 30 days)\n- Impose penalties for unauthorized water use (up to ₹10,000)\n\n**Emergency Powers:**\n- Order water tanker supply during drought\n- Requisition private wells for drinking water\n- Coordinate with WRD for emergency releases\n\n**Limitations:**\n- Cannot modify permanent water rights\n- Cannot override WRD allocation decisions\n- Appeals lie with Sub-Divisional Officer\n\nFor grievances, use: wrd.maharashtra.gov.in/grievance',
|
| 126 |
+
category: 'Irrigation & Water Rights',
|
| 127 |
+
relatedGRs: ['GR-2023-WRD-089'],
|
| 128 |
+
keywords: ['tahsildar', 'powers', 'dispute', 'water rights', 'grievance']
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
id: 'faq-irrigation-4',
|
| 132 |
+
question: 'How is water cess calculated and collected?',
|
| 133 |
+
answer: 'Water cess calculation and collection:\n\n**Cess Rates (per hectare per season):**\n- Kharif (Monsoon crop): ₹150\n- Rabi (Winter crop): ₹250\n- Perennial (Sugarcane, banana): ₹600\n\n**Collection Process:**\n1. Assessment by Irrigation Department based on cropped area\n2. Bill issued to farmer/WUA\n3. Payment at Talathi office or online via wrd.maharashtra.gov.in\n4. Receipt mandatory for next season\'s water supply\n\n**Concessions:**\n- 10% discount for WUA members\n- 50% concession for SC/ST farmers\n- Full waiver for crop failure due to natural calamity\n\n**Usage of Cess:**\n- 5% retained for local maintenance fund\n- 95% deposited to WRD consolidated fund\n\nDefaulters: Water supply disconnected; penalty of 25% on arrears.',
|
| 134 |
+
category: 'Irrigation & Water Rights',
|
| 135 |
+
keywords: ['water cess', 'calculation', 'collection', 'rates', 'payment']
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
id: 'faq-irrigation-5',
|
| 139 |
+
question: 'Can I use groundwater for irrigation without permission?',
|
| 140 |
+
answer: 'Groundwater extraction rules as per GR-2023-WRD-045:\n\n**Registration Required:**\n- All tubewells/borewells >60m depth\n- Extraction >100 cubic meters/day\n- Commercial/industrial use\n\n**NOC Requirements:**\n- NOC from Tahsildar for wells >60m\n- NOC from Groundwater Authority for >200m\n- Environmental clearance for >500m\n\n**Exemptions (No Permission Needed):**\n- Hand pumps (any depth)\n- Wells <60m for personal use\n- Extraction <50 cubic meters/day for domestic use\n\n**Penalties:**\n- Unauthorized extraction: ₹10,000 fine + well sealing\n- Over-extraction: ₹5,000 per 100 cubic meters\n- Repeated violation: Criminal prosecution\n\n**Apply Online:** groundwater.maharashtra.gov.in\n\nNote: Over-exploited blocks (marked red) have stricter controls.',
|
| 141 |
+
category: 'Irrigation & Water Rights',
|
| 142 |
+
relatedGRs: ['GR-2023-WRD-045'],
|
| 143 |
+
keywords: ['groundwater', 'permission', 'NOC', 'irrigation', 'penalty']
|
| 144 |
+
},
|
| 145 |
+
|
| 146 |
+
// Compensation & Relief
|
| 147 |
+
{
|
| 148 |
+
id: 'faq-compensation-1',
|
| 149 |
+
question: 'What compensation is available for flood damage?',
|
| 150 |
+
answer: 'Flood relief compensation under GR-2024-WRD-089:\n\n**Crop Loss:**\n- ₹5,000 per acre (maximum 10 acres)\n- Covers all crops including horticulture\n- Assessment by joint team (Revenue + Agriculture)\n\n**House Damage:**\n- Fully damaged: ₹95,000\n- Partially damaged: ₹50,000\n- Hut/temporary structure: ₹25,000\n\n**Livestock Loss:**\n- Large animals (cow, buffalo): ₹30,000\n- Small animals (goat, sheep): ₹4,000\n- Maximum 5 animals per household\n\n**Eligibility:**\n- Residence in notified flood-affected area\n- Damage during notified period (July 1 - October 31, 2024)\n- Registered with local Tahsildar\n\n**Application Process:**\n1. Submit Form WRD-FLOOD-01 to Tahsildar\n2. Attach damage assessment report\n3. Processing time: 30 days\n4. Payment via direct bank transfer',
|
| 151 |
+
category: 'Compensation & Relief',
|
| 152 |
+
relatedGRs: ['GR-2024-WRD-089'],
|
| 153 |
+
keywords: ['compensation', 'flood', 'crop loss', 'house damage', 'livestock']
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
id: 'faq-compensation-2',
|
| 157 |
+
question: 'How do I apply for flood compensation?',
|
| 158 |
+
answer: 'Flood compensation application process:\n\n**Step 1: Report Damage (Within 7 Days)**\n- Inform Gram Panchayat/Talathi\n- Get damage report number\n\n**Step 2: Document Damage**\n- Take photos/videos of damage\n- Preserve damaged items for inspection\n- Get witness statements\n\n**Step 3: Submit Application**\n- Form: WRD-FLOOD-01 (available at Tahsil office)\n- Documents: Aadhaar, bank passbook, land records, damage photos\n- Submit to: Tahsildar office\n\n**Step 4: Inspection**\n- Joint team visits within 15 days\n- Assessment report prepared\n- You get inspection copy\n\n**Step 5: Approval & Payment**\n- Tahsildar approves within 7 days\n- Payment within 30 days\n- Direct bank transfer\n\n**Track Application:**\n- SMS "FLOOD <Application ID>" to 9223456789\n- Online: wrd.maharashtra.gov.in/relief\n\n**Appeal:** If rejected, appeal to SDO within 30 days.',
|
| 159 |
+
category: 'Compensation & Relief',
|
| 160 |
+
relatedGRs: ['GR-2024-WRD-089'],
|
| 161 |
+
keywords: ['apply', 'compensation', 'process', 'documents', 'tracking']
|
| 162 |
+
},
|
| 163 |
+
{
|
| 164 |
+
id: 'faq-compensation-3',
|
| 165 |
+
question: 'What drought relief is available for farmers?',
|
| 166 |
+
answer: 'Drought relief package (GR-2024-WRD-078 for Vidarbha, similar for other regions):\n\n**Drinking Water Supply:**\n- Free tanker water supply\n- Borewell drilling at government cost\n- Pipeline extension to uncovered villages\n\n**Agricultural Support:**\n- Crop loss compensation: ₹6,800/ha (same as flood)\n- Input subsidy: ₹2,000/ha for re-sowing\n- Seed distribution: Free drought-resistant seeds\n\n**Employment:**\n- MGNREGA works extended (150 days instead of 100)\n- Priority for water conservation works\n- Fodder camp employment\n\n**Livestock Support:**\n- Fodder camps (free fodder + water)\n- Cattle camps with veterinary care\n- Subsidized cattle feed\n\n**Financial:**\n- Crop loan waiver for affected farmers\n- Fresh crop loans at 0% interest\n- Electricity bill waiver for agricultural pumps\n\n**Application:** Through Gram Panchayat to District Collector.',
|
| 167 |
+
category: 'Compensation & Relief',
|
| 168 |
+
relatedGRs: ['GR-2024-WRD-078'],
|
| 169 |
+
keywords: ['drought', 'relief', 'farmer', 'employment', 'MGNREGA']
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
id: 'faq-compensation-4',
|
| 173 |
+
question: 'How long does it take to receive compensation?',
|
| 174 |
+
answer: 'Compensation processing timelines:\n\n**Standard Timeline (30 Days):**\n- Application submission to payment: 30 days\n- Breakdown:\n - Inspection: 15 days\n - Approval: 7 days\n - Payment processing: 8 days\n\n**Fast-Track Cases (15 Days):**\n- Death compensation\n- Complete house collapse\n- Large-scale crop failure (>50% village)\n\n**Delayed Cases:**\n- Disputed land ownership: Additional 30 days\n- Multiple claimants: Additional 15 days\n- Bank account issues: Resolved within 7 days\n\n**Tracking:**\n- SMS updates at each stage\n- Online portal shows current status\n- Helpline: 1800-XXX-XXXX\n\n**Complaints:**\n- Delay beyond 30 days: Complain to SDO\n- No response in 7 days: Escalate to Collector\n- Grievance portal: wrd.maharashtra.gov.in/grievance\n\n**Interest:** If delayed beyond 45 days, 6% interest payable.',
|
| 175 |
+
category: 'Compensation & Relief',
|
| 176 |
+
keywords: ['timeline', 'processing', 'days', 'tracking', 'delay']
|
| 177 |
+
},
|
| 178 |
+
{
|
| 179 |
+
id: 'faq-compensation-5',
|
| 180 |
+
question: 'What documents are needed for compensation claim?',
|
| 181 |
+
answer: 'Required documents for compensation claim:\n\n**Identity & Address:**\n- Aadhaar card (mandatory)\n- Ration card\n- Voter ID (if Aadhaar not available)\n\n**Land Documents:**\n- 7/12 extract (Satbara)\n- 8A extract\n- Land ownership proof\n\n**Bank Details:**\n- Bank passbook copy (first page)\n- Account must be Aadhaar-linked\n- Cancelled cheque (optional)\n\n**Damage Evidence:**\n- Photos of damage (minimum 5)\n- Video if available\n- Witness statements (2 witnesses)\n\n**Additional for Specific Claims:**\n- Crop loss: Girdawari report\n- House damage: Structural assessment report\n- Livestock: Veterinary certificate, ear tag number\n\n**Self-Declaration:**\n- Affidavit stating damage details\n- Declaration of no previous claim\n- Signature verification\n\n**Submission:**\n- Originals for verification\n- Self-attested copies retained\n- Online upload option available',
|
| 182 |
+
category: 'Compensation & Relief',
|
| 183 |
+
keywords: ['documents', 'required', 'Aadhaar', 'bank', 'evidence']
|
| 184 |
+
},
|
| 185 |
+
|
| 186 |
+
// GRs & Acts
|
| 187 |
+
{
|
| 188 |
+
id: 'faq-grs-1',
|
| 189 |
+
question: 'What is the difference between a GR, Act, and Circular?',
|
| 190 |
+
answer: 'Government document types explained:\n\n**Government Resolution (GR):**\n- Executive order by government department\n- Implements policy decisions\n- Can be amended or superseded by later GR\n- Example: GR-2024-WRD-089 (Flood compensation)\n\n**Act (Law):**\n- Passed by State Legislature\n- Creates legal framework\n- Cannot be changed by GR (only amendment by legislature)\n- Example: Maharashtra Irrigation Act, 1976\n\n**Circular:**\n- Administrative instruction\n- Clarifies implementation of GR/Act\n- Internal departmental guidance\n- Example: Monsoon preparedness circular\n\n**Hierarchy:**\nAct > GR > Circular\n\n**Legal Force:**\n- Act: Full legal force, courts enforce\n- GR: Administrative binding, can be challenged\n- Circular: Internal guidance, not legally binding\n\n**Finding Documents:** Use the GR Finder on this portal to search all types.',
|
| 191 |
+
category: 'GRs & Acts',
|
| 192 |
+
keywords: ['GR', 'Act', 'Circular', 'difference', 'types']
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
id: 'faq-grs-2',
|
| 196 |
+
question: 'How do I find a specific Government Resolution?',
|
| 197 |
+
answer: 'Ways to find a Government Resolution:\n\n**On This Portal:**\n1. Go to "GR Finder" from menu\n2. Search by GR number (e.g., "GR-2024-WRD-089")\n3. Or search by keywords (e.g., "flood compensation")\n4. Filter by year, department, category\n\n**Official Sources:**\n- wrd.maharashtra.gov.in → Documents section\n- maharashtra.gov.in → GR search\n- Mantralaya library (in-person)\n\n**GR Number Format:**\nGR-YYYY-DEPT-NUMBER\nExample: GR-2024-WRD-089\n- 2024: Year\n- WRD: Water Resources Department\n- 089: Serial number\n\n**Tips:**\n- Recent GRs are digitized (2015 onwards)\n- Older GRs may need RTI request\n- Popular GRs shown in GR Finder homepage\n\n**Need Help?** Use "GR Helper" AI assistant to find related GRs.',
|
| 198 |
+
category: 'GRs & Acts',
|
| 199 |
+
keywords: ['find', 'search', 'GR number', 'portal', 'official']
|
| 200 |
+
},
|
| 201 |
+
{
|
| 202 |
+
id: 'faq-grs-3',
|
| 203 |
+
question: 'What are the key Acts governing water resources in Maharashtra?',
|
| 204 |
+
answer: 'Major water resource Acts in Maharashtra:\n\n**Maharashtra Irrigation Act, 1976**\n- Governs irrigation water distribution\n- Defines WRD powers and responsibilities\n- Establishes water rights framework\n- Key sections: 33 (water priority), 45 (maintenance)\n\n**MWRRA Act, 2005**\n- Created Maharashtra Water Resources Regulatory Authority\n- Water pricing and tariff determination\n- Inter-basin water transfer approval\n- Annual water audit requirement\n\n**Maharashtra Groundwater Act, 2013**\n- Groundwater extraction regulation\n- Registration of wells\n- Protection of drinking water sources\n\n**Dam Safety Act, 2021 (Central)**\n- Dam classification and inspection\n- Emergency action plans\n- Safety review committees\n\n**Related Acts:**\n- Maharashtra Water Supply & Sewerage Act, 1976\n- Maharashtra Farmers\' Participation Act, 2005\n\nAll available in Repository section of this portal.',
|
| 205 |
+
category: 'GRs & Acts',
|
| 206 |
+
keywords: ['Acts', 'Irrigation Act', 'MWRRA', 'Groundwater', 'Dam Safety']
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
id: 'faq-grs-4',
|
| 210 |
+
question: 'How can I get a copy of an old GR?',
|
| 211 |
+
answer: 'Methods to obtain old Government Resolutions:\n\n**Online (2015 onwards):**\n- wrd.maharashtra.gov.in → Documents\n- This portal\'s GR Finder\n- Download PDF directly\n\n**RTI Application (Pre-2015):**\n- Apply to WRD Public Information Officer\n- Fee: ₹10 per page\n- Timeline: 30 days\n- Format: RTI application with GR details\n\n**Mantralaya Library:**\n- Visit: Mantralaya, Mumbai\n- Working hours: 10 AM - 5 PM\n- Reference section has GR archives\n- Photocopy available (₹2/page)\n\n**District Collector Office:**\n- Regional GR archives\n- Available in Marathi and English\n- Prior appointment recommended\n\n**GR Helper AI:**\n- Ask "Find GR about [topic] from [year]"\n- AI will search and provide summary\n- Link to download if digitized\n\n**Note:** Some sensitive GRs may have restricted access.',
|
| 212 |
+
category: 'GRs & Acts',
|
| 213 |
+
keywords: ['old GR', 'copy', 'RTI', 'archive', 'download']
|
| 214 |
+
},
|
| 215 |
+
{
|
| 216 |
+
id: 'faq-grs-5',
|
| 217 |
+
question: 'How do I know if a GR is still active or superseded?',
|
| 218 |
+
answer: 'Checking GR status:\n\n**On This Portal:**\n- GR Finder shows status badge: Active/Amended/Superseded\n- Click GR to see full history\n- Related GRs section shows amendments\n\n**Status Types:**\n- **Active:** Currently in force, follow this\n- **Amended:** Modified by later GR, check both\n- **Superseded:** Replaced by new GR, follow new one\n\n**Official Verification:**\n- WRD website shows current status\n- Contact department for clarification\n- Legal cell can confirm validity\n\n**Common Reasons for Supersession:**\n- Policy change by new government\n- Court order striking down provisions\n- Expiry of time-bound GR\n- Amendment incorporating new provisions\n\n**Example:**\nGR-2022-WRD-089 (Crop compensation) was superseded by GR-2024-WRD-089 with revised rates.\n\n**Best Practice:** Always check the latest GR on the subject before taking action.',
|
| 219 |
+
category: 'GRs & Acts',
|
| 220 |
+
keywords: ['status', 'active', 'superseded', 'amended', 'validity']
|
| 221 |
+
},
|
| 222 |
+
];
|
| 223 |
+
|
| 224 |
+
// Helper functions
|
| 225 |
+
export const getFAQsByCategory = (category: string) => faqData.filter(faq => faq.category === category);
|
| 226 |
+
export const searchFAQs = (query: string) => {
|
| 227 |
+
const q = query.toLowerCase();
|
| 228 |
+
return faqData.filter(faq =>
|
| 229 |
+
faq.question.toLowerCase().includes(q) ||
|
| 230 |
+
faq.answer.toLowerCase().includes(q) ||
|
| 231 |
+
faq.keywords.some(k => k.includes(q))
|
| 232 |
+
);
|
| 233 |
+
};
|
src/data/mockGRs.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Mock Government Resolutions (GRs) data for WRD demo
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
export interface GRDocument {
|
| 6 |
+
id: string;
|
| 7 |
+
title: string;
|
| 8 |
+
department: string;
|
| 9 |
+
date: string;
|
| 10 |
+
year: number;
|
| 11 |
+
category: 'Flood Management' | 'Dam Safety' | 'Irrigation' | 'Compensation' | 'Water Policy' | 'Administrative';
|
| 12 |
+
status: 'Active' | 'Superseded' | 'Amended';
|
| 13 |
+
summary: string;
|
| 14 |
+
keywords: string[];
|
| 15 |
+
relatedGRs?: string[];
|
| 16 |
+
popularQuery?: boolean;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export const mockGRs: GRDocument[] = [
|
| 20 |
+
{
|
| 21 |
+
id: 'GR-2024-WRD-089',
|
| 22 |
+
title: 'Flood Relief Compensation Guidelines 2024',
|
| 23 |
+
department: 'Relief & Rehabilitation',
|
| 24 |
+
date: '2024-07-15',
|
| 25 |
+
year: 2024,
|
| 26 |
+
category: 'Compensation',
|
| 27 |
+
status: 'Active',
|
| 28 |
+
summary: 'Comprehensive guidelines for flood relief compensation including crop loss (₹5,000/acre), house damage (₹95,000 full), and livestock loss. Covers eligibility criteria, application process through Tahsildar, and direct bank transfer mechanism.',
|
| 29 |
+
keywords: ['compensation', 'flood relief', 'crop loss', 'house damage', 'tahsildar'],
|
| 30 |
+
popularQuery: true
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
id: 'GR-2024-WRD-056',
|
| 34 |
+
title: 'Emergency Dam Safety Protocol',
|
| 35 |
+
department: 'Dam Safety',
|
| 36 |
+
date: '2024-06-20',
|
| 37 |
+
year: 2024,
|
| 38 |
+
category: 'Dam Safety',
|
| 39 |
+
status: 'Active',
|
| 40 |
+
summary: 'Mandatory protocols for dam safety during monsoon including 2-hour advance notification for downstream districts, seismic monitoring requirements, and emergency discharge procedures.',
|
| 41 |
+
keywords: ['dam safety', 'emergency', 'discharge', 'monsoon', 'notification'],
|
| 42 |
+
relatedGRs: ['GR-2023-WRD-124'],
|
| 43 |
+
popularQuery: true
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
id: 'GR-2024-WRD-042',
|
| 47 |
+
title: 'Water Resource Allocation Policy',
|
| 48 |
+
department: 'Water Resources Planning',
|
| 49 |
+
date: '2024-04-20',
|
| 50 |
+
year: 2024,
|
| 51 |
+
category: 'Water Policy',
|
| 52 |
+
status: 'Active',
|
| 53 |
+
summary: 'Defines water allocation priority: Drinking water > Agriculture > Industrial use. Includes provisions for real-time allocation adjustments during scarcity periods.',
|
| 54 |
+
keywords: ['water allocation', 'priority', 'drinking water', 'agriculture', 'scarcity'],
|
| 55 |
+
popularQuery: true
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
id: 'GR-2023-WRD-124',
|
| 59 |
+
title: 'Dam Gate Operations Standard Operating Procedure',
|
| 60 |
+
department: 'Operations',
|
| 61 |
+
date: '2023-08-10',
|
| 62 |
+
year: 2023,
|
| 63 |
+
category: 'Dam Safety',
|
| 64 |
+
status: 'Active',
|
| 65 |
+
summary: 'Detailed SOP for dam gate operations including pre-opening checklist, gradual opening procedure (10% increments every 15 minutes), downstream notification requirements, and emergency override provisions.',
|
| 66 |
+
keywords: ['gate operation', 'SOP', 'dam gate', 'discharge', 'downstream'],
|
| 67 |
+
relatedGRs: ['GR-2024-WRD-056'],
|
| 68 |
+
popularQuery: true
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
id: 'GR-2023-WRD-098',
|
| 72 |
+
title: 'Canal Maintenance Guidelines',
|
| 73 |
+
department: 'Irrigation',
|
| 74 |
+
date: '2023-07-25',
|
| 75 |
+
year: 2023,
|
| 76 |
+
category: 'Irrigation',
|
| 77 |
+
status: 'Active',
|
| 78 |
+
summary: 'Guidelines for canal maintenance including responsibilities of WRD (major repairs >₹1 lakh) and Water User Associations (minor maintenance). Includes 5% water cess provision for maintenance fund.',
|
| 79 |
+
keywords: ['canal', 'maintenance', 'WUA', 'repair', 'water cess'],
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
id: 'GR-2022-WRD-089',
|
| 83 |
+
title: 'Crop Damage Compensation Policy',
|
| 84 |
+
department: 'Revenue & Forest',
|
| 85 |
+
date: '2022-09-22',
|
| 86 |
+
year: 2022,
|
| 87 |
+
category: 'Compensation',
|
| 88 |
+
status: 'Amended',
|
| 89 |
+
summary: 'Revised compensation rates and assessment methodology for agricultural losses caused by natural calamities. Superseded by GR-2024-WRD-089.',
|
| 90 |
+
keywords: ['crop damage', 'compensation', 'agriculture', 'natural calamity'],
|
| 91 |
+
relatedGRs: ['GR-2024-WRD-089']
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
id: 'GR-2024-WRD-023',
|
| 95 |
+
title: 'Monsoon Preparedness Circular',
|
| 96 |
+
department: 'Operations',
|
| 97 |
+
date: '2024-05-01',
|
| 98 |
+
year: 2024,
|
| 99 |
+
category: 'Flood Management',
|
| 100 |
+
status: 'Active',
|
| 101 |
+
summary: 'Pre-monsoon checklist for all dam control rooms including sensor calibration, alert system testing, control room staffing requirements (min 2 engineers), and coordination protocols.',
|
| 102 |
+
keywords: ['monsoon', 'preparedness', 'control room', 'sensor', 'alert'],
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
id: 'GR-2024-WRD-067',
|
| 106 |
+
title: 'Real-time Monitoring Guidelines',
|
| 107 |
+
department: 'IT & Operations',
|
| 108 |
+
date: '2024-04-10',
|
| 109 |
+
year: 2024,
|
| 110 |
+
category: 'Dam Safety',
|
| 111 |
+
status: 'Active',
|
| 112 |
+
summary: 'IoT sensor deployment guidelines for live dam monitoring including water level sensors, flow meters, seismic monitors, and data transmission protocols.',
|
| 113 |
+
keywords: ['IoT', 'monitoring', 'sensor', 'real-time', 'data'],
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
id: 'GR-2023-WRD-156',
|
| 117 |
+
title: 'Flood Evacuation Standard Operating Procedure',
|
| 118 |
+
department: 'Disaster Management',
|
| 119 |
+
date: '2023-06-15',
|
| 120 |
+
year: 2023,
|
| 121 |
+
category: 'Flood Management',
|
| 122 |
+
status: 'Active',
|
| 123 |
+
summary: 'Comprehensive evacuation protocols for flood-affected areas including warning levels, evacuation zones, relief camp setup, and inter-departmental coordination.',
|
| 124 |
+
keywords: ['evacuation', 'flood', 'relief camp', 'warning', 'coordination'],
|
| 125 |
+
popularQuery: true
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
id: 'GR-2023-WRD-078',
|
| 129 |
+
title: 'Water User Association Formation Guidelines',
|
| 130 |
+
department: 'Irrigation',
|
| 131 |
+
date: '2023-03-20',
|
| 132 |
+
year: 2023,
|
| 133 |
+
category: 'Irrigation',
|
| 134 |
+
status: 'Active',
|
| 135 |
+
summary: 'Guidelines for forming Water User Associations including membership criteria, election procedures, responsibilities, and water distribution management.',
|
| 136 |
+
keywords: ['WUA', 'water user association', 'formation', 'irrigation'],
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
id: 'GR-2022-WRD-112',
|
| 140 |
+
title: 'Dam Inspection Frequency Requirements',
|
| 141 |
+
department: 'Dam Safety',
|
| 142 |
+
date: '2022-12-05',
|
| 143 |
+
year: 2022,
|
| 144 |
+
category: 'Dam Safety',
|
| 145 |
+
status: 'Active',
|
| 146 |
+
summary: 'Mandatory inspection schedules for dams: Daily during monsoon, Weekly during normal period, Comprehensive annual inspection before monsoon.',
|
| 147 |
+
keywords: ['inspection', 'dam', 'frequency', 'monsoon', 'annual'],
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
id: 'GR-2024-WRD-034',
|
| 151 |
+
title: 'Inter-state River Water Sharing Agreement',
|
| 152 |
+
department: 'Water Resources',
|
| 153 |
+
date: '2024-03-15',
|
| 154 |
+
year: 2024,
|
| 155 |
+
category: 'Water Policy',
|
| 156 |
+
status: 'Active',
|
| 157 |
+
summary: 'Agreement with neighboring states on Krishna and Godavari river water sharing including allocation percentages, dispute resolution mechanism, and monitoring committee.',
|
| 158 |
+
keywords: ['inter-state', 'river', 'water sharing', 'Krishna', 'Godavari'],
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
id: 'GR-2023-WRD-045',
|
| 162 |
+
title: 'Groundwater Extraction Regulation',
|
| 163 |
+
department: 'Groundwater',
|
| 164 |
+
date: '2023-05-10',
|
| 165 |
+
year: 2023,
|
| 166 |
+
category: 'Water Policy',
|
| 167 |
+
status: 'Active',
|
| 168 |
+
summary: 'Regulations for groundwater extraction including registration requirements, permissible limits, NOC requirements, and penalty provisions for over-extraction.',
|
| 169 |
+
keywords: ['groundwater', 'extraction', 'regulation', 'NOC', 'penalty'],
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
id: 'GR-2024-WRD-101',
|
| 173 |
+
title: 'Digital Grievance Portal Launch',
|
| 174 |
+
department: 'Administration',
|
| 175 |
+
date: '2024-08-01',
|
| 176 |
+
year: 2024,
|
| 177 |
+
category: 'Administrative',
|
| 178 |
+
status: 'Active',
|
| 179 |
+
summary: 'Launch of digital grievance portal (wrd.maharashtra.gov.in/grievance) for water-related complaints including tracking system, response timelines (7-30 days), and escalation matrix.',
|
| 180 |
+
keywords: ['grievance', 'portal', 'digital', 'complaint', 'tracking'],
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
id: 'GR-2023-WRD-089',
|
| 184 |
+
title: 'Tahsildar Water Rights Powers',
|
| 185 |
+
department: 'Revenue',
|
| 186 |
+
date: '2023-04-15',
|
| 187 |
+
year: 2023,
|
| 188 |
+
category: 'Administrative',
|
| 189 |
+
status: 'Active',
|
| 190 |
+
summary: 'Defines powers of Tahsildar regarding water rights including dispute resolution, temporary water allocation during scarcity, and penalty imposition for unauthorized use.',
|
| 191 |
+
keywords: ['tahsildar', 'water rights', 'powers', 'dispute', 'scarcity'],
|
| 192 |
+
},
|
| 193 |
+
{
|
| 194 |
+
id: 'GR-2024-WRD-078',
|
| 195 |
+
title: 'Drought Relief Package for Vidarbha',
|
| 196 |
+
department: 'Relief & Rehabilitation',
|
| 197 |
+
date: '2024-05-20',
|
| 198 |
+
year: 2024,
|
| 199 |
+
category: 'Compensation',
|
| 200 |
+
status: 'Active',
|
| 201 |
+
summary: 'Special drought relief package for Vidarbha region including drinking water supply arrangements, fodder camps, crop loss compensation, and employment guarantee schemes.',
|
| 202 |
+
keywords: ['drought', 'Vidarbha', 'relief', 'drinking water', 'employment'],
|
| 203 |
+
},
|
| 204 |
+
{
|
| 205 |
+
id: 'GR-2022-WRD-067',
|
| 206 |
+
title: 'Dam Safety Audit Requirements',
|
| 207 |
+
department: 'Dam Safety',
|
| 208 |
+
date: '2022-10-15',
|
| 209 |
+
year: 2022,
|
| 210 |
+
category: 'Dam Safety',
|
| 211 |
+
status: 'Active',
|
| 212 |
+
summary: 'Mandatory annual safety audit requirements for all large dams including structural integrity assessment, seismic risk evaluation, and emergency action plan review.',
|
| 213 |
+
keywords: ['safety audit', 'dam', 'structural', 'seismic', 'emergency plan'],
|
| 214 |
+
},
|
| 215 |
+
{
|
| 216 |
+
id: 'GR-2024-WRD-056',
|
| 217 |
+
title: 'Konkan Region Flood Early Warning System',
|
| 218 |
+
department: 'Disaster Management',
|
| 219 |
+
date: '2024-06-01',
|
| 220 |
+
year: 2024,
|
| 221 |
+
category: 'Flood Management',
|
| 222 |
+
status: 'Active',
|
| 223 |
+
summary: 'Implementation of early warning system for Konkan region including rainfall monitoring, river level sensors, SMS alerts, and community communication network.',
|
| 224 |
+
keywords: ['early warning', 'Konkan', 'flood', 'SMS', 'monitoring'],
|
| 225 |
+
},
|
| 226 |
+
];
|
| 227 |
+
|
| 228 |
+
// Helper functions
|
| 229 |
+
export const getPopularGRs = () => mockGRs.filter(gr => gr.popularQuery);
|
| 230 |
+
export const getGRsByCategory = (category: string) => mockGRs.filter(gr => gr.category === category);
|
| 231 |
+
export const getGRsByYear = (year: number) => mockGRs.filter(gr => gr.year === year);
|
| 232 |
+
export const searchGRs = (query: string) => {
|
| 233 |
+
const q = query.toLowerCase();
|
| 234 |
+
return mockGRs.filter(gr =>
|
| 235 |
+
gr.title.toLowerCase().includes(q) ||
|
| 236 |
+
gr.id.toLowerCase().includes(q) ||
|
| 237 |
+
gr.keywords.some(k => k.includes(q)) ||
|
| 238 |
+
gr.summary.toLowerCase().includes(q)
|
| 239 |
+
);
|
| 240 |
+
};
|
| 241 |
+
|
| 242 |
+
export const grCategories = ['Flood Management', 'Dam Safety', 'Irrigation', 'Compensation', 'Water Policy', 'Administrative'] as const;
|
| 243 |
+
export const grYears = [...new Set(mockGRs.map(gr => gr.year))].sort((a, b) => b - a);
|
| 244 |
+
export const grDepartments = [...new Set(mockGRs.map(gr => gr.department))];
|
src/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {StrictMode} from 'react';
|
| 2 |
+
import {createRoot} from 'react-dom/client';
|
| 3 |
+
import App from './App.tsx';
|
| 4 |
+
import './index.css';
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
);
|
src/pages/Chatbot.tsx
ADDED
|
@@ -0,0 +1,781 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
+
import { useLocation, useNavigate } from 'react-router-dom';
|
| 3 |
+
import { Input } from '@/src/components/ui/Input';
|
| 4 |
+
import { Button } from '@/src/components/ui/Button';
|
| 5 |
+
import { Badge } from '@/src/components/ui/Badge';
|
| 6 |
+
import {
|
| 7 |
+
Send, Bot, User, Sparkles, Loader2, X, MessageCircle,
|
| 8 |
+
ScrollText, BookOpen, Scale, FileText, AlertTriangle,
|
| 9 |
+
Droplets, MapPin, TrendingUp, Clock,
|
| 10 |
+
Shield, Waves, Database, Building2, FileSearch,
|
| 11 |
+
Search, ChevronRight, Star, ArrowRight, HelpCircle
|
| 12 |
+
} from 'lucide-react';
|
| 13 |
+
import { cn } from '@/src/utils/cn';
|
| 14 |
+
import { getPopularGRs, mockGRs } from '@/src/data/mockGRs';
|
| 15 |
+
|
| 16 |
+
type DocumentType = 'GR' | 'Act' | 'Circular' | 'Manual' | 'SOP' | 'Report';
|
| 17 |
+
|
| 18 |
+
interface Document {
|
| 19 |
+
id: string;
|
| 20 |
+
title: string;
|
| 21 |
+
type: DocumentType;
|
| 22 |
+
date: string;
|
| 23 |
+
department: string;
|
| 24 |
+
relevance: number;
|
| 25 |
+
summary?: string;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
interface Insight {
|
| 29 |
+
title: string;
|
| 30 |
+
value: string;
|
| 31 |
+
trend?: string;
|
| 32 |
+
trendUp?: boolean;
|
| 33 |
+
icon: React.ElementType;
|
| 34 |
+
color: string;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
type Message = {
|
| 38 |
+
id: string;
|
| 39 |
+
role: 'user' | 'assistant';
|
| 40 |
+
content: string;
|
| 41 |
+
sources?: Document[];
|
| 42 |
+
insights?: Insight[];
|
| 43 |
+
timestamp?: Date;
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
// Extended mock document database
|
| 47 |
+
const mockDocuments: Document[] = [
|
| 48 |
+
{ id: 'GR-2024-WRD-089', title: 'Flood Relief Compensation Guidelines 2024', type: 'GR', date: '2024-07-15', department: 'Relief & Rehabilitation', relevance: 95, summary: 'Compensation rates: ₹5,000/acre for crop loss, ₹95,000 for house damage' },
|
| 49 |
+
{ id: 'GR-2024-WRD-056', title: 'Emergency Dam Safety Protocol', type: 'GR', date: '2024-06-20', department: 'Dam Safety', relevance: 92, summary: 'Mandatory 2-hour advance notification for downstream districts' },
|
| 50 |
+
{ id: 'ACT-1976-012', title: 'Maharashtra Irrigation Act', type: 'Act', date: '1976-05-15', department: 'Legislative', relevance: 88, summary: 'Governs water distribution, irrigation rights, and canal maintenance' },
|
| 51 |
+
{ id: 'CIRC-2024-WRD-023', title: 'Monsoon Preparedness Circular', type: 'Circular', date: '2024-05-01', department: 'Operations', relevance: 85, summary: 'Pre-monsoon checklist for all dam control rooms' },
|
| 52 |
+
{ id: 'GR-2023-WRD-124', title: 'Dam Gate Operations SOP', type: 'SOP', date: '2023-08-10', department: 'Operations', relevance: 90, summary: 'Standard procedures for gate opening/closing with safety checks' },
|
| 53 |
+
{ id: 'MANUAL-2024-001', title: 'Flood Management Field Manual', type: 'Manual', date: '2024-03-15', department: 'Training', relevance: 82, summary: 'Field guide for emergency response teams' },
|
| 54 |
+
{ id: 'GR-2024-WRD-042', title: 'Water Resource Allocation Policy', type: 'GR', date: '2024-04-20', department: 'Planning', relevance: 78, summary: 'Priority: Drinking > Agriculture > Industrial use' },
|
| 55 |
+
{ id: 'ACT-2005-WRD-003', title: 'MWRRA Act', type: 'Act', date: '2005-08-12', department: 'Legislative', relevance: 75, summary: 'Regulatory authority for water pricing and distribution' },
|
| 56 |
+
{ id: 'CIRC-2024-WRD-015', title: 'Real-time Monitoring Guidelines', type: 'Circular', date: '2024-04-10', department: 'IT & Operations', relevance: 80, summary: 'IoT sensor deployment for live dam monitoring' },
|
| 57 |
+
{ id: 'RPT-2024-001', title: 'Koyna Dam Safety Audit', type: 'Report', date: '2024-06-01', department: 'Dam Safety', relevance: 94, summary: 'Structural integrity: GOOD | Seismic risk: MODERATE' },
|
| 58 |
+
];
|
| 59 |
+
|
| 60 |
+
// GR Helper Quick Actions
|
| 61 |
+
const grQuickActions = [
|
| 62 |
+
{ text: 'Find latest flood relief GR', icon: Search, color: 'blue' },
|
| 63 |
+
{ text: 'Explain GR-2024-WRD-089', icon: FileText, color: 'indigo' },
|
| 64 |
+
{ text: 'Compare compensation GRs', icon: Scale, color: 'purple' },
|
| 65 |
+
{ text: 'Dam safety protocols', icon: Shield, color: 'red' },
|
| 66 |
+
];
|
| 67 |
+
|
| 68 |
+
// Query suggestions with categories - Focused on rainfall regions and GR rules
|
| 69 |
+
const queryCategories = [
|
| 70 |
+
{
|
| 71 |
+
title: 'Rainfall & Weather',
|
| 72 |
+
icon: Droplets,
|
| 73 |
+
color: 'blue',
|
| 74 |
+
queries: [
|
| 75 |
+
{ text: 'Konkan region rainfall data', icon: Droplets },
|
| 76 |
+
{ text: 'Vidarbha drought relief GR', icon: FileText },
|
| 77 |
+
]
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
title: 'GR Rules & Acts',
|
| 81 |
+
icon: Scale,
|
| 82 |
+
color: 'purple',
|
| 83 |
+
queries: [
|
| 84 |
+
{ text: 'Water distribution priority rules', icon: Shield },
|
| 85 |
+
{ text: 'Canal maintenance responsibility', icon: ScrollText },
|
| 86 |
+
]
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
title: 'Relief & Compensation',
|
| 90 |
+
icon: Waves,
|
| 91 |
+
color: 'red',
|
| 92 |
+
queries: [
|
| 93 |
+
{ text: 'Flood affected area compensation', icon: FileText },
|
| 94 |
+
{ text: 'Crop damage claim procedure', icon: BookOpen },
|
| 95 |
+
]
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
title: 'Administrative',
|
| 99 |
+
icon: Building2,
|
| 100 |
+
color: 'emerald',
|
| 101 |
+
queries: [
|
| 102 |
+
{ text: 'Tahsildar water rights powers', icon: Scale },
|
| 103 |
+
{ text: 'WUA formation guidelines', icon: MapPin },
|
| 104 |
+
]
|
| 105 |
+
},
|
| 106 |
+
];
|
| 107 |
+
|
| 108 |
+
// Generate insights based on query
|
| 109 |
+
const generateInsights = (query: string): Insight[] => {
|
| 110 |
+
const q = query.toLowerCase();
|
| 111 |
+
|
| 112 |
+
if (q.includes('koyna')) {
|
| 113 |
+
return [
|
| 114 |
+
{ title: 'Current Level', value: '92.4%', trend: '+2.1%', trendUp: true, icon: Droplets, color: 'text-blue-600' },
|
| 115 |
+
{ title: 'Storage', value: '2.8 TMC', trend: 'of 3.0 TMC', icon: Database, color: 'text-cyan-600' },
|
| 116 |
+
{ title: 'Inflow', value: '45,000', trend: 'cusecs', icon: Waves, color: 'text-indigo-600' },
|
| 117 |
+
{ title: 'Outflow', value: '32,000', trend: 'cusecs', icon: Waves, color: 'text-orange-600' },
|
| 118 |
+
];
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
if (q.includes('flood') && q.includes('compensation')) {
|
| 122 |
+
return [
|
| 123 |
+
{ title: 'Crop Loss', value: '₹5,000', trend: 'per acre', icon: FileText, color: 'text-emerald-600' },
|
| 124 |
+
{ title: 'House Damage', value: '₹95,000', trend: 'full damage', icon: Building2, color: 'text-blue-600' },
|
| 125 |
+
{ title: 'Processing Time', value: '30', trend: 'days', icon: Clock, color: 'text-orange-600' },
|
| 126 |
+
{ title: 'Claims 2024', value: '12,450', trend: '+23%', trendUp: true, icon: TrendingUp, color: 'text-purple-600' },
|
| 127 |
+
];
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
if (q.includes('dam') && q.includes('gate')) {
|
| 131 |
+
return [
|
| 132 |
+
{ title: 'Active Releases', value: '12', trend: '+3 today', trendUp: true, icon: Waves, color: 'text-blue-600' },
|
| 133 |
+
{ title: 'Critical Dams', value: '3', trend: 'High alert', icon: AlertTriangle, color: 'text-red-600' },
|
| 134 |
+
{ title: 'Total Outflow', value: '1.2M', trend: 'cusecs', icon: Droplets, color: 'text-cyan-600' },
|
| 135 |
+
{ title: 'Downstream Alert', value: '18', trend: 'districts', icon: Shield, color: 'text-orange-600' },
|
| 136 |
+
];
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
return [
|
| 140 |
+
{ title: 'Total Dams', value: '287', trend: 'monitored', icon: Database, color: 'text-blue-600' },
|
| 141 |
+
{ title: 'GRs 2024', value: '142', trend: 'indexed', icon: FileText, color: 'text-emerald-600' },
|
| 142 |
+
{ title: 'Active Alerts', value: '7', trend: 'critical', icon: AlertTriangle, color: 'text-red-600' },
|
| 143 |
+
{ title: 'Avg Storage', value: '68%', trend: '+5%', trendUp: true, icon: Droplets, color: 'text-cyan-600' },
|
| 144 |
+
];
|
| 145 |
+
};
|
| 146 |
+
|
| 147 |
+
// Generate response based on query
|
| 148 |
+
const generateResponse = (query: string): { content: string; docs: Document[] } => {
|
| 149 |
+
const q = query.toLowerCase();
|
| 150 |
+
|
| 151 |
+
if (q.includes('koyna')) {
|
| 152 |
+
return {
|
| 153 |
+
content: `**Koyna Dam - Current Status (as of ${new Date().toLocaleTimeString('en-IN')})**
|
| 154 |
+
|
| 155 |
+
• **Storage Level:** 92.4% (2.8 TMC of 3.0 TMC capacity)
|
| 156 |
+
• **Inflow:** 45,000 cusecs | **Outflow:** 32,000 cusecs
|
| 157 |
+
• **Status:** ⚠️ CRITICAL - Emergency discharge initiated
|
| 158 |
+
• **Districts Alerted:** Satara, Sangli, Kolhapur collectors notified
|
| 159 |
+
|
| 160 |
+
**Recent Actions:**
|
| 161 |
+
- 4 radial gates opened at 50% capacity
|
| 162 |
+
- Downstream evacuation advisory issued
|
| 163 |
+
- Hourly monitoring activated
|
| 164 |
+
|
| 165 |
+
Refer to **GR-2024-WRD-056** for emergency protocols.`,
|
| 166 |
+
docs: mockDocuments.filter(d => d.id.includes('056') || d.id.includes('RPT-2024-001'))
|
| 167 |
+
};
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
if (q.includes('flood') && q.includes('compensation')) {
|
| 171 |
+
return {
|
| 172 |
+
content: `**Flood Relief Compensation - 2024 Guidelines (GR-2024-WRD-089)**
|
| 173 |
+
|
| 174 |
+
**Eligibility Criteria:**
|
| 175 |
+
• Residence in notified flood-affected area
|
| 176 |
+
• Damage between July 1 - October 31, 2024
|
| 177 |
+
• Registration with local Tahsildar office
|
| 178 |
+
|
| 179 |
+
**Compensation Rates:**
|
| 180 |
+
• Crop Loss: ₹5,000 per acre (max 10 acres)
|
| 181 |
+
• House Damage: ₹95,000 (full) / ₹50,000 (partial)
|
| 182 |
+
• Livestock: ₹50,000 per animal (max 5)
|
| 183 |
+
|
| 184 |
+
**Application Process:**
|
| 185 |
+
1. Submit Form WRD-FLOOD-01 to Tahsildar
|
| 186 |
+
2. Attach damage assessment report
|
| 187 |
+
3. Processing time: 30 days
|
| 188 |
+
4. Direct bank transfer
|
| 189 |
+
|
| 190 |
+
Contact: District Relief Office for assistance.`,
|
| 191 |
+
docs: mockDocuments.filter(d => d.id.includes('089') || d.id.includes('001'))
|
| 192 |
+
};
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
if (q.includes('gate') || q.includes('sop')) {
|
| 196 |
+
return {
|
| 197 |
+
content: `**Dam Gate Operations SOP (GR-2023-WRD-124)**
|
| 198 |
+
|
| 199 |
+
**Pre-Opening Checklist:**
|
| 200 |
+
✓ Seismic sensors calibrated
|
| 201 |
+
✓ Downstream flow sensors active
|
| 202 |
+
✓ Alert systems tested
|
| 203 |
+
✓ Control room staffed (min 2 engineers)
|
| 204 |
+
|
| 205 |
+
**Opening Procedure:**
|
| 206 |
+
1. **Notification** (T-2 hours): Alert downstream districts
|
| 207 |
+
2. **Gradual Opening**: 10% increments every 15 minutes
|
| 208 |
+
3. **Flow Monitoring**: Continuous discharge measurement
|
| 209 |
+
4. **Public Announcement**: Local media + sirens
|
| 210 |
+
|
| 211 |
+
**Emergency Override:**
|
| 212 |
+
Chief Engineer can authorize immediate release if dam level exceeds 95%.
|
| 213 |
+
|
| 214 |
+
**Documentation:** All gate operations logged in DAMS system within 1 hour.`,
|
| 215 |
+
docs: mockDocuments.filter(d => d.id.includes('124') || d.id.includes('056'))
|
| 216 |
+
};
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
if (q.includes('irrigation') || q.includes('act')) {
|
| 220 |
+
return {
|
| 221 |
+
content: `**Maharashtra Irrigation Act, 1976 - Key Provisions**
|
| 222 |
+
|
| 223 |
+
**Section 33 - Water Rights:**
|
| 224 |
+
• Priority order: Drinking water > Agriculture > Industry
|
| 225 |
+
• Canal water distribution by WRD, not local bodies
|
| 226 |
+
• Penalty for unauthorized diversion: Up to 3 years imprisonment
|
| 227 |
+
|
| 228 |
+
**Section 45 - Maintenance:**
|
| 229 |
+
• WRD responsible for major repairs (>₹1 lakh)
|
| 230 |
+
• Water User Associations (WUAs) for minor maintenance
|
| 231 |
+
• 5% water cess collected for maintenance fund
|
| 232 |
+
|
| 233 |
+
**Recent Amendment (2024):**
|
| 234 |
+
• IoT-based usage monitoring mandated
|
| 235 |
+
• Real-time allocation adjustments during scarcity
|
| 236 |
+
• Digital grievance portal: wrd.maharashtra.gov.in/grievance`,
|
| 237 |
+
docs: mockDocuments.filter(d => d.type === 'Act')
|
| 238 |
+
};
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
const relevantDocs = mockDocuments
|
| 242 |
+
.filter(d => q.includes(d.type.toLowerCase()) ||
|
| 243 |
+
d.title.toLowerCase().includes(q.split(' ')[0]))
|
| 244 |
+
.slice(0, 3);
|
| 245 |
+
|
| 246 |
+
return {
|
| 247 |
+
content: `I found **${relevantDocs.length} relevant document(s)** for your query on "${query}".
|
| 248 |
+
|
| 249 |
+
${relevantDocs.map(d => `**${d.id}** - ${d.title}\n• ${d.summary || 'No summary available'}`).join('\n\n')}
|
| 250 |
+
|
| 251 |
+
Would you like me to provide detailed information about any of these documents, or search for something more specific?`,
|
| 252 |
+
docs: relevantDocs
|
| 253 |
+
};
|
| 254 |
+
};
|
| 255 |
+
|
| 256 |
+
export function Chatbot() {
|
| 257 |
+
const location = useLocation();
|
| 258 |
+
const navigate = useNavigate();
|
| 259 |
+
const [messages, setMessages] = useState<Message[]>([]);
|
| 260 |
+
const [input, setInput] = useState('');
|
| 261 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 262 |
+
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
| 263 |
+
const [showWelcome, setShowWelcome] = useState(true);
|
| 264 |
+
const [showSidebar, setShowSidebar] = useState(true);
|
| 265 |
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
| 266 |
+
const inputRef = useRef<HTMLInputElement>(null);
|
| 267 |
+
const popularGRs = getPopularGRs();
|
| 268 |
+
|
| 269 |
+
useEffect(() => {
|
| 270 |
+
const searchParams = new URLSearchParams(location.search);
|
| 271 |
+
const query = searchParams.get('q');
|
| 272 |
+
if (query) {
|
| 273 |
+
handleSend(query);
|
| 274 |
+
setShowWelcome(false);
|
| 275 |
+
}
|
| 276 |
+
}, [location]);
|
| 277 |
+
|
| 278 |
+
useEffect(() => {
|
| 279 |
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 280 |
+
}, [messages]);
|
| 281 |
+
|
| 282 |
+
const handleSend = async (text: string) => {
|
| 283 |
+
if (!text.trim()) return;
|
| 284 |
+
|
| 285 |
+
setShowWelcome(false);
|
| 286 |
+
const userMsg: Message = {
|
| 287 |
+
id: Date.now().toString(),
|
| 288 |
+
role: 'user',
|
| 289 |
+
content: text,
|
| 290 |
+
timestamp: new Date()
|
| 291 |
+
};
|
| 292 |
+
setMessages(prev => [...prev, userMsg]);
|
| 293 |
+
setInput('');
|
| 294 |
+
setIsLoading(true);
|
| 295 |
+
|
| 296 |
+
await new Promise(r => setTimeout(r, 800));
|
| 297 |
+
|
| 298 |
+
const { content, docs } = generateResponse(text);
|
| 299 |
+
const insights = generateInsights(text);
|
| 300 |
+
|
| 301 |
+
const aiMsg: Message = {
|
| 302 |
+
id: (Date.now() + 1).toString(),
|
| 303 |
+
role: 'assistant',
|
| 304 |
+
content,
|
| 305 |
+
sources: docs,
|
| 306 |
+
insights,
|
| 307 |
+
timestamp: new Date()
|
| 308 |
+
};
|
| 309 |
+
|
| 310 |
+
setMessages(prev => [...prev, aiMsg]);
|
| 311 |
+
setIsLoading(false);
|
| 312 |
+
};
|
| 313 |
+
|
| 314 |
+
const getTypeIcon = (type: DocumentType) => {
|
| 315 |
+
switch (type) {
|
| 316 |
+
case 'GR': return ScrollText;
|
| 317 |
+
case 'Act': return Scale;
|
| 318 |
+
case 'Circular': return FileText;
|
| 319 |
+
case 'Manual': return BookOpen;
|
| 320 |
+
case 'SOP': return Shield;
|
| 321 |
+
case 'Report': return FileSearch;
|
| 322 |
+
}
|
| 323 |
+
};
|
| 324 |
+
|
| 325 |
+
const formatTime = (date?: Date) => {
|
| 326 |
+
if (!date) return '';
|
| 327 |
+
return date.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit' });
|
| 328 |
+
};
|
| 329 |
+
|
| 330 |
+
const WelcomeScreen = () => (
|
| 331 |
+
<div className="flex flex-col justify-center gap-4 h-full w-full max-w-2xl mx-auto py-2">
|
| 332 |
+
{/* Top Section - Title */}
|
| 333 |
+
<div className="text-center">
|
| 334 |
+
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center mx-auto mb-2 shadow-md">
|
| 335 |
+
<Sparkles className="h-6 w-6 text-white" />
|
| 336 |
+
</div>
|
| 337 |
+
<h2 className="text-lg font-semibold text-slate-800">GR Helper</h2>
|
| 338 |
+
<p className="text-xs text-slate-500 mt-1">AI Assistant for Government Resolutions & Acts</p>
|
| 339 |
+
</div>
|
| 340 |
+
|
| 341 |
+
{/* GR Quick Actions */}
|
| 342 |
+
<div className="px-2 mt-2">
|
| 343 |
+
<p className="text-xs font-medium text-slate-500 mb-2 px-1">Quick Actions</p>
|
| 344 |
+
<div className="grid grid-cols-2 gap-2">
|
| 345 |
+
{grQuickActions.map((action, idx) => {
|
| 346 |
+
const Icon = action.icon;
|
| 347 |
+
return (
|
| 348 |
+
<button
|
| 349 |
+
key={idx}
|
| 350 |
+
onClick={() => handleSend(action.text)}
|
| 351 |
+
className="text-left p-3 bg-white rounded-lg border border-slate-200 hover:border-indigo-300 hover:shadow-sm active:bg-slate-50 transition-all"
|
| 352 |
+
>
|
| 353 |
+
<div className="flex items-center gap-2">
|
| 354 |
+
<div className={cn("p-1 rounded", `bg-${action.color}-100`)}>
|
| 355 |
+
<Icon className={cn("h-3.5 w-3.5", `text-${action.color}-600`)} />
|
| 356 |
+
</div>
|
| 357 |
+
<span className="text-xs font-medium text-slate-700 line-clamp-1">{action.text}</span>
|
| 358 |
+
</div>
|
| 359 |
+
</button>
|
| 360 |
+
);
|
| 361 |
+
})}
|
| 362 |
+
</div>
|
| 363 |
+
</div>
|
| 364 |
+
|
| 365 |
+
{/* Middle Section - Category Cards */}
|
| 366 |
+
<div className="px-2 mt-2">
|
| 367 |
+
<p className="text-xs font-medium text-slate-500 mb-2 px-1">Browse by Category</p>
|
| 368 |
+
<div className="grid grid-cols-2 gap-2">
|
| 369 |
+
{queryCategories.map((cat, idx) => {
|
| 370 |
+
const Icon = cat.icon;
|
| 371 |
+
return (
|
| 372 |
+
<button
|
| 373 |
+
key={idx}
|
| 374 |
+
onClick={() => handleSend(cat.queries[0].text)}
|
| 375 |
+
className="text-left p-3 bg-white rounded-lg border border-slate-200 hover:border-blue-300 active:bg-slate-50 transition-all shadow-sm"
|
| 376 |
+
>
|
| 377 |
+
<div className="flex items-center gap-2 mb-1">
|
| 378 |
+
<div className={cn("p-1 rounded", `bg-${cat.color}-100`)}>
|
| 379 |
+
<Icon className={cn("h-3.5 w-3.5", `text-${cat.color}-600`)} />
|
| 380 |
+
</div>
|
| 381 |
+
<span className="text-xs font-semibold text-slate-700">{cat.title}</span>
|
| 382 |
+
</div>
|
| 383 |
+
<p className="text-[10px] text-slate-500 truncate">{cat.queries[0].text}</p>
|
| 384 |
+
</button>
|
| 385 |
+
);
|
| 386 |
+
})}
|
| 387 |
+
</div>
|
| 388 |
+
</div>
|
| 389 |
+
|
| 390 |
+
{/* Bottom Section - Quick Links */}
|
| 391 |
+
<div className="flex flex-wrap justify-center gap-1.5 px-2 mt-2">
|
| 392 |
+
{['Konkan rainfall', 'Vidarbha GR', 'Water rights', 'Compensation'].map((s, i) => (
|
| 393 |
+
<button
|
| 394 |
+
key={i}
|
| 395 |
+
onClick={() => handleSend(s)}
|
| 396 |
+
className="text-[10px] px-2.5 py-1 bg-slate-100 text-slate-600 rounded-full hover:bg-slate-200 transition-colors"
|
| 397 |
+
>
|
| 398 |
+
{s}
|
| 399 |
+
</button>
|
| 400 |
+
))}
|
| 401 |
+
</div>
|
| 402 |
+
</div>
|
| 403 |
+
);
|
| 404 |
+
|
| 405 |
+
const RecentGRsSidebar = () => (
|
| 406 |
+
<div className={cn(
|
| 407 |
+
"w-64 border-l border-slate-200 bg-white flex-col shrink-0 hidden lg:flex",
|
| 408 |
+
showSidebar ? "lg:flex" : "lg:hidden"
|
| 409 |
+
)}>
|
| 410 |
+
<div className="p-3 border-b border-slate-100">
|
| 411 |
+
<div className="flex items-center justify-between">
|
| 412 |
+
<h3 className="text-xs font-semibold text-slate-700">Popular GRs</h3>
|
| 413 |
+
<Star className="h-3 w-3 text-yellow-500" />
|
| 414 |
+
</div>
|
| 415 |
+
</div>
|
| 416 |
+
<div className="flex-1 overflow-y-auto p-2 space-y-1.5">
|
| 417 |
+
{popularGRs.map(gr => (
|
| 418 |
+
<button
|
| 419 |
+
key={gr.id}
|
| 420 |
+
onClick={() => handleSend(`Explain ${gr.id}: ${gr.title}`)}
|
| 421 |
+
className="w-full text-left p-2 rounded-md hover:bg-slate-50 transition-colors group"
|
| 422 |
+
>
|
| 423 |
+
<div className="flex items-start justify-between gap-1">
|
| 424 |
+
<p className="text-[10px] font-mono text-indigo-600">{gr.id}</p>
|
| 425 |
+
<ChevronRight className="h-3 w-3 text-slate-300 group-hover:text-indigo-500 shrink-0 mt-0.5" />
|
| 426 |
+
</div>
|
| 427 |
+
<p className="text-xs text-slate-700 line-clamp-2 mt-0.5">{gr.title}</p>
|
| 428 |
+
<div className="flex items-center gap-1 mt-1">
|
| 429 |
+
<Badge className="text-[8px] px-1 py-0 h-auto bg-slate-100 text-slate-600">
|
| 430 |
+
{gr.category}
|
| 431 |
+
</Badge>
|
| 432 |
+
</div>
|
| 433 |
+
</button>
|
| 434 |
+
))}
|
| 435 |
+
</div>
|
| 436 |
+
<div className="p-2 border-t border-slate-100">
|
| 437 |
+
<Button
|
| 438 |
+
variant="ghost"
|
| 439 |
+
size="sm"
|
| 440 |
+
className="w-full text-xs h-8"
|
| 441 |
+
onClick={() => navigate('/gr-finder')}
|
| 442 |
+
>
|
| 443 |
+
<Search className="h-3 w-3 mr-1" />
|
| 444 |
+
Browse All GRs
|
| 445 |
+
</Button>
|
| 446 |
+
</div>
|
| 447 |
+
</div>
|
| 448 |
+
);
|
| 449 |
+
|
| 450 |
+
const MessageBubble = ({ msg }: { msg: Message }) => (
|
| 451 |
+
<div className={cn(
|
| 452 |
+
"flex w-full mb-4",
|
| 453 |
+
msg.role === 'user' ? "justify-end" : "justify-start"
|
| 454 |
+
)}>
|
| 455 |
+
<div className={cn(
|
| 456 |
+
"flex gap-3 max-w-[90%] md:max-w-[80%]",
|
| 457 |
+
msg.role === 'user' ? "flex-row-reverse" : "flex-row"
|
| 458 |
+
)}>
|
| 459 |
+
<div className={cn(
|
| 460 |
+
"flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center",
|
| 461 |
+
msg.role === 'user'
|
| 462 |
+
? "bg-blue-600 text-white"
|
| 463 |
+
: "bg-gradient-to-br from-blue-500 to-indigo-600 text-white"
|
| 464 |
+
)}>
|
| 465 |
+
{msg.role === 'user' ? <User className="h-4 w-4" /> : <Bot className="h-4 w-4" />}
|
| 466 |
+
</div>
|
| 467 |
+
|
| 468 |
+
<div className="flex flex-col gap-2">
|
| 469 |
+
<div className={cn(
|
| 470 |
+
"px-4 py-3 rounded-2xl text-sm leading-relaxed",
|
| 471 |
+
msg.role === 'user'
|
| 472 |
+
? "bg-blue-600 text-white rounded-tr-sm"
|
| 473 |
+
: "bg-white border border-slate-200 text-slate-700 rounded-tl-sm shadow-sm"
|
| 474 |
+
)}>
|
| 475 |
+
<div className="whitespace-pre-wrap">
|
| 476 |
+
{msg.content.split('**').map((part, i) =>
|
| 477 |
+
i % 2 === 0 ? part : <strong key={i} className="font-semibold">{part}</strong>
|
| 478 |
+
)}
|
| 479 |
+
</div>
|
| 480 |
+
</div>
|
| 481 |
+
|
| 482 |
+
{msg.insights && msg.insights.length > 0 && (
|
| 483 |
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-2 mt-1">
|
| 484 |
+
{msg.insights.map((insight, idx) => {
|
| 485 |
+
const Icon = insight.icon;
|
| 486 |
+
return (
|
| 487 |
+
<div key={idx} className="bg-white border border-slate-200 rounded-lg p-2.5 shadow-sm">
|
| 488 |
+
<div className="flex items-center gap-1.5 mb-1">
|
| 489 |
+
<Icon className={cn("h-3.5 w-3.5", insight.color)} />
|
| 490 |
+
<span className="text-[10px] text-slate-500 uppercase tracking-wider">{insight.title}</span>
|
| 491 |
+
</div>
|
| 492 |
+
<div className="flex items-baseline gap-1">
|
| 493 |
+
<span className="text-lg font-bold text-slate-800">{insight.value}</span>
|
| 494 |
+
{insight.trend && (
|
| 495 |
+
<span className={cn(
|
| 496 |
+
"text-[10px]",
|
| 497 |
+
insight.trendUp ? "text-emerald-600" : "text-slate-400"
|
| 498 |
+
)}>
|
| 499 |
+
{insight.trend}
|
| 500 |
+
</span>
|
| 501 |
+
)}
|
| 502 |
+
</div>
|
| 503 |
+
</div>
|
| 504 |
+
);
|
| 505 |
+
})}
|
| 506 |
+
</div>
|
| 507 |
+
)}
|
| 508 |
+
|
| 509 |
+
{msg.sources && msg.sources.length > 0 && (
|
| 510 |
+
<div className="flex flex-wrap gap-1.5 mt-1">
|
| 511 |
+
{msg.sources.map((doc, idx) => {
|
| 512 |
+
const TypeIcon = getTypeIcon(doc.type);
|
| 513 |
+
return (
|
| 514 |
+
<div
|
| 515 |
+
key={idx}
|
| 516 |
+
className="flex items-center gap-1.5 text-[11px] px-2.5 py-1.5 bg-slate-100 text-slate-600 rounded-md border border-slate-200"
|
| 517 |
+
>
|
| 518 |
+
<TypeIcon className="h-3 w-3" />
|
| 519 |
+
<span className="font-medium">{doc.id}</span>
|
| 520 |
+
<span className="text-slate-400">|</span>
|
| 521 |
+
<span className="truncate max-w-[120px]">{doc.title}</span>
|
| 522 |
+
</div>
|
| 523 |
+
);
|
| 524 |
+
})}
|
| 525 |
+
</div>
|
| 526 |
+
)}
|
| 527 |
+
|
| 528 |
+
<span className="text-[10px] text-slate-400 ml-1">
|
| 529 |
+
{formatTime(msg.timestamp)}
|
| 530 |
+
</span>
|
| 531 |
+
</div>
|
| 532 |
+
</div>
|
| 533 |
+
</div>
|
| 534 |
+
);
|
| 535 |
+
|
| 536 |
+
return (
|
| 537 |
+
<>
|
| 538 |
+
{/* Desktop */}
|
| 539 |
+
<div className="hidden md:flex h-full w-full flex-row bg-slate-50 overflow-hidden rounded-lg min-h-0">
|
| 540 |
+
{/* Main Chat Area */}
|
| 541 |
+
<div className="flex-1 flex flex-col min-w-0">
|
| 542 |
+
<div className="bg-white border-b border-slate-200 px-4 py-3 shrink-0">
|
| 543 |
+
<div className="flex items-center justify-between">
|
| 544 |
+
<div className="flex items-center gap-3">
|
| 545 |
+
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center shadow-sm">
|
| 546 |
+
<Sparkles className="h-4 w-4 text-white" />
|
| 547 |
+
</div>
|
| 548 |
+
<div>
|
| 549 |
+
<h1 className="text-base font-semibold text-slate-900">GR Helper</h1>
|
| 550 |
+
<p className="text-[11px] text-slate-500">AI Assistant for Government Resolutions</p>
|
| 551 |
+
</div>
|
| 552 |
+
</div>
|
| 553 |
+
<div className="flex items-center gap-2">
|
| 554 |
+
<Button
|
| 555 |
+
variant="ghost"
|
| 556 |
+
size="sm"
|
| 557 |
+
className="text-xs h-8"
|
| 558 |
+
onClick={() => navigate('/faq')}
|
| 559 |
+
>
|
| 560 |
+
<HelpCircle className="h-3.5 w-3.5 mr-1" />
|
| 561 |
+
FAQ
|
| 562 |
+
</Button>
|
| 563 |
+
<Button
|
| 564 |
+
variant="ghost"
|
| 565 |
+
size="sm"
|
| 566 |
+
className="text-xs h-8"
|
| 567 |
+
onClick={() => navigate('/gr-finder')}
|
| 568 |
+
>
|
| 569 |
+
<Search className="h-3.5 w-3.5 mr-1" />
|
| 570 |
+
GR Finder
|
| 571 |
+
</Button>
|
| 572 |
+
</div>
|
| 573 |
+
</div>
|
| 574 |
+
</div>
|
| 575 |
+
|
| 576 |
+
<div className="flex-1 overflow-hidden px-4 py-4 min-h-0">
|
| 577 |
+
{showWelcome && messages.length === 0 ? (
|
| 578 |
+
<WelcomeScreen />
|
| 579 |
+
) : (
|
| 580 |
+
<div className="max-w-3xl mx-auto h-full overflow-y-auto pr-2 min-h-0">
|
| 581 |
+
{messages.map((msg, i) => (
|
| 582 |
+
<React.Fragment key={msg.id || String(i)}>
|
| 583 |
+
<MessageBubble msg={msg} />
|
| 584 |
+
</React.Fragment>
|
| 585 |
+
))}
|
| 586 |
+
{isLoading && (
|
| 587 |
+
<div className="flex items-center gap-3 text-slate-500">
|
| 588 |
+
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-full flex items-center justify-center">
|
| 589 |
+
<Bot className="h-4 w-4 text-white" />
|
| 590 |
+
</div>
|
| 591 |
+
<div className="flex items-center gap-2 bg-white border border-slate-200 px-4 py-2.5 rounded-2xl rounded-tl-sm shadow-sm">
|
| 592 |
+
<Loader2 className="h-4 w-4 animate-spin text-blue-500" />
|
| 593 |
+
<span className="text-sm">Searching GR database...</span>
|
| 594 |
+
</div>
|
| 595 |
+
</div>
|
| 596 |
+
)}
|
| 597 |
+
<div ref={messagesEndRef} />
|
| 598 |
+
</div>
|
| 599 |
+
)}
|
| 600 |
+
</div>
|
| 601 |
+
|
| 602 |
+
<div className="bg-white border-t border-slate-200 px-4 py-3 shrink-0">
|
| 603 |
+
<div className="max-w-3xl mx-auto">
|
| 604 |
+
<form onSubmit={(e) => { e.preventDefault(); handleSend(input); }} className="flex gap-3">
|
| 605 |
+
<div className="flex-1 relative">
|
| 606 |
+
<Input
|
| 607 |
+
ref={inputRef}
|
| 608 |
+
type="text"
|
| 609 |
+
placeholder="Ask about GRs, Acts, compensation, flood relief..."
|
| 610 |
+
value={input}
|
| 611 |
+
onChange={(e) => setInput(e.target.value)}
|
| 612 |
+
className="h-11 pl-4 pr-10 bg-slate-50 border-slate-200 text-sm focus:bg-white focus:border-blue-500 transition-all rounded-xl"
|
| 613 |
+
disabled={isLoading}
|
| 614 |
+
/>
|
| 615 |
+
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400">
|
| 616 |
+
<FileSearch className="h-4 w-4" />
|
| 617 |
+
</div>
|
| 618 |
+
</div>
|
| 619 |
+
<Button
|
| 620 |
+
type="submit"
|
| 621 |
+
disabled={!input.trim() || isLoading}
|
| 622 |
+
className="h-11 px-5 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white text-sm font-medium rounded-xl shadow-sm"
|
| 623 |
+
>
|
| 624 |
+
<Send className="h-4 w-4 mr-1.5" />
|
| 625 |
+
Send
|
| 626 |
+
</Button>
|
| 627 |
+
</form>
|
| 628 |
+
</div>
|
| 629 |
+
</div>
|
| 630 |
+
</div>
|
| 631 |
+
|
| 632 |
+
{/* Sidebar with Popular GRs */}
|
| 633 |
+
<RecentGRsSidebar />
|
| 634 |
+
</div>
|
| 635 |
+
|
| 636 |
+
{/* Mobile */}
|
| 637 |
+
<>
|
| 638 |
+
{!isMobileOpen && (
|
| 639 |
+
<button
|
| 640 |
+
onClick={() => setIsMobileOpen(true)}
|
| 641 |
+
className="fixed bottom-20 right-4 z-50 w-14 h-14 bg-gradient-to-br from-blue-600 to-indigo-600 text-white rounded-full shadow-lg flex items-center justify-center md:hidden"
|
| 642 |
+
>
|
| 643 |
+
<MessageCircle className="h-6 w-6" />
|
| 644 |
+
</button>
|
| 645 |
+
)}
|
| 646 |
+
|
| 647 |
+
{isMobileOpen && (
|
| 648 |
+
<div className="fixed inset-0 z-50 bg-slate-50 flex flex-col md:hidden">
|
| 649 |
+
<div className="bg-white border-b border-slate-200 px-4 py-3 flex items-center justify-between">
|
| 650 |
+
<div className="flex items-center gap-2.5">
|
| 651 |
+
<div className="w-9 h-9 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center">
|
| 652 |
+
<Sparkles className="h-4 w-4 text-white" />
|
| 653 |
+
</div>
|
| 654 |
+
<div>
|
| 655 |
+
<h1 className="font-semibold text-slate-900 text-sm">GR Helper</h1>
|
| 656 |
+
<p className="text-[10px] text-slate-500">AI Assistant for Government GRs</p>
|
| 657 |
+
</div>
|
| 658 |
+
</div>
|
| 659 |
+
<button
|
| 660 |
+
onClick={() => setIsMobileOpen(false)}
|
| 661 |
+
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-slate-100"
|
| 662 |
+
>
|
| 663 |
+
<X className="h-5 w-5 text-slate-500" />
|
| 664 |
+
</button>
|
| 665 |
+
</div>
|
| 666 |
+
|
| 667 |
+
<div className="flex-1 overflow-hidden px-3 py-1 min-h-0">
|
| 668 |
+
{showWelcome && messages.length === 0 ? (
|
| 669 |
+
<div className="flex flex-col justify-center gap-4 h-full w-full max-w-sm mx-auto">
|
| 670 |
+
{/* Top */}
|
| 671 |
+
<div className="text-center">
|
| 672 |
+
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center mx-auto mb-2 shadow-md">
|
| 673 |
+
<Sparkles className="h-5 w-5 text-white" />
|
| 674 |
+
</div>
|
| 675 |
+
<h2 className="text-base font-semibold text-slate-800">GR Helper</h2>
|
| 676 |
+
<p className="text-[11px] text-slate-500 mt-1">AI Assistant for Government GRs</p>
|
| 677 |
+
</div>
|
| 678 |
+
|
| 679 |
+
{/* Quick Actions */}
|
| 680 |
+
<div className="px-2">
|
| 681 |
+
<p className="text-[10px] font-medium text-slate-500 mb-1.5 px-1">Quick Actions</p>
|
| 682 |
+
<div className="grid grid-cols-2 gap-1.5">
|
| 683 |
+
{grQuickActions.slice(0, 4).map((action, idx) => (
|
| 684 |
+
<button
|
| 685 |
+
key={idx}
|
| 686 |
+
onClick={() => handleSend(action.text)}
|
| 687 |
+
className="text-left p-2 bg-white rounded-lg border border-slate-200 active:bg-slate-50"
|
| 688 |
+
>
|
| 689 |
+
<div className="flex items-center gap-1.5">
|
| 690 |
+
<action.icon className={cn("h-3 w-3", `text-${action.color}-600`)} />
|
| 691 |
+
<span className="text-[10px] font-medium text-slate-700 line-clamp-1">{action.text}</span>
|
| 692 |
+
</div>
|
| 693 |
+
</button>
|
| 694 |
+
))}
|
| 695 |
+
</div>
|
| 696 |
+
</div>
|
| 697 |
+
|
| 698 |
+
{/* Middle - Cards */}
|
| 699 |
+
<div className="px-2">
|
| 700 |
+
<p className="text-[10px] font-medium text-slate-500 mb-1.5 px-1">Browse by Category</p>
|
| 701 |
+
<div className="grid grid-cols-2 gap-1.5">
|
| 702 |
+
{queryCategories.map((cat, idx) => (
|
| 703 |
+
<button
|
| 704 |
+
key={idx}
|
| 705 |
+
onClick={() => handleSend(cat.queries[0].text)}
|
| 706 |
+
className="text-left p-2 bg-white rounded-lg border border-slate-200 active:bg-slate-50"
|
| 707 |
+
>
|
| 708 |
+
<div className="flex items-center gap-1.5 mb-0.5">
|
| 709 |
+
<div className={cn("p-0.5 rounded", `bg-${cat.color}-100`)}>
|
| 710 |
+
<cat.icon className={cn("h-3 w-3", `text-${cat.color}-600`)} />
|
| 711 |
+
</div>
|
| 712 |
+
<span className="text-[10px] font-medium text-slate-700">{cat.title}</span>
|
| 713 |
+
</div>
|
| 714 |
+
<p className="text-[9px] text-slate-500 truncate">{cat.queries[0].text}</p>
|
| 715 |
+
</button>
|
| 716 |
+
))}
|
| 717 |
+
</div>
|
| 718 |
+
</div>
|
| 719 |
+
|
| 720 |
+
{/* Bottom - Quick Links */}
|
| 721 |
+
<div className="flex flex-wrap justify-center gap-1.5 px-2">
|
| 722 |
+
{['Konkan rainfall', 'Vidarbha GR', 'Water rights', 'Compensation'].map((s, i) => (
|
| 723 |
+
<button
|
| 724 |
+
key={i}
|
| 725 |
+
onClick={() => handleSend(s)}
|
| 726 |
+
className="text-[10px] px-2.5 py-1 bg-slate-100 text-slate-600 rounded-full"
|
| 727 |
+
>
|
| 728 |
+
{s}
|
| 729 |
+
</button>
|
| 730 |
+
))}
|
| 731 |
+
</div>
|
| 732 |
+
</div>
|
| 733 |
+
) : (
|
| 734 |
+
<div className="w-full h-full overflow-y-auto space-y-3 min-h-0">
|
| 735 |
+
{messages.map((msg, i) => (
|
| 736 |
+
<React.Fragment key={msg.id || String(i)}>
|
| 737 |
+
<MessageBubble msg={msg} />
|
| 738 |
+
</React.Fragment>
|
| 739 |
+
))}
|
| 740 |
+
{isLoading && (
|
| 741 |
+
<div className="flex items-center gap-2 text-slate-500">
|
| 742 |
+
<div className="w-7 h-7 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-full flex items-center justify-center">
|
| 743 |
+
<Bot className="h-3.5 w-3.5 text-white" />
|
| 744 |
+
</div>
|
| 745 |
+
<div className="flex items-center gap-2 bg-white border border-slate-200 px-3 py-2 rounded-xl rounded-tl-sm">
|
| 746 |
+
<Loader2 className="h-3 w-3 animate-spin text-blue-500" />
|
| 747 |
+
<span className="text-xs">Searching...</span>
|
| 748 |
+
</div>
|
| 749 |
+
</div>
|
| 750 |
+
)}
|
| 751 |
+
<div ref={messagesEndRef} />
|
| 752 |
+
</div>
|
| 753 |
+
)}
|
| 754 |
+
</div>
|
| 755 |
+
|
| 756 |
+
<div className="bg-white border-t border-slate-200 px-4 py-2 shrink-0">
|
| 757 |
+
<form onSubmit={(e) => { e.preventDefault(); handleSend(input); }} className="flex gap-2">
|
| 758 |
+
<Input
|
| 759 |
+
type="text"
|
| 760 |
+
placeholder="Ask about GRs, Acts, compensation..."
|
| 761 |
+
value={input}
|
| 762 |
+
onChange={(e) => setInput(e.target.value)}
|
| 763 |
+
className="flex-1 h-10 bg-slate-50 text-sm rounded-xl"
|
| 764 |
+
disabled={isLoading}
|
| 765 |
+
/>
|
| 766 |
+
<Button
|
| 767 |
+
type="submit"
|
| 768 |
+
size="icon"
|
| 769 |
+
disabled={!input.trim() || isLoading}
|
| 770 |
+
className="h-10 w-10 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-xl"
|
| 771 |
+
>
|
| 772 |
+
<Send className="h-4 w-4" />
|
| 773 |
+
</Button>
|
| 774 |
+
</form>
|
| 775 |
+
</div>
|
| 776 |
+
</div>
|
| 777 |
+
)}
|
| 778 |
+
</>
|
| 779 |
+
</>
|
| 780 |
+
);
|
| 781 |
+
}
|
src/pages/Dashboard.tsx
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/src/components/ui/Card';
|
| 4 |
+
import { Badge } from '@/src/components/ui/Badge';
|
| 5 |
+
import { Button } from '@/src/components/ui/Button';
|
| 6 |
+
import {
|
| 7 |
+
AlertTriangle,
|
| 8 |
+
Droplets,
|
| 9 |
+
FileText,
|
| 10 |
+
ArrowRight,
|
| 11 |
+
CloudRain,
|
| 12 |
+
Bell,
|
| 13 |
+
Database,
|
| 14 |
+
MapPin,
|
| 15 |
+
TrendingUp,
|
| 16 |
+
TrendingDown,
|
| 17 |
+
Waves,
|
| 18 |
+
Calendar,
|
| 19 |
+
Search,
|
| 20 |
+
MessageSquare,
|
| 21 |
+
HelpCircle,
|
| 22 |
+
FileSearch,
|
| 23 |
+
Sparkles
|
| 24 |
+
} from 'lucide-react';
|
| 25 |
+
import { cn } from '@/src/utils/cn';
|
| 26 |
+
import {
|
| 27 |
+
AreaChart,
|
| 28 |
+
Area,
|
| 29 |
+
XAxis,
|
| 30 |
+
YAxis,
|
| 31 |
+
CartesianGrid,
|
| 32 |
+
Tooltip,
|
| 33 |
+
ResponsiveContainer,
|
| 34 |
+
BarChart,
|
| 35 |
+
Bar,
|
| 36 |
+
Legend,
|
| 37 |
+
PieChart,
|
| 38 |
+
Pie,
|
| 39 |
+
Cell
|
| 40 |
+
} from 'recharts';
|
| 41 |
+
|
| 42 |
+
// Extended water level data with more WRD-specific metrics
|
| 43 |
+
const waterLevelData = [
|
| 44 |
+
{ name: 'Jan', level: 40, avg: 35, rainfall: 0 },
|
| 45 |
+
{ name: 'Feb', level: 35, avg: 30, rainfall: 2 },
|
| 46 |
+
{ name: 'Mar', level: 30, avg: 25, rainfall: 5 },
|
| 47 |
+
{ name: 'Apr', level: 25, avg: 20, rainfall: 12 },
|
| 48 |
+
{ name: 'May', level: 20, avg: 15, rainfall: 28 },
|
| 49 |
+
{ name: 'Jun', level: 45, avg: 40, rainfall: 85 },
|
| 50 |
+
{ name: 'Jul', level: 75, avg: 65, rainfall: 245 },
|
| 51 |
+
{ name: 'Aug', level: 90, avg: 85, rainfall: 298 },
|
| 52 |
+
{ name: 'Sep', level: 85, avg: 80, rainfall: 180 },
|
| 53 |
+
{ name: 'Oct', level: 70, avg: 65, rainfall: 65 },
|
| 54 |
+
{ name: 'Nov', level: 60, avg: 55, rainfall: 18 },
|
| 55 |
+
{ name: 'Dec', level: 50, avg: 45, rainfall: 3 },
|
| 56 |
+
];
|
| 57 |
+
|
| 58 |
+
// Dam storage by region
|
| 59 |
+
const regionData = [
|
| 60 |
+
{ name: 'Konkan', full: 85, capacity: '45 TMC' },
|
| 61 |
+
{ name: 'Western Maharashtra', full: 72, capacity: '120 TMC' },
|
| 62 |
+
{ name: 'Marathwada', full: 45, capacity: '89 TMC' },
|
| 63 |
+
{ name: 'Vidarbha', full: 58, capacity: '67 TMC' },
|
| 64 |
+
{ name: 'North Maharashtra', full: 63, capacity: '34 TMC' },
|
| 65 |
+
];
|
| 66 |
+
|
| 67 |
+
// Recent alerts with WRD-specific details
|
| 68 |
+
const alerts = [
|
| 69 |
+
{ id: 1, title: 'Critical Water Level', location: 'Koyna Dam, Satara', severity: 'Critical', time: '30 min ago', type: 'dam', description: '92% capacity - Emergency discharge initiated' },
|
| 70 |
+
{ id: 2, title: 'Flash Flood Warning', location: 'Panchaganga Basin, Kolhapur', severity: 'Critical', time: '1 hour ago', type: 'flood', description: 'Rising levels - Evacuation alert issued' },
|
| 71 |
+
{ id: 3, title: 'Heavy Rainfall Alert', location: 'Ratnagiri District', severity: 'High', time: '3 hours ago', type: 'rain', description: '250mm in 24h - Landslide risk' },
|
| 72 |
+
{ id: 4, title: 'New GR Published', location: 'WRD Circular No. 2024/08', severity: 'Info', time: '5 hours ago', type: 'gr', description: 'Updated flood response protocols' },
|
| 73 |
+
{ id: 5, title: 'Gate Maintenance', location: 'Jayakwadi Dam', severity: 'Medium', time: '1 day ago', type: 'maintenance', description: 'Scheduled maintenance - Reduced outflow' },
|
| 74 |
+
];
|
| 75 |
+
|
| 76 |
+
// WRD KPI data
|
| 77 |
+
const kpiData = [
|
| 78 |
+
{ title: 'Critical Alerts', value: '7', trend: '+2', trendUp: false, icon: AlertTriangle, color: 'red', subtitle: 'Immediate action required' },
|
| 79 |
+
{ title: 'Active Releases', value: '12', trend: '+3', trendUp: true, icon: Waves, color: 'blue', subtitle: 'Dams with open gates' },
|
| 80 |
+
{ title: 'Avg Storage', value: '68%', trend: '+5%', trendUp: true, icon: Droplets, color: 'cyan', subtitle: 'Above normal for season' },
|
| 81 |
+
{ title: '24h Rainfall', value: '145mm', trend: '+85mm', trendUp: false, icon: CloudRain, color: 'indigo', subtitle: 'Konkan region' },
|
| 82 |
+
{ title: 'GRs This Month', value: '24', trend: '+8', trendUp: true, icon: FileText, color: 'emerald', subtitle: 'New policies/circulars' },
|
| 83 |
+
{ title: 'Flood Zones', value: '3', trend: '+1', trendUp: false, icon: MapPin, color: 'orange', subtitle: 'Active predictions' },
|
| 84 |
+
];
|
| 85 |
+
|
| 86 |
+
const COLORS = ['#3b82f6', '#06b6d4', '#8b5cf6', '#f59e0b', '#ef4444'];
|
| 87 |
+
|
| 88 |
+
export function Dashboard() {
|
| 89 |
+
const navigate = useNavigate();
|
| 90 |
+
|
| 91 |
+
const quickLinks = [
|
| 92 |
+
{ name: 'Find a GR', description: 'Search Government Resolutions', icon: FileSearch, path: '/gr-finder', color: 'blue' },
|
| 93 |
+
{ name: 'Ask GR Helper', description: 'AI assistant for GR queries', icon: Sparkles, path: '/ai-assistant', color: 'indigo' },
|
| 94 |
+
{ name: 'View FAQ', description: 'Common WRD questions', icon: HelpCircle, path: '/faq', color: 'emerald' },
|
| 95 |
+
{ name: 'Flood Map', description: 'Real-time flood prediction', icon: MapPin, path: '/map', color: 'cyan' },
|
| 96 |
+
];
|
| 97 |
+
|
| 98 |
+
return (
|
| 99 |
+
<div className="space-y-4 lg:space-y-6 p-4 lg:p-6">
|
| 100 |
+
{/* Header */}
|
| 101 |
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
| 102 |
+
<div>
|
| 103 |
+
<h1 className="text-xl lg:text-2xl font-bold tracking-tight text-slate-900">JalDrishti Dashboard</h1>
|
| 104 |
+
<p className="text-sm text-slate-500">Maharashtra WRD - Real-time Water Resources Monitoring</p>
|
| 105 |
+
</div>
|
| 106 |
+
<div className="flex items-center gap-2 text-xs lg:text-sm text-slate-500">
|
| 107 |
+
<Calendar className="h-4 w-4" />
|
| 108 |
+
<span>Last updated: {new Date().toLocaleString('en-IN', { dateStyle: 'medium', timeStyle: 'short' })}</span>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
{/* Quick Links */}
|
| 113 |
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
| 114 |
+
{quickLinks.map((link, idx) => {
|
| 115 |
+
const Icon = link.icon;
|
| 116 |
+
return (
|
| 117 |
+
<Card
|
| 118 |
+
key={idx}
|
| 119 |
+
className="cursor-pointer hover:shadow-md transition-all border-slate-200 group"
|
| 120 |
+
onClick={() => navigate(link.path)}
|
| 121 |
+
>
|
| 122 |
+
<CardContent className="p-4">
|
| 123 |
+
<div className="flex items-start gap-3">
|
| 124 |
+
<div className={cn(
|
| 125 |
+
"rounded-lg p-2 shrink-0",
|
| 126 |
+
link.color === 'blue' ? "bg-blue-100 text-blue-600" :
|
| 127 |
+
link.color === 'indigo' ? "bg-indigo-100 text-indigo-600" :
|
| 128 |
+
link.color === 'emerald' ? "bg-emerald-100 text-emerald-600" :
|
| 129 |
+
"bg-cyan-100 text-cyan-600"
|
| 130 |
+
)}>
|
| 131 |
+
<Icon className="h-5 w-5" />
|
| 132 |
+
</div>
|
| 133 |
+
<div className="flex-1 min-w-0">
|
| 134 |
+
<p className="text-sm font-semibold text-slate-900 group-hover:text-blue-600 transition-colors">{link.name}</p>
|
| 135 |
+
<p className="text-xs text-slate-500 mt-0.5">{link.description}</p>
|
| 136 |
+
</div>
|
| 137 |
+
<ArrowRight className="h-4 w-4 text-slate-300 group-hover:text-blue-500 transition-colors shrink-0" />
|
| 138 |
+
</div>
|
| 139 |
+
</CardContent>
|
| 140 |
+
</Card>
|
| 141 |
+
);
|
| 142 |
+
})}
|
| 143 |
+
</div>
|
| 144 |
+
|
| 145 |
+
{/* KPI Cards - Horizontal scroll on mobile */}
|
| 146 |
+
<div className="overflow-x-auto -mx-4 px-4 lg:mx-0 lg:px-0 pb-2 lg:pb-0">
|
| 147 |
+
<div className="flex gap-3 lg:grid lg:grid-cols-6 min-w-max lg:min-w-0">
|
| 148 |
+
{kpiData.map((kpi, idx) => {
|
| 149 |
+
const Icon = kpi.icon;
|
| 150 |
+
return (
|
| 151 |
+
<Card key={idx} className="w-[160px] lg:w-auto shrink-0">
|
| 152 |
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2 p-3">
|
| 153 |
+
<CardTitle className="text-xs font-medium">{kpi.title}</CardTitle>
|
| 154 |
+
<Icon className={cn("h-4 w-4", `text-${kpi.color}-500`)} />
|
| 155 |
+
</CardHeader>
|
| 156 |
+
<CardContent className="p-3 pt-0">
|
| 157 |
+
<div className="text-xl lg:text-2xl font-bold">{kpi.value}</div>
|
| 158 |
+
<div className="flex items-center gap-1 mt-1">
|
| 159 |
+
{kpi.trendUp ? (
|
| 160 |
+
<TrendingUp className={cn("h-3 w-3", kpi.color === 'red' ? "text-red-500" : "text-green-500")} />
|
| 161 |
+
) : (
|
| 162 |
+
<TrendingDown className={cn("h-3 w-3", kpi.color === 'red' ? "text-red-500" : "text-orange-500")} />
|
| 163 |
+
)}
|
| 164 |
+
<span className={cn("text-xs", kpi.trendUp ? "text-green-600" : "text-red-600")}>
|
| 165 |
+
{kpi.trend}
|
| 166 |
+
</span>
|
| 167 |
+
</div>
|
| 168 |
+
<p className="text-[10px] text-slate-400 mt-1 hidden lg:block">{kpi.subtitle}</p>
|
| 169 |
+
</CardContent>
|
| 170 |
+
</Card>
|
| 171 |
+
);
|
| 172 |
+
})}
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
{/* Main Grid */}
|
| 177 |
+
<div className="grid gap-4 lg:grid-cols-12">
|
| 178 |
+
{/* Water Level Chart - Full width on mobile */}
|
| 179 |
+
<Card className="lg:col-span-7">
|
| 180 |
+
<CardHeader className="pb-2">
|
| 181 |
+
<CardTitle className="text-base lg:text-lg">State Water Storage Trends</CardTitle>
|
| 182 |
+
<CardDescription className="text-xs lg:text-sm">Current year vs Historical Average with Rainfall overlay</CardDescription>
|
| 183 |
+
</CardHeader>
|
| 184 |
+
<CardContent className="pl-0 lg:pl-2">
|
| 185 |
+
<div className="h-[250px] lg:h-[300px] w-full">
|
| 186 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 187 |
+
<AreaChart data={waterLevelData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
| 188 |
+
<defs>
|
| 189 |
+
<linearGradient id="colorLevel" x1="0" y1="0" x2="0" y2="1">
|
| 190 |
+
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3}/>
|
| 191 |
+
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0}/>
|
| 192 |
+
</linearGradient>
|
| 193 |
+
<linearGradient id="colorAvg" x1="0" y1="0" x2="0" y2="1">
|
| 194 |
+
<stop offset="5%" stopColor="#94a3b8" stopOpacity={0.3}/>
|
| 195 |
+
<stop offset="95%" stopColor="#94a3b8" stopOpacity={0}/>
|
| 196 |
+
</linearGradient>
|
| 197 |
+
</defs>
|
| 198 |
+
<XAxis dataKey="name" stroke="#888888" fontSize={10} tickLine={false} axisLine={false} />
|
| 199 |
+
<YAxis stroke="#888888" fontSize={10} tickLine={false} axisLine={false} tickFormatter={(value) => `${value}%`} />
|
| 200 |
+
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
|
| 201 |
+
<Tooltip
|
| 202 |
+
contentStyle={{ backgroundColor: '#fff', borderRadius: '8px', border: '1px solid #e2e8f0', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)', fontSize: '12px' }}
|
| 203 |
+
/>
|
| 204 |
+
<Legend wrapperStyle={{ fontSize: '12px' }} />
|
| 205 |
+
<Area type="monotone" dataKey="level" name="Current Storage %" stroke="#3b82f6" strokeWidth={2} fillOpacity={1} fill="url(#colorLevel)" />
|
| 206 |
+
<Area type="monotone" dataKey="avg" name="Historical Avg" stroke="#94a3b8" strokeWidth={2} fillOpacity={1} fill="url(#colorAvg)" />
|
| 207 |
+
</AreaChart>
|
| 208 |
+
</ResponsiveContainer>
|
| 209 |
+
</div>
|
| 210 |
+
</CardContent>
|
| 211 |
+
</Card>
|
| 212 |
+
|
| 213 |
+
{/* Regional Storage + Alerts */}
|
| 214 |
+
<div className="lg:col-span-5 space-y-4">
|
| 215 |
+
{/* Regional Storage Chart */}
|
| 216 |
+
<Card>
|
| 217 |
+
<CardHeader className="pb-2">
|
| 218 |
+
<CardTitle className="text-sm">Storage by Region</CardTitle>
|
| 219 |
+
</CardHeader>
|
| 220 |
+
<CardContent>
|
| 221 |
+
<div className="h-[150px] lg:h-[180px]">
|
| 222 |
+
<ResponsiveContainer width="100%" height="100%">
|
| 223 |
+
<BarChart data={regionData} layout="vertical" margin={{ top: 5, right: 30, left: 40, bottom: 5 }}>
|
| 224 |
+
<XAxis type="number" stroke="#888888" fontSize={10} tickLine={false} axisLine={false} tickFormatter={(v) => `${v}%`} />
|
| 225 |
+
<YAxis dataKey="name" type="category" stroke="#888888" fontSize={10} tickLine={false} axisLine={false} width={100} />
|
| 226 |
+
<Tooltip
|
| 227 |
+
contentStyle={{ fontSize: '12px', borderRadius: '8px' }}
|
| 228 |
+
formatter={(value: number) => [`${value}% Full`, 'Storage']}
|
| 229 |
+
/>
|
| 230 |
+
<Bar dataKey="full" fill="#3b82f6" radius={[0, 4, 4, 0]} />
|
| 231 |
+
</BarChart>
|
| 232 |
+
</ResponsiveContainer>
|
| 233 |
+
</div>
|
| 234 |
+
</CardContent>
|
| 235 |
+
</Card>
|
| 236 |
+
|
| 237 |
+
{/* Quick Stats */}
|
| 238 |
+
<Card>
|
| 239 |
+
<CardContent className="p-4">
|
| 240 |
+
<div className="grid grid-cols-3 gap-4 text-center">
|
| 241 |
+
<div>
|
| 242 |
+
<div className="text-2xl font-bold text-blue-600">287</div>
|
| 243 |
+
<div className="text-xs text-slate-500">Total Dams</div>
|
| 244 |
+
</div>
|
| 245 |
+
<div>
|
| 246 |
+
<div className="text-2xl font-bold text-red-600">18</div>
|
| 247 |
+
<div className="text-xs text-slate-500">Critical</div>
|
| 248 |
+
</div>
|
| 249 |
+
<div>
|
| 250 |
+
<div className="text-2xl font-bold text-emerald-600">142</div>
|
| 251 |
+
<div className="text-xs text-slate-500">Indexed GRs</div>
|
| 252 |
+
</div>
|
| 253 |
+
</div>
|
| 254 |
+
</CardContent>
|
| 255 |
+
</Card>
|
| 256 |
+
</div>
|
| 257 |
+
</div>
|
| 258 |
+
|
| 259 |
+
{/* Recent Alerts Section */}
|
| 260 |
+
<Card>
|
| 261 |
+
<CardHeader className="pb-3">
|
| 262 |
+
<div className="flex items-center justify-between">
|
| 263 |
+
<div>
|
| 264 |
+
<CardTitle className="text-base lg:text-lg flex items-center gap-2">
|
| 265 |
+
<Bell className="h-5 w-5 text-slate-500" />
|
| 266 |
+
Recent Alerts & Notifications
|
| 267 |
+
</CardTitle>
|
| 268 |
+
<CardDescription className="text-xs lg:text-sm">System alerts, GR updates, and operational notifications</CardDescription>
|
| 269 |
+
</div>
|
| 270 |
+
<Button variant="outline" size="sm" className="hidden sm:flex text-xs h-8">
|
| 271 |
+
View All <ArrowRight className="ml-2 h-3 w-3" />
|
| 272 |
+
</Button>
|
| 273 |
+
</div>
|
| 274 |
+
</CardHeader>
|
| 275 |
+
<CardContent>
|
| 276 |
+
<div className="space-y-3">
|
| 277 |
+
{alerts.map((alert) => (
|
| 278 |
+
<div key={alert.id} className="flex items-start gap-3 p-3 rounded-lg bg-slate-50/50 border border-slate-100">
|
| 279 |
+
<div className={cn(
|
| 280 |
+
"mt-0.5 rounded-full p-1.5 shrink-0",
|
| 281 |
+
alert.severity === 'Critical' ? "bg-red-100 text-red-600" :
|
| 282 |
+
alert.severity === 'High' ? "bg-orange-100 text-orange-600" :
|
| 283 |
+
alert.severity === 'Medium' ? "bg-yellow-100 text-yellow-600" :
|
| 284 |
+
"bg-blue-100 text-blue-600"
|
| 285 |
+
)}>
|
| 286 |
+
{alert.type === 'gr' ? <FileText className="h-4 w-4" /> :
|
| 287 |
+
alert.type === 'dam' ? <Database className="h-4 w-4" /> :
|
| 288 |
+
alert.type === 'flood' ? <Waves className="h-4 w-4" /> :
|
| 289 |
+
<AlertTriangle className="h-4 w-4" />}
|
| 290 |
+
</div>
|
| 291 |
+
<div className="flex-1 min-w-0">
|
| 292 |
+
<div className="flex items-center gap-2 flex-wrap">
|
| 293 |
+
<p className="text-sm font-medium">{alert.title}</p>
|
| 294 |
+
<Badge variant={
|
| 295 |
+
alert.severity === 'Critical' ? 'destructive' :
|
| 296 |
+
alert.severity === 'High' ? 'outline' :
|
| 297 |
+
alert.severity === 'Medium' ? 'secondary' :
|
| 298 |
+
'outline'
|
| 299 |
+
} className={cn(
|
| 300 |
+
"text-[10px] h-5",
|
| 301 |
+
alert.severity === 'High' ? 'border-orange-500 text-orange-700' : ''
|
| 302 |
+
)}>
|
| 303 |
+
{alert.severity}
|
| 304 |
+
</Badge>
|
| 305 |
+
</div>
|
| 306 |
+
<p className="text-xs text-slate-500 mt-0.5">{alert.location}</p>
|
| 307 |
+
<p className="text-xs text-slate-600 mt-1 hidden sm:block">{alert.description}</p>
|
| 308 |
+
</div>
|
| 309 |
+
<div className="text-xs text-slate-400 whitespace-nowrap shrink-0">{alert.time}</div>
|
| 310 |
+
</div>
|
| 311 |
+
))}
|
| 312 |
+
</div>
|
| 313 |
+
<Button variant="outline" className="w-full mt-4 sm:hidden text-xs h-10">
|
| 314 |
+
View All Alerts <ArrowRight className="ml-2 h-4 w-4" />
|
| 315 |
+
</Button>
|
| 316 |
+
</CardContent>
|
| 317 |
+
</Card>
|
| 318 |
+
</div>
|
| 319 |
+
);
|
| 320 |
+
}
|
src/pages/Faq.tsx
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useMemo } from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { Card, CardContent, CardHeader, CardTitle } from '@/src/components/ui/Card';
|
| 4 |
+
import { Input } from '@/src/components/ui/Input';
|
| 5 |
+
import { Button } from '@/src/components/ui/Button';
|
| 6 |
+
import { Badge } from '@/src/components/ui/Badge';
|
| 7 |
+
import {
|
| 8 |
+
Search, MessageSquare, ChevronDown, ChevronUp,
|
| 9 |
+
Waves, Database, Droplets, IndianRupee, FileText,
|
| 10 |
+
HelpCircle, ArrowRight, BookOpen
|
| 11 |
+
} from 'lucide-react';
|
| 12 |
+
import { cn } from '@/src/utils/cn';
|
| 13 |
+
import { faqData, faqCategories, getFAQsByCategory, FAQItem } from '@/src/data/mockFAQs';
|
| 14 |
+
|
| 15 |
+
const categoryIcons: Record<string, React.ElementType> = {
|
| 16 |
+
'Flood Management': Waves,
|
| 17 |
+
'Dam Operations': Database,
|
| 18 |
+
'Irrigation & Water Rights': Droplets,
|
| 19 |
+
'Compensation & Relief': IndianRupee,
|
| 20 |
+
'GRs & Acts': FileText,
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
export function Faq() {
|
| 24 |
+
const navigate = useNavigate();
|
| 25 |
+
const [searchQuery, setSearchQuery] = useState('');
|
| 26 |
+
const [selectedCategory, setSelectedCategory] = useState<string>('All');
|
| 27 |
+
const [expandedId, setExpandedId] = useState<string | null>(null);
|
| 28 |
+
|
| 29 |
+
const filteredFAQs = useMemo(() => {
|
| 30 |
+
let faqs = selectedCategory === 'All'
|
| 31 |
+
? faqData
|
| 32 |
+
: getFAQsByCategory(selectedCategory);
|
| 33 |
+
|
| 34 |
+
if (searchQuery) {
|
| 35 |
+
const q = searchQuery.toLowerCase();
|
| 36 |
+
faqs = faqs.filter(faq =>
|
| 37 |
+
faq.question.toLowerCase().includes(q) ||
|
| 38 |
+
faq.answer.toLowerCase().includes(q) ||
|
| 39 |
+
faq.keywords.some(k => k.toLowerCase().includes(q))
|
| 40 |
+
);
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
return faqs;
|
| 44 |
+
}, [searchQuery, selectedCategory]);
|
| 45 |
+
|
| 46 |
+
const handleAskAI = (question: string) => {
|
| 47 |
+
navigate(`/ai-assistant?q=${encodeURIComponent(question)}`);
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
const toggleExpand = (id: string) => {
|
| 51 |
+
setExpandedId(expandedId === id ? null : id);
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
return (
|
| 55 |
+
<div className="space-y-6 max-w-4xl mx-auto">
|
| 56 |
+
{/* Header */}
|
| 57 |
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
| 58 |
+
<div>
|
| 59 |
+
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Frequently Asked Questions</h1>
|
| 60 |
+
<p className="text-slate-500">Find answers to common WRD queries</p>
|
| 61 |
+
</div>
|
| 62 |
+
<Button onClick={() => navigate('/ai-assistant')} className="bg-indigo-600 hover:bg-indigo-700 text-white">
|
| 63 |
+
<MessageSquare className="mr-2 h-4 w-4" />
|
| 64 |
+
Ask GR Helper
|
| 65 |
+
</Button>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
{/* Search */}
|
| 69 |
+
<Card className="border-none shadow-md bg-white/50 backdrop-blur-sm">
|
| 70 |
+
<CardContent className="p-4 sm:p-6">
|
| 71 |
+
<div className="relative">
|
| 72 |
+
<Search className="absolute left-3 top-3 h-5 w-5 text-slate-400" />
|
| 73 |
+
<Input
|
| 74 |
+
type="text"
|
| 75 |
+
placeholder="Search questions..."
|
| 76 |
+
className="pl-10 h-12 text-base bg-white"
|
| 77 |
+
value={searchQuery}
|
| 78 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 79 |
+
/>
|
| 80 |
+
</div>
|
| 81 |
+
</CardContent>
|
| 82 |
+
</Card>
|
| 83 |
+
|
| 84 |
+
{/* Category Tabs */}
|
| 85 |
+
<div className="flex flex-wrap gap-2">
|
| 86 |
+
<Badge
|
| 87 |
+
variant={selectedCategory === 'All' ? 'default' : 'secondary'}
|
| 88 |
+
className="cursor-pointer px-4 py-2 text-sm"
|
| 89 |
+
onClick={() => setSelectedCategory('All')}
|
| 90 |
+
>
|
| 91 |
+
All ({faqData.length})
|
| 92 |
+
</Badge>
|
| 93 |
+
{faqCategories.map(cat => {
|
| 94 |
+
const count = getFAQsByCategory(cat.name).length;
|
| 95 |
+
const Icon = categoryIcons[cat.name] || HelpCircle;
|
| 96 |
+
return (
|
| 97 |
+
<Badge
|
| 98 |
+
key={cat.id}
|
| 99 |
+
variant={selectedCategory === cat.name ? 'default' : 'secondary'}
|
| 100 |
+
className="cursor-pointer px-4 py-2 text-sm flex items-center gap-1"
|
| 101 |
+
onClick={() => setSelectedCategory(cat.name)}
|
| 102 |
+
>
|
| 103 |
+
<Icon className="h-3 w-3" />
|
| 104 |
+
{cat.name} ({count})
|
| 105 |
+
</Badge>
|
| 106 |
+
);
|
| 107 |
+
})}
|
| 108 |
+
</div>
|
| 109 |
+
|
| 110 |
+
{/* FAQ List */}
|
| 111 |
+
<div className="space-y-3">
|
| 112 |
+
{filteredFAQs.map((faq) => {
|
| 113 |
+
const isExpanded = expandedId === faq.id;
|
| 114 |
+
const CategoryIcon = categoryIcons[faq.category] || HelpCircle;
|
| 115 |
+
|
| 116 |
+
return (
|
| 117 |
+
<Card
|
| 118 |
+
key={faq.id}
|
| 119 |
+
className={cn(
|
| 120 |
+
"overflow-hidden transition-all border-slate-200",
|
| 121 |
+
isExpanded ? "shadow-md" : "hover:shadow-sm"
|
| 122 |
+
)}
|
| 123 |
+
>
|
| 124 |
+
<div
|
| 125 |
+
className="p-4 cursor-pointer"
|
| 126 |
+
onClick={() => toggleExpand(faq.id)}
|
| 127 |
+
>
|
| 128 |
+
<div className="flex items-start gap-3">
|
| 129 |
+
<div className={cn(
|
| 130 |
+
"rounded-full p-2 shrink-0",
|
| 131 |
+
faq.category === 'Flood Management' ? "bg-blue-100 text-blue-600" :
|
| 132 |
+
faq.category === 'Dam Operations' ? "bg-red-100 text-red-600" :
|
| 133 |
+
faq.category === 'Irrigation & Water Rights' ? "bg-cyan-100 text-cyan-600" :
|
| 134 |
+
faq.category === 'Compensation & Relief' ? "bg-emerald-100 text-emerald-600" :
|
| 135 |
+
"bg-purple-100 text-purple-600"
|
| 136 |
+
)}>
|
| 137 |
+
<CategoryIcon className="h-4 w-4" />
|
| 138 |
+
</div>
|
| 139 |
+
<div className="flex-1 min-w-0">
|
| 140 |
+
<div className="flex items-center justify-between gap-2">
|
| 141 |
+
<h3 className="text-sm font-semibold text-slate-900 pr-4">
|
| 142 |
+
{faq.question}
|
| 143 |
+
</h3>
|
| 144 |
+
{isExpanded ? (
|
| 145 |
+
<ChevronUp className="h-5 w-5 text-slate-400 shrink-0" />
|
| 146 |
+
) : (
|
| 147 |
+
<ChevronDown className="h-5 w-5 text-slate-400 shrink-0" />
|
| 148 |
+
)}
|
| 149 |
+
</div>
|
| 150 |
+
{!isExpanded && (
|
| 151 |
+
<p className="text-xs text-slate-500 mt-1 line-clamp-1">
|
| 152 |
+
Click to expand answer
|
| 153 |
+
</p>
|
| 154 |
+
)}
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
</div>
|
| 158 |
+
|
| 159 |
+
{isExpanded && (
|
| 160 |
+
<CardContent className="pt-0 pb-4 px-4">
|
| 161 |
+
<div className="pl-11">
|
| 162 |
+
{/* Answer */}
|
| 163 |
+
<div className="text-sm text-slate-700 whitespace-pre-line leading-relaxed">
|
| 164 |
+
{faq.answer}
|
| 165 |
+
</div>
|
| 166 |
+
|
| 167 |
+
{/* Related GRs */}
|
| 168 |
+
{faq.relatedGRs && faq.relatedGRs.length > 0 && (
|
| 169 |
+
<div className="mt-4 pt-4 border-t border-slate-100">
|
| 170 |
+
<p className="text-xs font-medium text-slate-500 mb-2">Related GRs:</p>
|
| 171 |
+
<div className="flex flex-wrap gap-2">
|
| 172 |
+
{faq.relatedGRs.map(grId => (
|
| 173 |
+
<Badge
|
| 174 |
+
key={grId}
|
| 175 |
+
variant="outline"
|
| 176 |
+
className="text-xs cursor-pointer hover:bg-slate-100"
|
| 177 |
+
onClick={() => navigate(`/gr-finder?search=${grId}`)}
|
| 178 |
+
>
|
| 179 |
+
{grId}
|
| 180 |
+
</Badge>
|
| 181 |
+
))}
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
)}
|
| 185 |
+
|
| 186 |
+
{/* Keywords */}
|
| 187 |
+
<div className="mt-3 flex flex-wrap gap-1">
|
| 188 |
+
{faq.keywords.map(keyword => (
|
| 189 |
+
<span
|
| 190 |
+
key={keyword}
|
| 191 |
+
className="text-[10px] bg-slate-100 text-slate-600 px-2 py-0.5 rounded"
|
| 192 |
+
>
|
| 193 |
+
{keyword}
|
| 194 |
+
</span>
|
| 195 |
+
))}
|
| 196 |
+
</div>
|
| 197 |
+
|
| 198 |
+
{/* Ask AI Button */}
|
| 199 |
+
<div className="mt-4 pt-4 border-t border-slate-100">
|
| 200 |
+
<Button
|
| 201 |
+
variant="secondary"
|
| 202 |
+
size="sm"
|
| 203 |
+
className="bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border-indigo-100"
|
| 204 |
+
onClick={() => handleAskAI(faq.question)}
|
| 205 |
+
>
|
| 206 |
+
<MessageSquare className="mr-2 h-4 w-4" />
|
| 207 |
+
Ask AI about this
|
| 208 |
+
</Button>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
</CardContent>
|
| 212 |
+
)}
|
| 213 |
+
</Card>
|
| 214 |
+
);
|
| 215 |
+
})}
|
| 216 |
+
|
| 217 |
+
{filteredFAQs.length === 0 && (
|
| 218 |
+
<div className="text-center py-12">
|
| 219 |
+
<HelpCircle className="mx-auto h-12 w-12 text-slate-300 mb-4" />
|
| 220 |
+
<h3 className="text-lg font-medium text-slate-900">No questions found</h3>
|
| 221 |
+
<p className="text-slate-500">Try adjusting your search or category filter.</p>
|
| 222 |
+
<Button
|
| 223 |
+
variant="outline"
|
| 224 |
+
className="mt-4"
|
| 225 |
+
onClick={() => {
|
| 226 |
+
setSearchQuery('');
|
| 227 |
+
setSelectedCategory('All');
|
| 228 |
+
}}
|
| 229 |
+
>
|
| 230 |
+
Clear filters
|
| 231 |
+
</Button>
|
| 232 |
+
</div>
|
| 233 |
+
)}
|
| 234 |
+
</div>
|
| 235 |
+
|
| 236 |
+
{/* Still have questions? */}
|
| 237 |
+
<Card className="bg-gradient-to-r from-indigo-50 to-blue-50 border-indigo-100">
|
| 238 |
+
<CardContent className="p-6">
|
| 239 |
+
<div className="flex items-start gap-4">
|
| 240 |
+
<div className="rounded-full bg-indigo-100 p-3">
|
| 241 |
+
<BookOpen className="h-6 w-6 text-indigo-600" />
|
| 242 |
+
</div>
|
| 243 |
+
<div className="flex-1">
|
| 244 |
+
<h3 className="text-lg font-semibold text-slate-900">Still have questions?</h3>
|
| 245 |
+
<p className="text-sm text-slate-600 mt-1">
|
| 246 |
+
Can't find what you're looking for? Our GR Helper AI assistant can help you find answers
|
| 247 |
+
related to Government Resolutions, Acts, and WRD policies.
|
| 248 |
+
</p>
|
| 249 |
+
<Button
|
| 250 |
+
className="mt-4 bg-indigo-600 hover:bg-indigo-700"
|
| 251 |
+
onClick={() => navigate('/ai-assistant')}
|
| 252 |
+
>
|
| 253 |
+
Open GR Helper <ArrowRight className="ml-2 h-4 w-4" />
|
| 254 |
+
</Button>
|
| 255 |
+
</div>
|
| 256 |
+
</div>
|
| 257 |
+
</CardContent>
|
| 258 |
+
</Card>
|
| 259 |
+
|
| 260 |
+
{/* Quick Links */}
|
| 261 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
| 262 |
+
<Card className="border-slate-200 hover:shadow-md transition-shadow cursor-pointer" onClick={() => navigate('/gr-finder')}>
|
| 263 |
+
<CardContent className="p-4 flex items-center gap-3">
|
| 264 |
+
<div className="rounded-full bg-blue-100 p-2">
|
| 265 |
+
<FileText className="h-5 w-5 text-blue-600" />
|
| 266 |
+
</div>
|
| 267 |
+
<div>
|
| 268 |
+
<p className="text-sm font-medium text-slate-900">Browse Government Resolutions</p>
|
| 269 |
+
<p className="text-xs text-slate-500">Search and filter GRs by category, year, department</p>
|
| 270 |
+
</div>
|
| 271 |
+
<ArrowRight className="h-5 w-5 text-slate-400 ml-auto" />
|
| 272 |
+
</CardContent>
|
| 273 |
+
</Card>
|
| 274 |
+
|
| 275 |
+
<Card className="border-slate-200 hover:shadow-md transition-shadow cursor-pointer" onClick={() => navigate('/map')}>
|
| 276 |
+
<CardContent className="p-4 flex items-center gap-3">
|
| 277 |
+
<div className="rounded-full bg-cyan-100 p-2">
|
| 278 |
+
<Waves className="h-5 w-5 text-cyan-600" />
|
| 279 |
+
</div>
|
| 280 |
+
<div>
|
| 281 |
+
<p className="text-sm font-medium text-slate-900">View Flood Prediction Map</p>
|
| 282 |
+
<p className="text-xs text-slate-500">Real-time flood status and predictions</p>
|
| 283 |
+
</div>
|
| 284 |
+
<ArrowRight className="h-5 w-5 text-slate-400 ml-auto" />
|
| 285 |
+
</CardContent>
|
| 286 |
+
</Card>
|
| 287 |
+
</div>
|
| 288 |
+
</div>
|
| 289 |
+
);
|
| 290 |
+
}
|
src/pages/GisMap.tsx
ADDED
|
@@ -0,0 +1,969 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
| 2 |
+
import { MapContainer, TileLayer, Marker, Popup, Circle, useMap, GeoJSON, useMapEvents } from 'react-leaflet';
|
| 3 |
+
import 'leaflet/dist/leaflet.css';
|
| 4 |
+
import L from 'leaflet';
|
| 5 |
+
import { Card, CardContent, CardHeader, CardTitle } from '@/src/components/ui/Card';
|
| 6 |
+
import { Badge } from '@/src/components/ui/Badge';
|
| 7 |
+
import { Button } from '@/src/components/ui/Button';
|
| 8 |
+
import {
|
| 9 |
+
Layers, Play, Pause, AlertTriangle, Menu, X, Droplets,
|
| 10 |
+
MapPin, Waves, ArrowUpRight, Wind, Activity, Gauge,
|
| 11 |
+
ChevronRight, Info
|
| 12 |
+
} from 'lucide-react';
|
| 13 |
+
import { cn } from '@/src/utils/cn';
|
| 14 |
+
|
| 15 |
+
// Fix for default marker icons in react-leaflet
|
| 16 |
+
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
| 17 |
+
L.Icon.Default.mergeOptions({
|
| 18 |
+
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
|
| 19 |
+
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
|
| 20 |
+
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
const customMarkerIcon = new L.Icon({
|
| 24 |
+
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-blue.png',
|
| 25 |
+
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
| 26 |
+
iconSize: [25, 41],
|
| 27 |
+
iconAnchor: [12, 41],
|
| 28 |
+
popupAnchor: [1, -34],
|
| 29 |
+
shadowSize: [41, 41]
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
const warningMarkerIcon = new L.Icon({
|
| 33 |
+
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-orange.png',
|
| 34 |
+
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
| 35 |
+
iconSize: [25, 41],
|
| 36 |
+
iconAnchor: [12, 41],
|
| 37 |
+
popupAnchor: [1, -34],
|
| 38 |
+
shadowSize: [41, 41]
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
const criticalMarkerIcon = new L.Icon({
|
| 42 |
+
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
|
| 43 |
+
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
| 44 |
+
iconSize: [25, 41],
|
| 45 |
+
iconAnchor: [12, 41],
|
| 46 |
+
popupAnchor: [1, -34],
|
| 47 |
+
shadowSize: [41, 41]
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
// Maharashtra major dams with realistic data
|
| 51 |
+
const dams = [
|
| 52 |
+
{ id: 1, name: 'Koyna Dam', lat: 17.4014, lng: 73.7458, level: 92, capacity: '2.8 TMC', inflow: '45,000 cusecs', outflow: '32,000 cusecs', status: 'Critical', district: 'Satara' },
|
| 53 |
+
{ id: 2, name: 'Jayakwadi Dam', lat: 19.4833, lng: 75.3167, level: 78, capacity: '2.9 TMC', inflow: '5,000 cusecs', outflow: '3,000 cusecs', status: 'Normal', district: 'Aurangabad' },
|
| 54 |
+
{ id: 3, name: 'Ujani Dam', lat: 18.0736, lng: 75.1164, level: 85, capacity: '3.3 TMC', inflow: '15,000 cusecs', outflow: '12,000 cusecs', status: 'Warning', district: 'Solapur' },
|
| 55 |
+
{ id: 4, name: 'Panshet Dam', lat: 18.3858, lng: 73.6158, level: 65, capacity: '0.3 TMC', inflow: '2,000 cusecs', outflow: '0 cusecs', status: 'Normal', district: 'Pune' },
|
| 56 |
+
{ id: 5, name: 'Radhanagari Dam', lat: 16.4100, lng: 73.9900, level: 95, capacity: '0.2 TMC', inflow: '25,000 cusecs', outflow: '18,000 cusecs', status: 'Critical', district: 'Kolhapur' },
|
| 57 |
+
{ id: 6, name: 'Ghatghar Dam', lat: 19.5670, lng: 73.4840, level: 88, capacity: '0.5 TMC', inflow: '8,000 cusecs', outflow: '5,000 cusecs', status: 'Warning', district: 'Thane' },
|
| 58 |
+
{ id: 7, name: 'Bhandardara Dam', lat: 19.5500, lng: 73.7600, level: 72, capacity: '0.3 TMC', inflow: '3,000 cusecs', outflow: '1,500 cusecs', status: 'Normal', district: 'Ahmednagar' },
|
| 59 |
+
];
|
| 60 |
+
|
| 61 |
+
// Flood prediction zones (Pur Sadrushya Paristhiti)
|
| 62 |
+
const floodZones = [
|
| 63 |
+
{ id: 1, lat: 16.6965, lng: 74.2433, radius: 15000, risk: 'High', area: 'Kolhapur City & Surroundings', affectedPopulation: '500,000+', river: 'Panchaganga' },
|
| 64 |
+
{ id: 2, lat: 16.8524, lng: 74.5815, radius: 12000, risk: 'Medium', area: 'Sangli City', affectedPopulation: '200,000+', river: 'Krishna' },
|
| 65 |
+
{ id: 3, lat: 17.2777, lng: 74.1844, radius: 8000, risk: 'Low', area: 'Karad', affectedPopulation: '50,000+', river: 'Koyna' },
|
| 66 |
+
{ id: 4, lat: 19.2000, lng: 74.7500, radius: 10000, risk: 'Medium', area: 'Beed District', affectedPopulation: '100,000+', river: 'Godavari' },
|
| 67 |
+
];
|
| 68 |
+
|
| 69 |
+
// Maharashtra districts for boundary fetching
|
| 70 |
+
const maharashtraDistricts = [
|
| 71 |
+
'Kolhapur', 'Sangli', 'Satara', 'Pune', 'Ahmednagar', 'Nashik', 'Aurangabad', 'Thane', 'Raigad', 'Ratnagiri'
|
| 72 |
+
];
|
| 73 |
+
|
| 74 |
+
interface DistrictBoundary {
|
| 75 |
+
name: string;
|
| 76 |
+
geojson: any;
|
| 77 |
+
risk: 'High' | 'Medium' | 'Low' | 'None';
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
interface LiveCell {
|
| 81 |
+
id: string;
|
| 82 |
+
name: string;
|
| 83 |
+
lat: number;
|
| 84 |
+
lng: number;
|
| 85 |
+
intensity: number; // 0..1
|
| 86 |
+
type: 'rain' | 'flood';
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
// Rain drop particle for animation
|
| 90 |
+
interface RainDrop {
|
| 91 |
+
x: number;
|
| 92 |
+
y: number;
|
| 93 |
+
speed: number;
|
| 94 |
+
length: number;
|
| 95 |
+
opacity: number;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// Flood wave parameters
|
| 99 |
+
interface FloodWave {
|
| 100 |
+
offset: number;
|
| 101 |
+
amplitude: number;
|
| 102 |
+
frequency: number;
|
| 103 |
+
speed: number;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// Custom canvas layer for rain animation inside polygons
|
| 107 |
+
class RainCanvasLayer extends L.Layer {
|
| 108 |
+
private _canvas: HTMLCanvasElement | null = null;
|
| 109 |
+
private _ctx: CanvasRenderingContext2D | null = null;
|
| 110 |
+
private _rainDrops: RainDrop[] = [];
|
| 111 |
+
private _polygons: L.Polygon[] = [];
|
| 112 |
+
private _animationFrame: number = 0;
|
| 113 |
+
private _isAnimating: boolean = false;
|
| 114 |
+
|
| 115 |
+
constructor(polygons: L.Polygon[]) {
|
| 116 |
+
super();
|
| 117 |
+
this._polygons = polygons;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
onAdd(map: L.Map) {
|
| 121 |
+
this._canvas = L.DomUtil.create('canvas', 'leaflet-rain-layer');
|
| 122 |
+
this._canvas.style.position = 'absolute';
|
| 123 |
+
this._canvas.style.pointerEvents = 'none';
|
| 124 |
+
map.getPanes().overlayPane.appendChild(this._canvas);
|
| 125 |
+
this._ctx = this._canvas.getContext('2d');
|
| 126 |
+
|
| 127 |
+
// Initialize rain drops
|
| 128 |
+
this._initRainDrops();
|
| 129 |
+
|
| 130 |
+
// Start animation
|
| 131 |
+
this._isAnimating = true;
|
| 132 |
+
this._animate();
|
| 133 |
+
|
| 134 |
+
map.on('moveend', this._updateCanvasSize, this);
|
| 135 |
+
map.on('zoomend', this._updateCanvasSize, this);
|
| 136 |
+
this._updateCanvasSize();
|
| 137 |
+
|
| 138 |
+
return this;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
onRemove(map: L.Map) {
|
| 142 |
+
this._isAnimating = false;
|
| 143 |
+
if (this._animationFrame) {
|
| 144 |
+
cancelAnimationFrame(this._animationFrame);
|
| 145 |
+
}
|
| 146 |
+
if (this._canvas && this._canvas.parentNode) {
|
| 147 |
+
this._canvas.parentNode.removeChild(this._canvas);
|
| 148 |
+
}
|
| 149 |
+
map.off('moveend', this._updateCanvasSize, this);
|
| 150 |
+
map.off('zoomend', this._updateCanvasSize, this);
|
| 151 |
+
return this;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
_initRainDrops() {
|
| 155 |
+
this._rainDrops = [];
|
| 156 |
+
for (let i = 0; i < 150; i++) {
|
| 157 |
+
this._rainDrops.push({
|
| 158 |
+
x: Math.random() * 2000,
|
| 159 |
+
y: Math.random() * 2000,
|
| 160 |
+
speed: 8 + Math.random() * 12,
|
| 161 |
+
length: 10 + Math.random() * 20,
|
| 162 |
+
opacity: 0.2 + Math.random() * 0.4
|
| 163 |
+
});
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
_updateCanvasSize() {
|
| 168 |
+
const map = this._map;
|
| 169 |
+
if (!map || !this._canvas) return;
|
| 170 |
+
|
| 171 |
+
const size = map.getSize();
|
| 172 |
+
const topLeft = map.containerPointToLayerPoint([0, 0]);
|
| 173 |
+
|
| 174 |
+
L.DomUtil.setPosition(this._canvas, topLeft);
|
| 175 |
+
this._canvas.width = size.x;
|
| 176 |
+
this._canvas.height = size.y;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
_animate = () => {
|
| 180 |
+
if (!this._isAnimating || !this._ctx || !this._canvas || !this._map) return;
|
| 181 |
+
|
| 182 |
+
const ctx = this._ctx;
|
| 183 |
+
const canvas = this._canvas;
|
| 184 |
+
const map = this._map;
|
| 185 |
+
|
| 186 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 187 |
+
|
| 188 |
+
// Draw rain drops clipped to polygons
|
| 189 |
+
ctx.save();
|
| 190 |
+
|
| 191 |
+
// Create clipping path from polygons
|
| 192 |
+
ctx.beginPath();
|
| 193 |
+
for (const polygon of this._polygons) {
|
| 194 |
+
const latLngs = polygon.getLatLngs() as L.LatLng[][];
|
| 195 |
+
if (latLngs[0]) {
|
| 196 |
+
for (const ring of latLngs) {
|
| 197 |
+
const firstPoint = map.latLngToContainerPoint(ring[0]);
|
| 198 |
+
ctx.moveTo(firstPoint.x, firstPoint.y);
|
| 199 |
+
for (let i = 1; i < ring.length; i++) {
|
| 200 |
+
const point = map.latLngToContainerPoint(ring[i]);
|
| 201 |
+
ctx.lineTo(point.x, point.y);
|
| 202 |
+
}
|
| 203 |
+
ctx.closePath();
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
ctx.clip();
|
| 208 |
+
|
| 209 |
+
// Draw rain drops
|
| 210 |
+
for (const drop of this._rainDrops) {
|
| 211 |
+
const screenPoint = map.containerPointToLayerPoint([drop.x % canvas.width, drop.y % canvas.height]);
|
| 212 |
+
|
| 213 |
+
ctx.beginPath();
|
| 214 |
+
ctx.strokeStyle = `rgba(100, 180, 255, ${drop.opacity * 0.6})`;
|
| 215 |
+
ctx.lineWidth = 1.5;
|
| 216 |
+
ctx.moveTo(screenPoint.x, screenPoint.y);
|
| 217 |
+
ctx.lineTo(screenPoint.x, screenPoint.y + drop.length);
|
| 218 |
+
ctx.stroke();
|
| 219 |
+
|
| 220 |
+
// Update position
|
| 221 |
+
drop.y += drop.speed;
|
| 222 |
+
if (drop.y > canvas.height + drop.length) {
|
| 223 |
+
drop.y = -drop.length;
|
| 224 |
+
drop.x = Math.random() * canvas.width;
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
ctx.restore();
|
| 229 |
+
|
| 230 |
+
this._animationFrame = requestAnimationFrame(this._animate);
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// Custom canvas layer for flood wave animation inside polygons
|
| 235 |
+
class FloodWaveCanvasLayer extends L.Layer {
|
| 236 |
+
private _canvas: HTMLCanvasElement | null = null;
|
| 237 |
+
private _ctx: CanvasRenderingContext2D | null = null;
|
| 238 |
+
private _polygons: L.Polygon[] = [];
|
| 239 |
+
private _animationFrame: number = 0;
|
| 240 |
+
private _isAnimating: boolean = false;
|
| 241 |
+
private _time: number = 0;
|
| 242 |
+
|
| 243 |
+
constructor(polygons: L.Polygon[]) {
|
| 244 |
+
super();
|
| 245 |
+
this._polygons = polygons;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
onAdd(map: L.Map) {
|
| 249 |
+
this._canvas = L.DomUtil.create('canvas', 'leaflet-flood-layer');
|
| 250 |
+
this._canvas.style.position = 'absolute';
|
| 251 |
+
this._canvas.style.pointerEvents = 'none';
|
| 252 |
+
map.getPanes().overlayPane.appendChild(this._canvas);
|
| 253 |
+
this._ctx = this._canvas.getContext('2d');
|
| 254 |
+
|
| 255 |
+
this._isAnimating = true;
|
| 256 |
+
this._animate();
|
| 257 |
+
|
| 258 |
+
map.on('moveend', this._updateCanvasSize, this);
|
| 259 |
+
map.on('zoomend', this._updateCanvasSize, this);
|
| 260 |
+
this._updateCanvasSize();
|
| 261 |
+
|
| 262 |
+
return this;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
onRemove(map: L.Map) {
|
| 266 |
+
this._isAnimating = false;
|
| 267 |
+
if (this._animationFrame) {
|
| 268 |
+
cancelAnimationFrame(this._animationFrame);
|
| 269 |
+
}
|
| 270 |
+
if (this._canvas && this._canvas.parentNode) {
|
| 271 |
+
this._canvas.parentNode.removeChild(this._canvas);
|
| 272 |
+
}
|
| 273 |
+
map.off('moveend', this._updateCanvasSize, this);
|
| 274 |
+
map.off('zoomend', this._updateCanvasSize, this);
|
| 275 |
+
return this;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
_updateCanvasSize() {
|
| 279 |
+
const map = this._map;
|
| 280 |
+
if (!map || !this._canvas) return;
|
| 281 |
+
|
| 282 |
+
const size = map.getSize();
|
| 283 |
+
const topLeft = map.containerPointToLayerPoint([0, 0]);
|
| 284 |
+
|
| 285 |
+
L.DomUtil.setPosition(this._canvas, topLeft);
|
| 286 |
+
this._canvas.width = size.x;
|
| 287 |
+
this._canvas.height = size.y;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
_animate = () => {
|
| 291 |
+
if (!this._isAnimating || !this._ctx || !this._canvas || !this._map) return;
|
| 292 |
+
|
| 293 |
+
const ctx = this._ctx;
|
| 294 |
+
const canvas = this._canvas;
|
| 295 |
+
const map = this._map;
|
| 296 |
+
|
| 297 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 298 |
+
|
| 299 |
+
this._time += 0.03;
|
| 300 |
+
|
| 301 |
+
ctx.save();
|
| 302 |
+
|
| 303 |
+
// Create clipping path from polygons
|
| 304 |
+
ctx.beginPath();
|
| 305 |
+
for (const polygon of this._polygons) {
|
| 306 |
+
const latLngs = polygon.getLatLngs() as L.LatLng[][];
|
| 307 |
+
if (latLngs[0]) {
|
| 308 |
+
for (const ring of latLngs) {
|
| 309 |
+
const firstPoint = map.latLngToContainerPoint(ring[0]);
|
| 310 |
+
ctx.moveTo(firstPoint.x, firstPoint.y);
|
| 311 |
+
for (let i = 1; i < ring.length; i++) {
|
| 312 |
+
const point = map.latLngToContainerPoint(ring[i]);
|
| 313 |
+
ctx.lineTo(point.x, point.y);
|
| 314 |
+
}
|
| 315 |
+
ctx.closePath();
|
| 316 |
+
}
|
| 317 |
+
}
|
| 318 |
+
}
|
| 319 |
+
ctx.clip();
|
| 320 |
+
|
| 321 |
+
// Draw flood wave layers with low opacity
|
| 322 |
+
const waveCount = 5;
|
| 323 |
+
for (let w = 0; w < waveCount; w++) {
|
| 324 |
+
const waveOffset = (this._time + w * 0.4) % 1;
|
| 325 |
+
const baseOpacity = 0.08 - w * 0.012;
|
| 326 |
+
|
| 327 |
+
ctx.beginPath();
|
| 328 |
+
|
| 329 |
+
// Create wave pattern across the clipped area
|
| 330 |
+
for (let x = 0; x < canvas.width; x += 3) {
|
| 331 |
+
const waveY = canvas.height * 0.3 + Math.sin((x * 0.02) + this._time * 2 + w) * 15 + waveOffset * canvas.height * 0.5;
|
| 332 |
+
|
| 333 |
+
if (x === 0) {
|
| 334 |
+
ctx.moveTo(x, waveY);
|
| 335 |
+
} else {
|
| 336 |
+
ctx.lineTo(x, waveY);
|
| 337 |
+
}
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
// Complete the wave shape
|
| 341 |
+
ctx.lineTo(canvas.width, canvas.height);
|
| 342 |
+
ctx.lineTo(0, canvas.height);
|
| 343 |
+
ctx.closePath();
|
| 344 |
+
|
| 345 |
+
// Gradient fill for water effect
|
| 346 |
+
const gradient = ctx.createLinearGradient(0, canvas.height * 0.3, 0, canvas.height);
|
| 347 |
+
gradient.addColorStop(0, `rgba(34, 197, 94, ${baseOpacity})`);
|
| 348 |
+
gradient.addColorStop(0.5, `rgba(14, 165, 233, ${baseOpacity * 0.8})`);
|
| 349 |
+
gradient.addColorStop(1, `rgba(59, 130, 246, ${baseOpacity * 0.5})`);
|
| 350 |
+
|
| 351 |
+
ctx.fillStyle = gradient;
|
| 352 |
+
ctx.fill();
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
// Add ripple circles
|
| 356 |
+
for (let i = 0; i < 8; i++) {
|
| 357 |
+
const rippleX = (Math.sin(this._time * 0.5 + i * 1.3) + 1) * canvas.width * 0.5;
|
| 358 |
+
const rippleY = (Math.cos(this._time * 0.3 + i * 1.7) + 1) * canvas.height * 0.5;
|
| 359 |
+
const rippleRadius = 20 + Math.sin(this._time * 2 + i) * 10;
|
| 360 |
+
const rippleOpacity = 0.05 + Math.sin(this._time * 3 + i) * 0.03;
|
| 361 |
+
|
| 362 |
+
ctx.beginPath();
|
| 363 |
+
ctx.arc(rippleX, rippleY, rippleRadius, 0, Math.PI * 2);
|
| 364 |
+
ctx.strokeStyle = `rgba(96, 165, 250, ${rippleOpacity})`;
|
| 365 |
+
ctx.lineWidth = 2;
|
| 366 |
+
ctx.stroke();
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
ctx.restore();
|
| 370 |
+
|
| 371 |
+
this._animationFrame = requestAnimationFrame(this._animate);
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
// Component to handle map animations
|
| 376 |
+
function MapAnimator({ isPlaying, timeStep }: { isPlaying: boolean; timeStep: number }) {
|
| 377 |
+
const map = useMap();
|
| 378 |
+
|
| 379 |
+
useEffect(() => {
|
| 380 |
+
if (isPlaying) {
|
| 381 |
+
// Simulate flood spread animation
|
| 382 |
+
}
|
| 383 |
+
}, [isPlaying, timeStep, map]);
|
| 384 |
+
|
| 385 |
+
return null;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
// Component to fit bounds to Maharashtra
|
| 389 |
+
function MaharashtraBounds() {
|
| 390 |
+
const map = useMap();
|
| 391 |
+
|
| 392 |
+
useEffect(() => {
|
| 393 |
+
map.fitBounds([
|
| 394 |
+
[15.6, 72.8],
|
| 395 |
+
[22.0, 80.0]
|
| 396 |
+
], { padding: [20, 20] });
|
| 397 |
+
}, [map]);
|
| 398 |
+
|
| 399 |
+
return null;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
// Component to manage live animation layers (rain + flood)
|
| 403 |
+
function LiveAnimationManager({
|
| 404 |
+
isActive,
|
| 405 |
+
polygons
|
| 406 |
+
}: {
|
| 407 |
+
isActive: boolean;
|
| 408 |
+
polygons: L.Polygon[];
|
| 409 |
+
}) {
|
| 410 |
+
const map = useMap();
|
| 411 |
+
const rainLayerRef = useRef<RainCanvasLayer | null>(null);
|
| 412 |
+
const floodLayerRef = useRef<FloodWaveCanvasLayer | null>(null);
|
| 413 |
+
|
| 414 |
+
useEffect(() => {
|
| 415 |
+
if (isActive && polygons.length > 0) {
|
| 416 |
+
// Add rain animation layer
|
| 417 |
+
if (!rainLayerRef.current) {
|
| 418 |
+
rainLayerRef.current = new RainCanvasLayer(polygons);
|
| 419 |
+
rainLayerRef.current.addTo(map);
|
| 420 |
+
}
|
| 421 |
+
// Add flood wave animation layer
|
| 422 |
+
if (!floodLayerRef.current) {
|
| 423 |
+
floodLayerRef.current = new FloodWaveCanvasLayer(polygons);
|
| 424 |
+
floodLayerRef.current.addTo(map);
|
| 425 |
+
}
|
| 426 |
+
} else {
|
| 427 |
+
// Remove animation layers when not active
|
| 428 |
+
if (rainLayerRef.current) {
|
| 429 |
+
map.removeLayer(rainLayerRef.current);
|
| 430 |
+
rainLayerRef.current = null;
|
| 431 |
+
}
|
| 432 |
+
if (floodLayerRef.current) {
|
| 433 |
+
map.removeLayer(floodLayerRef.current);
|
| 434 |
+
floodLayerRef.current = null;
|
| 435 |
+
}
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
return () => {
|
| 439 |
+
if (rainLayerRef.current) {
|
| 440 |
+
map.removeLayer(rainLayerRef.current);
|
| 441 |
+
rainLayerRef.current = null;
|
| 442 |
+
}
|
| 443 |
+
if (floodLayerRef.current) {
|
| 444 |
+
map.removeLayer(floodLayerRef.current);
|
| 445 |
+
floodLayerRef.current = null;
|
| 446 |
+
}
|
| 447 |
+
};
|
| 448 |
+
}, [isActive, polygons, map]);
|
| 449 |
+
|
| 450 |
+
return null;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
export function GisMap() {
|
| 454 |
+
const [isPlaying, setIsPlaying] = useState(false);
|
| 455 |
+
const [timeStep, setTimeStep] = useState(0);
|
| 456 |
+
const [activeLayer, setActiveLayer] = useState<'current' | 'prediction'>('prediction');
|
| 457 |
+
const [showMobileControls, setShowMobileControls] = useState(false);
|
| 458 |
+
const [districtBoundaries, setDistrictBoundaries] = useState<DistrictBoundary[]>([]);
|
| 459 |
+
const [loadingBoundaries, setLoadingBoundaries] = useState(false);
|
| 460 |
+
const [selectedDam, setSelectedDam] = useState<number | null>(null);
|
| 461 |
+
const [showDistricts, setShowDistricts] = useState(true);
|
| 462 |
+
const [liveTick, setLiveTick] = useState(0);
|
| 463 |
+
const districtPolygonsRef = useRef<L.Polygon[]>([]);
|
| 464 |
+
const [polygonsReady, setPolygonsReady] = useState(false);
|
| 465 |
+
|
| 466 |
+
useEffect(() => {
|
| 467 |
+
const loadCachedBoundaries = async () => {
|
| 468 |
+
setLoadingBoundaries(true);
|
| 469 |
+
try {
|
| 470 |
+
const res = await fetch('/data/maharashtra-districts.geojson', {
|
| 471 |
+
headers: { 'Accept': 'application/json' }
|
| 472 |
+
});
|
| 473 |
+
const geo = await res.json();
|
| 474 |
+
|
| 475 |
+
const features: any[] = Array.isArray(geo?.features) ? geo.features : [];
|
| 476 |
+
const boundaries: DistrictBoundary[] = features.map((feature: any, idx: number) => {
|
| 477 |
+
const district = String(feature?.properties?.name || feature?.properties?.NAME || feature?.properties?.district || `District ${idx + 1}`);
|
| 478 |
+
const risk = floodZones.some(z => z.area.toLowerCase().includes(district.toLowerCase()))
|
| 479 |
+
? 'High'
|
| 480 |
+
: ['Satara', 'Thane', 'Raigad', 'Ratnagiri'].some((d) => district.toLowerCase().includes(d.toLowerCase()))
|
| 481 |
+
? 'Medium'
|
| 482 |
+
: 'Low';
|
| 483 |
+
return { name: district, geojson: feature, risk };
|
| 484 |
+
});
|
| 485 |
+
|
| 486 |
+
setDistrictBoundaries(boundaries);
|
| 487 |
+
} catch (e) {
|
| 488 |
+
console.error('Failed to load cached district boundaries:', e);
|
| 489 |
+
setDistrictBoundaries([]);
|
| 490 |
+
} finally {
|
| 491 |
+
setLoadingBoundaries(false);
|
| 492 |
+
}
|
| 493 |
+
};
|
| 494 |
+
|
| 495 |
+
loadCachedBoundaries();
|
| 496 |
+
}, []);
|
| 497 |
+
|
| 498 |
+
// Create polygon references for animation clipping
|
| 499 |
+
const onEachDistrictFeature = useCallback((feature: any, layer: L.Layer) => {
|
| 500 |
+
if (layer instanceof L.Polygon) {
|
| 501 |
+
districtPolygonsRef.current.push(layer);
|
| 502 |
+
setPolygonsReady(true);
|
| 503 |
+
}
|
| 504 |
+
}, []);
|
| 505 |
+
|
| 506 |
+
useEffect(() => {
|
| 507 |
+
let interval: NodeJS.Timeout;
|
| 508 |
+
if (activeLayer === 'current') {
|
| 509 |
+
interval = setInterval(() => setLiveTick((t) => t + 1), 900);
|
| 510 |
+
}
|
| 511 |
+
return () => clearInterval(interval);
|
| 512 |
+
}, [activeLayer]);
|
| 513 |
+
|
| 514 |
+
useEffect(() => {
|
| 515 |
+
let interval: NodeJS.Timeout;
|
| 516 |
+
if (isPlaying) {
|
| 517 |
+
interval = setInterval(() => {
|
| 518 |
+
setTimeStep((prev) => (prev >= 48 ? 0 : prev + 1));
|
| 519 |
+
}, 800);
|
| 520 |
+
}
|
| 521 |
+
return () => clearInterval(interval);
|
| 522 |
+
}, [isPlaying]);
|
| 523 |
+
|
| 524 |
+
const togglePlay = useCallback(() => setIsPlaying(!isPlaying), [isPlaying]);
|
| 525 |
+
|
| 526 |
+
const getDamIcon = (status: string) => {
|
| 527 |
+
switch (status) {
|
| 528 |
+
case 'Critical': return criticalMarkerIcon;
|
| 529 |
+
case 'Warning': return warningMarkerIcon;
|
| 530 |
+
default: return customMarkerIcon;
|
| 531 |
+
}
|
| 532 |
+
};
|
| 533 |
+
|
| 534 |
+
const districtStyle = (risk: string) => {
|
| 535 |
+
const colors: Record<string, { color: string; fillColor: string; fillOpacity: number }> = {
|
| 536 |
+
High: { color: '#dc2626', fillColor: '#dc2626', fillOpacity: 0.15 },
|
| 537 |
+
Medium: { color: '#ea580c', fillColor: '#ea580c', fillOpacity: 0.1 },
|
| 538 |
+
Low: { color: '#ca8a04', fillColor: '#ca8a04', fillOpacity: 0.05 },
|
| 539 |
+
None: { color: '#64748b', fillColor: '#64748b', fillOpacity: 0.02 }
|
| 540 |
+
};
|
| 541 |
+
return colors[risk] || colors.None;
|
| 542 |
+
};
|
| 543 |
+
|
| 544 |
+
const liveCells: LiveCell[] = [
|
| 545 |
+
{ id: 'konkan-rain', name: 'Konkan Heavy Rain', lat: 17.2, lng: 73.3, intensity: 0.9, type: 'rain' },
|
| 546 |
+
{ id: 'pune-rain', name: 'Pune Thunderstorm', lat: 18.52, lng: 73.86, intensity: 0.6, type: 'rain' },
|
| 547 |
+
{ id: 'kolhapur-flood', name: 'Kolhapur Flood Pulse', lat: 16.70, lng: 74.24, intensity: 0.85, type: 'flood' },
|
| 548 |
+
{ id: 'sangli-flood', name: 'Sangli River Rise', lat: 16.85, lng: 74.58, intensity: 0.55, type: 'flood' },
|
| 549 |
+
];
|
| 550 |
+
|
| 551 |
+
return (
|
| 552 |
+
<div className="flex flex-col h-full w-full min-h-0">
|
| 553 |
+
{/* Header */}
|
| 554 |
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 px-4 py-3 bg-white border-b border-slate-200 shrink-0">
|
| 555 |
+
<div>
|
| 556 |
+
<h1 className="text-lg lg:text-2xl font-bold tracking-tight text-slate-900 flex items-center gap-2">
|
| 557 |
+
<Droplets className="h-5 w-5 lg:h-6 lg:w-6 text-blue-600" />
|
| 558 |
+
Flood Prediction GIS
|
| 559 |
+
</h1>
|
| 560 |
+
<p className="text-xs lg:text-sm text-slate-500">Pur Sadrushya Paristhiti Analysis & Live Monitoring</p>
|
| 561 |
+
</div>
|
| 562 |
+
<div className="flex items-center gap-2">
|
| 563 |
+
<div className="flex bg-slate-100 rounded-lg p-1">
|
| 564 |
+
<Button
|
| 565 |
+
variant={activeLayer === 'current' ? 'default' : 'ghost'}
|
| 566 |
+
onClick={() => setActiveLayer('current')}
|
| 567 |
+
size="sm"
|
| 568 |
+
className="text-xs h-7 lg:h-8"
|
| 569 |
+
>
|
| 570 |
+
Live Data
|
| 571 |
+
</Button>
|
| 572 |
+
<Button
|
| 573 |
+
variant={activeLayer === 'prediction' ? 'default' : 'ghost'}
|
| 574 |
+
onClick={() => setActiveLayer('prediction')}
|
| 575 |
+
size="sm"
|
| 576 |
+
className="text-xs h-7 lg:h-8"
|
| 577 |
+
>
|
| 578 |
+
+48h Prediction
|
| 579 |
+
</Button>
|
| 580 |
+
</div>
|
| 581 |
+
<Button
|
| 582 |
+
variant="outline"
|
| 583 |
+
size="icon"
|
| 584 |
+
className="lg:hidden h-8 w-8"
|
| 585 |
+
onClick={() => setShowMobileControls(true)}
|
| 586 |
+
>
|
| 587 |
+
<Menu className="h-4 w-4" />
|
| 588 |
+
</Button>
|
| 589 |
+
</div>
|
| 590 |
+
</div>
|
| 591 |
+
|
| 592 |
+
{/* Main Content */}
|
| 593 |
+
<div className="flex-1 flex flex-col lg:grid lg:grid-cols-4 min-h-0">
|
| 594 |
+
{/* Map */}
|
| 595 |
+
<Card className="lg:col-span-3 relative overflow-hidden border-0 rounded-none lg:rounded-lg lg:border lg:m-2">
|
| 596 |
+
{/* Time Control Overlay */}
|
| 597 |
+
<div className="absolute top-3 left-3 right-3 lg:right-auto lg:min-w-[200px] z-[1000] bg-white/95 backdrop-blur-sm p-2 rounded-lg shadow-md flex items-center justify-between">
|
| 598 |
+
<div className="flex items-center gap-2">
|
| 599 |
+
<span className="text-sm font-semibold text-slate-700">
|
| 600 |
+
+{timeStep}h Forecast
|
| 601 |
+
</span>
|
| 602 |
+
{isPlaying && (
|
| 603 |
+
<span className="text-xs text-slate-500 animate-pulse">Live</span>
|
| 604 |
+
)}
|
| 605 |
+
</div>
|
| 606 |
+
<div className="flex items-center gap-1">
|
| 607 |
+
<Button size="icon" variant="ghost" onClick={togglePlay} className="h-8 w-8">
|
| 608 |
+
{isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
| 609 |
+
</Button>
|
| 610 |
+
</div>
|
| 611 |
+
</div>
|
| 612 |
+
|
| 613 |
+
{/* Legend Overlay */}
|
| 614 |
+
<div className="absolute bottom-3 left-3 z-[1000] bg-white/95 backdrop-blur-sm p-2 rounded-lg shadow-md hidden sm:block">
|
| 615 |
+
<div className="text-xs font-medium text-slate-700 mb-1">Risk Zones</div>
|
| 616 |
+
<div className="space-y-1">
|
| 617 |
+
<div className="flex items-center gap-2 text-xs">
|
| 618 |
+
<span className="w-3 h-3 rounded-full bg-red-500/60"></span>
|
| 619 |
+
<span>High Risk</span>
|
| 620 |
+
</div>
|
| 621 |
+
<div className="flex items-center gap-2 text-xs">
|
| 622 |
+
<span className="w-3 h-3 rounded-full bg-orange-500/40"></span>
|
| 623 |
+
<span>Medium Risk</span>
|
| 624 |
+
</div>
|
| 625 |
+
<div className="flex items-center gap-2 text-xs">
|
| 626 |
+
<span className="w-3 h-3 rounded-full bg-yellow-500/20"></span>
|
| 627 |
+
<span>Low Risk</span>
|
| 628 |
+
</div>
|
| 629 |
+
</div>
|
| 630 |
+
</div>
|
| 631 |
+
|
| 632 |
+
<MapContainer
|
| 633 |
+
center={[18.5204, 73.8567]}
|
| 634 |
+
zoom={7}
|
| 635 |
+
className="h-full w-full"
|
| 636 |
+
style={{ zIndex: 0 }}
|
| 637 |
+
>
|
| 638 |
+
<TileLayer
|
| 639 |
+
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
| 640 |
+
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
| 641 |
+
/>
|
| 642 |
+
|
| 643 |
+
<MaharashtraBounds />
|
| 644 |
+
<MapAnimator isPlaying={isPlaying} timeStep={timeStep} />
|
| 645 |
+
|
| 646 |
+
{/* Live Animation Layers - Rain drops and flood waves inside district boundaries */}
|
| 647 |
+
<LiveAnimationManager
|
| 648 |
+
isActive={activeLayer === 'current' && showDistricts}
|
| 649 |
+
polygons={districtPolygonsRef.current}
|
| 650 |
+
/>
|
| 651 |
+
|
| 652 |
+
{/* District Boundaries */}
|
| 653 |
+
{showDistricts && districtBoundaries.map((district) => (
|
| 654 |
+
<GeoJSON
|
| 655 |
+
key={district.name}
|
| 656 |
+
data={district.geojson}
|
| 657 |
+
style={() => districtStyle(district.risk)}
|
| 658 |
+
onEachFeature={onEachDistrictFeature}
|
| 659 |
+
>
|
| 660 |
+
<Popup>
|
| 661 |
+
<div className="p-1">
|
| 662 |
+
<h3 className="font-bold text-sm">{district.name} District</h3>
|
| 663 |
+
<p className="text-xs mt-1">
|
| 664 |
+
Flood Risk: <span className={cn(
|
| 665 |
+
"font-semibold",
|
| 666 |
+
district.risk === 'High' ? "text-red-600" :
|
| 667 |
+
district.risk === 'Medium' ? "text-orange-600" :
|
| 668 |
+
"text-yellow-600"
|
| 669 |
+
)}>{district.risk}</span>
|
| 670 |
+
</p>
|
| 671 |
+
</div>
|
| 672 |
+
</Popup>
|
| 673 |
+
</GeoJSON>
|
| 674 |
+
))}
|
| 675 |
+
|
| 676 |
+
{/* Dams */}
|
| 677 |
+
{dams.map(dam => (
|
| 678 |
+
<Marker
|
| 679 |
+
key={dam.id}
|
| 680 |
+
position={[dam.lat, dam.lng]}
|
| 681 |
+
icon={getDamIcon(dam.status)}
|
| 682 |
+
eventHandlers={{
|
| 683 |
+
click: () => setSelectedDam(dam.id)
|
| 684 |
+
}}
|
| 685 |
+
>
|
| 686 |
+
<Popup>
|
| 687 |
+
<div className="p-2 min-w-[200px]">
|
| 688 |
+
<div className="flex items-center justify-between mb-2">
|
| 689 |
+
<h3 className="font-bold text-sm">{dam.name}</h3>
|
| 690 |
+
<Badge variant={dam.status === 'Critical' ? 'destructive' : dam.status === 'Warning' ? 'outline' : 'secondary'} className={cn(
|
| 691 |
+
"text-xs",
|
| 692 |
+
dam.status === 'Warning' ? 'border-orange-500 text-orange-700 bg-orange-50' : ''
|
| 693 |
+
)}>
|
| 694 |
+
{dam.status}
|
| 695 |
+
</Badge>
|
| 696 |
+
</div>
|
| 697 |
+
<div className="space-y-1.5 text-xs">
|
| 698 |
+
<div className="flex justify-between">
|
| 699 |
+
<span className="text-slate-500">District:</span>
|
| 700 |
+
<span className="font-medium">{dam.district}</span>
|
| 701 |
+
</div>
|
| 702 |
+
<div className="flex justify-between">
|
| 703 |
+
<span className="text-slate-500">Storage:</span>
|
| 704 |
+
<span className={cn(
|
| 705 |
+
"font-medium",
|
| 706 |
+
dam.level > 90 ? "text-red-600" : dam.level > 75 ? "text-orange-600" : "text-green-600"
|
| 707 |
+
)}>{dam.level}% ({dam.capacity})</span>
|
| 708 |
+
</div>
|
| 709 |
+
<div className="flex justify-between">
|
| 710 |
+
<span className="text-slate-500">Inflow:</span>
|
| 711 |
+
<span className="font-medium text-blue-600">{dam.inflow}</span>
|
| 712 |
+
</div>
|
| 713 |
+
<div className="flex justify-between">
|
| 714 |
+
<span className="text-slate-500">Outflow:</span>
|
| 715 |
+
<span className="font-medium text-cyan-600">{dam.outflow}</span>
|
| 716 |
+
</div>
|
| 717 |
+
</div>
|
| 718 |
+
</div>
|
| 719 |
+
</Popup>
|
| 720 |
+
</Marker>
|
| 721 |
+
))}
|
| 722 |
+
|
| 723 |
+
{/* Flood Risk Zones */}
|
| 724 |
+
{activeLayer === 'prediction' && floodZones.map(zone => {
|
| 725 |
+
const currentRadius = isPlaying ? zone.radius * (1 + (timeStep * 0.015)) : zone.radius;
|
| 726 |
+
const opacity = zone.risk === 'High' ? 0.5 : zone.risk === 'Medium' ? 0.35 : 0.2;
|
| 727 |
+
const color = zone.risk === 'High' ? '#dc2626' : zone.risk === 'Medium' ? '#ea580c' : '#ca8a04';
|
| 728 |
+
|
| 729 |
+
return (
|
| 730 |
+
<Circle
|
| 731 |
+
key={zone.id}
|
| 732 |
+
center={[zone.lat, zone.lng]}
|
| 733 |
+
radius={currentRadius}
|
| 734 |
+
pathOptions={{
|
| 735 |
+
color: color,
|
| 736 |
+
fillColor: color,
|
| 737 |
+
fillOpacity: isPlaying ? opacity * (0.8 + Math.sin(timeStep * 0.2) * 0.2) : opacity,
|
| 738 |
+
weight: 2
|
| 739 |
+
}}
|
| 740 |
+
>
|
| 741 |
+
<Popup>
|
| 742 |
+
<div className="p-2 min-w-[180px]">
|
| 743 |
+
<div className="flex items-center gap-2 mb-2">
|
| 744 |
+
<AlertTriangle className={cn(
|
| 745 |
+
"h-4 w-4",
|
| 746 |
+
zone.risk === 'High' ? "text-red-500" :
|
| 747 |
+
zone.risk === 'Medium' ? "text-orange-500" :
|
| 748 |
+
"text-yellow-500"
|
| 749 |
+
)} />
|
| 750 |
+
<h3 className="font-bold text-sm">{zone.area}</h3>
|
| 751 |
+
</div>
|
| 752 |
+
<div className="space-y-1 text-xs">
|
| 753 |
+
<p><span className="text-slate-500">Risk Level:</span> <span className="font-semibold">{zone.risk}</span></p>
|
| 754 |
+
<p><span className="text-slate-500">River:</span> <span className="font-medium">{zone.river}</span></p>
|
| 755 |
+
<p><span className="text-slate-500">Population at Risk:</span> <span className="font-medium">{zone.affectedPopulation}</span></p>
|
| 756 |
+
<p><span className="text-slate-500">Impact Radius:</span> <span className="font-medium">{Math.round(currentRadius / 1000)} km</span></p>
|
| 757 |
+
</div>
|
| 758 |
+
</div>
|
| 759 |
+
</Popup>
|
| 760 |
+
</Circle>
|
| 761 |
+
);
|
| 762 |
+
})}
|
| 763 |
+
|
| 764 |
+
{/* Live layer: Rainfall/Flood indicator circles with pulsing opacity */}
|
| 765 |
+
{activeLayer === 'current' && liveCells.map((cell) => {
|
| 766 |
+
const phase = (liveTick % 20) / 20;
|
| 767 |
+
const pulse = 0.65 + Math.sin((phase * 2 * Math.PI) + (cell.lat + cell.lng)) * 0.25;
|
| 768 |
+
const intensity = Math.max(0.15, Math.min(1, cell.intensity * pulse));
|
| 769 |
+
const baseRadius = cell.type === 'rain' ? 12000 : 15000;
|
| 770 |
+
const radius = baseRadius * (0.75 + phase * 0.5);
|
| 771 |
+
const color = cell.type === 'rain' ? '#3b82f6' : '#06b6d4';
|
| 772 |
+
const fill = cell.type === 'rain' ? '#60a5fa' : '#22d3ee';
|
| 773 |
+
|
| 774 |
+
return (
|
| 775 |
+
<Circle
|
| 776 |
+
key={cell.id}
|
| 777 |
+
center={[cell.lat, cell.lng]}
|
| 778 |
+
radius={radius}
|
| 779 |
+
pathOptions={{
|
| 780 |
+
color,
|
| 781 |
+
fillColor: fill,
|
| 782 |
+
fillOpacity: 0.06 + intensity * 0.12,
|
| 783 |
+
weight: 1,
|
| 784 |
+
dashArray: cell.type === 'rain' ? '4 8' : undefined,
|
| 785 |
+
}}
|
| 786 |
+
>
|
| 787 |
+
<Popup>
|
| 788 |
+
<div className="p-2 min-w-[180px]">
|
| 789 |
+
<div className="flex items-center justify-between">
|
| 790 |
+
<h3 className="font-bold text-sm">{cell.name}</h3>
|
| 791 |
+
<Badge className={cn(
|
| 792 |
+
'text-xs',
|
| 793 |
+
cell.type === 'rain' ? 'bg-blue-600' : 'bg-cyan-600'
|
| 794 |
+
)}>
|
| 795 |
+
{cell.type === 'rain' ? 'Rain' : 'Flood'}
|
| 796 |
+
</Badge>
|
| 797 |
+
</div>
|
| 798 |
+
<p className="text-xs text-slate-600 mt-1">
|
| 799 |
+
Intensity: <span className="font-semibold">{Math.round(intensity * 100)}%</span>
|
| 800 |
+
</p>
|
| 801 |
+
<p className="text-[11px] text-slate-500 mt-1">
|
| 802 |
+
Animation visible inside district boundaries.
|
| 803 |
+
</p>
|
| 804 |
+
</div>
|
| 805 |
+
</Popup>
|
| 806 |
+
</Circle>
|
| 807 |
+
);
|
| 808 |
+
})}
|
| 809 |
+
</MapContainer>
|
| 810 |
+
</Card>
|
| 811 |
+
|
| 812 |
+
{/* Sidebar - Desktop */}
|
| 813 |
+
<div className="hidden lg:block space-y-3 p-2 overflow-y-auto">
|
| 814 |
+
<Card>
|
| 815 |
+
<CardHeader className="pb-2">
|
| 816 |
+
<CardTitle className="text-sm flex items-center">
|
| 817 |
+
<AlertTriangle className="h-4 w-4 mr-2 text-red-500" />
|
| 818 |
+
Critical Alert Areas
|
| 819 |
+
</CardTitle>
|
| 820 |
+
</CardHeader>
|
| 821 |
+
<CardContent className="space-y-3">
|
| 822 |
+
<div className="p-3 bg-red-50 border border-red-100 rounded-md">
|
| 823 |
+
<div className="flex items-center justify-between mb-1">
|
| 824 |
+
<h4 className="text-sm font-semibold text-red-900">Kolhapur City</h4>
|
| 825 |
+
<Badge variant="destructive" className="text-xs">Critical</Badge>
|
| 826 |
+
</div>
|
| 827 |
+
<p className="text-xs text-red-700">Panchaganga river flowing above danger mark. Evacuation recommended for low-lying areas.</p>
|
| 828 |
+
<p className="text-xs text-red-600 mt-1 font-medium">500,000+ people at risk</p>
|
| 829 |
+
</div>
|
| 830 |
+
<div className="p-3 bg-orange-50 border border-orange-100 rounded-md">
|
| 831 |
+
<div className="flex items-center justify-between mb-1">
|
| 832 |
+
<h4 className="text-sm font-semibold text-orange-900">Sangli District</h4>
|
| 833 |
+
<Badge variant="outline" className="text-xs border-orange-500 text-orange-700">High</Badge>
|
| 834 |
+
</div>
|
| 835 |
+
<p className="text-xs text-orange-700">Water level rising steadily. Keep emergency response teams on standby.</p>
|
| 836 |
+
</div>
|
| 837 |
+
<div className="p-3 bg-yellow-50 border border-yellow-100 rounded-md">
|
| 838 |
+
<h4 className="text-sm font-semibold text-yellow-900">Satara Region</h4>
|
| 839 |
+
<p className="text-xs text-yellow-700 mt-1">Koyna dam at 92% capacity. Monitor discharge rates.</p>
|
| 840 |
+
</div>
|
| 841 |
+
</CardContent>
|
| 842 |
+
</Card>
|
| 843 |
+
|
| 844 |
+
<Card>
|
| 845 |
+
<CardHeader className="pb-2">
|
| 846 |
+
<CardTitle className="text-sm flex items-center">
|
| 847 |
+
<Layers className="h-4 w-4 mr-2 text-blue-500" />
|
| 848 |
+
Dam Status Overview
|
| 849 |
+
</CardTitle>
|
| 850 |
+
</CardHeader>
|
| 851 |
+
<CardContent className="space-y-2">
|
| 852 |
+
{dams.slice(0, 5).map(dam => (
|
| 853 |
+
<div
|
| 854 |
+
key={dam.id}
|
| 855 |
+
className={cn(
|
| 856 |
+
"flex justify-between items-center text-sm p-2 rounded-md cursor-pointer transition-colors",
|
| 857 |
+
selectedDam === dam.id ? "bg-slate-100" : "hover:bg-slate-50"
|
| 858 |
+
)}
|
| 859 |
+
onClick={() => setSelectedDam(dam.id)}
|
| 860 |
+
>
|
| 861 |
+
<div>
|
| 862 |
+
<span className="font-medium text-slate-700 block">{dam.name}</span>
|
| 863 |
+
<span className="text-xs text-slate-500">{dam.district}</span>
|
| 864 |
+
</div>
|
| 865 |
+
<div className="flex items-center gap-2">
|
| 866 |
+
<span className="text-xs text-slate-500">{dam.level}%</span>
|
| 867 |
+
<Badge
|
| 868 |
+
variant={dam.status === 'Critical' ? 'destructive' : dam.status === 'Warning' ? 'outline' : 'secondary'}
|
| 869 |
+
className={cn(
|
| 870 |
+
"text-xs",
|
| 871 |
+
dam.status === 'Warning' ? 'border-orange-500 text-orange-700 bg-orange-50' : ''
|
| 872 |
+
)}
|
| 873 |
+
>
|
| 874 |
+
{dam.status}
|
| 875 |
+
</Badge>
|
| 876 |
+
</div>
|
| 877 |
+
</div>
|
| 878 |
+
))}
|
| 879 |
+
</CardContent>
|
| 880 |
+
</Card>
|
| 881 |
+
|
| 882 |
+
<Card>
|
| 883 |
+
<CardHeader className="pb-2">
|
| 884 |
+
<CardTitle className="text-sm">District Boundaries</CardTitle>
|
| 885 |
+
</CardHeader>
|
| 886 |
+
<CardContent>
|
| 887 |
+
<div className="flex items-center justify-between">
|
| 888 |
+
<span className="text-xs text-slate-600">Show district boundaries</span>
|
| 889 |
+
<Button
|
| 890 |
+
variant={showDistricts ? "default" : "outline"}
|
| 891 |
+
size="sm"
|
| 892 |
+
className="h-7 text-xs"
|
| 893 |
+
onClick={() => setShowDistricts(!showDistricts)}
|
| 894 |
+
>
|
| 895 |
+
{showDistricts ? 'On' : 'Off'}
|
| 896 |
+
</Button>
|
| 897 |
+
</div>
|
| 898 |
+
{loadingBoundaries && (
|
| 899 |
+
<p className="text-xs text-slate-500 mt-2">Loading cached boundaries...</p>
|
| 900 |
+
)}
|
| 901 |
+
</CardContent>
|
| 902 |
+
</Card>
|
| 903 |
+
</div>
|
| 904 |
+
</div>
|
| 905 |
+
|
| 906 |
+
{/* Mobile Controls Overlay */}
|
| 907 |
+
{showMobileControls && (
|
| 908 |
+
<div className="fixed inset-0 z-[1001] lg:hidden">
|
| 909 |
+
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setShowMobileControls(false)} />
|
| 910 |
+
<div className="absolute bottom-0 left-0 right-0 bg-white rounded-t-2xl p-4 max-h-[70vh] overflow-y-auto animate-in slide-in-from-bottom duration-200">
|
| 911 |
+
<div className="flex items-center justify-between mb-4">
|
| 912 |
+
<h2 className="text-lg font-semibold">Controls & Alerts</h2>
|
| 913 |
+
<Button variant="ghost" size="icon" onClick={() => setShowMobileControls(false)}>
|
| 914 |
+
<X className="h-5 w-5" />
|
| 915 |
+
</Button>
|
| 916 |
+
</div>
|
| 917 |
+
|
| 918 |
+
{/* Mobile: Critical Areas */}
|
| 919 |
+
<div className="space-y-3 mb-4">
|
| 920 |
+
<h3 className="text-sm font-semibold flex items-center">
|
| 921 |
+
<AlertTriangle className="h-4 w-4 mr-2 text-red-500" />
|
| 922 |
+
Critical Alert Areas
|
| 923 |
+
</h3>
|
| 924 |
+
<div className="p-3 bg-red-50 border border-red-100 rounded-md">
|
| 925 |
+
<div className="flex items-center justify-between mb-1">
|
| 926 |
+
<h4 className="text-sm font-semibold text-red-900">Kolhapur City</h4>
|
| 927 |
+
<Badge variant="destructive" className="text-xs">Critical</Badge>
|
| 928 |
+
</div>
|
| 929 |
+
<p className="text-xs text-red-700">Panchaganga river above danger mark. Evacuation recommended.</p>
|
| 930 |
+
</div>
|
| 931 |
+
<div className="p-3 bg-orange-50 border border-orange-100 rounded-md">
|
| 932 |
+
<div className="flex items-center justify-between mb-1">
|
| 933 |
+
<h4 className="text-sm font-semibold text-orange-900">Sangli District</h4>
|
| 934 |
+
<Badge variant="outline" className="text-xs border-orange-500 text-orange-700">High</Badge>
|
| 935 |
+
</div>
|
| 936 |
+
<p className="text-xs text-orange-700">Water level rising steadily. Emergency teams on standby.</p>
|
| 937 |
+
</div>
|
| 938 |
+
</div>
|
| 939 |
+
|
| 940 |
+
{/* Mobile: Dam Status */}
|
| 941 |
+
<div className="space-y-2">
|
| 942 |
+
<h3 className="text-sm font-semibold flex items-center">
|
| 943 |
+
<Layers className="h-4 w-4 mr-2 text-blue-500" />
|
| 944 |
+
Dam Status
|
| 945 |
+
</h3>
|
| 946 |
+
{dams.slice(0, 5).map(dam => (
|
| 947 |
+
<div key={dam.id} className="flex justify-between items-center text-sm p-2 bg-slate-50 rounded-md">
|
| 948 |
+
<span className="font-medium text-slate-700">{dam.name}</span>
|
| 949 |
+
<div className="flex items-center gap-2">
|
| 950 |
+
<span className="text-xs text-slate-500">{dam.level}%</span>
|
| 951 |
+
<Badge
|
| 952 |
+
variant={dam.status === 'Critical' ? 'destructive' : dam.status === 'Warning' ? 'outline' : 'secondary'}
|
| 953 |
+
className={cn(
|
| 954 |
+
"text-xs",
|
| 955 |
+
dam.status === 'Warning' ? 'border-orange-500 text-orange-700 bg-orange-50' : ''
|
| 956 |
+
)}
|
| 957 |
+
>
|
| 958 |
+
{dam.status}
|
| 959 |
+
</Badge>
|
| 960 |
+
</div>
|
| 961 |
+
</div>
|
| 962 |
+
))}
|
| 963 |
+
</div>
|
| 964 |
+
</div>
|
| 965 |
+
</div>
|
| 966 |
+
)}
|
| 967 |
+
</div>
|
| 968 |
+
);
|
| 969 |
+
}
|
src/pages/GrFinder.tsx
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useMemo } from 'react';
|
| 2 |
+
import { useNavigate } from 'react-router-dom';
|
| 3 |
+
import { Card, CardContent, CardHeader, CardTitle } from '@/src/components/ui/Card';
|
| 4 |
+
import { Input } from '@/src/components/ui/Input';
|
| 5 |
+
import { Button } from '@/src/components/ui/Button';
|
| 6 |
+
import { Badge } from '@/src/components/ui/Badge';
|
| 7 |
+
import {
|
| 8 |
+
Search, Filter, FileText, Calendar, Building, ChevronRight,
|
| 9 |
+
MessageSquare, Download, Eye, Clock, TrendingUp, Star
|
| 10 |
+
} from 'lucide-react';
|
| 11 |
+
import { cn } from '@/src/utils/cn';
|
| 12 |
+
import { mockGRs, grCategories, grYears, grDepartments, GRDocument } from '@/src/data/mockGRs';
|
| 13 |
+
|
| 14 |
+
export function GrFinder() {
|
| 15 |
+
const navigate = useNavigate();
|
| 16 |
+
const [searchQuery, setSearchQuery] = useState('');
|
| 17 |
+
const [selectedCategory, setSelectedCategory] = useState<string>('All');
|
| 18 |
+
const [selectedYear, setSelectedYear] = useState<string>('All');
|
| 19 |
+
const [selectedDepartment, setSelectedDepartment] = useState<string>('All');
|
| 20 |
+
const [showFilters, setShowFilters] = useState(false);
|
| 21 |
+
|
| 22 |
+
const filteredGRs = useMemo(() => {
|
| 23 |
+
return mockGRs.filter(gr => {
|
| 24 |
+
const matchesSearch = searchQuery === '' ||
|
| 25 |
+
gr.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
| 26 |
+
gr.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
| 27 |
+
gr.keywords.some(k => k.toLowerCase().includes(searchQuery.toLowerCase()));
|
| 28 |
+
const matchesCategory = selectedCategory === 'All' || gr.category === selectedCategory;
|
| 29 |
+
const matchesYear = selectedYear === 'All' || gr.year.toString() === selectedYear;
|
| 30 |
+
const matchesDepartment = selectedDepartment === 'All' || gr.department === selectedDepartment;
|
| 31 |
+
return matchesSearch && matchesCategory && matchesYear && matchesDepartment;
|
| 32 |
+
});
|
| 33 |
+
}, [searchQuery, selectedCategory, selectedYear, selectedDepartment]);
|
| 34 |
+
|
| 35 |
+
const popularGRs = mockGRs.filter(gr => gr.popularQuery);
|
| 36 |
+
|
| 37 |
+
const handleAskAI = (gr: GRDocument) => {
|
| 38 |
+
navigate(`/ai-assistant?q=${encodeURIComponent(`Explain ${gr.id}: ${gr.title}`)}`);
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
const getStatusColor = (status: string) => {
|
| 42 |
+
switch (status) {
|
| 43 |
+
case 'Active': return 'bg-green-100 text-green-700 border-green-200';
|
| 44 |
+
case 'Amended': return 'bg-yellow-100 text-yellow-700 border-yellow-200';
|
| 45 |
+
case 'Superseded': return 'bg-red-100 text-red-700 border-red-200';
|
| 46 |
+
default: return 'bg-slate-100 text-slate-700 border-slate-200';
|
| 47 |
+
}
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
const getCategoryColor = (category: string) => {
|
| 51 |
+
const colors: Record<string, string> = {
|
| 52 |
+
'Flood Management': 'bg-blue-100 text-blue-700',
|
| 53 |
+
'Dam Safety': 'bg-red-100 text-red-700',
|
| 54 |
+
'Irrigation': 'bg-cyan-100 text-cyan-700',
|
| 55 |
+
'Compensation': 'bg-emerald-100 text-emerald-700',
|
| 56 |
+
'Water Policy': 'bg-purple-100 text-purple-700',
|
| 57 |
+
'Administrative': 'bg-orange-100 text-orange-700',
|
| 58 |
+
};
|
| 59 |
+
return colors[category] || 'bg-slate-100 text-slate-700';
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
return (
|
| 63 |
+
<div className="space-y-6 max-w-6xl mx-auto">
|
| 64 |
+
{/* Header */}
|
| 65 |
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
| 66 |
+
<div>
|
| 67 |
+
<h1 className="text-2xl font-bold tracking-tight text-slate-900">GR Finder</h1>
|
| 68 |
+
<p className="text-slate-500">Search Government Resolutions, Acts, and Circulars</p>
|
| 69 |
+
</div>
|
| 70 |
+
<Button onClick={() => navigate('/ai-assistant')} className="bg-indigo-600 hover:bg-indigo-700 text-white">
|
| 71 |
+
<MessageSquare className="mr-2 h-4 w-4" />
|
| 72 |
+
Ask GR Helper
|
| 73 |
+
</Button>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
{/* Search Card */}
|
| 77 |
+
<Card className="border-none shadow-md bg-white/50 backdrop-blur-sm">
|
| 78 |
+
<CardContent className="p-4 sm:p-6">
|
| 79 |
+
<div className="flex flex-col sm:flex-row gap-4">
|
| 80 |
+
<div className="relative flex-1">
|
| 81 |
+
<Search className="absolute left-3 top-3 h-5 w-5 text-slate-400" />
|
| 82 |
+
<Input
|
| 83 |
+
type="text"
|
| 84 |
+
placeholder="Search by GR number, title, or keywords..."
|
| 85 |
+
className="pl-10 h-12 text-base bg-white"
|
| 86 |
+
value={searchQuery}
|
| 87 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 88 |
+
/>
|
| 89 |
+
</div>
|
| 90 |
+
<Button
|
| 91 |
+
variant="outline"
|
| 92 |
+
className="h-12 px-6 bg-white shrink-0"
|
| 93 |
+
onClick={() => setShowFilters(!showFilters)}
|
| 94 |
+
>
|
| 95 |
+
<Filter className="mr-2 h-4 w-4" />
|
| 96 |
+
Filters
|
| 97 |
+
{(selectedCategory !== 'All' || selectedYear !== 'All' || selectedDepartment !== 'All') && (
|
| 98 |
+
<span className="ml-2 bg-indigo-100 text-indigo-700 rounded-full px-2 py-0.5 text-xs">
|
| 99 |
+
Active
|
| 100 |
+
</span>
|
| 101 |
+
)}
|
| 102 |
+
</Button>
|
| 103 |
+
</div>
|
| 104 |
+
|
| 105 |
+
{/* Expanded Filters */}
|
| 106 |
+
{showFilters && (
|
| 107 |
+
<div className="mt-4 pt-4 border-t border-slate-200 grid grid-cols-1 sm:grid-cols-3 gap-4">
|
| 108 |
+
<div>
|
| 109 |
+
<label className="text-xs font-medium text-slate-500 mb-1 block">Category</label>
|
| 110 |
+
<select
|
| 111 |
+
className="w-full h-10 rounded-md border border-slate-200 bg-white px-3 text-sm"
|
| 112 |
+
value={selectedCategory}
|
| 113 |
+
onChange={(e) => setSelectedCategory(e.target.value)}
|
| 114 |
+
>
|
| 115 |
+
<option value="All">All Categories</option>
|
| 116 |
+
{grCategories.map(cat => (
|
| 117 |
+
<option key={cat} value={cat}>{cat}</option>
|
| 118 |
+
))}
|
| 119 |
+
</select>
|
| 120 |
+
</div>
|
| 121 |
+
<div>
|
| 122 |
+
<label className="text-xs font-medium text-slate-500 mb-1 block">Year</label>
|
| 123 |
+
<select
|
| 124 |
+
className="w-full h-10 rounded-md border border-slate-200 bg-white px-3 text-sm"
|
| 125 |
+
value={selectedYear}
|
| 126 |
+
onChange={(e) => setSelectedYear(e.target.value)}
|
| 127 |
+
>
|
| 128 |
+
<option value="All">All Years</option>
|
| 129 |
+
{grYears.map(year => (
|
| 130 |
+
<option key={year} value={year}>{year}</option>
|
| 131 |
+
))}
|
| 132 |
+
</select>
|
| 133 |
+
</div>
|
| 134 |
+
<div>
|
| 135 |
+
<label className="text-xs font-medium text-slate-500 mb-1 block">Department</label>
|
| 136 |
+
<select
|
| 137 |
+
className="w-full h-10 rounded-md border border-slate-200 bg-white px-3 text-sm"
|
| 138 |
+
value={selectedDepartment}
|
| 139 |
+
onChange={(e) => setSelectedDepartment(e.target.value)}
|
| 140 |
+
>
|
| 141 |
+
<option value="All">All Departments</option>
|
| 142 |
+
{grDepartments.map(dept => (
|
| 143 |
+
<option key={dept} value={dept}>{dept}</option>
|
| 144 |
+
))}
|
| 145 |
+
</select>
|
| 146 |
+
</div>
|
| 147 |
+
</div>
|
| 148 |
+
)}
|
| 149 |
+
|
| 150 |
+
{/* Quick Category Filters */}
|
| 151 |
+
<div className="flex flex-wrap gap-2 mt-4">
|
| 152 |
+
<Badge
|
| 153 |
+
variant={selectedCategory === 'All' ? 'default' : 'secondary'}
|
| 154 |
+
className="cursor-pointer px-3 py-1 text-sm"
|
| 155 |
+
onClick={() => setSelectedCategory('All')}
|
| 156 |
+
>
|
| 157 |
+
All
|
| 158 |
+
</Badge>
|
| 159 |
+
{grCategories.slice(0, 4).map(cat => (
|
| 160 |
+
<Badge
|
| 161 |
+
key={cat}
|
| 162 |
+
variant={selectedCategory === cat ? 'default' : 'secondary'}
|
| 163 |
+
className="cursor-pointer px-3 py-1 text-sm"
|
| 164 |
+
onClick={() => setSelectedCategory(cat)}
|
| 165 |
+
>
|
| 166 |
+
{cat}
|
| 167 |
+
</Badge>
|
| 168 |
+
))}
|
| 169 |
+
</div>
|
| 170 |
+
</CardContent>
|
| 171 |
+
</Card>
|
| 172 |
+
|
| 173 |
+
{/* Popular GRs Section (shown when no search) */}
|
| 174 |
+
{searchQuery === '' && selectedCategory === 'All' && (
|
| 175 |
+
<Card className="border-none shadow-md bg-gradient-to-r from-indigo-50 to-blue-50">
|
| 176 |
+
<CardHeader className="pb-2">
|
| 177 |
+
<CardTitle className="text-base flex items-center gap-2">
|
| 178 |
+
<Star className="h-4 w-4 text-yellow-500" />
|
| 179 |
+
Popular GRs
|
| 180 |
+
</CardTitle>
|
| 181 |
+
</CardHeader>
|
| 182 |
+
<CardContent>
|
| 183 |
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
| 184 |
+
{popularGRs.slice(0, 4).map(gr => (
|
| 185 |
+
<div
|
| 186 |
+
key={gr.id}
|
| 187 |
+
className="bg-white rounded-lg p-3 border border-slate-200 hover:shadow-md transition-shadow cursor-pointer"
|
| 188 |
+
onClick={() => {
|
| 189 |
+
setSearchQuery(gr.id);
|
| 190 |
+
}}
|
| 191 |
+
>
|
| 192 |
+
<div className="flex items-start justify-between gap-2">
|
| 193 |
+
<div className="flex-1 min-w-0">
|
| 194 |
+
<p className="text-xs font-mono text-indigo-600">{gr.id}</p>
|
| 195 |
+
<p className="text-sm font-medium text-slate-900 truncate">{gr.title}</p>
|
| 196 |
+
</div>
|
| 197 |
+
<Badge className={cn("text-[10px] shrink-0", getCategoryColor(gr.category))}>
|
| 198 |
+
{gr.category}
|
| 199 |
+
</Badge>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
))}
|
| 203 |
+
</div>
|
| 204 |
+
</CardContent>
|
| 205 |
+
</Card>
|
| 206 |
+
)}
|
| 207 |
+
|
| 208 |
+
{/* Results Count */}
|
| 209 |
+
<div className="flex items-center justify-between">
|
| 210 |
+
<p className="text-sm text-slate-500">
|
| 211 |
+
Found <span className="font-semibold text-slate-700">{filteredGRs.length}</span> Government Resolutions
|
| 212 |
+
</p>
|
| 213 |
+
{(selectedCategory !== 'All' || selectedYear !== 'All' || selectedDepartment !== 'All' || searchQuery) && (
|
| 214 |
+
<Button
|
| 215 |
+
variant="ghost"
|
| 216 |
+
size="sm"
|
| 217 |
+
className="text-xs"
|
| 218 |
+
onClick={() => {
|
| 219 |
+
setSearchQuery('');
|
| 220 |
+
setSelectedCategory('All');
|
| 221 |
+
setSelectedYear('All');
|
| 222 |
+
setSelectedDepartment('All');
|
| 223 |
+
}}
|
| 224 |
+
>
|
| 225 |
+
Clear filters
|
| 226 |
+
</Button>
|
| 227 |
+
)}
|
| 228 |
+
</div>
|
| 229 |
+
|
| 230 |
+
{/* Results List */}
|
| 231 |
+
<div className="space-y-4">
|
| 232 |
+
{filteredGRs.map((gr) => (
|
| 233 |
+
<Card key={gr.id} className="overflow-hidden transition-all hover:shadow-md border-slate-200">
|
| 234 |
+
<CardContent className="p-0">
|
| 235 |
+
<div className="flex flex-col sm:flex-row">
|
| 236 |
+
<div className="p-6 flex-1">
|
| 237 |
+
<div className="flex items-start justify-between gap-2 mb-2">
|
| 238 |
+
<div className="flex items-center gap-2 flex-wrap">
|
| 239 |
+
<Badge className={cn("text-[10px]", getStatusColor(gr.status))}>
|
| 240 |
+
{gr.status}
|
| 241 |
+
</Badge>
|
| 242 |
+
<Badge className={cn("text-[10px]", getCategoryColor(gr.category))}>
|
| 243 |
+
{gr.category}
|
| 244 |
+
</Badge>
|
| 245 |
+
<span className="text-xs font-mono text-slate-600">{gr.id}</span>
|
| 246 |
+
</div>
|
| 247 |
+
{gr.popularQuery && (
|
| 248 |
+
<Star className="h-4 w-4 text-yellow-500 fill-yellow-500" />
|
| 249 |
+
)}
|
| 250 |
+
</div>
|
| 251 |
+
|
| 252 |
+
<h3 className="text-lg font-semibold text-slate-900 mb-2 leading-tight">
|
| 253 |
+
{gr.title}
|
| 254 |
+
</h3>
|
| 255 |
+
|
| 256 |
+
<p className="text-sm text-slate-600 mb-4 line-clamp-2">
|
| 257 |
+
{gr.summary}
|
| 258 |
+
</p>
|
| 259 |
+
|
| 260 |
+
<div className="flex flex-wrap items-center gap-4 text-xs text-slate-500">
|
| 261 |
+
<div className="flex items-center">
|
| 262 |
+
<Building className="mr-1 h-3.5 w-3.5" />
|
| 263 |
+
{gr.department}
|
| 264 |
+
</div>
|
| 265 |
+
<div className="flex items-center">
|
| 266 |
+
<Calendar className="mr-1 h-3.5 w-3.5" />
|
| 267 |
+
{gr.date}
|
| 268 |
+
</div>
|
| 269 |
+
{gr.relatedGRs && gr.relatedGRs.length > 0 && (
|
| 270 |
+
<div className="flex items-center">
|
| 271 |
+
<FileText className="mr-1 h-3.5 w-3.5" />
|
| 272 |
+
{gr.relatedGRs.length} Related
|
| 273 |
+
</div>
|
| 274 |
+
)}
|
| 275 |
+
</div>
|
| 276 |
+
|
| 277 |
+
{/* Keywords */}
|
| 278 |
+
<div className="flex flex-wrap gap-1 mt-3">
|
| 279 |
+
{gr.keywords.slice(0, 4).map(keyword => (
|
| 280 |
+
<span
|
| 281 |
+
key={keyword}
|
| 282 |
+
className="text-[10px] bg-slate-100 text-slate-600 px-2 py-0.5 rounded"
|
| 283 |
+
>
|
| 284 |
+
{keyword}
|
| 285 |
+
</span>
|
| 286 |
+
))}
|
| 287 |
+
</div>
|
| 288 |
+
</div>
|
| 289 |
+
|
| 290 |
+
<div className="bg-slate-50 border-t sm:border-t-0 sm:border-l border-slate-100 p-4 sm:p-6 flex sm:flex-col items-center justify-center gap-3 sm:w-48">
|
| 291 |
+
<Button variant="outline" size="sm" className="w-full justify-start bg-white">
|
| 292 |
+
<Eye className="mr-2 h-4 w-4" /> View
|
| 293 |
+
</Button>
|
| 294 |
+
<Button variant="outline" size="sm" className="w-full justify-start bg-white">
|
| 295 |
+
<Download className="mr-2 h-4 w-4" /> Download
|
| 296 |
+
</Button>
|
| 297 |
+
<Button
|
| 298 |
+
variant="secondary"
|
| 299 |
+
size="sm"
|
| 300 |
+
className="w-full justify-start bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border-indigo-100"
|
| 301 |
+
onClick={() => handleAskAI(gr)}
|
| 302 |
+
>
|
| 303 |
+
<MessageSquare className="mr-2 h-4 w-4" /> Ask AI
|
| 304 |
+
</Button>
|
| 305 |
+
</div>
|
| 306 |
+
</div>
|
| 307 |
+
</CardContent>
|
| 308 |
+
</Card>
|
| 309 |
+
))}
|
| 310 |
+
|
| 311 |
+
{filteredGRs.length === 0 && (
|
| 312 |
+
<div className="text-center py-12">
|
| 313 |
+
<FileText className="mx-auto h-12 w-12 text-slate-300 mb-4" />
|
| 314 |
+
<h3 className="text-lg font-medium text-slate-900">No GRs found</h3>
|
| 315 |
+
<p className="text-slate-500">Try adjusting your search or filters.</p>
|
| 316 |
+
<Button
|
| 317 |
+
variant="outline"
|
| 318 |
+
className="mt-4"
|
| 319 |
+
onClick={() => {
|
| 320 |
+
setSearchQuery('');
|
| 321 |
+
setSelectedCategory('All');
|
| 322 |
+
setSelectedYear('All');
|
| 323 |
+
setSelectedDepartment('All');
|
| 324 |
+
}}
|
| 325 |
+
>
|
| 326 |
+
Clear all filters
|
| 327 |
+
</Button>
|
| 328 |
+
</div>
|
| 329 |
+
)}
|
| 330 |
+
</div>
|
| 331 |
+
|
| 332 |
+
{/* Help Section */}
|
| 333 |
+
<Card className="bg-slate-50 border-slate-200">
|
| 334 |
+
<CardContent className="p-4">
|
| 335 |
+
<div className="flex items-start gap-3">
|
| 336 |
+
<MessageSquare className="h-5 w-5 text-indigo-600 mt-0.5" />
|
| 337 |
+
<div>
|
| 338 |
+
<p className="text-sm font-medium text-slate-900">Can't find what you're looking for?</p>
|
| 339 |
+
<p className="text-sm text-slate-500 mt-1">
|
| 340 |
+
Try asking the GR Helper AI assistant for personalized help with your query.
|
| 341 |
+
</p>
|
| 342 |
+
<Button
|
| 343 |
+
variant="link"
|
| 344 |
+
className="px-0 text-indigo-600 mt-1"
|
| 345 |
+
onClick={() => navigate('/ai-assistant')}
|
| 346 |
+
>
|
| 347 |
+
Open GR Helper <ChevronRight className="ml-1 h-4 w-4" />
|
| 348 |
+
</Button>
|
| 349 |
+
</div>
|
| 350 |
+
</div>
|
| 351 |
+
</CardContent>
|
| 352 |
+
</Card>
|
| 353 |
+
</div>
|
| 354 |
+
);
|
| 355 |
+
}
|
src/pages/Repository.tsx
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { Card, CardContent } from '@/src/components/ui/Card';
|
| 3 |
+
import { Input } from '@/src/components/ui/Input';
|
| 4 |
+
import { Button } from '@/src/components/ui/Button';
|
| 5 |
+
import { Badge } from '@/src/components/ui/Badge';
|
| 6 |
+
import {
|
| 7 |
+
Search,
|
| 8 |
+
Filter,
|
| 9 |
+
FileText,
|
| 10 |
+
Download,
|
| 11 |
+
Eye,
|
| 12 |
+
MessageSquare,
|
| 13 |
+
Calendar,
|
| 14 |
+
Building
|
| 15 |
+
} from 'lucide-react';
|
| 16 |
+
import { useNavigate } from 'react-router-dom';
|
| 17 |
+
|
| 18 |
+
const documents = [
|
| 19 |
+
{
|
| 20 |
+
id: 'GR-2023-01',
|
| 21 |
+
title: 'Guidelines for Flood Management and Evacuation Procedures',
|
| 22 |
+
type: 'GR',
|
| 23 |
+
department: 'Disaster Management',
|
| 24 |
+
date: '2023-06-15',
|
| 25 |
+
summary: 'Comprehensive guidelines for district administrations regarding flood preparedness, evacuation protocols, and relief camp management.'
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
id: 'ACT-1976',
|
| 29 |
+
title: 'Maharashtra Irrigation Act, 1976',
|
| 30 |
+
type: 'Act',
|
| 31 |
+
department: 'Water Resources',
|
| 32 |
+
date: '1976-08-05',
|
| 33 |
+
summary: 'An Act to unify and amend the law relating to irrigation in the State of Maharashtra.'
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
id: 'CIR-2024-45',
|
| 37 |
+
title: 'Standard Operating Procedure for Dam Gate Operations during Monsoon',
|
| 38 |
+
type: 'Circular',
|
| 39 |
+
department: 'Water Resources',
|
| 40 |
+
date: '2024-05-10',
|
| 41 |
+
summary: 'Updated SOPs for dam engineers regarding phased water discharge and coordination with downstream authorities.'
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
id: 'GR-2022-89',
|
| 45 |
+
title: 'Compensation Policy for Crop Damage due to Floods',
|
| 46 |
+
type: 'GR',
|
| 47 |
+
department: 'Revenue & Forest',
|
| 48 |
+
date: '2022-09-22',
|
| 49 |
+
summary: 'Revised compensation rates and assessment methodology for agricultural losses caused by natural calamities including floods.'
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
id: 'ACT-2005',
|
| 53 |
+
title: 'Maharashtra Water Resources Regulatory Authority Act, 2005',
|
| 54 |
+
type: 'Act',
|
| 55 |
+
department: 'Water Resources',
|
| 56 |
+
date: '2005-05-04',
|
| 57 |
+
summary: 'Act to provide for the establishment of the Maharashtra Water Resources Regulatory Authority to regulate water resources within the State.'
|
| 58 |
+
},
|
| 59 |
+
];
|
| 60 |
+
|
| 61 |
+
export function Repository() {
|
| 62 |
+
const [searchQuery, setSearchQuery] = useState('');
|
| 63 |
+
const [activeFilter, setActiveFilter] = useState('All');
|
| 64 |
+
const navigate = useNavigate();
|
| 65 |
+
|
| 66 |
+
const filteredDocs = documents.filter(doc => {
|
| 67 |
+
const matchesSearch = doc.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
| 68 |
+
doc.id.toLowerCase().includes(searchQuery.toLowerCase());
|
| 69 |
+
const matchesFilter = activeFilter === 'All' || doc.type === activeFilter;
|
| 70 |
+
return matchesSearch && matchesFilter;
|
| 71 |
+
});
|
| 72 |
+
|
| 73 |
+
const handleAskAI = (docTitle: string) => {
|
| 74 |
+
navigate(`/ai-assistant?q=Summarize the key points of ${encodeURIComponent(docTitle)}`);
|
| 75 |
+
};
|
| 76 |
+
|
| 77 |
+
return (
|
| 78 |
+
<div className="space-y-6 max-w-6xl mx-auto">
|
| 79 |
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
| 80 |
+
<div>
|
| 81 |
+
<h1 className="text-2xl font-bold tracking-tight text-slate-900">Centralized Repository</h1>
|
| 82 |
+
<p className="text-slate-500">Search and access GRs, Acts, and Circulars</p>
|
| 83 |
+
</div>
|
| 84 |
+
<Button onClick={() => navigate('/ai-assistant')} className="bg-indigo-600 hover:bg-indigo-700 text-white">
|
| 85 |
+
<MessageSquare className="mr-2 h-4 w-4" />
|
| 86 |
+
Ask AI Assistant
|
| 87 |
+
</Button>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
<Card className="border-none shadow-md bg-white/50 backdrop-blur-sm">
|
| 91 |
+
<CardContent className="p-4 sm:p-6">
|
| 92 |
+
<div className="flex flex-col sm:flex-row gap-4">
|
| 93 |
+
<div className="relative flex-1">
|
| 94 |
+
<Search className="absolute left-3 top-3 h-5 w-5 text-slate-400" />
|
| 95 |
+
<Input
|
| 96 |
+
type="text"
|
| 97 |
+
placeholder="Search by title, GR number, or keywords..."
|
| 98 |
+
className="pl-10 h-12 text-base bg-white"
|
| 99 |
+
value={searchQuery}
|
| 100 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 101 |
+
/>
|
| 102 |
+
</div>
|
| 103 |
+
<Button variant="outline" className="h-12 px-6 bg-white shrink-0">
|
| 104 |
+
<Filter className="mr-2 h-4 w-4" />
|
| 105 |
+
Advanced Filters
|
| 106 |
+
</Button>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<div className="flex flex-wrap gap-2 mt-4">
|
| 110 |
+
{['All', 'GR', 'Act', 'Circular'].map(filter => (
|
| 111 |
+
<Badge
|
| 112 |
+
key={filter}
|
| 113 |
+
variant={activeFilter === filter ? 'default' : 'secondary'}
|
| 114 |
+
className="cursor-pointer px-3 py-1 text-sm"
|
| 115 |
+
onClick={() => setActiveFilter(filter)}
|
| 116 |
+
>
|
| 117 |
+
{filter}
|
| 118 |
+
</Badge>
|
| 119 |
+
))}
|
| 120 |
+
</div>
|
| 121 |
+
</CardContent>
|
| 122 |
+
</Card>
|
| 123 |
+
|
| 124 |
+
<div className="space-y-4">
|
| 125 |
+
{filteredDocs.map((doc) => (
|
| 126 |
+
<Card key={doc.id} className="overflow-hidden transition-all hover:shadow-md border-slate-200">
|
| 127 |
+
<CardContent className="p-0">
|
| 128 |
+
<div className="flex flex-col sm:flex-row">
|
| 129 |
+
<div className="p-6 flex-1">
|
| 130 |
+
<div className="flex items-start justify-between">
|
| 131 |
+
<div>
|
| 132 |
+
<div className="flex items-center space-x-2 mb-2">
|
| 133 |
+
<Badge variant={
|
| 134 |
+
doc.type === 'GR' ? 'default' :
|
| 135 |
+
doc.type === 'Act' ? 'destructive' : 'secondary'
|
| 136 |
+
}>
|
| 137 |
+
{doc.type}
|
| 138 |
+
</Badge>
|
| 139 |
+
<span className="text-xs font-medium text-slate-500">{doc.id}</span>
|
| 140 |
+
</div>
|
| 141 |
+
<h3 className="text-lg font-semibold text-slate-900 mb-2 leading-tight">
|
| 142 |
+
{doc.title}
|
| 143 |
+
</h3>
|
| 144 |
+
<p className="text-sm text-slate-600 mb-4 line-clamp-2">
|
| 145 |
+
{doc.summary}
|
| 146 |
+
</p>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
|
| 150 |
+
<div className="flex flex-wrap items-center gap-4 text-xs text-slate-500">
|
| 151 |
+
<div className="flex items-center">
|
| 152 |
+
<Building className="mr-1 h-3.5 w-3.5" />
|
| 153 |
+
{doc.department}
|
| 154 |
+
</div>
|
| 155 |
+
<div className="flex items-center">
|
| 156 |
+
<Calendar className="mr-1 h-3.5 w-3.5" />
|
| 157 |
+
{doc.date}
|
| 158 |
+
</div>
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
<div className="bg-slate-50 border-t sm:border-t-0 sm:border-l border-slate-100 p-4 sm:p-6 flex sm:flex-col items-center justify-center gap-3 sm:w-48">
|
| 163 |
+
<Button variant="outline" size="sm" className="w-full justify-start bg-white">
|
| 164 |
+
<Eye className="mr-2 h-4 w-4" /> View
|
| 165 |
+
</Button>
|
| 166 |
+
<Button variant="outline" size="sm" className="w-full justify-start bg-white">
|
| 167 |
+
<Download className="mr-2 h-4 w-4" /> Download
|
| 168 |
+
</Button>
|
| 169 |
+
<Button
|
| 170 |
+
variant="secondary"
|
| 171 |
+
size="sm"
|
| 172 |
+
className="w-full justify-start bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border-indigo-100"
|
| 173 |
+
onClick={() => handleAskAI(doc.title)}
|
| 174 |
+
>
|
| 175 |
+
<MessageSquare className="mr-2 h-4 w-4" /> Ask AI
|
| 176 |
+
</Button>
|
| 177 |
+
</div>
|
| 178 |
+
</div>
|
| 179 |
+
</CardContent>
|
| 180 |
+
</Card>
|
| 181 |
+
))}
|
| 182 |
+
{filteredDocs.length === 0 && (
|
| 183 |
+
<div className="text-center py-12">
|
| 184 |
+
<FileText className="mx-auto h-12 w-12 text-slate-300 mb-4" />
|
| 185 |
+
<h3 className="text-lg font-medium text-slate-900">No documents found</h3>
|
| 186 |
+
<p className="text-slate-500">Try adjusting your search or filters.</p>
|
| 187 |
+
</div>
|
| 188 |
+
)}
|
| 189 |
+
</div>
|
| 190 |
+
</div>
|
| 191 |
+
);
|
| 192 |
+
}
|
src/utils/cn.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { clsx, type ClassValue } from "clsx";
|
| 2 |
+
import { twMerge } from "tailwind-merge";
|
| 3 |
+
|
| 4 |
+
export function cn(...inputs: ClassValue[]) {
|
| 5 |
+
return twMerge(clsx(inputs));
|
| 6 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"experimentalDecorators": true,
|
| 5 |
+
"useDefineForClassFields": false,
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"lib": [
|
| 8 |
+
"ES2022",
|
| 9 |
+
"DOM",
|
| 10 |
+
"DOM.Iterable"
|
| 11 |
+
],
|
| 12 |
+
"skipLibCheck": true,
|
| 13 |
+
"moduleResolution": "bundler",
|
| 14 |
+
"isolatedModules": true,
|
| 15 |
+
"moduleDetection": "force",
|
| 16 |
+
"allowJs": true,
|
| 17 |
+
"jsx": "react-jsx",
|
| 18 |
+
"paths": {
|
| 19 |
+
"@/*": [
|
| 20 |
+
"./*"
|
| 21 |
+
]
|
| 22 |
+
},
|
| 23 |
+
"allowImportingTsExtensions": true,
|
| 24 |
+
"noEmit": true
|
| 25 |
+
}
|
| 26 |
+
}
|
vite.config.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import path from 'path';
|
| 2 |
+
import { defineConfig, loadEnv } from 'vite';
|
| 3 |
+
import react from '@vitejs/plugin-react';
|
| 4 |
+
|
| 5 |
+
export default defineConfig(({ mode }) => {
|
| 6 |
+
const env = loadEnv(mode, '.', '');
|
| 7 |
+
return {
|
| 8 |
+
server: {
|
| 9 |
+
port: 3000,
|
| 10 |
+
host: '0.0.0.0',
|
| 11 |
+
},
|
| 12 |
+
preview: {
|
| 13 |
+
host: true, // Listen on all addresses
|
| 14 |
+
port: 7860,
|
| 15 |
+
strictPort: true,
|
| 16 |
+
allowedHosts: ['devarshia5-wrd-drishti.hf.space'] // Add this line
|
| 17 |
+
},
|
| 18 |
+
plugins: [react()],
|
| 19 |
+
define: {
|
| 20 |
+
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
| 21 |
+
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
| 22 |
+
},
|
| 23 |
+
resolve: {
|
| 24 |
+
alias: {
|
| 25 |
+
'@': path.resolve(__dirname, '.'),
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
};
|
| 29 |
+
});
|