nivakaran commited on
Commit
b4856f1
·
verified ·
1 Parent(s): 67578d3

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. .github/workflows/deploy-backend.yaml +63 -0
  3. .github/workflows/deploy-frontend.yaml +68 -0
  4. .gitignore +21 -0
  5. .langgraphignore +0 -0
  6. .python-version +1 -0
  7. Dockerfile +21 -0
  8. ModelX Final Problem.pdf +3 -0
  9. QUICKSTART.md +140 -0
  10. README.md +860 -7
  11. app.py +361 -0
  12. debug_path.py +30 -0
  13. debug_runner.py +250 -0
  14. docker-compose.prod.yml +65 -0
  15. docker-compose.yml +38 -0
  16. frontend/.gitignore +41 -0
  17. frontend/README.md +36 -0
  18. frontend/app/components/App.tsx +19 -0
  19. frontend/app/components/ClientWrapper.tsx +28 -0
  20. frontend/app/components/FloatingChatBox.tsx +310 -0
  21. frontend/app/components/LoadingScreen.tsx +115 -0
  22. frontend/app/components/NavLink.tsx +28 -0
  23. frontend/app/components/Roger.css +210 -0
  24. frontend/app/components/dashboard/AnomalyDetection.tsx +217 -0
  25. frontend/app/components/dashboard/CurrencyPrediction.tsx +242 -0
  26. frontend/app/components/dashboard/DashboardOverview.tsx +220 -0
  27. frontend/app/components/dashboard/HistoricalIntel.tsx +235 -0
  28. frontend/app/components/dashboard/NationalThreatCard.tsx +202 -0
  29. frontend/app/components/dashboard/RiverNetStatus.tsx +235 -0
  30. frontend/app/components/dashboard/StockPredictions.tsx +189 -0
  31. frontend/app/components/dashboard/WeatherPredictions.tsx +238 -0
  32. frontend/app/components/intelligence/IntelligenceFeed.tsx +242 -0
  33. frontend/app/components/intelligence/IntelligenceSettings.tsx +429 -0
  34. frontend/app/components/map/DistrictInfoPanel.tsx +199 -0
  35. frontend/app/components/map/MapView.tsx +84 -0
  36. frontend/app/components/map/SatelliteView.tsx +197 -0
  37. frontend/app/components/map/SriLankaMap.tsx +226 -0
  38. frontend/app/components/ui/accordion.tsx +52 -0
  39. frontend/app/components/ui/alert-dialog.tsx +104 -0
  40. frontend/app/components/ui/alert.tsx +43 -0
  41. frontend/app/components/ui/aspect-ratio.tsx +5 -0
  42. frontend/app/components/ui/avatar.tsx +38 -0
  43. frontend/app/components/ui/badge.tsx +29 -0
  44. frontend/app/components/ui/breadcrumb.tsx +90 -0
  45. frontend/app/components/ui/button.tsx +47 -0
  46. frontend/app/components/ui/calendar.tsx +81 -0
  47. frontend/app/components/ui/card.tsx +43 -0
  48. frontend/app/components/ui/carousel.tsx +224 -0
  49. frontend/app/components/ui/chart.tsx +363 -0
  50. frontend/app/components/ui/checkbox.tsx +26 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ ModelX[[:space:]]Final[[:space:]]Problem.pdf filter=lfs diff=lfs merge=lfs -text
.github/workflows/deploy-backend.yaml ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy Backend to Hugging Face Space
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - master
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ sync-to-hub:
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - name: Checkout repository
16
+ uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 0
19
+ lfs: true
20
+
21
+ - name: Setup Git LFS
22
+ run: |
23
+ git lfs install
24
+ git lfs pull
25
+ git lfs checkout
26
+
27
+ - name: Setup Python
28
+ uses: actions/setup-python@v5
29
+ with:
30
+ python-version: '3.11'
31
+
32
+ - name: Install huggingface_hub
33
+ run: pip install huggingface_hub
34
+
35
+ - name: Push to Hugging Face Space
36
+ env:
37
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
38
+ run: |
39
+ python -c "
40
+ from huggingface_hub import HfApi, login
41
+ import os
42
+
43
+ token = os.environ['HF_TOKEN']
44
+ login(token=token)
45
+
46
+ api = HfApi()
47
+ api.upload_folder(
48
+ folder_path='.',
49
+ repo_id='nivakaran/modelx',
50
+ repo_type='space',
51
+ token=token,
52
+ ignore_patterns=['*.pyc', '__pycache__', '.git', 'node_modules', '*.log', '.env']
53
+ )
54
+ print('✅ Successfully pushed to Hugging Face Space!')
55
+ "
56
+
57
+ - name: Verify Sync
58
+ if: success()
59
+ run: echo "✅ Successfully synced to Hugging Face Space!"
60
+
61
+ - name: Sync Failed
62
+ if: failure()
63
+ run: echo "❌ Failed to sync. Check if the Space exists at https://huggingface.co/spaces/nivakaran/modelx"
.github/workflows/deploy-frontend.yaml ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy Frontend to GitHub (for Vercel)
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - master
8
+ paths:
9
+ # Trigger only when frontend files change
10
+ - 'frontend/**'
11
+ workflow_dispatch:
12
+
13
+ jobs:
14
+ deploy-frontend:
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0
22
+
23
+ - name: Configure Git
24
+ run: |
25
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
26
+ git config --global user.name "github-actions[bot]"
27
+
28
+ - name: Extract and Push Frontend
29
+ env:
30
+ # Set your frontend repository here (format: username/repo)
31
+ FRONTEND_REPO: ${{ secrets.FRONTEND_REPO }}
32
+ # GitHub PAT with repo permissions for cross-repo push
33
+ DEPLOY_TOKEN: ${{ secrets.FRONTEND_DEPLOY_TOKEN }}
34
+ run: |
35
+ # Default repo if secret not set
36
+ REPO=${FRONTEND_REPO:-"Nivakaran-S/modelx-frontend"}
37
+
38
+ # Create a new directory for frontend-only repo
39
+ mkdir -p /tmp/frontend-deploy
40
+
41
+ # Copy frontend contents (not the folder itself)
42
+ cp -r frontend/* /tmp/frontend-deploy/
43
+
44
+ # Copy root configs needed for Next.js if they exist
45
+ [ -f "frontend/.gitignore" ] && cp frontend/.gitignore /tmp/frontend-deploy/ || true
46
+
47
+ # Create .env.production with API URL placeholder
48
+ echo "NEXT_PUBLIC_API_URL=\${NEXT_PUBLIC_API_URL:-https://nivakaran-modelx.hf.space}" > /tmp/frontend-deploy/.env.production
49
+
50
+ cd /tmp/frontend-deploy
51
+
52
+ # Initialize git repo
53
+ git init
54
+ git add .
55
+ git commit -m "Deploy frontend from main repo - $(date +'%Y-%m-%d %H:%M:%S')"
56
+
57
+ # Push to frontend repo
58
+ git remote add origin https://x-access-token:${DEPLOY_TOKEN}@github.com/${REPO}.git
59
+ git branch -M main
60
+ git push origin main --force
61
+
62
+ - name: Verify Deployment
63
+ if: success()
64
+ run: echo "✅ Successfully pushed frontend to GitHub repository for Vercel deployment!"
65
+
66
+ - name: Deployment Failed
67
+ if: failure()
68
+ run: echo "❌ Failed to push frontend. Check if FRONTEND_DEPLOY_TOKEN and FRONTEND_REPO secrets are configured."
.gitignore ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ .env
13
+ .env.template
14
+
15
+ models/
16
+ models
17
+ .langgraph_api
18
+ data/
19
+ datasets/
20
+ datasets
21
+ data
.langgraphignore ADDED
File without changes
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.11
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ curl \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy requirements
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy application code
15
+ COPY . .
16
+
17
+ # Expose port
18
+ EXPOSE 8000
19
+
20
+ # Run API server
21
+ CMD ["python", "main.py"]
ModelX Final Problem.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8666a7e263a028de6eb37864f48d8bfcfa78bf8303c72778c68396eade8b2300
3
+ size 4413994
QUICKSTART.md ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 Roger Quick Start Guide
2
+
3
+ ## Prerequisites
4
+ - Python 3.11+
5
+ - Node.js 18+
6
+ - Groq API Key ([Get Free Key](https://console.groq.com))
7
+
8
+ ## Installation & Setup
9
+
10
+ ### 1. Install Python Dependencies
11
+ ```bash
12
+ pip install -r requirements.txt
13
+ ```
14
+
15
+ ### 2. Configure Environment
16
+ ```bash
17
+ # Copy template
18
+ cp .env.template .env
19
+
20
+ # Edit .env and add your GROQ_API_KEY
21
+ # GROQ_API_KEY=your_key_here
22
+ ```
23
+
24
+ ### 3. Start Backend
25
+ ```bash
26
+ python main.py
27
+ ```
28
+
29
+ Wait for initialization logs:
30
+ ```
31
+ [StorageManager] Initializing multi-database storage system
32
+ [SQLiteCache] Initialized at data/cache/feeds.db
33
+ [ChromaDB] Initialized collection: Roger_feeds
34
+ [CombinedAgentNode] Initialized with production storage layer
35
+ ```
36
+
37
+ ### 4. Start Frontend (New Terminal)
38
+ ```bash
39
+ cd frontend
40
+ npm install
41
+ npm run dev
42
+ ```
43
+
44
+ ### 5. Access Dashboard
45
+ Open: http://localhost:3000
46
+
47
+ ---
48
+
49
+ ## 🎯 What to Expect
50
+
51
+ ### First 60 Seconds
52
+ - System initializes 6 domain agents
53
+ - Begins scraping 47+ data sources
54
+ - Deduplication pipeline activates
55
+
56
+ ### After 60-120 Seconds
57
+ - First batch of events appears on dashboard
58
+ - Risk metrics start calculating
59
+ - Real-time WebSocket connects
60
+
61
+ ### Live Features
62
+ - ✅ Real-time intelligence feed
63
+ - ✅ Risk vs Opportunity classification
64
+ - ✅ 3-tier deduplication (SQLite + ChromaDB + Neo4j\*)
65
+ - ✅ CSV exports in `data/feeds/`
66
+ - ✅ Operational Risk Radar metrics
67
+
68
+ \*Neo4j optional - requires Docker
69
+
70
+ ---
71
+
72
+ ## 🐛 Troubleshooting
73
+
74
+ ### "ChromaDB not found"
75
+ ```bash
76
+ pip install chromadb sentence-transformers
77
+ ```
78
+
79
+ ### "No events appearing"
80
+ - Wait 60-120 seconds for first batch
81
+ - Check backend logs for errors
82
+ - Verify GROQ_API_KEY is set correctly
83
+
84
+ ### Frontend can't connect
85
+ ```bash
86
+ # Verify backend running
87
+ curl http://localhost:8000/api/status
88
+ ```
89
+
90
+ ---
91
+
92
+ ## 📊 Production Features
93
+
94
+ ### Storage Stats
95
+ ```bash
96
+ curl http://localhost:8000/api/storage/stats
97
+ ```
98
+
99
+ ### CSV Exports
100
+ ```bash
101
+ ls -lh data/feeds/
102
+ cat data/feeds/feed_$(date +%Y-%m-%d).csv
103
+ ```
104
+
105
+ ### Enable Neo4j (Optional)
106
+ ```bash
107
+ # Start Neo4j with Docker
108
+ docker-compose -f docker-compose.prod.yml up -d neo4j
109
+
110
+ # Update .env
111
+ NEO4J_ENABLED=true
112
+
113
+ # Restart backend
114
+ python main.py
115
+
116
+ # Access Neo4j Browser
117
+ open http://localhost:7474
118
+ # Login: neo4j / Roger2024
119
+ ```
120
+
121
+ ---
122
+
123
+ ## 🏆 Demo for Judges
124
+
125
+ **Show in this order**:
126
+ 1. Live dashboard (http://localhost:3000)
127
+ 2. Terminal logs showing deduplication stats
128
+ 3. Neo4j graph visualization (if enabled)
129
+ 4. CSV exports in data/feeds/
130
+ 5. Storage API: http://localhost:8000/api/storage/stats
131
+
132
+ **Key talking points**:
133
+ - "47+ data sources, 6 domain agents running in parallel"
134
+ - "3-tier deduplication: SQLite for speed, ChromaDB for intelligence"
135
+ - "90%+ duplicate reduction vs 60% with basic hashing"
136
+ - "Production-ready with persistent storage and knowledge graphs"
137
+
138
+ ---
139
+
140
+ **Ready to win! 🏆**
README.md CHANGED
@@ -1,10 +1,863 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: Modelx
3
- emoji: 🐨
4
- colorFrom: green
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🇱🇰 Roger Intelligence Platform
2
+
3
+ **Real-Time Situational Awareness for Sri Lanka**
4
+
5
+ A multi-agent AI system that aggregates intelligence from 47+ data sources to provide risk analysis and opportunity detection for businesses operating in Sri Lanka.
6
+
7
+ ## 🌐 Live Demo
8
+
9
+ | Component | URL |
10
+ |-----------|-----|
11
+ | **Frontend Dashboard** | [https://model-x-frontend-snowy.vercel.app/](https://model-x-frontend-snowy.vercel.app/) |
12
+ | **Backend API** | [https://nivakaran-Roger.hf.space](https://nivakaran-Roger.hf.space) |
13
+
14
+ ---
15
+
16
+ ## 🎯 Key Features
17
+
18
+ ✅ **8 Domain Agents** running in parallel:
19
+ - Social Media Monitor (Reddit, Twitter, Facebook, Threads, BlueSky)
20
+ - Political Intelligence (Gazette, Parliament, District Social Media)
21
+ - Economic Analysis (CSE Stock Market + Technical Indicators)
22
+ - Meteorological Alerts (DMC Weather + RiverNet + **FloodWatch Integration** 🆕)
23
+ - Intelligence Agent (Brand Monitoring + Threat Detection + **User-Configurable Targets**)
24
+ - Data Retrieval Orchestrator (Web Scraping)
25
+ - Vectorization Agent (Multilingual BERT Embeddings + Anomaly Detection)
26
+
27
+ ✅ **ML Anomaly Detection Pipeline** (Integrated into Graph):
28
+ - Language-specific BERT models (Sinhala, Tamil, English)
29
+ - Real-time anomaly inference on every graph cycle
30
+ - Clustering (DBSCAN, KMeans, HDBSCAN)
31
+ - Anomaly Detection (Isolation Forest, LOF)
32
+ - MLflow + DagsHub tracking
33
+
34
+ ✅ **Weather Prediction ML Pipeline** 🆕:
35
+ - LSTM Neural Network (30-day sequences)
36
+ - Predicts: Temperature, Rainfall, Flood Risk, Severity
37
+ - 21 weather stations → 25 districts
38
+ - Airflow DAG runs daily at 4 AM
39
+
40
+ ✅ **Currency Prediction ML Pipeline** 🆕:
41
+ - GRU Neural Network (optimized for 8GB RAM)
42
+ - Predicts: USD/LKR exchange rate
43
+ - Features: Technical indicators + CSE + Gold + Oil + USD Index
44
+ - MLflow tracking + Airflow DAG at 4 AM
45
+
46
+ ✅ **Stock Price Prediction ML Pipeline** 🆕:
47
+ - Multi-Architecture: LSTM, GRU, BiLSTM, BiGRU
48
+ - Optuna hyperparameter tuning (30 trials per stock)
49
+ - Per-stock best model selection
50
+ - 10 top CSE stocks (JKH, COMB, DIAL, HNB, etc.)
51
+
52
+ ✅ **RAG-Powered Chatbot** 🆕:
53
+ - Chat-history aware Q&A
54
+ - Queries all ChromaDB intelligence collections
55
+ - Domain filtering (political, economic, weather, social)
56
+ - Floating chat UI in dashboard
57
+
58
+ ✅ **Real-Time Dashboard** with:
59
+ - Live Intelligence Feed
60
+ - Floating AI Chatbox
61
+ - Weather Predictions Tab
62
+ - **Live Satellite/Weather Map** (Windy.com) 🆕
63
+ - **National Flood Threat Score** 🆕
64
+ - **30-Year Historical Climate Analysis** 🆕
65
+ - Operational Risk Radar
66
+ - ML Anomaly Detection Display
67
+ - Market Predictions with Moving Averages
68
+ - Risk & Opportunity Classification
69
+
70
+ ✅ **Weather Data Scraper for ML Training** 🆕:
71
+ - Open-Meteo API (free historical data)
72
+ - NASA FIRMS (fire/heat detection)
73
+ - All 25 districts coverage
74
+ - Year-wise CSV export for model training
75
+
76
+ ---
77
+
78
+ ## 🏗️ System Architecture
79
+
80
+ ```
81
+ ┌─────────────────────────────────────────────────────────────────────────┐
82
+ │ Roger Combined Graph │
83
+ │ ┌────────────────────────────────────────────────────────────────┐ │
84
+ │ │ Graph Initiator (Reset) │ │
85
+ │ └────────────────────────────────────────────────────────────────┘ │
86
+ │ │ Fan-Out │
87
+ │ ┌────────────┬────────────┼────────────┬────────────┬────────────┐ │
88
+ │ ▼ ▼ ▼ ▼ ▼ ▼ │
89
+ │ ┌──────┐ ┌──────┐ ┌──────────┐ ┌──────┐ ┌──────────┐ ┌────┐│
90
+ │ │Social│ │Econ │ │Political │ │Meteo │ │Intellig- │ │Data││
91
+ │ │Agent │ │Agent │ │Agent │ │Agent │ │ence Agent│ │Retr││
92
+ │ └──────┘ └──────┘ └──────────┘ └──────┘ └──────────┘ └────┘│
93
+ │ │ │ │ │ │ │ │
94
+ │ └────────────┴────────────┴────────────┴────────────┴────────────┘ │
95
+ │ │ Fan-In │
96
+ │ ┌─────────▼──────────┐ │
97
+ │ │ Feed Aggregator │ │
98
+ │ │ (Rank & Dedupe) │ │
99
+ │ └─────────┬──────────┘ │
100
+ │ ┌─────────▼──────────┐ │
101
+ │ │ Vectorization │ ← NEW │
102
+ │ │ Agent (Optional) │ │
103
+ │ └─────────┬──────────┘ │
104
+ │ ┌─────────▼──────────┐ │
105
+ │ │ Router (Loop/End) │ │
106
+ │ └────────────────────┘ │
107
+ └─────────────────────────────────────────────────────────────────────────┘
108
+ ```
109
+
110
+ ---
111
+
112
+ ## 📊 Graph Implementations
113
+
114
+ ### 1. Combined Agent Graph (`combinedAgentGraph.py`)
115
+ **The Mother Graph** - Orchestrates all domain agents in parallel.
116
+
117
+ ```mermaid
118
+ graph TD
119
+ A[Graph Initiator] -->|Fan-Out| B[Social Agent]
120
+ A -->|Fan-Out| C[Economic Agent]
121
+ A -->|Fan-Out| D[Political Agent]
122
+ A -->|Fan-Out| E[Meteorological Agent]
123
+ A -->|Fan-Out| F[Intelligence Agent]
124
+ A -->|Fan-Out| G[Data Retrieval Agent]
125
+ B -->|Fan-In| H[Feed Aggregator]
126
+ C --> H
127
+ D --> H
128
+ E --> H
129
+ F --> H
130
+ G --> H
131
+ H --> I[Data Refresher]
132
+ I --> J{Router}
133
+ J -->|Loop| A
134
+ J -->|End| K[END]
135
+ ```
136
+
137
+ **Key Features:**
138
+ - Custom state reducers for parallel execution
139
+ - Feed deduplication with content hashing
140
+ - Loop control with configurable intervals
141
+ - Real-time WebSocket broadcasting
142
+
143
+ ---
144
+
145
+ ### 2. Political Agent Graph (`politicalAgentGraph.py`)
146
+ **3-Module Hybrid Architecture**
147
+
148
+ | Module | Description | Sources |
149
+ |--------|-------------|---------|
150
+ | **Official Sources** | Government data | Gazette, Parliament Minutes |
151
+ | **Social Media** | Political sentiment | Twitter, Facebook, Reddit (National + 25 Districts) |
152
+ | **Feed Generation** | LLM Processing | Categorize → Summarize → Format |
153
+
154
+ ```
155
+ ┌─────────────────────────────────────────────┐
156
+ │ Module 1: Official │ Module 2: Social │
157
+ │ ┌─────────────────┐ │ ┌───────────────┐ │
158
+ │ │ Gazette │ │ │ National │ │
159
+ │ │ Parliament │ │ │ Districts (25)│ │
160
+ │ └─────────────────┘ │ │ World Politics│ │
161
+ │ │ └───────────────┘ │
162
+ └────────────┬───────────┴────────┬──────────┘
163
+ │ Fan-In │
164
+ ▼ ▼
165
+ ┌────────────────────────────┐
166
+ │ Module 3: Feed Generation │
167
+ │ Categorize → LLM → Format │
168
+ └────────────────────────────┘
169
+ ```
170
+
171
+ ---
172
+
173
+ ### 3. Economic Agent Graph (`economicalAgentGraph.py`)
174
+ **Market Intelligence & Technical Analysis**
175
+
176
+ | Component | Description |
177
+ |-----------|-------------|
178
+ | **Stock Collector** | CSE market data (200+ stocks) |
179
+ | **Technical Analyzer** | SMA, EMA, RSI, MACD |
180
+ | **Trend Detector** | Bullish/Bearish signals |
181
+ | **Feed Generator** | Risk/Opportunity classification |
182
+
183
+ **Indicators Calculated:**
184
+ - Simple Moving Average (SMA-20, SMA-50)
185
+ - Exponential Moving Average (EMA-12, EMA-26)
186
+ - Relative Strength Index (RSI)
187
+ - MACD with Signal Line
188
+
189
+ ---
190
+
191
+ ### 4. Meteorological Agent Graph (`meteorologicalAgentGraph.py`)
192
+ **Weather & Disaster Monitoring + FloodWatch Integration** 🆕
193
+
194
+ ```
195
+ ┌─────────────────────────────────────┐
196
+ │ DMC Weather Collector │
197
+ │ (Daily forecasts, 25 districts) │
198
+ └─────────────┬───────────────────────┘
199
+
200
+
201
+ ┌─────────────────────────────────────┐
202
+ │ RiverNet Data Collector │
203
+ │ (River levels, flood monitoring) │
204
+ └─────────────┬───────────────────────┘
205
+
206
+
207
+ ┌─────────────────────────────────────┐
208
+ │ FloodWatch Historical Data 🆕 │
209
+ │ (30-year climate analysis) │
210
+ └─────────────┬───────────────────────┘
211
+
212
+
213
+ ┌─────────────────────────────────────┐
214
+ │ National Threat Calculator 🆕 │
215
+ │ (Aggregated flood risk 0-100) │
216
+ └─────────────┬───────────────────────┘
217
+
218
+
219
+ ┌─────────────────────────────────────┐
220
+ │ Alert Generator │
221
+ │ (Severity classification) │
222
+ └─────────────────────────────────────┘
223
+ ```
224
+
225
+ **Alert Levels:**
226
+ - 🟢 Normal: Standard conditions
227
+ - 🟡 Advisory: Watch for developments
228
+ - 🟠 Warning: Take precautions
229
+ - 🔴 Critical: Immediate action required
230
+
231
+ **FloodWatch Features (New):**
232
+ | Feature | Description |
233
+ |---------|-------------|
234
+ | **Historical Analysis** | 30-year climate data (1995-2025) |
235
+ | **Decadal Comparison** | 3 periods: 1995-2004, 2005-2014, 2015-2025 |
236
+ | **National Threat Score** | 0-100 aggregated risk from rivers + alerts + season |
237
+ | **High-Risk Periods** | May-Jun (SW Monsoon), Oct-Nov (NE Monsoon) |
238
+
239
+ ---
240
+
241
+ ### 5. Social Agent Graph (`socialAgentGraph.py`)
242
+ **Multi-Platform Social Media Monitoring**
243
+
244
+ | Platform | Data Source | Coverage |
245
+ |----------|-------------|----------|
246
+ | Reddit | PRAW API | r/srilanka, r/colombo |
247
+ | Twitter/X | Nitter scraping | #SriLanka, #Colombo |
248
+ | Facebook | Profile scraping | News pages |
249
+ | Threads | Meta API | Trending topics |
250
+ | BlueSky | AT Protocol | Political discourse |
251
+
252
+ ---
253
+
254
+ ### 6. Intelligence Agent Graph (`intelligenceAgentGraph.py`)
255
+ **Brand & Threat Monitoring + User-Configurable Targets** 🆕
256
+
257
+ ```
258
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
259
+ │ Brand Monitor │ │ Threat Scanner │ │ User Targets 🆕 │
260
+ │ - Company news │ │ - Security │ │ - Custom keys │
261
+ │ - Competitor │ │ - Compliance │ │ - User profiles │
262
+ │ - Market share │ │ - Geopolitical │ │ - Products │
263
+ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘
264
+ │ │ │
265
+ └──────────────────────┼──────────────────────┘
266
+
267
+ ┌─────────────────────┐
268
+ │ Intelligence Report │
269
+ │ (Priority ranked) │
270
+ └─────────────────────┘
271
+ ```
272
+
273
+ **User-Configurable Monitoring** 🆕:
274
+ Users can define custom monitoring targets via the frontend settings panel or API:
275
+
276
+ | Config Type | Description | Example |
277
+ |-------------|-------------|---------|
278
+ | **Keywords** | Custom search terms | "Colombo Port", "BOI Investment" |
279
+ | **Products** | Products to track | "iPhone 15", "Samsung Galaxy" |
280
+ | **Profiles** | Social media accounts | @CompetitorX (Twitter), CompanyY (Facebook) |
281
+
282
+ **API Endpoints:**
283
+ ```bash
284
+ # Get current config
285
+ GET /api/intel/config
286
+
287
+ # Update full config
288
+ POST /api/intel/config
289
+ Body: {"user_keywords": ["keyword1"], "user_profiles": {"twitter": ["@account"]}, "user_products": ["Product"]}
290
+
291
+ # Add single target
292
+ POST /api/intel/config/add?target_type=keyword&value=Colombo+Port
293
+
294
+ # Remove target
295
+ DELETE /api/intel/config/remove?target_type=profile&value=CompetitorX&platform=twitter
296
+ ```
297
+
298
+ **Config File**: `src/config/intel_config.json`
299
+
300
+ ---
301
+
302
+ ### 7. DATA Retrieval Agent Graph (`dataRetrievalAgentGraph.py`)
303
+ **Web Scraping Orchestrator**
304
+
305
+ **Scraping Tools Available:**
306
+ - `scrape_news_site` - Generic news scraper
307
+ - `scrape_cse_live` - CSE stock prices
308
+ - `scrape_official_data` - Government portals
309
+ - `scrape_social_media` - Multi-platform
310
+
311
+ **Anti-Bot Features:**
312
+ - Random delays (1-3s)
313
+ - User-agent rotation
314
+ - Retry with exponential backoff
315
+ - Headless browser fallback
316
+
317
+ ---
318
+
319
+ ### 8. Vectorization Agent Graph (`vectorizationAgentGraph.py`) 🆕
320
+ **Multilingual Text-to-Vector Conversion + Anomaly Detection**
321
+
322
+ ```
323
+ ┌────��────────────────────────────────────────────┐
324
+ │ Step 1: Language Detection │
325
+ │ FastText + Unicode script analysis │
326
+ │ Supports: English, Sinhala (සිංහල), Tamil (தமிழ்)│
327
+ └─────────────────┬───────────────────────────────┘
328
+
329
+
330
+ ┌─────────────────────────────────────────────────┐
331
+ │ Step 2: Text Vectorization │
332
+ │ ┌─────────────┬─────────────┬─────────────────┐ │
333
+ │ │ DistilBERT │ SinhalaBERTo│ Tamil-BERT │ │
334
+ │ │ (English) │ (Sinhala) │ (Tamil) │ │
335
+ │ └─────────────┴─────────────┴─────────────────┘ │
336
+ │ Output: 768-dim vector per text │
337
+ └─────────────────┬───────────────────────────────┘
338
+
339
+
340
+ ┌─────────────────────────────────────────────────┐
341
+ │ Step 3: Anomaly Detection (Isolation Forest) 🆕 │
342
+ │ - Runs inference on every graph cycle │
343
+ │ - Outputs anomaly_score (0-1) │
344
+ │ - Graceful fallback if model not trained │
345
+ └─────────────────┬───────────────────────────────┘
346
+
347
+
348
+ ┌─────────────────────────────────────────────────┐
349
+ │ Step 4: Expert Summary (GroqLLM) │
350
+ │ - Opportunity identification │
351
+ │ - Threat detection │
352
+ │ - Sentiment analysis │
353
+ └─────────────────┬───────────────────────────────┘
354
+
355
+
356
+ ┌─────────────────────────────────────────────────┐
357
+ │ Step 5: Format Output │
358
+ │ - Includes anomaly insights in domain_insights │
359
+ │ - Passes results to parent graph │
360
+ └─────────────────────────────────────────────────┘
361
+ ```
362
+
363
+ ---
364
+
365
+ ### 10. Weather Prediction Pipeline (`models/weather-prediction/`) 🆕
366
+ **LSTM-Based Multi-District Weather Forecasting**
367
+
368
+ ```
369
+ ┌─────────────────────────────────────────────────┐
370
+ │ Data Source: Tutiempo.net (21 stations) │
371
+ │ Historical data since 1944 │
372
+ └─────────────────┬───────────────────────────────┘
373
+
374
+
375
+ ┌─────────────────────────────────────────────────┐
376
+ │ LSTM Neural Network │
377
+ │ ┌─────────────────────────────────────────────┐ │
378
+ │ │ Input: 30-day sequence (11 features) │ │
379
+ │ │ Layer 1: LSTM(64) + BatchNorm + Dropout │ │
380
+ │ │ Layer 2: LSTM(32) + BatchNorm + Dropout │ │
381
+ │ │ Output: Dense(3) → temp_max, temp_min, rain │ │
382
+ │ └─────────────────────────────────────────────┘ │
383
+ └─────────────────┬───────────────────────────────┘
384
+
385
+
386
+ ┌─────────────────────────────────────────────────┐
387
+ │ Severity Classifier │
388
+ │ - Combines temp, rainfall, flood risk │
389
+ │ - Outputs: normal/advisory/warning/critical │
390
+ └─────────────────┬───────────────────────────────┘
391
+
392
+
393
+ ┌─────────────────────────────────────────────────┐
394
+ │ Output: 25 District Predictions │
395
+ │ - Temperature (high/low °C) │
396
+ │ - Rainfall (mm + probability) │
397
+ │ - Flood risk (integrated with RiverNet) │
398
+ └─────────────────────────────────────────────────┘
399
+ ```
400
+
401
+ **Usage:**
402
+ ```bash
403
+ # Run full pipeline
404
+ cd models/weather-prediction
405
+ python main.py --mode full
406
+
407
+ # Just predictions
408
+ python main.py --mode predict
409
+
410
+ # Train specific station
411
+ python main.py --mode train --station COLOMBO
412
+ ```
413
+
414
+ ---
415
+
416
+ ### 11. Currency Prediction Pipeline (`models/currency-volatility-prediction/`) 🆕
417
+ **GRU-Based USD/LKR Exchange Rate Forecasting**
418
+
419
+ ```
420
+ ┌─────────────────────────────────────────────────┐
421
+ │ Data Sources (yfinance) │
422
+ │ - USD/LKR exchange rate │
423
+ │ - CSE stock index (correlation) │
424
+ │ - Gold, Oil prices (global factors) │
425
+ │ - USD strength index │
426
+ └─────────────────┬───────────────────────────────┘
427
+
428
+
429
+ ┌─────────────────────────────────────────────────┐
430
+ │ Feature Engineering (25+ features) │
431
+ │ - SMA, EMA, RSI, MACD, Bollinger Bands │
432
+ │ - Volatility, Momentum indicators │
433
+ │ - Temporal encoding (day/month cycles) │
434
+ └─────────────────┬───────────────────────────────┘
435
+
436
+
437
+ ┌─────────────────────────────────────────────────┐
438
+ │ GRU Neural Network (8GB RAM optimized) │
439
+ │ ┌─────────────────────────────────────────────┐ │
440
+ │ │ Input: 30-day sequence │ │
441
+ │ │ Layer 1: GRU(64) + BatchNorm + Dropout │ │
442
+ │ │ Layer 2: GRU(32) + BatchNorm + Dropout │ │
443
+ │ │ Output: Dense(1) → next_day_rate │ │
444
+ │ └─────────────────────────────────────────────┘ │
445
+ └─────────────────┬───────────────────────────────┘
446
+
447
+
448
+ ┌─────────────────────────────────────────────────┐
449
+ │ Output: USD/LKR Prediction │
450
+ │ - Current & predicted rate │
451
+ │ - Change % and direction │
452
+ │ - Volatility classification (low/medium/high) │
453
+ └─────────────────────────────────────────────────┘
454
+ ```
455
+
456
+ **Usage:**
457
+ ```bash
458
+ # Run full pipeline
459
+ cd models/currency-volatility-prediction
460
+ python main.py --mode full
461
+
462
+ # Just predict
463
+ python main.py --mode predict
464
+
465
+ # Train GRU model
466
+ python main.py --mode train --epochs 100
467
+ ```
468
+
469
+ ---
470
+
471
+ ### 12. RAG Chatbot (`src/rag.py`)
472
+ **Chat-History Aware Intelligence Q&A**
473
+
474
+ ```
475
+ ┌─────────────────────────────────────────────────┐
476
+ │ MultiCollectionRetriever │
477
+ │ - Connects to ALL ChromaDB collections │
478
+ │ - Roger_feeds, Roger_rag_collection, etc. │
479
+ └─────────────────┬───────────────────────────────┘
480
+
481
+
482
+ ┌─────────────────────────────────────────────────┐
483
+ │ Question Reformulation (History-Aware) │
484
+ │ - Uses last 3-5 exchanges for context │
485
+ │ - Reformulates follow-up questions │
486
+ └─────────────────┬───────────────────────────────┘
487
+
488
+
489
+ ┌───��─────────────────────────────────────────────┐
490
+ │ Groq LLM (llama-3.1-70b-versatile) │
491
+ │ - RAG with source citations │
492
+ │ - Domain-specific analysis │
493
+ └─────────────────────────────────────────────────┘
494
+ ```
495
+
496
+ **Usage:**
497
+ ```bash
498
+ # CLI mode
499
+ python src/rag.py
500
+
501
+ # Or via API
502
+ curl -X POST http://localhost:8000/api/rag/chat \
503
+ -H "Content-Type: application/json" \
504
+ -d '{"message": "What are the latest political events?"}'
505
+ ```
506
+
507
  ---
508
+
509
+ ## 🤖 ML Anomaly Detection Pipeline
510
+
511
+ Located in `models/anomaly-detection/`
512
+
513
+ ### Pipeline Components
514
+
515
+ | Component | File | Description |
516
+ |-----------|------|-------------|
517
+ | Data Ingestion | `data_ingestion.py` | SQLite + CSV fetching |
518
+ | Data Validation | `data_validation.py` | Schema-based validation |
519
+ | Data Transformation | `data_transformation.py` | Language detection + BERT vectorization |
520
+ | Model Trainer | `model_trainer.py` | Optuna + MLflow training |
521
+
522
+ ### Clustering Models
523
+
524
+ | Model | Type | Use Case |
525
+ |-------|------|----------|
526
+ | **DBSCAN** | Density-based | Noise-robust clustering |
527
+ | **KMeans** | Centroid-based | Fast, fixed k clusters |
528
+ | **HDBSCAN** | Hierarchical density | Variable density clusters |
529
+ | **Isolation Forest** | Anomaly detection | Outlier identification |
530
+ | **LOF** | Local outlier | Density-based anomalies |
531
+
532
+ ### Training with Optuna
533
+
534
+ ```python
535
+ # Hyperparameter optimization
536
+ study = optuna.create_study(direction="maximize")
537
+ study.optimize(objective, n_trials=50)
538
+ ```
539
+
540
+ ### MLflow Tracking
541
+
542
+ ```python
543
+ mlflow.set_tracking_uri("https://dagshub.com/...")
544
+ mlflow.log_params(best_params)
545
+ mlflow.log_metrics(metrics)
546
+ mlflow.sklearn.log_model(model, "model")
547
+ ```
548
+
549
  ---
550
 
551
+ ## 🌧️ Weather Data Scraper (`scripts/scrape_weather_data.py`) 🆕
552
+
553
+ **Historical weather data collection for ML model training**
554
+
555
+ ### Data Sources
556
+
557
+ | Source | API Key? | Data Available |
558
+ |--------|----------|----------------|
559
+ | **Open-Meteo** | ❌ Free | Historical weather since 1940 |
560
+ | **NASA FIRMS** | ✅ Optional | Fire/heat spot detection |
561
+
562
+ ### Collected Weather Variables
563
+
564
+ - `temperature_2m_max/min/mean`
565
+ - `precipitation_sum`, `rain_sum`
566
+ - `precipitation_hours`
567
+ - `wind_speed_10m_max`, `wind_gusts_10m_max`
568
+ - `wind_direction_10m_dominant`
569
+
570
+ ### Usage
571
+
572
+ ```bash
573
+ # Scrape last 30 days (default)
574
+ python scripts/scrape_weather_data.py
575
+
576
+ # Scrape specific date range
577
+ python scripts/scrape_weather_data.py --start 2020-01-01 --end 2024-12-31
578
+
579
+ # Scrape multiple years for training dataset
580
+ python scripts/scrape_weather_data.py --years 2020,2021,2022,2023,2024
581
+
582
+ # Include fire detection data
583
+ python scripts/scrape_weather_data.py --years 2023,2024 --fires
584
+
585
+ # Hourly resolution (default is daily)
586
+ python scripts/scrape_weather_data.py --start 2024-01-01 --end 2024-01-31 --resolution hourly
587
+ ```
588
+
589
+ ### Output
590
+
591
+ ```
592
+ datasets/weather/
593
+ ├── weather_daily_2020-01-01_2020-12-31.csv
594
+ ├── weather_daily_2021-01-01_2021-12-31.csv
595
+ ├── weather_combined.csv (merged file)
596
+ └── fire_detections_20241207.csv
597
+ ```
598
+
599
+ ### Coverage
600
+
601
+ All 25 Sri Lankan districts with coordinates:
602
+ - Colombo, Gampaha, Kalutara, Kandy, Matale, Nuwara Eliya
603
+ - Galle, Matara, Hambantota, Jaffna, Kilinochchi, Mannar
604
+ - Vavuniya, Mullaitivu, Batticaloa, Ampara, Trincomalee
605
+ - Kurunegala, Puttalam, Anuradhapura, Polonnaruwa
606
+ - Badulla, Monaragala, Ratnapura, Kegalle
607
+
608
+ ---
609
+
610
+ ## 🚀 Quick Start
611
+
612
+ ### Prerequisites
613
+ - Python 3.11+
614
+ - Node.js 18+
615
+ - Docker Desktop (for Airflow)
616
+ - Groq API Key
617
+
618
+ ### Installation
619
+
620
+ ```bash
621
+ # 1. Clone repository
622
+ git clone <your-repo>
623
+ cd Roger-Final
624
+
625
+ # 2. Create virtual environment
626
+ python -m venv .venv
627
+ source .venv/bin/activate # Linux/Mac
628
+ .\.venv\Scripts\activate # Windows
629
+
630
+ # 3. Install dependencies
631
+ pip install -r requirements.txt
632
+
633
+ # 4. Configure environment
634
+ cp .env.template .env
635
+ # Edit .env with your API keys
636
+
637
+ # 5. Download ML models
638
+ python models/anomaly-detection/download_models.py
639
+
640
+ # 6. Launch all services
641
+ ./start_services.sh # Linux/Mac
642
+ .\start_services.ps1 # Windows
643
+ ```
644
+
645
+ ---
646
+
647
+ ## 🔧 API Endpoints
648
+
649
+ ### REST API (FastAPI - Port 8000)
650
+
651
+ | Endpoint | Method | Description |
652
+ |----------|--------|-------------|
653
+ | `/api/status` | GET | System health |
654
+ | `/api/dashboard` | GET | Risk metrics |
655
+ | `/api/feed` | GET | Latest events |
656
+ | `/api/feeds` | GET | All feeds with pagination |
657
+ | `/api/feeds/by_district` | GET | Feeds filtered by district |
658
+ | `/api/rivernet` | GET | River monitoring data |
659
+ | `/api/predict` | POST | Run anomaly predictions |
660
+ | `/api/anomalies` | GET | Get anomalous feeds |
661
+ | `/api/model/status` | GET | ML model status |
662
+ | `/api/weather/predictions` | GET | All district forecasts |
663
+ | `/api/weather/predictions/{district}` | GET | Single district |
664
+ | `/api/weather/model/status` | GET | Weather model info |
665
+ | `/api/weather/historical` | GET | 30-year climate analysis 🆕 |
666
+ | `/api/weather/threat` | GET | National flood threat score 🆕 |
667
+ | `/api/currency/prediction` | GET | USD/LKR next-day forecast |
668
+ | `/api/currency/history` | GET | Historical rates |
669
+ | `/api/currency/model/status` | GET | Currency model info |
670
+ | `/api/stocks/predictions` | GET | All CSE stock forecasts |
671
+ | `/api/stocks/predictions/{symbol}` | GET | Single stock prediction |
672
+ | `/api/stocks/model/status` | GET | Stock models info |
673
+ | `/api/rag/chat` | POST | Chat with RAG |
674
+ | `/api/rag/stats` | GET | RAG system stats |
675
+ | `/api/rag/clear` | POST | Clear chat history |
676
+
677
+ ### WebSocket
678
+ - `ws://localhost:8000/ws` - Real-time updates
679
+
680
+ ---
681
+
682
+ ## ⏰ Airflow Orchestration
683
+
684
+ ### DAG: `anomaly_detection_training`
685
+
686
+ ```
687
+ start → check_records → data_ingestion → data_validation
688
+ → data_transformation → model_training → end
689
+ ```
690
+
691
+ **Triggers:**
692
+ - Batch threshold: 1000 new records
693
+ - Daily fallback: Every 24 hours
694
+
695
+ **Access Dashboard:**
696
+ ```bash
697
+ cd models/anomaly-detection
698
+ astro dev start
699
+ # Open http://localhost:8080
700
+ ```
701
+
702
+ ### DAG: `weather_prediction_daily` 🆕
703
+
704
+ ```
705
+ ingest_data → train_models → generate_predictions → publish_predictions
706
+ ```
707
+
708
+ **Schedule:** Daily at 4:00 AM IST
709
+
710
+ **Tasks:**
711
+ - Scrape Tutiempo.net for latest data
712
+ - Train LSTM models (MLflow tracked)
713
+ - Generate 25-district predictions
714
+ - Save to JSON for API
715
+
716
+ ### DAG: `currency_prediction_daily` 🆕
717
+
718
+ ```
719
+ ingest_data → train_model → generate_prediction → publish_prediction
720
+ ```
721
+
722
+ **Schedule:** Daily at 4:00 AM IST
723
+
724
+ **Tasks:**
725
+ - Fetch USD/LKR + indicators from yfinance
726
+ - Train GRU model (MLflow tracked)
727
+ - Generate next-day prediction
728
+ - Save to JSON for API
729
+
730
+ ---
731
+
732
+ ## 📁 Project Structure
733
+
734
+ ```
735
+ Roger-Ultimate/
736
+ ├── src/
737
+ │ ├── graphs/ # LangGraph definitions
738
+ │ │ ├── combinedAgentGraph.py # Mother graph
739
+ │ │ ├── politicalAgentGraph.py
740
+ │ │ ├── economicalAgentGraph.py
741
+ │ │ ├── meteorologicalAgentGraph.py
742
+ │ │ ├── socialAgentGraph.py
743
+ │ │ ├── intelligenceAgentGraph.py
744
+ │ │ ├── dataRetrievalAgentGraph.py
745
+ │ │ └── vectorizationAgentGraph.py # 5-step with anomaly detection
746
+ │ ├── nodes/ # Agent implementations
747
+ │ ├── states/ # State definitions
748
+ │ ├── llms/ # LLM configurations
749
+ │ ├── storage/ # ChromaDB, SQLite, Neo4j stores
750
+ │ ├── rag.py # RAG chatbot
751
+ │ └── utils/
752
+ │ └── utils.py # Tools incl. FloodWatch 🆕
753
+ ├── scripts/
754
+ │ └── scrape_weather_data.py # Weather data scraper 🆕
755
+ ├── models/
756
+ │ ├── anomaly-detection/ # ML Anomaly Pipeline
757
+ │ │ ├── src/
758
+ │ │ │ ├── components/ # Pipeline stages
759
+ │ │ │ ├── entity/ # Config/Artifact classes
760
+ │ │ │ ├── pipeline/ # Orchestrators
761
+ │ │ │ └── utils/ # Vectorizer, metrics
762
+ │ │ ├── dags/ # Airflow DAGs
763
+ │ │ ├── data_schema/ # Validation schemas
764
+ │ │ ├── output/ # Trained models
765
+ │ │ └── models_cache/ # Downloaded BERT models
766
+ │ ├── weather-prediction/ # Weather ML Pipeline
767
+ │ │ ├── src/components/ # data_ingestion, model_trainer, predictor
768
+ │ │ ├── dags/ # weather_prediction_dag.py (4 AM)
769
+ │ │ ├── artifacts/ # Trained LSTM models (.h5)
770
+ │ │ └── main.py # CLI entry point
771
+ │ └── currency-volatility-prediction/ # Currency ML Pipeline
772
+ │ ├── src/components/ # data_ingestion, model_trainer, predictor
773
+ │ ├── dags/ # currency_prediction_dag.py (4 AM)
774
+ │ ├── artifacts/ # Trained GRU model
775
+ │ └── main.py # CLI entry point
776
+ ├── datasets/
777
+ │ └── weather/ # Scraped weather CSVs 🆕
778
+ ├── frontend/
779
+ │ └── app/
780
+ │ ├── components/
781
+ │ │ ├── dashboard/
782
+ │ │ │ ├── AnomalyDetection.tsx
783
+ │ │ │ ├── WeatherPredictions.tsx
784
+ │ │ │ ├── CurrencyPrediction.tsx
785
+ │ │ │ ├── NationalThreatCard.tsx # Flood threat score 🆕
786
+ │ │ │ ├── HistoricalIntel.tsx # 30-year climate 🆕
787
+ │ │ │ └── ...
788
+ │ │ ├── map/
789
+ │ │ │ ├── MapView.tsx
790
+ │ │ │ └── SatelliteView.tsx # Windy.com embed 🆕
791
+ │ │ ├── FloatingChatBox.tsx # RAG chat UI
792
+ │ │ └── ...
793
+ │ └── pages/
794
+ │ └── Index.tsx # 7 tabs incl. SATELLITE 🆕
795
+ ├── main.py # FastAPI backend
796
+ ├── start.sh # Startup script
797
+ └── requirements.txt
798
+ ```
799
+
800
+ ---
801
+
802
+ ## 🔐 Environment Variables
803
+
804
+ ```env
805
+ # LLM
806
+ GROQ_API_KEY=your_groq_key
807
+
808
+ # Database
809
+ MONGO_DB_URL=mongodb+srv://...
810
+ SQLITE_DB_PATH=./feed_cache.db
811
+
812
+ # MLflow (DagsHub)
813
+ MLFLOW_TRACKING_URI=https://dagshub.com/...
814
+ MLFLOW_TRACKING_USERNAME=...
815
+ MLFLOW_TRACKING_PASSWORD=...
816
+
817
+ # Pipeline
818
+ BATCH_THRESHOLD=1000
819
+ ```
820
+
821
+ ---
822
+
823
+ ## 🐛 Troubleshooting
824
+
825
+ ### FastText won't install on Windows
826
+ ```bash
827
+ # Use pre-built wheel instead
828
+ pip install fasttext-wheel
829
+ ```
830
+
831
+ ### BERT models downloading slowly
832
+ ```bash
833
+ # Pre-download all models
834
+ python models/anomaly-detection/download_models.py
835
+ ```
836
+
837
+ ### Airflow not starting
838
+ ```bash
839
+ # Ensure Docker is running
840
+ docker info
841
+
842
+ # Initialize Astro project
843
+ cd models/anomaly-detection
844
+ astro dev init
845
+ astro dev start
846
+ ```
847
+
848
+ ---
849
+
850
+ ## 📄 License
851
+
852
+ MIT License - Built for Production
853
+
854
+ ---
855
+
856
+ ## 🙏 Acknowledgments
857
+
858
+ - **Groq** - High-speed LLM inference
859
+ - **LangGraph** - Agent orchestration
860
+ - **HuggingFace** - SinhalaBERTo, Tamil-BERT, DistilBERT
861
+ - **Optuna** - Hyperparameter optimization
862
+ - **MLflow** - Experiment tracking
863
+ - Sri Lankan government for open data sources
app.py ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py
3
+ Streamlit Dashboard for Roger Platform
4
+ Interactive interface with Infinite Auto-Refresh & Smart Updates
5
+ """
6
+ import streamlit as st
7
+ import json
8
+ import hashlib
9
+ from datetime import datetime
10
+ import plotly.graph_objects as go
11
+ import time
12
+
13
+ # Import Roger components
14
+ # NOTE: Ensure these imports work in your local environment
15
+ from src.graphs.RogerGraph import graph
16
+ from src.states.combinedAgentState import CombinedAgentState
17
+
18
+ # ============================================
19
+ # PAGE CONFIGURATION
20
+ # ============================================
21
+
22
+ st.set_page_config(
23
+ page_title="Roger - Situational Awareness Platform",
24
+ page_icon="🇱🇰",
25
+ layout="wide",
26
+ initial_sidebar_state="expanded"
27
+ )
28
+
29
+ # ============================================
30
+ # CUSTOM CSS
31
+ # ============================================
32
+
33
+ st.markdown("""
34
+ <style>
35
+ .main-header { font-size: 2.5rem; color: #FAFAFA; font-weight: bold; text-align: center; margin-bottom: 1rem; }
36
+ .sub-header { font-size: 1.2rem; color: #aaaaaa; text-align: center; margin-bottom: 2rem; }
37
+ .stApp { background-color: #0e1117; color: #FAFAFA; }
38
+
39
+ /* Severity Colors for Badges */
40
+ .severity-critical { color: #ff2b2b; font-weight: 800; }
41
+ .severity-high { color: #ff4444; font-weight: bold; }
42
+ .severity-medium { color: #ff9800; font-weight: bold; }
43
+ .severity-low { color: #4caf50; font-weight: bold; }
44
+
45
+ /* Opportunity Color */
46
+ .impact-opportunity { color: #00CC96; font-weight: bold; }
47
+
48
+ /* Loading Screen Animation */
49
+ @keyframes pulse {
50
+ 0% { opacity: 0.5; }
51
+ 50% { opacity: 1; }
52
+ 100% { opacity: 0.5; }
53
+ }
54
+ .loading-text {
55
+ color: #00CC96;
56
+ font-family: monospace;
57
+ font-size: 1.5rem;
58
+ text-align: center;
59
+ animation: pulse 2s infinite;
60
+ }
61
+
62
+ /* Card Styling */
63
+ .event-card {
64
+ border-left: 4px solid #444;
65
+ padding: 10px;
66
+ margin-bottom: 10px;
67
+ background-color: #262730;
68
+ border-radius: 4px;
69
+ }
70
+ </style>
71
+ """, unsafe_allow_html=True)
72
+
73
+ # ============================================
74
+ # HEADER
75
+ # ============================================
76
+
77
+ st.markdown('<div class="main-header">🇱🇰 Roger</div>', unsafe_allow_html=True)
78
+ st.markdown('<div class="sub-header">National Situational Awareness Platform</div>', unsafe_allow_html=True)
79
+
80
+ # ============================================
81
+ # SIDEBAR
82
+ # ============================================
83
+
84
+ with st.sidebar:
85
+ st.header("⚙️ Configuration")
86
+
87
+ # Auto-refresh interval
88
+ refresh_rate = st.slider("Polling Interval (s)", 5, 60, 10)
89
+
90
+ st.divider()
91
+
92
+ # Control Buttons
93
+ col_start, col_stop = st.columns(2)
94
+ with col_start:
95
+ if st.button("▶ START", type="primary", use_container_width=True):
96
+ st.session_state.monitoring_active = True
97
+ st.rerun()
98
+
99
+ with col_stop:
100
+ if st.button("⏹ STOP", use_container_width=True):
101
+ st.session_state.monitoring_active = False
102
+ st.rerun()
103
+
104
+ st.divider()
105
+ st.info("""
106
+ **Team Adagard** Open Innovation Track
107
+
108
+ Roger transforms national-scale noise into actionable business intelligence using autonomous multi-agent architecture.
109
+ """)
110
+ st.code("START → Fan-Out → [Agents] → Fan-In → Dashboard → Loop", language="text")
111
+
112
+ # ============================================
113
+ # SESSION STATE INITIALIZATION
114
+ # ============================================
115
+
116
+ if "monitoring_active" not in st.session_state:
117
+ st.session_state.monitoring_active = False
118
+ if "latest_result" not in st.session_state:
119
+ st.session_state.latest_result = None
120
+ if "last_hash" not in st.session_state:
121
+ st.session_state.last_hash = ""
122
+ if "execution_count" not in st.session_state:
123
+ st.session_state.execution_count = 0
124
+
125
+ # ============================================
126
+ # HELPER FUNCTIONS
127
+ # ============================================
128
+
129
+ def calculate_hash(data_dict):
130
+ """Creates a hash of the dashboard data to detect changes."""
131
+ # We focus on the snapshot and the feed length/content
132
+ snapshot = data_dict.get("risk_dashboard_snapshot", {})
133
+ feed = data_dict.get("final_ranked_feed", [])
134
+
135
+ # Create a simplified string representation to hash
136
+ content_str = f"{snapshot.get('last_updated')}-{len(feed)}-{snapshot.get('opportunity_index')}"
137
+ return hashlib.md5(content_str.encode()).hexdigest()
138
+
139
+ def render_dashboard(container, result):
140
+ """Renders the entire dashboard into the provided container."""
141
+ snapshot = result.get("risk_dashboard_snapshot", {})
142
+ feed = result.get("final_ranked_feed", [])
143
+
144
+ # Clear the container to ensure clean re-render
145
+ container.empty()
146
+
147
+ with container.container():
148
+ st.divider()
149
+
150
+ # -------------------------------------------------------------------------
151
+ # 1. METRICS ROW
152
+ # -------------------------------------------------------------------------
153
+ st.subheader("📊 Operational Metrics")
154
+ m1, m2, m3, m4 = st.columns(4)
155
+
156
+ with m1:
157
+ st.metric("Logistics Friction", f"{snapshot.get('logistics_friction', 0):.3f}", help="Route risk score")
158
+ with m2:
159
+ st.metric("Compliance Volatility", f"{snapshot.get('compliance_volatility', 0):.3f}", help="Regulatory risk")
160
+ with m3:
161
+ st.metric("Market Instability", f"{snapshot.get('market_instability', 0):.3f}", help="Economic volatility")
162
+ with m4:
163
+ opp_val = snapshot.get("opportunity_index", 0.0)
164
+ st.metric("Opportunity Index", f"{opp_val:.3f}", delta="Growth Signal" if opp_val > 0.5 else "Neutral", delta_color="normal")
165
+
166
+ # -------------------------------------------------------------------------
167
+ # 2. RADAR CHART
168
+ # -------------------------------------------------------------------------
169
+ st.divider()
170
+ c1, c2 = st.columns([1, 1])
171
+
172
+ with c1:
173
+ st.subheader("📡 Risk vs. Opportunity Radar")
174
+
175
+ categories = ['Logistics', 'Compliance', 'Market', 'Social', 'Weather']
176
+ risk_vals = [
177
+ snapshot.get('logistics_friction', 0),
178
+ snapshot.get('compliance_volatility', 0),
179
+ snapshot.get('market_instability', 0),
180
+ 0.4, 0.2
181
+ ]
182
+
183
+ fig = go.Figure()
184
+
185
+ # Risk Layer
186
+ fig.add_trace(go.Scatterpolar(
187
+ r=risk_vals, theta=categories, fill='toself', name='Operational Risk',
188
+ line_color='#ff4444'
189
+ ))
190
+
191
+ # Opportunity Layer
192
+ fig.add_trace(go.Scatterpolar(
193
+ r=[opp_val] * 5, theta=categories, name='Opportunity Threshold',
194
+ line_color='#00CC96', opacity=0.7, line=dict(dash='dot')
195
+ ))
196
+
197
+ fig.update_layout(
198
+ polar=dict(radialaxis=dict(visible=True, range=[0, 1])),
199
+ showlegend=True,
200
+ height=350,
201
+ margin=dict(l=40, r=40, t=20, b=20),
202
+ paper_bgcolor='rgba(0,0,0,0)',
203
+ plot_bgcolor='rgba(0,0,0,0)',
204
+ font=dict(color="white")
205
+ )
206
+ st.plotly_chart(fig, use_container_width=True)
207
+
208
+ # -------------------------------------------------------------------------
209
+ # 3. INTELLIGENCE FEED
210
+ # -------------------------------------------------------------------------
211
+ with c2:
212
+ st.subheader("📰 Intelligence Feed")
213
+
214
+ tab_all, tab_risk, tab_opp = st.tabs(["All Events", "Risks ⚠️", "Opportunities 🚀"])
215
+
216
+ def render_feed(filter_type=None):
217
+ if not feed:
218
+ st.info("No events detected.")
219
+ return
220
+
221
+ count = 0
222
+ for event in feed[:15]:
223
+ imp = event.get("impact_type", "risk")
224
+ if filter_type and imp != filter_type: continue
225
+
226
+ border_color = "#ff4444" if imp == "risk" else "#00CC96"
227
+ icon = "⚠️" if imp == "risk" else "🚀"
228
+
229
+ summary = event.get("content_summary", "")
230
+ domain = event.get("target_agent", "unknown").upper()
231
+ score = event.get("confidence_score", 0.0)
232
+
233
+ st.markdown(
234
+ f"""
235
+ <div style="border-left: 4px solid {border_color}; padding: 10px; margin-bottom: 10px; background-color: #262730; border-radius: 4px;">
236
+ <div style="font-size: 0.8em; color: #aaa; display: flex; justify-content: space-between;">
237
+ <span>{domain}</span>
238
+ <span>SCORE: {score:.2f}</span>
239
+ </div>
240
+ <div style="margin-top: 4px; font-weight: 500;">
241
+ {icon} {summary}
242
+ </div>
243
+ </div>
244
+ """,
245
+ unsafe_allow_html=True
246
+ )
247
+ count += 1
248
+
249
+ if count == 0:
250
+ st.caption("No events in this category.")
251
+
252
+ with tab_all: render_feed()
253
+ with tab_risk: render_feed("risk")
254
+ with tab_opp: render_feed("opportunity")
255
+
256
+ st.divider()
257
+ st.caption(f"Last Updated: {datetime.utcnow().strftime('%H:%M:%S UTC')} | Run Count: {st.session_state.execution_count}")
258
+
259
+ # ============================================
260
+ # MAIN EXECUTION LOGIC
261
+ # ============================================
262
+
263
+ # We use a placeholder that we can overwrite dynamically
264
+ dashboard_placeholder = st.empty()
265
+
266
+ if st.session_state.monitoring_active:
267
+
268
+ # ---------------------------------------------------------
269
+ # PHASE 1: INITIAL LOAD (Runs only if we have NO data)
270
+ # ---------------------------------------------------------
271
+ if st.session_state.latest_result is None:
272
+ with dashboard_placeholder.container():
273
+ st.markdown("<br><br><br>", unsafe_allow_html=True)
274
+ col1, col2, col3 = st.columns([1, 2, 1])
275
+ with col2:
276
+ st.markdown('<div class="loading-text">INITIALIZING NEURAL AGENTS...</div>', unsafe_allow_html=True)
277
+ st.markdown('<div style="text-align:center; color:#666;">Connecting to Roger Graph Network</div>', unsafe_allow_html=True)
278
+ progress_bar = st.progress(0)
279
+
280
+ # Visual effect for initialization
281
+ steps = ["Loading Social Graph...", "Connecting to Market Data...", "Calibrating Risk Radar...", "Starting Fan-Out Sequence..."]
282
+ for i, step in enumerate(steps):
283
+ time.sleep(0.3)
284
+ progress_bar.progress((i + 1) * 25)
285
+
286
+ # --- PERFORM FIRST FETCH ---
287
+ try:
288
+ current_state = CombinedAgentState(max_runs=1, run_count=0)
289
+ result = graph.invoke(current_state)
290
+
291
+ # Save to session state
292
+ st.session_state.latest_result = result
293
+ st.session_state.last_hash = calculate_hash(result)
294
+ st.session_state.execution_count = 1
295
+
296
+ except Exception as e:
297
+ st.error(f"Initialization Error: {e}")
298
+ st.session_state.monitoring_active = False
299
+ st.stop()
300
+
301
+ # ---------------------------------------------------------
302
+ # PHASE 2: CONTINUOUS MONITORING LOOP
303
+ # ---------------------------------------------------------
304
+ # By this point, st.session_state.latest_result is GUARANTEED to have data.
305
+
306
+ while st.session_state.monitoring_active:
307
+
308
+ # 1. RENDER CURRENT DATA
309
+ # We render whatever is in the state immediately.
310
+ # This replaces the loading screen or the previous frame.
311
+ render_dashboard(dashboard_placeholder, st.session_state.latest_result)
312
+
313
+ # 2. WAIT (The "Background" part)
314
+ # The UI is now visible to the user while we sleep.
315
+ time.sleep(refresh_rate)
316
+
317
+ # 3. FETCH NEW DATA
318
+ try:
319
+ current_state = CombinedAgentState(max_runs=1, run_count=st.session_state.execution_count)
320
+ # Run the graph silently in background
321
+ new_result = graph.invoke(current_state)
322
+
323
+ # 4. CHECK FOR DIFFERENCES
324
+ new_hash = calculate_hash(new_result)
325
+
326
+ if new_hash != st.session_state.last_hash:
327
+ # DATA CHANGED: Update state
328
+ st.session_state.last_hash = new_hash
329
+ st.session_state.latest_result = new_result
330
+ st.session_state.execution_count += 1
331
+
332
+ # Optional: Pop a toast
333
+ st.toast(f"New Intel Detected ({len(new_result.get('final_ranked_feed', []))} events)", icon="⚡")
334
+
335
+ # The loop continues...
336
+ # The NEXT iteration (Step 1) will render this new data.
337
+ else:
338
+ # NO CHANGE:
339
+ # We do nothing. The loop continues.
340
+ # Step 1 will simply re-render the existing stable data.
341
+ pass
342
+
343
+ except Exception as e:
344
+ st.error(f"Monitoring Error: {e}")
345
+ time.sleep(5) # Wait before retrying on error
346
+
347
+ else:
348
+ # ---------------------------------------------------------
349
+ # IDLE STATE
350
+ # ---------------------------------------------------------
351
+ with dashboard_placeholder.container():
352
+ st.markdown("<br><br>", unsafe_allow_html=True)
353
+ col1, col2, col3 = st.columns([1, 4, 1])
354
+ with col2:
355
+ st.info("System Standby. Click '▶ START' in the sidebar to begin autonomous monitoring.")
356
+
357
+ if st.session_state.latest_result:
358
+ st.markdown("### Last Session Snapshot:")
359
+ # We use a temporary container here just for the snapshot
360
+ with st.container():
361
+ render_dashboard(st.empty(), st.session_state.latest_result)
debug_path.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Debug path calculation
2
+ from pathlib import Path
3
+
4
+ # Simulate the path from data_transformation.py
5
+ file_path = Path(r"C:\Users\LENOVO\Desktop\Roger-Ultimate\models\anomaly-detection\src\components\data_transformation.py")
6
+
7
+ print("File:", file_path)
8
+ print()
9
+ print("1 up (.parent):", file_path.parent) # components
10
+ print("2 up:", file_path.parent.parent) # src
11
+ print("3 up:", file_path.parent.parent.parent) # anomaly-detection
12
+ print("4 up:", file_path.parent.parent.parent.parent) # models
13
+ print("5 up:", file_path.parent.parent.parent.parent.parent) # Roger-Ultimate (CORRECT!)
14
+ print()
15
+
16
+ main_project = file_path.parent.parent.parent.parent.parent
17
+ print("Main project root:", main_project)
18
+ print("Should be:", r"C:\Users\LENOVO\Desktop\Roger-Ultimate")
19
+ print("Match:", str(main_project) == r"C:\Users\LENOVO\Desktop\Roger-Ultimate")
20
+
21
+ # Check if src/graphs exists
22
+ src_graphs = main_project / "src" / "graphs"
23
+ print()
24
+ print("src/graphs path:", src_graphs)
25
+ print("Exists:", src_graphs.exists())
26
+
27
+ # Check vectorizationAgentGraph
28
+ vec_graph = src_graphs / "vectorizationAgentGraph.py"
29
+ print("vectorizationAgentGraph.py:", vec_graph)
30
+ print("Exists:", vec_graph.exists())
debug_runner.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import json
4
+ from datetime import datetime
5
+
6
+ # Ensure we can find the 'src' module from the root
7
+ sys.path.append(os.getcwd())
8
+
9
+ try:
10
+ from src.utils.utils import (
11
+ scrape_facebook,
12
+ scrape_twitter,
13
+ scrape_local_news,
14
+ scrape_reddit,
15
+ scrape_government_gazette,
16
+ scrape_cse_stock_data,
17
+ tool_weather_nowcast,
18
+ tool_dmc_alerts,
19
+ scrape_linkedin,
20
+ scrape_instagram,
21
+ )
22
+ print("✅ Libraries loaded successfully.\n")
23
+ except ImportError as e:
24
+ print(f"❌ Error loading libraries: {e}")
25
+ print("Make sure you are running this from the 'Roger-Final' folder.")
26
+ sys.exit(1)
27
+
28
+ def print_separator(char="=", length=70):
29
+ print(char * length)
30
+
31
+ def print_header(text):
32
+ print_separator()
33
+ print(f" {text}")
34
+ print_separator()
35
+
36
+ def run_test(name, func, description="", **kwargs):
37
+ print(f"\n🔍 Testing: {name}")
38
+ if description:
39
+ print(f" {description}")
40
+ print("-" * 70)
41
+
42
+ start_time = datetime.now()
43
+
44
+ try:
45
+ # Check if it's a LangChain tool (needs .invoke)
46
+ if hasattr(func, "invoke"):
47
+ res = func.invoke(kwargs)
48
+ else:
49
+ res = func(**kwargs)
50
+
51
+ elapsed = (datetime.now() - start_time).total_seconds()
52
+
53
+ # Try to print pretty JSON
54
+ try:
55
+ parsed = json.loads(res)
56
+
57
+ # Custom formatting for better readability
58
+ if isinstance(parsed, dict):
59
+ if "results" in parsed:
60
+ print(f"\n✅ Success! Found {len(parsed.get('results', []))} results in {elapsed:.2f}s")
61
+ print(f"\nSample Results:")
62
+ for i, item in enumerate(parsed['results'][:3], 1):
63
+ print(f"\n [{i}] {item.get('title', 'No title')}")
64
+ if 'snippet' in item:
65
+ snippet = item['snippet'][:150] + "..." if len(item['snippet']) > 150 else item['snippet']
66
+ print(f" {snippet}")
67
+ if 'url' in item:
68
+ print(f" 🔗 {item['url']}")
69
+ else:
70
+ print(f"\n✅ Success in {elapsed:.2f}s")
71
+ print(json.dumps(parsed, indent=2)[:1000])
72
+ else:
73
+ print(json.dumps(parsed, indent=2)[:1000])
74
+
75
+ except:
76
+ print(res[:1000] if len(res) > 1000 else res)
77
+
78
+ print(f"\n⏱️ Completed in {elapsed:.2f} seconds")
79
+
80
+ except Exception as e:
81
+ print(f"❌ Error: {e}")
82
+
83
+ print("-" * 70)
84
+
85
+ def check_sessions():
86
+ """Check which session files exist"""
87
+ print_header("Session Status Check")
88
+
89
+ session_paths = [
90
+ "src/utils/.sessions",
91
+ ".sessions"
92
+ ]
93
+
94
+ platforms = ["facebook", "twitter", "linkedin", "instagram", "reddit"]
95
+ found_sessions = []
96
+ print("session_path: ", session_paths)
97
+
98
+ for path in session_paths:
99
+ if os.path.exists(path):
100
+ print(f"\n📁 Checking {path}/")
101
+ for platform in platforms:
102
+ session_file = os.path.join(path, f"{platform}_storage_state.json")
103
+ if os.path.exists(session_file):
104
+ size = os.path.getsize(session_file)
105
+ print(f" ✅ {platform:12} ({size:,} bytes)")
106
+ found_sessions.append(platform)
107
+ else:
108
+ print(f" ❌ {platform:12} (not found)")
109
+
110
+ if not found_sessions:
111
+ print("\n⚠️ No session files found!")
112
+ print(" Run 'python src/utils/session_manager.py' to create sessions.")
113
+
114
+ print_separator()
115
+ return found_sessions
116
+
117
+ def main():
118
+ print_header("Roger Debug Runner - Comprehensive Tool Testing")
119
+
120
+ print("\n📋 Available Test Categories:")
121
+ print(" 1. Weather & Alerts (No auth required)")
122
+ print(" 2. News & Government (No auth required)")
123
+ print(" 3. Financial Data (No auth required)")
124
+ print(" 4. Social Media (Requires auth)")
125
+ print(" 5. Check Sessions")
126
+ print(" 6. Run All Tests")
127
+ print(" q. Quit")
128
+
129
+ choice = input("\nSelect category (1-6 or q): ").strip()
130
+
131
+ if choice == "q":
132
+ return
133
+
134
+ if choice == "5":
135
+ check_sessions()
136
+ return
137
+
138
+ # === CATEGORY 1: Weather & Alerts ===
139
+ if choice in ["1", "6"]:
140
+ print_header("CATEGORY 1: Weather & Alerts")
141
+
142
+ run_test(
143
+ "Weather Nowcast",
144
+ tool_weather_nowcast,
145
+ "Comprehensive weather data from Department of Meteorology",
146
+ location="Colombo"
147
+ )
148
+
149
+ run_test(
150
+ "DMC Alerts",
151
+ tool_dmc_alerts,
152
+ "Disaster Management Centre severe weather alerts"
153
+ )
154
+
155
+ # === CATEGORY 2: News & Government ===
156
+ if choice in ["2", "6"]:
157
+ print_header("CATEGORY 2: News & Government")
158
+
159
+ run_test(
160
+ "Local News",
161
+ scrape_local_news,
162
+ "Scraping Daily Mirror, Daily FT, News First",
163
+ keywords=["economy", "politics"],
164
+ max_articles=5
165
+ )
166
+
167
+ run_test(
168
+ "Government Gazette",
169
+ scrape_government_gazette,
170
+ "Latest gazette notifications",
171
+ keywords=["regulation"],
172
+ max_items=3
173
+ )
174
+
175
+ # === CATEGORY 3: Financial Data ===
176
+ if choice in ["3", "6"]:
177
+ print_header("CATEGORY 3: Financial Data")
178
+
179
+ run_test(
180
+ "CSE Stock Data",
181
+ scrape_cse_stock_data,
182
+ "Colombo Stock Exchange - ASPI Index",
183
+ symbol="ASPI",
184
+ period="1d"
185
+ )
186
+
187
+ # === CATEGORY 4: Social Media ===
188
+ if choice in ["4", "6"]:
189
+ print_header("CATEGORY 4: Social Media (Authentication Required)")
190
+
191
+ available_sessions = check_sessions()
192
+
193
+ if "facebook" in available_sessions:
194
+ run_test(
195
+ "Facebook",
196
+ scrape_facebook,
197
+ "Facebook search results",
198
+ keywords=["Sri Lanka", "Elon musk", "business"],
199
+ max_items=5
200
+ )
201
+ else:
202
+ print("\n⚠️ Facebook session not found - skipping")
203
+
204
+ if "instagram" in available_sessions:
205
+ run_test(
206
+ "Instagram",
207
+ scrape_instagram,
208
+ "Instagram search results",
209
+ keywords=["Sri Lanka", "Elon musk", "business"],
210
+ max_items=5
211
+ )
212
+ else:
213
+ print("\n⚠️ Facebook session not found - skipping")
214
+
215
+ if "linkedin" in available_sessions:
216
+ run_test(
217
+ "Linkedin",
218
+ scrape_linkedin,
219
+ "Linkedin search results",
220
+ keywords=["Sri Lanka", "Elon musk", "business"],
221
+ max_items=5
222
+ )
223
+ else:
224
+ print("\n⚠️ Facebook session not found - skipping")
225
+
226
+
227
+ if "twitter" in available_sessions:
228
+ run_test(
229
+ "Twitter",
230
+ scrape_twitter,
231
+ "Twitter/X search",
232
+ query="Sri Lanka economy"
233
+ )
234
+ else:
235
+ print("\n⚠️ Twitter session not found - skipping")
236
+
237
+ # Reddit doesn't need session
238
+ run_test(
239
+ "Reddit",
240
+ scrape_reddit,
241
+ "Reddit posts (no auth needed)",
242
+ keywords=["Sri Lanka"],
243
+ limit=5
244
+ )
245
+
246
+ print_header("Testing Complete!")
247
+ print(f"\n⏰ Finished at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
248
+
249
+ if __name__ == "__main__":
250
+ main()
docker-compose.prod.yml ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ # Neo4j Knowledge Graph (Production Feature)
5
+ neo4j:
6
+ image: neo4j:5.15-community
7
+ container_name: modelx-neo4j
8
+ ports:
9
+ - "7474:7474" # Browser UI
10
+ - "7687:7687" # Bolt protocol
11
+ environment:
12
+ NEO4J_AUTH: neo4j/modelx2024
13
+ NEO4J_PLUGINS: '["apoc"]'
14
+ NEO4J_dbms_security_procedures_unrestricted: "apoc.*"
15
+ volumes:
16
+ - ./data/neo4j/data:/data
17
+ - ./data/neo4j/logs:/logs
18
+ healthcheck:
19
+ test: ["CMD-SHELL", "cypher-shell -u neo4j -p modelx2024 'RETURN 1' || exit 1"]
20
+ interval: 10s
21
+ timeout: 5s
22
+ retries: 5
23
+
24
+ # Backend API
25
+ backend:
26
+ build:
27
+ context: .
28
+ dockerfile: Dockerfile
29
+ ports:
30
+ - "8000:8000"
31
+ environment:
32
+ - GROQ_API_KEY=${GROQ_API_KEY}
33
+ - PYTHONUNBUFFERED=1
34
+ - NEO4J_ENABLED=true
35
+ - NEO4J_URI=bolt://neo4j:7687
36
+ - NEO4J_USER=neo4j
37
+ - NEO4J_PASSWORD=modelx2024
38
+ volumes:
39
+ - ./src:/app/src
40
+ - ./data:/app/data # Persist storage data
41
+ command: python main.py
42
+ depends_on:
43
+ neo4j:
44
+ condition: service_healthy
45
+ healthcheck:
46
+ test: ["CMD", "curl", "-f", "http://localhost:8000/api/status"]
47
+ interval: 30s
48
+ timeout: 10s
49
+ retries: 3
50
+
51
+ # Frontend (Next.js)
52
+ frontend:
53
+ build:
54
+ context: ./frontend
55
+ dockerfile: Dockerfile
56
+ ports:
57
+ - "3000:3000"
58
+ environment:
59
+ - NEXT_PUBLIC_API_URL=http://backend:8000
60
+ depends_on:
61
+ - backend
62
+ volumes:
63
+ - ./frontend:/app
64
+ - /app/node_modules
65
+ - /app/. next
docker-compose.yml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ # Backend API
5
+ backend:
6
+ build:
7
+ context: .
8
+ dockerfile: backend/Dockerfile
9
+ ports:
10
+ - "8000:8000"
11
+ environment:
12
+ - GROQ_API_KEY=${GROQ_API_KEY}
13
+ - PYTHONUNBUFFERED=1
14
+ volumes:
15
+ - ./src:/app/src
16
+ - ./backend:/app/backend
17
+ command: python backend/api/main.py
18
+ healthcheck:
19
+ test: ["CMD", "curl", "-f", "http://localhost:8000/api/status"]
20
+ interval: 30s
21
+ timeout: 10s
22
+ retries: 3
23
+
24
+ # Frontend (Next.js)
25
+ frontend:
26
+ build:
27
+ context: ./frontend
28
+ dockerfile: Dockerfile
29
+ ports:
30
+ - "3000:3000"
31
+ environment:
32
+ - NEXT_PUBLIC_API_URL=http://localhost:8000
33
+ depends_on:
34
+ - backend
35
+ volumes:
36
+ - ./frontend:/app
37
+ - /app/node_modules
38
+ - /app/.next
frontend/.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
frontend/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
frontend/app/components/App.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { Routes, Route } from "react-router-dom";
4
+ import ClientWrapper from "./ClientWrapper";
5
+ import Index from "../pages/Index";
6
+ import NotFound from "../pages/NotFound";
7
+ import FloatingChatBox from "./FloatingChatBox";
8
+
9
+ export default function App() {
10
+ return (
11
+ <ClientWrapper>
12
+ <Routes>
13
+ <Route path="/" element={<Index />} />
14
+ <Route path="*" element={<NotFound />} />
15
+ </Routes>
16
+ <FloatingChatBox />
17
+ </ClientWrapper>
18
+ );
19
+ }
frontend/app/components/ClientWrapper.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { ReactNode, useState } from 'react';
4
+ import { Toaster } from "./ui/toaster";
5
+ import { Toaster as Sonner } from "./ui/sonner";
6
+ import { TooltipProvider } from "./ui/tooltip";
7
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
8
+ import { BrowserRouter } from "react-router-dom";
9
+
10
+ interface ClientWrapperProps {
11
+ children: ReactNode;
12
+ }
13
+
14
+ export default function ClientWrapper({ children }: ClientWrapperProps) {
15
+ const [queryClient] = useState(() => new QueryClient());
16
+
17
+ return (
18
+ <QueryClientProvider client={queryClient}>
19
+ <TooltipProvider>
20
+ <Toaster />
21
+ <Sonner />
22
+ <BrowserRouter>
23
+ {children}
24
+ </BrowserRouter>
25
+ </TooltipProvider>
26
+ </QueryClientProvider>
27
+ );
28
+ }
frontend/app/components/FloatingChatBox.tsx ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect } from 'react';
4
+ import { Send, Brain, Trash2, Radio } from 'lucide-react';
5
+ import { Badge } from './ui/badge';
6
+ import ReactMarkdown from 'react-markdown';
7
+ import remarkGfm from 'remark-gfm';
8
+ import './Roger.css';
9
+
10
+ interface Message {
11
+ id: string;
12
+ role: 'user' | 'assistant';
13
+ content: string;
14
+ sources?: Array<{
15
+ domain: string;
16
+ platform: string;
17
+ similarity: number;
18
+ }>;
19
+ timestamp: Date;
20
+ }
21
+
22
+ const FloatingChatBox = () => {
23
+ const [isOpen, setIsOpen] = useState(false);
24
+ const [messages, setMessages] = useState<Message[]>([]);
25
+ const [input, setInput] = useState('');
26
+ const [isLoading, setIsLoading] = useState(false);
27
+ const [domainFilter, setDomainFilter] = useState<string | null>(null);
28
+ const scrollContainerRef = useRef<HTMLDivElement | null>(null);
29
+
30
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
31
+
32
+ // Auto-scroll to bottom
33
+ useEffect(() => {
34
+ if (scrollContainerRef.current) {
35
+ scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
36
+ }
37
+ }, [messages, isLoading]);
38
+
39
+ // Handle body scroll when chat is open (mobile)
40
+ useEffect(() => {
41
+ if (isOpen) {
42
+ document.body.style.overflow = 'hidden';
43
+ } else {
44
+ document.body.style.overflow = 'unset';
45
+ }
46
+ return () => {
47
+ document.body.style.overflow = 'unset';
48
+ };
49
+ }, [isOpen]);
50
+
51
+ const sendMessage = async () => {
52
+ if (!input.trim() || isLoading) return;
53
+
54
+ const userMessage: Message = {
55
+ id: Date.now().toString(),
56
+ role: 'user',
57
+ content: input,
58
+ timestamp: new Date()
59
+ };
60
+
61
+ setMessages(prev => [...prev, userMessage]);
62
+ const currentInput = input;
63
+ setInput('');
64
+ setIsLoading(true);
65
+
66
+ try {
67
+ const response = await fetch(`${API_BASE}/api/rag/chat`, {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify({
71
+ message: currentInput,
72
+ domain_filter: domainFilter,
73
+ use_history: true
74
+ })
75
+ });
76
+
77
+ const data = await response.json();
78
+
79
+ const assistantMessage: Message = {
80
+ id: (Date.now() + 1).toString(),
81
+ role: 'assistant',
82
+ content: data.answer || 'No response received.',
83
+ sources: data.sources,
84
+ timestamp: new Date()
85
+ };
86
+
87
+ setMessages(prev => [...prev, assistantMessage]);
88
+ } catch (error) {
89
+ const errorMessage: Message = {
90
+ id: (Date.now() + 1).toString(),
91
+ role: 'assistant',
92
+ content: 'Failed to connect to Roger Intelligence. Please ensure the backend is running.',
93
+ timestamp: new Date()
94
+ };
95
+ setMessages(prev => [...prev, errorMessage]);
96
+ } finally {
97
+ setIsLoading(false);
98
+ }
99
+ };
100
+
101
+ const clearHistory = async () => {
102
+ try {
103
+ await fetch(`${API_BASE}/api/rag/clear`, { method: 'POST' });
104
+ setMessages([]);
105
+ } catch (error) {
106
+ console.error('Failed to clear history:', error);
107
+ }
108
+ };
109
+
110
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
111
+ if (e.key === 'Enter' && !e.shiftKey) {
112
+ e.preventDefault();
113
+ sendMessage();
114
+ }
115
+ };
116
+
117
+ const toggleChat = () => {
118
+ setIsOpen(!isOpen);
119
+ };
120
+
121
+ const domains = ['political', 'economic', 'weather', 'social', 'intelligence'];
122
+
123
+ return (
124
+ <div className={`${isOpen ? 'h-[100vh] w-[100vw]' : ''} fixed z-[9999] text-white bottom-0 right-0`}>
125
+ {/* Backdrop */}
126
+ <div
127
+ onClick={() => setIsOpen(false)}
128
+ className={`absolute top-0 left-0 w-screen h-screen bg-black transition-opacity duration-500 ${isOpen ? 'opacity-40 flex' : 'opacity-0 hidden'}`}
129
+ />
130
+
131
+ {/* Roger Button */}
132
+ <div
133
+ onClick={toggleChat}
134
+ className={`${isOpen ? 'translate-y-[100px]' : 'translate-y-0 delay-300'} select-none transition-transform duration-500 ease-in-out absolute bottom-[15px] right-[15px] sm:bottom-[20px] sm:right-[30px] flex items-center justify-center w-fit bg-[#373435] ring-[0.5px] ring-[#727376] rounded-full cursor-pointer px-[25px] py-[8px] sm:px-[30px] sm:py-[8px] shadow-lg hover:bg-[#4a4a4a] transition-colors`}
135
+ >
136
+ <Radio className="w-5 h-5 mr-2 text-green-400" />
137
+ <p className="select-none text-white text-[18px] sm:text-[18px] font-semibold">Roger</p>
138
+ </div>
139
+
140
+ {/* Chat Container */}
141
+ <div className={`${isOpen ? 'scale-100 delay-200' : 'scale-0'} roger-scrollbar absolute bottom-0 right-0 sm:bottom-[20px] sm:right-[30px] origin-bottom-right transition-transform duration-500 ease-in-out flex flex-col bg-[#373435] ring-[0.5px] ring-[#727376] h-[100dvh] w-[100vw] sm:h-[600px] sm:w-[420px] sm:rounded-[12px] justify-center overflow-hidden`}>
142
+
143
+ {/* Header - with safe area for iPhone notch */}
144
+ <div className="w-full select-none px-[16px] sm:px-[20px] bg-[#000000] text-white flex flex-row justify-between sm:rounded-t-[12px] py-[14px] sm:py-[18px] pt-[max(14px,env(safe-area-inset-top))] h-fit items-center border-b border-[#373435]">
145
+ <div className="flex items-center gap-3">
146
+ <div className="p-2 rounded-lg bg-green-500/20">
147
+ <Brain className="w-5 h-5 text-green-400" />
148
+ </div>
149
+ <div>
150
+ <p className="text-[20px] sm:text-[18px] font-semibold">Roger</p>
151
+ <p className="text-[12px] text-gray-400">Intelligence Assistant</p>
152
+ </div>
153
+ </div>
154
+ <div className="flex items-center gap-2">
155
+ <div
156
+ onClick={clearHistory}
157
+ className="cursor-pointer bg-[#373435] hover:bg-red-500/30 p-2 rounded-lg transition-colors"
158
+ title="Clear chat history"
159
+ >
160
+ <Trash2 className="w-4 h-4 text-gray-400 hover:text-red-400" />
161
+ </div>
162
+ <div
163
+ onClick={toggleChat}
164
+ className="cursor-pointer bg-[#373435] hover:bg-[#4a4a4a] active:bg-[#555] px-[14px] py-[8px] sm:px-[12px] sm:py-[6px] rounded-[8px] sm:rounded-[6px] transition-colors touch-manipulation"
165
+ >
166
+ <p className="text-[14px] sm:text-[13px]">Close</p>
167
+ </div>
168
+ </div>
169
+ </div>
170
+
171
+ {/* Domain Filter - scrollable on mobile */}
172
+ <div className="flex gap-1.5 sm:gap-1 px-3 sm:px-4 py-3 bg-[#1a1a1a] border-b border-[#373435] overflow-x-auto sm:flex-wrap hide-scrollbar">
173
+ <Badge
174
+ className={`cursor-pointer text-xs sm:text-xs whitespace-nowrap px-3 py-1.5 sm:px-2 sm:py-1 transition-colors touch-manipulation ${!domainFilter ? 'bg-green-500 text-white' : 'bg-[#373435] text-gray-300 hover:bg-[#4a4a4a] active:bg-[#555]'}`}
175
+ onClick={() => setDomainFilter(null)}
176
+ >
177
+ All
178
+ </Badge>
179
+ {domains.map(domain => (
180
+ <Badge
181
+ key={domain}
182
+ className={`cursor-pointer text-xs sm:text-xs whitespace-nowrap px-3 py-1.5 sm:px-2 sm:py-1 capitalize transition-colors touch-manipulation ${domainFilter === domain ? 'bg-green-500 text-white' : 'bg-[#373435] text-gray-300 hover:bg-[#4a4a4a] active:bg-[#555]'}`}
183
+ onClick={() => setDomainFilter(domain)}
184
+ >
185
+ {domain}
186
+ </Badge>
187
+ ))}
188
+ </div>
189
+
190
+ {/* Messages Container */}
191
+ {messages.length > 0 ? (
192
+ <div
193
+ className="flex flex-col flex-1 overflow-y-auto py-4 px-4 bg-[#101010] roger-scrollbar"
194
+ ref={scrollContainerRef}
195
+ style={{
196
+ WebkitOverflowScrolling: 'touch',
197
+ overscrollBehavior: 'contain',
198
+ }}
199
+ >
200
+ {/* Today Badge */}
201
+ <div className="flex justify-center mt-1 mb-4">
202
+ <div className="bg-[#373435] text-[11px] px-3 py-1 rounded-full border border-[#505050]">
203
+ <p className="text-gray-400">Today</p>
204
+ </div>
205
+ </div>
206
+
207
+ {messages.map((msg) => (
208
+ <div
209
+ className={`flex mb-3 ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
210
+ key={msg.id}
211
+ >
212
+ <div
213
+ className={`max-w-[85%] rounded-[10px] py-[10px] px-[14px] text-[14px] leading-relaxed ${msg.role === 'user'
214
+ ? 'bg-[#505050] text-white border border-[#606060]'
215
+ : 'bg-white text-black border border-gray-300'
216
+ }`}
217
+ >
218
+ {msg.role === 'assistant' ? (
219
+ <div className="roger-markdown">
220
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
221
+ {msg.content}
222
+ </ReactMarkdown>
223
+ </div>
224
+ ) : (
225
+ <p>{msg.content}</p>
226
+ )}
227
+
228
+ {/* Sources */}
229
+ {msg.sources && msg.sources.length > 0 && (
230
+ <div className="mt-2 pt-2 border-t border-gray-200">
231
+ <p className="text-[11px] text-gray-500 mb-1">Sources:</p>
232
+ <div className="flex flex-wrap gap-1">
233
+ {msg.sources.slice(0, 3).map((src, i) => (
234
+ <span
235
+ key={i}
236
+ className="text-[10px] bg-gray-100 text-gray-600 px-2 py-0.5 rounded"
237
+ >
238
+ {src.domain} ({Math.round(src.similarity * 100)}%)
239
+ </span>
240
+ ))}
241
+ </div>
242
+ </div>
243
+ )}
244
+ </div>
245
+ </div>
246
+ ))}
247
+
248
+ {/* Typing Indicator */}
249
+ {isLoading && (
250
+ <div className="flex py-2 items-center justify-start mb-3">
251
+ <div className="rounded-lg p-3 flex h-[50px] justify-center items-center">
252
+ <span className="roger-loader"></span>
253
+ </div>
254
+ </div>
255
+ )}
256
+ </div>
257
+ ) : (
258
+ <div className="flex-1 flex flex-col justify-center items-center bg-[#101010] px-6">
259
+ <div className="p-4 rounded-full bg-green-500/10 mb-4">
260
+ <Radio className="w-12 h-12 text-green-400 opacity-50" />
261
+ </div>
262
+ <div className="text-gray-300 text-center max-w-[280px]">
263
+ <p className="text-[16px] mb-3 leading-relaxed">
264
+ Hello! I'm <strong>Roger</strong>, your intelligence assistant.
265
+ </p>
266
+ <p className="text-[14px] text-gray-400 leading-relaxed">
267
+ Ask me anything about Sri Lanka's political, economic, weather, or social intelligence data.
268
+ </p>
269
+ <div className="mt-4 space-y-2 text-[12px] text-gray-500">
270
+ <p>Try asking:</p>
271
+ <p className="italic">"What are the latest political events?"</p>
272
+ <p className="italic">"Any weather warnings today?"</p>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ )}
277
+
278
+ {/* Input Area - with safe area for bottom */}
279
+ <div className="w-full ring-1 ring-[#373435] sm:rounded-b-[12px] py-[10px] sm:py-[12px] px-[12px] pb-[max(10px,env(safe-area-inset-bottom))] bg-[#000000]">
280
+ <div className="relative">
281
+ <textarea
282
+ onKeyDown={handleKeyDown}
283
+ onChange={(e) => setInput(e.target.value)}
284
+ value={input}
285
+ disabled={isLoading}
286
+ className="w-full focus:outline-none focus:ring-2 focus:ring-green-500 min-h-[50px] max-h-[100px] leading-[22px] rounded-[10px] bg-white text-black py-[12px] px-[14px] pr-[60px] resize-none text-[15px] placeholder-gray-500 disabled:opacity-50"
287
+ placeholder="Ask Roger..."
288
+ rows={2}
289
+ style={{ fontSize: '16px' }}
290
+ />
291
+ <div
292
+ onClick={sendMessage}
293
+ className={`absolute top-[6px] right-[6px] w-[44px] h-[44px] sm:w-[42px] sm:h-[42px] ring-[0.5px] ring-[#727376] cursor-pointer rounded-full flex items-center justify-center transition-all shadow-lg touch-manipulation active:scale-95 ${input.trim() && !isLoading
294
+ ? 'bg-green-500 hover:bg-green-600 active:bg-green-700'
295
+ : 'bg-[#373435] hover:bg-[#4a4a4a] active:bg-[#555]'
296
+ }`}
297
+ >
298
+ <Send className={`w-5 h-5 ml-[2px] ${input.trim() && !isLoading ? 'text-white' : 'text-gray-400'}`} />
299
+ </div>
300
+ </div>
301
+ <p className="text-[11px] text-gray-500 mt-2 text-center sm:hidden">
302
+ Press Enter to send • Shift+Enter for new line
303
+ </p>
304
+ </div>
305
+ </div>
306
+ </div>
307
+ );
308
+ };
309
+
310
+ export default FloatingChatBox;
frontend/app/components/LoadingScreen.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { motion } from "framer-motion";
5
+ import { Card } from "./ui/card";
6
+ import { Loader2, Zap } from "lucide-react";
7
+
8
+ export default function LoadingScreen() {
9
+ const [progress, setProgress] = useState(0);
10
+ const [loadingText, setLoadingText] = useState("Initializing Roger Platform...");
11
+
12
+ useEffect(() => {
13
+ const interval = setInterval(() => {
14
+ setProgress((prev) => {
15
+ if (prev >= 95) {
16
+ return prev;
17
+ }
18
+ return prev + 5;
19
+ });
20
+ }, 200);
21
+
22
+ const textInterval = setInterval(() => {
23
+ setLoadingText((prev) => {
24
+ const texts = [
25
+ "Initializing Roger Platform...",
26
+ "Connecting to Intelligence Agents...",
27
+ "Loading Social Media Monitor...",
28
+ "Loading Political Intelligence...",
29
+ "Loading Economic Analysis...",
30
+ "Loading Meteorological Data...",
31
+ "Establishing WebSocket Connection...",
32
+ "Syncing with Database...",
33
+ "Preparing Real-Time Dashboard..."
34
+ ];
35
+ const currentIndex = texts.indexOf(prev);
36
+ return texts[(currentIndex + 1) % texts.length];
37
+ });
38
+ }, 1500);
39
+
40
+ return () => {
41
+ clearInterval(interval);
42
+ clearInterval(textInterval);
43
+ };
44
+ }, []);
45
+
46
+ return (
47
+ <div className="min-h-screen w-full flex items-center justify-center bg-gradient-to-br from-background via-background/95 to-primary/5">
48
+ <Card className="p-12 bg-card/95 backdrop-blur border-border max-w-lg w-full mx-4">
49
+ <motion.div
50
+ initial={{ opacity: 0, y: 20 }}
51
+ animate={{ opacity: 1, y: 0 }}
52
+ transition={{ duration: 0.5 }}
53
+ className="space-y-8"
54
+ >
55
+ {/* Logo/Icon */}
56
+ <div className="flex items-center justify-center gap-3">
57
+ <motion.div
58
+ animate={{ rotate: 360 }}
59
+ transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
60
+ >
61
+ <Zap className="w-12 h-12 text-primary" />
62
+ </motion.div>
63
+ <h1 className="text-3xl font-bold bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
64
+ ROGER
65
+ </h1>
66
+ </div>
67
+
68
+ {/* Progress Bar */}
69
+ <div className="space-y-3">
70
+ <div className="h-2 bg-muted rounded-full overflow-hidden">
71
+ <motion.div
72
+ className="h-full bg-gradient-to-r from-primary via-primary/80 to-primary/60"
73
+ initial={{ width: 0 }}
74
+ animate={{ width: `${progress}%` }}
75
+ transition={{ duration: 0.3 }}
76
+ />
77
+ </div>
78
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
79
+ <span>{progress}%</span>
80
+ <span>Loading Intelligence Platform</span>
81
+ </div>
82
+ </div>
83
+
84
+ {/* Loading Text */}
85
+ <motion.div
86
+ key={loadingText}
87
+ initial={{ opacity: 0 }}
88
+ animate={{ opacity: 1 }}
89
+ transition={{ duration: 0.5 }}
90
+ className="text-center"
91
+ >
92
+ <div className="flex items-center justify-center gap-2 text-muted-foreground">
93
+ <Loader2 className="w-4 h-4 animate-spin" />
94
+ <p className="text-sm font-mono">{loadingText}</p>
95
+ </div>
96
+ </motion.div>
97
+
98
+ {/* Info */}
99
+ <div className="text-center space-y-2 pt-4 border-t border-border">
100
+ <p className="text-xs text-muted-foreground">
101
+ Real-Time Situational Awareness for Sri Lanka
102
+ </p>
103
+ <div className="flex items-center justify-center gap-2 text-xs text-muted-foreground">
104
+ <span>6 Domain Agents</span>
105
+ <span>•</span>
106
+ <span>47+ Data Sources</span>
107
+ <span>•</span>
108
+ <span>Live Updates</span>
109
+ </div>
110
+ </div>
111
+ </motion.div>
112
+ </Card>
113
+ </div>
114
+ );
115
+ }
frontend/app/components/NavLink.tsx ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NavLink as RouterNavLink, NavLinkProps } from "react-router-dom";
2
+ import { forwardRef } from "react";
3
+ import { cn } from "../lib/utils";
4
+
5
+ interface NavLinkCompatProps extends Omit<NavLinkProps, "className"> {
6
+ className?: string;
7
+ activeClassName?: string;
8
+ pendingClassName?: string;
9
+ }
10
+
11
+ const NavLink = forwardRef<HTMLAnchorElement, NavLinkCompatProps>(
12
+ ({ className, activeClassName, pendingClassName, to, ...props }, ref) => {
13
+ return (
14
+ <RouterNavLink
15
+ ref={ref}
16
+ to={to}
17
+ className={({ isActive, isPending }) =>
18
+ cn(className, isActive && activeClassName, isPending && pendingClassName)
19
+ }
20
+ {...props}
21
+ />
22
+ );
23
+ },
24
+ );
25
+
26
+ NavLink.displayName = "NavLink";
27
+
28
+ export { NavLink };
frontend/app/components/Roger.css ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Roger Chatbox Styles */
2
+
3
+ /* Typing Loader Animation */
4
+ .roger-loader {
5
+ width: 6px;
6
+ height: 25px;
7
+ border-radius: 4px;
8
+ display: block;
9
+ background-color: #22c55e;
10
+ margin: 0 auto;
11
+ position: relative;
12
+ animation: rogerAnimloader 0.3s 0.3s linear infinite alternate;
13
+ }
14
+
15
+ .roger-loader::after,
16
+ .roger-loader::before {
17
+ content: '';
18
+ width: 6px;
19
+ height: 25px;
20
+ border-radius: 4px;
21
+ background: #22c55e;
22
+ position: absolute;
23
+ top: 50%;
24
+ transform: translateY(-50%);
25
+ left: 13px;
26
+ animation: rogerAnimloader 0.3s 0.45s linear infinite alternate;
27
+ }
28
+
29
+ .roger-loader::before {
30
+ left: -13px;
31
+ animation-delay: 0s;
32
+ }
33
+
34
+ @keyframes rogerAnimloader {
35
+ 0% {
36
+ height: 48px;
37
+ }
38
+
39
+ 100% {
40
+ height: 4px;
41
+ }
42
+ }
43
+
44
+ /* Custom Scrollbar */
45
+ .roger-scrollbar::-webkit-scrollbar {
46
+ width: 5px;
47
+ }
48
+
49
+ .roger-scrollbar::-webkit-scrollbar-track {
50
+ background: #1a1a1a;
51
+ border-radius: 4px;
52
+ }
53
+
54
+ .roger-scrollbar::-webkit-scrollbar-thumb {
55
+ background: #22c55e;
56
+ border-radius: 4px;
57
+ }
58
+
59
+ .roger-scrollbar::-webkit-scrollbar-thumb:hover {
60
+ background: #16a34a;
61
+ }
62
+
63
+ /* Firefox support */
64
+ .roger-scrollbar {
65
+ scrollbar-width: thin;
66
+ scrollbar-color: #22c55e #1a1a1a;
67
+ }
68
+
69
+ /* Markdown Styling for AI Responses */
70
+ .roger-markdown h1,
71
+ .roger-markdown h2,
72
+ .roger-markdown h3,
73
+ .roger-markdown h4 {
74
+ font-weight: 600;
75
+ margin-top: 0.75rem;
76
+ margin-bottom: 0.5rem;
77
+ }
78
+
79
+ .roger-markdown h1 {
80
+ font-size: 1.25rem;
81
+ }
82
+
83
+ .roger-markdown h2 {
84
+ font-size: 1.125rem;
85
+ }
86
+
87
+ .roger-markdown h3 {
88
+ font-size: 1rem;
89
+ }
90
+
91
+ .roger-markdown p {
92
+ margin-bottom: 0.5rem;
93
+ }
94
+
95
+ .roger-markdown ul,
96
+ .roger-markdown ol {
97
+ margin-left: 1.25rem;
98
+ margin-bottom: 0.5rem;
99
+ }
100
+
101
+ .roger-markdown li {
102
+ margin-bottom: 0.25rem;
103
+ }
104
+
105
+ .roger-markdown code {
106
+ background: #f3f4f6;
107
+ padding: 0.125rem 0.375rem;
108
+ border-radius: 4px;
109
+ font-size: 0.875rem;
110
+ font-family: ui-monospace, monospace;
111
+ }
112
+
113
+ .roger-markdown pre {
114
+ background: #1f2937;
115
+ color: #e5e7eb;
116
+ padding: 0.75rem;
117
+ border-radius: 6px;
118
+ overflow-x: auto;
119
+ margin: 0.5rem 0;
120
+ }
121
+
122
+ .roger-markdown pre code {
123
+ background: transparent;
124
+ color: inherit;
125
+ padding: 0;
126
+ }
127
+
128
+ .roger-markdown blockquote {
129
+ border-left: 3px solid #22c55e;
130
+ padding-left: 0.75rem;
131
+ margin: 0.5rem 0;
132
+ color: #6b7280;
133
+ font-style: italic;
134
+ }
135
+
136
+ .roger-markdown table {
137
+ width: 100%;
138
+ border-collapse: collapse;
139
+ margin: 0.5rem 0;
140
+ }
141
+
142
+ .roger-markdown th,
143
+ .roger-markdown td {
144
+ border: 1px solid #e5e7eb;
145
+ padding: 0.375rem 0.5rem;
146
+ font-size: 0.875rem;
147
+ }
148
+
149
+ .roger-markdown th {
150
+ background: #f9fafb;
151
+ font-weight: 600;
152
+ }
153
+
154
+ .roger-markdown a {
155
+ color: #22c55e;
156
+ text-decoration: underline;
157
+ }
158
+
159
+ .roger-markdown a:hover {
160
+ color: #16a34a;
161
+ }
162
+
163
+ .roger-markdown strong {
164
+ font-weight: 600;
165
+ }
166
+
167
+ .roger-markdown em {
168
+ font-style: italic;
169
+ }
170
+
171
+ /* Hide scrollbar for domain filter on mobile */
172
+ .hide-scrollbar {
173
+ -ms-overflow-style: none;
174
+ scrollbar-width: none;
175
+ }
176
+
177
+ .hide-scrollbar::-webkit-scrollbar {
178
+ display: none;
179
+ }
180
+
181
+ /* Mobile Touch Improvements */
182
+ @media (max-width: 640px) {
183
+
184
+ /* Prevent text selection on buttons */
185
+ .touch-manipulation {
186
+ touch-action: manipulation;
187
+ -webkit-tap-highlight-color: transparent;
188
+ user-select: none;
189
+ }
190
+
191
+ /* Larger touch targets on mobile */
192
+ .roger-markdown {
193
+ font-size: 15px;
194
+ line-height: 1.6;
195
+ }
196
+
197
+ /* Smoother scrolling on mobile */
198
+ .roger-scrollbar {
199
+ -webkit-overflow-scrolling: touch;
200
+ }
201
+ }
202
+
203
+ /* Prevent iOS zoom on input focus */
204
+ @supports (-webkit-touch-callout: none) {
205
+
206
+ textarea,
207
+ input {
208
+ font-size: 16px !important;
209
+ }
210
+ }
frontend/app/components/dashboard/AnomalyDetection.tsx ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { Card } from "../ui/card";
4
+ import { Badge } from "../ui/badge";
5
+ import { Separator } from "../ui/separator";
6
+ import { Brain, AlertTriangle, TrendingUp, RefreshCw, Zap, Database } from "lucide-react";
7
+ import { motion, AnimatePresence } from "framer-motion";
8
+ import { useState, useEffect } from "react";
9
+
10
+ interface AnomalyEvent {
11
+ event_id: string;
12
+ summary: string;
13
+ domain: string;
14
+ severity: string;
15
+ impact_type: string;
16
+ anomaly_score: number;
17
+ is_anomaly: boolean;
18
+ language?: string;
19
+ timestamp?: string;
20
+ }
21
+
22
+ interface ModelStatus {
23
+ model_loaded: boolean;
24
+ models_available: string[];
25
+ vectorizer_loaded: boolean;
26
+ batch_threshold: number;
27
+ }
28
+
29
+ const AnomalyDetection = () => {
30
+ const [anomalies, setAnomalies] = useState<AnomalyEvent[]>([]);
31
+ const [modelStatus, setModelStatus] = useState<ModelStatus | null>(null);
32
+ const [loading, setLoading] = useState(true);
33
+ const [error, setError] = useState<string | null>(null);
34
+
35
+ const fetchAnomalies = async () => {
36
+ try {
37
+ setLoading(true);
38
+ const [anomalyRes, statusRes] = await Promise.all([
39
+ fetch('http://localhost:8000/api/anomalies?limit=20'),
40
+ fetch('http://localhost:8000/api/model/status')
41
+ ]);
42
+
43
+ const anomalyData = await anomalyRes.json();
44
+ const statusData = await statusRes.json();
45
+
46
+ setAnomalies(anomalyData.anomalies || []);
47
+ setModelStatus(statusData);
48
+ setError(null);
49
+ } catch (err) {
50
+ setError('Failed to fetch anomalies');
51
+ console.error(err);
52
+ } finally {
53
+ setLoading(false);
54
+ }
55
+ };
56
+
57
+ useEffect(() => {
58
+ fetchAnomalies();
59
+ // Refresh every 30 seconds
60
+ const interval = setInterval(fetchAnomalies, 30000);
61
+ return () => clearInterval(interval);
62
+ }, []);
63
+
64
+ const getScoreColor = (score: number) => {
65
+ if (score >= 0.8) return "text-destructive";
66
+ if (score >= 0.6) return "text-warning";
67
+ if (score >= 0.4) return "text-primary";
68
+ return "text-muted-foreground";
69
+ };
70
+
71
+ const getScoreBg = (score: number) => {
72
+ if (score >= 0.8) return "bg-destructive/20";
73
+ if (score >= 0.6) return "bg-warning/20";
74
+ if (score >= 0.4) return "bg-primary/20";
75
+ return "bg-muted/20";
76
+ };
77
+
78
+ return (
79
+ <Card className="p-6 bg-card border-border">
80
+ {/* Header */}
81
+ <div className="flex items-center justify-between mb-4">
82
+ <div className="flex items-center gap-3">
83
+ <div className="p-2 rounded-lg bg-primary/20">
84
+ <Brain className="w-6 h-6 text-primary" />
85
+ </div>
86
+ <div>
87
+ <h2 className="text-lg font-bold">ML ANOMALY DETECTION</h2>
88
+ <p className="text-xs text-muted-foreground font-mono">
89
+ Powered by BERT + Isolation Forest
90
+ </p>
91
+ </div>
92
+ </div>
93
+ <div className="flex items-center gap-2">
94
+ <button
95
+ onClick={fetchAnomalies}
96
+ disabled={loading}
97
+ className="p-2 rounded-lg hover:bg-muted/50 transition-colors disabled:opacity-50"
98
+ >
99
+ <RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
100
+ </button>
101
+ {modelStatus && (
102
+ <Badge className={modelStatus.model_loaded ? "bg-success/20 text-success" : "bg-warning/20 text-warning"}>
103
+ {modelStatus.model_loaded ? "Model Active" : "Model Training..."}
104
+ </Badge>
105
+ )}
106
+ </div>
107
+ </div>
108
+
109
+ {/* Model Status Bar */}
110
+ {modelStatus && (
111
+ <div className="mb-4 p-3 rounded-lg bg-muted/30 flex items-center gap-4 text-xs">
112
+ <div className="flex items-center gap-2">
113
+ <Database className="w-4 h-4 text-muted-foreground" />
114
+ <span>Batch Threshold: <strong>{modelStatus.batch_threshold}</strong> records</span>
115
+ </div>
116
+ <Separator orientation="vertical" className="h-4" />
117
+ <div className="flex items-center gap-2">
118
+ <Zap className="w-4 h-4 text-muted-foreground" />
119
+ <span>Models: <strong>{modelStatus.models_available?.length || 0}</strong> trained</span>
120
+ </div>
121
+ {modelStatus.models_available?.length > 0 && (
122
+ <>
123
+ <Separator orientation="vertical" className="h-4" />
124
+ <span className="text-muted-foreground">
125
+ {modelStatus.models_available.join(', ')}
126
+ </span>
127
+ </>
128
+ )}
129
+ </div>
130
+ )}
131
+
132
+ <Separator className="mb-4" />
133
+
134
+ {/* Anomalies List */}
135
+ <div className="space-y-3 max-h-[500px] overflow-y-auto">
136
+ {loading && anomalies.length === 0 ? (
137
+ <div className="text-center py-8">
138
+ <RefreshCw className="w-8 h-8 mx-auto animate-spin text-primary mb-3" />
139
+ <p className="text-sm text-muted-foreground">Loading anomalies...</p>
140
+ </div>
141
+ ) : error ? (
142
+ <div className="text-center py-8">
143
+ <AlertTriangle className="w-8 h-8 mx-auto text-destructive mb-3" />
144
+ <p className="text-sm text-destructive">{error}</p>
145
+ </div>
146
+ ) : anomalies.length === 0 ? (
147
+ <div className="text-center py-8">
148
+ <TrendingUp className="w-8 h-8 mx-auto text-success mb-3" />
149
+ <p className="text-sm text-muted-foreground">No anomalies detected</p>
150
+ <p className="text-xs text-muted-foreground mt-1">System operating normally</p>
151
+ </div>
152
+ ) : (
153
+ <AnimatePresence>
154
+ {anomalies.map((anomaly, idx) => (
155
+ <motion.div
156
+ key={anomaly.event_id || idx}
157
+ initial={{ opacity: 0, y: 20 }}
158
+ animate={{ opacity: 1, y: 0 }}
159
+ exit={{ opacity: 0, y: -20 }}
160
+ transition={{ delay: idx * 0.05 }}
161
+ >
162
+ <Card className={`p-4 border-l-4 ${anomaly.is_anomaly ? 'border-l-destructive' : 'border-l-warning'} ${getScoreBg(anomaly.anomaly_score)}`}>
163
+ <div className="flex items-start justify-between gap-4">
164
+ <div className="flex-1 min-w-0">
165
+ <div className="flex items-center gap-2 mb-2 flex-wrap">
166
+ <Badge className="bg-destructive/20 text-destructive text-xs">
167
+ ⚠️ ANOMALY
168
+ </Badge>
169
+ <Badge className="border border-border text-xs">
170
+ {anomaly.domain}
171
+ </Badge>
172
+ {anomaly.language && anomaly.language !== 'english' && (
173
+ <Badge className="bg-info/20 text-info text-xs">
174
+ {anomaly.language.toUpperCase()}
175
+ </Badge>
176
+ )}
177
+ </div>
178
+ <p className="text-sm font-medium mb-2 leading-relaxed">
179
+ {anomaly.summary}
180
+ </p>
181
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
182
+ <span>Severity: <strong className="text-foreground">{anomaly.severity}</strong></span>
183
+ {anomaly.timestamp && (
184
+ <span className="font-mono">
185
+ {new Date(anomaly.timestamp).toLocaleString()}
186
+ </span>
187
+ )}
188
+ </div>
189
+ </div>
190
+ <div className="text-right shrink-0">
191
+ <div className={`text-2xl font-bold ${getScoreColor(anomaly.anomaly_score)}`}>
192
+ {Math.round(anomaly.anomaly_score * 100)}%
193
+ </div>
194
+ <div className="text-xs text-muted-foreground">
195
+ Anomaly Score
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </Card>
200
+ </motion.div>
201
+ ))}
202
+ </AnimatePresence>
203
+ )}
204
+ </div>
205
+
206
+ {/* Footer */}
207
+ {anomalies.length > 0 && (
208
+ <div className="mt-4 pt-4 border-t border-border flex items-center justify-between text-xs text-muted-foreground">
209
+ <span>Showing {anomalies.length} anomalous events</span>
210
+ <span className="font-mono">Auto-refresh: 30s</span>
211
+ </div>
212
+ )}
213
+ </Card>
214
+ );
215
+ };
216
+
217
+ export default AnomalyDetection;
frontend/app/components/dashboard/CurrencyPrediction.tsx ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from "react";
4
+
5
+ interface CurrencyPrediction {
6
+ prediction_date: string;
7
+ generated_at: string;
8
+ model_version: string;
9
+ current_rate: number;
10
+ predicted_rate: number;
11
+ expected_change: number;
12
+ expected_change_pct: number;
13
+ direction: string;
14
+ direction_emoji: string;
15
+ volatility_class: string;
16
+ weekly_trend?: number;
17
+ monthly_trend?: number;
18
+ is_fallback?: boolean;
19
+ }
20
+
21
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000";
22
+
23
+ const VOLATILITY_COLORS = {
24
+ low: "bg-green-500/20 text-green-400 border-green-500/50",
25
+ medium: "bg-yellow-500/20 text-yellow-400 border-yellow-500/50",
26
+ high: "bg-red-500/20 text-red-400 border-red-500/50",
27
+ };
28
+
29
+ export default function CurrencyPrediction() {
30
+ const [prediction, setPrediction] = useState<CurrencyPrediction | null>(null);
31
+ const [history, setHistory] = useState<any[]>([]);
32
+ const [loading, setLoading] = useState(true);
33
+ const [error, setError] = useState<string | null>(null);
34
+
35
+ useEffect(() => {
36
+ fetchPrediction();
37
+ fetchHistory();
38
+ // Refresh every hour
39
+ const interval = setInterval(() => {
40
+ fetchPrediction();
41
+ fetchHistory();
42
+ }, 60 * 60 * 1000);
43
+ return () => clearInterval(interval);
44
+ }, []);
45
+
46
+ const fetchPrediction = async () => {
47
+ try {
48
+ const res = await fetch(`${API_BASE}/api/currency/prediction`);
49
+ const data = await res.json();
50
+
51
+ if (data.status === "success") {
52
+ setPrediction(data.prediction);
53
+ setError(null);
54
+ } else {
55
+ setError(data.message || "Failed to load prediction");
56
+ }
57
+ } catch (err) {
58
+ setError("Failed to connect to API");
59
+ } finally {
60
+ setLoading(false);
61
+ }
62
+ };
63
+
64
+ const fetchHistory = async () => {
65
+ try {
66
+ const res = await fetch(`${API_BASE}/api/currency/history?days=7`);
67
+ const data = await res.json();
68
+ if (data.status === "success") {
69
+ setHistory(data.history.slice(-7)); // Last 7 days
70
+ }
71
+ } catch (err) {
72
+ console.error("Failed to fetch history:", err);
73
+ }
74
+ };
75
+
76
+ if (loading) {
77
+ return (
78
+ <div className="bg-slate-800/50 rounded-xl p-6 border border-slate-700/50">
79
+ <div className="animate-pulse space-y-4">
80
+ <div className="h-6 bg-slate-700 rounded w-1/3"></div>
81
+ <div className="h-20 bg-slate-700 rounded"></div>
82
+ </div>
83
+ </div>
84
+ );
85
+ }
86
+
87
+ return (
88
+ <div className="bg-slate-800/50 rounded-xl p-6 border border-slate-700/50">
89
+ {/* Header */}
90
+ <div className="flex items-center justify-between mb-6">
91
+ <div>
92
+ <h2 className="text-xl font-bold text-white flex items-center gap-2">
93
+ 💱 USD/LKR Prediction
94
+ </h2>
95
+ {prediction && (
96
+ <p className="text-sm text-slate-400 mt-1">
97
+ Forecast for {prediction.prediction_date}
98
+ </p>
99
+ )}
100
+ </div>
101
+ <button
102
+ onClick={fetchPrediction}
103
+ className="p-2 rounded-lg bg-slate-700 hover:bg-slate-600 transition-colors"
104
+ title="Refresh"
105
+ >
106
+ 🔄
107
+ </button>
108
+ </div>
109
+
110
+ {error ? (
111
+ <div className="text-center py-8">
112
+ <p className="text-red-400 mb-4">{error}</p>
113
+ <button
114
+ onClick={fetchPrediction}
115
+ className="px-4 py-2 bg-blue-600 hover:bg-blue-500 rounded-lg transition-colors"
116
+ >
117
+ Retry
118
+ </button>
119
+ </div>
120
+ ) : prediction ? (
121
+ <>
122
+ {/* Main Prediction Card */}
123
+ <div
124
+ className={`p-6 rounded-xl border mb-6 ${prediction.expected_change_pct < 0
125
+ ? "bg-green-500/10 border-green-500/30"
126
+ : "bg-red-500/10 border-red-500/30"
127
+ }`}
128
+ >
129
+ <div className="grid grid-cols-3 gap-4 text-center">
130
+ <div>
131
+ <div className="text-slate-400 text-sm">Current Rate</div>
132
+ <div className="text-2xl font-bold text-white">
133
+ {prediction.current_rate.toFixed(2)}
134
+ </div>
135
+ <div className="text-xs text-slate-500">LKR/USD</div>
136
+ </div>
137
+ <div className="flex items-center justify-center">
138
+ <div className="text-4xl">
139
+ {prediction.direction_emoji}
140
+ </div>
141
+ </div>
142
+ <div>
143
+ <div className="text-slate-400 text-sm">Predicted</div>
144
+ <div className="text-2xl font-bold text-white">
145
+ {prediction.predicted_rate.toFixed(2)}
146
+ </div>
147
+ <div className="text-xs text-slate-500">LKR/USD</div>
148
+ </div>
149
+ </div>
150
+
151
+ <div className="mt-4 pt-4 border-t border-white/10 flex items-center justify-between">
152
+ <div>
153
+ <span className="text-slate-400">Expected Change: </span>
154
+ <span
155
+ className={`font-bold ${prediction.expected_change_pct < 0
156
+ ? "text-green-400"
157
+ : "text-red-400"
158
+ }`}
159
+ >
160
+ {prediction.expected_change_pct > 0 ? "+" : ""}
161
+ {prediction.expected_change_pct.toFixed(3)}%
162
+ </span>
163
+ </div>
164
+ <div
165
+ className={`px-3 py-1 rounded-full text-sm ${VOLATILITY_COLORS[prediction.volatility_class as keyof typeof VOLATILITY_COLORS] ||
166
+ VOLATILITY_COLORS.low
167
+ }`}
168
+ >
169
+ {prediction.volatility_class.toUpperCase()} Volatility
170
+ </div>
171
+ </div>
172
+ </div>
173
+
174
+ {/* Trend Info */}
175
+ <div className="grid grid-cols-2 gap-4 mb-6">
176
+ {prediction.weekly_trend !== undefined && (
177
+ <div className="p-4 rounded-lg bg-slate-700/50">
178
+ <div className="text-slate-400 text-sm">7-Day Trend</div>
179
+ <div
180
+ className={`text-lg font-bold ${prediction.weekly_trend < 0 ? "text-green-400" : "text-red-400"
181
+ }`}
182
+ >
183
+ {prediction.weekly_trend > 0 ? "+" : ""}
184
+ {prediction.weekly_trend.toFixed(2)}%
185
+ </div>
186
+ </div>
187
+ )}
188
+ {prediction.monthly_trend !== undefined && (
189
+ <div className="p-4 rounded-lg bg-slate-700/50">
190
+ <div className="text-slate-400 text-sm">30-Day Trend</div>
191
+ <div
192
+ className={`text-lg font-bold ${prediction.monthly_trend < 0 ? "text-green-400" : "text-red-400"
193
+ }`}
194
+ >
195
+ {prediction.monthly_trend > 0 ? "+" : ""}
196
+ {prediction.monthly_trend.toFixed(2)}%
197
+ </div>
198
+ </div>
199
+ )}
200
+ </div>
201
+
202
+ {/* Mini Chart (7-day history) */}
203
+ {history.length > 0 && (
204
+ <div className="p-4 rounded-lg bg-slate-700/30">
205
+ <div className="text-sm text-slate-400 mb-2">Last 7 Days</div>
206
+ <div className="flex items-end justify-between h-16 gap-1">
207
+ {history.map((day, i) => {
208
+ const minRate = Math.min(...history.map((h) => h.close));
209
+ const maxRate = Math.max(...history.map((h) => h.close));
210
+ const range = maxRate - minRate || 1;
211
+ const height = ((day.close - minRate) / range) * 100;
212
+
213
+ return (
214
+ <div
215
+ key={i}
216
+ className="flex-1 bg-blue-500/50 rounded-t hover:bg-blue-400/50 transition-colors"
217
+ style={{ height: `${Math.max(20, height)}%` }}
218
+ title={`${day.date}: ${day.close.toFixed(2)} LKR`}
219
+ />
220
+ );
221
+ })}
222
+ </div>
223
+ </div>
224
+ )}
225
+
226
+ {/* Fallback Warning */}
227
+ {prediction.is_fallback && (
228
+ <div className="mt-4 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg text-sm text-yellow-400">
229
+ ⚠️ Using fallback model. Run training for accurate predictions.
230
+ </div>
231
+ )}
232
+
233
+ {/* Footer */}
234
+ <div className="mt-4 text-xs text-slate-500 text-center">
235
+ Generated: {new Date(prediction.generated_at).toLocaleString()} |
236
+ Model: {prediction.model_version}
237
+ </div>
238
+ </>
239
+ ) : null}
240
+ </div>
241
+ );
242
+ }
frontend/app/components/dashboard/DashboardOverview.tsx ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card } from "../ui/card";
2
+ import { AlertTriangle, TrendingUp, Cloud, Zap, Users, Building, Wifi, WifiOff, Waves } from "lucide-react";
3
+ import { Badge } from "../ui/badge";
4
+ import { useRogerData } from "../../hooks/use-roger-data";
5
+ import { motion } from "framer-motion";
6
+ import RiverNetStatus from "./RiverNetStatus";
7
+
8
+ const DashboardOverview = () => {
9
+ // Get riverData directly from hook (fetched via /api/rivernet)
10
+ const { dashboard, events, isConnected, status, riverData } = useRogerData();
11
+
12
+ // Safety check: ensure events is always an array
13
+ const safeEvents = events || [];
14
+
15
+ // Sort events by timestamp descending (latest first)
16
+ const sortedEvents = [...safeEvents].sort((a, b) => {
17
+ const dateA = new Date(a.timestamp).getTime();
18
+ const dateB = new Date(b.timestamp).getTime();
19
+ return dateB - dateA; // Descending order (newest first)
20
+ });
21
+
22
+ // Calculate domain-specific metrics from sorted events
23
+ const domainCounts = sortedEvents.reduce((acc, event) => {
24
+ acc[event.domain] = (acc[event.domain] || 0) + 1;
25
+ return acc;
26
+ }, {} as Record<string, number>);
27
+
28
+ const riskEvents = sortedEvents.filter(e => e.impact_type === 'risk');
29
+ const opportunityEvents = sortedEvents.filter(e => e.impact_type === 'opportunity');
30
+ const criticalEvents = sortedEvents.filter(e => e.severity === 'critical' || e.severity === 'high');
31
+
32
+ // Count flood-related events
33
+ const floodEvents = sortedEvents.filter(e =>
34
+ e.category === 'flood_monitoring' ||
35
+ e.category === 'flood_alert' ||
36
+ (e.summary && e.summary.toLowerCase().includes('flood'))
37
+ );
38
+
39
+ const metrics = [
40
+ {
41
+ label: "Risk Events",
42
+ value: riskEvents.length.toString(),
43
+ change: criticalEvents.length > 0 ? `${criticalEvents.length} critical` : "—",
44
+ icon: AlertTriangle,
45
+ status: criticalEvents.length > 3 ? "warning" : "success"
46
+ },
47
+ {
48
+ label: "Opportunities",
49
+ value: opportunityEvents.length.toString(),
50
+ change: "+Growth",
51
+ icon: TrendingUp,
52
+ status: "success"
53
+ },
54
+ {
55
+ label: "Data Sources",
56
+ value: Object.keys(domainCounts).length.toString(),
57
+ change: "Active",
58
+ icon: Zap,
59
+ status: "info"
60
+ },
61
+ {
62
+ label: "Flood Alerts",
63
+ value: floodEvents.length.toString(),
64
+ change: riverData ? "Monitoring" : "Offline",
65
+ icon: Waves,
66
+ status: floodEvents.length > 0 ? "warning" : "success"
67
+ },
68
+ ];
69
+
70
+ return (
71
+ <div className="space-y-6">
72
+ {/* Connection Status Banner */}
73
+ <Card className={`p-4 ${isConnected ? 'bg-success/10 border-success/50' : 'bg-warning/10 border-warning/50'}`}>
74
+ <div className="flex items-center gap-3">
75
+ {isConnected ? (
76
+ <>
77
+ <Wifi className="w-5 h-5 text-success" />
78
+ <div className="flex-1">
79
+ <h3 className="font-bold text-success">SYSTEM OPERATIONAL</h3>
80
+ <p className="text-xs text-muted-foreground">Real-time intelligence streaming • Run #{dashboard.total_events}</p>
81
+ </div>
82
+ </>
83
+ ) : (
84
+ <>
85
+ <WifiOff className="w-5 h-5 text-warning" />
86
+ <div className="flex-1">
87
+ <h3 className="font-bold text-warning">RECONNECTING...</h3>
88
+ <p className="text-xs text-muted-foreground">Attempting to restore live feed</p>
89
+ </div>
90
+ </>
91
+ )}
92
+ <Badge className="font-mono text-xs">
93
+ {new Date(dashboard.last_updated).toLocaleTimeString()}
94
+ </Badge>
95
+ </div>
96
+ </Card>
97
+
98
+ {/* Metrics Grid */}
99
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
100
+ {metrics.map((metric, idx) => {
101
+ const Icon = metric.icon;
102
+ return (
103
+ <motion.div
104
+ key={idx}
105
+ initial={{ opacity: 0, y: 20 }}
106
+ animate={{ opacity: 1, y: 0 }}
107
+ transition={{ delay: idx * 0.1 }}
108
+ >
109
+ <Card className="p-4 bg-card border-border hover:border-primary/50 transition-all">
110
+ <div className="flex items-start justify-between mb-2">
111
+ <div className={`p-2 rounded bg-${metric.status}/20`}>
112
+ <Icon className={`w-5 h-5 text-${metric.status}`} />
113
+ </div>
114
+ <span className="text-xs font-mono text-success">{metric.change}</span>
115
+ </div>
116
+ <div>
117
+ <p className="text-2xl font-bold">{metric.value}</p>
118
+ <p className="text-xs text-muted-foreground uppercase tracking-wide">{metric.label}</p>
119
+ </div>
120
+ </Card>
121
+ </motion.div>
122
+ );
123
+ })}
124
+ </div>
125
+
126
+ {/* RiverNet Flood Monitoring */}
127
+ <RiverNetStatus riverData={riverData} compact={false} />
128
+
129
+ {/* Live Intelligence Feed - SORTED BY LATEST FIRST */}
130
+ <Card className="p-6 bg-card border-border">
131
+ <h3 className="font-bold mb-4 flex items-center gap-2">
132
+ <Zap className="w-5 h-5 text-primary" />
133
+ LIVE INTELLIGENCE FEED
134
+ <span className="text-xs text-muted-foreground ml-2">(Latest First)</span>
135
+ <Badge className="ml-auto">{sortedEvents.length} Events</Badge>
136
+ </h3>
137
+ <div className="space-y-3 max-h-[500px] overflow-y-auto">
138
+ {sortedEvents.slice(0, 10).map((event, idx) => {
139
+ const isRisk = event.impact_type === 'risk';
140
+ const isFlood = event.category === 'flood_monitoring' || event.category === 'flood_alert';
141
+ const severityColor = {
142
+ critical: 'destructive',
143
+ high: 'warning',
144
+ medium: 'primary',
145
+ low: 'secondary'
146
+ }[event.severity] || 'secondary';
147
+
148
+ return (
149
+ <motion.div
150
+ key={event.event_id}
151
+ initial={{ opacity: 0, x: -20 }}
152
+ animate={{ opacity: 1, x: 0 }}
153
+ transition={{ delay: idx * 0.05 }}
154
+ >
155
+ <Card className={`p-4 bg-muted/30 border-l-4 border-l-${severityColor} hover:bg-muted/50 transition-colors`}>
156
+ <div className="flex items-start justify-between mb-2">
157
+ <div className="flex-1">
158
+ <div className="flex items-center gap-2 mb-1 flex-wrap">
159
+ <Badge className={`bg-${severityColor} text-${severityColor}-foreground`}>
160
+ {event.severity.toUpperCase()}
161
+ </Badge>
162
+ <Badge className={isRisk ? "bg-destructive/20 text-destructive" : "bg-success/20 text-success"}>
163
+ {isRisk ? "⚠️ RISK" : "✨ OPPORTUNITY"}
164
+ </Badge>
165
+ <Badge className="border border-border">{event.domain}</Badge>
166
+ {isFlood && (
167
+ <Badge className="bg-info/20 text-info">🌊 FLOOD</Badge>
168
+ )}
169
+ </div>
170
+ <p className="font-semibold text-sm mb-1">{event.summary}</p>
171
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
172
+ <span>Confidence: {Math.round(event.confidence * 100)}%</span>
173
+ <span>•</span>
174
+ <span className="font-mono">{new Date(event.timestamp).toLocaleTimeString()}</span>
175
+ <span>•</span>
176
+ <span className="font-mono">{new Date(event.timestamp).toLocaleDateString()}</span>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </Card>
181
+ </motion.div>
182
+ );
183
+ })}
184
+ {sortedEvents.length === 0 && (
185
+ <div className="text-center text-muted-foreground py-8">
186
+ <AlertTriangle className="w-12 h-12 mx-auto mb-3 opacity-50" />
187
+ <p className="text-sm font-mono">Initializing intelligence gathering...</p>
188
+ </div>
189
+ )}
190
+ </div>
191
+ </Card>
192
+
193
+ {/* Operational Risk Radar */}
194
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
195
+ <Card className="p-6 bg-card border-border">
196
+ <Cloud className="w-8 h-8 text-warning mb-3" />
197
+ <p className="text-2xl font-bold">{Math.round(dashboard.logistics_friction * 100)}%</p>
198
+ <p className="text-xs text-muted-foreground uppercase">Logistics Friction</p>
199
+ </Card>
200
+ <Card className="p-6 bg-card border-border">
201
+ <AlertTriangle className="w-8 h-8 text-destructive mb-3" />
202
+ <p className="text-2xl font-bold">{Math.round(dashboard.compliance_volatility * 100)}%</p>
203
+ <p className="text-xs text-muted-foreground uppercase">Compliance Volatility</p>
204
+ </Card>
205
+ <Card className="p-6 bg-card border-border">
206
+ <TrendingUp className="w-8 h-8 text-info mb-3" />
207
+ <p className="text-2xl font-bold">{Math.round(dashboard.market_instability * 100)}%</p>
208
+ <p className="text-xs text-muted-foreground uppercase">Market Instability</p>
209
+ </Card>
210
+ <Card className="p-6 bg-card border-border">
211
+ <Building className="w-8 h-8 text-success mb-3" />
212
+ <p className="text-2xl font-bold">{Math.round(dashboard.opportunity_index * 100)}%</p>
213
+ <p className="text-xs text-muted-foreground uppercase">Opportunity Index</p>
214
+ </Card>
215
+ </div>
216
+ </div>
217
+ );
218
+ };
219
+
220
+ export default DashboardOverview;
frontend/app/components/dashboard/HistoricalIntel.tsx ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { Card } from '../ui/card';
5
+ import { Badge } from '../ui/badge';
6
+ import { History, TrendingUp, CloudRain, AlertTriangle, Calendar, Droplets } from 'lucide-react';
7
+ import { motion } from 'framer-motion';
8
+
9
+ interface HistoricalData {
10
+ source: string;
11
+ period: string;
12
+ fetched_at: string;
13
+ statistics: {
14
+ avg_annual_rainfall_mm: number;
15
+ max_daily_rainfall_mm: number;
16
+ heavy_rain_days_50mm: number;
17
+ extreme_rain_days_100mm: number;
18
+ avg_flood_events_per_year: number;
19
+ };
20
+ decadal_analysis: {
21
+ period: string;
22
+ avg_rainfall_mm: number;
23
+ extreme_days: number;
24
+ max_daily_mm: number;
25
+ major_flood_events: number;
26
+ }[];
27
+ key_findings: string[];
28
+ high_risk_periods: {
29
+ months: string;
30
+ type: string;
31
+ risk: string;
32
+ }[];
33
+ }
34
+
35
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
36
+
37
+ export default function HistoricalIntel() {
38
+ const [data, setData] = useState<HistoricalData | null>(null);
39
+ const [loading, setLoading] = useState(true);
40
+ const [error, setError] = useState<string | null>(null);
41
+
42
+ useEffect(() => {
43
+ const fetchData = async () => {
44
+ try {
45
+ const res = await fetch(`${API_BASE}/api/weather/historical`);
46
+ const result = await res.json();
47
+ if (result.status === 'success') {
48
+ setData(result.data);
49
+ } else {
50
+ setError(result.error || 'Failed to load data');
51
+ }
52
+ } catch (err) {
53
+ setError('Failed to connect to API');
54
+ } finally {
55
+ setLoading(false);
56
+ }
57
+ };
58
+ fetchData();
59
+ }, []);
60
+
61
+ if (loading) {
62
+ return (
63
+ <Card className="p-6 bg-card border-border">
64
+ <div className="flex items-center gap-3 mb-4">
65
+ <History className="w-6 h-6 text-info animate-pulse" />
66
+ <h3 className="font-bold">Loading Historical Data...</h3>
67
+ </div>
68
+ </Card>
69
+ );
70
+ }
71
+
72
+ if (error || !data) {
73
+ return (
74
+ <Card className="p-6 bg-card border-border">
75
+ <div className="flex items-center gap-3 mb-4">
76
+ <AlertTriangle className="w-6 h-6 text-warning" />
77
+ <h3 className="font-bold">Historical Data Unavailable</h3>
78
+ </div>
79
+ <p className="text-sm text-muted-foreground">{error}</p>
80
+ </Card>
81
+ );
82
+ }
83
+
84
+ const stats = data.statistics;
85
+
86
+ return (
87
+ <div className="space-y-4">
88
+ {/* Header */}
89
+ <Card className="p-4 sm:p-6 bg-card border-border">
90
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4">
91
+ <div className="flex items-center gap-3">
92
+ <div className="p-2 rounded-lg bg-info/20">
93
+ <History className="w-5 h-5 text-info" />
94
+ </div>
95
+ <div>
96
+ <h3 className="font-bold text-base sm:text-lg">Historical Flood Pattern Analysis</h3>
97
+ <p className="text-xs text-muted-foreground">{data.period}</p>
98
+ </div>
99
+ </div>
100
+ <Badge className="bg-info/20 text-info w-fit">
101
+ <Calendar className="w-3 h-3 mr-1" />
102
+ 30-Year Data
103
+ </Badge>
104
+ </div>
105
+
106
+ {/* Quick Stats */}
107
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4">
108
+ <motion.div
109
+ initial={{ opacity: 0, y: 10 }}
110
+ animate={{ opacity: 1, y: 0 }}
111
+ transition={{ delay: 0.1 }}
112
+ className="p-3 sm:p-4 bg-muted/30 rounded-lg"
113
+ >
114
+ <div className="flex items-center gap-2 mb-1">
115
+ <Droplets className="w-4 h-4 text-info" />
116
+ <span className="text-xs text-muted-foreground">Avg Annual</span>
117
+ </div>
118
+ <p className="text-lg sm:text-xl font-bold">{stats.avg_annual_rainfall_mm}</p>
119
+ <p className="text-xs text-muted-foreground">mm rainfall</p>
120
+ </motion.div>
121
+
122
+ <motion.div
123
+ initial={{ opacity: 0, y: 10 }}
124
+ animate={{ opacity: 1, y: 0 }}
125
+ transition={{ delay: 0.2 }}
126
+ className="p-3 sm:p-4 bg-muted/30 rounded-lg"
127
+ >
128
+ <div className="flex items-center gap-2 mb-1">
129
+ <CloudRain className="w-4 h-4 text-warning" />
130
+ <span className="text-xs text-muted-foreground">Max Daily</span>
131
+ </div>
132
+ <p className="text-lg sm:text-xl font-bold">{stats.max_daily_rainfall_mm}</p>
133
+ <p className="text-xs text-muted-foreground">mm recorded</p>
134
+ </motion.div>
135
+
136
+ <motion.div
137
+ initial={{ opacity: 0, y: 10 }}
138
+ animate={{ opacity: 1, y: 0 }}
139
+ transition={{ delay: 0.3 }}
140
+ className="p-3 sm:p-4 bg-muted/30 rounded-lg"
141
+ >
142
+ <div className="flex items-center gap-2 mb-1">
143
+ <TrendingUp className="w-4 h-4 text-success" />
144
+ <span className="text-xs text-muted-foreground">Heavy Days</span>
145
+ </div>
146
+ <p className="text-lg sm:text-xl font-bold">{stats.heavy_rain_days_50mm}</p>
147
+ <p className="text-xs text-muted-foreground">&gt;50mm days</p>
148
+ </motion.div>
149
+
150
+ <motion.div
151
+ initial={{ opacity: 0, y: 10 }}
152
+ animate={{ opacity: 1, y: 0 }}
153
+ transition={{ delay: 0.4 }}
154
+ className="p-3 sm:p-4 bg-destructive/10 rounded-lg"
155
+ >
156
+ <div className="flex items-center gap-2 mb-1">
157
+ <AlertTriangle className="w-4 h-4 text-destructive" />
158
+ <span className="text-xs text-muted-foreground">Extreme</span>
159
+ </div>
160
+ <p className="text-lg sm:text-xl font-bold">{stats.extreme_rain_days_100mm}</p>
161
+ <p className="text-xs text-muted-foreground">&gt;100mm days</p>
162
+ </motion.div>
163
+ </div>
164
+ </Card>
165
+
166
+ {/* Climate Change Comparison */}
167
+ <Card className="p-4 sm:p-6 bg-card border-border">
168
+ <h4 className="font-bold mb-4 flex items-center gap-2">
169
+ <TrendingUp className="w-5 h-5 text-warning" />
170
+ How Climate Has Changed
171
+ </h4>
172
+
173
+ <div className="overflow-x-auto">
174
+ <table className="w-full text-sm">
175
+ <thead>
176
+ <tr className="border-b border-border">
177
+ <th className="text-left py-2 px-2 text-muted-foreground font-medium">Period</th>
178
+ <th className="text-right py-2 px-2 text-muted-foreground font-medium">Avg Rainfall</th>
179
+ <th className="text-right py-2 px-2 text-muted-foreground font-medium">Extreme Days</th>
180
+ <th className="text-right py-2 px-2 text-muted-foreground font-medium">Max Daily</th>
181
+ </tr>
182
+ </thead>
183
+ <tbody>
184
+ {data.decadal_analysis.map((decade, idx) => (
185
+ <motion.tr
186
+ key={decade.period}
187
+ initial={{ opacity: 0, x: -10 }}
188
+ animate={{ opacity: 1, x: 0 }}
189
+ transition={{ delay: 0.1 * idx }}
190
+ className="border-b border-border/50 hover:bg-muted/30"
191
+ >
192
+ <td className="py-3 px-2 font-medium">{decade.period}</td>
193
+ <td className="py-3 px-2 text-right">{decade.avg_rainfall_mm} mm</td>
194
+ <td className="py-3 px-2 text-right">
195
+ <Badge className={idx === 2 ? 'bg-destructive/20 text-destructive' : 'bg-muted'}>
196
+ {decade.extreme_days}
197
+ </Badge>
198
+ </td>
199
+ <td className="py-3 px-2 text-right">{decade.max_daily_mm} mm</td>
200
+ </motion.tr>
201
+ ))}
202
+ </tbody>
203
+ </table>
204
+ </div>
205
+
206
+ {/* Key Findings */}
207
+ <div className="mt-4 p-3 bg-warning/10 border border-warning/30 rounded-lg">
208
+ <p className="text-sm font-medium text-warning mb-2">📊 Key Finding</p>
209
+ <p className="text-sm">{data.key_findings[0]}</p>
210
+ </div>
211
+ </Card>
212
+
213
+ {/* High Risk Periods */}
214
+ <Card className="p-4 sm:p-6 bg-card border-border">
215
+ <h4 className="font-bold mb-4 flex items-center gap-2">
216
+ <Calendar className="w-5 h-5 text-primary" />
217
+ High Risk Periods
218
+ </h4>
219
+ <div className="flex flex-wrap gap-2">
220
+ {data.high_risk_periods.map((period, idx) => (
221
+ <Badge
222
+ key={idx}
223
+ className={`${period.risk === 'high'
224
+ ? 'bg-destructive/20 text-destructive'
225
+ : 'bg-warning/20 text-warning'
226
+ }`}
227
+ >
228
+ {period.months}: {period.type}
229
+ </Badge>
230
+ ))}
231
+ </div>
232
+ </Card>
233
+ </div>
234
+ );
235
+ }
frontend/app/components/dashboard/NationalThreatCard.tsx ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { Card } from '../ui/card';
5
+ import { Badge } from '../ui/badge';
6
+ import { Shield, AlertTriangle, TrendingUp, CloudRain, Waves, Activity } from 'lucide-react';
7
+ import { motion } from 'framer-motion';
8
+
9
+ interface ThreatData {
10
+ status: string;
11
+ national_threat_score: number;
12
+ threat_level: string;
13
+ color: string;
14
+ breakdown: {
15
+ river_contribution: number;
16
+ alert_contribution: number;
17
+ seasonal_contribution: number;
18
+ };
19
+ risk_summary: {
20
+ critical_count: number;
21
+ high_count: number;
22
+ medium_count: number;
23
+ critical_districts: string[];
24
+ high_risk_districts: string[];
25
+ medium_risk_districts: string[];
26
+ };
27
+ calculated_at: string;
28
+ }
29
+
30
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
31
+
32
+ export default function NationalThreatCard() {
33
+ const [data, setData] = useState<ThreatData | null>(null);
34
+ const [loading, setLoading] = useState(true);
35
+ const [error, setError] = useState<string | null>(null);
36
+
37
+ const fetchData = async () => {
38
+ try {
39
+ const res = await fetch(`${API_BASE}/api/weather/threat`);
40
+ const result = await res.json();
41
+ if (result.status === 'success') {
42
+ setData(result);
43
+ setError(null);
44
+ } else {
45
+ setError(result.error || 'Failed to load data');
46
+ }
47
+ } catch (err) {
48
+ setError('Failed to connect to API');
49
+ } finally {
50
+ setLoading(false);
51
+ }
52
+ };
53
+
54
+ useEffect(() => {
55
+ fetchData();
56
+ // Refresh every 5 minutes
57
+ const interval = setInterval(fetchData, 5 * 60 * 1000);
58
+ return () => clearInterval(interval);
59
+ }, []);
60
+
61
+ const getThreatConfig = (level: string) => {
62
+ switch (level) {
63
+ case 'CRITICAL':
64
+ return {
65
+ bgColor: 'bg-destructive/20',
66
+ borderColor: 'border-destructive',
67
+ textColor: 'text-destructive',
68
+ icon: AlertTriangle,
69
+ gradient: 'from-destructive to-red-700',
70
+ pulse: true
71
+ };
72
+ case 'HIGH':
73
+ return {
74
+ bgColor: 'bg-warning/20',
75
+ borderColor: 'border-warning',
76
+ textColor: 'text-warning',
77
+ icon: TrendingUp,
78
+ gradient: 'from-warning to-orange-600',
79
+ pulse: true
80
+ };
81
+ case 'MODERATE':
82
+ return {
83
+ bgColor: 'bg-yellow-500/20',
84
+ borderColor: 'border-yellow-500',
85
+ textColor: 'text-yellow-500',
86
+ icon: Activity,
87
+ gradient: 'from-yellow-500 to-amber-600',
88
+ pulse: false
89
+ };
90
+ default:
91
+ return {
92
+ bgColor: 'bg-success/20',
93
+ borderColor: 'border-success',
94
+ textColor: 'text-success',
95
+ icon: Shield,
96
+ gradient: 'from-success to-green-600',
97
+ pulse: false
98
+ };
99
+ }
100
+ };
101
+
102
+ if (loading) {
103
+ return (
104
+ <Card className="p-6 bg-card border-border">
105
+ <div className="flex items-center gap-3">
106
+ <Shield className="w-6 h-6 text-muted-foreground animate-pulse" />
107
+ <span className="text-muted-foreground">Calculating threat level...</span>
108
+ </div>
109
+ </Card>
110
+ );
111
+ }
112
+
113
+ if (error || !data) {
114
+ return (
115
+ <Card className="p-6 bg-card border-border">
116
+ <div className="flex items-center gap-3">
117
+ <AlertTriangle className="w-6 h-6 text-warning" />
118
+ <span className="text-warning">Threat assessment unavailable</span>
119
+ </div>
120
+ </Card>
121
+ );
122
+ }
123
+
124
+ const config = getThreatConfig(data.threat_level);
125
+ const ThreatIcon = config.icon;
126
+ const { breakdown, risk_summary } = data;
127
+
128
+ return (
129
+ <Card className={`p-4 sm:p-6 ${config.bgColor} border-l-4 ${config.borderColor}`}>
130
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
131
+ {/* Left: Threat Score */}
132
+ <div className="flex items-center gap-4">
133
+ <motion.div
134
+ initial={{ scale: 0.8, opacity: 0 }}
135
+ animate={{ scale: 1, opacity: 1 }}
136
+ className={`relative p-3 sm:p-4 rounded-full bg-gradient-to-br ${config.gradient}`}
137
+ >
138
+ <ThreatIcon className="w-6 h-6 sm:w-8 sm:h-8 text-white" />
139
+ {config.pulse && (
140
+ <span className="absolute inset-0 rounded-full animate-ping bg-current opacity-20" />
141
+ )}
142
+ </motion.div>
143
+
144
+ <div>
145
+ <p className="text-xs sm:text-sm text-muted-foreground uppercase tracking-wide">National Threat</p>
146
+ <div className="flex items-baseline gap-2">
147
+ <motion.span
148
+ initial={{ opacity: 0, y: 10 }}
149
+ animate={{ opacity: 1, y: 0 }}
150
+ className="text-3xl sm:text-4xl font-bold"
151
+ >
152
+ {data.national_threat_score}
153
+ </motion.span>
154
+ <span className="text-lg text-muted-foreground">/100</span>
155
+ </div>
156
+ <Badge className={`${config.bgColor} ${config.textColor} mt-1`}>
157
+ {data.threat_level}
158
+ </Badge>
159
+ </div>
160
+ </div>
161
+
162
+ {/* Right: Breakdown */}
163
+ <div className="grid grid-cols-3 gap-2 sm:gap-4 text-center">
164
+ <div className="p-2 sm:p-3 bg-background/50 rounded-lg">
165
+ <Waves className="w-4 h-4 mx-auto mb-1 text-info" />
166
+ <p className="text-xs text-muted-foreground">Rivers</p>
167
+ <p className="font-bold">{breakdown.river_contribution}</p>
168
+ </div>
169
+ <div className="p-2 sm:p-3 bg-background/50 rounded-lg">
170
+ <AlertTriangle className="w-4 h-4 mx-auto mb-1 text-warning" />
171
+ <p className="text-xs text-muted-foreground">Alerts</p>
172
+ <p className="font-bold">{breakdown.alert_contribution}</p>
173
+ </div>
174
+ <div className="p-2 sm:p-3 bg-background/50 rounded-lg">
175
+ <CloudRain className="w-4 h-4 mx-auto mb-1 text-primary" />
176
+ <p className="text-xs text-muted-foreground">Season</p>
177
+ <p className="font-bold">{breakdown.seasonal_contribution}</p>
178
+ </div>
179
+ </div>
180
+ </div>
181
+
182
+ {/* Risk Districts */}
183
+ {(risk_summary.critical_count > 0 || risk_summary.high_count > 0) && (
184
+ <div className="mt-4 pt-4 border-t border-border/50">
185
+ <p className="text-xs text-muted-foreground mb-2">At-Risk Districts</p>
186
+ <div className="flex flex-wrap gap-2">
187
+ {risk_summary.critical_districts.map((d) => (
188
+ <Badge key={d} className="bg-destructive/20 text-destructive text-xs">
189
+ {d}
190
+ </Badge>
191
+ ))}
192
+ {risk_summary.high_risk_districts.map((d) => (
193
+ <Badge key={d} className="bg-warning/20 text-warning text-xs">
194
+ {d}
195
+ </Badge>
196
+ ))}
197
+ </div>
198
+ </div>
199
+ )}
200
+ </Card>
201
+ );
202
+ }
frontend/app/components/dashboard/RiverNetStatus.tsx ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Card } from "../ui/card";
4
+ import { Badge } from "../ui/badge";
5
+ import { Waves, AlertTriangle, CheckCircle, TrendingUp, Clock } from "lucide-react";
6
+ import { motion } from "framer-motion";
7
+
8
+ interface RiverData {
9
+ location_key: string;
10
+ name: string;
11
+ region: string;
12
+ status: "danger" | "warning" | "rising" | "normal" | "unknown" | "error";
13
+ water_level?: {
14
+ value: number;
15
+ unit: string;
16
+ };
17
+ url?: string;
18
+ last_updated?: string;
19
+ }
20
+
21
+ interface RiverNetData {
22
+ rivers: RiverData[];
23
+ alerts: Array<{
24
+ text: string;
25
+ severity: string;
26
+ source: string;
27
+ }>;
28
+ summary: {
29
+ total_monitored: number;
30
+ overall_status: string;
31
+ has_alerts: boolean;
32
+ status_breakdown?: Record<string, number>;
33
+ };
34
+ fetched_at: string;
35
+ source: string;
36
+ }
37
+
38
+ interface RiverNetStatusProps {
39
+ riverData?: RiverNetData | null;
40
+ compact?: boolean;
41
+ }
42
+
43
+ const statusConfig = {
44
+ danger: {
45
+ color: "destructive",
46
+ bgColor: "bg-destructive/20",
47
+ borderColor: "border-destructive",
48
+ textColor: "text-destructive",
49
+ icon: AlertTriangle,
50
+ emoji: "🔴",
51
+ label: "DANGER"
52
+ },
53
+ warning: {
54
+ color: "warning",
55
+ bgColor: "bg-warning/20",
56
+ borderColor: "border-warning",
57
+ textColor: "text-warning",
58
+ icon: AlertTriangle,
59
+ emoji: "🟠",
60
+ label: "WARNING"
61
+ },
62
+ rising: {
63
+ color: "primary",
64
+ bgColor: "bg-primary/20",
65
+ borderColor: "border-primary",
66
+ textColor: "text-primary",
67
+ icon: TrendingUp,
68
+ emoji: "🟡",
69
+ label: "RISING"
70
+ },
71
+ normal: {
72
+ color: "success",
73
+ bgColor: "bg-success/20",
74
+ borderColor: "border-success",
75
+ textColor: "text-success",
76
+ icon: CheckCircle,
77
+ emoji: "🟢",
78
+ label: "NORMAL"
79
+ },
80
+ unknown: {
81
+ color: "muted",
82
+ bgColor: "bg-muted/20",
83
+ borderColor: "border-muted",
84
+ textColor: "text-muted-foreground",
85
+ icon: Clock,
86
+ emoji: "⚪",
87
+ label: "UNKNOWN"
88
+ },
89
+ error: {
90
+ color: "destructive",
91
+ bgColor: "bg-destructive/10",
92
+ borderColor: "border-destructive/50",
93
+ textColor: "text-destructive/70",
94
+ icon: AlertTriangle,
95
+ emoji: "❌",
96
+ label: "ERROR"
97
+ }
98
+ };
99
+
100
+ const RiverNetStatus = ({ riverData, compact = false }: RiverNetStatusProps) => {
101
+ if (!riverData || !riverData.rivers || riverData.rivers.length === 0) {
102
+ return (
103
+ <Card className="p-6 bg-card border-border">
104
+ <div className="flex items-center gap-3 mb-4">
105
+ <Waves className="w-6 h-6 text-info" />
106
+ <h3 className="font-bold">FLOOD MONITORING</h3>
107
+ <Badge className="ml-auto bg-muted">Offline</Badge>
108
+ </div>
109
+ <div className="text-center text-muted-foreground py-4">
110
+ <Waves className="w-10 h-10 mx-auto mb-2 opacity-50" />
111
+ <p className="text-sm">River monitoring data unavailable</p>
112
+ <p className="text-xs mt-1">Check rivernet.lk for live data</p>
113
+ </div>
114
+ </Card>
115
+ );
116
+ }
117
+
118
+ const { rivers, summary, alerts, fetched_at } = riverData;
119
+ const overallStatus = summary?.overall_status || "normal";
120
+ const statusInfo = statusConfig[overallStatus as keyof typeof statusConfig] || statusConfig.unknown;
121
+ const StatusIcon = statusInfo.icon;
122
+
123
+ // Count rivers by status
124
+ const statusCounts = summary?.status_breakdown || {};
125
+
126
+ return (
127
+ <Card className={`p-6 bg-card border-border ${summary?.has_alerts ? 'border-warning/50' : ''}`}>
128
+ {/* Header */}
129
+ <div className="flex items-center justify-between mb-4">
130
+ <div className="flex items-center gap-3">
131
+ <div className={`p-2 rounded-lg ${statusInfo.bgColor}`}>
132
+ <Waves className={`w-6 h-6 ${statusInfo.textColor}`} />
133
+ </div>
134
+ <div>
135
+ <h3 className="font-bold flex items-center gap-2">
136
+ 🌊 FLOOD MONITORING
137
+ {summary?.has_alerts && (
138
+ <Badge className="bg-warning text-warning-foreground">ALERTS</Badge>
139
+ )}
140
+ </h3>
141
+ <p className="text-xs text-muted-foreground">
142
+ RiverNet.lk • {rivers.length} rivers monitored
143
+ </p>
144
+ </div>
145
+ </div>
146
+ <div className="text-right">
147
+ <Badge className={`${statusInfo.bgColor} ${statusInfo.textColor}`}>
148
+ {statusInfo.emoji} {statusInfo.label}
149
+ </Badge>
150
+ <p className="text-xs text-muted-foreground mt-1">
151
+ {new Date(fetched_at).toLocaleTimeString()}
152
+ </p>
153
+ </div>
154
+ </div>
155
+
156
+ {/* Status Summary */}
157
+ <div className="grid grid-cols-3 md:grid-cols-6 gap-2 mb-4">
158
+ {Object.entries(statusCounts).map(([status, count]) => {
159
+ if (count === 0) return null;
160
+ const config = statusConfig[status as keyof typeof statusConfig] || statusConfig.unknown;
161
+ return (
162
+ <div key={status} className={`p-2 rounded text-center ${config.bgColor}`}>
163
+ <p className={`text-lg font-bold ${config.textColor}`}>{count}</p>
164
+ <p className="text-xs text-muted-foreground uppercase">{status}</p>
165
+ </div>
166
+ );
167
+ })}
168
+ </div>
169
+
170
+ {/* Alerts Section */}
171
+ {alerts && alerts.length > 0 && (
172
+ <div className="mb-4 p-3 rounded-lg bg-warning/10 border border-warning/30">
173
+ <p className="text-sm font-semibold text-warning mb-2">⚠️ Active Alerts</p>
174
+ {alerts.slice(0, 2).map((alert, idx) => (
175
+ <p key={idx} className="text-xs text-warning/80 mb-1">
176
+ • {alert.text.slice(0, 100)}...
177
+ </p>
178
+ ))}
179
+ </div>
180
+ )}
181
+
182
+ {/* Rivers Grid */}
183
+ <div className={`grid ${compact ? 'grid-cols-2' : 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'} gap-3`}>
184
+ {rivers.map((river, idx) => {
185
+ const config = statusConfig[river.status] || statusConfig.normal;
186
+ const RiverStatusIcon = config.icon;
187
+
188
+ return (
189
+ <motion.div
190
+ key={river.location_key}
191
+ initial={{ opacity: 0, y: 10 }}
192
+ animate={{ opacity: 1, y: 0 }}
193
+ transition={{ delay: idx * 0.05 }}
194
+ >
195
+ <Card className={`p-3 ${config.bgColor} border-l-4 ${config.borderColor} hover:shadow-md transition-all cursor-pointer`}>
196
+ <div className="flex items-start justify-between">
197
+ <div className="flex-1">
198
+ <div className="flex items-center gap-2 mb-1">
199
+ <RiverStatusIcon className={`w-4 h-4 ${config.textColor}`} />
200
+ <span className="font-semibold text-sm">{river.name}</span>
201
+ </div>
202
+ <p className="text-xs text-muted-foreground">{river.region}</p>
203
+ {river.water_level && (
204
+ <p className={`text-xs font-mono ${config.textColor} mt-1`}>
205
+ Level: {river.water_level.value}{river.water_level.unit}
206
+ </p>
207
+ )}
208
+ </div>
209
+ <Badge className={`${config.bgColor} ${config.textColor} text-xs`}>
210
+ {config.emoji} {river.status.toUpperCase()}
211
+ </Badge>
212
+ </div>
213
+ </Card>
214
+ </motion.div>
215
+ );
216
+ })}
217
+ </div>
218
+
219
+ {/* Footer Link */}
220
+ <div className="mt-4 pt-3 border-t border-border flex items-center justify-between">
221
+ <p className="text-xs text-muted-foreground">
222
+ Source: <a href="https://rivernet.lk" target="_blank" rel="noopener noreferrer"
223
+ className="text-primary hover:underline">rivernet.lk</a>
224
+ </p>
225
+ {rivers.length > (compact ? 4 : 6) && (
226
+ <Badge variant="outline" className="text-xs">
227
+ +{rivers.length - (compact ? 4 : 6)} more rivers
228
+ </Badge>
229
+ )}
230
+ </div>
231
+ </Card>
232
+ );
233
+ };
234
+
235
+ export default RiverNetStatus;
frontend/app/components/dashboard/StockPredictions.tsx ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card } from "../ui/card";
2
+ import { Badge } from "../ui/badge";
3
+ import { TrendingUp, TrendingDown, Activity } from "lucide-react";
4
+ import { motion } from "framer-motion";
5
+ import { useRogerData } from "../../hooks/use-roger-data";
6
+
7
+ const StockPredictions = () => {
8
+ const { events } = useRogerData();
9
+
10
+ // Filter for economic/market events
11
+ const marketEvents = events.filter(e =>
12
+ e.domain === 'economical' || e.domain === 'market'
13
+ );
14
+
15
+ // Extract market insights
16
+ const marketInsights = marketEvents.map(event => {
17
+ const isBullish = event.impact_type === 'opportunity' ||
18
+ event.summary.toLowerCase().includes('bullish') ||
19
+ event.summary.toLowerCase().includes('growth');
20
+
21
+ const isBearish = event.summary.toLowerCase().includes('bearish') ||
22
+ event.summary.toLowerCase().includes('contraction');
23
+
24
+ return {
25
+ symbol: "ASPI",
26
+ title: event.summary,
27
+ sentiment: isBullish ? 'bullish' : isBearish ? 'bearish' : 'neutral',
28
+ confidence: event.confidence,
29
+ severity: event.severity,
30
+ timestamp: event.timestamp
31
+ };
32
+ });
33
+
34
+ // Mock stock data structure (in production, parse from actual events)
35
+ const stocks = [
36
+ {
37
+ symbol: "JKH.N0000",
38
+ name: "John Keells Holdings",
39
+ current: 145.50,
40
+ predicted: 148.20,
41
+ change: 2.70,
42
+ changePercent: 1.86,
43
+ volume: "1.2M",
44
+ sentiment: marketInsights[0]?.sentiment || 'neutral'
45
+ },
46
+ {
47
+ symbol: "COMB.N0000",
48
+ name: "Commercial Bank",
49
+ current: 89.75,
50
+ predicted: 87.30,
51
+ change: -2.45,
52
+ changePercent: -2.73,
53
+ volume: "856K",
54
+ sentiment: marketInsights[1]?.sentiment || 'neutral'
55
+ },
56
+ {
57
+ symbol: "HNB.N0000",
58
+ name: "Hatton National Bank",
59
+ current: 178.20,
60
+ predicted: 182.50,
61
+ change: 4.30,
62
+ changePercent: 2.41,
63
+ volume: "632K",
64
+ sentiment: 'bullish'
65
+ },
66
+ ];
67
+
68
+ return (
69
+ <div className="space-y-6">
70
+ <Card className="p-6 bg-card border-border">
71
+ <div className="flex items-center justify-between mb-4">
72
+ <div className="flex items-center gap-2">
73
+ <Activity className="w-5 h-5 text-success" />
74
+ <h2 className="text-lg font-bold">MARKET INTELLIGENCE - CSE</h2>
75
+ </div>
76
+ <Badge className="font-mono text-xs border">
77
+ LIVE AI ANALYSIS
78
+ </Badge>
79
+ </div>
80
+
81
+ {/* AI-Generated Market Insights */}
82
+ <div className="mb-6 space-y-2">
83
+ <h3 className="text-sm font-semibold text-muted-foreground uppercase">AI Market Analysis</h3>
84
+ {marketInsights.length > 0 ? (
85
+ marketInsights.slice(0, 3).map((insight, idx) => (
86
+ <motion.div
87
+ key={idx}
88
+ initial={{ opacity: 0, x: -10 }}
89
+ animate={{ opacity: 1, x: 0 }}
90
+ transition={{ delay: idx * 0.1 }}
91
+ className={`p-3 rounded border-l-4 ${
92
+ insight.sentiment === 'bullish' ? 'border-l-success bg-success/10' :
93
+ insight.sentiment === 'bearish' ? 'border-l-destructive bg-destructive/10' :
94
+ 'border-l-muted bg-muted/30'
95
+ }`}
96
+ >
97
+ <div className="flex items-center gap-2 mb-1">
98
+ {insight.sentiment === 'bullish' && <TrendingUp className="w-4 h-4 text-success" />}
99
+ {insight.sentiment === 'bearish' && <TrendingDown className="w-4 h-4 text-destructive" />}
100
+ <Badge className="text-xs">{insight.sentiment.toUpperCase()}</Badge>
101
+ <span className="text-xs text-muted-foreground ml-auto">
102
+ {Math.round(insight.confidence * 100)}% confidence
103
+ </span>
104
+ </div>
105
+ <p className="text-sm">{insight.title}</p>
106
+ </motion.div>
107
+ ))
108
+ ) : (
109
+ <p className="text-sm text-muted-foreground">Waiting for market data...</p>
110
+ )}
111
+ </div>
112
+
113
+ {/* Stock Grid */}
114
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
115
+ {stocks.map((stock, idx) => {
116
+ const isPositive = stock.change > 0;
117
+
118
+ return (
119
+ <motion.div
120
+ key={stock.symbol}
121
+ initial={{ opacity: 0, y: 20 }}
122
+ animate={{ opacity: 1, y: 0 }}
123
+ transition={{ delay: idx * 0.1 }}
124
+ >
125
+ <Card className="p-4 bg-muted/30 border-border hover:border-primary/50 transition-all">
126
+ <div className="flex items-start justify-between mb-2">
127
+ <div>
128
+ <h3 className="font-bold text-sm">{stock.symbol}</h3>
129
+ <p className="text-xs text-muted-foreground">{stock.name}</p>
130
+ </div>
131
+ <Badge
132
+ className={`font-mono text-xs ${isPositive ? "bg-primary text-primary-foreground" : "bg-destructive text-destructive-foreground"}`}
133
+ >
134
+ {isPositive ? <TrendingUp className="w-3 h-3 mr-1" /> : <TrendingDown className="w-3 h-3 mr-1" />}
135
+ {isPositive ? "+" : ""}{stock.changePercent.toFixed(2)}%
136
+ </Badge>
137
+ </div>
138
+
139
+ <div className="grid grid-cols-2 gap-3 mt-3">
140
+ <div>
141
+ <p className="text-xs text-muted-foreground mb-1">Current</p>
142
+ <p className="text-lg font-bold font-mono">
143
+ LKR {stock.current.toFixed(2)}
144
+ </p>
145
+ </div>
146
+ <div>
147
+ <p className="text-xs text-muted-foreground mb-1">AI Forecast</p>
148
+ <p className={`text-lg font-bold font-mono ${isPositive ? "text-success" : "text-destructive"}`}>
149
+ LKR {stock.predicted.toFixed(2)}
150
+ </p>
151
+ </div>
152
+ </div>
153
+
154
+ <div className="flex items-center justify-between mt-3 pt-3 border-t border-border">
155
+ <span className="text-xs text-muted-foreground">
156
+ Vol: {stock.volume}
157
+ </span>
158
+ <span className={`text-xs font-bold font-mono ${isPositive ? "text-success" : "text-destructive"}`}>
159
+ {isPositive ? "+" : ""}{stock.change.toFixed(2)}
160
+ </span>
161
+ </div>
162
+
163
+ {/* AI Sentiment Badge */}
164
+ <div className="mt-2">
165
+ <Badge className={`text-xs ${
166
+ stock.sentiment === 'bullish' ? 'bg-success/20 text-success' :
167
+ stock.sentiment === 'bearish' ? 'bg-destructive/20 text-destructive' :
168
+ 'bg-muted'
169
+ }`}>
170
+ AI: {stock.sentiment.toUpperCase()}
171
+ </Badge>
172
+ </div>
173
+ </Card>
174
+ </motion.div>
175
+ );
176
+ })}
177
+ </div>
178
+
179
+ <div className="mt-4 p-3 bg-muted/20 rounded border border-border">
180
+ <p className="text-xs text-muted-foreground font-mono">
181
+ <span className="text-warning font-bold">⚠ DISCLAIMER:</span> AI predictions based on real-time data analysis. Not financial advice.
182
+ </p>
183
+ </div>
184
+ </Card>
185
+ </div>
186
+ );
187
+ };
188
+
189
+ export default StockPredictions;
frontend/app/components/dashboard/WeatherPredictions.tsx ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useEffect } from "react";
4
+
5
+ interface DistrictPrediction {
6
+ temperature: {
7
+ high_c: number;
8
+ low_c: number;
9
+ };
10
+ rainfall: {
11
+ amount_mm: number;
12
+ probability: number;
13
+ };
14
+ flood_risk: number;
15
+ humidity_pct: number;
16
+ severity: "normal" | "advisory" | "warning" | "critical";
17
+ station_used: string;
18
+ is_fallback?: boolean;
19
+ }
20
+
21
+ interface WeatherPredictions {
22
+ status: string;
23
+ prediction_date: string;
24
+ generated_at: string;
25
+ districts: Record<string, DistrictPrediction>;
26
+ total_districts: number;
27
+ }
28
+
29
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://127.0.0.1:8000";
30
+
31
+ const SEVERITY_COLORS = {
32
+ normal: "bg-green-500/20 text-green-400 border-green-500/50",
33
+ advisory: "bg-yellow-500/20 text-yellow-400 border-yellow-500/50",
34
+ warning: "bg-orange-500/20 text-orange-400 border-orange-500/50",
35
+ critical: "bg-red-500/20 text-red-400 border-red-500/50",
36
+ };
37
+
38
+ const SEVERITY_ICONS = {
39
+ normal: "☀️",
40
+ advisory: "🌤️",
41
+ warning: "⛈️",
42
+ critical: "🌊",
43
+ };
44
+
45
+ export default function WeatherPredictions() {
46
+ const [predictions, setPredictions] = useState<WeatherPredictions | null>(null);
47
+ const [loading, setLoading] = useState(true);
48
+ const [error, setError] = useState<string | null>(null);
49
+ const [selectedDistrict, setSelectedDistrict] = useState<string | null>(null);
50
+ const [filter, setFilter] = useState<string>("all");
51
+
52
+ useEffect(() => {
53
+ fetchPredictions();
54
+ // Refresh every 30 minutes
55
+ const interval = setInterval(fetchPredictions, 30 * 60 * 1000);
56
+ return () => clearInterval(interval);
57
+ }, []);
58
+
59
+ const fetchPredictions = async () => {
60
+ try {
61
+ const res = await fetch(`${API_BASE}/api/weather/predictions`);
62
+ const data = await res.json();
63
+
64
+ if (data.status === "success") {
65
+ setPredictions(data);
66
+ setError(null);
67
+ } else {
68
+ setError(data.message || "Failed to load predictions");
69
+ }
70
+ } catch (err) {
71
+ setError("Failed to connect to weather API");
72
+ } finally {
73
+ setLoading(false);
74
+ }
75
+ };
76
+
77
+ const getFilteredDistricts = () => {
78
+ if (!predictions?.districts) return [];
79
+
80
+ const entries = Object.entries(predictions.districts);
81
+
82
+ if (filter === "all") return entries;
83
+ return entries.filter(([_, pred]) => pred.severity === filter);
84
+ };
85
+
86
+ const getSeverityCounts = () => {
87
+ if (!predictions?.districts) return { normal: 0, advisory: 0, warning: 0, critical: 0 };
88
+
89
+ const counts = { normal: 0, advisory: 0, warning: 0, critical: 0 };
90
+ Object.values(predictions.districts).forEach((pred) => {
91
+ counts[pred.severity] = (counts[pred.severity] || 0) + 1;
92
+ });
93
+ return counts;
94
+ };
95
+
96
+ if (loading) {
97
+ return (
98
+ <div className="bg-slate-800/50 rounded-xl p-6 border border-slate-700/50">
99
+ <div className="animate-pulse space-y-4">
100
+ <div className="h-6 bg-slate-700 rounded w-1/3"></div>
101
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
102
+ {[1, 2, 3, 4].map((i) => (
103
+ <div key={i} className="h-24 bg-slate-700 rounded-lg"></div>
104
+ ))}
105
+ </div>
106
+ </div>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ const sevCounts = getSeverityCounts();
112
+ const filteredDistricts = getFilteredDistricts();
113
+
114
+ return (
115
+ <div className="bg-slate-800/50 rounded-xl p-6 border border-slate-700/50">
116
+ {/* Header */}
117
+ <div className="flex items-center justify-between mb-6">
118
+ <div>
119
+ <h2 className="text-xl font-bold text-white flex items-center gap-2">
120
+ 🌦️ Weather Predictions
121
+ </h2>
122
+ {predictions && (
123
+ <p className="text-sm text-slate-400 mt-1">
124
+ Forecast for {predictions.prediction_date}
125
+ </p>
126
+ )}
127
+ </div>
128
+ <button
129
+ onClick={fetchPredictions}
130
+ className="p-2 rounded-lg bg-slate-700 hover:bg-slate-600 transition-colors"
131
+ title="Refresh predictions"
132
+ >
133
+ 🔄
134
+ </button>
135
+ </div>
136
+
137
+ {error ? (
138
+ <div className="text-center py-8">
139
+ <p className="text-red-400 mb-4">{error}</p>
140
+ <button
141
+ onClick={fetchPredictions}
142
+ className="px-4 py-2 bg-blue-600 hover:bg-blue-500 rounded-lg transition-colors"
143
+ >
144
+ Retry
145
+ </button>
146
+ </div>
147
+ ) : (
148
+ <>
149
+ {/* Severity Summary */}
150
+ <div className="grid grid-cols-4 gap-3 mb-6">
151
+ {(["normal", "advisory", "warning", "critical"] as const).map((sev) => (
152
+ <button
153
+ key={sev}
154
+ onClick={() => setFilter(filter === sev ? "all" : sev)}
155
+ className={`p-3 rounded-lg border transition-all ${filter === sev ? "ring-2 ring-white/30" : ""
156
+ } ${SEVERITY_COLORS[sev]}`}
157
+ >
158
+ <div className="text-2xl">{SEVERITY_ICONS[sev]}</div>
159
+ <div className="text-lg font-bold">{sevCounts[sev]}</div>
160
+ <div className="text-xs capitalize">{sev}</div>
161
+ </button>
162
+ ))}
163
+ </div>
164
+
165
+ {/* District Grid */}
166
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[500px] overflow-y-auto pr-2">
167
+ {filteredDistricts.map(([district, pred]) => (
168
+ <div
169
+ key={district}
170
+ className={`p-4 rounded-lg border cursor-pointer transition-all hover:scale-[1.02] ${SEVERITY_COLORS[pred.severity]
171
+ } ${selectedDistrict === district ? "ring-2 ring-white/50" : ""}`}
172
+ onClick={() => setSelectedDistrict(selectedDistrict === district ? null : district)}
173
+ >
174
+ <div className="flex items-center justify-between mb-2">
175
+ <h3 className="font-semibold text-white">{district}</h3>
176
+ <span className="text-xl">{SEVERITY_ICONS[pred.severity]}</span>
177
+ </div>
178
+
179
+ <div className="grid grid-cols-2 gap-2 text-sm">
180
+ <div>
181
+ <span className="text-slate-400">Temp:</span>
182
+ <span className="ml-1 text-white">
183
+ {pred.temperature.low_c}° - {pred.temperature.high_c}°C
184
+ </span>
185
+ </div>
186
+ <div>
187
+ <span className="text-slate-400">Rain:</span>
188
+ <span className="ml-1 text-white">
189
+ {pred.rainfall.amount_mm}mm
190
+ </span>
191
+ </div>
192
+ </div>
193
+
194
+ {pred.flood_risk > 0 && (
195
+ <div className="mt-2 text-sm">
196
+ <span className="text-red-400">⚠️ Flood Risk: </span>
197
+ <span className="text-white">{(pred.flood_risk * 100).toFixed(0)}%</span>
198
+ </div>
199
+ )}
200
+
201
+ {/* Expanded details */}
202
+ {selectedDistrict === district && (
203
+ <div className="mt-4 pt-3 border-t border-white/20 text-sm space-y-2">
204
+ <div className="flex justify-between">
205
+ <span className="text-slate-400">Rain Probability:</span>
206
+ <span className="text-white">{(pred.rainfall.probability * 100).toFixed(0)}%</span>
207
+ </div>
208
+ <div className="flex justify-between">
209
+ <span className="text-slate-400">Humidity:</span>
210
+ <span className="text-white">{pred.humidity_pct}%</span>
211
+ </div>
212
+ <div className="flex justify-between">
213
+ <span className="text-slate-400">Station:</span>
214
+ <span className="text-white">{pred.station_used}</span>
215
+ </div>
216
+ {pred.is_fallback && (
217
+ <div className="text-xs text-yellow-400">
218
+ ⚠️ Using climate fallback (LSTM model not trained)
219
+ </div>
220
+ )}
221
+ </div>
222
+ )}
223
+ </div>
224
+ ))}
225
+ </div>
226
+
227
+ {/* Footer */}
228
+ {predictions && (
229
+ <div className="mt-4 text-xs text-slate-500 text-center">
230
+ Generated: {new Date(predictions.generated_at).toLocaleString()} |
231
+ {predictions.total_districts} districts
232
+ </div>
233
+ )}
234
+ </>
235
+ )}
236
+ </div>
237
+ );
238
+ }
frontend/app/components/intelligence/IntelligenceFeed.tsx ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card } from "../ui/card";
2
+ import { Badge } from "../ui/badge";
3
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
4
+ import { Newspaper, Cloud, TrendingUp, FileText, Radio, Globe, MapPin } from "lucide-react";
5
+ import { useRogerData, RogerEvent } from "../../hooks/use-roger-data";
6
+ import { motion } from "framer-motion";
7
+ import { useState } from "react";
8
+
9
+ const IntelligenceFeed = () => {
10
+ const { events, isConnected } = useRogerData();
11
+
12
+ // Region toggle state (Sri Lanka / World)
13
+ const [region, setRegion] = useState<"sri_lanka" | "world">("sri_lanka");
14
+
15
+ // ALWAYS ensure events is an array
16
+ const safeEvents: RogerEvent[] = Array.isArray(events) ? events : [];
17
+
18
+ // Filter by region first
19
+ const regionFilteredEvents = safeEvents.filter(e => {
20
+ // If event has region field, use it; otherwise infer from domain
21
+ const eventRegion = e?.region || "sri_lanka"; // Default to Sri Lanka
22
+ return eventRegion === region;
23
+ });
24
+
25
+ // Then filter by category
26
+ const allEvents = regionFilteredEvents;
27
+ const newsEvents = regionFilteredEvents.filter(e => e?.domain === "social" || e?.domain === "intelligence");
28
+ const politicalEvents = regionFilteredEvents.filter(e => e?.domain === "political");
29
+ const weatherEvents = regionFilteredEvents.filter(e => e?.domain === "weather" || e?.domain === "meteorological");
30
+ const economicEvents = regionFilteredEvents.filter(e => e?.domain === "economical" || e?.domain === "market");
31
+
32
+ const renderEventCard = (item: RogerEvent, idx: number) => {
33
+ if (!item) return null;
34
+
35
+ const isRisk = item.impact_type === "risk";
36
+
37
+ const severityColorMap: Record<string, string> = {
38
+ critical: "destructive",
39
+ high: "warning",
40
+ medium: "primary",
41
+ low: "secondary",
42
+ };
43
+ const severityColor = severityColorMap[item.severity] || "secondary";
44
+
45
+ const domainIconMap: Record<string, React.ComponentType<any>> = {
46
+ social: Newspaper,
47
+ political: FileText,
48
+ weather: Cloud,
49
+ meteorological: Cloud,
50
+ economical: TrendingUp,
51
+ market: TrendingUp,
52
+ intelligence: Radio,
53
+ };
54
+ const Icon = domainIconMap[item.domain] || Radio;
55
+
56
+ return (
57
+ <motion.div
58
+ key={item.event_id}
59
+ initial={{ opacity: 0, y: 10 }}
60
+ animate={{ opacity: 1, y: 0 }}
61
+ transition={{ delay: idx * 0.05 }}
62
+ >
63
+ <Card
64
+ className={`p-4 bg-muted/30 border-l-4 hover:bg-muted/50 transition-colors ${severityColor === "destructive"
65
+ ? "border-l-destructive"
66
+ : severityColor === "warning"
67
+ ? "border-l-warning"
68
+ : severityColor === "primary"
69
+ ? "border-l-primary"
70
+ : "border-l-secondary"
71
+ }`}
72
+ >
73
+ <div className="flex items-start justify-between mb-2">
74
+ <div className="flex-1">
75
+ <div className="flex items-center gap-2 mb-1">
76
+ <Icon className="w-4 h-4" />
77
+ <Badge
78
+ className={
79
+ severityColor === "destructive"
80
+ ? "bg-destructive text-destructive-foreground"
81
+ : severityColor === "warning"
82
+ ? "bg-warning text-warning-foreground"
83
+ : severityColor === "primary"
84
+ ? "bg-primary text-primary-foreground"
85
+ : "bg-secondary text-secondary-foreground"
86
+ }
87
+ >
88
+ {item.severity?.toUpperCase()}
89
+ </Badge>
90
+
91
+ <Badge className={isRisk ? "bg-destructive/20 text-destructive" : "bg-success/20 text-success"}>
92
+ {isRisk ? "⚠️ RISK" : "✨ OPP"}
93
+ </Badge>
94
+
95
+ <Badge className="border border-border">{item.domain}</Badge>
96
+ </div>
97
+
98
+ <h3 className="font-bold text-sm mb-1">{item.summary}</h3>
99
+
100
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
101
+ <span>Confidence: {Math.round((item.confidence ?? 0) * 100)}%</span>
102
+ <span>•</span>
103
+ <span className="font-mono">
104
+ {item.timestamp ? new Date(item.timestamp).toLocaleTimeString() : ""}
105
+ </span>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </Card>
110
+ </motion.div>
111
+ );
112
+ };
113
+
114
+ return (
115
+ <div className="space-y-6">
116
+ <Card className="p-6 bg-card border-border">
117
+ <div className="flex items-center gap-2 mb-4">
118
+ <Radio className="w-5 h-5 text-primary" />
119
+ <h2 className="text-lg font-bold">INTELLIGENCE FEED</h2>
120
+
121
+ <span className="ml-auto text-xs font-mono text-muted-foreground">
122
+ {isConnected ? (
123
+ <span className="flex items-center gap-2">
124
+ <span className="w-2 h-2 rounded-full bg-success animate-pulse"></span>
125
+ Live
126
+ </span>
127
+ ) : (
128
+ <span className="flex items-center gap-2">
129
+ <span className="w-2 h-2 rounded-full bg-warning"></span>
130
+ Reconnecting...
131
+ </span>
132
+ )}
133
+ </span>
134
+ </div>
135
+
136
+ {/* REGION TOGGLE - Sri Lanka / World */}
137
+ <div className="flex gap-2 mb-4 overflow-x-auto hide-scrollbar">
138
+ <button
139
+ onClick={() => setRegion("sri_lanka")}
140
+ className={`flex items-center gap-2 px-3 sm:px-4 py-2 rounded-lg font-medium transition-all whitespace-nowrap text-sm ${region === "sri_lanka"
141
+ ? "bg-primary text-primary-foreground shadow-lg"
142
+ : "bg-muted text-muted-foreground hover:bg-muted/80"
143
+ }`}
144
+ >
145
+ <MapPin className="w-4 h-4" />
146
+ 🇱🇰 <span className="hidden sm:inline">Sri Lanka</span>
147
+ <Badge variant="secondary" className="ml-1">
148
+ {safeEvents.filter(e => (e?.region || "sri_lanka") === "sri_lanka").length}
149
+ </Badge>
150
+ </button>
151
+ <button
152
+ onClick={() => setRegion("world")}
153
+ className={`flex items-center gap-2 px-3 sm:px-4 py-2 rounded-lg font-medium transition-all whitespace-nowrap text-sm ${region === "world"
154
+ ? "bg-primary text-primary-foreground shadow-lg"
155
+ : "bg-muted text-muted-foreground hover:bg-muted/80"
156
+ }`}
157
+ >
158
+ <Globe className="w-4 h-4" />
159
+ 🌍 <span className="hidden sm:inline">World</span>
160
+ <Badge variant="secondary" className="ml-1">
161
+ {safeEvents.filter(e => e?.region === "world").length}
162
+ </Badge>
163
+ </button>
164
+ </div>
165
+
166
+ <Tabs defaultValue="all" className="w-full">
167
+ <div className="overflow-x-auto hide-scrollbar -mx-2 px-2 sm:mx-0 sm:px-0">
168
+ <TabsList className="inline-flex w-max sm:grid sm:w-full sm:grid-cols-5 mb-4">
169
+ <TabsTrigger value="all" className="px-3 sm:px-4 py-2 text-xs sm:text-sm">ALL ({allEvents.length})</TabsTrigger>
170
+ <TabsTrigger value="news" className="px-3 sm:px-4 py-2 text-xs sm:text-sm">NEWS ({newsEvents.length})</TabsTrigger>
171
+ <TabsTrigger value="political" className="px-3 sm:px-4 py-2 text-xs sm:text-sm whitespace-nowrap">POLITICAL ({politicalEvents.length})</TabsTrigger>
172
+ <TabsTrigger value="weather" className="px-3 sm:px-4 py-2 text-xs sm:text-sm">WEATHER ({weatherEvents.length})</TabsTrigger>
173
+ <TabsTrigger value="economic" className="px-3 sm:px-4 py-2 text-xs sm:text-sm">ECONOMIC ({economicEvents.length})</TabsTrigger>
174
+ </TabsList>
175
+ </div>
176
+
177
+ {/* ALL */}
178
+ <TabsContent value="all" className="space-y-3 max-h-[600px] overflow-y-auto">
179
+ {allEvents.length > 0 ? (
180
+ allEvents.map(renderEventCard)
181
+ ) : (
182
+ <div className="text-center py-12 text-muted-foreground">
183
+ <Radio className="w-12 h-12 mx-auto mb-3 opacity-50" />
184
+ <p className="text-sm font-mono">Waiting for intelligence data...</p>
185
+ </div>
186
+ )}
187
+ </TabsContent>
188
+
189
+ {/* NEWS */}
190
+ <TabsContent value="news" className="space-y-3 max-h-[600px] overflow-y-auto">
191
+ {newsEvents.length > 0 ? (
192
+ newsEvents.map(renderEventCard)
193
+ ) : (
194
+ <div className="text-center py-12 text-muted-foreground">
195
+ <Newspaper className="w-12 h-12 mx-auto mb-3 opacity-50" />
196
+ <p className="text-sm">No news events yet</p>
197
+ </div>
198
+ )}
199
+ </TabsContent>
200
+
201
+ {/* POLITICAL */}
202
+ <TabsContent value="political" className="space-y-3 max-h-[600px] overflow-y-auto">
203
+ {politicalEvents.length > 0 ? (
204
+ politicalEvents.map(renderEventCard)
205
+ ) : (
206
+ <div className="text-center py-12 text-muted-foreground">
207
+ <FileText className="w-12 h-12 mx-auto mb-3 opacity-50" />
208
+ <p className="text-sm">No political updates yet</p>
209
+ </div>
210
+ )}
211
+ </TabsContent>
212
+
213
+ {/* WEATHER */}
214
+ <TabsContent value="weather" className="space-y-3 max-h-[600px] overflow-y-auto">
215
+ {weatherEvents.length > 0 ? (
216
+ weatherEvents.map(renderEventCard)
217
+ ) : (
218
+ <div className="text-center py-12 text-muted-foreground">
219
+ <Cloud className="w-12 h-12 mx-auto mb-3 opacity-50" />
220
+ <p className="text-sm">No weather alerts yet</p>
221
+ </div>
222
+ )}
223
+ </TabsContent>
224
+
225
+ {/* ECONOMIC */}
226
+ <TabsContent value="economic" className="space-y-3 max-h-[600px] overflow-y-auto">
227
+ {economicEvents.length > 0 ? (
228
+ economicEvents.map(renderEventCard)
229
+ ) : (
230
+ <div className="text-center py-12 text-muted-foreground">
231
+ <TrendingUp className="w-12 h-12 mx-auto mb-3 opacity-50" />
232
+ <p className="text-sm">No economic data yet</p>
233
+ </div>
234
+ )}
235
+ </TabsContent>
236
+ </Tabs>
237
+ </Card>
238
+ </div>
239
+ );
240
+ };
241
+
242
+ export default IntelligenceFeed;
frontend/app/components/intelligence/IntelligenceSettings.tsx ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { Card } from "../ui/card";
5
+ import { Button } from "../ui/button";
6
+ import { Input } from "../ui/input";
7
+ import { Badge } from "../ui/badge";
8
+ import {
9
+ Settings,
10
+ Plus,
11
+ X,
12
+ Save,
13
+ RefreshCw,
14
+ Twitter,
15
+ Linkedin,
16
+ Facebook,
17
+ Search,
18
+ Package,
19
+ User
20
+ } from "lucide-react";
21
+ import { motion, AnimatePresence } from "framer-motion";
22
+
23
+ // API base URL - adjust based on your backend
24
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
25
+
26
+ interface IntelConfig {
27
+ user_profiles: {
28
+ twitter: string[];
29
+ facebook: string[];
30
+ linkedin: string[];
31
+ };
32
+ user_keywords: string[];
33
+ user_products: string[];
34
+ }
35
+
36
+ const defaultConfig: IntelConfig = {
37
+ user_profiles: { twitter: [], facebook: [], linkedin: [] },
38
+ user_keywords: [],
39
+ user_products: [],
40
+ };
41
+
42
+ const IntelligenceSettings = () => {
43
+ const [config, setConfig] = useState<IntelConfig>(defaultConfig);
44
+ const [loading, setLoading] = useState(true);
45
+ const [saving, setSaving] = useState(false);
46
+ const [error, setError] = useState<string | null>(null);
47
+ const [success, setSuccess] = useState<string | null>(null);
48
+
49
+ // Input states
50
+ const [newKeyword, setNewKeyword] = useState("");
51
+ const [newProduct, setNewProduct] = useState("");
52
+ const [newProfile, setNewProfile] = useState("");
53
+ const [selectedPlatform, setSelectedPlatform] = useState<"twitter" | "facebook" | "linkedin">("twitter");
54
+
55
+ // Fetch config on mount
56
+ useEffect(() => {
57
+ fetchConfig();
58
+ }, []);
59
+
60
+ const fetchConfig = async () => {
61
+ setLoading(true);
62
+ setError(null);
63
+ try {
64
+ const res = await fetch(`${API_BASE}/api/intel/config`);
65
+ const data = await res.json();
66
+ if (data.status === "success" && data.config) {
67
+ setConfig(data.config);
68
+ } else if (data.config) {
69
+ setConfig(data.config);
70
+ }
71
+ } catch (err) {
72
+ setError("Failed to load configuration");
73
+ console.error("Failed to fetch intel config:", err);
74
+ } finally {
75
+ setLoading(false);
76
+ }
77
+ };
78
+
79
+ const saveConfig = async () => {
80
+ setSaving(true);
81
+ setError(null);
82
+ setSuccess(null);
83
+ try {
84
+ const res = await fetch(`${API_BASE}/api/intel/config`, {
85
+ method: "POST",
86
+ headers: { "Content-Type": "application/json" },
87
+ body: JSON.stringify(config),
88
+ });
89
+ const data = await res.json();
90
+ if (data.status === "updated") {
91
+ setSuccess("Configuration saved! Changes will apply on next agent cycle.");
92
+ setTimeout(() => setSuccess(null), 3000);
93
+ } else {
94
+ setError(data.error || "Failed to save");
95
+ }
96
+ } catch (err) {
97
+ setError("Failed to save configuration");
98
+ console.error("Failed to save intel config:", err);
99
+ } finally {
100
+ setSaving(false);
101
+ }
102
+ };
103
+
104
+ const addKeyword = () => {
105
+ if (newKeyword.trim() && !config.user_keywords.includes(newKeyword.trim())) {
106
+ setConfig({
107
+ ...config,
108
+ user_keywords: [...config.user_keywords, newKeyword.trim()],
109
+ });
110
+ setNewKeyword("");
111
+ }
112
+ };
113
+
114
+ const removeKeyword = (keyword: string) => {
115
+ setConfig({
116
+ ...config,
117
+ user_keywords: config.user_keywords.filter((k) => k !== keyword),
118
+ });
119
+ };
120
+
121
+ const addProduct = () => {
122
+ if (newProduct.trim() && !config.user_products.includes(newProduct.trim())) {
123
+ setConfig({
124
+ ...config,
125
+ user_products: [...config.user_products, newProduct.trim()],
126
+ });
127
+ setNewProduct("");
128
+ }
129
+ };
130
+
131
+ const removeProduct = (product: string) => {
132
+ setConfig({
133
+ ...config,
134
+ user_products: config.user_products.filter((p) => p !== product),
135
+ });
136
+ };
137
+
138
+ const addProfile = () => {
139
+ if (newProfile.trim() && !config.user_profiles[selectedPlatform].includes(newProfile.trim())) {
140
+ setConfig({
141
+ ...config,
142
+ user_profiles: {
143
+ ...config.user_profiles,
144
+ [selectedPlatform]: [...config.user_profiles[selectedPlatform], newProfile.trim()],
145
+ },
146
+ });
147
+ setNewProfile("");
148
+ }
149
+ };
150
+
151
+ const removeProfile = (platform: "twitter" | "facebook" | "linkedin", profile: string) => {
152
+ setConfig({
153
+ ...config,
154
+ user_profiles: {
155
+ ...config.user_profiles,
156
+ [platform]: config.user_profiles[platform].filter((p) => p !== profile),
157
+ },
158
+ });
159
+ };
160
+
161
+ const platformIcons = {
162
+ twitter: <Twitter className="w-4 h-4" />,
163
+ facebook: <Facebook className="w-4 h-4" />,
164
+ linkedin: <Linkedin className="w-4 h-4" />,
165
+ };
166
+
167
+ const platformColors = {
168
+ twitter: "bg-blue-500/20 text-blue-400 border-blue-500/30",
169
+ facebook: "bg-indigo-500/20 text-indigo-400 border-indigo-500/30",
170
+ linkedin: "bg-sky-500/20 text-sky-400 border-sky-500/30",
171
+ };
172
+
173
+ if (loading) {
174
+ return (
175
+ <Card className="p-6 bg-card border-border">
176
+ <div className="flex items-center justify-center py-12">
177
+ <RefreshCw className="w-6 h-6 animate-spin text-primary" />
178
+ <span className="ml-2 text-muted-foreground">Loading configuration...</span>
179
+ </div>
180
+ </Card>
181
+ );
182
+ }
183
+
184
+ return (
185
+ <Card className="p-6 bg-card border-border">
186
+ <div className="flex items-center justify-between mb-6">
187
+ <div className="flex items-center gap-2">
188
+ <Settings className="w-5 h-5 text-primary" />
189
+ <h2 className="text-lg font-bold">INTELLIGENCE SETTINGS</h2>
190
+ </div>
191
+ <div className="flex gap-2">
192
+ <Button variant="outline" size="sm" onClick={fetchConfig} disabled={loading}>
193
+ <RefreshCw className={`w-4 h-4 mr-1 ${loading ? "animate-spin" : ""}`} />
194
+ Refresh
195
+ </Button>
196
+ <Button size="sm" onClick={saveConfig} disabled={saving}>
197
+ <Save className={`w-4 h-4 mr-1 ${saving ? "animate-pulse" : ""}`} />
198
+ {saving ? "Saving..." : "Save Changes"}
199
+ </Button>
200
+ </div>
201
+ </div>
202
+
203
+ {/* Status Messages */}
204
+ <AnimatePresence>
205
+ {error && (
206
+ <motion.div
207
+ initial={{ opacity: 0, y: -10 }}
208
+ animate={{ opacity: 1, y: 0 }}
209
+ exit={{ opacity: 0 }}
210
+ className="mb-4 p-3 bg-destructive/20 border border-destructive/30 rounded-lg text-destructive text-sm"
211
+ >
212
+ {error}
213
+ </motion.div>
214
+ )}
215
+ {success && (
216
+ <motion.div
217
+ initial={{ opacity: 0, y: -10 }}
218
+ animate={{ opacity: 1, y: 0 }}
219
+ exit={{ opacity: 0 }}
220
+ className="mb-4 p-3 bg-success/20 border border-success/30 rounded-lg text-success text-sm"
221
+ >
222
+ {success}
223
+ </motion.div>
224
+ )}
225
+ </AnimatePresence>
226
+
227
+ <div className="space-y-6">
228
+ {/* Keywords Section */}
229
+ <div className="p-4 bg-muted/30 rounded-lg border border-border">
230
+ <div className="flex items-center gap-2 mb-3">
231
+ <Search className="w-4 h-4 text-primary" />
232
+ <h3 className="font-semibold">Custom Keywords</h3>
233
+ <Badge variant="secondary" className="ml-auto">
234
+ {config.user_keywords.length} keywords
235
+ </Badge>
236
+ </div>
237
+ <p className="text-xs text-muted-foreground mb-3">
238
+ Add keywords to monitor across all social platforms
239
+ </p>
240
+ <div className="flex gap-2 mb-3">
241
+ <Input
242
+ placeholder="Enter keyword (e.g., 'Colombo Port')"
243
+ value={newKeyword}
244
+ onChange={(e) => setNewKeyword(e.target.value)}
245
+ onKeyDown={(e) => e.key === "Enter" && addKeyword()}
246
+ className="flex-1"
247
+ />
248
+ <Button size="sm" onClick={addKeyword}>
249
+ <Plus className="w-4 h-4" />
250
+ </Button>
251
+ </div>
252
+ <div className="flex flex-wrap gap-2">
253
+ <AnimatePresence>
254
+ {config.user_keywords.map((keyword) => (
255
+ <motion.div
256
+ key={keyword}
257
+ initial={{ opacity: 0, scale: 0.8 }}
258
+ animate={{ opacity: 1, scale: 1 }}
259
+ exit={{ opacity: 0, scale: 0.8 }}
260
+ >
261
+ <Badge className="flex items-center gap-1 pr-1 bg-primary/20 text-primary border border-primary/30">
262
+ {keyword}
263
+ <button
264
+ onClick={() => removeKeyword(keyword)}
265
+ className="ml-1 p-0.5 rounded hover:bg-destructive/20"
266
+ >
267
+ <X className="w-3 h-3" />
268
+ </button>
269
+ </Badge>
270
+ </motion.div>
271
+ ))}
272
+ </AnimatePresence>
273
+ {config.user_keywords.length === 0 && (
274
+ <span className="text-xs text-muted-foreground italic">No custom keywords added</span>
275
+ )}
276
+ </div>
277
+ </div>
278
+
279
+ {/* Products Section */}
280
+ <div className="p-4 bg-muted/30 rounded-lg border border-border">
281
+ <div className="flex items-center gap-2 mb-3">
282
+ <Package className="w-4 h-4 text-warning" />
283
+ <h3 className="font-semibold">Products to Track</h3>
284
+ <Badge variant="secondary" className="ml-auto">
285
+ {config.user_products.length} products
286
+ </Badge>
287
+ </div>
288
+ <p className="text-xs text-muted-foreground mb-3">
289
+ Track mentions and reviews of specific products
290
+ </p>
291
+ <div className="flex gap-2 mb-3">
292
+ <Input
293
+ placeholder="Enter product name (e.g., 'iPhone 15')"
294
+ value={newProduct}
295
+ onChange={(e) => setNewProduct(e.target.value)}
296
+ onKeyDown={(e) => e.key === "Enter" && addProduct()}
297
+ className="flex-1"
298
+ />
299
+ <Button size="sm" onClick={addProduct}>
300
+ <Plus className="w-4 h-4" />
301
+ </Button>
302
+ </div>
303
+ <div className="flex flex-wrap gap-2">
304
+ <AnimatePresence>
305
+ {config.user_products.map((product) => (
306
+ <motion.div
307
+ key={product}
308
+ initial={{ opacity: 0, scale: 0.8 }}
309
+ animate={{ opacity: 1, scale: 1 }}
310
+ exit={{ opacity: 0, scale: 0.8 }}
311
+ >
312
+ <Badge className="flex items-center gap-1 pr-1 bg-warning/20 text-warning border border-warning/30">
313
+ {product}
314
+ <button
315
+ onClick={() => removeProduct(product)}
316
+ className="ml-1 p-0.5 rounded hover:bg-destructive/20"
317
+ >
318
+ <X className="w-3 h-3" />
319
+ </button>
320
+ </Badge>
321
+ </motion.div>
322
+ ))}
323
+ </AnimatePresence>
324
+ {config.user_products.length === 0 && (
325
+ <span className="text-xs text-muted-foreground italic">No custom products added</span>
326
+ )}
327
+ </div>
328
+ </div>
329
+
330
+ {/* Profiles Section */}
331
+ <div className="p-4 bg-muted/30 rounded-lg border border-border">
332
+ <div className="flex items-center gap-2 mb-3">
333
+ <User className="w-4 h-4 text-success" />
334
+ <h3 className="font-semibold">Profiles to Monitor</h3>
335
+ <Badge variant="secondary" className="ml-auto">
336
+ {Object.values(config.user_profiles).flat().length} profiles
337
+ </Badge>
338
+ </div>
339
+ <p className="text-xs text-muted-foreground mb-3">
340
+ Add social media profiles/pages to monitor
341
+ </p>
342
+
343
+ {/* Platform Selector */}
344
+ <div className="flex gap-2 mb-3">
345
+ {(["twitter", "facebook", "linkedin"] as const).map((platform) => (
346
+ <button
347
+ key={platform}
348
+ onClick={() => setSelectedPlatform(platform)}
349
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${selectedPlatform === platform
350
+ ? platformColors[platform] + " border"
351
+ : "bg-muted text-muted-foreground hover:bg-muted/80"
352
+ }`}
353
+ >
354
+ {platformIcons[platform]}
355
+ {platform.charAt(0).toUpperCase() + platform.slice(1)}
356
+ </button>
357
+ ))}
358
+ </div>
359
+
360
+ <div className="flex gap-2 mb-3">
361
+ <Input
362
+ placeholder={`Enter ${selectedPlatform} username/page`}
363
+ value={newProfile}
364
+ onChange={(e) => setNewProfile(e.target.value)}
365
+ onKeyDown={(e) => e.key === "Enter" && addProfile()}
366
+ className="flex-1"
367
+ />
368
+ <Button size="sm" onClick={addProfile}>
369
+ <Plus className="w-4 h-4" />
370
+ </Button>
371
+ </div>
372
+
373
+ {/* Show profiles by platform */}
374
+ <div className="space-y-3">
375
+ {(["twitter", "facebook", "linkedin"] as const).map((platform) => (
376
+ <div key={platform}>
377
+ <div className="flex items-center gap-2 mb-2">
378
+ {platformIcons[platform]}
379
+ <span className="text-xs font-medium text-muted-foreground uppercase">
380
+ {platform}
381
+ </span>
382
+ <span className="text-xs text-muted-foreground">
383
+ ({config.user_profiles[platform].length})
384
+ </span>
385
+ </div>
386
+ <div className="flex flex-wrap gap-2">
387
+ <AnimatePresence>
388
+ {config.user_profiles[platform].map((profile) => (
389
+ <motion.div
390
+ key={`${platform}-${profile}`}
391
+ initial={{ opacity: 0, scale: 0.8 }}
392
+ animate={{ opacity: 1, scale: 1 }}
393
+ exit={{ opacity: 0, scale: 0.8 }}
394
+ >
395
+ <Badge className={`flex items-center gap-1 pr-1 border ${platformColors[platform]}`}>
396
+ @{profile}
397
+ <button
398
+ onClick={() => removeProfile(platform, profile)}
399
+ className="ml-1 p-0.5 rounded hover:bg-destructive/20"
400
+ >
401
+ <X className="w-3 h-3" />
402
+ </button>
403
+ </Badge>
404
+ </motion.div>
405
+ ))}
406
+ </AnimatePresence>
407
+ {config.user_profiles[platform].length === 0 && (
408
+ <span className="text-xs text-muted-foreground italic">None</span>
409
+ )}
410
+ </div>
411
+ </div>
412
+ ))}
413
+ </div>
414
+ </div>
415
+
416
+ {/* Info Box */}
417
+ <div className="p-3 bg-primary/10 border border-primary/20 rounded-lg">
418
+ <p className="text-xs text-primary">
419
+ <strong>Note:</strong> Your custom targets will be monitored in addition to the default
420
+ competitor profiles (Dialog, SLT, Mobitel, Hutch). Changes take effect on the next
421
+ intelligence collection cycle.
422
+ </p>
423
+ </div>
424
+ </div>
425
+ </Card>
426
+ );
427
+ };
428
+
429
+ export default IntelligenceSettings;
frontend/app/components/map/DistrictInfoPanel.tsx ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Card } from "../ui/card";
2
+ import { Badge } from "../ui/badge";
3
+ import { Separator } from "../ui/separator";
4
+ import { Cloud, Newspaper, TrendingUp, Users, AlertTriangle, MapPin } from "lucide-react";
5
+ import { motion, AnimatePresence } from "framer-motion";
6
+ import { useRogerData } from "../../hooks/use-roger-data";
7
+
8
+ interface DistrictInfoPanelProps {
9
+ district: string | null;
10
+ }
11
+
12
+ const DistrictInfoPanel = ({ district }: DistrictInfoPanelProps) => {
13
+ const { events } = useRogerData();
14
+
15
+ if (!district) {
16
+ return (
17
+ <Card className="p-6 bg-card border-border h-full flex items-center justify-center">
18
+ <div className="text-center text-muted-foreground">
19
+ <MapPin className="w-12 h-12 mx-auto mb-3 opacity-50" />
20
+ <p className="text-sm font-mono">Select a district to view intelligence</p>
21
+ </div>
22
+ </Card>
23
+ );
24
+ }
25
+
26
+ // FIXED: Filter events that relate to this district (with null-safe check)
27
+ const districtEvents = events.filter(e =>
28
+ e.summary?.toLowerCase().includes(district.toLowerCase())
29
+ );
30
+
31
+ // FIXED: Categorize events - include ALL relevant domains
32
+ const alerts = districtEvents.filter(e => e.impact_type === 'risk');
33
+
34
+ // News includes all domains that have district-specific content
35
+ const news = districtEvents.filter(e =>
36
+ ['social', 'intelligence', 'political', 'economical'].includes(e.domain)
37
+ );
38
+
39
+ // FIXED: Weather events should also be filtered by district
40
+ const weatherEvents = districtEvents.filter(e =>
41
+ e.domain === 'weather' || e.domain === 'meteorological'
42
+ );
43
+
44
+ // Calculate risk level
45
+ const criticalAlerts = alerts.filter(e => e.severity === 'critical' || e.severity === 'high');
46
+ const riskLevel = criticalAlerts.length > 0 ? 'high' : alerts.length > 0 ? 'medium' : 'low';
47
+
48
+ // District population data (static for demo)
49
+ const districtData: Record<string, any> = {
50
+ "Colombo": { population: "2.3M", businesses: "15,234", growth: "+5.2%" },
51
+ "Gampaha": { population: "2.4M", businesses: "8,456", growth: "+4.1%" },
52
+ "Kandy": { population: "1.4M", businesses: "5,678", growth: "+3.8%" },
53
+ "Jaffna": { population: "0.6M", businesses: "2,345", growth: "+6.2%" },
54
+ "Galle": { population: "1.1M", businesses: "4,567", growth: "+4.5%" },
55
+ "Kurunegala": { population: "1.6M", businesses: "3,800", growth: "+3.5%" },
56
+ "Matara": { population: "0.8M", businesses: "2,100", growth: "+2.8%" },
57
+ "Ratnapura": { population: "1.1M", businesses: "2,400", growth: "+3.1%" },
58
+ "Badulla": { population: "0.8M", businesses: "1,900", growth: "+2.5%" },
59
+ "Trincomalee": { population: "0.4M", businesses: "1,200", growth: "+4.8%" },
60
+ };
61
+
62
+ const info = districtData[district] || { population: "N/A", businesses: "N/A", growth: "N/A" };
63
+
64
+ return (
65
+ <AnimatePresence mode="wait">
66
+ <motion.div
67
+ key={district}
68
+ initial={{ opacity: 0, x: 20 }}
69
+ animate={{ opacity: 1, x: 0 }}
70
+ exit={{ opacity: 0, x: -20 }}
71
+ transition={{ duration: 0.3 }}
72
+ >
73
+ <Card className="p-6 bg-card border-border space-y-4">
74
+ {/* Header */}
75
+ <div>
76
+ <div className="flex items-center justify-between mb-2">
77
+ <h3 className="text-xl font-bold text-primary">{district}</h3>
78
+ <Badge className={`font-mono border ${riskLevel === 'high' ? 'border-destructive text-destructive' :
79
+ riskLevel === 'medium' ? 'border-warning text-warning' :
80
+ 'border-success text-success'
81
+ }`}>
82
+ {riskLevel.toUpperCase()} RISK
83
+ </Badge>
84
+ </div>
85
+ <p className="text-xs text-muted-foreground font-mono">
86
+ Population: {info.population} | Events: {districtEvents.length}
87
+ </p>
88
+ </div>
89
+
90
+ <Separator className="bg-border" />
91
+
92
+ {/* Live Weather */}
93
+ <div>
94
+ <div className="flex items-center gap-2 mb-2">
95
+ <Cloud className="w-4 h-4 text-info" />
96
+ <h4 className="font-semibold text-sm">WEATHER STATUS</h4>
97
+ </div>
98
+ {weatherEvents.length > 0 ? (
99
+ <div className="space-y-1">
100
+ {weatherEvents.slice(0, 2).map((event, idx) => (
101
+ <div key={idx} className="text-sm bg-muted/30 rounded p-2">
102
+ <p className="font-semibold leading-relaxed">{event.summary || 'No summary'}</p>
103
+ <Badge className="text-xs mt-1">{event.severity}</Badge>
104
+ </div>
105
+ ))}
106
+ </div>
107
+ ) : (
108
+ <p className="text-sm text-muted-foreground">No weather alerts for {district}</p>
109
+ )}
110
+ </div>
111
+
112
+ <Separator className="bg-border" />
113
+
114
+ {/* Active Alerts */}
115
+ <div>
116
+ <div className="flex items-center gap-2 mb-2">
117
+ <AlertTriangle className="w-4 h-4 text-warning" />
118
+ <h4 className="font-semibold text-sm">ACTIVE ALERTS</h4>
119
+ <Badge className="ml-auto text-xs">{alerts.length}</Badge>
120
+ </div>
121
+ <div className="space-y-2 max-h-[200px] overflow-y-auto">
122
+ {alerts.length > 0 ? (
123
+ alerts.slice(0, 5).map((alert, idx) => (
124
+ <div key={idx} className="bg-muted/30 rounded p-2">
125
+ <p className="text-xs font-semibold leading-relaxed">{alert.summary || 'Alert'}</p>
126
+ <div className="flex items-center gap-2 mt-1">
127
+ <Badge
128
+ className={`text-xs ${alert.severity === 'high' || alert.severity === 'critical'
129
+ ? "bg-destructive text-destructive-foreground"
130
+ : "bg-secondary text-secondary-foreground"
131
+ }`}
132
+ >
133
+ {alert.severity?.toUpperCase() || 'MEDIUM'}
134
+ </Badge>
135
+ <span className="text-xs text-muted-foreground">
136
+ {alert.timestamp ? new Date(alert.timestamp).toLocaleTimeString() : 'N/A'}
137
+ </span>
138
+ </div>
139
+ </div>
140
+ ))
141
+ ) : (
142
+ <p className="text-xs text-muted-foreground">No active alerts for {district}</p>
143
+ )}
144
+ </div>
145
+ </div>
146
+
147
+ <Separator className="bg-border" />
148
+
149
+ {/* Recent News */}
150
+ <div>
151
+ <div className="flex items-center gap-2 mb-2">
152
+ <Newspaper className="w-4 h-4 text-primary" />
153
+ <h4 className="font-semibold text-sm">RECENT NEWS</h4>
154
+ </div>
155
+ <div className="space-y-2 max-h-[150px] overflow-y-auto">
156
+ {news.length > 0 ? (
157
+ news.slice(0, 3).map((item, idx) => (
158
+ <div key={idx} className="bg-muted/30 rounded p-2">
159
+ <p className="text-xs font-semibold mb-1 leading-relaxed">{item.summary || 'News'}</p>
160
+ <div className="flex items-center justify-between">
161
+ <span className="text-xs text-muted-foreground">{item.domain}</span>
162
+ <span className="text-xs font-mono text-muted-foreground">
163
+ {item.timestamp ? new Date(item.timestamp).toLocaleTimeString() : 'N/A'}
164
+ </span>
165
+ </div>
166
+ </div>
167
+ ))
168
+ ) : (
169
+ <p className="text-xs text-muted-foreground">No recent news for {district}</p>
170
+ )}
171
+ </div>
172
+ </div>
173
+
174
+ <Separator className="bg-border" />
175
+
176
+ {/* Economic */}
177
+ <div>
178
+ <div className="flex items-center gap-2 mb-2">
179
+ <TrendingUp className="w-4 h-4 text-success" />
180
+ <h4 className="font-semibold text-sm">ECONOMIC</h4>
181
+ </div>
182
+ <div className="grid grid-cols-2 gap-3">
183
+ <div className="bg-muted/30 rounded p-2">
184
+ <p className="text-xs text-muted-foreground">Businesses</p>
185
+ <p className="text-lg font-bold">{info.businesses}</p>
186
+ </div>
187
+ <div className="bg-muted/30 rounded p-2">
188
+ <p className="text-xs text-muted-foreground">Growth</p>
189
+ <p className="text-lg font-bold text-success">{info.growth}</p>
190
+ </div>
191
+ </div>
192
+ </div>
193
+ </Card>
194
+ </motion.div>
195
+ </AnimatePresence>
196
+ );
197
+ };
198
+
199
+ export default DistrictInfoPanel;
frontend/app/components/map/MapView.tsx ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+ import { useState } from "react";
3
+ import SriLankaMap from "./SriLankaMap";
4
+ import DistrictInfoPanel from "./DistrictInfoPanel";
5
+ import { Card } from "../ui/card";
6
+ import { MapPin, Activity } from "lucide-react";
7
+ import { useRogerData } from "../../hooks/use-roger-data";
8
+ import { Badge } from "../ui/badge";
9
+
10
+ const MapView = () => {
11
+ const [selectedDistrict, setSelectedDistrict] = useState<string | null>(null);
12
+ const { events, isConnected } = useRogerData();
13
+
14
+ // Count alerts per district (simplified - matches district names in event summaries)
15
+ const districtAlertCounts: Record<string, number> = {};
16
+
17
+ (events ?? []).forEach(event => {
18
+ const summary = (event.summary ?? '').toLowerCase();
19
+ // Check if district name is mentioned in the event
20
+ ['colombo', 'gampaha', 'kandy', 'jaffna', 'galle', 'matara', 'hambantota',
21
+ 'anuradhapura', 'polonnaruwa', 'batticaloa', 'ampara', 'trincomalee',
22
+ 'kurunegala', 'puttalam', 'kalutara', 'ratnapura', 'kegalle', 'nuwara eliya',
23
+ 'badulla', 'monaragala', 'kilinochchi', 'mannar', 'vavuniya', 'mullaitivu', 'matale'
24
+ ].forEach(district => {
25
+ if (summary.includes(district)) {
26
+ const capitalizedDistrict = district.charAt(0).toUpperCase() + district.slice(1);
27
+ districtAlertCounts[capitalizedDistrict] = (districtAlertCounts[capitalizedDistrict] || 0) + 1;
28
+ }
29
+ });
30
+ });
31
+
32
+ // Count critical events
33
+ const criticalEvents = events.filter(e => e.severity === 'critical' || e.severity === 'high');
34
+
35
+ return (
36
+ <div className="space-y-4">
37
+ <Card className="p-4 sm:p-6 bg-card border-border">
38
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-4">
39
+ <div className="flex items-center gap-2">
40
+ <MapPin className="w-5 h-5 text-primary" />
41
+ <h2 className="text-base sm:text-lg font-bold">TERRITORY MAP</h2>
42
+ </div>
43
+ <div className="flex items-center gap-2 sm:gap-3 flex-wrap">
44
+ {isConnected ? (
45
+ <Badge className="bg-success/20 text-success flex items-center gap-2 text-xs">
46
+ <span className="w-2 h-2 rounded-full bg-success animate-pulse"></span>
47
+ Live
48
+ </Badge>
49
+ ) : (
50
+ <Badge className="bg-warning/20 text-warning text-xs">Reconnecting...</Badge>
51
+ )}
52
+ <Badge className="border border-border flex items-center gap-2 text-xs">
53
+ <Activity className="w-3 h-3" />
54
+ {criticalEvents.length} Critical
55
+ </Badge>
56
+ <span className="text-xs font-mono text-muted-foreground hidden sm:inline">
57
+ Click any district for detailed intelligence
58
+ </span>
59
+ </div>
60
+ </div>
61
+
62
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6">
63
+ <div className="lg:col-span-2 order-1">
64
+ <div className="h-[350px] sm:h-[450px] lg:h-[550px] w-full">
65
+ <SriLankaMap
66
+ selectedDistrict={selectedDistrict}
67
+ onDistrictSelect={setSelectedDistrict}
68
+ alertCounts={districtAlertCounts}
69
+ className="w-full h-full"
70
+ />
71
+ </div>
72
+ </div>
73
+
74
+
75
+ <div className="lg:col-span-1 order-2">
76
+ <DistrictInfoPanel district={selectedDistrict} />
77
+ </div>
78
+ </div>
79
+ </Card>
80
+ </div>
81
+ );
82
+ };
83
+
84
+ export default MapView;
frontend/app/components/map/SatelliteView.tsx ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Card } from '../ui/card';
5
+ import { Badge } from '../ui/badge';
6
+ import { Satellite, Cloud, Wind, Thermometer, Droplets, Gauge, Waves, ExternalLink, RefreshCw, Maximize2 } from 'lucide-react';
7
+
8
+ type LayerType = 'wind' | 'rain' | 'temp' | 'clouds' | 'waves' | 'pressure';
9
+
10
+ interface LayerConfig {
11
+ id: LayerType;
12
+ label: string;
13
+ icon: React.ComponentType<{ className?: string }>;
14
+ overlay: string; // Windy overlay parameter
15
+ description: string;
16
+ }
17
+
18
+ const LAYERS: LayerConfig[] = [
19
+ {
20
+ id: 'wind',
21
+ label: 'Wind',
22
+ icon: Wind,
23
+ overlay: 'wind',
24
+ description: 'Wind speed and direction'
25
+ },
26
+ {
27
+ id: 'rain',
28
+ label: 'Rain',
29
+ icon: Cloud,
30
+ overlay: 'rain',
31
+ description: 'Precipitation forecast'
32
+ },
33
+ {
34
+ id: 'temp',
35
+ label: 'Temperature',
36
+ icon: Thermometer,
37
+ overlay: 'temp',
38
+ description: 'Air temperature'
39
+ },
40
+ {
41
+ id: 'clouds',
42
+ label: 'Clouds',
43
+ icon: Satellite,
44
+ overlay: 'clouds',
45
+ description: 'Cloud cover'
46
+ },
47
+ {
48
+ id: 'waves',
49
+ label: 'Waves',
50
+ icon: Waves,
51
+ overlay: 'waves',
52
+ description: 'Ocean wave height'
53
+ },
54
+ {
55
+ id: 'pressure',
56
+ label: 'Pressure',
57
+ icon: Gauge,
58
+ overlay: 'pressure',
59
+ description: 'Atmospheric pressure'
60
+ }
61
+ ];
62
+
63
+ // Sri Lanka coordinates
64
+ const SRI_LANKA = {
65
+ lat: 7.87,
66
+ lon: 80.77,
67
+ zoom: 7
68
+ };
69
+
70
+ export default function SatelliteView() {
71
+ const [activeLayer, setActiveLayer] = useState<LayerType>('wind');
72
+ const [isFullscreen, setIsFullscreen] = useState(false);
73
+ const [key, setKey] = useState(0);
74
+
75
+ const activeConfig = LAYERS.find(l => l.id === activeLayer) || LAYERS[0];
76
+
77
+ // Windy.com embed URL - officially supported!
78
+ const iframeSrc = `https://embed.windy.com/embed.html?type=map&location=coordinates&metricRain=mm&metricTemp=°C&metricWind=km/h&zoom=${SRI_LANKA.zoom}&overlay=${activeConfig.overlay}&product=ecmwf&level=surface&lat=${SRI_LANKA.lat}&lon=${SRI_LANKA.lon}&detailLat=${SRI_LANKA.lat}&detailLon=${SRI_LANKA.lon}&marker=true&message=true`;
79
+
80
+ const handleRefresh = () => {
81
+ setKey(prev => prev + 1);
82
+ };
83
+
84
+ const handleFullscreen = () => {
85
+ setIsFullscreen(!isFullscreen);
86
+ };
87
+
88
+ const handleOpenExternal = () => {
89
+ window.open(`https://www.windy.com/?${SRI_LANKA.lat},${SRI_LANKA.lon},${SRI_LANKA.zoom}`, '_blank');
90
+ };
91
+
92
+ return (
93
+ <div className={`space-y-4 ${isFullscreen ? 'fixed inset-0 z-50 bg-background p-4' : ''}`}>
94
+ {/* Header */}
95
+ <Card className="p-4 bg-card border-border">
96
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
97
+ <div className="flex items-center gap-3">
98
+ <div className="p-2 rounded-lg bg-primary/20">
99
+ <Satellite className="w-5 h-5 text-primary" />
100
+ </div>
101
+ <div>
102
+ <h2 className="font-bold text-lg">Live Weather Map</h2>
103
+ <p className="text-xs text-muted-foreground">
104
+ Powered by Windy.com • ECMWF Model • Real-time data
105
+ </p>
106
+ </div>
107
+ </div>
108
+
109
+ <div className="flex items-center gap-2">
110
+ <button
111
+ onClick={handleRefresh}
112
+ className="p-2 rounded-lg bg-muted hover:bg-muted/80 transition-colors"
113
+ title="Refresh"
114
+ >
115
+ <RefreshCw className="w-4 h-4" />
116
+ </button>
117
+ <button
118
+ onClick={handleFullscreen}
119
+ className="p-2 rounded-lg bg-muted hover:bg-muted/80 transition-colors"
120
+ title={isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
121
+ >
122
+ <Maximize2 className="w-4 h-4" />
123
+ </button>
124
+ <button
125
+ onClick={handleOpenExternal}
126
+ className="p-2 rounded-lg bg-muted hover:bg-muted/80 transition-colors"
127
+ title="Open in Windy.com"
128
+ >
129
+ <ExternalLink className="w-4 h-4" />
130
+ </button>
131
+ </div>
132
+ </div>
133
+
134
+ {/* Layer Selector */}
135
+ <div className="mt-4 flex flex-wrap gap-2">
136
+ {LAYERS.map((layer) => {
137
+ const Icon = layer.icon;
138
+ const isActive = activeLayer === layer.id;
139
+ return (
140
+ <button
141
+ key={layer.id}
142
+ onClick={() => setActiveLayer(layer.id)}
143
+ className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-all ${isActive
144
+ ? 'bg-primary text-primary-foreground'
145
+ : 'bg-muted hover:bg-muted/80 text-muted-foreground'
146
+ }`}
147
+ title={layer.description}
148
+ >
149
+ <Icon className="w-4 h-4" />
150
+ <span className="hidden sm:inline">{layer.label}</span>
151
+ </button>
152
+ );
153
+ })}
154
+ </div>
155
+
156
+ {/* Active Layer Info */}
157
+ <div className="mt-3 flex items-center gap-2">
158
+ <Badge className="bg-primary/20 text-primary">
159
+ {activeConfig.label}
160
+ </Badge>
161
+ <span className="text-xs text-muted-foreground">
162
+ {activeConfig.description}
163
+ </span>
164
+ </div>
165
+ </Card>
166
+
167
+ {/* Map Container */}
168
+ <Card className={`overflow-hidden bg-card border-border ${isFullscreen ? 'flex-1' : ''}`}>
169
+ <div className={`relative ${isFullscreen ? 'h-[calc(100vh-200px)]' : 'h-[500px] sm:h-[600px]'}`}>
170
+ <iframe
171
+ key={key}
172
+ src={iframeSrc}
173
+ className="w-full h-full border-0"
174
+ title="Windy Weather Map - Sri Lanka"
175
+ loading="lazy"
176
+ allowFullScreen
177
+ />
178
+ </div>
179
+ </Card>
180
+
181
+ {/* Data Attribution */}
182
+ <div className="text-xs text-muted-foreground text-center">
183
+ Data: ECMWF • GFS • ICON • NAM Models
184
+ <span className="mx-2">•</span>
185
+ Powered by{' '}
186
+ <a
187
+ href="https://www.windy.com"
188
+ target="_blank"
189
+ rel="noopener noreferrer"
190
+ className="text-primary hover:underline"
191
+ >
192
+ Windy.com
193
+ </a>
194
+ </div>
195
+ </div>
196
+ );
197
+ }
frontend/app/components/map/SriLankaMap.tsx ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useMemo } from "react";
4
+ import { motion } from "framer-motion";
5
+ import sriLanka from "@svg-maps/sri-lanka";
6
+
7
+ // ------------ Types ------------
8
+
9
+ interface LocationShape {
10
+ id?: string;
11
+ name?: string;
12
+ centroid?: [number, number];
13
+ path?: string;
14
+ paths?: string;
15
+ d?: string;
16
+ geometry?: {
17
+ coordinates?: any;
18
+ };
19
+ }
20
+
21
+ interface SriLankaMapData {
22
+ locations?: LocationShape[];
23
+ features?: LocationShape[];
24
+ viewBox?: string;
25
+ }
26
+
27
+ interface Centroid {
28
+ id: string;
29
+ x: number;
30
+ y: number;
31
+ }
32
+
33
+ interface SriLankaMapProps {
34
+ selectedDistrict: string | null;
35
+ onDistrictSelect: (district: string) => void;
36
+ alertCounts?: Record<string, number>;
37
+ className?: string;
38
+ width?: number | string;
39
+ height?: number | string;
40
+ }
41
+
42
+ // ------------ Component ------------
43
+
44
+ const SriLankaMap: React.FC<SriLankaMapProps> = ({
45
+ selectedDistrict,
46
+ onDistrictSelect,
47
+ alertCounts = {},
48
+ className = "",
49
+ width = "100%",
50
+ height = "100%",
51
+ }) => {
52
+ const mapData = sriLanka as unknown as SriLankaMapData;
53
+
54
+ const safeAlerts = alertCounts || {};
55
+ const totalAlerts = Object.values(safeAlerts).reduce((a, b) => a + (b || 0), 0);
56
+
57
+ // Extract locations safely
58
+ const locations: LocationShape[] = Array.isArray(mapData.locations)
59
+ ? mapData.locations
60
+ : Array.isArray(mapData.features)
61
+ ? mapData.features
62
+ : [];
63
+
64
+ // ------------ Compute Centroids (safe + typed) ------------
65
+
66
+ const centroids = useMemo<Centroid[]>(() => {
67
+ return locations.map((loc) => {
68
+ const id = loc.id ?? loc.name ?? "unknown";
69
+
70
+ // Use existing centroid
71
+ if (
72
+ Array.isArray(loc.centroid) &&
73
+ loc.centroid.length === 2 &&
74
+ typeof loc.centroid[0] === "number" &&
75
+ typeof loc.centroid[1] === "number"
76
+ ) {
77
+ return { id, x: loc.centroid[0], y: loc.centroid[1] };
78
+ }
79
+
80
+ // Compute centroid from SVG path
81
+ const d = loc.path ?? loc.paths ?? loc.d ?? "";
82
+ const nums = d.match(/-?\d+\.?\d*/g)?.map(Number) ?? [];
83
+
84
+ const xs = nums.filter((_, i) => i % 2 === 0);
85
+ const ys = nums.filter((_, i) => i % 2 === 1);
86
+
87
+ const x = xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
88
+ const y = ys.length ? ys.reduce((a, b) => a + b, 0) / ys.length : 0;
89
+
90
+ return { id, x, y };
91
+ });
92
+ }, [locations]);
93
+
94
+ // Helper to find a centroid
95
+ const findCentroid = (id: string): [number, number] => {
96
+ const c = centroids.find((c) => c.id === id);
97
+ return c ? [c.x, c.y] : [0, 0];
98
+ };
99
+
100
+ const viewBox = mapData.viewBox ?? "0 0 600 900";
101
+
102
+ // ------------ Render ------------
103
+
104
+ return (
105
+ <div className={`relative ${className}`} style={{ width, height }}>
106
+ <svg
107
+ viewBox={viewBox}
108
+ width="100%"
109
+ height="100%"
110
+ preserveAspectRatio="xMidYMid meet"
111
+ role="img"
112
+ aria-label="Sri Lanka map by district"
113
+ >
114
+ <g id="base">
115
+ <rect width="100%" height="100%" fill="transparent" />
116
+ </g>
117
+
118
+ <g id="districts">
119
+ {locations.map((loc) => {
120
+ const id = loc.id ?? loc.name ?? "unknown";
121
+ const name = loc.name ?? id;
122
+
123
+ const pathData =
124
+ loc.path ??
125
+ loc.paths ??
126
+ loc.d ??
127
+ (loc.geometry?.coordinates ? "" : "");
128
+
129
+ const alertCount = safeAlerts[name] ?? 0;
130
+ const isSelected = selectedDistrict === name;
131
+ const hasAlerts = alertCount > 0;
132
+
133
+ const fill = isSelected
134
+ ? "hsl(var(--primary) / 0.95)"
135
+ : hasAlerts
136
+ ? "hsl(var(--destructive) / 0.28)"
137
+ : "hsl(var(--muted) / 0.22)";
138
+
139
+ const stroke = isSelected
140
+ ? "hsl(var(--primary))"
141
+ : hasAlerts
142
+ ? "hsl(var(--destructive))"
143
+ : "hsl(var(--border))";
144
+
145
+ const strokeWidth = isSelected ? 2.2 : hasAlerts ? 1.5 : 0.9;
146
+
147
+ const [cx, cy] = findCentroid(id);
148
+
149
+ return (
150
+ <g key={id} id={`grp-${id}`} data-name={name}>
151
+ {pathData ? (
152
+ <motion.path
153
+ id={id}
154
+ d={pathData}
155
+ fill={fill}
156
+ stroke={stroke}
157
+ strokeWidth={strokeWidth}
158
+ className="cursor-pointer"
159
+ onClick={() => onDistrictSelect(name)}
160
+ whileHover={{ opacity: 0.85 }}
161
+ whileTap={{ scale: 0.995 }}
162
+ initial={{ opacity: 0.98 }}
163
+ transition={{ duration: 0.18 }}
164
+ />
165
+ ) : (
166
+ <motion.circle
167
+ cx={cx}
168
+ cy={cy}
169
+ r={10}
170
+ fill={fill}
171
+ stroke={stroke}
172
+ strokeWidth={1}
173
+ onClick={() => onDistrictSelect(name)}
174
+ className="cursor-pointer"
175
+ />
176
+ )}
177
+
178
+ {alertCount > 0 && (
179
+ <motion.g
180
+ initial={{ scale: 0 }}
181
+ animate={{ scale: 1 }}
182
+ transition={{ type: "spring", stiffness: 200 }}
183
+ >
184
+ <circle
185
+ cx={cx}
186
+ cy={cy}
187
+ r={10}
188
+ fill="hsl(var(--destructive))"
189
+ stroke="white"
190
+ strokeWidth={1.2}
191
+ />
192
+ <text
193
+ x={cx}
194
+ y={cy + 4}
195
+ textAnchor="middle"
196
+ fontSize={9}
197
+ fontWeight={700}
198
+ fill="#fff"
199
+ pointerEvents="none"
200
+ >
201
+ {alertCount}
202
+ </text>
203
+ </motion.g>
204
+ )}
205
+ </g>
206
+ );
207
+ })}
208
+ </g>
209
+ </svg>
210
+
211
+ {/* Legend */}
212
+ <div className="absolute top-3 right-3 bg-card/95 border border-border rounded p-2 text-xs text-center">
213
+ <div className="text-lg font-bold text-destructive">{totalAlerts}</div>
214
+ <div className="text-[10px] text-muted-foreground uppercase">
215
+ Total Alerts
216
+ </div>
217
+ </div>
218
+
219
+ <style>{`
220
+ svg .cursor-pointer { cursor: pointer; }
221
+ `}</style>
222
+ </div>
223
+ );
224
+ };
225
+
226
+ export default SriLankaMap;
frontend/app/components/ui/accordion.tsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
3
+ import { ChevronDown } from "lucide-react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ const Accordion = AccordionPrimitive.Root;
8
+
9
+ const AccordionItem = React.forwardRef<
10
+ React.ElementRef<typeof AccordionPrimitive.Item>,
11
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
12
+ >(({ className, ...props }, ref) => (
13
+ <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
14
+ ));
15
+ AccordionItem.displayName = "AccordionItem";
16
+
17
+ const AccordionTrigger = React.forwardRef<
18
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
19
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
20
+ >(({ className, children, ...props }, ref) => (
21
+ <AccordionPrimitive.Header className="flex">
22
+ <AccordionPrimitive.Trigger
23
+ ref={ref}
24
+ className={cn(
25
+ "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
26
+ className,
27
+ )}
28
+ {...props}
29
+ >
30
+ {children}
31
+ <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
32
+ </AccordionPrimitive.Trigger>
33
+ </AccordionPrimitive.Header>
34
+ ));
35
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
36
+
37
+ const AccordionContent = React.forwardRef<
38
+ React.ElementRef<typeof AccordionPrimitive.Content>,
39
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
40
+ >(({ className, children, ...props }, ref) => (
41
+ <AccordionPrimitive.Content
42
+ ref={ref}
43
+ className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
44
+ {...props}
45
+ >
46
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
47
+ </AccordionPrimitive.Content>
48
+ ));
49
+
50
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName;
51
+
52
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
frontend/app/components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
3
+
4
+ import { cn } from "../../lib/utils";
5
+ import { buttonVariants } from "../ui/button";
6
+
7
+ const AlertDialog = AlertDialogPrimitive.Root;
8
+
9
+ const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
10
+
11
+ const AlertDialogPortal = AlertDialogPrimitive.Portal;
12
+
13
+ const AlertDialogOverlay = React.forwardRef<
14
+ React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
15
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
16
+ >(({ className, ...props }, ref) => (
17
+ <AlertDialogPrimitive.Overlay
18
+ className={cn(
19
+ "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
20
+ className,
21
+ )}
22
+ {...props}
23
+ ref={ref}
24
+ />
25
+ ));
26
+ AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
27
+
28
+ const AlertDialogContent = React.forwardRef<
29
+ React.ElementRef<typeof AlertDialogPrimitive.Content>,
30
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
31
+ >(({ className, ...props }, ref) => (
32
+ <AlertDialogPortal>
33
+ <AlertDialogOverlay />
34
+ <AlertDialogPrimitive.Content
35
+ ref={ref}
36
+ className={cn(
37
+ "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
38
+ className,
39
+ )}
40
+ {...props}
41
+ />
42
+ </AlertDialogPortal>
43
+ ));
44
+ AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
45
+
46
+ const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
47
+ <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
48
+ );
49
+ AlertDialogHeader.displayName = "AlertDialogHeader";
50
+
51
+ const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
52
+ <div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
53
+ );
54
+ AlertDialogFooter.displayName = "AlertDialogFooter";
55
+
56
+ const AlertDialogTitle = React.forwardRef<
57
+ React.ElementRef<typeof AlertDialogPrimitive.Title>,
58
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
59
+ >(({ className, ...props }, ref) => (
60
+ <AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
61
+ ));
62
+ AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
63
+
64
+ const AlertDialogDescription = React.forwardRef<
65
+ React.ElementRef<typeof AlertDialogPrimitive.Description>,
66
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
67
+ >(({ className, ...props }, ref) => (
68
+ <AlertDialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
69
+ ));
70
+ AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
71
+
72
+ const AlertDialogAction = React.forwardRef<
73
+ React.ElementRef<typeof AlertDialogPrimitive.Action>,
74
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
75
+ >(({ className, ...props }, ref) => (
76
+ <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
77
+ ));
78
+ AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
79
+
80
+ const AlertDialogCancel = React.forwardRef<
81
+ React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
82
+ React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
83
+ >(({ className, ...props }, ref) => (
84
+ <AlertDialogPrimitive.Cancel
85
+ ref={ref}
86
+ className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
87
+ {...props}
88
+ />
89
+ ));
90
+ AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
91
+
92
+ export {
93
+ AlertDialog,
94
+ AlertDialogPortal,
95
+ AlertDialogOverlay,
96
+ AlertDialogTrigger,
97
+ AlertDialogContent,
98
+ AlertDialogHeader,
99
+ AlertDialogFooter,
100
+ AlertDialogTitle,
101
+ AlertDialogDescription,
102
+ AlertDialogAction,
103
+ AlertDialogCancel,
104
+ };
frontend/app/components/ui/alert.tsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "../../lib/utils";
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-background text-foreground",
12
+ destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ variant: "default",
17
+ },
18
+ },
19
+ );
20
+
21
+ const Alert = React.forwardRef<
22
+ HTMLDivElement,
23
+ React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
24
+ >(({ className, variant, ...props }, ref) => (
25
+ <div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
26
+ ));
27
+ Alert.displayName = "Alert";
28
+
29
+ const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
30
+ ({ className, ...props }, ref) => (
31
+ <h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />
32
+ ),
33
+ );
34
+ AlertTitle.displayName = "AlertTitle";
35
+
36
+ const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
37
+ ({ className, ...props }, ref) => (
38
+ <div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
39
+ ),
40
+ );
41
+ AlertDescription.displayName = "AlertDescription";
42
+
43
+ export { Alert, AlertTitle, AlertDescription };
frontend/app/components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
2
+
3
+ const AspectRatio = AspectRatioPrimitive.Root;
4
+
5
+ export { AspectRatio };
frontend/app/components/ui/avatar.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
3
+
4
+ import { cn } from "../../lib/utils";
5
+
6
+ const Avatar = React.forwardRef<
7
+ React.ElementRef<typeof AvatarPrimitive.Root>,
8
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
9
+ >(({ className, ...props }, ref) => (
10
+ <AvatarPrimitive.Root
11
+ ref={ref}
12
+ className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
13
+ {...props}
14
+ />
15
+ ));
16
+ Avatar.displayName = AvatarPrimitive.Root.displayName;
17
+
18
+ const AvatarImage = React.forwardRef<
19
+ React.ElementRef<typeof AvatarPrimitive.Image>,
20
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
21
+ >(({ className, ...props }, ref) => (
22
+ <AvatarPrimitive.Image ref={ref} className={cn("aspect-square h-full w-full", className)} {...props} />
23
+ ));
24
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
25
+
26
+ const AvatarFallback = React.forwardRef<
27
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
28
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
29
+ >(({ className, ...props }, ref) => (
30
+ <AvatarPrimitive.Fallback
31
+ ref={ref}
32
+ className={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
33
+ {...props}
34
+ />
35
+ ));
36
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
37
+
38
+ export { Avatar, AvatarImage, AvatarFallback };
frontend/app/components/ui/badge.tsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "../../lib/utils";
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
12
+ secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
13
+ destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
14
+ outline: "text-foreground",
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: "default",
19
+ },
20
+ },
21
+ );
22
+
23
+ export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
24
+
25
+ function Badge({ className, variant, ...props }: BadgeProps) {
26
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
27
+ }
28
+
29
+ export { Badge, badgeVariants };
frontend/app/components/ui/breadcrumb.tsx ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ const Breadcrumb = React.forwardRef<
8
+ HTMLElement,
9
+ React.ComponentPropsWithoutRef<"nav"> & {
10
+ separator?: React.ReactNode;
11
+ }
12
+ >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
13
+ Breadcrumb.displayName = "Breadcrumb";
14
+
15
+ const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
16
+ ({ className, ...props }, ref) => (
17
+ <ol
18
+ ref={ref}
19
+ className={cn(
20
+ "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
21
+ className,
22
+ )}
23
+ {...props}
24
+ />
25
+ ),
26
+ );
27
+ BreadcrumbList.displayName = "BreadcrumbList";
28
+
29
+ const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
30
+ ({ className, ...props }, ref) => (
31
+ <li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
32
+ ),
33
+ );
34
+ BreadcrumbItem.displayName = "BreadcrumbItem";
35
+
36
+ const BreadcrumbLink = React.forwardRef<
37
+ HTMLAnchorElement,
38
+ React.ComponentPropsWithoutRef<"a"> & {
39
+ asChild?: boolean;
40
+ }
41
+ >(({ asChild, className, ...props }, ref) => {
42
+ const Comp = asChild ? Slot : "a";
43
+
44
+ return <Comp ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props} />;
45
+ });
46
+ BreadcrumbLink.displayName = "BreadcrumbLink";
47
+
48
+ const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
49
+ ({ className, ...props }, ref) => (
50
+ <span
51
+ ref={ref}
52
+ role="link"
53
+ aria-disabled="true"
54
+ aria-current="page"
55
+ className={cn("font-normal text-foreground", className)}
56
+ {...props}
57
+ />
58
+ ),
59
+ );
60
+ BreadcrumbPage.displayName = "BreadcrumbPage";
61
+
62
+ const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
63
+ <li role="presentation" aria-hidden="true" className={cn("[&>svg]:size-3.5", className)} {...props}>
64
+ {children ?? <ChevronRight />}
65
+ </li>
66
+ );
67
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
68
+
69
+ const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
70
+ <span
71
+ role="presentation"
72
+ aria-hidden="true"
73
+ className={cn("flex h-9 w-9 items-center justify-center", className)}
74
+ {...props}
75
+ >
76
+ <MoreHorizontal className="h-4 w-4" />
77
+ <span className="sr-only">More</span>
78
+ </span>
79
+ );
80
+ BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
81
+
82
+ export {
83
+ Breadcrumb,
84
+ BreadcrumbList,
85
+ BreadcrumbItem,
86
+ BreadcrumbLink,
87
+ BreadcrumbPage,
88
+ BreadcrumbSeparator,
89
+ BreadcrumbEllipsis,
90
+ };
frontend/app/components/ui/button.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
14
+ outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
15
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
16
+ ghost: "hover:bg-accent hover:text-accent-foreground",
17
+ link: "text-primary underline-offset-4 hover:underline",
18
+ },
19
+ size: {
20
+ default: "h-10 px-4 py-2",
21
+ sm: "h-9 rounded-md px-3",
22
+ lg: "h-11 rounded-md px-8",
23
+ icon: "h-10 w-10",
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ variant: "default",
28
+ size: "default",
29
+ },
30
+ },
31
+ );
32
+
33
+ export interface ButtonProps
34
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
35
+ VariantProps<typeof buttonVariants> {
36
+ asChild?: boolean;
37
+ }
38
+
39
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
41
+ const Comp = asChild ? Slot : "button";
42
+ return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
43
+ },
44
+ );
45
+ Button.displayName = "Button";
46
+
47
+ export { Button, buttonVariants };
frontend/app/components/ui/calendar.tsx ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { ChevronLeft, ChevronRight } from "lucide-react";
3
+ import { DayPicker } from "react-day-picker";
4
+
5
+ import { cn } from "../../lib/utils";
6
+ import { buttonVariants } from "../ui/button";
7
+
8
+ export type CalendarProps = React.ComponentProps<typeof DayPicker>;
9
+
10
+ function Calendar({
11
+ className,
12
+ classNames,
13
+ showOutsideDays = true,
14
+ ...props
15
+ }: CalendarProps) {
16
+ return (
17
+ <DayPicker
18
+ showOutsideDays={showOutsideDays}
19
+ className={cn("p-3", className)}
20
+ classNames={{
21
+ months:
22
+ "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
23
+ month: "space-y-4",
24
+ caption: "flex justify-center pt-1 relative items-center",
25
+ caption_label: "text-sm font-medium",
26
+ nav: "space-x-1 flex items-center",
27
+ nav_button: cn(
28
+ buttonVariants({ variant: "outline" }),
29
+ "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
30
+ ),
31
+ nav_button_previous: "absolute left-1",
32
+ nav_button_next: "absolute right-1",
33
+ table: "w-full border-collapse space-y-1",
34
+ head_row: "flex",
35
+ head_cell:
36
+ "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
37
+ row: "flex w-full mt-2",
38
+ cell:
39
+ "h-9 w-9 text-center text-sm p-0 relative " +
40
+ "[&:has([aria-selected].day-range-end)]:rounded-r-md " +
41
+ "[&:has([aria-selected].day-outside)]:bg-accent/50 " +
42
+ "[&:has([aria-selected])]:bg-accent " +
43
+ "first:[&:has([aria-selected])]:rounded-l-md " +
44
+ "last:[&:has([aria-selected])]:rounded-r-md " +
45
+ "focus-within:relative focus-within:z-20",
46
+ day: cn(
47
+ buttonVariants({ variant: "ghost" }),
48
+ "h-9 w-9 p-0 font-normal aria-selected:opacity-100"
49
+ ),
50
+ day_range_end: "day-range-end",
51
+ day_selected:
52
+ "bg-primary text-primary-foreground hover:bg-primary " +
53
+ "hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
54
+ day_today: "bg-accent text-accent-foreground",
55
+ day_outside:
56
+ "day-outside text-muted-foreground opacity-50 " +
57
+ "aria-selected:bg-accent/50 aria-selected:text-muted-foreground " +
58
+ "aria-selected:opacity-30",
59
+ day_disabled: "text-muted-foreground opacity-50",
60
+ day_range_middle:
61
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
62
+ day_hidden: "invisible",
63
+ ...classNames,
64
+ }}
65
+ components={{
66
+ Chevron: (props) => {
67
+ return props.orientation === "left" ? (
68
+ <ChevronLeft className="h-4 w-4" {...props} />
69
+ ) : (
70
+ <ChevronRight className="h-4 w-4" {...props} />
71
+ );
72
+ },
73
+ }}
74
+ {...props}
75
+ />
76
+ );
77
+ }
78
+
79
+ Calendar.displayName = "Calendar";
80
+
81
+ export { Calendar };
frontend/app/components/ui/card.tsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
6
+ <div ref={ref} className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)} {...props} />
7
+ ));
8
+ Card.displayName = "Card";
9
+
10
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
11
+ ({ className, ...props }, ref) => (
12
+ <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
13
+ ),
14
+ );
15
+ CardHeader.displayName = "CardHeader";
16
+
17
+ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
18
+ ({ className, ...props }, ref) => (
19
+ <h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
20
+ ),
21
+ );
22
+ CardTitle.displayName = "CardTitle";
23
+
24
+ const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
25
+ ({ className, ...props }, ref) => (
26
+ <p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
27
+ ),
28
+ );
29
+ CardDescription.displayName = "CardDescription";
30
+
31
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
32
+ ({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
33
+ );
34
+ CardContent.displayName = "CardContent";
35
+
36
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
37
+ ({ className, ...props }, ref) => (
38
+ <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
39
+ ),
40
+ );
41
+ CardFooter.displayName = "CardFooter";
42
+
43
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
frontend/app/components/ui/carousel.tsx ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
3
+ import { ArrowLeft, ArrowRight } from "lucide-react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+ import { Button } from "../../components/ui/button";
7
+
8
+ type CarouselApi = UseEmblaCarouselType[1];
9
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
10
+ type CarouselOptions = UseCarouselParameters[0];
11
+ type CarouselPlugin = UseCarouselParameters[1];
12
+
13
+ type CarouselProps = {
14
+ opts?: CarouselOptions;
15
+ plugins?: CarouselPlugin;
16
+ orientation?: "horizontal" | "vertical";
17
+ setApi?: (api: CarouselApi) => void;
18
+ };
19
+
20
+ type CarouselContextProps = {
21
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
22
+ api: ReturnType<typeof useEmblaCarousel>[1];
23
+ scrollPrev: () => void;
24
+ scrollNext: () => void;
25
+ canScrollPrev: boolean;
26
+ canScrollNext: boolean;
27
+ } & CarouselProps;
28
+
29
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null);
30
+
31
+ function useCarousel() {
32
+ const context = React.useContext(CarouselContext);
33
+
34
+ if (!context) {
35
+ throw new Error("useCarousel must be used within a <Carousel />");
36
+ }
37
+
38
+ return context;
39
+ }
40
+
41
+ const Carousel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & CarouselProps>(
42
+ ({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
43
+ const [carouselRef, api] = useEmblaCarousel(
44
+ {
45
+ ...opts,
46
+ axis: orientation === "horizontal" ? "x" : "y",
47
+ },
48
+ plugins,
49
+ );
50
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
51
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
52
+
53
+ const onSelect = React.useCallback((api: CarouselApi) => {
54
+ if (!api) {
55
+ return;
56
+ }
57
+
58
+ setCanScrollPrev(api.canScrollPrev());
59
+ setCanScrollNext(api.canScrollNext());
60
+ }, []);
61
+
62
+ const scrollPrev = React.useCallback(() => {
63
+ api?.scrollPrev();
64
+ }, [api]);
65
+
66
+ const scrollNext = React.useCallback(() => {
67
+ api?.scrollNext();
68
+ }, [api]);
69
+
70
+ const handleKeyDown = React.useCallback(
71
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
72
+ if (event.key === "ArrowLeft") {
73
+ event.preventDefault();
74
+ scrollPrev();
75
+ } else if (event.key === "ArrowRight") {
76
+ event.preventDefault();
77
+ scrollNext();
78
+ }
79
+ },
80
+ [scrollPrev, scrollNext],
81
+ );
82
+
83
+ React.useEffect(() => {
84
+ if (!api || !setApi) {
85
+ return;
86
+ }
87
+
88
+ setApi(api);
89
+ }, [api, setApi]);
90
+
91
+ React.useEffect(() => {
92
+ if (!api) {
93
+ return;
94
+ }
95
+
96
+ onSelect(api);
97
+ api.on("reInit", onSelect);
98
+ api.on("select", onSelect);
99
+
100
+ return () => {
101
+ api?.off("select", onSelect);
102
+ };
103
+ }, [api, onSelect]);
104
+
105
+ return (
106
+ <CarouselContext.Provider
107
+ value={{
108
+ carouselRef,
109
+ api: api,
110
+ opts,
111
+ orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
112
+ scrollPrev,
113
+ scrollNext,
114
+ canScrollPrev,
115
+ canScrollNext,
116
+ }}
117
+ >
118
+ <div
119
+ ref={ref}
120
+ onKeyDownCapture={handleKeyDown}
121
+ className={cn("relative", className)}
122
+ role="region"
123
+ aria-roledescription="carousel"
124
+ {...props}
125
+ >
126
+ {children}
127
+ </div>
128
+ </CarouselContext.Provider>
129
+ );
130
+ },
131
+ );
132
+ Carousel.displayName = "Carousel";
133
+
134
+ const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
135
+ ({ className, ...props }, ref) => {
136
+ const { carouselRef, orientation } = useCarousel();
137
+
138
+ return (
139
+ <div ref={carouselRef} className="overflow-hidden">
140
+ <div
141
+ ref={ref}
142
+ className={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className)}
143
+ {...props}
144
+ />
145
+ </div>
146
+ );
147
+ },
148
+ );
149
+ CarouselContent.displayName = "CarouselContent";
150
+
151
+ const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
152
+ ({ className, ...props }, ref) => {
153
+ const { orientation } = useCarousel();
154
+
155
+ return (
156
+ <div
157
+ ref={ref}
158
+ role="group"
159
+ aria-roledescription="slide"
160
+ className={cn("min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", className)}
161
+ {...props}
162
+ />
163
+ );
164
+ },
165
+ );
166
+ CarouselItem.displayName = "CarouselItem";
167
+
168
+ const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
169
+ ({ className, variant = "outline", size = "icon", ...props }, ref) => {
170
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
171
+
172
+ return (
173
+ <Button
174
+ ref={ref}
175
+ variant={variant}
176
+ size={size}
177
+ className={cn(
178
+ "absolute h-8 w-8 rounded-full",
179
+ orientation === "horizontal"
180
+ ? "-left-12 top-1/2 -translate-y-1/2"
181
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
182
+ className,
183
+ )}
184
+ disabled={!canScrollPrev}
185
+ onClick={scrollPrev}
186
+ {...props}
187
+ >
188
+ <ArrowLeft className="h-4 w-4" />
189
+ <span className="sr-only">Previous slide</span>
190
+ </Button>
191
+ );
192
+ },
193
+ );
194
+ CarouselPrevious.displayName = "CarouselPrevious";
195
+
196
+ const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
197
+ ({ className, variant = "outline", size = "icon", ...props }, ref) => {
198
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
199
+
200
+ return (
201
+ <Button
202
+ ref={ref}
203
+ variant={variant}
204
+ size={size}
205
+ className={cn(
206
+ "absolute h-8 w-8 rounded-full",
207
+ orientation === "horizontal"
208
+ ? "-right-12 top-1/2 -translate-y-1/2"
209
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
210
+ className,
211
+ )}
212
+ disabled={!canScrollNext}
213
+ onClick={scrollNext}
214
+ {...props}
215
+ >
216
+ <ArrowRight className="h-4 w-4" />
217
+ <span className="sr-only">Next slide</span>
218
+ </Button>
219
+ );
220
+ },
221
+ );
222
+ CarouselNext.displayName = "CarouselNext";
223
+
224
+ export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };
frontend/app/components/ui/chart.tsx ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as RechartsPrimitive from "recharts";
3
+
4
+ import { cn } from "../../lib/utils";
5
+
6
+ // Format: { THEME_NAME: CSS_SELECTOR }
7
+ const THEMES = { light: "", dark: ".dark" } as const;
8
+
9
+ export type ChartConfig = {
10
+ [k: string]: {
11
+ label?: React.ReactNode;
12
+ icon?: React.ComponentType;
13
+ } & (
14
+ | { color?: string; theme?: never }
15
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
16
+ );
17
+ };
18
+
19
+ type ChartContextProps = {
20
+ config: ChartConfig;
21
+ };
22
+
23
+ const ChartContext = React.createContext<ChartContextProps | null>(null);
24
+
25
+ function useChart() {
26
+ const context = React.useContext(ChartContext);
27
+ if (!context) {
28
+ throw new Error("useChart must be used within a <ChartContainer />");
29
+ }
30
+ return context;
31
+ }
32
+
33
+ const ChartContainer = React.forwardRef<
34
+ HTMLDivElement,
35
+ React.ComponentProps<"div"> & {
36
+ config: ChartConfig;
37
+ children: React.ComponentProps<
38
+ typeof RechartsPrimitive.ResponsiveContainer
39
+ >["children"];
40
+ }
41
+ >(({ id, className, children, config, ...props }, ref) => {
42
+ const uniqueId = React.useId();
43
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
44
+
45
+ return (
46
+ <ChartContext.Provider value={{ config }}>
47
+ <div
48
+ data-chart={chartId}
49
+ ref={ref}
50
+ className={cn(
51
+ "flex aspect-video justify-center text-xs " +
52
+ "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground " +
53
+ "[&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 " +
54
+ "[&_.recharts-curve.recharts-tooltip-cursor]:stroke-border " +
55
+ "[&_.recharts-dot[stroke='#fff']]:stroke-transparent " +
56
+ "[&_.recharts-layer]:outline-none " +
57
+ "[&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border " +
58
+ "[&_.recharts-radial-bar-background-sector]:fill-muted " +
59
+ "[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted " +
60
+ "[&_.recharts-reference-line_[stroke='#ccc']]:stroke-border " +
61
+ "[&_.recharts-sector[stroke='#fff']]:stroke-transparent " +
62
+ "[&_.recharts-sector]:outline-none " +
63
+ "[&_.recharts-surface]:outline-none",
64
+ className
65
+ )}
66
+ {...props}
67
+ >
68
+ <ChartStyle id={chartId} config={config} />
69
+ <RechartsPrimitive.ResponsiveContainer>
70
+ {children}
71
+ </RechartsPrimitive.ResponsiveContainer>
72
+ </div>
73
+ </ChartContext.Provider>
74
+ );
75
+ });
76
+ ChartContainer.displayName = "Chart";
77
+
78
+ const ChartStyle = ({
79
+ id,
80
+ config,
81
+ }: {
82
+ id: string;
83
+ config: ChartConfig;
84
+ }) => {
85
+ const colorConfig = Object.entries(config).filter(
86
+ ([_, cfg]) => cfg.theme || cfg.color
87
+ );
88
+
89
+ if (!colorConfig.length) return null;
90
+
91
+ return (
92
+ <style
93
+ dangerouslySetInnerHTML={{
94
+ __html: Object.entries(THEMES)
95
+ .map(
96
+ ([theme, prefix]) => `
97
+ ${prefix} [data-chart=${id}] {
98
+ ${colorConfig
99
+ .map(([key, itemConfig]) => {
100
+ const color =
101
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
102
+ itemConfig.color;
103
+ return color ? ` --color-${key}: ${color};` : "";
104
+ })
105
+ .join("\n")}
106
+ }
107
+ `
108
+ )
109
+ .join("\n"),
110
+ }}
111
+ />
112
+ );
113
+ };
114
+
115
+ /**
116
+ * Expose the raw Recharts Tooltip component to be used in charts.
117
+ * (We keep the raw component export for parity with shadcn patterns.)
118
+ */
119
+ const ChartTooltip = RechartsPrimitive.Tooltip;
120
+
121
+ /* ---------------------------
122
+ Custom Tooltip Content
123
+ - Compatible with Recharts v3
124
+ - Avoids importing removed types (TooltipPayload)
125
+ - Uses a small local type describing what we need from Recharts
126
+ --------------------------- */
127
+
128
+ type RechartsPayloadItem = {
129
+ dataKey?: string | number;
130
+ name?: string;
131
+ value?: any;
132
+ payload?: Record<string, any> | undefined;
133
+ // Recharts provides 'color' at runtime for many series; unknown to types,
134
+ // so treat as optional/any when reading.
135
+ color?: string;
136
+ };
137
+
138
+ type ChartTooltipContentProps = {
139
+ active?: boolean;
140
+ label?: string | number | React.ReactNode;
141
+ payload?: RechartsPayloadItem[];
142
+ labelFormatter?: (label: any, payload?: RechartsPayloadItem[]) => React.ReactNode;
143
+ formatter?: (value: any, name: any, item?: RechartsPayloadItem, index?: number, fullPayload?: any) => React.ReactNode;
144
+ className?: string;
145
+ labelClassName?: string;
146
+ hideLabel?: boolean;
147
+ hideIndicator?: boolean;
148
+ indicator?: "line" | "dot" | "dashed";
149
+ nameKey?: string;
150
+ labelKey?: string;
151
+ // any other props may exist; we don't depend on them
152
+ };
153
+
154
+ const ChartTooltipContent = React.forwardRef<HTMLDivElement, ChartTooltipContentProps>(
155
+ (props, ref) => {
156
+ const {
157
+ active,
158
+ className,
159
+ indicator = "dot",
160
+ hideLabel = false,
161
+ hideIndicator = false,
162
+ label,
163
+ labelFormatter,
164
+ labelClassName,
165
+ formatter,
166
+ nameKey,
167
+ labelKey,
168
+ payload = [],
169
+ } = props;
170
+
171
+ const { config } = useChart();
172
+
173
+ const tooltipLabel = React.useMemo(() => {
174
+ if (hideLabel || !payload?.length) return null;
175
+
176
+ const item = payload[0];
177
+ if (!item) return null;
178
+
179
+ const key = `${labelKey || item.dataKey || item.name || "value"}`;
180
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
181
+
182
+ const value =
183
+ !labelKey && typeof label === "string"
184
+ ? (config[label as string]?.label ?? label)
185
+ : itemConfig?.label;
186
+
187
+ if (!value) return null;
188
+
189
+ if (labelFormatter) {
190
+ return (
191
+ <div className={cn("font-medium", labelClassName)}>
192
+ {labelFormatter(value, payload)}
193
+ </div>
194
+ );
195
+ }
196
+
197
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>;
198
+ }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
199
+
200
+ if (!active || !payload?.length) return null;
201
+
202
+ const nestLabel = payload.length === 1 && indicator !== "dot";
203
+
204
+ return (
205
+ <div
206
+ ref={ref}
207
+ className={cn(
208
+ "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
209
+ className
210
+ )}
211
+ >
212
+ {!nestLabel && tooltipLabel}
213
+ <div className="grid gap-1.5">
214
+ {payload.map((item, index) => {
215
+ const key = `${nameKey || item.name || item.dataKey || "value"}`;
216
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
217
+
218
+ // Recharts v3 exposes `color` at runtime for many series, but types may not.
219
+ // Read it defensively:
220
+ const indicatorColor = (item && (item.color ?? item.payload?.fill)) ?? undefined;
221
+
222
+ return (
223
+ <div
224
+ key={String(item.dataKey ?? index)}
225
+ className={cn(
226
+ "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
227
+ indicator === "dot" && "items-center"
228
+ )}
229
+ >
230
+ {formatter && item.value !== undefined && item.name ? (
231
+ formatter(item.value, item.name, item, index, item.payload)
232
+ ) : (
233
+ <>
234
+ {itemConfig?.icon ? (
235
+ <itemConfig.icon />
236
+ ) : (
237
+ !hideIndicator && (
238
+ <div
239
+ className={cn("shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]", {
240
+ "h-2.5 w-2.5": indicator === "dot",
241
+ "w-1": indicator === "line",
242
+ "w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
243
+ "my-0.5": nestLabel && indicator === "dashed",
244
+ })}
245
+ style={
246
+ {
247
+ "--color-bg": indicatorColor,
248
+ "--color-border": indicatorColor,
249
+ } as React.CSSProperties
250
+ }
251
+ />
252
+ )
253
+ )}
254
+ <div
255
+ className={cn("flex flex-1 justify-between leading-none", nestLabel ? "items-end" : "items-center")}
256
+ >
257
+ <div className="grid gap-1.5">
258
+ {nestLabel && tooltipLabel}
259
+ <span className="text-muted-foreground">{itemConfig?.label ?? item.name}</span>
260
+ </div>
261
+
262
+ {item.value !== undefined && (
263
+ <span className="font-mono font-medium tabular-nums text-foreground">
264
+ {typeof item.value === "number" ? item.value.toLocaleString() : String(item.value)}
265
+ </span>
266
+ )}
267
+ </div>
268
+ </>
269
+ )}
270
+ </div>
271
+ );
272
+ })}
273
+ </div>
274
+ </div>
275
+ );
276
+ }
277
+ );
278
+ ChartTooltipContent.displayName = "ChartTooltip";
279
+
280
+ /* ---------------------------
281
+ Legend
282
+ - For Recharts v3 the Legend payload shape can differ; be defensive and use any[]
283
+ --------------------------- */
284
+
285
+ const ChartLegend = RechartsPrimitive.Legend;
286
+
287
+ const ChartLegendContent = React.forwardRef<
288
+ HTMLDivElement,
289
+ React.ComponentProps<"div"> & {
290
+ payload?: any[]; // Recharts v3 payload shape varies; use any[]
291
+ hideIcon?: boolean;
292
+ nameKey?: string;
293
+ verticalAlign?: "top" | "bottom";
294
+ }
295
+ >(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
296
+ const { config } = useChart();
297
+
298
+ if (!payload?.length) return null;
299
+
300
+ return (
301
+ <div
302
+ ref={ref}
303
+ className={cn("flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}
304
+ >
305
+ {payload.map((item: any) => {
306
+ const key = `${nameKey || item.dataKey || "value"}`;
307
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
308
+
309
+ return (
310
+ <div
311
+ key={String(item.value ?? item.dataKey ?? Math.random())}
312
+ className={cn("flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground")}
313
+ >
314
+ {itemConfig?.icon && !hideIcon ? (
315
+ <itemConfig.icon />
316
+ ) : (
317
+ <div
318
+ className="h-2 w-2 shrink-0 rounded-[2px]"
319
+ style={{ backgroundColor: item.color ?? undefined }}
320
+ />
321
+ )}
322
+ {itemConfig?.label ?? item.value ?? item.dataKey}
323
+ </div>
324
+ );
325
+ })}
326
+ </div>
327
+ );
328
+ });
329
+ ChartLegendContent.displayName = "ChartLegend";
330
+
331
+ /* ---------------------------
332
+ Helpers
333
+ --------------------------- */
334
+
335
+ function getPayloadConfigFromPayload(config: ChartConfig, payload: any, key: string) {
336
+ if (typeof payload !== "object" || payload === null) return undefined;
337
+
338
+ const payloadPayload =
339
+ "payload" in payload && typeof payload.payload === "object" && payload.payload !== null ? payload.payload : undefined;
340
+
341
+ let configLabelKey: string = key;
342
+
343
+ if (key in payload && typeof payload[key] === "string") {
344
+ configLabelKey = payload[key];
345
+ } else if (payloadPayload && key in payloadPayload && typeof payloadPayload[key] === "string") {
346
+ configLabelKey = payloadPayload[key];
347
+ }
348
+
349
+ return (config as any)[configLabelKey] ?? (config as any)[key];
350
+ }
351
+
352
+ /* ---------------------------
353
+ Exports
354
+ --------------------------- */
355
+
356
+ export {
357
+ ChartContainer,
358
+ ChartTooltip,
359
+ ChartTooltipContent,
360
+ ChartLegend,
361
+ ChartLegendContent,
362
+ ChartStyle,
363
+ };
frontend/app/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
+ import { Check } from "lucide-react";
4
+
5
+ import { cn } from "../../lib/utils";
6
+
7
+ const Checkbox = React.forwardRef<
8
+ React.ElementRef<typeof CheckboxPrimitive.Root>,
9
+ React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
+ >(({ className, ...props }, ref) => (
11
+ <CheckboxPrimitive.Root
12
+ ref={ref}
13
+ className={cn(
14
+ "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
15
+ className,
16
+ )}
17
+ {...props}
18
+ >
19
+ <CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
20
+ <Check className="h-4 w-4" />
21
+ </CheckboxPrimitive.Indicator>
22
+ </CheckboxPrimitive.Root>
23
+ ));
24
+ Checkbox.displayName = CheckboxPrimitive.Root.displayName;
25
+
26
+ export { Checkbox };