diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..af3893485aa4093b9adaac626ef9be8729e04931 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,87 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.venv/ +venv/ +ENV/ +env/ + +# Node +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* +frontend/dist/ +frontend/node_modules/ +website/build/ +website/node_modules/ +website/.docusaurus/ + +# Data and cache +data/ +cache/ +logs/ +output/ +*.db +*.sqlite + +# Git +.git/ +.gitignore +.github/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# Documentation (we copy specific dirs) +docs/ +*.md +!README.md + +# Test and development +tests/ +examples/ +notebooks/ +*.ipynb + +# Large files +*.zip +*.tar.gz +*.mp4 +*.avi +*.mov + +# Environment +.env +.env.local +*.key +*.pem + +# OS +.DS_Store +Thumbs.db + +# Build artifacts +build/ +dist/ +*.egg-info/ + +# Temporary +tmp/ +temp/ +*.tmp + +# Scripts we don't need in container +start-all.sh +stop-all.sh +deploy-huggingface.sh +test-huggingface-build.sh +migrate-docs.sh +install.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..92fc1b2b9dc11d5b78584404b82438853e691d7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,100 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +!frontend/src/lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Node.js +node_modules/ + +# Virtual environments +venv/ +env/ +ENV/ +.venv +.venv-intel + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Environment variables +.env +.env.local +.env.*.local +.env.production +.env.development +.env.test + +# Credentials and secrets +credentials.json +service-account.json +*_credentials.json +auth.json +token.json +*_token.txt +*-token.txt +gcp-*.json +bigquery-*.json +.gcp/ + +# Logs +logs/ +*.log + +# Jupyter Notebook +.ipynb_checkpoints +*.ipynb + +# Data files +data/ +.migration_backup/ +*.csv +*.parquet +*.delta + +# Delta Lake +_delta_log/ + +# Testing +.coverage +htmlcov/ +.pytest_cache/ +.tox/ + +# Documentation +docs/_build/ + +# Databricks +.databricks/ + +# Secrets +secrets/ +*.key +*.pem + +# OS +Thumbs.db +# Binary files for Docker build only (not in git) +website/static/img/communityone_card.png diff --git a/.huggingface/README.md b/.huggingface/README.md new file mode 100644 index 0000000000000000000000000000000000000000..87f182b6345f51d2a69b8c316e7028b2aa37dc1d --- /dev/null +++ b/.huggingface/README.md @@ -0,0 +1,101 @@ +--- +title: CommunityOne - Open Navigator +emoji: 🏛️ +colorFrom: blue +colorTo: green +sdk: docker +app_port: 7860 +pinned: false +license: apache-2.0 +tags: + - civic-engagement + - policy-tracking + - government-transparency + - nonprofit-discovery + - open-data +--- + +# 🏛️ CommunityOne - Open Navigator + +**Track 90,000+ jurisdictions. Monitor 1.8M nonprofits. Amplify your voice.** + +CommunityOne is a civic engagement platform that helps you discover advocacy opportunities, track policy changes, and connect with organizations working on the causes you care about. + +## ✨ Features + +- **🔍 Unified Search**: Find contacts, meetings, organizations, and causes across the entire United States +- **📊 Real-time Stats**: Track policy activity across 90,000+ cities, counties, and states +- **🏢 Nonprofit Discovery**: Explore 1.8M organizations from IRS data enriched with Every.org +- **📅 Meeting Minutes**: Search 250,000+ government meeting transcripts and agendas +- **🎯 Geographic Filtering**: Browse by state, county, or city to find local opportunities +- **🔐 OAuth Login**: Sign in with HuggingFace, GitHub, or Google to save your preferences + +## 🚀 Three Services Architecture + +This deployment runs three integrated services: + +1. **📚 Documentation** (Docusaurus) - `/docs/` +2. **🖥️ Main Application** (React + Vite) - `/` +3. **⚡ API Backend** (FastAPI) - `/api/` + +All services are reverse-proxied through nginx on port 7860. + +## 📖 Quick Start + +### Browse Without Login +- Click "Browse All" to explore data by state +- Use the search bar to find organizations, contacts, or causes +- Filter by location using the state/county/city selectors + +### Sign In for Personalization +- Click "Login" in the top right +- Choose your OAuth provider (HuggingFace, GitHub, or Google) +- Follow organizations, leaders, and causes you care about +- Get personalized recommendations + +### Explore the API +- Visit `/redoc` for interactive API documentation +- Try the search endpoints with state filters +- Export data in JSON format for your own projects + +## 🛠️ Technology Stack + +- **Frontend**: React 18 + TypeScript + Vite + TailwindCSS + shadcn/ui +- **Backend**: Python 3.11 + FastAPI + Pydantic +- **Data**: Delta Lake + Parquet (90GB+ of civic data) +- **Docs**: Docusaurus v3 +- **Infrastructure**: nginx + supervisor + Docker + +## 📊 Data Sources + +- **IRS BMF**: 1.8M tax-exempt organizations +- **Every.org**: Nonprofit enrichment (logos, causes, revenue) +- **Open States**: State legislators and bills (7,300+ officials) +- **Census**: Jurisdictions and boundaries (90,000+) +- **CityScrapers**: Local government meetings +- **OpenCivicData**: Standardized government data + +## 🔗 Links + +- **Repository**: [github.com/getcommunityone/open-navigator](https://github.com/getcommunityone/open-navigator) +- **Documentation**: Click "📚 Browse Documentation" on the homepage +- **API Docs**: `/redoc` endpoint +- **Website**: [www.communityone.com](https://www.communityone.com) + +## 📝 License + +Apache License 2.0 - Free for commercial and non-commercial use + +## 🤝 Contributing + +We welcome contributions! See CONTRIBUTING.md in the repository for guidelines. + +## 💬 Support + +- **Issues**: [GitHub Issues](https://github.com/getcommunityone/open-navigator/issues) +- **Discussions**: [GitHub Discussions](https://github.com/getcommunityone/open-navigator/discussions) +- **Email**: hello@communityone.com + +--- + +Built with ❤️ for civic engagement and government transparency. diff --git a/.huggingface/nginx.conf b/.huggingface/nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..258d74441e574d71b7d9387c60b00e787fc0445c --- /dev/null +++ b/.huggingface/nginx.conf @@ -0,0 +1,125 @@ +worker_processes auto; +pid /tmp/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + access_log /app/logs/nginx-access.log; + error_log /app/logs/nginx-error.log; + + # Performance + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 50M; + + # Compression + gzip on; + gzip_vary on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + upstream fastapi_backend { + server 127.0.0.1:8000; + } + + server { + listen 7860; + server_name _; + + # Force HTTPS - HSTS header tells browsers to ALWAYS use HTTPS + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # Additional security headers + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + + # Documentation - serve static files built by Docusaurus + location /docs { + alias /app/static/docs; + try_files $uri $uri/ /docs/index.html; + + # Cache static assets - shorter for easier updates + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1d; + add_header Cache-Control "public, max-age=86400"; + } + } + + # API backend at /api/ + location /api/ { + proxy_pass http://fastapi_backend/api/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 300s; + proxy_connect_timeout 75s; + } + + # API docs - route /api/docs to backend /docs + location = /api/docs { + proxy_pass http://fastapi_backend/docs; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # API OpenAPI schema - support both /api/openapi.json and /openapi.json + location ~ ^/(api/)?(openapi\.json|redoc) { + proxy_pass http://fastapi_backend/$2; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Frontend assets - shorter cache for easier updates + location /assets/ { + alias /app/static/frontend/assets/; + expires 1d; + add_header Cache-Control "public, max-age=86400"; + } + + # Main frontend app at root + location / { + root /app/static/frontend; + try_files $uri $uri/ /index.html; + + # NEVER cache index.html - force browser to check for new version + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + + # Cache hashed assets (immutable) but shorter time for easier updates + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1d; + add_header Cache-Control "public, max-age=86400"; + } + } + + # Health check endpoint + location /health { + access_log off; + return 200 "OK"; + add_header Content-Type text/plain; + } + } +} diff --git a/.huggingface/start.sh b/.huggingface/start.sh new file mode 100755 index 0000000000000000000000000000000000000000..189211a3ef2096da386bd93c93ddb87329d27428 --- /dev/null +++ b/.huggingface/start.sh @@ -0,0 +1,61 @@ +#!/bin/bash +set -e + +echo "🚀 Starting CommunityOne on Hugging Face Spaces..." +echo "📊 Three services architecture:" +echo " 1. Documentation (Docusaurus) - Port 3000" +echo " 2. Main Application (React + Vite) - Port 5173" +echo " 3. API Backend (FastAPI) - Port 8000" +echo " 4. Nginx Reverse Proxy - Port 7860 (HF Spaces public port)" +echo "" + +# DEBUG: Check environment variable +echo "🔍 Environment Check:" +echo " HF_SPACES = ${HF_SPACES:-NOT SET}" +if [ "$HF_SPACES" = "1" ]; then + echo " ✅ HF_SPACES is correctly set to 1" +else + echo " ❌ WARNING: HF_SPACES is not set to 1" + echo " Setting HF_SPACES=1 now..." + export HF_SPACES=1 +fi +echo "" + +# Create required directories +mkdir -p /app/logs /app/data /var/log/supervisor + +# Verify static files exist +echo "📁 Verifying static files..." +if [ -d "/app/static/docs" ]; then + echo "✅ Documentation static files found" + ls -lh /app/static/docs/ | head -5 +else + echo "❌ ERROR: Documentation static files missing at /app/static/docs" + exit 1 +fi + +if [ -d "/app/static/frontend" ]; then + echo "✅ Frontend static files found" + ls -lh /app/static/frontend/ | head -5 +else + echo "❌ ERROR: Frontend static files missing at /app/static/frontend" + exit 1 +fi + +# Install serve for static file hosting (if not already installed) +if ! command -v serve &> /dev/null; then + echo "📦 Installing serve for static file hosting..." + npm install -g serve +fi + +# Test nginx configuration +echo "🔧 Testing nginx configuration..." +nginx -t + +# Initialize database if needed +echo "💾 Initializing database..." +python -c "from api.database import init_db; init_db()" || echo "⚠️ Database init skipped" + +# Start all services with supervisor +echo "🎬 Starting all services with supervisor..." +exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf diff --git a/.huggingface/supervisord.conf b/.huggingface/supervisord.conf new file mode 100644 index 0000000000000000000000000000000000000000..d90e1c942749a0d2ea3c29a17197b70a7e316260 --- /dev/null +++ b/.huggingface/supervisord.conf @@ -0,0 +1,28 @@ +[supervisord] +nodaemon=true +logfile=/dev/stdout +logfile_maxbytes=0 +pidfile=/tmp/supervisord.pid +user=root + +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +priority=10 + +[program:fastapi] +command=uvicorn api.main:app --host 0.0.0.0 --port 8000 --log-level info --proxy-headers +directory=/app +autostart=true +autorestart=true +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +environment=PYTHONUNBUFFERED="1",HF_SPACES="1" +priority=20 diff --git a/CITATIONS.md b/CITATIONS.md new file mode 100644 index 0000000000000000000000000000000000000000..b25edae392d46813a5b891010c65bff5dbf06b53 --- /dev/null +++ b/CITATIONS.md @@ -0,0 +1,1474 @@ +# Citations and Acknowledgments + +This project uses several open datasets and research contributions. Please cite the following works when using or referencing this project. + +## 📚 **Datasets** + +### **MeetingBank Dataset** + +We use the MeetingBank benchmark dataset for meeting summarization and analysis. + +**Citation:** +``` +Yebowen Hu, Tim Ganter, Hanieh Deilamsalehy, Franck Dernoncourt, Hassan Foroosh, Fei Liu. +"MeetingBank: A Benchmark Dataset for Meeting Summarization" +In Proceedings of the 61st Annual Meeting of the Association for Computational Linguistics (ACL), +July 2023, Toronto, Canada. +``` + +**BibTeX:** +```bibtex +@inproceedings{hu-etal-2023-meetingbank, + title = "MeetingBank: A Benchmark Dataset for Meeting Summarization", + author = "Yebowen Hu and Tim Ganter and Hanieh Deilamsalehy and Franck Dernoncourt and Hassan Foroosh and Fei Liu", + booktitle = "Proceedings of the 61st Annual Meeting of the Association for Computational Linguistics (ACL)", + month = July, + year = "2023", + address = "Toronto, Canada", + publisher = "Association for Computational Linguistics", +} +``` + +**Resources:** +- Paper: https://arxiv.org/abs/2305.17529 +- Dataset: https://huggingface.co/datasets/huuuyeah/meetingbank +- Zenodo: https://zenodo.org/record/7989108 + +**What we use:** +- 1,366 city council meetings from 6 U.S. cities +- Meeting transcripts and summaries +- Used for: Meeting discovery, transcript analysis, summarization benchmarking + +--- + +## 🗂️ **Other Data Sources** + +### **U.S. Census Bureau** +- Geographic boundaries and demographic data +- Source: https://www.census.gov/ +- License: Public Domain (U.S. Government) + +### **Open States / Plural Policy** ⭐ +- Comprehensive state and local legislative information +- Organization: Plural Policy (formerly Open States Foundation) +- Source: https://openstates.org/ +- API: https://openstates.org/api/ +- Data Downloads: https://open.pluralpolicy.com/data/ +- License: Various (check per state) +- API Key: Required for access (free tier: 50,000 requests/month) + +**Coverage:** +- All 50 states + DC + Puerto Rico +- 7,300+ state legislators +- Millions of bills, votes, and legislative sessions +- Monthly PostgreSQL database dumps (9.8GB+) + +**What we use:** +- Bulk legislative session downloads (CSV/JSON/PostgreSQL) +- State legislator data with committee assignments +- Bill tracking and voting records +- Legislative video sources (YouTube channels, Granicus portals) + +**Resources:** +- Open Data: https://open.pluralpolicy.com/data/ +- Scrapers Repository: https://github.com/openstates/openstates-scrapers +- Local Database Setup: https://docs.openstates.org/contributing/local-database/ +- Code of Conduct: https://docs.openstates.org/code-of-conduct/ +- Schema Documentation: https://github.com/openstates/people/blob/master/schema.md + +**BibTeX:** +```bibtex +@software{openstates, + title = {Open States}, + author = {{Plural Policy}}, + year = {2024}, + url = {https://openstates.org/}, + note = {Comprehensive state legislative data for all 50 U.S. states} +} +``` + +**Potential Contributions:** +- Our scraper patterns could be contributed to openstates-scrapers +- Video source discovery could enhance their data +- We follow their Code of Conduct for all contributions + +### **LegiScan** ⭐ +- Comprehensive legislative tracking and bill text database +- Organization: LegiScan LLC +- Source: https://legiscan.com/ +- API: https://legiscan.com/legiscan +- License: API access requires subscription (free tier available with limitations) +- Coverage: All 50 states + U.S. Congress + Washington D.C. +- API Key: Required for access (free tier: limited requests) + +**Coverage:** +- Real-time legislative tracking for all U.S. states and Congress +- Full bill text, amendments, and legislative documents +- Roll call votes and voting records +- Committee assignments and hearings +- Bill status tracking and history +- Sponsor and co-sponsor information +- Bill text in PDF, HTML, and plain text formats + +**What we use:** +- Bill text downloads and full-text search +- Legislative document archives +- Bill status and tracking data +- Voting records and roll calls +- Supplement to Open States data for missing jurisdictions +- Historical legislative data back to 2011 + +**API Features:** +- GetBillText: Retrieve full bill text in multiple formats +- GetBill: Detailed bill metadata and status +- GetRollCall: Voting records with legislator positions +- GetSponsor: Sponsor and co-sponsor information +- Search: Full-text search across all bills +- GetDatasetList: Bulk dataset downloads + +**Resources:** +- API Documentation: https://legiscan.com/legiscan +- Dataset Downloads: https://legiscan.com/datasets +- Search Interface: https://legiscan.com/gaits/search +- State Coverage: https://legiscan.com/legiscan/states + +**BibTeX:** +```bibtex +@software{legiscan, + title = {LegiScan}, + author = {{LegiScan LLC}}, + year = {2024}, + url = {https://legiscan.com/}, + note = {Comprehensive legislative tracking and bill text database covering all 50 U.S. states and Congress} +} +``` + +**Complementary to Open States:** +- LegiScan provides bill text in multiple formats (PDF, HTML, plain text) +- Historical data back to 2011 for all states +- Real-time updates and notifications +- More comprehensive document archives +- Paid API provides higher rate limits and bulk downloads +- Use LegiScan for bill text analysis, Open States for structured legislative data + +### **Harvard Dataverse** +- Meeting datasets and civic engagement research +- Source: https://dataverse.harvard.edu/ +- License: Varies by dataset + +### **City Scrapers** ⭐ +- Open source civic tech project for scraping local government meetings +- Organization: Documenters.org / City Bureau +- Source: https://cityscrapers.org/ +- GitHub: https://github.com/city-scrapers +- License: MIT License (open source) +- Coverage: Chicago, Pittsburgh, Detroit, Cleveland, Los Angeles (250+ government agencies) +- What we use: Validated meeting URLs, Legistar/Granicus platform endpoints, spider code for scraper patterns +- Used for: Meeting discovery, URL extraction, platform detection, scraper validation + +**City Scrapers Repositories:** +- Chicago: https://github.com/city-scrapers/city-scrapers (~100 agencies) +- Pittsburgh: https://github.com/city-scrapers/city-scrapers-pitt (~30 agencies) +- Detroit: https://github.com/city-scrapers/city-scrapers-detroit (~40 agencies) +- Cleveland: https://github.com/city-scrapers/city-scrapers-cle (~30 agencies) +- Los Angeles: https://github.com/city-scrapers/city-scrapers-la (~50 agencies) + +**BibTeX:** +```bibtex +@software{city_scrapers, + title = {City Scrapers}, + author = {{Documenters.org}}, + year = {2024}, + url = {https://cityscrapers.org/}, + note = {Open source civic tech project providing validated scrapers for local government meetings across major U.S. cities} +} +``` + +### **Google Civic Information API** ⭐ +- Government officials, polling locations, and election data +- Organization: Google LLC +- API Documentation: https://developers.google.com/civic-information +- License: Free (with quota limits) +- Rate Limit: 25,000 requests/day (free tier) +- Coverage: U.S. federal, state, and local government officials; polling locations; election data +- What we use: Elected officials by address, representative contact info, voting districts +- Used for: Contact discovery, official verification, civic engagement tools + +**API Endpoints Used:** +- Representatives by Address: Get all elected officials for a given address +- Elections: Voter information, polling locations, ballot information +- Divisions: Geographic/political divisions (OCD-IDs) + +**BibTeX:** +```bibtex +@misc{google_civic_api, + title = {Google Civic Information API}, + author = {{Google LLC}}, + year = {2024}, + url = {https://developers.google.com/civic-information}, + note = {API providing government official contact information, election data, and polling locations} +} +``` + +**Terms of Service:** +- Attribution required when displaying official data +- Caching limited to 30 days +- Must comply with Google API Terms of Service + +### **YouTube Data API v3** ⭐ +- Video metadata, channel information, and search for government meetings +- Organization: Google LLC +- API Documentation: https://developers.google.com/youtube/v3 +- License: Free (with quota limits) +- Rate Limit: 10,000 units/day (free tier), search costs 100 units per request +- Coverage: Global video platform with millions of government channels +- What we use: Government channel discovery, meeting video metadata, transcript availability +- Used for: Video discovery, channel statistics, meeting video archival + +**API Features Used:** +- Search: Find government channels by jurisdiction name +- Channels: Get channel metadata, subscriber counts, video counts +- Videos: Metadata including title, description, upload date, duration +- Captions: Check for closed caption/transcript availability + +**BibTeX:** +```bibtex +@misc{youtube_data_api, + title = {YouTube Data API v3}, + author = {{Google LLC}}, + year = {2024}, + url = {https://developers.google.com/youtube/v3}, + note = {API for accessing YouTube video metadata, channel information, and search functionality} +} +``` + +**Terms of Service:** +- YouTube API Services Terms: https://developers.google.com/youtube/terms/api-services-terms-of-service +- Attribution required with YouTube logo +- Quota limits enforced (10,000 units/day free) +- Video embeds must use official YouTube player + +### **Ballotpedia** ⭐ +- Ballot measures, referendums, and propositions +- Organization: Lucy Burns Institute +- Source: https://ballotpedia.org/ +- API: https://ballotpedia.org/API-documentation +- License: API access is limited at scale (paid tier available) +- Coverage: All 50 states, historical measures back to 1990s +- Used for: Tracking fluoridation votes, school bond measures, health policy propositions + +### **MIT Election Data + Science Lab** +- Presidential, Congressional, and gubernatorial election results +- Organization: Massachusetts Institute of Technology +- Source: https://electionlab.mit.edu/data +- Repository: https://github.com/MEDSL/official-returns +- License: Free for research and commercial use +- Coverage: 1976-present, county-level results +- Used for: Political composition analysis, jurisdiction context + +### **OpenElections** +- State-by-state certified election results in standardized CSV format +- Source: https://openelections.net/ +- GitHub: https://github.com/openelections +- License: Open source (various by state) +- Coverage: All 50 states (various completion levels), precinct-level data +- Used for: Detailed election results, local race outcomes, advocacy targeting + +### **Open Civic Data (OCD) Standards** +- Division identifiers and civic data standards +- Specification: https://open-civic-data.readthedocs.io/en/latest/proposals/0002.html +- Repository: https://github.com/opencivicdata/ocd-division-ids +- License: Open source +- Used for: Standardized jurisdiction identifiers, cross-platform compatibility + +### **Popolo Project** +- International open government data specification for people, organizations, and elected positions +- Specification: https://www.popoloproject.com/ +- GitHub: https://github.com/popolo-project/popolo-spec +- Documentation: http://www.popoloproject.com/specs/ +- License: Creative Commons Attribution 4.0 International + +### **BillMap** ⭐ +- Tracks bill text similarity across all 50 U.S. states to identify copy-paste legislation and model bill influence +- Organization: Sunlight Foundation / @unitedstates community +- Repository: https://github.com/unitedstates/BillMap +- Research: Anderson et al., "Detecting Policy Influence in Legislatures" (2019) +- Paper: https://arxiv.org/abs/1906.03699 +- Live Demo: https://billmap.cs.princeton.edu/ +- License: Open source +- Coverage: All 50 states, tracks legislative text diffusion across jurisdictions +- Used for: Identifying model legislation, tracking policy influence, finding similar bills across states +- Method: Text similarity analysis, n-gram matching, bill text alignment + +**What we use:** +- Bill similarity detection algorithms +- Model legislation tracking methodology +- Cross-state policy diffusion analysis +- Legislative text comparison techniques + +**BibTeX:** +```bibtex +@article{anderson2019billmap, + title = {Detecting Policy Influence in Legislatures}, + author = {Anderson, Evan and Fowler, Anthony and Grossmann, Matt and Sahn, Alexander and Shiraito, Yuki}, + journal = {arXiv preprint arXiv:1906.03699}, + year = {2019}, + url = {https://arxiv.org/abs/1906.03699} +} +``` + +### **@unitedstates Images Repository** ⭐ +- High-resolution photos of all U.S. Congress members (past and present) +- Organization: @unitedstates community (Sunlight Foundation legacy project) +- Repository: https://github.com/unitedstates/images +- CDN: https://theunitedstates.io/images/congress/ +- License: Public domain (government photos) +- Coverage: All U.S. Senators and Representatives (1789-present), updated regularly +- Image Format: JPEG, multiple resolutions (original, 450x550, 225x275) +- Used for: Legislator profile photos, visual identification, representative directories + +**Image URL Format:** +``` +https://theunitedstates.io/images/congress/original/[bioguide_id].jpg +https://theunitedstates.io/images/congress/450x550/[bioguide_id].jpg +https://theunitedstates.io/images/congress/225x275/[bioguide_id].jpg +``` + +**Example:** +``` +https://theunitedstates.io/images/congress/original/P000197.jpg +(Nancy Pelosi, bioguide_id: P000197) +``` + +**What we use:** +- Legislator profile photos for federal representatives +- Visual identification in advocacy tools +- Representative directories and contact pages +- Cross-referenced with Open States data using bioguide IDs + +**Related Projects:** +- **congress-legislators**: https://github.com/unitedstates/congress-legislators (YAML data files) +- **congress**: https://github.com/unitedstates/congress (scraping tools) +- **districts**: https://github.com/unitedstates/districts (GeoJSON boundaries) + +--- + +## 💰 **Nonprofit Financial Data** + +### **GivingTuesday 990 Data Infrastructure** ⭐ + +We use the GivingTuesday 990 Data Lake for detailed nonprofit financial data from IRS Form 990 XML filings. + +**Organization:** GivingTuesday +**Website:** https://990data.givingtuesday.org/ +**Data Lake:** `s3://gt990datalake-rawdata` (AWS S3, us-east-1 Virginia, Public Access) +**Console:** https://us-east-1.console.aws.amazon.com/s3/buckets/gt990datalake-rawdata +**License:** Public domain (IRS data) + Open source tools +**Access:** Free, no AWS credentials required (anonymous access via `--no-sign-request`) + +**What we use:** +- **Raw 990 XMLs**: Individual e-filed Form 990 returns in XML format (1-2 MB each) +- **Indices**: CSV/Parquet files listing all available 990s with metadata +- **Coverage**: 5.4M+ e-filed Form 990s (2011-present, ~300K new filings/year) +- **Scale**: ~10 TB of raw XML data +- **Data extracted**: Revenue, expenses, assets, liabilities, grants, programs, officer compensation, mission statements, website URLs + +**Data Lake Structure:** +``` +s3://gt990datalake-rawdata/ +├── EfileData/ +│ ├── XmlFiles/ # Individual 990 XMLs (~5.4M files, ~10 TB) +│ │ └── [OBJECT_ID]_public.xml (e.g., 202233259349300703_public.xml) +│ └── XmlZips/ # ZIP archives (97 files, ~38 GB → ~95 GB uncompressed) +│ └── YYYY_TEOS_XML_*.zip (e.g., 2023_TEOS_XML_01A.zip ~400 MB) +└── Indices/ + └── 990xmls/ # CSV indices with metadata + └── index_all_years_efiledata_xmls_created_on_2023-10-29.csv (~925 MB) +``` + +**Download Strategies:** + +| Approach | Best For | Time | Bandwidth | Storage | +|----------|----------|------|-----------|---------| +| **Individual XMLs** | Single state or targeted download | ~2 hrs (22K orgs) | 32 GB | 32 GB | +| **ZIP Archives** | All states / nationwide | ~6 hrs total | 38 GB | 95 GB | + +**Choose Individual XMLs when:** +- You need data for 1-5 states only +- You want to download only specific EINs +- Storage space is limited +- You want incremental caching (download as needed) + +**Choose ZIP Archives when:** +- You need all 50 states +- You're building a comprehensive nonprofit database +- You have 100+ GB storage +- You want offline access to all filings + +**S3 Access Examples:** + +**Individual XMLs (for single state or targeted download):** +```bash +# List index files (no credentials needed) +aws s3 ls s3://gt990datalake-rawdata/Indices/990xmls/ --no-sign-request + +# Download index (~925 MB) +aws s3 cp s3://gt990datalake-rawdata/Indices/990xmls/index_all_years_efiledata_xmls_created_on_2023-10-29.csv . --no-sign-request + +# Download specific XML +aws s3 cp s3://gt990datalake-rawdata/EfileData/XmlFiles/202233259349300703_public.xml . --no-sign-request + +# Batch download for single state (using our script) +python scripts/batch_download_990s.py --state MA --health-only --concurrent 1000 +``` + +**ZIP Archives (for all states / nationwide):** +```bash +# Download all 97 ZIPs (~38 GB) to local directory +./scripts/download_990_zips.sh + +# Extract all ZIPs to get ~384K XMLs (~95 GB) +./scripts/extract_990_zips.sh + +# Build local index for fast lookup +python scripts/build_990_local_index.py + +# Now enrich from local files (no network needed!) +python scripts/enrich_all_states_990.py +``` + +**Index Schema:** +The CSV index contains columns: `EIN`, `TaxPeriod`, `ObjectId`, `URL`, `FormType`, `OrganizationName`, `DLN`, `SubmittedOn` + +**Python Access:** +```python +import boto3 +from botocore import UNSIGNED +from botocore.config import Config + +# Configure anonymous S3 client +s3 = boto3.client('s3', config=Config(signature_version=UNSIGNED)) + +# Download XML +xml_obj = s3.get_object( + Bucket='gt990datalake-rawdata', + Key='EfileData/XmlFiles/202233259349300703_public.xml' +) +xml_content = xml_obj['Body'].read() +``` + +**BibTeX:** +```bibtex +@misc{givingtuesday990data, + title = {GivingTuesday 990 Data Infrastructure}, + author = {{GivingTuesday}}, + year = {2023}, + url = {https://990data.givingtuesday.org/}, + note = {Collaborative data lake of standardized IRS Form 990 XML filings} +} +``` + +**Attribution:** When publishing analyses using this data, please cite both: +1. GivingTuesday 990 Data Infrastructure: https://990data.givingtuesday.org/ +2. Our enrichment tools: https://github.com/getcommunityone/open-navigator-for-engagement + +--- + +### **Google Cloud Public Datasets: IRS 990** ⭐ + +Google hosts the complete IRS Form 990 dataset in BigQuery for fast SQL-based querying. + +**Platform:** Google Cloud BigQuery +**Dataset:** `bigquery-public-data.irs_990` +**Table:** `bigquery-public-data.irs_990.irs_990_xml` +**Documentation:** https://console.cloud.google.com/marketplace/product/internal-revenue-service/irs-990 +**Cost:** First 1 TB of queries per month is **FREE** +**Coverage:** All e-filed Form 990s (2011-present, 5M+ records) + +**What we use:** +- **Mission statements**: Extracted from `return_header` or `part_i_mission_desc` fields +- **Website URLs**: Found in `website_address_txt` field +- **Financial data**: All Form 990 fields accessible via SQL +- **Fast bulk queries**: Extract data for 1M+ orgs in seconds (vs hours downloading XMLs) + +**Advantages:** +- ✅ No local XML downloads needed +- ✅ Single SQL query to bulk-extract fields +- ✅ Serverless (no infrastructure to manage) +- ✅ Fast (queries complete in seconds) +- ✅ Free tier covers most research use cases + +**Example Query:** +```sql +SELECT + ein, + org_name, + website_address_txt, + part_i_mission_desc, + total_revenue_current_year, + total_expenses_current_year +FROM `bigquery-public-data.irs_990.irs_990_2023` +WHERE state = 'AL' + AND ntee_code LIKE 'E%' +LIMIT 1000; +``` + +**BibTeX:** +```bibtex +@misc{googlecloud_irs990, + title = {IRS 990 Public Dataset}, + author = {{Google Cloud Public Datasets}}, + year = {2024}, + publisher = {Google Cloud Platform}, + url = {https://console.cloud.google.com/marketplace/product/internal-revenue-service/irs-990}, + note = {BigQuery public dataset of IRS Form 990 e-file data} +} +``` + +**Attribution:** When using BigQuery 990 data, cite: +1. IRS 990 Public Dataset (Google Cloud) +2. Internal Revenue Service (original data source) + +--- + +### **National Center for Charitable Statistics (NCCS) Unified BMF** ⭐ + +The NCCS Unified BMF is a longitudinal nonprofit dataset specifically designed for AI and "Lakehouse" projects with pre-geocoded locations and Census integration. + +**Organization:** National Center for Charitable Statistics (NCCS), Urban Institute +**Website:** https://nccs.urban.org/ +**Dataset:** Unified BMF (Business Master File) +**Documentation:** https://nccs.urban.org/project/irs-exempt-organizations-business-master-file +**License:** Public domain (IRS data) + Urban Institute terms +**Released:** Late 2025/Early 2026 +**Coverage:** 1989 through mid-2025 (update pending) + +**What we use:** +- **Longitudinal tracking**: Single file with one row per organization that has ever held tax-exempt status +- **Pre-geocoded addresses**: Most recent address geocoded to Census block level +- **Geographic codes**: FIPS codes at block, tract, county, and state levels +- **Metropolitan area codes**: Current CBSA (Core Based Statistical Area) definitions +- **Temporal tracking**: `ORG_YEAR_FIRST` and `ORG_YEAR_LAST` variables for organization lifecycle +- **Census integration**: Ready for merging with Census demographic and economic data + +**Key Features:** +- ✅ **Eliminates annual file merging**: Consolidates all historical BMF releases into single file +- ✅ **AI/Lakehouse optimized**: Designed for modern data infrastructure +- ✅ **Census-ready**: FIPS codes enable direct joins with Census data +- ✅ **Metropolitan vs Rural**: CBSA codes identify urban/rural areas +- ✅ **Historical analysis**: Track organizations over time without complex ETL +- ✅ **Geographic analysis**: Pre-geocoded to Census block granularity + +**Use Cases:** +- Longitudinal analysis of nonprofit sector +- Building historical sampling frames +- Linking nonprofit data to Census demographics +- Metropolitan vs rural nonprofit analysis +- Policy research requiring geographic precision +- Time-series analysis of organizational entry/exit + +**Geographic Levels Available:** +- Census Block (finest granularity) +- Census Tract +- County (FIPS codes) +- State (FIPS codes) +- CBSA (Core Based Statistical Area) + +**Related Resources:** +- NCCS Census Crosswalk: For aggregating to additional geographic levels +- BMF Overview: https://nccs.urban.org/project/irs-exempt-organizations-business-master-file +- NCCS Data Archive: https://nccs.urban.org/nccs-data-archive + +**BibTeX:** +```bibtex +@dataset{nccs_unified_bmf, + title = {Unified Business Master File (BMF)}, + author = {{National Center for Charitable Statistics}}, + year = {2026}, + publisher = {Urban Institute}, + url = {https://nccs.urban.org/}, + note = {Longitudinal nonprofit dataset with pre-geocoded Census integration, 1989-2025} +} +``` + +**Attribution:** When using NCCS Unified BMF data, cite: +1. National Center for Charitable Statistics, Urban Institute +2. IRS Business Master File (original data source) +3. Specify the data vintage/update date used + +--- + +### **Charity Navigator** ⭐ + +**Powered by Charity Navigator** + +We use the Charity Navigator GraphQL API to enrich nonprofit profiles with star ratings, mission statements, and organizational metrics. + +**Organization:** Charity Navigator, Inc. +**Website:** https://www.charitynavigator.org +**API Documentation:** https://www.charitynavigator.org/partner/api +**Principal Office:** 299 Market Street, Suite 250, Saddle Brook, NJ 07663 +**License:** API Terms of Use (Last updated March 2025) +**Rate Limit:** 1,000 API calls per day + +**What we use:** +- **Charity Ratings**: Encompass Star Rating (0-4 stars) +- **Mission Statements**: Organization mission and purpose +- **Website URLs**: Official organization websites +- **Organizational Data**: EIN, name, address, category, cause +- **Active Advisories**: Alerts about organization status +- **Encompass Score**: Overall rating score +- **Rating Publication Date**: When the rating was last updated + +**Data Fields Accessed:** +``` +- Employer Identification Number (EIN) +- Charity Name +- Mission +- Organization Website URL +- Charity Navigator URL +- Category & Cause +- Street Address, City, State, Zip, Country +- Active Advisories +- Encompass Score & Star Rating +- Encompass Rating Publication Date & ID +``` + +**Attribution Requirements:** +- **Text Credit:** "Powered by Charity Navigator" (displayed on pages using their data) +- **Source Citation:** Charity Navigator cited as source on all pages displaying their data +- **Linkbacks:** All charity data links back to corresponding Charity Navigator profile pages +- **Trademark Notice:** CHARITY NAVIGATOR and the CHARITY NAVIGATOR logo are registered trademarks of Charity Navigator. All rights reserved. Used with permission. + +**BibTeX:** +```bibtex +@misc{charitynavigator_api, + title = {Charity Navigator API}, + author = {{Charity Navigator, Inc.}}, + year = {2025}, + url = {https://www.charitynavigator.org}, + note = {GraphQL API providing nonprofit ratings, mission statements, and organizational data} +} +``` + +**Compliance:** +This project complies with Charity Navigator's API Terms of Use, including: +- Rate limit compliance (max 1,000 calls/day) +- Proper attribution and branding +- Linkbacks to Charity Navigator profile pages +- Trademark acknowledgment +- Data caching for performance only (not for redistribution) + +**Example Profile Link Format:** +```html + + Michael J. Fox Foundation for Parkinson's Research + +``` + +**Related Tools:** +- [Nonprofit enrichment script](scripts/enrich_nonprofits_charitynavigator.py) (if created) +- [API integration documentation](website/docs/data-sources/charity-navigator.md) (if created) + +--- + +### **OpenSecrets.org (Center for Responsive Politics)** ⭐ + +**Organization:** OpenSecrets, a nonpartisan research organization tracking money in U.S. politics +**Website:** https://www.opensecrets.org +**Bulk Data:** https://www.opensecrets.org/open-data/bulk-data +**API Documentation:** https://www.opensecrets.org/open-data/api +**Status:** Bulk data access pending approval + +**What they offer:** +- **Campaign Finance Data**: Federal campaign contributions, expenditures, and fundraising +- **Lobbying Data**: Federal lobbying spending by organizations and industries +- **Political Action Committees (PACs)**: PAC contributions and expenditures +- **Personal Finance Disclosures**: Wealth and financial interests of federal lawmakers +- **501(c) Organizations**: Political spending by nonprofits and dark money groups +- **Foreign Lobby Influence**: Foreign agents registered under FARA + +**Data Access:** +- **Bulk Data Downloads**: Available to nonprofits upon approval (application pending) +- **Public API**: Available with rate limits for smaller queries +- **Data Format**: CSV files with detailed transaction-level records +- **Update Frequency**: Regular updates as new filings are processed +- **Coverage**: Federal-level political finance data (1990-present) + +**What we plan to use:** +- Nonprofit political spending and advocacy activity +- Lobbying expenditures by healthcare and oral health organizations +- Campaign contributions from dental associations and health policy groups +- 501(c)(4) "dark money" spending on ballot measures +- Cross-reference EINs with IRS nonprofit data for comprehensive profiles + +**BibTeX:** +```bibtex +@misc{opensecrets, + title = {OpenSecrets.org: Money in Politics Database}, + author = {{Center for Responsive Politics}}, + year = {2024}, + url = {https://www.opensecrets.org}, + note = {Comprehensive database of campaign finance, lobbying, and political spending in U.S. politics} +} +``` + +**License & Attribution:** +- Data collected from Federal Election Commission (FEC) and other public sources +- Attribution required: "Data from OpenSecrets.org, a project of the Center for Responsive Politics" +- Nonprofit bulk data access subject to approval and terms of use + +**Application Status:** +- ⏳ Bulk data access application pending approval +- Will enable comprehensive analysis of nonprofit political activity +- Integration planned upon approval + +--- + +### **IRS Exempt Organizations Business Master File (EO-BMF)** + +Basic nonprofit registration data (name, EIN, address, NTEE code). + +### **IRS Exempt Organizations Business Master File (EO-BMF)** +- Complete database of 1.9M+ U.S. tax-exempt organizations +- Organization: Internal Revenue Service (IRS) +- Source: https://www.irs.gov/charities-non-profits/exempt-organizations-business-master-file-extract-eo-bmf +- Download: https://www.irs.gov/pub/irs-soi/ (4 regional CSV files) +- Format: CSV (basic organizational data: name, EIN, address, NTEE code, etc.) +- Update frequency: Monthly +- License: Public Domain (U.S. Government data) +- Coverage: All registered tax-exempt organizations under sections 501(c)(3), 501(c)(4), etc. +- Used for: Nonprofit discovery, organization matching, NTEE categorization + +**Note:** This is the **Business Master File** (basic info). For detailed financial data, see IRS Form 990 XML below. + +### **IRS Form 990 XML Filings** ⭐ +- Detailed financial filings from nonprofit tax returns +- Organization: Internal Revenue Service (IRS) +- Source: https://www.irs.gov/charities-non-profits/form-990-series-downloads +- Format: XML (highly detailed financial and operational data) +- Parser Tools: **Giving Tuesday** open source libraries + - XML Parser: https://github.com/Giving-Tuesday/form-990-xml-parser + - XML Mapper: https://github.com/Giving-Tuesday/form-990-xml-mapper +- AWS S3 Index: https://registry.opendata.aws/irs990/ +- License: Public Domain (U.S. Government data) +- Coverage: Annual filings from organizations with >$50K revenue +- Data includes: Detailed revenue, expenses, program services, officer compensation, grants, donors +- Used for: Financial analysis, transparency, grant research, program evaluation + +**Giving Tuesday Attribution:** +The Giving Tuesday Data Commons provides essential tools for parsing IRS Form 990 XML data: +```bibtex +@software{giving_tuesday_form990_parser, + title = {Form 990 XML Parser}, + author = {{Giving Tuesday}}, + year = {2024}, + url = {https://github.com/Giving-Tuesday/form-990-xml-parser}, + note = {Open source Python library for parsing IRS Form 990 XML filings} +} + +@software{giving_tuesday_form990_mapper, + title = {Form 990 XML Mapper}, + author = {{Giving Tuesday}}, + year = {2024}, + url = {https://github.com/Giving-Tuesday/form-990-xml-mapper}, + note = {Maps Form 990 XML to standardized data structures} +} +``` + +**More Giving Tuesday Resources:** +- GitHub Organization: https://github.com/Giving-Tuesday +- Data Commons: https://www.givingtuesday.org/data-commons +- Research & Insights: https://www.givingtuesday.org/research +- Coverage: Standardized schemas for Person, Organization, Membership, Post, Area, Motion, VoteEvent, Count +- Used for: Leader/official data modeling, organization structure, membership tracking, voting records +- Adoption: Used by Civic Commons, OpenNorth, mySociety, Sunlight Foundation, and 30+ civic tech organizations worldwide +- Citation: "Popolo Project. Open government data specifications. https://www.popoloproject.com/" +- **Key Features:** + - **Person**: Names, contact details, identifiers, links to images/sources + - **Organization**: Names, classification, founding/dissolution dates, contact information + - **Membership**: Relationship between persons and organizations (with roles and time periods) + - **Post**: Positions within organizations (e.g., "Mayor", "City Council Member District 3") + - **VoteEvent**: Votes on motions/bills with individual voter positions +- **Our Implementation**: LEADER and ORGANIZATION entities follow Popolo schema for maximum interoperability with civic tech platforms + +**Popolo Dependencies & Standards:** +The Popolo specification builds upon and references the following W3C, IETF, and open data standards: + +| Publisher | Specification | Prefix | Use in Popolo | URL | +|-----------|---------------|--------|---------------|-----| +| Bibliographic Framework Initiative | BIBFRAME Vocabulary | `bf` | Bibliographic references | https://www.loc.gov/bibframe/ | +| Ian Davis | BIO: Biographical Information | `bio` | Life events, relationships | http://purl.org/vocab/bio/0.1/ | +| W3C | Contact: Utility concepts | `con` | Contact information | http://www.w3.org/2000/10/swap/pim/contact# | +| DCMI | DCMI Metadata Terms | `dcterms` | Metadata, provenance | https://www.dublincore.org/specifications/dublin-core/dcmi-terms/ | +| FOAF Project | FOAF Vocabulary | `foaf` | People, social networks | http://xmlns.com/foaf/0.1/ | +| GeoNames | GeoNames Ontology | `gn` | Geographic names | http://www.geonames.org/ontology/ | +| ISA Programme | Location Core Vocabulary | `locn` | Addresses, locations | https://www.w3.org/ns/locn | +| OSCA Foundation | NEPOMUK Calendar Ontology | `ncal` | Events, meetings | http://www.semanticdesktop.org/ontologies/ncal/ | +| Open Data Institute | Open Data Rights Statement | `odrs` | Data licensing | http://schema.theodi.org/odrs | +| W3C | The Organization Ontology | `org` | Organizational structures | https://www.w3.org/TR/vocab-org/ | +| ISA Programme | Person Core Vocabulary | `person` | Person attributes | http://www.w3.org/ns/person | +| W3C | RDF Schema | `rdfs` | Semantic web foundation | https://www.w3.org/TR/rdf-schema/ | +| W3C | Schema.org | `schema` | Structured data | https://schema.org/ | +| W3C | SKOS | `skos` | Taxonomies, classification | https://www.w3.org/2004/02/skos/ | +| IETF | vCard Format | `vcard` | Contact information | https://www.rfc-editor.org/rfc/rfc6350.html | + +**Popolo Classes Implemented:** +- ✅ **Person** → LEADER entity (elected officials, appointees) +- ✅ **Organization** → ORGANIZATION entity (nonprofits, government agencies) +- ✅ **Membership** → Implicit through leader_id/organization relationships +- ✅ **Post** → position_type, office fields in LEADER +- ✅ **Contact Detail** → email, phone, website fields +- ✅ **Motion** → AGENDA items, LEGISLATION entities +- ✅ **Vote Event** → VOTE entity +- ✅ **Count** → vote_yes, vote_no in VOTE and LEGISLATION +- ✅ **Area** → JURISDICTION entity (geographic/political boundaries) +- ✅ **Event** → MEETING entity +- ✅ **Speech** → Extracted from MINUTES, VIDEO transcripts + +### **Roper Center for Public Opinion Research** +- Scientifically validated survey questions and public opinion data +- Organization: Cornell University +- Source: https://ropercenter.cornell.edu/ +- iPoll Database: https://ropercenter.cornell.edu/ipoll/ +- License: Free public search (metadata and question wording), full data requires institutional membership +- Coverage: 500,000+ survey questions from 1930s-present, all major polling organizations +- Used for: Topic definitions, validated question wording, national opinion baselines, messaging optimization +- Citation: "Roper Center for Public Opinion Research, Cornell University. iPoll Databank. https://ropercenter.cornell.edu/ipoll/" + +### **Google Fact Check Tools API** +- Aggregated fact-checking data with ClaimReview structured data +- Organization: Google LLC +- Source: https://toolbox.google.com/factcheck/explorer +- API: https://developers.google.com/fact-check/tools/api +- Schema: https://developers.google.com/search/docs/appearance/structured-data/factcheck +- License: Free API with quota (10,000 queries/day) +- Coverage: 100+ fact-checking organizations worldwide, all claim types +- Used for: Verifying claims from meetings/legislation, tracking misinformation, accountability scoring +- Citation: "Google Fact Check Tools API. Google LLC. https://developers.google.com/fact-check/tools/api" + +### **FactCheck.org** +- Nonpartisan fact-checking of political claims and viral misinformation +- Organization: Annenberg Public Policy Center, University of Pennsylvania +- Source: https://www.factcheck.org/ +- License: Free (web scraping allowed with rate limiting) +- Coverage: National politics, health claims, science, viral content (2003-present) +- Used for: Verifying political claims, health policy fact-checking, scientific claim verification +- Citation: "FactCheck.org. Annenberg Public Policy Center, University of Pennsylvania. https://www.factcheck.org/" + +### **PolitiFact** +- Pulitzer Prize-winning fact-checking with Truth-O-Meter ratings +- Organization: Poynter Institute +- Source: https://www.politifact.com/ +- License: Free (web scraping allowed with rate limiting) +- Coverage: All 50 states, federal politics, ballot measures (2007-present) +- Rating Scale: 6-point (True, Mostly True, Half True, Mostly False, False, Pants on Fire) +- Used for: State-level fact-checking, tracking politician claims, ballot measure verification +- Citation: "PolitiFact. Poynter Institute. https://www.politifact.com/" + +### **Schema.org** +- Structured data vocabulary for semantic web markup +- Organization: W3C Community Group (sponsors: Google, Microsoft, Yahoo, Yandex) +- Source: https://schema.org/ +- Documentation: https://schema.org/docs/schemas.html +- License: Creative Commons Attribution-ShareAlike License (CC BY-SA 3.0) +- Coverage: 800+ types, 1,400+ properties for describing web content +- Used for: SEO-optimized structured data, JSON-LD exports, API documentation, search engine compatibility +- Citation: "Schema.org. W3C Community Group. https://schema.org/" + +**Our Schema.org Type Mappings:** + +| Our Entity | Schema.org Type | Properties Used | Use Case | +|------------|----------------|-----------------|----------| +| JURISDICTION | [AdministrativeArea](https://schema.org/AdministrativeArea) | name, address, geo, telephone, url | City/county geographic data | +| MEETING | [Event](https://schema.org/Event) + [GovernmentService](https://schema.org/GovernmentService) | name, startDate, location, organizer, description | Public meetings, hearings | +| LEADER | [Person](https://schema.org/Person) + [GovernmentOfficial](https://schema.org/GovernmentOfficial) | name, email, telephone, jobTitle, worksFor | Elected officials | +| ORGANIZATION | [Organization](https://schema.org/Organization) + [NGO](https://schema.org/NGO) | name, address, telephone, url, foundingDate | Nonprofits, agencies | +| LEGISLATION | [Legislation](https://schema.org/Legislation) | name, legislationDate, legislationPassedBy, legislationType | Bills, ordinances | +| BALLOT_MEASURE | [Legislation](https://schema.org/Legislation) + referendumProposal | name, datePosted, legislationChanges | Referendums, propositions | +| VOTE | [VoteAction](https://schema.org/VoteAction) | agent (Person), candidate (Legislation), actionOption | Roll call votes | +| FACT_CHECK | [ClaimReview](https://schema.org/ClaimReview) | claimReviewed, reviewRating, author, datePublished | Verified fact-checks | +| SCHOOL_DISTRICT | [EducationalOrganization](https://schema.org/EducationalOrganization) | name, address, telephone, numberOfStudents | K-12 school districts | +| NONPROFIT_FINANCES | [MonetaryGrant](https://schema.org/MonetaryGrant) | funder, amount, fundedItem | IRS Form 990 data | +| VIDEO | [VideoObject](https://schema.org/VideoObject) | name, description, uploadDate, duration, thumbnailUrl | Meeting recordings | +| DOCUMENT | [DigitalDocument](https://schema.org/DigitalDocument) | name, fileFormat, datePublished, url | PDFs, agendas, minutes | + +**Benefits:** +- ✅ **SEO Enhancement**: Google Search rich results for meetings, officials, organizations +- ✅ **Voice Assistant Ready**: Alexa, Google Assistant can parse our structured data +- ✅ **Knowledge Graph**: Data appears in Google Knowledge Panels +- ✅ **API Discoverability**: Standards-compliant REST/GraphQL responses +- ✅ **Cross-platform**: Compatible with Apple Podcasts, Microsoft Bing, Yandex + +### **Common Education Data Standards (CEDS)** +- Comprehensive education data standards for K-12, postsecondary, and workforce +- Organization: U.S. Department of Education, National Center for Education Statistics (NCES) +- Source: https://ceds.ed.gov/ +- GitHub: https://github.com/CEDStandards +- Specification Repository: https://github.com/CEDStandards/CEDS-Elements +- License: Public Domain (U.S. Government) +- Coverage: 2,300+ data elements, 500+ option sets, alignment with NCES surveys +- Used for: School district data modeling, NCES interoperability, education finance tracking +- Citation: "Common Education Data Standards (CEDS). National Center for Education Statistics. https://ceds.ed.gov/" + +**CEDS Alignment for School Districts:** + +| Our Field | CEDS Element ID | CEDS Element Name | Description | +|-----------|----------------|-------------------|-------------| +| `nces_id` | 000827 | LEA Identifier (NCES) | National Center for Education Statistics LEA ID | +| `district_name` | 000168 | Name of Institution | Legal name of the school district | +| `district_type` | 000108 | LEA Type | Local, State, Federal, or Other | +| `total_students` | 001475 | Student Count | Total number of students enrolled | +| `total_schools` | 000856 | Number of Schools | Count of schools in district | +| `total_revenue` | 000612 | Total Revenue | Sum of all revenue sources | +| `total_expenditures` | 000611 | Total Expenditures | Sum of all spending categories | +| `per_pupil_spending` | 000613 | Expenditure per Student | Total expenditures / student count | +| `federal_revenue` | 000614 | Federal Revenue | Revenue from federal government | +| `state_revenue` | 000615 | State Revenue | Revenue from state sources | +| `local_revenue` | 000616 | Local Revenue | Revenue from property taxes, bonds | +| `superintendent` | 000240 | Chief Administrator Name | District superintendent name | +| `school_year` | 000243 | School Year | Academic year (e.g., 2023-2024) | + +**CEDS Option Sets Used:** +- **LEA Type** (CEDS 000108): Regular, Specialized, Supervisory Union, Service Agency, State Agency, Federal Agency +- **Grade Level** (CEDS 000100): PK, KG, 01-12, UG (ungraded) +- **Operational Status** (CEDS 000533): Open, Closed, New, Added, Changed Agency, Temporarily Closed +- **Locale Type** (CEDS 001315): City, Suburb, Town, Rural (NCES Urban-centric locale codes) + +**Benefits of CEDS Compliance:** +- ✅ **NCES Compatibility**: Direct mapping to Common Core of Data (CCD) and F-33 Finance Survey +- ✅ **State Reporting**: Aligns with state education department data systems +- ✅ **Federal Grants**: Standardized reporting for ESSA, Title I, IDEA compliance +- ✅ **Longitudinal Tracking**: Consistent identifiers for multi-year analysis +- ✅ **Interoperability**: Works with Ed-Fi Alliance, IMS Global, SIF Association standards + +### **Microsoft Common Data Model for Nonprofits** +- Industry-standard data model for nonprofit organizations built on Microsoft Dataverse +- Organization: Microsoft Corporation +- Repository: https://github.com/microsoft/Nonprofits/tree/master/CommonDataModelforNonprofits +- ERD Documentation: https://github.com/microsoft/Nonprofits/blob/master/CommonDataModelforNonprofits/Documents/common-data-model-for-nonprofits-erds.pdf +- License: MIT License +- Coverage: Donor management, fundraising, program delivery, volunteer management, impact measurement, award/grant tracking +- Used for: Nonprofit data standardization, Dynamics 365 integration, constituent relationship management, outcome tracking +- Citation: "Microsoft Common Data Model for Nonprofits. Microsoft Corporation. https://github.com/microsoft/Nonprofits/" + +**Microsoft CDM Nonprofit Core Entities:** + +| Entity | Description | Our Implementation | +|--------|-------------|--------------------| +| **Constituent** | Individuals who interact with nonprofit (donors, volunteers, members, beneficiaries) | CONSTITUENT entity | +| **Donation** | Financial contributions and in-kind gifts | DONATION entity | +| **Designation** | How donations are allocated (programs, funds, campaigns) | designation_id in DONATION | +| **Campaign** | Fundraising campaigns and appeals | CAMPAIGN entity | +| **Membership** | Member enrollment and renewal tracking | MEMBERSHIP entity | +| **Volunteer** | Volunteer activities, hours, and preferences | VOLUNTEER_ACTIVITY entity | +| **Award** | Grants received by the nonprofit | Awards captured in NONPROFIT_FINANCES | +| **Disbursement** | Spending of grant/award funds | Expenditures in GOVERNMENT_BUDGET | +| **Objective** | Measurable program outcomes and impact | PROGRAM_OUTCOME entity | +| **DeliveryFramework** | Programs and services delivered | PROGRAM_DELIVERY entity | +| **Budget** | Organizational budgets and allocations | GOVERNMENT_BUDGET, SCHOOL_DISTRICT budgets | +| **Indicator** | Key performance indicators for impact | Metrics in PROGRAM_OUTCOME | + +**Key Entity Relationships (Microsoft CDM Pattern):** +- Constituent → Donation (one-to-many): A constituent makes many donations +- Donation → Designation (many-to-one): Multiple donations to one fund/program +- Campaign → Donation (one-to-many): A campaign receives many donations +- Constituent → Membership (one-to-many): A constituent can have multiple memberships over time +- Constituent → Volunteer (one-to-many): A constituent volunteers for multiple activities +- Organization → DeliveryFramework (one-to-many): An organization delivers multiple programs +- DeliveryFramework → Objective (one-to-many): A program has multiple outcome objectives + +**Benefits of Microsoft CDM Alignment:** +- ✅ **Dynamics 365 Integration**: Native compatibility with Microsoft Cloud for Nonprofits +- ✅ **Power Platform**: Direct use in Power BI, Power Apps, Power Automate +- ✅ **Azure Synapse**: Seamless analytics with Azure data services +- ✅ **Industry Standard**: Adopted by large nonprofits using Microsoft ecosystem +- ✅ **Grant Compliance**: Built-in support for grant reporting and outcome measurement +- ✅ **Constituent 360**: Unified view of donor, volunteer, member activities + +--- + +## 🎯 **Grant Research and Fundraising Platforms** + +These platforms are built on open-source principles or community-funded models to keep grant and fundraising data accessible. + +### **Grantmakers.io** ⭐ + +**"Free as in Freedom" Grant Research** + +Grantmakers.io is the gold standard for open, community-supported foundation research. It provides lightning-fast search through IRS 990-PF data with no login required. + +**Organization:** Community-supported open-source project +**Website:** https://www.grantmakers.io/ +**Data Source:** IRS Form 990-PF (Private Foundation tax returns) +**License:** Open source, community-funded +**Access:** 100% free, no account or API key required +**Coverage:** All U.S. private foundations filing Form 990-PF (75,000+ grantmaking foundations) + +**What we use:** +- **Foundation Giving Histories**: Search foundations by who they've funded in the past +- **Grantee Databases**: Find all grants made to specific organizations +- **Geographic Targeting**: Search by state, city, or region +- **Funding Amounts**: Filter by grant size ranges +- **NTEE Categories**: Search by nonprofit sector (health, education, environment, etc.) +- **Year-over-Year Trends**: Track foundation giving patterns over time + +**Key Features:** +- ⚡ **Lightning-Fast Search**: Instant results across millions of grant records +- 🔓 **No Login Required**: Completely open access, no barriers +- 📊 **Detailed 990-PF Data**: Full foundation financials, officers, assets +- 🎯 **Relationship Mapping**: Discover foundation-grantee connections +- 📈 **Trend Analysis**: Multi-year giving patterns and focus areas +- 🆓 **Always Free**: Community-funded to remain accessible + +**Use Cases:** +- **Grant Prospecting**: Find foundations that fund similar organizations in your area +- **Relationship Research**: Identify foundations that have supported oral health, public health, or civic engagement +- **Competitive Analysis**: See which organizations are receiving grants in your field +- **Foundation Vetting**: Review foundation assets, giving patterns, and leadership before applying + +**Example Searches:** +- Foundations that funded "fluoridation" or "oral health" projects +- Grantmakers in Massachusetts supporting health policy advocacy +- Foundations with >$10M assets funding civic engagement +- All grants made by Robert Wood Johnson Foundation to nonprofits in Alabama + +**BibTeX:** +```bibtex +@misc{grantmakersio, + title = {Grantmakers.io: Open Foundation Research Platform}, + year = {2026}, + url = {https://www.grantmakers.io/}, + note = {Community-supported open-source platform for searching IRS 990-PF private foundation data} +} +``` + +**Citation:** "Grantmakers.io. Community-supported open foundation research. https://www.grantmakers.io/" + +--- + +### **Zeffy** ⭐ + +**100% Free Fundraising with AI-Powered Grant Matching** + +Zeffy is unique for being a completely free fundraising platform that also offers an AI-powered grant search tool to help match nonprofit missions with potential grant opportunities. + +**Organization:** Zeffy, Inc. +**Website:** https://www.zeffy.com/ +**Platform:** Fundraising + Grant Discovery +**Cost:** 100% free for nonprofits (donor-covered fees model) +**Grant Tool:** AI-powered grant opportunity matching +**Coverage:** U.S. and Canadian grant opportunities + +**What we use:** +- **AI Grant Matching**: Automated matching of nonprofit missions to relevant grant opportunities +- **Fundraising Infrastructure**: Donation processing, event ticketing, membership management +- **Donor Management**: CRM for tracking constituent relationships +- **Grant Alerts**: Notifications when new matching opportunities are posted + +**Key Features:** +- 💰 **100% Free**: No platform fees, monthly charges, or hidden costs +- 🤖 **AI-Powered Matching**: Machine learning matches your mission to grant opportunities +- 📧 **Grant Alerts**: Email notifications for new matching grants +- 🎟️ **All-in-One Platform**: Donations, events, memberships, grants in one system +- 🇺🇸 🇨🇦 **North America Coverage**: U.S. and Canadian grant databases +- 📊 **Impact Reporting**: Built-in analytics for grant reporting requirements + +**Grant Discovery Capabilities:** +- **Mission-Based Matching**: Upload your mission statement, get matched grants +- **Federal Grants**: Monitors Grants.gov for federal opportunities +- **Foundation Grants**: Tracks private foundation RFPs and announcements +- **Corporate Giving**: Alerts for corporate philanthropy programs +- **Local Grants**: Community foundation and regional funder opportunities + +**Use Cases for This Project:** +- **Nonprofit Fundraising**: Organizations can use Zeffy for zero-cost donation processing +- **Grant Prospecting**: AI helps match oral health nonprofits to relevant grant opportunities +- **Event Fundraising**: Free ticketing for fundraising galas, community events +- **Membership Management**: Track supporters, volunteers, members at no cost +- **Sustainability**: Recommend to small nonprofits to reduce overhead costs + +**Why It's Important:** +Traditional fundraising platforms charge 3-5% fees on donations, which drains resources from small nonprofits. Zeffy's donor-covered model means 100% of donations go to the organization, making it especially valuable for grassroots oral health advocacy groups. + +**BibTeX:** +```bibtex +@misc{zeffy_platform, + title = {Zeffy: 100% Free Fundraising Platform with AI Grant Matching}, + author = {{Zeffy, Inc.}}, + year = {2026}, + url = {https://www.zeffy.com/}, + note = {Free fundraising platform with AI-powered grant discovery for U.S. and Canadian nonprofits} +} +``` + +**Citation:** "Zeffy. 100% Free Fundraising Platform with AI Grant Matching. https://www.zeffy.com/" + +--- + +### **Community Foundations** ⭐ + +**Local Grant Opportunities Often Overlooked** + +Community foundations are often the most accessible grant sources for local nonprofits, yet they're frequently overlooked because they don't appear in major federal databases. Most maintain their own open listings for regional grants. + +**What Community Foundations Are:** +Community foundations are public charities that pool donations from individuals, families, and businesses to support local nonprofits through competitive grants, scholarship programs, and donor-advised funds. + +**Why They Matter:** +- 🏘️ **Local Focus**: Prioritize organizations serving their specific geographic region +- 💵 **Smaller, Accessible Grants**: $500-$50,000 range, ideal for grassroots groups +- 🤝 **Relationship-Based**: Local foundations know local issues and local leaders +- 📋 **Simpler Applications**: Less bureaucratic than federal or national foundations +- ⚡ **Faster Decisions**: Many have quarterly or rolling deadlines +- 🎯 **Mission Alignment**: Support for community health, civic engagement, education + +**Examples of Community Foundations:** + +| Foundation | Region | Website | Grant Focus Areas | +|------------|--------|---------|-------------------| +| **Central Alabama Community Foundation** | Birmingham, AL metro | https://www.cacfbirmingham.org/ | Health, education, civic engagement, arts | +| **Community Foundation for Greater Atlanta** | Atlanta, GA metro | https://cfgreateratlanta.org/ | Health equity, education, economic mobility | +| **Boston Foundation** | Boston, MA metro | https://www.tbf.org/ | Health, housing, education, civic participation | +| **Community Foundation of Greater Memphis** | Memphis, TN metro | https://cfgm.org/ | Health, youth development, community engagement | +| **Silicon Valley Community Foundation** | San Francisco Bay Area | https://www.siliconvalleycf.org/ | Health, education, immigration, environment | +| **Greater Kansas City Community Foundation** | Kansas City, MO/KS | https://www.growyourgiving.org/ | Health, education, civic infrastructure | +| **Seattle Foundation** | Seattle, WA metro | https://www.seattlefoundation.org/ | Racial equity, community health, economic opportunity | + +**How to Find Your Local Community Foundation:** +1. **Council on Foundations Directory**: https://www.cof.org/community-foundation-locator +2. **Candid (formerly Foundation Center)**: https://candid.org/find-us/foundation-finder +3. **State Associations**: Most states have a community foundation association +4. **Google Search**: "[Your City] Community Foundation" or "[Your County] Community Foundation" + +**Grant Opportunities:** +- **Competitive Grants**: Open RFPs for nonprofits in specific focus areas +- **Capacity Building Grants**: Support for operations, staffing, strategic planning +- **Donor-Advised Funds**: Individuals/families make grants through the foundation +- **Fiscal Sponsorship**: Some foundations sponsor projects for groups without 501(c)(3) status +- **Scholarship Programs**: Education grants for students (often administered by community foundations) + +**For Oral Health Advocacy:** +Many community foundations have health equity or preventive health focus areas that align perfectly with fluoridation advocacy, dental access programs, and oral health education. They're often the best first step for local grassroots campaigns. + +**How We Use Community Foundation Data:** +- **Local Grant Mapping**: Identify which community foundations serve each jurisdiction +- **Nonprofit Funding Sources**: Link organizations to local foundation grants received +- **Geographic Targeting**: Recommend local funders when users search by city/county +- **Grant Prospecting**: Alert nonprofits to community foundation RFPs in their area + +**BibTeX:** +```bibtex +@misc{community_foundations, + title = {Community Foundations: Local Grant Opportunities}, + author = {{Council on Foundations}}, + year = {2026}, + url = {https://www.cof.org/community-foundation-locator}, + note = {Network of 700+ community foundations providing local grants across the United States} +} +``` + +**Citation:** "Community Foundations. Council on Foundations. https://www.cof.org/community-foundation-locator" + +--- + +## 🏛️ **Civic Tech Organizations & Resources** + +### **Code for America** ⭐ + +The flagship U.S. civic technology nonprofit organization, convening government leaders and technologists to transform public services. + +**Organization:** Code for America +**Website:** https://codeforamerica.org/ +**About:** National nonprofit working with government to build digital services that are simple, effective, and accessible to all +**Founded:** 2009 +**Coverage:** National (50 states), with focus on state-level government transformation + +**What we use:** +- **Summit Insights**: Annual Summit (most recently Summit 2026) where state-level AI leads and municipal CIOs set the civic tech agenda for the year +- **Brigade Network**: Community chapters across the U.S. working on local civic tech projects +- **Best Practices**: Government service design patterns, digital service standards +- **Technology Landscape**: Trends in state and local government digital transformation + +**Key Programs:** +- **Code for America Summit**: Annual conference bringing together 1,500+ government leaders, technologists, and advocates +- **Brigade Network**: 80+ volunteer chapters in cities across America building civic tech solutions +- **Get CalFresh**: Flagship product helping millions access food benefits through simplified digital applications +- **Clear My Record**: Automated criminal record clearance to help people move forward +- **Government Services Portfolio**: Digital tools for social safety net programs + +**Resources:** +- Summit: https://codeforamerica.org/summit/ +- Brigade Network: https://brigade.codeforamerica.org/ +- Blog: https://codeforamerica.org/news/ +- GitHub: https://github.com/codeforamerica +- Annual Reports: https://codeforamerica.org/news/category/annual-reports/ + +**Why Code for America:** +- **Agenda Setting**: The Summit is where state-level AI leads and municipal CIOs define priorities for the year +- **Network Effect**: Connects civic technologists across the country +- **Proven Impact**: Products serving millions of Americans annually +- **Open Source**: Many tools available as open source for other governments to adopt + +**BibTeX:** +```bibtex +@misc{code_for_america, + title = {Code for America}, + author = {{Code for America}}, + year = {2026}, + url = {https://codeforamerica.org/}, + note = {National nonprofit transforming government digital services and convening state and local government technology leaders} +} +``` + +**Citation:** "Code for America. https://codeforamerica.org/" + +--- + +### **GovTech.com (Government Technology)** ⭐ + +The primary news and ranking source for the government technology industry, providing market intelligence and trend analysis. + +**Organization:** Government Technology (e.Republic) +**Website:** https://www.govtech.com/ +**About:** Leading publication covering technology trends, policy, and innovation in state and local government +**Founded:** 1987 +**Coverage:** State and local government across all 50 states + +**What we use:** +- **GovTech 100**: Annual definitive directory of the top 100 trending companies in the U.S. public sector technology market +- **Industry Trends**: Analysis of emerging technologies, procurement trends, and digital transformation initiatives +- **Vendor Landscape**: Tracking government technology companies, products, and solutions +- **Policy Coverage**: Legislative and regulatory developments affecting civic technology + +**Key Resources:** +- **GovTech 100 List**: https://www.govtech.com/100/ - The definitive annual ranking of companies shaping the future of state and local government +- **Navigator Awards**: https://www.govtech.com/navigator - Recognition of state and local government IT leaders +- **Digital Cities Survey**: Annual ranking of America's most digitally advanced cities and counties +- **Research Center**: https://www.govtech.com/research/ - White papers, surveys, and industry reports +- **Webinars & Events**: https://www.govtech.com/events/ + +**GovTech 100 Categories:** +- Cloud & Infrastructure +- Cybersecurity +- Data & Analytics +- Digital Government Services +- Education Technology +- Emergency Management +- Financial Management +- GIS & Mapping +- Health & Human Services +- Public Safety +- Transportation + +**Why GovTech.com:** +- **Market Intelligence**: The GovTech 100 is the authoritative list of trending companies in government technology +- **Vendor Discovery**: Comprehensive directory of solutions available to state and local government +- **Industry Standards**: Defines what's considered "trending" and "emerging" in the civic tech marketplace +- **Procurement Insights**: Helps identify which technologies governments are actively adopting + +**Resources:** +- Main Site: https://www.govtech.com/ +- GovTech 100: https://www.govtech.com/100/ +- Newsletter: https://www.govtech.com/newsletters/ +- Podcasts: https://www.govtech.com/podcasts/ +- Magazine Archive: https://www.govtech.com/magazines/ + +**BibTeX:** +```bibtex +@misc{govtech_magazine, + title = {Government Technology Magazine}, + author = {{e.Republic Inc.}}, + year = {2026}, + url = {https://www.govtech.com/}, + note = {Leading publication and market intelligence source for state and local government technology, publisher of the annual GovTech 100 list} +} +``` + +**GovTech 100 BibTeX:** +```bibtex +@misc{govtech_100, + title = {GovTech 100: Companies Trending in State and Local Government}, + author = {{Government Technology}}, + year = {2026}, + url = {https://www.govtech.com/100/}, + note = {Annual directory of the top 100 trending companies serving the U.S. public sector} +} +``` + +**Citation:** "Government Technology. e.Republic Inc. https://www.govtech.com/" + +--- + +### **Civic Tech Guide** ⭐ + +Comprehensive, curated directory of civic technology projects, organizations, and tools worldwide, maintained by the civic tech community. + +**Organization:** Civic Tech Field Guide (Community-maintained) +**Website:** https://app.civictech.guide/ +**About:** Open directory and knowledge base of civic tech projects, tools, and organizations with detailed project profiles and categorization +**Founded:** 2018 +**Coverage:** Global civic technology ecosystem (1,000+ projects) + +**What we use:** +- **Project Directory**: Discovery of related civic tech tools and platforms +- **Categorization**: Understanding how civic tech projects are classified and tagged +- **Community Connections**: Network of civic technologists and organizations +- **Best Practices**: Learning from similar projects and their approaches + +**CommunityOne Profile:** +- **Listed as**: https://app.civictech.guide/p/communityone/r/recN0BG4gvjXT7WLf +- **Categories**: Open Government, Civic Engagement, AI/Machine Learning +- **Description**: AI-powered civic engagement platform tracking local government meetings, legislation, and advocacy opportunities + +**Key Features:** +- **Search & Filter**: Discover projects by topic, geography, technology, and impact area +- **Project Profiles**: Detailed information about civic tech initiatives including status, team, and technology +- **Tagging System**: Categorization by civic tech domains (transparency, participation, accountability, etc.) +- **API Access**: Programmatic access to the project database +- **Community Contributions**: Open for civic tech projects to self-list and update profiles + +**Categories in Civic Tech Guide:** +- Open Government & Transparency +- Civic Participation & Engagement +- Community Organizing +- Democracy & Voting +- Public Service Delivery +- Data & Research +- Advocacy & Policy +- Urban Planning & Development + +**Resources:** +- Main Site: https://app.civictech.guide/ +- About: https://civictech.guide/ +- Submit Project: https://app.civictech.guide/submit +- GitHub: https://github.com/compilerla/civic-tech-taxonomy + +**Why Civic Tech Guide:** +- **Discovery**: Find related projects and potential collaborators +- **Context**: Understand where your project fits in the broader civic tech ecosystem +- **Community**: Connect with civic technologists working on similar problems +- **Legitimacy**: Being listed establishes credibility in the civic tech community + +**BibTeX:** +```bibtex +@misc{civic_tech_guide, + title = {Civic Tech Field Guide}, + author = {{Civic Tech Field Guide Community}}, + year = {2026}, + url = {https://civictech.guide/}, + note = {Community-maintained directory of civic technology projects and organizations worldwide} +} +``` + +**Citation:** "Civic Tech Field Guide. https://civictech.guide/" + +--- + +## �️ **Technology Platforms & Support Programs** + +### **Databricks for Good Program** ⭐ + +A philanthropic initiative providing cloud data platform credits and technical support to nonprofits, academic institutions, and social impact organizations. + +**Organization:** Databricks, Inc. +**Website:** https://www.databricks.com/product/databricks-for-good +**Application:** https://www.databricks.com/product/databricks-for-good + +**Eligibility:** +- ✅ **Nonprofits** - 501(c)(3) status required +- ✅ **Academic institutions** - Universities, colleges, research organizations +- ✅ **Social impact organizations** - Civic engagement, public good projects +- ✅ **Government agencies** - Case-by-case evaluation for civic data initiatives + +**CommunityOne/Open Navigator Alignment:** +This project appears well-suited for the program as a civic engagement and social good initiative. Eligibility would depend on establishing formal nonprofit status or academic partnership. + +**Program Benefits:** +- **$10,000-50,000** in annual Databricks credits +- Access to **Unity Catalog** (normally $0.20 per million metadata operations) +- Access to **Databricks Marketplace** for data sharing and distribution +- **Standard tier** platform features included +- **Technical support** from Databricks team +- **Delta Sharing** protocol for secure data distribution +- **MLflow** for AI/ML experiment tracking +- **Databricks SQL** for analytics and dashboards + +**What This Enables for Open Navigator:** +- **Data Publishing**: Share 1.8M nonprofit profiles, 4.5M+ legislative documents via Databricks Marketplace +- **Unity Catalog**: Organize data assets with enterprise-grade governance +- **Delta Sharing**: Distribute datasets to enterprise/research consumers without data copying +- **Lakehouse Architecture**: Unified analytics on legislative, nonprofit, and civic data +- **Collaborative Notebooks**: Reproducible research and analysis +- **Scheduled Pipelines**: Automated data updates and quality checks + +**Alternative Path (Hybrid Approach):** +- **HuggingFace Hub**: Continue using for open-source community distribution (free) +- **Databricks Marketplace**: Add enterprise/research distribution channel (if approved) +- **Data stays in one place**: External tables in Unity Catalog point to existing Parquet files +- **No data duplication**: Delta Sharing streams data from your storage on-demand + +**BibTeX:** +```bibtex +@misc{databricks_for_good, + title = {Databricks for Good Program}, + author = {{Databricks, Inc.}}, + year = {2024}, + url = {https://www.databricks.com/product/databricks-for-good}, + note = {Cloud data platform credits and support for nonprofits, academic institutions, and social impact organizations} +} +``` + +**Application Process:** +1. Visit https://www.databricks.com/product/databricks-for-good +2. Submit organization details and project description +3. Describe social impact and data use case +4. Provide 501(c)(3) documentation (for nonprofits) or academic affiliation +5. Review process typically takes 2-4 weeks +6. Upon approval, receive credits and onboarding support + +**Compliance:** +Credits must be used for the approved social impact project and cannot be resold or transferred. Annual renewal required with impact reporting. + +--- + +## �🙏 **Acknowledgments** + +We are grateful to the authors of MeetingBank for making their dataset publicly available for research purposes. Their work on meeting summarization has been instrumental in developing civic engagement tools. + +Special thanks to: +- The Association for Computational Linguistics (ACL) +- HuggingFace for hosting datasets +- Open States for legislative data +- All municipal governments providing open access to meeting records + +--- + +## 📖 **How to Cite This Project** + +If you use Open Navigator in your research, please cite: + +``` +Open Navigator +GitHub: https://github.com/getcommunityone/open-navigator-for-engagement +License: MIT +``` + +**BibTeX:** +```bibtex +@software{open-navigator-2026, + title = {Open Navigator}, + author = {Community One}, + year = {2026}, + url = {https://github.com/getcommunityone/open-navigator-for-engagement}, + license = {MIT} +} +``` + +--- + +## 📝 **License Compliance** + +This project respects all dataset licenses and terms of use. See [LICENSE](LICENSE) for this project's MIT license. + +For dataset-specific licenses, please refer to the original sources listed above. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..96e273554b57c20b9a91ce86c511b924f987c4f7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,99 @@ +# Contributing to Oral Health Policy Pulse + +Thank you for your interest in contributing to the Oral Health Policy Pulse project! + +## How to Contribute + +### Reporting Bugs + +If you find a bug, please open an issue with: +- A clear description of the problem +- Steps to reproduce +- Expected vs actual behavior +- Your environment (OS, Python version, etc.) + +### Suggesting Features + +Feature requests are welcome! Please: +- Check if the feature has already been requested +- Clearly describe the feature and its use case +- Explain how it would benefit advocacy groups + +### Code Contributions + +1. **Fork the repository** +2. **Create a feature branch** + ```bash + git checkout -b feature/your-feature-name + ``` + +3. **Make your changes** + - Follow the existing code style + - Add tests for new functionality + - Update documentation as needed + +4. **Run tests** + ```bash + pytest + black . + ruff check . + ``` + +5. **Commit your changes** + ```bash + git commit -m "Add feature: description" + ``` + +6. **Push and create a pull request** + ```bash + git push origin feature/your-feature-name + ``` + +## Code Style + +- Follow PEP 8 guidelines +- Use type hints +- Write docstrings for all public functions +- Keep functions focused and single-purpose +- Use meaningful variable names + +## Code of Conduct + +This project values respectful, inclusive collaboration. We align with the principles of: +- **Open States Code of Conduct**: https://docs.openstates.org/code-of-conduct/ +- Be respectful and professional +- Welcome diverse perspectives +- Focus on what's best for the community +- Show empathy towards other contributors + +## Contributing to Upstream Projects + +We use data and patterns from several open source civic tech projects. When contributing scraper patterns or improvements back to upstream projects like **OpenStates**, please: + +1. **Follow their standards**: https://github.com/openstates/openstates-scrapers +2. **Reference their documentation**: https://docs.openstates.org/contributing/local-database/ +3. **Respect their Code of Conduct**: https://docs.openstates.org/code-of-conduct/ +4. **Test locally** before submitting pull requests +5. **Document data sources** used in scraper development + +## Testing + +All new features should include tests. Run the test suite with: + +```bash +pytest tests/ -v +``` + +## Documentation + +Update relevant documentation when: +- Adding new features +- Changing API endpoints +- Modifying configuration options +- Adding new dependencies + +## Questions? + +Open an issue or reach out to the maintainers. + +Thank you for helping improve oral health advocacy! 🦷 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..352fc29ef8242f8c5ed927e5a7b5f1005aae19e6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,90 @@ +# Multi-stage build for Hugging Face Spaces +# Runs all three apps: Docusaurus docs, React frontend, FastAPI backend + +FROM node:20-slim AS docs-builder +WORKDIR /build + +# Set baseUrl to /docs/ for HuggingFace deployment # Docs are served at nginx /docs/ location +# routeBasePath: '/' in docusaurus.config.ts prevents /docs/docs/ nesting +ENV DOCUSAURUS_BASE_URL=/docs/ + +COPY website/package*.json ./ +RUN npm config set fetch-retry-mintimeout 20000 && \ + npm config set fetch-retry-maxtimeout 120000 && \ + npm ci --prefer-offline --no-audit || npm install --prefer-offline --no-audit + +# Add cache-busting argument to force rebuild when needed +ARG CACHE_BUST=2026-04-27-12-00-fix-double-docs-prefix + +COPY website/ ./ + +# Verify environment variable is set and build +RUN echo "Building Docusaurus with DOCUSAURUS_BASE_URL=$DOCUSAURUS_BASE_URL" && \ + echo "Cache bust: 2026-04-27-12-00-fix-double-docs-prefix" && \ + npm run build && \ + echo "Verifying baseUrl in build output..." && \ + grep -r "baseUrl" build/ | head -5 || true + +FROM python:3.11-slim + +# Install system dependencies, nginx, and Node.js for frontend build +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + git \ + tesseract-ocr \ + nginx \ + supervisor \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy Python requirements and install +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# OPTIMIZATION: Copy frontend package files first for better caching +COPY frontend/package*.json /app/frontend/ +RUN cd /app/frontend && npm ci + +# Copy application code (now npm ci layer is cached) +COPY . . + +# Copy built static files from docs stage +COPY --from=docs-builder /build/build /app/static/docs + +# Build frontend (npm_modules already cached from above) +# Set production environment variables for Vite +ENV VITE_CANONICAL_DOMAIN=www.communityone.com +ENV VITE_API_URL=/api +# Cache bust: 2026-04-29-remove-axios +ARG CACHE_BUST_FRONTEND=2026-04-29-remove-axios +RUN cd /app/frontend && echo "Frontend build cache bust: $CACHE_BUST_FRONTEND" && npm run build + +# Frontend is already built to /app/api/static/ via vite.config.ts +# Create frontend directory in /app/static for nginx +RUN mkdir -p /app/static/frontend && \ + ls -la /app/api/static/ && \ + cp -r /app/api/static/* /app/static/frontend/ + +# Create necessary directories +RUN mkdir -p /app/logs /app/data /var/log/supervisor + +# Copy Hugging Face specific configs +COPY .huggingface/nginx.conf /etc/nginx/nginx.conf +COPY .huggingface/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY .huggingface/start.sh /app/start.sh +RUN chmod +x /app/start.sh + +# Expose port 7860 (Hugging Face Spaces default) +EXPOSE 7860 + +# Set environment variables +ENV PYTHONUNBUFFERED=1 +ENV LOG_LEVEL=INFO +ENV HF_SPACES=1 + +# Use supervisor to run all services +CMD ["/app/start.sh"] diff --git a/INTEL_ARC_QUICKSTART.md b/INTEL_ARC_QUICKSTART.md new file mode 100644 index 0000000000000000000000000000000000000000..0a2b5a1c16f01246a16613c19569024bb6c283e7 --- /dev/null +++ b/INTEL_ARC_QUICKSTART.md @@ -0,0 +1,281 @@ +# 🚀 Intel Arc + DuckDB Quick Reference + +**Get started with local AI legislative analysis in 5 minutes** + +## ⚡ Performance at a Glance + +| Task | Standard (Postgres + CPU) | Optimized (DuckDB + Arc GPU) | Speedup | +|------|--------------------------|------------------------------|---------| +| Context injection (100 bills) | 500ms | 20ms | **25x** | +| Vector search (10K records) | 800ms | 18ms | **44x** | +| LLM inference (3B model) | 350 tok/s | 1,200 tok/s | **3.4x** | +| Full testimony analysis | 2,000ms | 80ms | **25x** | + +## 🎯 Three-Step Setup + +### 1. Install (5 minutes) + +```bash +cd /path/to/open-navigator +./scripts/intel_llm_setup.sh +source .venv-intel/bin/activate +``` + +### 2. Test DuckDB VSS (30 seconds) + +```bash +python scripts/duckdb_vss_demo.py +``` + +Expected output: +``` +📊 Creating demo DuckDB database with VSS... +✅ Demo database created! +📈 Results (searching 1,000 bills): + Average: 18.45ms +🎯 Top 3 most similar bills: ... +``` + +### 3. Run Analysis (1 minute) + +```bash +python scripts/legislative_analysis_intel.py +``` + +## 🧠 Code Examples + +### Example 1: Fast Bill Search + +```python +from scripts.legislative_analysis_intel import DuckDBLegislativeAnalyzer + +with DuckDBLegislativeAnalyzer() as analyzer: + # Get bill context in < 50ms + bill = analyzer.get_bill_context("HB1234") + testimony = analyzer.get_all_testimony_for_bill("HB1234") + + print(f"Bill: {bill['title']}") + print(f"Testimony records: {len(testimony)}") +``` + +### Example 2: Vector Similarity Search + +```python +import numpy as np + +# Your query embedding (384 dimensions from sentence-transformers) +query_embedding = model.encode("water fluoridation policy") + +# Fast vector search (< 20ms for 10K bills) +similar_bills = analyzer.search_similar_testimony( + query_embedding.tolist(), + limit=10 +) + +for bill in similar_bills: + print(f"{bill['bill_id']}: {bill['text'][:100]}... (similarity: {bill['similarity']:.2f})") +``` + +### Example 3: Extract Interest Groups + +```python +from scripts.legislative_analysis_intel import IntelOptimizedLLM, InterestGroup + +# Initialize Intel-optimized LLM (uses Arc GPU) +llm = IntelOptimizedLLM(model_name="meta-llama/Llama-3.2-3B-Instruct") +llm.load_model(use_openvino=True) # OpenVINO = best Arc GPU performance + +# Extract structured data +groups = llm.extract_interest_groups(bill_context, testimony) + +# Results +for group in groups: + print(f""" + Group: {group.group_name} + Lobbyist: {group.lobbyist} + Stance: {group.stance} (score: {group.stance_score}) + Tradeoffs: {group.tradeoff_notes} + Confidence: {group.confidence} + """) +``` + +### Example 4: Query Hugging Face Datasets Directly + +```python +import duckdb + +conn = duckdb.connect() + +# No download needed - streams from HF! +df = conn.execute(""" + SELECT * + FROM read_parquet( + 'hf://datasets/CommunityOne/states-al-nonprofits-locations/data/train-*.parquet' + ) + WHERE city = 'Birmingham' + LIMIT 100 +""").fetchdf() + +print(f"Found {len(df)} organizations in Birmingham, AL") +``` + +## 🎨 Output Schema + +**Interest Group Extraction:** + +```json +{ + "groups": [ + { + "group_name": "Alabama Dental Association", + "lobbyist": "John Smith, DDS", + "stance": "conditional", + "stance_score": 0.6, + "tradeoff_notes": "Support if Section 4 amended to include rural exemption and phased implementation timeline", + "testimony_excerpt": "While we have concerns about Section 4's implementation timeline, we support the overall goals if rural communities receive proper resources...", + "bill_id": "HB1234", + "confidence": 0.85 + }, + { + "group_name": "Sierra Club Alabama Chapter", + "lobbyist": null, + "stance": "oppose", + "stance_score": -0.9, + "tradeoff_notes": null, + "testimony_excerpt": "We strongly oppose this bill due to environmental concerns...", + "bill_id": "HB1234", + "confidence": 0.92 + } + ] +} +``` + +## 🔧 Environment Variables + +```bash +# Enable Intel GPU +export ZES_ENABLE_SYSMAN=1 + +# Ollama GPU usage (if using Ollama) +export OLLAMA_NUM_GPU=999 + +# IPEX-LLM optimizations +export IPEX_LLM_NUM_GPU=1 +export ONEAPI_DEVICE_SELECTOR=level_zero:0 +``` + +## 💡 Best Practices + +### 1. Cache Embeddings + +**DON'T** recompute every time: +```python +# Slow - recomputes embeddings every run +for bill in bills: + embedding = model.encode(bill['text']) + analyze(embedding) +``` + +**DO** cache in DuckDB: +```python +# Fast - compute once, reuse forever +conn.execute(""" + CREATE TABLE bill_embeddings AS + SELECT bill_id, embedding + FROM ... -- computed once +""") + +# Then just query +similar = conn.execute(""" + SELECT * FROM bill_embeddings + ORDER BY array_distance(embedding, ?) + LIMIT 10 +""", [query]).fetchall() +``` + +### 2. Batch Processing + +**DON'T** process one at a time: +```python +for bill_id in bill_ids: # Slow! + result = analyze_single_bill(bill_id) +``` + +**DO** batch efficiently: +```python +# Fast - processes 100 bills in parallel +results = llm.extract_interest_groups_batch( + bill_contexts=bills, + testimony_batches=all_testimony, + batch_size=32 # Fits in Arc GPU memory +) +``` + +### 3. Monitor GPU Usage + +```bash +# Linux: intel_gpu_top +sudo apt install intel-gpu-tools +intel_gpu_top + +# Windows: Task Manager → Performance → GPU +# Look for "GPU 0 - Intel Arc Graphics" +``` + +## 🐛 Troubleshooting + +### Issue: "ModuleNotFoundError: optimum" + +```bash +pip install optimum[openvino] +``` + +### Issue: Slow inference (still using CPU) + +Check device: +```python +import torch +print(f"Device: {torch.cuda.get_device_name(0)}") # Should show Arc GPU + +# Force GPU +model = OVModelForCausalLM.from_pretrained( + model_name, + device="GPU" # Explicitly set +) +``` + +### Issue: Out of memory + +Use smaller model or reduce batch size: +```python +# Use 3B instead of 8B +model_name = "meta-llama/Llama-3.2-3B-Instruct" + +# Reduce context +testimony = testimony[:10] # Top 10 only +``` + +## 📚 Resources + +- **Full Guide**: [website/docs/guides/intel-arc-optimization.md](../website/docs/guides/intel-arc-optimization.md) +- **DuckDB Docs**: https://duckdb.org/docs/ +- **Intel IPEX**: https://github.com/intel/intel-extension-for-pytorch +- **OpenVINO**: https://docs.openvino.ai/ + +## 🎯 Next Steps + +1. ✅ Run the demo: `python scripts/duckdb_vss_demo.py` +2. ✅ Test analysis: `python scripts/legislative_analysis_intel.py` +3. 📚 Read full guide: [Intel Arc Optimization Guide](../website/docs/guides/intel-arc-optimization.md) +4. 🚀 Build your own: Use the `DuckDBLegislativeAnalyzer` class +5. 🤝 Share results: Open an issue with your findings! + +## 💬 Questions? + +- **GitHub Issues**: https://github.com/getcommunityone/open-navigator/issues +- **Documentation**: https://www.communityone.com/docs +- **Intel AI Forums**: https://community.intel.com/t5/Intel-AI-Analytics-and/bd-p/software-ai + +--- + +**Built with ❤️ for Data Engineering Managers who want local, private, fast legislative intelligence.** diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..7442a2a1966bf89af46bbf471b4f6f87c481c184 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Support. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Community One + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..f441ab9fdcdaf727e1a4f75f2ac0af35232afa38 --- /dev/null +++ b/Makefile @@ -0,0 +1,169 @@ +.PHONY: help install install-frontend install-docs build-frontend build-docs clean test run dev dev-frontend dev-docs start-all stop-all dev-full docker-up docker-down deploy-databricks + +help: + @echo "🦷 Open Navigator - Makefile Commands" + @echo "====================================================" + @echo "" + @echo "🚀 Quick Start:" + @echo " make start-all - Start ALL services (API + Dashboard + Docs) with tmux" + @echo " make stop-all - Stop all running services" + @echo "" + @echo "🐍 Python Backend:" + @echo " make install - Install Python dependencies in venv" + @echo " make dev - Start backend with auto-reload" + @echo " make run - Start backend (production)" + @echo "" + @echo "⚛️ React Dashboard:" + @echo " make install-frontend - Install dashboard npm dependencies" + @echo " make build-frontend - Build React dashboard for production" + @echo " make dev-frontend - Start dashboard dev server" + @echo "" + @echo "📚 Documentation Site:" + @echo " make install-docs - Install Docusaurus dependencies" + @echo " make build-docs - Build documentation for production" + @echo " make dev-docs - Start documentation dev server" + @echo "" + @echo "☁️ Deployment:" + @echo " make deploy-databricks - Deploy to Databricks Apps" + @echo "" + @echo "🐳 Docker:" + @echo " make docker-up - Start Docker containers" + @echo " make docker-down - Stop Docker containers" + @echo "" + @echo "🧪 Testing:" + @echo " make test - Run test suite" + @echo " make clean - Remove build artifacts" + @echo "" + +install: + @echo "📦 Creating virtual environment and installing dependencies..." + @chmod +x install.sh + @./install.sh + +install-frontend: + @echo "📦 Installing dashboard dependencies..." + @cd frontend && npm install + @echo "✅ Dashboard dependencies installed!" + +install-docs: + @echo "📦 Installing documentation dependencies..." + @cd website && npm install + @echo "✅ Documentation dependencies installed!" + +build-frontend: + @echo "🔨 Building React dashboard..." + @cd frontend && npm run build + @echo "✅ Dashboard built to api/static/" + +build-docs: + @echo "🔨 Building documentation site..." + @cd website && npm run build + @echo "✅ Documentation built to website/build/" + +clean: + @echo "🧹 Cleaning up..." + @rm -rf .venv venv + @rm -rf frontend/node_modules frontend/dist + @rm -rf website/node_modules website/build website/.docusaurus + @rm -rf api/static + @rm -rf __pycache__ + @find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + @find . -type f -name "*.pyc" -delete + @find . -type f -name "*.pyo" -delete + @rm -rf .pytest_cache + @rm -rf .coverage + @rm -rf htmlcov + @rm -rf dist + @rm -rf build + @rm -rf *.egg-info + @rm -rf logs/*.pid logs/*.log + @echo "✅ Cleanup complete" + +test: + @echo "🧪 Running tests..." + @. venv/bin/activate && pytest tests/ -v + +run: build-frontend + @echo "🚀 Starting application (production mode)..." + @. venv/bin/activate && uvicorn api.app:app --host 0.0.0.0 --port 8000 + +dev: + @echo "🔧 Starting backend with auto-reload..." + @echo "📡 Backend running at http://localhost:8000" + @. venv/bin/activate && uvicorn api.app:app --reload + +dev-frontend: + @echo "⚛️ Starting dashboard dev server..." + @echo "📡 Dashboard running at http://localhost:5173" + @cd frontend && npm run dev + +dev-docs: + @echo "📚 Starting documentation dev server..." + @echo "📡 Documentation running at http://localhost:3000" + @cd website && npm start + +start-all: + @echo "🚀 Starting all services with tmux..." + @chmod +x start-all.sh + @./start-all.sh + +stop-all: + @echo "🛑 Stopping all services..." + @chmod +x stop-all.sh + @./stop-all.sh + +dev-full: + @echo "🚀 Use 'make start-all' for better experience with tmux!" + @echo "" + @echo "Starting backend and frontend (manual)..." + @echo "📡 Backend: http://localhost:8000" + @echo "📡 Dashboard: http://localhost:5173" + @echo "📡 Docs: http://localhost:3000 (run 'make dev-docs' in another terminal)" + @echo "" + @. venv/bin/activate && uvicorn api.app:app --reload & \ + cd frontend && npm run dev + +deploy-databricks: + @echo "☁️ Deploying to Databricks Apps..." + @chmod +x scripts/deploy-databricks-app.sh + @./scripts/deploy-databricks-app.sh + +docker-up: + @echo "Starting Docker containers..." + @docker-compose up -d + @echo "✓ Containers started" + @echo " API: http://localhost:8000" + @echo " Docs: http://localhost:8000/docs" + +docker-down: + @echo "Stopping Docker containers..." + @docker-compose down + @echo "✓ Containers stopped" + +example: + @echo "Running example workflow..." + @. venv/bin/activate && python examples/example_workflow.py + +heatmap: + @echo "Generating example heatmap..." + @. venv/bin/activate && python main.py generate-heatmap --output example_heatmap.html + @echo "✓ Heatmap saved to example_heatmap.html" + +init: + @echo "Initializing system..." + @. venv/bin/activate && python main.py init + +status: + @echo "Checking system status..." + @. venv/bin/activate && python main.py status + +format: + @echo "Formatting code..." + @. venv/bin/activate && black . + @. venv/bin/activate && ruff check . --fix + @echo "✓ Code formatted" + +lint: + @echo "Linting code..." + @. venv/bin/activate && ruff check . + @. venv/bin/activate && mypy agents/ pipeline/ visualization/ api/ diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..06a923ee55d14c52d2bfd2a11f06789cb4015325 --- /dev/null +++ b/README.md @@ -0,0 +1,534 @@ +--- +title: Open Navigator +emoji: 🏛️ +colorFrom: blue +colorTo: green +sdk: docker +app_port: 7860 +pinned: false +license: apache-2.0 +--- + +# 🏛️ Open Navigator + +> **CommunityOne: The open path to everything local** +> +> AI-powered civic engagement platform with React + FastAPI web interface + +[![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) +[![React](https://img.shields.io/badge/React-18.2-61DAFB.svg)](https://reactjs.org) +[![FastAPI](https://img.shields.io/badge/FastAPI-0.109.0-009688.svg)](https://fastapi.tiangolo.com) + +## � Quick Links + +**[⚛️ Open Navigator →](https://www.communityone.com)** - **LIVE APPLICATION** (search, filters, heatmap, data exploration) + +**[📖 Documentation →](https://www.communityone.com/docs)** - Complete guides, architecture, and feature details + +The documentation site includes: +- Features and capabilities +- Data sources and integrations +- Architecture and deployment options +- Policy topics and advocacy tools +- API reference and examples + +--- + +## Quick Start + +### Three Services + +This project runs three separate services: + +| Service | Port (Local) | Live URL | Description | +|---------|------|----------|-------------| +| **⚛️ Open Navigator** 🚀 | 5173 | [www.communityone.com](https://www.communityone.com) | **MAIN APPLICATION** - Search, filters, heatmap, data exploration | +| **📚 Documentation** | 3000 | [www.communityone.com/docs](https://www.communityone.com/docs) | Docusaurus site with complete guides and tutorials | +| **🔥 API Backend** | 8000 | [www.communityone.com/api](https://www.communityone.com/api) | FastAPI server with AI agents | + +> **💡 LIVE DEMO:** Visit **[www.communityone.com](https://www.communityone.com)** to use the application! +> +> **💻 LOCAL DEV:** After running `./start-all.sh`, visit **http://localhost:5173** + +## 🚀 Deployment + +**Deploy to Hugging Face Spaces** in 3 commands: + +```bash +echo "HF_USERNAME=your_username" >> .env +./deploy-huggingface.sh +# Configure hardware and secrets at https://huggingface.co/spaces/YOUR_USERNAME/www.communityone.com +``` + +**Full deployment guides:** +- **[Hugging Face Spaces](website/docs/deployment/huggingface-spaces.md)** - Docker deployment (~$22/month) +- **[Databricks Apps](website/docs/deployment/databricks-apps.md)** - Enterprise deployment +- **[Local Development](website/docs/deployment/)** - Complete deployment documentation + +The `deploy-huggingface.sh` script automatically: +- ✅ Tests builds locally (catches errors before pushing) +- ✅ Creates the Space on Hugging Face +- ✅ Pushes code and triggers automatic build (~10-15 min) + + +### Prerequisites + +- Python 3.11+ +- Node.js 18+ +- Docker (optional) +- OpenAI API key + +### Installation + +**Option 1: Start Everything at Once (Recommended)** + +```bash +# Clone repository +git clone https://github.com/getcommunityone/open-navigator.git +cd open-navigator + +# Install dependencies +./install.sh # Python backend +cd frontend && npm install && cd .. # React app +cd website && npm install && cd .. # Documentation + +# Setup git hooks for build protection (one-time) +./setup-git-hooks.sh + +# Start all services in tmux +./start-all.sh +``` + +**Option 2: Using Makefile** + +```bash +# Install +make install +make install-frontend +make install-docs + +# Start all services +make start-all + +# Or individually: +make dev # API only +make dev-frontend # React app only +make dev-docs # Docs only +``` + +**Option 3: Manual Setup** + +```bash +# Python backend +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt + +# React app +cd frontend && npm install && cd .. + +# Documentation +cd website && npm install && cd .. + +# Configure environment +cp .env.example .env +# Edit .env with your API keys + +# Start services (separate terminals) +source .venv/bin/activate && python main.py serve # Terminal 1 +cd frontend && npm run dev # Terminal 2 +cd website && npm start # Terminal 3 +``` + +### Access Points + +**🌐 LIVE APPLICATION:** +- **🚀 Open Navigator:** https://www.communityone.com - Main application +- 📚 **Documentation:** https://www.communityone.com/docs - Guides and API reference +- 🔥 **API Docs:** https://www.communityone.com/api/docs - FastAPI interactive documentation + +**💻 LOCAL DEVELOPMENT:** +- **🚀 Main App:** http://localhost:5173 +- 📚 **Documentation:** http://localhost:3000 +- 🔥 **API Docs:** http://localhost:8000/docs + +### Stop Services + +```bash +./stop-all.sh +# or +make stop-all +``` + +--- + +## Usage + +### Command Line Interface + +Always activate the virtual environment first: + +```bash +source .venv/bin/activate +``` + +**API Server** + +```bash +python main.py serve --host 0.0.0.0 --port 8000 +``` + +**Jurisdiction Discovery** + +```bash +# Test run +python main.py discover-jurisdictions --limit 100 + +# Single state +python main.py discover-jurisdictions --state CA + +# Full discovery (~30k jurisdictions) +python main.py discover-jurisdictions + +# View statistics +python main.py discovery-stats +``` + +**Data Ingestion** + +```bash +# Census data (90,000+ jurisdictions) +python -m discovery.census_ingestion + +# NCES school districts (13,000+) +python -m discovery.nces_ingestion + +# Pre-built meeting datasets +python discovery/meetingbank_ingestion.py +python discovery/city_scrapers_urls.py +python discovery/openstates_sources.py + +# LocalView (requires Dataverse API key) +python discovery/localview_ingestion.py +``` + +**Scraping & Analysis** + +```bash +# Scrape batch from discovered sites +python main.py scrape-batch --source discovered --limit 50 + +# Scrape single source +python main.py scrape --url "https://city.legistar.com" \ + --state "CA" \ + --municipality "San Francisco" + +# Run analysis pipeline +python main.py analyze --targets-file examples/targets.json + +# Generate heatmap +python main.py generate-heatmap --output heatmap.html +``` + +**Publishing Datasets** + +```bash +# Publish to HuggingFace (requires HUGGINGFACE_TOKEN in .env) +python main.py publish-to-hf --dataset all +python main.py publish-to-hf --dataset discovered-urls +python main.py publish-to-hf --dataset census --sample +``` + +### API Usage + +**Start a workflow:** + +```bash +curl -X POST "http://localhost:8000/workflow/start" \ + -H "Content-Type: application/json" \ + -d '{ + "scrape_targets": [ + { + "url": "https://example-city.legistar.com", + "municipality": "Example City", + "state": "CA", + "platform": "legistar" + } + ] + }' +``` + +**Query opportunities:** + +```bash +curl "http://localhost:8000/opportunities?state=CA&urgency=critical" +``` + +**Get heatmap:** + +```bash +curl "http://localhost:8000/heatmap" > heatmap.html +``` + +### Python API + +```python +import asyncio +from agents.orchestrator import OrchestratorAgent +from agents.scraper import ScraperAgent +from agents.parser import ParserAgent +from agents.classifier import ClassifierAgent + +# Initialize orchestrator +orchestrator = OrchestratorAgent() + +# Register agents +orchestrator.register_agent(ScraperAgent()) +orchestrator.register_agent(ParserAgent()) +orchestrator.register_agent(ClassifierAgent()) + +# Execute pipeline +targets = [ + { + "url": "https://city.legistar.com", + "municipality": "Example City", + "state": "CA", + "platform": "legistar" + } +] + +results = await orchestrator.execute_pipeline(targets) +``` + +--- + +## Project Structure + +``` +open-navigator/ +├── agents/ # Multi-agent AI system +├── api/ # FastAPI application +├── frontend/ # React application (Open Navigator) +├── website/ # Docusaurus documentation +├── discovery/ # Data discovery modules +├── extraction/ # Document extraction +├── pipeline/ # Data pipeline components +├── visualization/ # Heatmap and charts +├── config/ # Configuration +├── tests/ # Test suite +├── main.py # CLI entry point +└── requirements.txt # Python dependencies +``` + +--- + +## Deployment Options + +### 1. Databricks Apps (Production) + +```bash +export DATABRICKS_HOST=https://your-workspace.cloud.databricks.com +export DATABRICKS_TOKEN=dapi... +export OPENAI_API_KEY=sk-... + +./scripts/deploy-databricks-app.sh +``` + +See [DATABRICKS_APP_GUIDE.md](DATABRICKS_APP_GUIDE.md) for details. + +### 2. Docker + +```bash +docker-compose up -d +``` + +Starts: +- API server (port 8000) +- Qdrant vector database (port 6333) +- Jupyter notebook (port 8888) + +### 3. Local Development + +See [Quick Start](#quick-start) above. + +--- + +## ⚡ Intel Arc GPU Optimization + +**Run Llama 4 at NVIDIA-like speeds on Intel Arc integrated graphics!** + +If you have **Intel Core Ultra 7** (or similar) with Arc Graphics + NPU, you can use **DuckDB + VSS** for 10-50x faster legislative analysis: + +```bash +# Setup Intel-optimized environment +./scripts/intel_llm_setup.sh +source .venv-intel/bin/activate + +# Run DuckDB vector search demo +python scripts/duckdb_vss_demo.py + +# Run legislative analysis with LLM +python scripts/legislative_analysis_intel.py +``` + +**Why DuckDB for Local AI?** +- ⚡ **10-50x faster** than Postgres for context injection +- 🎯 **< 20ms** vector similarity search across 10K bills +- 🧠 **Embedded** - no server needed, runs locally +- 🤗 **Hugging Face Integration** - query HF datasets directly + +**Performance:** +- **Context Injection**: 20ms vs 500ms (Postgres) = **25x faster** +- **LLM Inference**: 1,200 tok/s (Arc GPU) vs 350 tok/s (CPU) = **3.4x faster** +- **Vector Search**: 18ms vs 800ms = **44x faster** + +**Features:** +- Extract interest groups from legislative testimony +- Identify lobbyists and their positions +- Analyze support/oppose scores with confidence +- Detect tradeoffs and compromises + +**See full guide:** [Intel Arc Optimization Guide](website/docs/guides/intel-arc-optimization.md) + +--- + +## 🤖 AI Integration (MCP Server) + +**Connect your civic data to Claude and other AI assistants!** + +Open Navigator includes a **Model Context Protocol (MCP)** server that lets AI assistants directly access your data: + +```bash +# Install MCP dependencies +pip install mcp anthropic-mcp-sdk + +# Run the server +python scripts/mcp/open_navigator_server.py +``` + +**What AI assistants can do:** +- 🏛️ Search 90,000+ jurisdictions by name or location +- 🏢 Query 1.8M nonprofits with Form 990 data +- 📜 Semantic search across 4.5M+ legislative documents +- 📊 Get real-time statistics and analytics +- 🔍 Vector search meetings and bills with natural language + +**Example queries to Claude:** +> "Find all cities named Springfield in the database" + +> "Show me 501c3 nonprofits in San Francisco focused on education" + +> "What bills related to oral health were introduced in California?" + +**Configure Claude Desktop:** + +Add to `~/.config/Claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "open-navigator": { + "command": "python", + "args": ["/path/to/open-navigator/scripts/mcp/open_navigator_server.py"], + "env": { + "DATABASE_URL": "postgresql://postgres:password@localhost:5433/open_navigator" + } + } + } +} +``` + +**See full guide:** [MCP Server Documentation](website/docs/integrations/mcp-server.md) + +--- + +## Testing + +```bash +# Run all tests +pytest + +# With coverage +pytest --cov=agents --cov=pipeline --cov=visualization + +# Specific test file +pytest tests/test_agents.py +``` + +--- + +## Configuration + +Create `.env` file: + +```bash +# OpenAI +OPENAI_API_KEY=sk-... + +# Databricks (optional) +DATABRICKS_HOST=https://your-workspace.cloud.databricks.com +DATABRICKS_TOKEN=dapi... + +# HuggingFace (optional) +HUGGINGFACE_TOKEN=hf_... + +# Dataverse (optional) +DATAVERSE_API_KEY=... +``` + +--- + +## Contributing + +Contributions are welcome! Please: + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests +5. Submit a pull request + +See [CONTRIBUTING.md](CONTRIBUTING.md) for details. + +--- + +## Documentation + +- **[Full Documentation](http://localhost:3000)** - Complete guides and API reference +- **[Architecture](http://localhost:3000/docs/architecture)** - System architecture overview +- **[Quick Start](http://localhost:3000/docs/quickstart)** - Detailed setup instructions +- **[Quick Reference](http://localhost:3000/docs/quick-reference)** - Command reference card +- **[MCP Server](http://localhost:3000/docs/integrations/mcp-server)** - AI assistant integration guide +- **[Deployment](http://localhost:3000/docs/deployment/databricks-apps)** - Production deployment guides +- **[Case Studies](http://localhost:3000/docs/case-studies/tuscaloosa-complete)** - Real-world examples +- [CONTRIBUTING.md](CONTRIBUTING.md) - How to contribute + +--- + +## Citations + +This project uses several open datasets and research contributions. **Please see [CITATIONS.md](CITATIONS.md) for complete citation information.** + +**Key Dataset:** +- **MeetingBank**: Hu et al., "MeetingBank: A Benchmark Dataset for Meeting Summarization", ACL 2023 + - Used for meeting discovery and analysis + - 1,366 city council meetings from 6 U.S. cities + - See [CITATIONS.md](CITATIONS.md) for full citation and BibTeX + +--- + +## License + +Apache License 2.0 - see [LICENSE](LICENSE) file for details. + +--- + +## Support + +- GitHub Issues: [github.com/getcommunityone/open-navigator-for-engagement/issues](https://github.com/getcommunityone/open-navigator-for-engagement/issues) +- Email: johnbowyer@communityone.com + +--- + +**Note**: This system is designed to support advocacy efforts. All generated content should be reviewed by humans before use. diff --git a/README_HF.md b/README_HF.md new file mode 100644 index 0000000000000000000000000000000000000000..87f182b6345f51d2a69b8c316e7028b2aa37dc1d --- /dev/null +++ b/README_HF.md @@ -0,0 +1,101 @@ +--- +title: CommunityOne - Open Navigator +emoji: 🏛️ +colorFrom: blue +colorTo: green +sdk: docker +app_port: 7860 +pinned: false +license: apache-2.0 +tags: + - civic-engagement + - policy-tracking + - government-transparency + - nonprofit-discovery + - open-data +--- + +# 🏛️ CommunityOne - Open Navigator + +**Track 90,000+ jurisdictions. Monitor 1.8M nonprofits. Amplify your voice.** + +CommunityOne is a civic engagement platform that helps you discover advocacy opportunities, track policy changes, and connect with organizations working on the causes you care about. + +## ✨ Features + +- **🔍 Unified Search**: Find contacts, meetings, organizations, and causes across the entire United States +- **📊 Real-time Stats**: Track policy activity across 90,000+ cities, counties, and states +- **🏢 Nonprofit Discovery**: Explore 1.8M organizations from IRS data enriched with Every.org +- **📅 Meeting Minutes**: Search 250,000+ government meeting transcripts and agendas +- **🎯 Geographic Filtering**: Browse by state, county, or city to find local opportunities +- **🔐 OAuth Login**: Sign in with HuggingFace, GitHub, or Google to save your preferences + +## 🚀 Three Services Architecture + +This deployment runs three integrated services: + +1. **📚 Documentation** (Docusaurus) - `/docs/` +2. **🖥️ Main Application** (React + Vite) - `/` +3. **⚡ API Backend** (FastAPI) - `/api/` + +All services are reverse-proxied through nginx on port 7860. + +## 📖 Quick Start + +### Browse Without Login +- Click "Browse All" to explore data by state +- Use the search bar to find organizations, contacts, or causes +- Filter by location using the state/county/city selectors + +### Sign In for Personalization +- Click "Login" in the top right +- Choose your OAuth provider (HuggingFace, GitHub, or Google) +- Follow organizations, leaders, and causes you care about +- Get personalized recommendations + +### Explore the API +- Visit `/redoc` for interactive API documentation +- Try the search endpoints with state filters +- Export data in JSON format for your own projects + +## 🛠️ Technology Stack + +- **Frontend**: React 18 + TypeScript + Vite + TailwindCSS + shadcn/ui +- **Backend**: Python 3.11 + FastAPI + Pydantic +- **Data**: Delta Lake + Parquet (90GB+ of civic data) +- **Docs**: Docusaurus v3 +- **Infrastructure**: nginx + supervisor + Docker + +## 📊 Data Sources + +- **IRS BMF**: 1.8M tax-exempt organizations +- **Every.org**: Nonprofit enrichment (logos, causes, revenue) +- **Open States**: State legislators and bills (7,300+ officials) +- **Census**: Jurisdictions and boundaries (90,000+) +- **CityScrapers**: Local government meetings +- **OpenCivicData**: Standardized government data + +## 🔗 Links + +- **Repository**: [github.com/getcommunityone/open-navigator](https://github.com/getcommunityone/open-navigator) +- **Documentation**: Click "📚 Browse Documentation" on the homepage +- **API Docs**: `/redoc` endpoint +- **Website**: [www.communityone.com](https://www.communityone.com) + +## 📝 License + +Apache License 2.0 - Free for commercial and non-commercial use + +## 🤝 Contributing + +We welcome contributions! See CONTRIBUTING.md in the repository for guidelines. + +## 💬 Support + +- **Issues**: [GitHub Issues](https://github.com/getcommunityone/open-navigator/issues) +- **Discussions**: [GitHub Discussions](https://github.com/getcommunityone/open-navigator/discussions) +- **Email**: hello@communityone.com + +--- + +Built with ❤️ for civic engagement and government transparency. diff --git a/agents/__init__.py b/agents/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6f3393f84677587c2a450a0fc5066a4ab9a1756c --- /dev/null +++ b/agents/__init__.py @@ -0,0 +1,16 @@ +"""Agents module for the Oral Health Policy Pulse system.""" +from agents.base import BaseAgent, AgentRole, AgentMessage, MessageType, AgentStatus +from agents.orchestrator import OrchestratorAgent +from agents.debate_grader import DebateGraderAgent, DebateDimension, DebateScore + +__all__ = [ + "BaseAgent", + "AgentRole", + "AgentMessage", + "MessageType", + "AgentStatus", + "OrchestratorAgent", + "DebateGraderAgent", + "DebateDimension", + "DebateScore" +] diff --git a/agents/advocacy.py b/agents/advocacy.py new file mode 100644 index 0000000000000000000000000000000000000000..ff4e6cdb0e41e94428aabd0a0673a89a61a15136 --- /dev/null +++ b/agents/advocacy.py @@ -0,0 +1,408 @@ +""" +Advocacy Writer Agent for generating personalized outreach materials. +""" +import asyncio +from typing import List, Dict, Any, Optional +from datetime import datetime +from loguru import logger + +from agents.base import BaseAgent, AgentRole, AgentMessage, MessageType, AgentStatus + + +class AdvocacyWriterAgent(BaseAgent): + """ + Agent responsible for generating advocacy materials. + + Creates: + - Personalized emails to local officials + - Talking points for public testimony + - Social media content + - Policy briefs + - Community outreach materials + """ + + def __init__(self, agent_id: str = "advocacy-001"): + """Initialize the advocacy writer agent.""" + super().__init__(agent_id, AgentRole.ADVOCACY_WRITER) + self._initialize_templates() + + def _initialize_templates(self): + """Initialize email and content templates.""" + self.email_templates = { + "critical_vote": { + "subject": "Urgent: Support Oral Health Policy - Vote Upcoming in {municipality}", + "opening": ( + "I am writing to urge your support for the upcoming vote on " + "{policy_topic} in {municipality}." + ), + "urgency": "This matter requires immediate attention as a vote is scheduled for {meeting_date}." + }, + "introduce_topic": { + "subject": "Opportunity to Improve Community Oral Health in {municipality}", + "opening": ( + "I am writing to bring to your attention an important opportunity " + "to enhance oral health services in {municipality}." + ), + "urgency": None + }, + "address_opposition": { + "subject": "Addressing Concerns About {policy_topic} in {municipality}", + "opening": ( + "I understand there are concerns about {policy_topic}. " + "I would like to share evidence-based information that may help inform the discussion." + ), + "urgency": None + }, + "support_existing": { + "subject": "Thank You for Supporting Oral Health in {municipality}", + "opening": ( + "Thank you for your support of {policy_topic}. " + "I am writing to express my appreciation and offer additional support." + ), + "urgency": None + } + } + + self.policy_benefits = { + "water_fluoridation": [ + "Reduces tooth decay by 25% in children and adults", + "Costs approximately $1 per person per year", + "Recognized by CDC as one of 10 great public health achievements", + "Reduces dental treatment costs by $38 per $1 invested", + "Particularly benefits low-income families with limited access to dental care" + ], + "school_dental_screening": [ + "Early detection prevents costly emergency dental procedures", + "Identifies children who need care before problems become severe", + "Reduces school absences due to dental pain", + "Connects families to dental resources and services", + "Supported by American Academy of Pediatrics" + ], + "medicaid_dental": [ + "Improves health outcomes for vulnerable populations", + "Reduces emergency room visits for dental problems", + "Prevents progression of oral disease to systemic health issues", + "Supports working families and children", + "Generates economic returns through improved productivity" + ], + "dental_clinic_funding": [ + "Provides essential services to underserved communities", + "Reduces health disparities", + "Creates local jobs and economic activity", + "Prevents costly emergency care", + "Serves as safety net for uninsured and underinsured residents" + ] + } + + async def process(self, message: AgentMessage) -> List[AgentMessage]: + """ + Process advocacy generation commands. + + Args: + message: Message containing analyzed documents and opportunities + + Returns: + List of messages with generated advocacy materials + """ + self.update_status(AgentStatus.PROCESSING, "Generating advocacy materials") + + try: + documents = message.payload.get("documents", []) + opportunities = message.payload.get("opportunities", []) + + # Generate advocacy materials for each opportunity + advocacy_materials = [] + + for opp in opportunities: + materials = await self._generate_advocacy_materials(opp, documents) + advocacy_materials.append(materials) + + # Send results back to orchestrator + response = await self.send_message( + AgentRole.ORCHESTRATOR, + MessageType.RESPONSE, + { + "workflow_id": message.payload.get("workflow_id"), + "advocacy_materials": advocacy_materials, + "opportunities_count": len(opportunities), + "materials_generated": len(advocacy_materials) + } + ) + + self.log_success() + logger.info(f"Generated advocacy materials for {len(opportunities)} opportunities") + + return [response] + + except Exception as e: + self.log_failure(str(e)) + error_msg = await self.send_message( + AgentRole.ORCHESTRATOR, + MessageType.ERROR, + {"error": str(e), "agent": self.agent_id} + ) + return [error_msg] + + async def _generate_advocacy_materials( + self, + opportunity: Dict[str, Any], + all_documents: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Generate complete advocacy materials for an opportunity. + + Args: + opportunity: Advocacy opportunity details + all_documents: All analyzed documents for context + + Returns: + Dictionary containing all generated materials + """ + # Find the source document + doc = next( + (d for d in all_documents if d["document_id"] == opportunity["document_id"]), + None + ) + + if not doc: + logger.error(f"Document not found: {opportunity['document_id']}") + return {} + + # Determine template based on situation + template_type = self._select_template(opportunity) + + # Generate email + email = await self._generate_email(opportunity, doc, template_type) + + # Generate talking points + talking_points = self._generate_talking_points(opportunity, doc) + + # Generate social media content + social_media = self._generate_social_media(opportunity) + + # Generate policy brief + policy_brief = self._generate_policy_brief(opportunity, doc) + + materials = { + "opportunity_id": opportunity["document_id"], + "municipality": opportunity["municipality"], + "state": opportunity["state"], + "topic": opportunity["topic"], + "urgency": opportunity["urgency"], + "materials": { + "email": email, + "talking_points": talking_points, + "social_media": social_media, + "policy_brief": policy_brief + }, + "generated_at": datetime.utcnow().isoformat(), + "metadata": { + "source_url": opportunity["source_url"], + "meeting_date": opportunity["meeting_date"] + } + } + + return materials + + def _select_template(self, opportunity: Dict[str, Any]) -> str: + """Select appropriate email template based on situation.""" + urgency = opportunity.get("urgency") + stance = opportunity.get("stance") + + if urgency == "critical": + return "critical_vote" + elif stance in ["opposed", "strongly_opposed"]: + return "address_opposition" + elif stance in ["supportive", "strongly_supportive"]: + return "support_existing" + else: + return "introduce_topic" + + async def _generate_email( + self, + opportunity: Dict[str, Any], + doc: Dict[str, Any], + template_type: str + ) -> Dict[str, Any]: + """Generate personalized email content.""" + template = self.email_templates[template_type] + + # Format template variables + variables = { + "municipality": opportunity["municipality"], + "policy_topic": self._format_topic_name(opportunity["topic"]), + "meeting_date": opportunity["meeting_date"] + } + + subject = template["subject"].format(**variables) + opening = template["opening"].format(**variables) + + # Build email body + body_parts = [opening] + + # Add urgency if applicable + if template["urgency"]: + body_parts.append("\n\n" + template["urgency"].format(**variables)) + + # Add policy benefits + body_parts.append("\n\n**Key Benefits:**") + benefits = self.policy_benefits.get( + opportunity["topic"], + ["Improves community health outcomes"] + ) + for benefit in benefits[:3]: # Top 3 benefits + body_parts.append(f"• {benefit}") + + # Add call to action + body_parts.append(self._generate_call_to_action(opportunity)) + + # Add closing + body_parts.append( + "\n\nThank you for your time and consideration. " + "I would welcome the opportunity to discuss this further." + ) + body_parts.append("\n\nSincerely,") + body_parts.append("[Your Name]") + body_parts.append("[Your Organization]") + + email = { + "subject": subject, + "body": "\n".join(body_parts), + "template_type": template_type, + "personalization_variables": variables + } + + return email + + def _generate_call_to_action(self, opportunity: Dict[str, Any]) -> str: + """Generate appropriate call to action based on urgency.""" + urgency = opportunity.get("urgency") + stance = opportunity.get("stance") + + if urgency == "critical": + return ( + "\n\n**Action Needed:**\n" + f"Please vote in favor of this important measure at the upcoming meeting. " + f"Your constituents' oral health depends on this decision." + ) + elif stance in ["opposed", "strongly_opposed"]: + return ( + "\n\n**Requested Action:**\n" + "I respectfully request a meeting to discuss the evidence supporting this policy " + "and address any concerns you may have." + ) + else: + return ( + "\n\n**Requested Action:**\n" + "I encourage you to support this initiative and would be happy to provide " + "additional information or connect you with subject matter experts." + ) + + def _generate_talking_points( + self, + opportunity: Dict[str, Any], + doc: Dict[str, Any] + ) -> List[str]: + """Generate talking points for public testimony or meetings.""" + topic = opportunity["topic"] + + talking_points = [ + f"Introduction: Community member concerned about oral health in {opportunity['municipality']}" + ] + + # Add topic-specific points + benefits = self.policy_benefits.get(topic, []) + for i, benefit in enumerate(benefits[:5], 1): + talking_points.append(f"Point {i}: {benefit}") + + # Add local context + talking_points.append( + f"Local relevance: This policy addresses needs identified in " + f"recent community discussions" + ) + + # Add closing point + talking_points.append( + "Closing: Urge decision-makers to prioritize community oral health" + ) + + return talking_points + + def _generate_social_media( + self, + opportunity: Dict[str, Any] + ) -> Dict[str, str]: + """Generate social media content.""" + municipality = opportunity["municipality"] + topic = self._format_topic_name(opportunity["topic"]) + + twitter = ( + f"🦷 {municipality} is considering {topic}! " + f"This could improve oral health for thousands. " + f"Contact your local officials to show support. " + f"#OralHealth #PublicHealth" + ) + + facebook = ( + f"Important news for {municipality} residents!\n\n" + f"Our local government is discussing {topic}. " + f"This policy could significantly improve access to dental care " + f"for families in our community.\n\n" + f"Learn more and contact your representatives to voice your support: " + f"{opportunity.get('source_url', '')}" + ) + + return { + "twitter": twitter, + "facebook": facebook, + "instagram": twitter, # Similar to Twitter + "hashtags": ["OralHealth", "PublicHealth", municipality.replace(" ", "")] + } + + def _generate_policy_brief( + self, + opportunity: Dict[str, Any], + doc: Dict[str, Any] + ) -> Dict[str, Any]: + """Generate a concise policy brief.""" + topic = opportunity["topic"] + + brief = { + "title": f"Policy Brief: {self._format_topic_name(topic)} in {opportunity['municipality']}", + "summary": ( + f"This brief outlines the benefits and implementation considerations " + f"for {self._format_topic_name(topic)}." + ), + "background": ( + f"Current discussion in {opportunity['municipality']} presents " + f"an opportunity to improve community oral health." + ), + "key_benefits": self.policy_benefits.get(topic, []), + "recommendations": [ + "Approve the proposed policy", + "Allocate necessary funding", + "Establish implementation timeline", + "Monitor outcomes and adjust as needed" + ], + "evidence_sources": [ + "Centers for Disease Control and Prevention", + "American Dental Association", + "Community Preventive Services Task Force" + ] + } + + return brief + + def _format_topic_name(self, topic: str) -> str: + """Format topic identifier into readable name.""" + topic_names = { + "water_fluoridation": "community water fluoridation", + "school_dental_screening": "school-based dental screening", + "medicaid_dental": "Medicaid dental coverage expansion", + "dental_clinic_funding": "community dental clinic funding", + "community_dental_program": "community dental programs", + "children_dental_health": "children's dental health initiatives", + "dental_care_access": "dental care access improvements" + } + + return topic_names.get(topic, topic.replace("_", " ")) diff --git a/agents/base.py b/agents/base.py new file mode 100644 index 0000000000000000000000000000000000000000..f9315f18a77b6c9496905946a4da6a411d1d79dc --- /dev/null +++ b/agents/base.py @@ -0,0 +1,171 @@ +""" +Core agent base classes and protocols for the multi-agent system. +""" +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional, Union +from datetime import datetime +from enum import Enum +from pydantic import BaseModel, Field +from loguru import logger + + +class AgentRole(str, Enum): + """Enumeration of agent roles in the system.""" + SCRAPER = "scraper" + PARSER = "parser" + CLASSIFIER = "classifier" + SENTIMENT_ANALYZER = "sentiment_analyzer" + DEBATE_GRADER = "debate_grader" + ADVOCACY_WRITER = "advocacy_writer" + ORCHESTRATOR = "orchestrator" + + +class MessageType(str, Enum): + """Types of messages exchanged between agents.""" + DATA = "data" + COMMAND = "command" + QUERY = "query" + RESPONSE = "response" + ERROR = "error" + STATUS = "status" + + +class AgentMessage(BaseModel): + """Message structure for inter-agent communication.""" + message_id: str = Field(..., description="Unique message identifier") + sender: AgentRole = Field(..., description="Sending agent role") + recipient: AgentRole = Field(..., description="Receiving agent role") + message_type: MessageType = Field(..., description="Type of message") + timestamp: datetime = Field(default_factory=datetime.utcnow) + payload: Dict[str, Any] = Field(default_factory=dict, description="Message payload") + metadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata") + + class Config: + json_encoders = { + datetime: lambda v: v.isoformat() + } + + +class AgentStatus(str, Enum): + """Agent operational status.""" + IDLE = "idle" + PROCESSING = "processing" + WAITING = "waiting" + ERROR = "error" + COMPLETED = "completed" + + +class AgentState(BaseModel): + """Current state of an agent.""" + agent_id: str + role: AgentRole + status: AgentStatus = AgentStatus.IDLE + current_task: Optional[str] = None + tasks_completed: int = 0 + tasks_failed: int = 0 + last_activity: datetime = Field(default_factory=datetime.utcnow) + error_message: Optional[str] = None + + +class BaseAgent(ABC): + """ + Abstract base class for all agents in the system. + + Each agent must implement the process method to handle incoming messages + and perform its specific role in the pipeline. + """ + + def __init__(self, agent_id: str, role: AgentRole): + """ + Initialize the base agent. + + Args: + agent_id: Unique identifier for this agent instance + role: The role this agent plays in the system + """ + self.agent_id = agent_id + self.role = role + self.state = AgentState(agent_id=agent_id, role=role) + self.message_queue: List[AgentMessage] = [] + logger.info(f"Initialized {role.value} agent: {agent_id}") + + @abstractmethod + async def process(self, message: AgentMessage) -> Union[AgentMessage, List[AgentMessage]]: + """ + Process an incoming message and return response(s). + + Args: + message: The message to process + + Returns: + One or more response messages + """ + pass + + def update_status(self, status: AgentStatus, task: Optional[str] = None): + """Update the agent's current status.""" + self.state.status = status + self.state.current_task = task + self.state.last_activity = datetime.utcnow() + logger.debug(f"{self.role.value} agent {self.agent_id} status: {status.value}") + + def log_success(self): + """Log a successful task completion.""" + self.state.tasks_completed += 1 + self.update_status(AgentStatus.IDLE) + + def log_failure(self, error: str): + """Log a task failure.""" + self.state.tasks_failed += 1 + self.state.error_message = error + self.update_status(AgentStatus.ERROR) + logger.error(f"{self.role.value} agent {self.agent_id} error: {error}") + + async def send_message( + self, + recipient: AgentRole, + message_type: MessageType, + payload: Dict[str, Any], + metadata: Optional[Dict[str, Any]] = None + ) -> AgentMessage: + """ + Create and send a message to another agent. + + Args: + recipient: The receiving agent's role + message_type: Type of message to send + payload: Message content + metadata: Optional metadata + + Returns: + The created message + """ + import uuid + + message = AgentMessage( + message_id=str(uuid.uuid4()), + sender=self.role, + recipient=recipient, + message_type=message_type, + payload=payload, + metadata=metadata or {} + ) + + return message + + def get_state(self) -> AgentState: + """Get the current state of the agent.""" + return self.state + + +class AgentMetrics(BaseModel): + """Metrics for monitoring agent performance.""" + agent_id: str + role: AgentRole + total_messages_processed: int = 0 + total_processing_time_seconds: float = 0.0 + average_processing_time_seconds: float = 0.0 + success_rate: float = 0.0 + error_count: int = 0 + last_error: Optional[str] = None + uptime_seconds: float = 0.0 diff --git a/agents/classifier.py b/agents/classifier.py new file mode 100644 index 0000000000000000000000000000000000000000..d246e494751197b1c377003c41c5143e2eeb8c78 --- /dev/null +++ b/agents/classifier.py @@ -0,0 +1,295 @@ +""" +Classifier Agent for identifying oral health policy topics in meeting minutes. +""" +import asyncio +from typing import List, Dict, Any, Optional, Set +from datetime import datetime +from loguru import logger + +from agents.base import BaseAgent, AgentRole, AgentMessage, MessageType, AgentStatus +from config import settings + + +class PolicyTopic: + """Enumeration of oral health policy topics.""" + WATER_FLUORIDATION = "water_fluoridation" + SCHOOL_DENTAL_SCREENING = "school_dental_screening" + MEDICAID_DENTAL = "medicaid_dental" + DENTAL_CLINIC_FUNDING = "dental_clinic_funding" + COMMUNITY_DENTAL_PROGRAM = "community_dental_program" + CHILDREN_DENTAL_HEALTH = "children_dental_health" + DENTAL_CARE_ACCESS = "dental_care_access" + OTHER_ORAL_HEALTH = "other_oral_health" + NOT_RELEVANT = "not_relevant" + + +class ClassifierAgent(BaseAgent): + """ + Agent responsible for classifying documents by oral health policy topics. + + Uses a combination of: + - Keyword matching for high-precision identification + - LLM-based classification for nuanced topics + - Topic modeling for discovering new themes + """ + + def __init__(self, agent_id: str = "classifier-001"): + """Initialize the classifier agent.""" + super().__init__(agent_id, AgentRole.CLASSIFIER) + self._initialize_keywords() + self.llm_client = None # Will be initialized when needed + + def _initialize_keywords(self): + """Initialize keyword patterns for each topic.""" + self.topic_keywords = { + PolicyTopic.WATER_FLUORIDATION: [ + "fluoridation", "fluoride", "water fluoridation", + "fluoridated water", "fluoride level", "fluoride treatment", + "community water fluoridation" + ], + PolicyTopic.SCHOOL_DENTAL_SCREENING: [ + "school dental", "dental screening", "school screening", + "school health screening", "dental exam", "school nurse", + "student dental" + ], + PolicyTopic.MEDICAID_DENTAL: [ + "medicaid dental", "medicaid", "medicare dental", + "public assistance dental", "low-income dental", + "dental benefits", "dental coverage" + ], + PolicyTopic.DENTAL_CLINIC_FUNDING: [ + "dental clinic", "community dental clinic", + "dental center", "dental facility", "clinic funding", + "dental services funding" + ], + PolicyTopic.COMMUNITY_DENTAL_PROGRAM: [ + "community dental", "dental program", "oral health program", + "dental outreach", "mobile dental", "dental van" + ], + PolicyTopic.CHILDREN_DENTAL_HEALTH: [ + "children's dental", "pediatric dental", "child dental", + "kids dental", "youth dental", "infant oral health" + ], + PolicyTopic.DENTAL_CARE_ACCESS: [ + "dental access", "access to dental", "dental care", + "oral health access", "dental services", "dental disparities" + ] + } + + async def process(self, message: AgentMessage) -> List[AgentMessage]: + """ + Process classification commands. + + Args: + message: Message containing parsed documents to classify + + Returns: + List of messages with classification results + """ + self.update_status(AgentStatus.PROCESSING, "Classifying policy documents") + + try: + documents = message.payload.get("documents", []) + + # Classify documents in batches + batch_size = settings.classifier_batch_size + classified_documents = [] + + for i in range(0, len(documents), batch_size): + batch = documents[i:i + batch_size] + batch_results = await self._classify_batch(batch) + classified_documents.extend(batch_results) + + # Filter to only relevant documents + relevant_documents = [ + doc for doc in classified_documents + if doc["classification"]["primary_topic"] != PolicyTopic.NOT_RELEVANT + ] + + # Send classified documents to sentiment analyzer + response = await self.send_message( + AgentRole.SENTIMENT_ANALYZER, + MessageType.DATA, + { + "workflow_id": message.payload.get("workflow_id"), + "documents": relevant_documents, + "count": len(relevant_documents), + "filtered_count": len(documents) - len(relevant_documents) + } + ) + + self.log_success() + logger.info( + f"Classified {len(documents)} documents, " + f"{len(relevant_documents)} relevant to oral health policy" + ) + + return [response] + + except Exception as e: + self.log_failure(str(e)) + error_msg = await self.send_message( + AgentRole.ORCHESTRATOR, + MessageType.ERROR, + {"error": str(e), "agent": self.agent_id} + ) + return [error_msg] + + async def _classify_batch( + self, + documents: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """ + Classify a batch of documents. + + Args: + documents: Batch of documents to classify + + Returns: + Documents with classification results + """ + tasks = [self._classify_document(doc) for doc in documents] + results = await asyncio.gather(*tasks, return_exceptions=True) + + classified = [] + for doc, result in zip(documents, results): + if isinstance(result, Exception): + logger.error(f"Classification error for {doc['document_id']}: {result}") + doc["classification"] = { + "primary_topic": PolicyTopic.NOT_RELEVANT, + "error": str(result) + } + else: + doc["classification"] = result + + classified.append(doc) + + return classified + + async def _classify_document( + self, + doc: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Classify a single document. + + Args: + doc: Document to classify + + Returns: + Classification results + """ + text = self._get_searchable_text(doc) + text_lower = text.lower() + + # Keyword-based classification + topic_scores = {} + for topic, keywords in self.topic_keywords.items(): + score = sum(1 for keyword in keywords if keyword in text_lower) + if score > 0: + topic_scores[topic] = score + + # Determine primary topic + if topic_scores: + primary_topic = max(topic_scores, key=topic_scores.get) + confidence = "high" if topic_scores[primary_topic] >= 3 else "medium" + + # Get all topics mentioned + all_topics = list(topic_scores.keys()) + else: + primary_topic = PolicyTopic.NOT_RELEVANT + confidence = "high" + all_topics = [] + + # Extract relevant excerpts + excerpts = self._extract_relevant_excerpts(doc, primary_topic) + + classification = { + "primary_topic": primary_topic, + "all_topics": all_topics, + "topic_scores": topic_scores, + "confidence": confidence, + "relevant_excerpts": excerpts, + "classified_at": datetime.utcnow().isoformat() + } + + return classification + + def _get_searchable_text(self, doc: Dict[str, Any]) -> str: + """Extract searchable text from document.""" + parts = [ + doc.get("raw_title", ""), + doc.get("full_text", "") + ] + + # Add agenda items + for item in doc.get("agenda_items", []): + parts.append(item.get("description", "")) + + # Add discussion sections + for section in doc.get("discussion_sections", []): + parts.append(section.get("text", "")) + + return " ".join(parts) + + def _extract_relevant_excerpts( + self, + doc: Dict[str, Any], + topic: str + ) -> List[Dict[str, str]]: + """Extract text excerpts relevant to the topic.""" + if topic == PolicyTopic.NOT_RELEVANT: + return [] + + keywords = self.topic_keywords.get(topic, []) + excerpts = [] + + # Check discussion sections + for section in doc.get("discussion_sections", []): + text = section.get("text", "") + text_lower = text.lower() + + # Check if any keywords present + if any(keyword in text_lower for keyword in keywords): + excerpts.append({ + "source": "discussion", + "text": text[:500], # First 500 chars + "section_id": section.get("section_id") + }) + + # Check agenda items + for item in doc.get("agenda_items", []): + desc = item.get("description", "") + desc_lower = desc.lower() + + if any(keyword in desc_lower for keyword in keywords): + excerpts.append({ + "source": "agenda", + "text": desc, + "item_number": item.get("number") + }) + + return excerpts[:5] # Return top 5 excerpts + + async def _llm_classify( + self, + text: str, + preliminary_topics: List[str] + ) -> Dict[str, Any]: + """ + Use LLM for nuanced classification when keywords are ambiguous. + + Args: + text: Text to classify + preliminary_topics: Topics identified by keyword matching + + Returns: + LLM classification results + """ + # This would use OpenAI API or similar + # Placeholder for now + return { + "llm_topic": preliminary_topics[0] if preliminary_topics else PolicyTopic.NOT_RELEVANT, + "llm_confidence": 0.8, + "llm_reasoning": "Based on keyword analysis" + } diff --git a/agents/debate_grader.py b/agents/debate_grader.py new file mode 100644 index 0000000000000000000000000000000000000000..1a138bb106cf61945d74ed733b03277c5f3cc177 --- /dev/null +++ b/agents/debate_grader.py @@ -0,0 +1,424 @@ +""" +Debate Grader Agent for evaluating government decisions using debate framework. + +Evaluates decisions across three dimensions: +- Harms: The problem/crisis identified +- Solvency: How the proposed solution addresses the problem +- Topicality: Whether the solution fits within jurisdiction's authority +""" +import asyncio +from typing import List, Dict, Any, Optional +from datetime import datetime +from loguru import logger + +from agents.base import BaseAgent, AgentRole, AgentMessage, MessageType, AgentStatus + + +class DebateDimension: + """Enumeration of debate evaluation dimensions.""" + HARMS = "harms" # The problem + SOLVENCY = "solvency" # The fix + TOPICALITY = "topicality" # The scope + + +class DebateScore: + """Score levels for each debate dimension.""" + EXCELLENT = "excellent" # 4-5/5 + GOOD = "good" # 3-4/5 + FAIR = "fair" # 2-3/5 + WEAK = "weak" # 1-2/5 + MISSING = "missing" # 0-1/5 + + +class DebateGraderAgent(BaseAgent): + """ + Agent responsible for grading government decisions using debate framework. + + Translates debate concepts for laypeople: + - Harms → "The Problem": Why is this a crisis in our community? + - Solvency → "The Fix": How does this solution actually work? + - Topicality → "The Scope": Does the government have authority to do this? + """ + + def __init__(self, agent_id: str = "debate-grader-001"): + """Initialize the debate grader agent.""" + super().__init__(agent_id, AgentRole.SENTIMENT_ANALYZER) + self._initialize_criteria() + + def _initialize_criteria(self): + """Initialize evaluation criteria for each dimension.""" + + # Harms evaluation keywords + self.harms_indicators = { + "problem_identification": [ + "crisis", "emergency", "critical", "urgent need", + "widespread problem", "affecting", "impacting", + "suffering", "lack of", "shortage", "gap in services" + ], + "data_evidence": [ + "statistics", "data shows", "research indicates", + "study found", "percent", "%", "number of people", + "cases", "instances", "reports" + ], + "affected_population": [ + "children", "families", "residents", "citizens", + "low-income", "vulnerable", "underserved", + "community members", "students", "seniors" + ] + } + + # Solvency evaluation keywords + self.solvency_indicators = { + "solution_clarity": [ + "will", "would", "proposes to", "plans to", + "implement", "establish", "create", "provide", + "offer", "deliver", "fund", "allocate" + ], + "mechanism": [ + "through", "by", "using", "via", "process", + "program", "initiative", "partnership", + "collaboration", "service", "system" + ], + "evidence_of_effectiveness": [ + "proven", "successful in", "works in", + "demonstrated", "track record", "best practice", + "evidence-based", "research-backed" + ], + "implementation_plan": [ + "timeline", "budget", "staff", "resources", + "phase", "rollout", "launch", "start date", + "completion", "milestones" + ] + } + + # Topicality evaluation keywords + self.topicality_indicators = { + "legal_authority": [ + "authority", "jurisdiction", "mandate", + "chartered to", "empowered to", "authorized", + "within our purview", "responsibility" + ], + "precedent": [ + "previously", "historically", "past practice", + "similar actions", "other cities", "state law", + "federal law", "code", "ordinance" + ], + "scope_appropriateness": [ + "city council", "county commission", "board", + "department", "local government", "municipal", + "within scope", "appropriate for" + ] + } + + async def process(self, message: AgentMessage) -> List[AgentMessage]: + """ + Process debate grading commands. + + Args: + message: Message containing decisions/documents to grade + + Returns: + List of messages with debate grades + """ + self.update_status(AgentStatus.PROCESSING, "Grading decisions with debate framework") + + try: + documents = message.payload.get("documents", []) + + graded_documents = [] + + for doc in documents: + grade = await self._grade_document(doc) + doc["debate_grade"] = grade + graded_documents.append(doc) + + # Calculate aggregate insights + insights = self._generate_insights(graded_documents) + + # Send results + response = await self.send_message( + recipient=AgentRole.ORCHESTRATOR, + message_type=MessageType.RESPONSE, + payload={ + "documents": graded_documents, + "insights": insights, + "graded_count": len(graded_documents) + } + ) + + self.update_status(AgentStatus.COMPLETED, f"Graded {len(graded_documents)} decisions") + return [response] + + except Exception as e: + logger.error(f"Debate grading failed: {e}") + self.update_status(AgentStatus.ERROR, str(e)) + raise + + async def _grade_document(self, document: Dict[str, Any]) -> Dict[str, Any]: + """ + Grade a single document across all debate dimensions. + + Args: + document: Document to grade + + Returns: + Dictionary with grades for each dimension + """ + text = document.get("content", "").lower() + title = document.get("title", "").lower() + combined_text = f"{title} {text}" + + # Grade each dimension + harms_score = self._grade_harms(combined_text) + solvency_score = self._grade_solvency(combined_text) + topicality_score = self._grade_topicality(combined_text) + + # Calculate overall score + overall_score = self._calculate_overall_score( + harms_score, solvency_score, topicality_score + ) + + return { + "dimensions": { + "harms": { + "score": harms_score["score"], + "grade": harms_score["grade"], + "explanation": harms_score["explanation"], + "layperson_label": "The Problem", + "layperson_question": "Why is this a crisis in our community?" + }, + "solvency": { + "score": solvency_score["score"], + "grade": solvency_score["grade"], + "explanation": solvency_score["explanation"], + "layperson_label": "The Fix", + "layperson_question": "How does this solution actually work?" + }, + "topicality": { + "score": topicality_score["score"], + "grade": topicality_score["grade"], + "explanation": topicality_score["explanation"], + "layperson_label": "The Scope", + "layperson_question": "Does the government have authority to do this?" + } + }, + "overall": { + "score": overall_score, + "grade": self._score_to_grade(overall_score), + "summary": self._generate_summary(harms_score, solvency_score, topicality_score) + }, + "timestamp": datetime.utcnow().isoformat() + } + + def _grade_harms(self, text: str) -> Dict[str, Any]: + """Grade the 'harms' dimension - problem identification.""" + score = 0 + max_score = 5 + details = [] + + # Check for problem identification (0-2 points) + problem_count = sum(1 for keyword in self.harms_indicators["problem_identification"] if keyword in text) + if problem_count >= 3: + score += 2 + details.append("Strong problem identification") + elif problem_count >= 1: + score += 1 + details.append("Problem mentioned but not detailed") + + # Check for data/evidence (0-2 points) + data_count = sum(1 for keyword in self.harms_indicators["data_evidence"] if keyword in text) + if data_count >= 2: + score += 2 + details.append("Data-driven evidence provided") + elif data_count >= 1: + score += 1 + details.append("Some evidence mentioned") + + # Check for affected population (0-1 point) + population_count = sum(1 for keyword in self.harms_indicators["affected_population"] if keyword in text) + if population_count >= 1: + score += 1 + details.append("Affected population identified") + + return { + "score": score, + "max_score": max_score, + "grade": self._score_to_grade(score / max_score * 5), + "explanation": "; ".join(details) if details else "No clear problem statement" + } + + def _grade_solvency(self, text: str) -> Dict[str, Any]: + """Grade the 'solvency' dimension - solution effectiveness.""" + score = 0 + max_score = 5 + details = [] + + # Check for solution clarity (0-1 point) + solution_count = sum(1 for keyword in self.solvency_indicators["solution_clarity"] if keyword in text) + if solution_count >= 2: + score += 1 + details.append("Clear solution proposed") + + # Check for mechanism (0-2 points) + mechanism_count = sum(1 for keyword in self.solvency_indicators["mechanism"] if keyword in text) + if mechanism_count >= 3: + score += 2 + details.append("Implementation mechanism described") + elif mechanism_count >= 1: + score += 1 + details.append("Basic approach outlined") + + # Check for evidence of effectiveness (0-1 point) + evidence_count = sum(1 for keyword in self.solvency_indicators["evidence_of_effectiveness"] if keyword in text) + if evidence_count >= 1: + score += 1 + details.append("Evidence-based approach") + + # Check for implementation plan (0-1 point) + plan_count = sum(1 for keyword in self.solvency_indicators["implementation_plan"] if keyword in text) + if plan_count >= 2: + score += 1 + details.append("Implementation plan included") + + return { + "score": score, + "max_score": max_score, + "grade": self._score_to_grade(score / max_score * 5), + "explanation": "; ".join(details) if details else "No clear solution mechanism" + } + + def _grade_topicality(self, text: str) -> Dict[str, Any]: + """Grade the 'topicality' dimension - scope appropriateness.""" + score = 0 + max_score = 5 + details = [] + + # Check for legal authority (0-2 points) + authority_count = sum(1 for keyword in self.topicality_indicators["legal_authority"] if keyword in text) + if authority_count >= 2: + score += 2 + details.append("Legal authority cited") + elif authority_count >= 1: + score += 1 + details.append("Authority mentioned") + + # Check for precedent (0-2 points) + precedent_count = sum(1 for keyword in self.topicality_indicators["precedent"] if keyword in text) + if precedent_count >= 2: + score += 2 + details.append("Precedent established") + elif precedent_count >= 1: + score += 1 + details.append("Some precedent referenced") + + # Check for scope appropriateness (0-1 point) + scope_count = sum(1 for keyword in self.topicality_indicators["scope_appropriateness"] if keyword in text) + if scope_count >= 1: + score += 1 + details.append("Within appropriate scope") + + return { + "score": score, + "max_score": max_score, + "grade": self._score_to_grade(score / max_score * 5), + "explanation": "; ".join(details) if details else "Unclear jurisdictional authority" + } + + def _score_to_grade(self, normalized_score: float) -> str: + """Convert numerical score to grade.""" + if normalized_score >= 4.0: + return DebateScore.EXCELLENT + elif normalized_score >= 3.0: + return DebateScore.GOOD + elif normalized_score >= 2.0: + return DebateScore.FAIR + elif normalized_score >= 1.0: + return DebateScore.WEAK + else: + return DebateScore.MISSING + + def _calculate_overall_score( + self, + harms: Dict[str, Any], + solvency: Dict[str, Any], + topicality: Dict[str, Any] + ) -> float: + """Calculate weighted overall score.""" + # Weight: Harms 40%, Solvency 40%, Topicality 20% + harms_normalized = (harms["score"] / harms["max_score"]) * 5 + solvency_normalized = (solvency["score"] / solvency["max_score"]) * 5 + topicality_normalized = (topicality["score"] / topicality["max_score"]) * 5 + + overall = (harms_normalized * 0.4) + (solvency_normalized * 0.4) + (topicality_normalized * 0.2) + return round(overall, 2) + + def _generate_summary( + self, + harms: Dict[str, Any], + solvency: Dict[str, Any], + topicality: Dict[str, Any] + ) -> str: + """Generate human-readable summary.""" + parts = [] + + if harms["grade"] in [DebateScore.EXCELLENT, DebateScore.GOOD]: + parts.append("Strong problem identification") + else: + parts.append("Weak problem statement") + + if solvency["grade"] in [DebateScore.EXCELLENT, DebateScore.GOOD]: + parts.append("clear solution") + else: + parts.append("unclear fix") + + if topicality["grade"] in [DebateScore.EXCELLENT, DebateScore.GOOD]: + parts.append("within authority") + else: + parts.append("questionable scope") + + return "; ".join(parts).capitalize() + + def _generate_insights(self, documents: List[Dict[str, Any]]) -> Dict[str, Any]: + """Generate aggregate insights from all graded documents.""" + if not documents: + return {} + + total = len(documents) + dimension_scores = { + "harms": [], + "solvency": [], + "topicality": [] + } + overall_scores = [] + + for doc in documents: + grade = doc.get("debate_grade", {}) + dimensions = grade.get("dimensions", {}) + + for dim in ["harms", "solvency", "topicality"]: + if dim in dimensions: + dimension_scores[dim].append(dimensions[dim]["score"]) + + if "overall" in grade: + overall_scores.append(grade["overall"]["score"]) + + # Calculate averages + insights = { + "total_documents": total, + "average_scores": { + "harms": round(sum(dimension_scores["harms"]) / len(dimension_scores["harms"]), 2) if dimension_scores["harms"] else 0, + "solvency": round(sum(dimension_scores["solvency"]) / len(dimension_scores["solvency"]), 2) if dimension_scores["solvency"] else 0, + "topicality": round(sum(dimension_scores["topicality"]) / len(dimension_scores["topicality"]), 2) if dimension_scores["topicality"] else 0, + "overall": round(sum(overall_scores) / len(overall_scores), 2) if overall_scores else 0 + }, + "strongest_dimension": max( + dimension_scores.items(), + key=lambda x: sum(x[1]) / len(x[1]) if x[1] else 0 + )[0] if any(dimension_scores.values()) else None, + "weakest_dimension": min( + dimension_scores.items(), + key=lambda x: sum(x[1]) / len(x[1]) if x[1] else 0 + )[0] if any(dimension_scores.values()) else None + } + + return insights diff --git a/agents/mlflow_base.py b/agents/mlflow_base.py new file mode 100644 index 0000000000000000000000000000000000000000..619489ade33b2039ef88e5e20f61eb67cedba8c2 --- /dev/null +++ b/agents/mlflow_base.py @@ -0,0 +1,307 @@ +""" +MLflow-based agent foundation for Databricks Agent Bricks. + +Provides: +- MLflow Pyfunc model wrappers for agents +- Unity Catalog integration +- Automatic tracing and observability +- Model serving compatibility +""" +from typing import Any, Dict, List, Optional, Union +from abc import ABC, abstractmethod +import mlflow +from mlflow.pyfunc import PythonModel +from mlflow.models import infer_signature +from mlflow.tracking import MlflowClient +import pandas as pd +from datetime import datetime +from loguru import logger + +from agents.base import AgentRole, AgentMessage, AgentStatus +from config import settings + + +class MLflowAgentBase(PythonModel, ABC): + """ + Base class for agents that can be deployed via MLflow Model Serving. + + Integrates with: + - Unity Catalog for governance + - MLflow Tracking for experimentation + - Databricks Model Serving for deployment + - Mosaic AI Agent Framework for evaluation + """ + + def __init__(self, agent_id: str, role: AgentRole): + """ + Initialize MLflow agent. + + Args: + agent_id: Unique identifier for this agent + role: Agent role in the pipeline + """ + super().__init__() + self.agent_id = agent_id + self.role = role + self.status = AgentStatus.IDLE + self.client = MlflowClient() + + def load_context(self, context): + """ + Load agent context from MLflow (called during model loading). + + Args: + context: MLflow context with model artifacts + """ + logger.info(f"Loading {self.role.value} agent from MLflow context") + # Load any model artifacts, configs, etc. + pass + + @abstractmethod + def _process_request(self, request: Dict[str, Any]) -> Dict[str, Any]: + """ + Process a single agent request. + + Args: + request: Input request dictionary + + Returns: + Response dictionary + """ + pass + + def predict( + self, + context, + model_input: Union[pd.DataFrame, Dict[str, Any], List[Dict[str, Any]]] + ) -> Union[pd.DataFrame, List[Dict[str, Any]]]: + """ + MLflow Pyfunc predict interface. + + This is the main entry point when the agent is deployed as a Model Serving endpoint. + + Args: + context: MLflow context + model_input: Input data (DataFrame, dict, or list of dicts) + + Returns: + Predictions in same format as input + """ + # Enable MLflow tracing for observability + with mlflow.start_span(name=f"{self.role.value}_agent") as span: + span.set_attribute("agent_id", self.agent_id) + span.set_attribute("agent_role", self.role.value) + + try: + # Convert input to standard format + if isinstance(model_input, pd.DataFrame): + requests = model_input.to_dict('records') + return_df = True + elif isinstance(model_input, dict): + requests = [model_input] + return_df = False + else: + requests = model_input + return_df = False + + # Process each request with tracing + results = [] + for idx, request in enumerate(requests): + with mlflow.start_span(name=f"process_request_{idx}") as req_span: + req_span.set_attribute("request_id", request.get("request_id", f"req_{idx}")) + + try: + result = self._process_request(request) + result["status"] = "success" + result["agent_id"] = self.agent_id + result["timestamp"] = datetime.utcnow().isoformat() + results.append(result) + + req_span.set_attribute("status", "success") + + except Exception as e: + error_result = { + "status": "error", + "error": str(e), + "agent_id": self.agent_id, + "timestamp": datetime.utcnow().isoformat() + } + results.append(error_result) + + req_span.set_attribute("status", "error") + req_span.set_attribute("error", str(e)) + logger.error(f"Error processing request {idx}: {e}") + + # Return in requested format + if return_df: + return pd.DataFrame(results) + elif len(results) == 1 and not isinstance(model_input, list): + return results[0] + else: + return results + + except Exception as e: + span.set_attribute("status", "error") + span.set_attribute("error", str(e)) + logger.error(f"Error in {self.role.value} agent: {e}") + raise + + def log_to_mlflow( + self, + model_name: str, + artifact_path: str = "agent", + registered_model_name: Optional[str] = None, + **kwargs + ): + """ + Log this agent to MLflow. + + Args: + model_name: Name for the MLflow run + artifact_path: Path within the run to store the model + registered_model_name: Unity Catalog model name (e.g., "main.agents.scraper") + **kwargs: Additional MLflow logging parameters + """ + with mlflow.start_run(run_name=model_name) as run: + # Log agent metadata + mlflow.log_param("agent_id", self.agent_id) + mlflow.log_param("agent_role", self.role.value) + mlflow.log_param("framework", "databricks-agent-bricks") + + # Create example input/output for signature + example_input = self._get_example_input() + example_output = self.predict(None, example_input) + signature = infer_signature(example_input, example_output) + + # Log the model + mlflow.pyfunc.log_model( + artifact_path=artifact_path, + python_model=self, + signature=signature, + registered_model_name=registered_model_name, + **kwargs + ) + + logger.info(f"Logged {self.role.value} agent to MLflow run {run.info.run_id}") + + if registered_model_name: + logger.info(f"Registered model as {registered_model_name}") + + return run.info.run_id + + @abstractmethod + def _get_example_input(self) -> Union[pd.DataFrame, Dict[str, Any]]: + """ + Get example input for MLflow signature inference. + + Returns: + Example input data + """ + pass + + def deploy_to_model_serving( + self, + model_name: str, + endpoint_name: str, + workload_size: str = "Small", + scale_to_zero: bool = True + ) -> str: + """ + Deploy this agent to Databricks Model Serving. + + Args: + model_name: Registered model name in Unity Catalog + endpoint_name: Name for the serving endpoint + workload_size: Endpoint size (Small, Medium, Large) + scale_to_zero: Whether to scale to zero when idle + + Returns: + Endpoint URL + """ + from databricks.sdk import WorkspaceClient + from databricks.sdk.service.serving import ServedEntityInput, EndpointCoreConfigInput + + w = WorkspaceClient( + host=settings.databricks_host, + token=settings.databricks_token + ) + + # Get latest model version + latest_version = self.client.get_latest_versions(model_name, stages=["None"])[0].version + + # Create or update endpoint + endpoint_config = EndpointCoreConfigInput( + name=endpoint_name, + served_entities=[ + ServedEntityInput( + entity_name=model_name, + entity_version=latest_version, + workload_size=workload_size, + scale_to_zero_enabled=scale_to_zero + ) + ] + ) + + try: + endpoint = w.serving_endpoints.create_and_wait( + name=endpoint_name, + config=endpoint_config + ) + logger.info(f"Created endpoint: {endpoint_name}") + except Exception as e: + if "already exists" in str(e): + endpoint = w.serving_endpoints.update_config_and_wait( + name=endpoint_name, + served_entities=endpoint_config.served_entities + ) + logger.info(f"Updated endpoint: {endpoint_name}") + else: + raise + + endpoint_url = f"{settings.databricks_host}/serving-endpoints/{endpoint_name}/invocations" + return endpoint_url + + +class MLflowChainAgent(MLflowAgentBase): + """ + Agent that uses LangChain with MLflow tracing. + + Provides integration with: + - LangChain agents and chains + - Automatic prompt logging + - LLM call tracing + - Tool usage tracking + """ + + def __init__(self, agent_id: str, role: AgentRole): + """Initialize LangChain-based agent.""" + super().__init__(agent_id, role) + self.chain = None + + def _setup_langchain_tracing(self): + """Enable MLflow tracing for LangChain.""" + mlflow.langchain.autolog() + + @abstractmethod + def _build_chain(self): + """ + Build the LangChain chain for this agent. + + Returns: + LangChain chain or agent + """ + pass + + def _process_request(self, request: Dict[str, Any]) -> Dict[str, Any]: + """Process request through LangChain.""" + if self.chain is None: + self.chain = self._build_chain() + + with mlflow.start_span(name="langchain_invoke") as span: + result = self.chain.invoke(request) + + # Log relevant metrics + if hasattr(result, "llm_output"): + span.set_attribute("tokens_used", result.llm_output.get("token_usage", {}).get("total_tokens", 0)) + + return result diff --git a/agents/mlflow_classifier.py b/agents/mlflow_classifier.py new file mode 100644 index 0000000000000000000000000000000000000000..272df9a9124984d7d8f77e69c45da9704715dfa6 --- /dev/null +++ b/agents/mlflow_classifier.py @@ -0,0 +1,308 @@ +""" +Policy Classifier Agent - MLflow version for Databricks Agent Bricks. + +Classifies meeting documents for oral health policy topics using: +- Keyword matching and NLP +- LLM-based classification for ambiguous cases +- Unity Catalog for model governance +- MLflow tracing for observability +""" +from typing import Any, Dict, List, Optional +import pandas as pd +from enum import Enum +import mlflow +from langchain.chat_models import ChatOpenAI +from langchain.prompts import ChatPromptTemplate +from langchain.output_parsers import PydanticOutputParser +from pydantic import BaseModel, Field + +from agents.mlflow_base import MLflowChainAgent +from agents.base import AgentRole +from config import settings + + +class PolicyTopic(str, Enum): + """Oral health policy topics to classify.""" + WATER_FLUORIDATION = "water_fluoridation" + SCHOOL_DENTAL_SCREENING = "school_dental_screening" + MEDICAID_DENTAL = "medicaid_dental_expansion" + LOW_INCOME_DENTAL_FUNDING = "low_income_dental_funding" + DENTAL_INSURANCE_MANDATE = "dental_insurance_mandate" + DENTAL_WORKFORCE = "dental_workforce_development" + COMMUNITY_HEALTH_CENTER = "community_health_center_dental" + OTHER_ORAL_HEALTH = "other_oral_health" + NOT_ORAL_HEALTH = "not_oral_health_related" + + +class ClassificationResult(BaseModel): + """Structured classification output.""" + primary_topic: PolicyTopic = Field(description="Primary policy topic") + secondary_topics: List[PolicyTopic] = Field(default_factory=list, description="Additional relevant topics") + confidence: float = Field(ge=0.0, le=1.0, description="Classification confidence") + relevant_excerpts: List[str] = Field(default_factory=list, description="Key text excerpts") + reasoning: str = Field(description="Brief explanation of classification") + + +class PolicyClassifierAgent(MLflowChainAgent): + """ + Agent that classifies documents for oral health policy topics. + + Can be deployed to Databricks Model Serving and integrated with + Unity Catalog for governance. + """ + + # Keywords for each topic (fallback classification) + TOPIC_KEYWORDS = { + PolicyTopic.WATER_FLUORIDATION: { + "fluoride", "fluoridation", "water supply", "dental fluorosis", + "community water", "fluoride levels", "fluoridated water" + }, + PolicyTopic.SCHOOL_DENTAL_SCREENING: { + "school dental", "screening program", "student dental", "school health", + "dental exam", "school nurse", "oral health screening" + }, + PolicyTopic.MEDICAID_DENTAL: { + "medicaid dental", "adult dental coverage", "medicaid expansion", + "dental benefits", "state medicaid", "covered dental services" + }, + PolicyTopic.LOW_INCOME_DENTAL_FUNDING: { + "low-income dental", "dental safety net", "free dental clinic", + "dental voucher", "sliding scale dental", "charity care" + }, + PolicyTopic.DENTAL_INSURANCE_MANDATE: { + "dental insurance", "insurance mandate", "coverage requirement", + "pediatric dental", "essential health benefits" + }, + PolicyTopic.DENTAL_WORKFORCE: { + "dental hygienist", "dental therapist", "scope of practice", + "workforce shortage", "dental provider", "loan repayment" + }, + PolicyTopic.COMMUNITY_HEALTH_CENTER: { + "community health center", "FQHC", "health center dental", + "federally qualified", "CHC dental" + } + } + + def __init__(self, agent_id: str = "classifier-mlflow-001"): + """Initialize classifier agent.""" + super().__init__(agent_id, AgentRole.CLASSIFIER) + self._setup_langchain_tracing() + + def _build_chain(self): + """Build LangChain classification chain.""" + # Initialize LLM (will use AI Gateway if configured) + llm = ChatOpenAI( + model=settings.classifier_model, + temperature=0.1, + openai_api_key=settings.openai_api_key + ) + + # Create output parser + parser = PydanticOutputParser(pydantic_object=ClassificationResult) + + # Create prompt template + prompt = ChatPromptTemplate.from_messages([ + ("system", """You are an expert policy analyst specializing in oral health policy. + +Classify the following government meeting document for oral health policy topics. + +Available topics: +- water_fluoridation: Fluoride in public water systems +- school_dental_screening: School-based dental programs +- medicaid_dental_expansion: Medicaid dental coverage +- low_income_dental_funding: Funding for low-income dental care +- dental_insurance_mandate: Insurance coverage requirements +- dental_workforce_development: Training, scope of practice +- community_health_center_dental: CHC/FQHC dental services +- other_oral_health: Other oral health topics +- not_oral_health_related: Not related to oral health + +{format_instructions}"""), + ("user", """Document Title: {title} + +Document Content: +{content} + +Classify this document and provide relevant excerpts.""") + ]) + + # Build chain + chain = prompt | llm | parser + return chain + + def _process_request(self, request: Dict[str, Any]) -> Dict[str, Any]: + """ + Classify a document for oral health policy topics. + + Args: + request: Dict with 'document_id', 'title', 'content' + + Returns: + Classification results with topics and confidence + """ + document_id = request.get("document_id") + title = request.get("title", "") + content = request.get("content", "") + + with mlflow.start_span(name="classify_document") as span: + span.set_attribute("document_id", document_id) + + # Try keyword-based classification first (faster, cheaper) + keyword_result = self._classify_by_keywords(title + " " + content) + + if keyword_result["confidence"] >= 0.8: + # High confidence from keywords, no LLM needed + span.set_attribute("classification_method", "keywords") + result = keyword_result + else: + # Use LLM for ambiguous cases + span.set_attribute("classification_method", "llm") + + try: + llm_result = super()._process_request({ + "title": title, + "content": content[:4000], # Limit context length + "format_instructions": self._get_format_instructions() + }) + + result = { + "document_id": document_id, + "primary_topic": llm_result.primary_topic.value, + "secondary_topics": [t.value for t in llm_result.secondary_topics], + "confidence": llm_result.confidence, + "relevant_excerpts": llm_result.relevant_excerpts, + "reasoning": llm_result.reasoning, + "method": "llm" + } + + except Exception as e: + # Fallback to keywords if LLM fails + span.set_attribute("llm_error", str(e)) + result = keyword_result + result["method"] = "keywords_fallback" + + return result + + def _classify_by_keywords(self, text: str) -> Dict[str, Any]: + """ + Fast keyword-based classification. + + Args: + text: Document text + + Returns: + Classification result + """ + text_lower = text.lower() + scores = {} + + # Score each topic + for topic, keywords in self.TOPIC_KEYWORDS.items(): + score = sum(1 for keyword in keywords if keyword in text_lower) + if score > 0: + scores[topic] = score + + if not scores: + return { + "primary_topic": PolicyTopic.NOT_ORAL_HEALTH.value, + "secondary_topics": [], + "confidence": 0.9, + "relevant_excerpts": [], + "reasoning": "No oral health keywords found", + "method": "keywords" + } + + # Get top topics + sorted_topics = sorted(scores.items(), key=lambda x: x[1], reverse=True) + primary_topic = sorted_topics[0][0] + secondary_topics = [t for t, s in sorted_topics[1:3] if s >= 2] + + # Calculate confidence based on score gap + max_score = sorted_topics[0][1] + confidence = min(0.95, 0.5 + (max_score / 10)) + + # Extract relevant excerpts + excerpts = self._extract_excerpts(text, primary_topic) + + return { + "primary_topic": primary_topic.value, + "secondary_topics": [t.value for t in secondary_topics], + "confidence": confidence, + "relevant_excerpts": excerpts, + "reasoning": f"Found {max_score} keyword matches for {primary_topic.value}", + "method": "keywords" + } + + def _extract_excerpts(self, text: str, topic: PolicyTopic, max_excerpts: int = 3) -> List[str]: + """Extract relevant text excerpts for a topic.""" + keywords = self.TOPIC_KEYWORDS.get(topic, set()) + sentences = text.split('. ') + + relevant = [] + for sentence in sentences: + sentence_lower = sentence.lower() + if any(keyword in sentence_lower for keyword in keywords): + relevant.append(sentence.strip()) + if len(relevant) >= max_excerpts: + break + + return relevant + + def _get_format_instructions(self) -> str: + """Get format instructions for LLM output parsing.""" + parser = PydanticOutputParser(pydantic_object=ClassificationResult) + return parser.get_format_instructions() + + def _get_example_input(self) -> Dict[str, Any]: + """Get example input for MLflow signature.""" + return { + "document_id": "doc_12345", + "title": "City Council Meeting - Water Quality Discussion", + "content": "The council discussed adding fluoride to the municipal water supply..." + } + + +def register_classifier_to_unity_catalog(): + """ + Register the classifier agent to Unity Catalog. + + Usage: + python -c "from agents.mlflow_classifier import register_classifier_to_unity_catalog; register_classifier_to_unity_catalog()" + """ + agent = PolicyClassifierAgent() + + # Log and register to Unity Catalog + run_id = agent.log_to_mlflow( + model_name="policy_classifier_agent", + registered_model_name=f"{settings.catalog_name}.{settings.schema_name}.policy_classifier", + pip_requirements=[ + "mlflow>=2.10.0", + "langchain>=0.1.0", + "openai>=1.6.0", + "pydantic>=2.5.0" + ] + ) + + print(f"✅ Registered policy classifier agent to Unity Catalog") + print(f" Model: {settings.catalog_name}.{settings.schema_name}.policy_classifier") + print(f" Run ID: {run_id}") + + return run_id + + +if __name__ == "__main__": + # Test the agent locally + agent = PolicyClassifierAgent() + + test_input = { + "document_id": "test_001", + "title": "School Board Meeting Minutes", + "content": """ + The school board discussed implementing a new dental screening program + for elementary students. The program would provide free dental exams + and referrals to local dentists for students in need. + """ + } + + result = agent.predict(None, test_input) + print("Classification Result:", result) diff --git a/agents/orchestrator.py b/agents/orchestrator.py new file mode 100644 index 0000000000000000000000000000000000000000..5e43655cb81f585012436f80d7e851c04edea4da --- /dev/null +++ b/agents/orchestrator.py @@ -0,0 +1,269 @@ +""" +Multi-Agent Orchestrator for coordinating the policy analysis pipeline. +""" +import asyncio +from typing import Dict, List, Optional, Any +from datetime import datetime +from collections import defaultdict +from loguru import logger + +from agents.base import ( + BaseAgent, + AgentRole, + AgentMessage, + MessageType, + AgentStatus +) + + +class WorkflowStage(str): + """Workflow stage identifiers.""" + SCRAPE = "scrape" + PARSE = "parse" + CLASSIFY = "classify" + ANALYZE = "analyze" + GENERATE = "generate" + + +class OrchestratorAgent(BaseAgent): + """ + Orchestrator agent that coordinates the multi-agent workflow. + + The orchestrator manages the flow of data through the pipeline: + 1. Scraper Agent -> Collects meeting minutes + 2. Parser Agent -> Extracts structured data + 3. Classifier Agent -> Identifies oral health topics + 4. Sentiment Agent -> Analyzes policy positions + 5. Advocacy Agent -> Generates outreach materials + """ + + def __init__(self, agent_id: str = "orchestrator-001"): + """Initialize the orchestrator agent.""" + super().__init__(agent_id, AgentRole.ORCHESTRATOR) + self.agents: Dict[AgentRole, BaseAgent] = {} + self.workflow_state: Dict[str, Any] = defaultdict(dict) + self.active_workflows: Dict[str, Dict[str, Any]] = {} + + def register_agent(self, agent: BaseAgent): + """ + Register an agent with the orchestrator. + + Args: + agent: The agent to register + """ + self.agents[agent.role] = agent + logger.info(f"Registered {agent.role.value} agent: {agent.agent_id}") + + async def process(self, message: AgentMessage) -> List[AgentMessage]: + """ + Process orchestrator commands and route messages. + + Args: + message: The incoming message + + Returns: + List of response messages + """ + self.update_status(AgentStatus.PROCESSING, "Processing orchestrator command") + + try: + if message.message_type == MessageType.COMMAND: + command = message.payload.get("command") + + if command == "start_workflow": + return await self._start_workflow(message.payload) + elif command == "check_status": + return await self._check_workflow_status(message.payload) + elif command == "stop_workflow": + return await self._stop_workflow(message.payload) + + self.log_success() + return [] + + except Exception as e: + self.log_failure(str(e)) + return [await self.send_message( + message.sender, + MessageType.ERROR, + {"error": str(e)} + )] + + async def _start_workflow(self, payload: Dict[str, Any]) -> List[AgentMessage]: + """ + Start a new policy analysis workflow. + + Args: + payload: Workflow configuration + + Returns: + List of messages to initiate the workflow + """ + import uuid + + workflow_id = str(uuid.uuid4()) + workflow_config = payload.get("config", {}) + + # Initialize workflow state + self.active_workflows[workflow_id] = { + "id": workflow_id, + "started_at": datetime.utcnow(), + "stage": WorkflowStage.SCRAPE, + "config": workflow_config, + "status": "running" + } + + logger.info(f"Starting workflow {workflow_id}") + + # Create initial scraping task + scraper_message = await self.send_message( + AgentRole.SCRAPER, + MessageType.COMMAND, + { + "workflow_id": workflow_id, + "command": "scrape", + "targets": workflow_config.get("scrape_targets", []), + "date_range": workflow_config.get("date_range", {}) + } + ) + + return [scraper_message] + + async def _check_workflow_status(self, payload: Dict[str, Any]) -> List[AgentMessage]: + """ + Check the status of active workflows. + + Args: + payload: Status check request + + Returns: + List containing status response + """ + workflow_id = payload.get("workflow_id") + + if workflow_id and workflow_id in self.active_workflows: + workflow = self.active_workflows[workflow_id] + status_payload = { + "workflow_id": workflow_id, + "status": workflow.get("status"), + "stage": workflow.get("stage"), + "started_at": workflow.get("started_at").isoformat() + } + else: + # Return status of all workflows + status_payload = { + "active_workflows": len(self.active_workflows), + "workflows": [ + { + "id": wf_id, + "status": wf["status"], + "stage": wf["stage"] + } + for wf_id, wf in self.active_workflows.items() + ] + } + + response = await self.send_message( + AgentRole.ORCHESTRATOR, + MessageType.RESPONSE, + status_payload + ) + + return [response] + + async def _stop_workflow(self, payload: Dict[str, Any]) -> List[AgentMessage]: + """ + Stop a running workflow. + + Args: + payload: Stop request with workflow_id + + Returns: + List containing confirmation message + """ + workflow_id = payload.get("workflow_id") + + if workflow_id in self.active_workflows: + self.active_workflows[workflow_id]["status"] = "stopped" + logger.info(f"Stopped workflow {workflow_id}") + + response = await self.send_message( + AgentRole.ORCHESTRATOR, + MessageType.RESPONSE, + {"workflow_id": workflow_id, "status": "stopped"} + ) + else: + response = await self.send_message( + AgentRole.ORCHESTRATOR, + MessageType.ERROR, + {"error": f"Workflow {workflow_id} not found"} + ) + + return [response] + + async def route_message(self, message: AgentMessage) -> Optional[AgentMessage]: + """ + Route a message to the appropriate agent. + + Args: + message: The message to route + + Returns: + Response from the target agent + """ + target_agent = self.agents.get(message.recipient) + + if not target_agent: + logger.error(f"No agent found for role: {message.recipient}") + return None + + try: + response = await target_agent.process(message) + return response + except Exception as e: + logger.error(f"Error routing message to {message.recipient}: {e}") + return None + + async def execute_pipeline( + self, + scrape_targets: List[Dict[str, Any]], + date_range: Optional[Dict[str, str]] = None + ) -> Dict[str, Any]: + """ + Execute the complete policy analysis pipeline. + + Args: + scrape_targets: List of government entities to scrape + date_range: Optional date range for historical data + + Returns: + Dictionary containing pipeline results + """ + workflow_config = { + "scrape_targets": scrape_targets, + "date_range": date_range or {} + } + + # Start the workflow + start_message = await self.send_message( + AgentRole.ORCHESTRATOR, + MessageType.COMMAND, + { + "command": "start_workflow", + "config": workflow_config + } + ) + + results = await self.process(start_message) + + return { + "success": True, + "workflow_initiated": True, + "messages": [msg.dict() for msg in results] + } + + def get_all_agent_states(self) -> Dict[str, Any]: + """Get the current state of all registered agents.""" + return { + role.value: agent.get_state().dict() + for role, agent in self.agents.items() + } diff --git a/agents/parser.py b/agents/parser.py new file mode 100644 index 0000000000000000000000000000000000000000..d68fe1dc9c48bfe4f6f672da38a5ecfad5ba3d65 --- /dev/null +++ b/agents/parser.py @@ -0,0 +1,199 @@ +""" +Parser Agent for extracting and structuring data from raw meeting minutes. +""" +import re +from typing import List, Dict, Any, Optional +from datetime import datetime +from loguru import logger + +from agents.base import BaseAgent, AgentRole, AgentMessage, MessageType, AgentStatus + + +class ParserAgent(BaseAgent): + """ + Agent responsible for parsing raw meeting documents into structured data. + + Extracts: + - Meeting metadata (date, type, location) + - Attendees and participants + - Agenda items + - Discussion topics + - Votes and decisions + - Action items + """ + + def __init__(self, agent_id: str = "parser-001"): + """Initialize the parser agent.""" + super().__init__(agent_id, AgentRole.PARSER) + self._compile_patterns() + + def _compile_patterns(self): + """Compile regex patterns for parsing.""" + self.patterns = { + "date": re.compile( + r"(?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},?\s+\d{4}", + re.IGNORECASE + ), + "time": re.compile(r"\d{1,2}:\d{2}\s*(?:AM|PM|am|pm)?"), + "attendees": re.compile(r"(?:Present|Attending|Members Present):(.+?)(?:\n\n|\Z)", re.DOTALL | re.IGNORECASE), + "motion": re.compile(r"(?:MOTION|Motion|MOVED)(.+?)(?:CARRIED|PASSED|FAILED|$)", re.DOTALL | re.IGNORECASE), + "vote": re.compile(r"(?:Vote|VOTE):\s*(.+)", re.IGNORECASE), + "agenda_item": re.compile(r"(?:Item|ITEM)\s+#?(\d+|[A-Z])[\.:]\s*(.+?)(?=\n(?:Item|ITEM)|$)", re.DOTALL | re.IGNORECASE) + } + + async def process(self, message: AgentMessage) -> List[AgentMessage]: + """ + Process parsing commands. + + Args: + message: Message containing raw documents to parse + + Returns: + List of messages with parsed data + """ + self.update_status(AgentStatus.PROCESSING, "Parsing meeting documents") + + try: + documents = message.payload.get("documents", []) + + parsed_documents = [] + + for doc in documents: + parsed = await self._parse_document(doc) + if parsed: + parsed_documents.append(parsed) + + # Send parsed documents to classifier + response = await self.send_message( + AgentRole.CLASSIFIER, + MessageType.DATA, + { + "workflow_id": message.payload.get("workflow_id"), + "documents": parsed_documents, + "count": len(parsed_documents) + } + ) + + self.log_success() + logger.info(f"Parsed {len(parsed_documents)} documents") + + return [response] + + except Exception as e: + self.log_failure(str(e)) + error_msg = await self.send_message( + AgentRole.ORCHESTRATOR, + MessageType.ERROR, + {"error": str(e), "agent": self.agent_id} + ) + return [error_msg] + + async def _parse_document(self, doc: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """ + Parse a single meeting document. + + Args: + doc: Raw document data + + Returns: + Parsed document with structured fields + """ + try: + content = doc.get("content", "") + + parsed = { + "document_id": doc["document_id"], + "source_url": doc["source_url"], + "municipality": doc["municipality"], + "state": doc["state"], + "raw_title": doc["title"], + "parsed_at": datetime.utcnow().isoformat(), + + # Extracted structured data + "meeting_date": self._extract_date(content, doc.get("meeting_date")), + "meeting_time": self._extract_time(content), + "meeting_type": doc.get("meeting_type", "Unknown"), + "attendees": self._extract_attendees(content), + "agenda_items": self._extract_agenda_items(content), + "motions": self._extract_motions(content), + "votes": self._extract_votes(content), + "discussion_sections": self._extract_discussion_sections(content), + + # Full text for semantic search + "full_text": content, + + # Metadata + "metadata": doc.get("metadata", {}) + } + + return parsed + + except Exception as e: + logger.error(f"Error parsing document {doc.get('document_id')}: {e}") + return None + + def _extract_date(self, content: str, fallback_date: Optional[str]) -> str: + """Extract meeting date from content.""" + match = self.patterns["date"].search(content) + if match: + return match.group(0) + return fallback_date or datetime.utcnow().isoformat() + + def _extract_time(self, content: str) -> Optional[str]: + """Extract meeting time from content.""" + match = self.patterns["time"].search(content) + return match.group(0) if match else None + + def _extract_attendees(self, content: str) -> List[str]: + """Extract list of meeting attendees.""" + match = self.patterns["attendees"].search(content) + if match: + attendees_text = match.group(1) + # Split by comma or newline + attendees = re.split(r'[,\n]', attendees_text) + return [a.strip() for a in attendees if a.strip()] + return [] + + def _extract_agenda_items(self, content: str) -> List[Dict[str, str]]: + """Extract agenda items from content.""" + items = [] + for match in self.patterns["agenda_item"].finditer(content): + items.append({ + "number": match.group(1).strip(), + "description": match.group(2).strip() + }) + return items + + def _extract_motions(self, content: str) -> List[Dict[str, str]]: + """Extract motions from content.""" + motions = [] + for match in self.patterns["motion"].finditer(content): + motions.append({ + "text": match.group(1).strip(), + "full_match": match.group(0).strip() + }) + return motions + + def _extract_votes(self, content: str) -> List[Dict[str, str]]: + """Extract voting records from content.""" + votes = [] + for match in self.patterns["vote"].finditer(content): + votes.append({ + "result": match.group(1).strip() + }) + return votes + + def _extract_discussion_sections(self, content: str) -> List[Dict[str, str]]: + """Extract discussion sections from content.""" + # Split content into paragraphs + paragraphs = [p.strip() for p in content.split("\n\n") if p.strip()] + + sections = [] + for i, para in enumerate(paragraphs): + if len(para) > 100: # Only substantial paragraphs + sections.append({ + "section_id": i, + "text": para + }) + + return sections diff --git a/agents/scraper.py b/agents/scraper.py new file mode 100644 index 0000000000000000000000000000000000000000..68831e1cc53d1a5ee01f8f144dea9563b6521e5c --- /dev/null +++ b/agents/scraper.py @@ -0,0 +1,2113 @@ +""" +Scraper Agent for collecting government meeting minutes from various sources. +""" +import asyncio +import hashlib +import io +import json +import re +from typing import List, Dict, Any, Optional +from datetime import datetime, timedelta +from urllib.parse import urljoin, urlparse +import httpx +from bs4 import BeautifulSoup +from loguru import logger + +try: + from PyPDF2 import PdfReader +except Exception: + PdfReader = None + +try: + import pdfplumber +except Exception: + pdfplumber = None + +try: + import pytesseract + from pytesseract import TesseractNotFoundError +except Exception: + pytesseract = None + TesseractNotFoundError = Exception + +try: + from PIL import Image +except Exception: + Image = None + +try: + from youtube_transcript_api import YouTubeTranscriptApi +except Exception: + YouTubeTranscriptApi = None + +from agents.base import BaseAgent, AgentRole, AgentMessage, MessageType, AgentStatus + + +class MeetingDocument(dict): + """Structured representation of a meeting document.""" + + def __init__( + self, + document_id: str, + source_url: str, + municipality: str, + state: str, + meeting_date: datetime, + meeting_type: str, + title: str, + content: str, + metadata: Optional[Dict[str, Any]] = None + ): + super().__init__( + document_id=document_id, + source_url=source_url, + municipality=municipality, + state=state, + meeting_date=meeting_date.isoformat() if isinstance(meeting_date, datetime) else meeting_date, + meeting_type=meeting_type, + title=title, + content=content, + scraped_at=datetime.utcnow().isoformat(), + metadata=metadata or {} + ) + + +class ScraperAgent(BaseAgent): + """ + Agent responsible for scraping government meeting minutes from various sources. + + Supports multiple platforms: + - Legistar (widely used by city councils) + - Granicus (meeting management platform) + - Generic municipal websites + - PDF documents + """ + + def __init__(self, agent_id: str = "scraper-001"): + """Initialize the scraper agent.""" + super().__init__(agent_id, AgentRole.SCRAPER) + self.http_client: Optional[httpx.AsyncClient] = None + self.scraped_urls: set = set() + self.document_extensions = (".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx") + self.meeting_keywords = ("minutes", "agenda", "meeting", "council", "commission", "board") + self.document_route_keywords = ( + "getagendafile", + "getminutesfile", + "download", + "agendafile", + "minutesfile", + ) + self.ocr_max_pages = 10 + self._ocr_missing_tesseract_warned = False + self.social_source_limit = 8 + + # Policy and meeting-focused keywords for social media filtering + self.policy_meeting_keywords = ( + # Meetings + "council meeting", "city council", "town council", "board meeting", + "commission meeting", "public meeting", "town hall", "session", + "special meeting", "regular meeting", "work session", "workshop", + # Documents + "agenda", "minutes", "ordinance", "resolution", "public hearing", + "hearing", "vote", "voting", "motion", "legislation", + # Policy topics + "policy", "budget", "zoning", "planning", "development", + "public comment", "community meeting", "civic", "government", + # Video/meeting specific + "live stream", "livestream", "recorded meeting", "meeting video", + "council session", "board session", "official meeting" + ) + + async def __aenter__(self): + """Async context manager entry.""" + self.http_client = httpx.AsyncClient( + timeout=30.0, + follow_redirects=True, + headers={ + "User-Agent": "OpenNavigator/1.0 (+https://github.com/getcommunityone/open-navigator)" + } + ) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + if self.http_client: + await self.http_client.aclose() + + async def process(self, message: AgentMessage) -> List[AgentMessage]: + """ + Process scraping commands. + + Args: + message: Command message with scraping targets + + Returns: + List of messages containing scraped data + """ + self.update_status(AgentStatus.PROCESSING, "Scraping government meeting minutes") + + try: + command = message.payload.get("command") + + if command == "scrape": + targets = message.payload.get("targets", []) + date_range = message.payload.get("date_range", {}) + + # Initialize HTTP client if not already done + if not self.http_client: + async with self: + documents = await self._scrape_targets(targets, date_range) + else: + documents = await self._scrape_targets(targets, date_range) + + # Send scraped documents to parser + response = await self.send_message( + AgentRole.PARSER, + MessageType.DATA, + { + "workflow_id": message.payload.get("workflow_id"), + "documents": documents, + "count": len(documents) + } + ) + + self.log_success() + logger.info(f"Scraped {len(documents)} documents") + + return [response] + + return [] + + except Exception as e: + self.log_failure(str(e)) + error_msg = await self.send_message( + AgentRole.ORCHESTRATOR, + MessageType.ERROR, + {"error": str(e), "agent": self.agent_id} + ) + return [error_msg] + + async def _scrape_targets( + self, + targets: List[Dict[str, Any]], + date_range: Dict[str, str] + ) -> List[Dict[str, Any]]: + """ + Scrape multiple targets concurrently. + + Args: + targets: List of scraping targets + date_range: Date range for filtering meetings + + Returns: + List of scraped documents + """ + tasks = [] + + for target in targets: + platform = target.get("platform", "generic") + url = target.get("url", "") + + if platform == "legistar": + tasks.append(self._scrape_legistar(target, date_range)) + elif platform == "granicus": + tasks.append(self._scrape_granicus(target, date_range)) + elif platform == "suiteonemedia" or "suiteonemedia" in url.lower(): + tasks.append(self._scrape_suiteonemedia(target, date_range)) + elif platform == "eboard" or "eboardsolutions.com" in url.lower() or "simbli.eboardsolutions" in url.lower(): + tasks.append(self._scrape_eboard(target, date_range)) + elif platform == "youtube": + tasks.append(self._scrape_youtube_source(target)) + elif platform == "facebook": + tasks.append(self._scrape_facebook_source(target)) + else: + tasks.append(self._scrape_generic(target, date_range)) + + # Execute all scraping tasks concurrently + results = await asyncio.gather(*tasks, return_exceptions=True) + + # Flatten results and filter out errors + documents = [] + for result in results: + if isinstance(result, Exception): + logger.error(f"Scraping error: {result}") + elif isinstance(result, list): + documents.extend(result) + + return documents + + async def scrape_social_sources( + self, + municipality: str, + state: str, + seed_url: str, + max_sources: int = 8 + ) -> List[Dict[str, Any]]: + """Discover and scrape YouTube/Facebook sources for a jurisdiction.""" + social_documents: List[Dict[str, Any]] = [] + + homepage_url = await self._resolve_homepage_url(municipality, state, seed_url) + if not homepage_url: + logger.warning(f"Could not resolve homepage URL for social scraping: {municipality}, {state}") + return social_documents + + logger.info(f"Discovering social sources from homepage: {homepage_url}") + social_urls = await self._discover_social_urls(homepage_url, municipality, state) + + youtube_urls = list(dict.fromkeys(social_urls.get("youtube", [])))[:max_sources] + facebook_urls = list(dict.fromkeys(social_urls.get("facebook", [])))[:max_sources] + + logger.info( + f"Social discovery for {municipality}: " + f"{len(youtube_urls)} YouTube, {len(facebook_urls)} Facebook" + ) + + tasks = [] + for y_url in youtube_urls: + tasks.append(self._scrape_youtube_source({ + "url": y_url, + "municipality": municipality, + "state": state, + })) + for f_url in facebook_urls: + tasks.append(self._scrape_facebook_source({ + "url": f_url, + "municipality": municipality, + "state": state, + })) + + if not tasks: + return social_documents + + results = await asyncio.gather(*tasks, return_exceptions=True) + for result in results: + if isinstance(result, Exception): + logger.warning(f"Social scraping error: {result}") + continue + if isinstance(result, list): + social_documents.extend(result) + + return social_documents + + async def _resolve_homepage_url(self, municipality: str, state: str, seed_url: str) -> str: + """Resolve an official website homepage used for social discovery.""" + if seed_url and "suiteonemedia" not in seed_url.lower(): + parsed = urlparse(seed_url) + return f"{parsed.scheme}://{parsed.netloc}" if parsed.scheme and parsed.netloc else seed_url + + city = (municipality or "").lower().replace(" ", "").replace("'", "") + st = (state or "").lower() + candidates = [ + f"https://www.{city}{st}.gov", + f"https://{city}{st}.gov", + f"https://www.cityof{city}.com", + f"https://www.{city}.gov", + f"https://www.{city}.com", + f"https://{city}.com", + ] + + for candidate in candidates: + try: + resp = await self.http_client.get(candidate, timeout=8) + if resp.status_code < 400: + parsed = urlparse(str(resp.url)) + return f"{parsed.scheme}://{parsed.netloc}" + except Exception: + continue + + return "" + + async def _discover_social_urls(self, homepage_url: str, municipality: str, state: str) -> Dict[str, List[str]]: + """Discover social media URLs from homepage and YouTube pattern matching.""" + discovered = {"youtube": [], "facebook": []} + + try: + from discovery.social_media_discovery import SocialMediaDiscovery + + async with SocialMediaDiscovery() as discovery: + social = await discovery.discover_from_website( + homepage_url=homepage_url, + jurisdiction_name=municipality, + state=state, + ) + discovered["youtube"] = social.get("youtube", []) + discovered["facebook"] = social.get("facebook", []) + except Exception as err: + logger.debug(f"SocialMediaDiscovery unavailable/failed: {err}") + + # Augment YouTube discovery using handle pattern search for better recall. + try: + from discovery.youtube_channel_discovery import YouTubeChannelDiscovery + + async with YouTubeChannelDiscovery() as ydisc: + channels = await ydisc.discover_channels( + city_name=municipality, + state_code=state, + homepage_url=homepage_url, + ) + for channel in channels: + url = channel.get("channel_url") + if url: + discovered["youtube"].append(url) + except Exception as err: + logger.debug(f"YouTubeChannelDiscovery unavailable/failed: {err}") + + discovered["youtube"] = list(dict.fromkeys(discovered["youtube"])) + discovered["facebook"] = list(dict.fromkeys(discovered["facebook"])) + return discovered + + def _is_policy_meeting_content(self, text: str) -> bool: + """Check if text content is related to policy or meetings.""" + if not text: + return False + text_lower = text.lower() + return any(keyword in text_lower for keyword in self.policy_meeting_keywords) + + def _extract_youtube_video_metadata(self, html: str, video_id: str) -> Dict[str, str]: + """Extract title and description from YouTube video page HTML.""" + metadata = {"title": "", "description": ""} + + try: + # Extract title from various possible patterns + title_match = re.search(r'"title":"([^"]+)"', html) + if title_match: + metadata["title"] = title_match.group(1) + else: + # Fallback to meta tags + title_match = re.search(r'([^<]+)', html) + if title_match: + metadata["title"] = title_match.group(1).replace(" - YouTube", "") + + # Extract description + desc_match = re.search(r'"description":"([^"]+)"', html) + if desc_match: + # Unescape and limit description length + metadata["description"] = desc_match.group(1)[:500] + + except Exception as err: + logger.debug(f"Error extracting metadata for video {video_id}: {err}") + + return metadata + + async def _scrape_youtube_source(self, target: Dict[str, Any]) -> List[Dict[str, Any]]: + """Scrape recent YouTube videos and transcripts from a channel URL, focusing on policy and meeting content.""" + url = target.get("url", "") + municipality = target.get("municipality", "") + state = target.get("state", "") + + documents: List[Dict[str, Any]] = [] + if not url: + return documents + + videos_url = url.rstrip("/") + "/videos" + try: + resp = await self.http_client.get(videos_url) + if resp.status_code >= 400: + resp = await self.http_client.get(url) + text = resp.text + except Exception as err: + logger.debug(f"Could not fetch YouTube page {url}: {err}") + return documents + + video_ids = [] + for vid in re.findall(r'watch\?v=([A-Za-z0-9_-]{11})', text): + if vid not in video_ids: + video_ids.append(vid) + + # Process more videos initially to filter for relevant content + video_ids = video_ids[: self.social_source_limit * 3] + + policy_videos = [] + + for vid in video_ids: + video_url = f"https://www.youtube.com/watch?v={vid}" + + # Fetch video page to extract metadata + try: + video_resp = await self.http_client.get(video_url) + video_metadata = self._extract_youtube_video_metadata(video_resp.text, vid) + + # Filter: Only keep videos with policy/meeting-related titles or descriptions + if not self._is_policy_meeting_content(video_metadata["title"]) and \ + not self._is_policy_meeting_content(video_metadata["description"]): + logger.debug(f"Skipping non-policy video: {video_metadata['title']}") + continue + + logger.info(f"Found policy/meeting video: {video_metadata['title']}") + + except Exception as err: + logger.debug(f"Could not fetch metadata for video {vid}: {err}") + video_metadata = {"title": f"Video {vid}", "description": ""} + + # Fetch transcript + transcript_text = self._fetch_youtube_transcript(vid) + if not transcript_text: + logger.debug(f"No transcript available for video {vid}") + continue + + # Double-check transcript content for policy/meeting keywords + if not self._is_policy_meeting_content(transcript_text): + logger.debug(f"Transcript doesn't contain policy/meeting content: {vid}") + continue + + doc_id = hashlib.md5(f"youtube-{municipality}-{vid}".encode()).hexdigest() + policy_videos.append(MeetingDocument( + document_id=doc_id, + source_url=video_url, + municipality=municipality, + state=state, + meeting_date=datetime.utcnow().isoformat(), + meeting_type="YouTube Video - Policy/Meeting", + title=video_metadata["title"] or f"YouTube Video {vid}", + content=transcript_text, + metadata={ + "platform": "youtube", + "channel_url": url, + "video_id": vid, + "has_transcript": True, + "description": video_metadata["description"], + "filtered_for_policy": True, + } + )) + + # Limit to configured number of policy videos + if len(policy_videos) >= self.social_source_limit: + break + + # Rate limiting + await asyncio.sleep(0.5) + + logger.info(f"Found {len(policy_videos)} policy/meeting videos from {url}") + return policy_videos + + async def _scrape_facebook_source(self, target: Dict[str, Any]) -> List[Dict[str, Any]]: + """Scrape publicly accessible Facebook page/post text snippets, focusing on policy and meeting content.""" + url = target.get("url", "") + municipality = target.get("municipality", "") + state = target.get("state", "") + + documents: List[Dict[str, Any]] = [] + if not url: + return documents + + normalized = url.replace("www.facebook.com", "m.facebook.com") + try: + resp = await self.http_client.get(normalized) + if resp.status_code >= 400: + return documents + soup = BeautifulSoup(resp.content, "html.parser") + except Exception as err: + logger.debug(f"Could not fetch Facebook page {url}: {err}") + return documents + + # Try to extract links to individual post pages first. + post_links: List[str] = [] + for a in soup.find_all("a", href=True): + href = a["href"] + if "/posts/" in href or "/videos/" in href: + full = urljoin(normalized, href) + if full not in post_links: + post_links.append(full) + post_links = post_links[: self.social_source_limit * 2] # Check more posts to filter + + # If direct post links are unavailable, use page text as fallback content. + if not post_links: + page_text = soup.get_text(" ", strip=True) + # Filter: Only use page content if it contains policy/meeting keywords + if len(page_text) > 200 and self._is_policy_meeting_content(page_text): + doc_id = hashlib.md5(f"facebook-page-{municipality}-{url}".encode()).hexdigest() + documents.append(MeetingDocument( + document_id=doc_id, + source_url=url, + municipality=municipality, + state=state, + meeting_date=datetime.utcnow().isoformat(), + meeting_type="Facebook Page - Policy/Meeting", + title="Facebook Page Content (Policy-Related)", + content=page_text[:8000], + metadata={ + "platform": "facebook", + "content_source": "page_fallback", + "filtered_for_policy": True, + } + )) + else: + logger.debug(f"Facebook page content doesn't contain policy/meeting keywords: {url}") + return documents + + policy_posts = [] + for post_url in post_links: + try: + p_resp = await self.http_client.get(post_url) + if p_resp.status_code >= 400: + continue + p_soup = BeautifulSoup(p_resp.content, "html.parser") + post_text = p_soup.get_text(" ", strip=True) + if len(post_text) < 120: + continue + + # Filter: Only keep posts that mention policy/meeting keywords + if not self._is_policy_meeting_content(post_text): + logger.debug(f"Skipping non-policy Facebook post: {post_url[:80]}...") + continue + + logger.info(f"Found policy/meeting Facebook post: {post_url[:80]}...") + + doc_id = hashlib.md5(f"facebook-post-{municipality}-{post_url}".encode()).hexdigest() + policy_posts.append(MeetingDocument( + document_id=doc_id, + source_url=post_url, + municipality=municipality, + state=state, + meeting_date=datetime.utcnow().isoformat(), + meeting_type="Facebook Post - Policy/Meeting", + title="Facebook Post (Policy-Related)", + content=post_text[:8000], + metadata={ + "platform": "facebook", + "content_source": "post", + "filtered_for_policy": True, + } + )) + + # Limit to configured number of policy posts + if len(policy_posts) >= self.social_source_limit: + break + + # Rate limiting + await asyncio.sleep(0.5) + + except Exception as err: + logger.debug(f"Could not parse Facebook post {post_url}: {err}") + + logger.info(f"Found {len(policy_posts)} policy/meeting Facebook posts from {url}") + return policy_posts + + def _fetch_youtube_transcript(self, video_id: str) -> str: + """Return concatenated YouTube transcript text when available.""" + if YouTubeTranscriptApi is None: + return "" + + try: + transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=["en"]) + return " ".join(chunk.get("text", "") for chunk in transcript if chunk.get("text")).strip() + except Exception: + return "" + + async def _scrape_legistar( + self, + target: Dict[str, Any], + date_range: Dict[str, str] + ) -> List[Dict[str, Any]]: + """ + Scrape meeting minutes from Legistar platform using the official API. + + Legistar provides a REST API at https://webapi.legistar.com/v1/{city}/ + This is much more reliable than HTML scraping. + + Args: + target: Target configuration with 'url' and 'municipality' + date_range: Date range for filtering (optional) + + Returns: + List of meeting documents + """ + base_url = target["url"] + municipality = target["municipality"] + state = target["state"] + + # Extract city slug from URL (e.g., "chicago" from "chicago.legistar.com") + parsed = urlparse(base_url) + hostname = parsed.hostname or "" + city_slug = hostname.split('.')[0] if '.' in hostname else municipality.lower().replace(' ', '') + + # Use the official Legistar API + api_base = f"https://webapi.legistar.com/v1/{city_slug}" + + documents = [] + + try: + # Build OData filter for date range + params = { + "$top": 100, # Limit to 100 most recent meetings + "$orderby": "EventDate desc" + } + + if date_range and "start" in date_range: + params["$filter"] = f"EventDate ge datetime'{date_range['start']}'" + + # Get events (meetings) + events_url = f"{api_base}/events" + logger.info(f"Fetching Legistar events from {events_url}") + + response = await self.http_client.get(events_url, params=params) + response.raise_for_status() + events = response.json() + + logger.info(f"Found {len(events)} events for {municipality}") + + # Process each event + for event in events[:50]: # Limit to 50 meetings + event_id = event.get("EventId") + event_guid = event.get("EventGuid") + + if not event_id: + continue + + # Get agenda items for this event + try: + items_url = f"{api_base}/events/{event_id}/EventItems" + items_response = await self.http_client.get(items_url, timeout=10) + + if items_response.status_code == 200: + items = items_response.json() + + # Create document from event and items + doc = self._create_legistar_document( + event, + items, + municipality, + state, + base_url + ) + + if doc: + documents.append(doc) + + # Rate limiting - be respectful + await asyncio.sleep(0.3) + + except Exception as item_error: + logger.warning(f"Error fetching items for event {event_id}: {item_error}") + continue + + except Exception as e: + logger.error(f"Error scraping Legistar API for {municipality}: {e}") + + return documents + + def _create_legistar_document( + self, + event: Dict[str, Any], + items: List[Dict[str, Any]], + municipality: str, + state: str, + base_url: str + ) -> Optional[Dict[str, Any]]: + """ + Create a meeting document from Legistar API data. + + Args: + event: Event data from API + items: Agenda items from API + municipality: Municipality name + state: State code + base_url: Base URL for constructing links + + Returns: + Meeting document dict or None + """ + try: + event_id = event.get("EventId") + event_date = event.get("EventDate", "") + event_body = event.get("EventBodyName", "Unknown Body") + + # Combine agenda items into content + content_parts = [f"Meeting: {event_body}", f"Date: {event_date}", "\n=== AGENDA ===\n"] + + for item in items: + agenda_num = item.get("EventItemAgendaNumber", "") + title = item.get("EventItemTitle", "") + matter_file = item.get("EventItemMatterFile", "") + + if title: + item_text = f"\n{agenda_num}. {title}" + if matter_file: + item_text += f" (File: {matter_file})" + content_parts.append(item_text) + + content = "\n".join(content_parts) + + # Generate document ID + doc_id = hashlib.md5( + f"{municipality}-{state}-{event_id}".encode() + ).hexdigest() + + # Create meeting detail URL + parsed = urlparse(base_url) + hostname = parsed.hostname or base_url + meeting_url = f"https://{hostname}/MeetingDetail.aspx?ID={event_id}" + + return MeetingDocument( + document_id=doc_id, + source_url=meeting_url, + municipality=municipality, + state=state, + meeting_date=event_date, + meeting_type=event_body, + title=f"{event_body} - {event_date}", + content=content, + metadata={ + "event_id": event_id, + "event_guid": event.get("EventGuid"), + "event_time": event.get("EventTime"), + "event_location": event.get("EventLocation"), + "video_status": event.get("EventVideoStatus"), + "agenda_status": event.get("EventAgendaStatusName"), + "minutes_status": event.get("EventMinutesStatusName"), + "item_count": len(items), + "platform": "legistar_api" + } + ) + + except Exception as e: + logger.error(f"Error creating document from Legistar data: {e}") + return None + + async def _scrape_granicus( + self, + target: Dict[str, Any], + date_range: Dict[str, str] + ) -> List[Dict[str, Any]]: + """ + Scrape meeting minutes from Granicus platform. + + Args: + target: Target configuration + date_range: Date range for filtering + + Returns: + List of meeting documents + """ + base_url = target["url"] + municipality = target["municipality"] + state = target["state"] + + documents = [] + + try: + # Granicus often has an API endpoint + api_url = urljoin(base_url, "api/v1/meetings") + + response = await self.http_client.get(api_url) + response.raise_for_status() + + meetings_data = response.json() + + for meeting in meetings_data.get("meetings", [])[:50]: + meeting_id = meeting.get("id") + meeting_url = urljoin(base_url, f"meeting/{meeting_id}") + + if meeting_url in self.scraped_urls: + continue + + doc = await self._scrape_meeting_page( + meeting_url, + municipality, + state, + meeting_data=meeting + ) + + if doc: + documents.append(doc) + self.scraped_urls.add(meeting_url) + + await asyncio.sleep(0.5) + + except Exception as e: + logger.error(f"Error scraping Granicus {base_url}: {e}") + + return documents + + async def _scrape_suiteonemedia( + self, + target: Dict[str, Any], + date_range: Dict[str, str] + ) -> List[Dict[str, Any]]: + """ + Scrape meeting events from a SuiteOne Media portal. + + Strategy: + 1. Fetch the portal homepage — it renders ALL current-year events as + /event/?id=XXXX anchor links. + 2. Parse each in the eventTable to get: event ID, title, date, + agenda/minutes PDF links, and whether a media recording exists. + 3. For events with media (or missing title/date), fetch the event page + to extract the S3 MP4 video recording URL from jwplayer setup. + 4. Download PDFs for text extraction. + 5. Extend backwards through historical event IDs when max_events > homepage count. + + Parameters via target dict: + max_events - maximum events to process (default 500, 0 = unlimited) + start_year - only include events on/after this year (0 = all) + fetch_videos - whether to fetch event pages for S3 video URLs (default True) + """ + url = target["url"] + municipality = target.get("municipality", "") + state = target.get("state", "") + max_events: int = int(target.get("max_events", 500)) + start_year: int = int(target.get("start_year", 0)) + fetch_videos: bool = bool(target.get("fetch_videos", True)) + + parsed = urlparse(url) + base_url = f"{parsed.scheme}://{parsed.netloc}" + + documents: List[Dict[str, Any]] = [] + + try: + # ---- Step 1: fetch homepage and parse event table rows ---- + logger.info(f"Fetching SuiteOne homepage: {base_url}/Web/Home.aspx") + home_resp = await self.http_client.get(f"{base_url}/Web/Home.aspx") + home_resp.raise_for_status() + home_soup = BeautifulSoup(home_resp.content, "html.parser") + + # Each in an eventTable contains: event link, agenda/minutes PDF links, date text + events: list[dict] = [] + seen_event_ids: set[int] = set() + + for table in home_soup.find_all("table", class_=re.compile("eventTable", re.I)): + for tr in table.find_all("tr"): + row_links = [(a["href"], a.get_text(" ", strip=True)) for a in tr.find_all("a", href=True)] + row_text = tr.get_text(" ", strip=True) + + event_id = None + event_title = "" + agenda_url = "" + minutes_url = "" + has_media = False + + for href, text in row_links: + m = re.match(r'/event/\?id=(\d+)', href) + if m: + eid = int(m.group(1)) + if event_id is None: + event_id = eid + event_title = re.sub(r'\(opens in new window\)', '', text).strip() + elif "getagendafile" in href.lower(): + agenda_url = self._normalize_document_url(urljoin(base_url, href)) + elif "getminutesfile" in href.lower(): + minutes_url = self._normalize_document_url(urljoin(base_url, href)) + if "media" in text.lower(): + has_media = True + + if event_id is None or event_id in seen_event_ids: + continue + seen_event_ids.add(event_id) + + date_m = re.search( + r'((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\w*' + r'\s+\d{1,2},?\s*\d{4}(?:\s*\|\s*\d{2}:\d{2}\s*[AP]M)?)', + row_text, re.I + ) + meeting_date = date_m.group(1).strip() if date_m else "" + + year_m = re.search(r'\b(20\d{2})\b', meeting_date) + if start_year and year_m and int(year_m.group(1)) < start_year: + continue + + events.append({ + "id": event_id, + "title": event_title, + "date": meeting_date, + "agenda_url": agenda_url, + "minutes_url": minutes_url, + "has_media": has_media, + }) + + logger.info( + f"Parsed {len(events)} events from SuiteOne homepage table " + f"({len([e for e in events if e['agenda_url']])} with agenda, " + f"{len([e for e in events if e['minutes_url']])} with minutes, " + f"{len([e for e in events if e['has_media']])} with media)" + ) + + # ---- Step 2: extend with historical events if needed ---- + if events and (max_events == 0 or max_events > len(events)): + lowest_id = min(e["id"] for e in events) + logger.info(f"Probing historical events below ID {lowest_id}") + for eid in range(lowest_id - 1, max(1, lowest_id - 5000), -1): + if eid not in seen_event_ids: + seen_event_ids.add(eid) + events.append({ + "id": eid, "title": "", "date": "", "agenda_url": "", + "minutes_url": "", "has_media": True, + }) + logger.info(f"Expanded to {len(events)} total events (including historical)") + + events.sort(key=lambda e: e["id"], reverse=True) + if max_events > 0: + events = events[:max_events] + + logger.info(f"Processing {len(events)} SuiteOne events for {municipality}") + + # ---- Step 3 & 4: for each event, fetch video URL + download PDFs ---- + for i, ev in enumerate(events): + eid = ev["id"] + event_url = f"{base_url}/event/?id={eid}" + + meeting_date = ev["date"] + meeting_title = ev["title"] + meeting_type = re.sub(r'^\d+:\d+\s*[ap]\.m\.\s*', '', meeting_title, flags=re.I).strip() or "Meeting" + video_url = "" + + # Fetch event page when: has media flag, or missing title/date + if ev["has_media"] or not meeting_title or not meeting_date: + try: + ev_resp = await self.http_client.get(event_url) + if ev_resp.status_code == 404: + continue + ev_resp.raise_for_status() + ev_text = ev_resp.text + ev_soup = BeautifulSoup(ev_resp.content, "html.parser") + + title_tag = ev_soup.find("title") + if title_tag: + page_title = title_tag.get_text(strip=True).replace("Meeting:", "").strip() + if "upcoming meetings" in page_title.lower(): + continue + if not meeting_title: + meeting_title = page_title + meeting_type = re.sub(r'^\d+:\d+\s*[ap]\.m\.\s*', '', meeting_title, flags=re.I).strip() or "Meeting" + + if not meeting_date: + dm = re.search( + r'((?:January|February|March|April|May|June|July|August|' + r'September|October|November|December)\s+\d{1,2},?\s*\d{4})', + ev_text + ) + meeting_date = dm.group(1) if dm else "" + + year_m = re.search(r'\b(20\d{2})\b', meeting_date) + if start_year and year_m and int(year_m.group(1)) < start_year: + continue + + if fetch_videos: + src_m = re.search(r"var src\s*=\s*'([^']+)';", ev_text) + if src_m and src_m.group(1): + video_url = src_m.group(1) + + for a in ev_soup.find_all("a", href=True): + href = a["href"] + full = self._normalize_document_url(urljoin(base_url, href)) + if "getagendafile" in full.lower() and not ev["agenda_url"]: + ev["agenda_url"] = full + elif "getminutesfile" in full.lower() and not ev["minutes_url"]: + ev["minutes_url"] = full + + await asyncio.sleep(0.2) + + except Exception as fetch_err: + logger.debug(f"Could not fetch event page {eid}: {fetch_err}") + + doc_urls = [(ev["agenda_url"], "Agenda"), (ev["minutes_url"], "Minutes")] + produced = 0 + for doc_url, doc_type in doc_urls: + if not doc_url or doc_url in self.scraped_urls: + continue + doc = await self._scrape_document( + url=doc_url, + municipality=municipality, + state=state, + title=f"{meeting_title} — {doc_type}", + ) + if doc: + doc["meeting_date"] = meeting_date + doc["meeting_type"] = meeting_type + meta = doc.setdefault("metadata", {}) + meta["platform"] = "suiteonemedia" + meta["event_id"] = eid + meta["doc_type"] = doc_type.lower() + if video_url: + meta["video_url"] = video_url + documents.append(doc) + self.scraped_urls.add(doc_url) + produced += 1 + + if produced == 0 and meeting_title and "upcoming meetings" not in meeting_title.lower(): + doc_id = hashlib.md5(event_url.encode()).hexdigest() + documents.append(MeetingDocument( + document_id=doc_id, + source_url=event_url, + municipality=municipality, + state=state, + meeting_date=meeting_date or datetime.utcnow().isoformat(), + meeting_type=meeting_type, + title=meeting_title, + content="", + metadata={ + "platform": "suiteonemedia", + "event_id": eid, + "video_url": video_url, + "has_pdf": False, + } + )) + + if (i + 1) % 50 == 0: + logger.info( + f" SuiteOne: {i+1}/{len(events)} events processed, " + f"{len(documents)} docs so far" + ) + + await asyncio.sleep(0.3) + + except Exception as e: + logger.error(f"Error scraping SuiteOne portal {url}: {e}") + + logger.info(f"SuiteOne scrape complete: {len(documents)} documents from {municipality}") + return documents + + async def _scrape_eboard_undetected( + self, + target: Dict[str, Any], + date_range: Dict[str, str] + ) -> List[Dict[str, Any]]: + """ + Scrape eBoard using undetected-chromedriver to bypass Incapsula automatically. + + This method uses undetected-chromedriver which patches Selenium to avoid detection: + - Removes navigator.webdriver flag + - Uses real Chrome binary + - Randomizes browser fingerprints + + Args: + target: Scraping target with URL, municipality, state + date_range: Date range for filtering meetings + + Returns: + List of meeting documents + """ + url = target.get("url", "") + municipality = target.get("municipality", "Unknown") + state = target.get("state", "") + + import undetected_chromedriver as uc + from selenium.webdriver.common.by import By + from selenium.webdriver.support.ui import WebDriverWait + from selenium.webdriver.support import expected_conditions as EC + import time + import random + import re + from pathlib import Path + + # Extract school ID from URL + school_id_match = re.search(r'[?&]s=(\d+)', url, re.IGNORECASE) + school_id = school_id_match.group(1) if school_id_match else None + + if not school_id: + logger.error(f"Could not extract school ID from URL: {url}") + return [] + + base_url = "https://simbli.eboardsolutions.com" + meetings_url = f"{base_url}/SB_Meetings/SB_MeetingListing.aspx?S={school_id}" + + logger.info(f"Using undetected-chromedriver for: {meetings_url}") + + documents = [] + driver = None + + try: + # Create undetected Chrome instance + options = uc.ChromeOptions() + # Try headless mode (may work better with newer Chrome) + options.add_argument('--headless=new') # New headless mode + options.add_argument('--no-sandbox') + options.add_argument('--disable-dev-shm-usage') + options.add_argument('--disable-blink-features=AutomationControlled') + options.add_argument('--window-size=1920,1080') + + logger.info("Launching Chrome with anti-detection patches...") + + # Let undetected-chromedriver auto-download matching ChromeDriver + # Specify version_main to match system Chrome (147) + try: + driver = uc.Chrome( + options=options, + version_main=147, # Match Chromium version + use_subprocess=True + ) + except Exception as e: + logger.warning(f"Headless mode failed: {e}, trying with visible browser...") + # Try without headless as fallback + options = uc.ChromeOptions() + options.add_argument('--no-sandbox') + options.add_argument('--disable-dev-shm-usage') + driver = uc.Chrome(options=options, version_main=147, use_subprocess=True) + + # Navigate to meetings page + logger.info("Loading meeting listing page...") + driver.get(meetings_url) + + # Wait for Incapsula challenge to complete + wait_time = random.uniform(6.0, 9.0) + logger.info(f"Waiting {wait_time:.1f}s for Incapsula challenge...") + time.sleep(wait_time) + + # Check if we bypassed Incapsula + page_source = driver.page_source + + if 'Incapsula incident ID' in page_source or ('Incapsula' in page_source and len(page_source) < 10000): + logger.error(f"Still blocked by Incapsula ({len(page_source)} bytes)") + logger.error("Incapsula detection triggered despite undetected-chromedriver") + raise Exception("Incapsula block detected") + + logger.success(f"✓ Bypassed Incapsula! Page size: {len(page_source)} bytes") + + # Parse the page + soup = BeautifulSoup(page_source, 'html.parser') + + # Extract meeting links + meeting_links = [] + + # Look for links with MID parameter or PDFs + for link in soup.find_all('a', href=True): + href = link.get('href', '') + text = link.get_text().strip() + + if 'MID=' in href.upper() or 'meetingdetail' in href.lower(): + full_url = urljoin(base_url, href) + meeting_links.append({ + 'url': full_url, + 'text': text, + 'type': 'meeting' + }) + elif href.lower().endswith('.pdf') and any(word in text.lower() for word in ['agenda', 'minutes', 'packet', 'meeting']): + full_url = urljoin(base_url, href) + meeting_links.append({ + 'url': full_url, + 'text': text, + 'type': 'pdf' + }) + + logger.info(f"Found {len(meeting_links)} meeting/document links") + + # If no links found, try waiting for JavaScript-rendered content + if len(meeting_links) == 0: + logger.warning("No links found initially, waiting for JavaScript...") + try: + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.TAG_NAME, "a")) + ) + time.sleep(3) + + # Re-parse + page_source = driver.page_source + soup = BeautifulSoup(page_source, 'html.parser') + + for link in soup.find_all('a', href=True): + href = link.get('href', '') + text = link.get_text().strip() + + if 'MID=' in href.upper() or href.lower().endswith('.pdf'): + full_url = urljoin(base_url, href) + meeting_links.append({ + 'url': full_url, + 'text': text, + 'type': 'pdf' if href.lower().endswith('.pdf') else 'meeting' + }) + + logger.info(f"After JS wait: {len(meeting_links)} links") + except Exception as e: + logger.warning(f"JS content wait failed: {e}") + + # Save page HTML for debugging + debug_file = Path("/tmp/eboard_meeting_list_undetected.html") + with open(debug_file, 'w', encoding='utf-8') as f: + f.write(page_source) + logger.info(f"Saved page HTML to {debug_file} for debugging") + + # Process meeting links (limit to prevent overwhelming) + for idx, meeting_info in enumerate(meeting_links[:50]): + if idx > 0 and idx % 10 == 0: + logger.info(f"Progress: {idx}/{min(50, len(meeting_links))}") + + # Human-like delay + time.sleep(random.uniform(2.0, 4.0)) + + try: + meeting_url = meeting_info['url'] + meeting_title = meeting_info['text'] + + if meeting_info['type'] == 'pdf': + # Record PDF link for later download + logger.debug(f" Found PDF: {meeting_title[:50]}") + + # Try to extract date from title + meeting_date = datetime.now() + try: + from dateutil import parser as date_parser + date_match = re.search(r'(\d{1,2}[-/]\d{1,2}[-/]\d{2,4})', meeting_title) + if date_match: + meeting_date = date_parser.parse(date_match.group(0)) + except: + pass + + # Download and extract PDF content + try: + pdf_content = await self._scrape_pdf_document(meeting_url) + if pdf_content and len(pdf_content.strip()) > 100: + doc = MeetingDocument( + document_id=hashlib.md5(f"{meeting_url}{municipality}".encode()).hexdigest(), + source_url=meeting_url, + municipality=municipality, + state=state, + meeting_date=meeting_date, + meeting_type='Board Meeting', + title=meeting_title, + content=pdf_content[:50000], + metadata={ + 'platform': 'eboard', + 'school_id': school_id, + 'scraped_with': 'undetected_chromedriver' + } + ) + documents.append(doc) + logger.success(f" ✓ Scraped PDF: {meeting_title[:50]}") + except Exception as e: + logger.error(f" Error downloading PDF: {e}") + + else: + # Navigate to meeting detail page + logger.debug(f" Loading meeting: {meeting_title[:50]}") + driver.get(meeting_url) + time.sleep(random.uniform(2.0, 4.0)) + + meeting_soup = BeautifulSoup(driver.page_source, 'html.parser') + + # Extract meeting date + meeting_date = datetime.now() + try: + from dateutil import parser as date_parser + for elem in meeting_soup.find_all(['h1', 'h2', 'h3', 'div', 'span']): + text = elem.get_text().strip() + date_match = re.search(r'(\d{1,2}[-/]\d{1,2}[-/]\d{2,4})', text) + if date_match: + meeting_date = date_parser.parse(date_match.group(0)) + break + except: + pass + + # Find document links (PDFs) + doc_links = [] + for link in meeting_soup.find_all('a', href=True): + href = link.get('href', '') + link_text = link.get_text().strip() + + if href.lower().endswith('.pdf') or any(word in link_text.lower() for word in ['agenda', 'minutes', 'packet']): + doc_url = urljoin(base_url, href) + doc_links.append({ + 'url': doc_url, + 'text': link_text + }) + + logger.info(f" Found {len(doc_links)} documents") + + # Download each document + for doc_info in doc_links[:5]: + try: + doc_url = doc_info['url'] + doc_title = doc_info['text'] + + if doc_url.lower().endswith('.pdf'): + doc_content = await self._scrape_pdf_document(doc_url) + + if doc_content and len(doc_content.strip()) > 100: + doc = MeetingDocument( + document_id=hashlib.md5(f"{doc_url}{municipality}".encode()).hexdigest(), + source_url=doc_url, + municipality=municipality, + state=state, + meeting_date=meeting_date, + meeting_type='Board Meeting', + title=doc_title or meeting_title, + content=doc_content[:50000], + metadata={ + 'platform': 'eboard', + 'meeting_page': meeting_url, + 'school_id': school_id, + 'scraped_with': 'undetected_chromedriver' + } + ) + documents.append(doc) + logger.success(f" ✓ Scraped: {doc_title[:50]}") + + except Exception as e: + logger.error(f" Error scraping document: {e}") + + except Exception as e: + logger.error(f"Error processing {meeting_info.get('text', 'unknown')}: {e}") + continue + + except Exception as e: + logger.error(f"Error in undetected scraper: {e}") + import traceback + logger.error(traceback.format_exc()) + raise # Re-raise to trigger fallback + + finally: + if driver: + try: + driver.quit() + except: + pass + + logger.success(f"Undetected scraper complete: {len(documents)} documents") + return documents + + async def _scrape_eboard( + self, + target: Dict[str, Any], + date_range: Dict[str, str] + ) -> List[Dict[str, Any]]: + """ + Scrape eBoard Solutions platform (used by many school districts). + + eBoard uses ASP.NET with JavaScript and Incapsula bot protection. + This implementation uses undetected-chromedriver for automatic bypass. + + Bypass methods (in order): + 1. Undetected ChromeDriver - automatic bot detection evasion + 2. Playwright with manual cookies (fallback) + + Args: + target: Scraping target with URL, municipality, state + date_range: Date range for filtering meetings + + Returns: + List of meeting documents + """ + url = target.get("url", "") + municipality = target.get("municipality", "Unknown") + state = target.get("state", "") + + logger.info(f"Scraping eBoard platform: {url} for {municipality}") + + # Try undetected-chromedriver first (automatic bypass) + try: + import undetected_chromedriver as uc + logger.info("Attempting with undetected-chromedriver (automatic bot evasion)") + return await self._scrape_eboard_undetected(target, date_range) + except ImportError: + logger.warning("undetected-chromedriver not available, falling back to Playwright") + except Exception as e: + logger.warning(f"Undetected ChromeDriver failed: {e}, falling back to Playwright") + + # Fallback to Playwright with cookies + logger.info("Using Playwright with manual cookies (if available)") + + documents = [] + + try: + from playwright.async_api import async_playwright + from playwright_stealth import Stealth + import random + from pathlib import Path + + # Extract school ID from URL (S=xxxx parameter) + import re + school_id_match = re.search(r'[?&]s=(\d+)', url, re.IGNORECASE) + school_id = school_id_match.group(1) if school_id_match else None + + if not school_id: + logger.error(f"Could not extract school ID from URL: {url}") + return [] + + # Target the Meeting Listing page directly (bypasses some Incapsula triggers) + base_url = "https://simbli.eboardsolutions.com" + meetings_url = f"{base_url}/SB_Meetings/SB_MeetingListing.aspx?S={school_id}" + + # Check for manual cookies file + cookie_file = Path("eboard_cookies.json") + cookies = None + if cookie_file.exists(): + try: + import json + with open(cookie_file, 'r') as f: + cookies = json.load(f) + logger.success(f"✓ Loaded {len(cookies)} cookies from eboard_cookies.json") + logger.info("Using manual session cookies to bypass Incapsula") + except Exception as e: + logger.warning(f"Could not load cookies: {e}") + else: + logger.info("No cookie file found. Will attempt without cookies (may be blocked)") + logger.info(f"To bypass Incapsula: Create {cookie_file.absolute()}") + logger.info("See docs/EBOARD_MANUAL_DOWNLOAD.md for instructions") + + logger.info(f"Targeting Meeting Listing: {meetings_url}") + + async with async_playwright() as p: + # Launch browser with anti-detection settings + logger.info("Launching browser with stealth settings to bypass Incapsula") + browser = await p.chromium.launch( + headless=True, # Stealth makes headless work + args=[ + '--disable-blink-features=AutomationControlled', + '--disable-dev-shm-usage', + '--no-sandbox' + ] + ) + + # CRITICAL: User-Agent must match the browser used to generate cookies + # If cookies were from Chrome 123, use Chrome 123 UA + user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36' + + context = await browser.new_context( + viewport={'width': 1920, 'height': 1080}, + user_agent=user_agent, + locale='en-US', + timezone_id='America/Chicago', + # Additional fingerprinting evasion + geolocation={'latitude': 33.2098, 'longitude': -87.5692}, # Tuscaloosa, AL + permissions=['geolocation'] + ) + + page = await context.new_page() + + # Apply stealth to bypass Incapsula fingerprinting + stealth = Stealth() + await stealth.apply_stealth_async(page) + logger.info("Stealth mode activated") + + # Inject cookies if available (CRITICAL for bypassing Incapsula) + if cookies: + await context.add_cookies(cookies) + logger.success("✓ Cookies injected into browser session") + + # Navigate to Meeting Listing + logger.info(f"Loading Meeting Listing page...") + try: + # Simulate human behavior - move mouse before navigation + await page.mouse.move(random.randint(100, 500), random.randint(100, 500)) + + response = await page.goto(meetings_url, wait_until='networkidle', timeout=60000) + logger.info(f"Response status: {response.status if response else 'No response'}") + except Exception as e: + logger.warning(f"Navigation timeout/error: {e}, continuing anyway...") + + # Wait for Incapsula JavaScript challenge to complete + # CRITICAL: Use randomized delay (not flat sleep) to avoid pattern detection + wait_time = random.uniform(5.0, 7.0) + logger.info(f"Waiting {wait_time:.1f}s for Incapsula JavaScript challenge...") + await page.wait_for_timeout(int(wait_time * 1000)) + + # Check if we got through + content = await page.content() + + # More sophisticated Incapsula detection + is_blocked = False + if len(content) < 5000: + is_blocked = True + logger.error(f"Still blocked by Incapsula (page too small: {len(content)} bytes)") + elif 'Incapsula incident ID' in content: + is_blocked = True + logger.error(f"Still blocked by Incapsula (incident ID found)") + elif 'Request unsuccessful. Incapsula' in content: + is_blocked = True + logger.error(f"Still blocked by Incapsula (request unsuccessful message)") + elif 'Access Denied' in content or 'Blocked' in content: + is_blocked = True + logger.error(f"Still blocked by Incapsula (access denied title)") + + if is_blocked: + logger.warning(f"Try running with headless=False or use manual session cookies") + logger.info(f"See docs/EBOARD_MANUAL_DOWNLOAD.md for manual download guide") + await browser.close() + return [] + + logger.success(f"✓ Bypassed Incapsula! Got {len(content)} bytes") + + # Save HTML for debugging + debug_file = Path("/tmp/eboard_success.html") + with open(debug_file, 'w', encoding='utf-8') as f: + f.write(content) + logger.info(f"Saved successful page to {debug_file}") + + # Parse the page + soup = BeautifulSoup(content, 'html.parser') + + # Debug: Log all links found on page + all_links = soup.find_all('a', href=True) + logger.info(f"Total tags with href found: {len(all_links)}") + if len(all_links) > 0: + logger.info(f"Sample links (first 10):") + for i, link in enumerate(all_links[:10]): + href = link.get('href', '')[:100] + text = link.get_text().strip()[:50] + logger.info(f" {i+1}. {text} -> {href}") + + # Extract meeting links - eBoard uses MID parameter + # Look for links containing "MID=" (Meeting ID) + meeting_links = [] + + for link in soup.find_all('a', href=True): + href = link.get('href', '') + text = link.get_text().strip() + + # eBoard meeting detail links contain MID parameter + if 'MID=' in href.upper() or 'meetingdetail' in href.lower(): + full_url = urljoin(base_url, href) + meeting_links.append({ + 'url': full_url, + 'text': text, + 'mid': re.search(r'MID=(\d+)', href, re.IGNORECASE).group(1) if re.search(r'MID=(\d+)', href, re.IGNORECASE) else None + }) + + # Also look for direct PDF links (agendas/minutes) + for link in soup.find_all('a', href=True): + href = link.get('href', '') + text = link.get_text().strip() + + if href.lower().endswith('.pdf') and any(word in text.lower() for word in ['agenda', 'minutes', 'packet']): + full_url = urljoin(base_url, href) + meeting_links.append({ + 'url': full_url, + 'text': text, + 'type': 'pdf' + }) + + logger.info(f"Found {len(meeting_links)} meeting/document links") + + # Process each meeting (limit to prevent overwhelming) + for idx, meeting_info in enumerate(meeting_links[:50]): + try: + meeting_url = meeting_info['url'] + meeting_title = meeting_info['text'] + + if idx > 0 and idx % 10 == 0: + logger.info(f" Progress: {idx}/{min(50, len(meeting_links))} meetings processed") + + # CRITICAL: Randomized rate limiting to prevent Advanced Mode trigger + # Never use flat sleep - Incapsula detects patterns + wait_time = random.uniform(3.0, 7.0) + await asyncio.sleep(wait_time) + + # Simulate human mouse movement before each action + await page.mouse.move(random.randint(200, 800), random.randint(200, 600)) + + # Handle PDF links directly + if meeting_info.get('type') == 'pdf': + try: + # Download PDF + pdf_content = await self._scrape_pdf_document(meeting_url) + + if pdf_content and len(pdf_content.strip()) > 100: + # Extract date from title/text + meeting_date = None + try: + from dateutil import parser as date_parser + date_match = re.search(r'(\d{1,2}[-/]\d{1,2}[-/]\d{2,4})', meeting_title) + if not date_match: + date_match = re.search(r'(January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},?\s+\d{4}', meeting_title, re.IGNORECASE) + + if date_match: + meeting_date = date_parser.parse(date_match.group(0)) + except: + meeting_date = datetime.now() + + if not meeting_date: + meeting_date = datetime.now() + + document_id = hashlib.md5(f"{meeting_url}{municipality}".encode()).hexdigest() + + doc = MeetingDocument( + document_id=document_id, + source_url=meeting_url, + municipality=municipality, + state=state, + meeting_date=meeting_date, + meeting_type='Board Meeting', + title=meeting_title, + content=pdf_content[:50000], + metadata={ + 'platform': 'eboard', + 'school_id': school_id, + 'scraped_with': 'playwright_stealth' + } + ) + + documents.append(doc) + logger.success(f" ✓ Scraped PDF: {meeting_title[:50]}") + + except Exception as e: + logger.error(f" Error downloading PDF: {e}") + continue + + # Handle meeting detail pages + else: + logger.debug(f" Loading meeting detail: {meeting_title[:50]}") + + try: + # Simulate clicking on link (human-like behavior) + await page.mouse.move(random.randint(300, 700), random.randint(200, 500)) + await page.goto(meeting_url, wait_until='domcontentloaded', timeout=30000) + + # Random wait to appear human + await page.wait_for_timeout(random.randint(1500, 3000)) + + meeting_content = await page.content() + meeting_soup = BeautifulSoup(meeting_content, 'html.parser') + + # Extract meeting date + meeting_date = None + for elem in meeting_soup.find_all(['h1', 'h2', 'h3', 'div', 'span']): + text = elem.get_text().strip() + date_match = re.search(r'(\d{1,2}[-/]\d{1,2}[-/]\d{2,4})', text) + if not date_match: + date_match = re.search(r'(January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},?\s+\d{4}', text, re.IGNORECASE) + + if date_match: + try: + from dateutil import parser as date_parser + meeting_date = date_parser.parse(date_match.group(0)) + break + except: + pass + + if not meeting_date: + meeting_date = datetime.now() + + # Find document links (PDFs) + doc_links = [] + for link in meeting_soup.find_all('a', href=True): + href = link.get('href', '') + link_text = link.get_text().strip() + + if (href.lower().endswith('.pdf') or + 'agenda' in link_text.lower() or + 'minutes' in link_text.lower() or + 'packet' in link_text.lower()): + + doc_url = urljoin(base_url, href) + doc_links.append({ + 'url': doc_url, + 'text': link_text + }) + + logger.info(f" Found {len(doc_links)} documents for {meeting_title[:40]}") + + # Download each document + for doc_info in doc_links[:5]: # Limit per meeting + try: + doc_url = doc_info['url'] + doc_title = doc_info['text'] + + if doc_url.lower().endswith('.pdf'): + doc_content = await self._scrape_pdf_document(doc_url) + + if doc_content and len(doc_content.strip()) > 100: + document_id = hashlib.md5(f"{doc_url}{municipality}".encode()).hexdigest() + + doc = MeetingDocument( + document_id=document_id, + source_url=doc_url, + municipality=municipality, + state=state, + meeting_date=meeting_date, + meeting_type='Board Meeting', + title=doc_title or meeting_title, + content=doc_content[:50000], + metadata={ + 'platform': 'eboard', + 'meeting_page': meeting_url, + 'school_id': school_id, + 'meeting_id': meeting_info.get('mid'), + 'scraped_with': 'playwright_stealth' + } + ) + + documents.append(doc) + logger.success(f" ✓ Scraped: {doc_title[:50]}") + + except Exception as e: + logger.error(f" Error scraping document: {e}") + continue + + except Exception as e: + logger.error(f" Error processing meeting {meeting_title[:40]}: {e}") + continue + + except Exception as e: + logger.error(f"Error processing meeting link: {e}") + continue + + # Close browser + await browser.close() + + except ImportError as e: + logger.error(f"Missing dependency: {e}") + logger.error("Install with: pip install playwright-stealth && playwright install chromium") + return [] + except Exception as e: + logger.error(f"Error scraping eBoard {url}: {e}") + import traceback + logger.error(traceback.format_exc()) + + logger.success(f"eBoard scrape complete: {len(documents)} documents from {municipality}") + return documents + + async def _scrape_generic( + self, + target: Dict[str, Any], + date_range: Dict[str, str] + ) -> List[Dict[str, Any]]: + """ + Scrape meeting minutes from generic municipal websites. + + Args: + target: Target configuration + date_range: Date range for filtering + + Returns: + List of meeting documents + """ + url = target["url"] + municipality = target["municipality"] + state = target["state"] + + documents = [] + + try: + response = await self.http_client.get(url) + response.raise_for_status() + + soup = BeautifulSoup(response.content, "html.parser") + + candidate_documents = self._extract_document_candidates( + page_url=url, + html=response.text, + soup=soup + ) + + # Crawl a few likely meeting pages because many sites (including JS-heavy portals) + # keep document links off the landing page. + meeting_pages = self._extract_meeting_pages(page_url=url, soup=soup) + for meeting_page in meeting_pages[:8]: + try: + page_response = await self.http_client.get(meeting_page) + if page_response.status_code >= 400: + continue + + page_soup = BeautifulSoup(page_response.content, "html.parser") + page_candidates = self._extract_document_candidates( + page_url=meeting_page, + html=page_response.text, + soup=page_soup + ) + candidate_documents.extend(page_candidates) + await asyncio.sleep(0.2) + except Exception as page_err: + logger.debug(f"Could not scrape meeting page {meeting_page}: {page_err}") + + # De-duplicate while preserving order + seen_urls = set() + deduped_candidates = [] + for doc_url, doc_label in candidate_documents: + if doc_url not in seen_urls: + seen_urls.add(doc_url) + deduped_candidates.append((doc_url, doc_label)) + + for doc_url, doc_label in deduped_candidates[:50]: + if doc_url in self.scraped_urls: + continue + + # Prioritize meeting-related labels but still allow document URL heuristics. + label = (doc_label or "").lower() + if label and not any(keyword in label for keyword in self.meeting_keywords): + if not any(keyword in doc_url.lower() for keyword in self.meeting_keywords): + continue + + doc = await self._scrape_document( + url=doc_url, + municipality=municipality, + state=state, + title=doc_label or "meeting document" + ) + + if doc: + documents.append(doc) + self.scraped_urls.add(doc_url) + + await asyncio.sleep(0.2) + + except Exception as e: + logger.error(f"Error scraping generic site {url}: {e}") + + return documents + + def _extract_document_candidates( + self, + page_url: str, + html: str, + soup: BeautifulSoup + ) -> List[tuple[str, str]]: + """Extract document URLs from anchors and script text.""" + candidates: List[tuple[str, str]] = [] + + # Anchor/link extraction + for anchor in soup.find_all("a", href=True): + href = anchor.get("href", "") + if not href: + continue + full_url = urljoin(page_url, href) + full_url = self._normalize_document_url(full_url) + lowered = full_url.lower() + if any(ext in lowered for ext in self.document_extensions) or any(k in lowered for k in self.document_route_keywords): + text = anchor.get_text(" ", strip=True) or anchor.get("title", "") or "document" + candidates.append((full_url, text)) + + # Script extraction for JS-driven portals that embed links in JSON blobs. + url_pattern = r'(https?://[^"\'\s)]+\.(?:pdf|docx?|pptx?|xlsx?)(?:\?[^"\'\s)]*)?)' + rel_pattern = r'([\w/\-.]+\.(?:pdf|docx?|pptx?|xlsx?)(?:\?[^"\'\s)]*)?)' + + for raw in re.findall(url_pattern, html, flags=re.IGNORECASE): + candidates.append((self._normalize_document_url(raw), "document")) + + route_pattern = r'(["\'](?:/event/Get(?:Agenda|Minutes)File/[^"\']+)["\'])' + for raw in re.findall(route_pattern, html, flags=re.IGNORECASE): + cleaned = raw.strip("\"'") + candidates.append((self._normalize_document_url(urljoin(page_url, cleaned)), "document")) + + for raw in re.findall(rel_pattern, html, flags=re.IGNORECASE): + if raw.startswith("http"): + continue + if raw.startswith("/") or "/" in raw: + candidates.append((self._normalize_document_url(urljoin(page_url, raw)), "document")) + + return candidates + + def _normalize_document_url(self, url: str) -> str: + """Clean common malformed URL artifacts found in embedded portal markup.""" + normalized = url.strip() + normalized = normalized.replace(" %20?", "?") + normalized = normalized.replace("%20?", "?") + normalized = normalized.replace(" ?", "?") + return normalized + + def _extract_meeting_pages(self, page_url: str, soup: BeautifulSoup) -> List[str]: + """Find likely meeting-related subpages to expand document discovery.""" + pages = [] + for anchor in soup.find_all("a", href=True): + href = anchor.get("href", "") + text = anchor.get_text(" ", strip=True).lower() + if not href: + continue + + full_url = urljoin(page_url, href) + combined = f"{text} {full_url.lower()}" + if "/event/?id=" in full_url.lower() or any(keyword in combined for keyword in self.meeting_keywords): + pages.append(full_url) + + seen = set() + deduped = [] + for p in pages: + if p not in seen: + seen.add(p) + deduped.append(p) + return deduped + + async def _scrape_meeting_page( + self, + url: str, + municipality: str, + state: str, + meeting_data: Optional[Dict[str, Any]] = None + ) -> Optional[Dict[str, Any]]: + """ + Scrape a single meeting page. + + Args: + url: Meeting page URL + municipality: Municipality name + state: State code + meeting_data: Optional pre-fetched meeting data + + Returns: + Meeting document or None + """ + try: + response = await self.http_client.get(url) + response.raise_for_status() + + soup = BeautifulSoup(response.content, "html.parser") + + # Extract meeting details (simplified - actual implementation would be more robust) + title = soup.find("h1") or soup.find("title") + title_text = title.get_text().strip() if title else "Untitled Meeting" + + # Extract main content + content_div = soup.find("div", class_="meeting-content") or soup.find("main") + content = content_div.get_text(separator="\n").strip() if content_div else "" + + # Generate document ID + doc_id = hashlib.md5(f"{url}{municipality}".encode()).hexdigest() + + document = MeetingDocument( + document_id=doc_id, + source_url=url, + municipality=municipality, + state=state, + meeting_date=datetime.utcnow(), # Would parse from content + meeting_type="City Council", # Would determine from content + title=title_text, + content=content, + metadata={"platform": "web", "raw_data": meeting_data} + ) + + return document + + except Exception as e: + logger.error(f"Error scraping meeting page {url}: {e}") + return None + + async def _scrape_document( + self, + url: str, + municipality: str, + state: str, + title: str + ) -> Optional[Dict[str, Any]]: + """ + Download and extract text from a document URL. + + Args: + url: PDF URL + municipality: Municipality name + state: State code + title: Document title + + Returns: + Meeting document or None + """ + try: + response = await self.http_client.get(url) + response.raise_for_status() + + content_type = (response.headers.get("content-type") or "").lower() + url_lower = url.lower() + is_pdf = ".pdf" in url_lower or "application/pdf" in content_type + is_image = any(ext in url_lower for ext in [".png", ".jpg", ".jpeg", ".tif", ".tiff"]) or content_type.startswith("image/") + + content = "[Document content extraction unavailable]" + ocr_used = False + ocr_pages = 0 + + if is_pdf and PdfReader is not None: + try: + reader = PdfReader(io.BytesIO(response.content)) + pages = [] + for page in reader.pages[:30]: + pages.append(page.extract_text() or "") + extracted = "\n".join(pages).strip() + if extracted: + content = extracted + else: + content = "[PDF has no extractable text]" + except Exception as parse_error: + logger.debug(f"PDF parse failed for {url}: {parse_error}") + content = "[PDF parsing failed]" + + # OCR fallback for scanned/image-based PDFs. + if is_pdf and content in ["[PDF has no extractable text]", "[PDF parsing failed]"]: + ocr_text, ocr_pages = self._ocr_pdf_bytes(response.content) + if ocr_text: + content = ocr_text + ocr_used = True + + # OCR for image documents. + if is_image and content == "[Document content extraction unavailable]": + image_text = self._ocr_image_bytes(response.content) + if image_text: + content = image_text + ocr_used = True + ocr_pages = 1 + + doc_id = hashlib.md5(f"{url}{municipality}".encode()).hexdigest() + + document = MeetingDocument( + document_id=doc_id, + source_url=url, + municipality=municipality, + state=state, + meeting_date=datetime.utcnow(), + meeting_type="Unknown", + title=title, + content=content, + metadata={ + "platform": "document", + "file_size": len(response.content), + "content_type": response.headers.get("content-type"), + "is_pdf": is_pdf, + "is_image": is_image, + "ocr_used": ocr_used, + "ocr_pages": ocr_pages, + "text_extracted": content not in [ + "[Document content extraction unavailable]", + "[PDF has no extractable text]", + "[PDF parsing failed]" + ] + } + ) + + return document + + except Exception as e: + if isinstance(e, httpx.HTTPStatusError) and e.response.status_code == 404: + logger.debug(f"Document not found (404): {url}") + else: + logger.error(f"Error downloading document {url}: {e}") + return None + + def _ocr_pdf_bytes(self, pdf_bytes: bytes) -> tuple[str, int]: + """OCR PDF pages when direct PDF text extraction fails.""" + if pdfplumber is None or pytesseract is None: + return "", 0 + + try: + extracted_pages = [] + with pdfplumber.open(io.BytesIO(pdf_bytes)) as pdf: + for page in pdf.pages[:self.ocr_max_pages]: + try: + image = page.to_image(resolution=200).original + text = pytesseract.image_to_string(image).strip() + if text: + extracted_pages.append(text) + except TesseractNotFoundError: + if not self._ocr_missing_tesseract_warned: + logger.warning("Tesseract binary not found. Install 'tesseract-ocr' to enable OCR.") + self._ocr_missing_tesseract_warned = True + return "", 0 + except Exception as page_err: + logger.debug(f"OCR page failed: {page_err}") + + if not extracted_pages: + return "", 0 + return "\n\n".join(extracted_pages), len(extracted_pages) + except Exception as err: + logger.debug(f"OCR PDF fallback failed: {err}") + return "", 0 + + def _ocr_image_bytes(self, image_bytes: bytes) -> str: + """OCR text from image-based documents.""" + if pytesseract is None or Image is None: + return "" + + try: + image = Image.open(io.BytesIO(image_bytes)) + return pytesseract.image_to_string(image).strip() + except TesseractNotFoundError: + if not self._ocr_missing_tesseract_warned: + logger.warning("Tesseract binary not found. Install 'tesseract-ocr' to enable OCR.") + self._ocr_missing_tesseract_warned = True + return "" + except Exception as err: + logger.debug(f"Image OCR failed: {err}") + return "" + + async def _scrape_pdf_document( + self, + url: str, + municipality: str, + state: str, + title: str + ) -> Optional[Dict[str, Any]]: + """Backward-compatible wrapper for existing call sites.""" + return await self._scrape_document(url, municipality, state, title) diff --git a/agents/scraper_undetected.py b/agents/scraper_undetected.py new file mode 100644 index 0000000000000000000000000000000000000000..fc2a351aee4efb48c7b1ee7638553e00a4679a5d --- /dev/null +++ b/agents/scraper_undetected.py @@ -0,0 +1,261 @@ +""" +Alternative eBoard scraper using undetected-chromedriver +This bypasses Incapsula without manual cookies +""" +import asyncio +import re +from typing import Dict, Any, List +from pathlib import Path +from bs4 import BeautifulSoup +from urllib.parse import urljoin +from datetime import datetime +import hashlib + +from loguru import logger + + +class UndetectedEboardScraper: + """ + Scrape eBoard using undetected-chromedriver to bypass Incapsula. + + This library patches Selenium ChromeDriver to avoid detection by: + - Removing Selenium markers from navigator.webdriver + - Randomizing browser fingerprints + - Using real Chrome instead of ChromeDriver + """ + + async def scrape_eboard( + self, + url: str, + municipality: str, + state: str, + school_id: str = None + ) -> List[Dict[str, Any]]: + """ + Scrape eBoard platform without manual cookies. + + Args: + url: eBoard URL + municipality: School district name + state: State code + school_id: Optional school ID (extracted from URL if not provided) + + Returns: + List of meeting documents + """ + try: + import undetected_chromedriver as uc + from selenium.webdriver.common.by import By + from selenium.webdriver.support.ui import WebDriverWait + from selenium.webdriver.support import expected_conditions as EC + import time + import random + except ImportError: + logger.error("Missing undetected-chromedriver. Install: pip install undetected-chromedriver") + return [] + + # Extract school ID + if not school_id: + match = re.search(r'[?&]s=(\d+)', url, re.IGNORECASE) + school_id = match.group(1) if match else None + + if not school_id: + logger.error(f"Could not extract school ID from URL: {url}") + return [] + + base_url = "https://simbli.eboardsolutions.com" + meetings_url = f"{base_url}/SB_Meetings/SB_MeetingListing.aspx?S={school_id}" + + logger.info(f"Using undetected-chromedriver to bypass Incapsula") + logger.info(f"Target: {meetings_url}") + + documents = [] + + try: + # Create undetected Chrome instance + options = uc.ChromeOptions() + # options.add_argument('--headless') # Headless may still be detected + options.add_argument('--no-sandbox') + options.add_argument('--disable-dev-shm-usage') + options.add_argument('--disable-blink-features=AutomationControlled') + + # Create driver with version management + driver = uc.Chrome(options=options, version_main=None) + + logger.info("Chrome launched with anti-detection patches") + + # Navigate to meetings page + driver.get(meetings_url) + logger.info(f"Loaded page: {driver.title[:100]}") + + # Wait for Incapsula challenge to complete + # The challenge usually takes 3-5 seconds + wait_time = random.uniform(5.0, 8.0) + logger.info(f"Waiting {wait_time:.1f}s for Incapsula challenge...") + time.sleep(wait_time) + + # Check if we bypassed Incapsula + page_source = driver.page_source + + if 'Incapsula' in page_source and len(page_source) < 10000: + logger.error("Still blocked by Incapsula") + logger.warning("Try running with headless=False or use Option 2 (Residential Proxies)") + driver.quit() + return [] + + logger.success(f"✓ Bypassed Incapsula! Page size: {len(page_source)} bytes") + + # Parse the page + soup = BeautifulSoup(page_source, 'html.parser') + + # Extract meeting links + meeting_links = [] + + # Method 1: Look for MID parameter + for link in soup.find_all('a', href=True): + href = link.get('href', '') + text = link.get_text().strip() + + if 'MID=' in href.upper() or 'meetingdetail' in href.lower(): + full_url = urljoin(base_url, href) + meeting_links.append({ + 'url': full_url, + 'text': text, + 'type': 'meeting' + }) + elif href.lower().endswith('.pdf'): + full_url = urljoin(base_url, href) + meeting_links.append({ + 'url': full_url, + 'text': text, + 'type': 'pdf' + }) + + logger.info(f"Found {len(meeting_links)} meeting/document links") + + # If no links found, try JavaScript execution + if len(meeting_links) == 0: + logger.warning("No links found in HTML, checking for JavaScript-rendered content...") + + # Wait for dynamic content + try: + WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.TAG_NAME, "a")) + ) + time.sleep(3) # Additional wait for JS + + # Re-parse + page_source = driver.page_source + soup = BeautifulSoup(page_source, 'html.parser') + + for link in soup.find_all('a', href=True): + href = link.get('href', '') + text = link.get_text().strip() + + if 'MID=' in href.upper() or href.lower().endswith('.pdf'): + full_url = urljoin(base_url, href) + meeting_links.append({ + 'url': full_url, + 'text': text, + 'type': 'pdf' if href.lower().endswith('.pdf') else 'meeting' + }) + + logger.info(f"After JS wait: Found {len(meeting_links)} links") + except Exception as e: + logger.warning(f"JS content wait failed: {e}") + + # Process meeting links (limit to prevent overwhelming) + for idx, meeting_info in enumerate(meeting_links[:50]): + if idx > 0 and idx % 10 == 0: + logger.info(f"Progress: {idx}/{min(50, len(meeting_links))}") + + # Human-like delay + time.sleep(random.uniform(2.0, 5.0)) + + try: + meeting_url = meeting_info['url'] + meeting_title = meeting_info['text'] + + if meeting_info['type'] == 'pdf': + # Download PDF directly + logger.debug(f" Downloading PDF: {meeting_title[:50]}") + # TODO: Implement PDF download + # For now, just record the URL + doc = { + 'document_id': hashlib.md5(f"{meeting_url}{municipality}".encode()).hexdigest(), + 'source_url': meeting_url, + 'municipality': municipality, + 'state': state, + 'meeting_date': datetime.now(), + 'meeting_type': 'Board Meeting', + 'title': meeting_title, + 'content': '', # Would need PDF extraction + 'metadata': { + 'platform': 'eboard', + 'school_id': school_id, + 'scraped_with': 'undetected_chromedriver' + } + } + documents.append(doc) + else: + # Navigate to meeting detail page + logger.debug(f" Loading meeting: {meeting_title[:50]}") + driver.get(meeting_url) + time.sleep(random.uniform(2.0, 4.0)) + + meeting_soup = BeautifulSoup(driver.page_source, 'html.parser') + + # Extract PDFs from meeting page + for link in meeting_soup.find_all('a', href=True): + href = link.get('href', '') + if href.lower().endswith('.pdf'): + doc_url = urljoin(base_url, href) + doc_title = link.get_text().strip() + + doc = { + 'document_id': hashlib.md5(f"{doc_url}{municipality}".encode()).hexdigest(), + 'source_url': doc_url, + 'municipality': municipality, + 'state': state, + 'meeting_date': datetime.now(), + 'meeting_type': 'Board Meeting', + 'title': doc_title or meeting_title, + 'content': '', + 'metadata': { + 'platform': 'eboard', + 'meeting_page': meeting_url, + 'school_id': school_id, + 'scraped_with': 'undetected_chromedriver' + } + } + documents.append(doc) + logger.success(f" ✓ Found: {doc_title[:50]}") + + except Exception as e: + logger.error(f"Error processing {meeting_info.get('text', 'unknown')}: {e}") + continue + + driver.quit() + logger.success(f"Scraping complete: {len(documents)} documents") + return documents + + except Exception as e: + logger.error(f"Error in undetected scraper: {e}") + import traceback + logger.error(traceback.format_exc()) + return [] + + +# Example usage +async def main(): + scraper = UndetectedEboardScraper() + docs = await scraper.scrape_eboard( + url="http://simbli.eboardsolutions.com/index.aspx?s=2088", + municipality="Tuscaloosa City Schools", + state="AL" + ) + print(f"Scraped {len(docs)} documents") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/agents/sentiment.py b/agents/sentiment.py new file mode 100644 index 0000000000000000000000000000000000000000..e3b8d948070ed36035629e59d2967e7400bc969c --- /dev/null +++ b/agents/sentiment.py @@ -0,0 +1,381 @@ +""" +Sentiment Analyzer Agent for determining policy stance and debate intensity. +""" +import asyncio +from typing import List, Dict, Any, Optional +from datetime import datetime +from loguru import logger + +from agents.base import BaseAgent, AgentRole, AgentMessage, MessageType, AgentStatus + + +class PolicyStance: + """Enumeration of policy stances.""" + STRONGLY_SUPPORTIVE = "strongly_supportive" + SUPPORTIVE = "supportive" + NEUTRAL = "neutral" + OPPOSED = "opposed" + STRONGLY_OPPOSED = "strongly_opposed" + DEBATED = "debated" # When there's active debate + + +class DebateIntensity: + """Enumeration of debate intensity levels.""" + NONE = "none" # Passing mention + LOW = "low" # Brief discussion + MODERATE = "moderate" # Extended discussion + HIGH = "high" # Heated debate with multiple viewpoints + CRITICAL = "critical" # Vote imminent or major decision pending + + +class SentimentAnalyzerAgent(BaseAgent): + """ + Agent responsible for analyzing sentiment and policy stance. + + Determines: + - Overall stance toward oral health policies + - Intensity of debate/discussion + - Key arguments for and against + - Likelihood of policy action + - Advocacy opportunities + """ + + def __init__(self, agent_id: str = "sentiment-001"): + """Initialize the sentiment analyzer agent.""" + super().__init__(agent_id, AgentRole.SENTIMENT_ANALYZER) + self._initialize_indicators() + + def _initialize_indicators(self): + """Initialize sentiment and debate indicators.""" + self.supportive_indicators = [ + "approve", "support", "favor", "endorse", "recommend", + "beneficial", "important", "necessary", "implement", + "move forward", "proceed with" + ] + + self.opposition_indicators = [ + "oppose", "against", "reject", "deny", "concerns about", + "problems with", "issues with", "delay", "postpone", + "table the motion", "reconsider" + ] + + self.debate_indicators = [ + "discussion", "debate", "motion", "vote", "amendment", + "public comment", "testimony", "hearing", "concerns", + "questions about", "divided" + ] + + self.urgency_indicators = [ + "urgent", "immediate", "deadline", "vote", "decision", + "approval needed", "time-sensitive", "pressing", + "second reading", "final vote" + ] + + async def process(self, message: AgentMessage) -> List[AgentMessage]: + """ + Process sentiment analysis commands. + + Args: + message: Message containing classified documents + + Returns: + List of messages with sentiment analysis results + """ + self.update_status(AgentStatus.PROCESSING, "Analyzing policy sentiment and debate") + + try: + documents = message.payload.get("documents", []) + + analyzed_documents = [] + + for doc in documents: + analysis = await self._analyze_document(doc) + doc["sentiment_analysis"] = analysis + analyzed_documents.append(doc) + + # Identify advocacy opportunities + opportunities = self._identify_advocacy_opportunities(analyzed_documents) + + # Send to advocacy writer agent + response = await self.send_message( + AgentRole.ADVOCACY_WRITER, + MessageType.DATA, + { + "workflow_id": message.payload.get("workflow_id"), + "documents": analyzed_documents, + "opportunities": opportunities, + "count": len(analyzed_documents) + } + ) + + self.log_success() + logger.info( + f"Analyzed sentiment for {len(analyzed_documents)} documents, " + f"found {len(opportunities)} advocacy opportunities" + ) + + return [response] + + except Exception as e: + self.log_failure(str(e)) + error_msg = await self.send_message( + AgentRole.ORCHESTRATOR, + MessageType.ERROR, + {"error": str(e), "agent": self.agent_id} + ) + return [error_msg] + + async def _analyze_document( + self, + doc: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Analyze sentiment and policy stance for a document. + + Args: + doc: Document to analyze + + Returns: + Sentiment analysis results + """ + text = self._get_analyzable_text(doc) + text_lower = text.lower() + + # Count sentiment indicators + support_score = sum( + 1 for indicator in self.supportive_indicators + if indicator in text_lower + ) + + opposition_score = sum( + 1 for indicator in self.opposition_indicators + if indicator in text_lower + ) + + debate_score = sum( + 1 for indicator in self.debate_indicators + if indicator in text_lower + ) + + urgency_score = sum( + 1 for indicator in self.urgency_indicators + if indicator in text_lower + ) + + # Determine policy stance + stance = self._determine_stance(support_score, opposition_score, debate_score) + + # Determine debate intensity + intensity = self._determine_intensity(debate_score, urgency_score, doc) + + # Extract key arguments + arguments = self._extract_arguments(doc, text_lower) + + # Calculate advocacy urgency + advocacy_urgency = self._calculate_advocacy_urgency( + stance, intensity, urgency_score + ) + + analysis = { + "stance": stance, + "debate_intensity": intensity, + "support_score": support_score, + "opposition_score": opposition_score, + "debate_score": debate_score, + "urgency_score": urgency_score, + "advocacy_urgency": advocacy_urgency, + "key_arguments": arguments, + "analyzed_at": datetime.utcnow().isoformat() + } + + return analysis + + def _get_analyzable_text(self, doc: Dict[str, Any]) -> str: + """Extract text for sentiment analysis.""" + parts = [] + + # Prioritize excerpts from classification + for excerpt in doc.get("classification", {}).get("relevant_excerpts", []): + parts.append(excerpt.get("text", "")) + + # Add motions (highly relevant) + for motion in doc.get("motions", []): + parts.append(motion.get("text", "")) + + # Add votes + for vote in doc.get("votes", []): + parts.append(vote.get("result", "")) + + # Fallback to full text if needed + if not parts: + parts.append(doc.get("full_text", "")) + + return " ".join(parts) + + def _determine_stance( + self, + support_score: int, + opposition_score: int, + debate_score: int + ) -> str: + """Determine overall policy stance.""" + if debate_score >= 3 and abs(support_score - opposition_score) <= 1: + return PolicyStance.DEBATED + + if support_score > opposition_score: + if support_score >= 3: + return PolicyStance.STRONGLY_SUPPORTIVE + else: + return PolicyStance.SUPPORTIVE + elif opposition_score > support_score: + if opposition_score >= 3: + return PolicyStance.STRONGLY_OPPOSED + else: + return PolicyStance.OPPOSED + else: + return PolicyStance.NEUTRAL + + def _determine_intensity( + self, + debate_score: int, + urgency_score: int, + doc: Dict[str, Any] + ) -> str: + """Determine debate intensity.""" + # Check for votes or motions (indicates high intensity) + has_vote = len(doc.get("votes", [])) > 0 + has_motion = len(doc.get("motions", [])) > 0 + + if urgency_score >= 2 or (has_vote and has_motion): + return DebateIntensity.CRITICAL + elif debate_score >= 5 or has_vote or has_motion: + return DebateIntensity.HIGH + elif debate_score >= 3: + return DebateIntensity.MODERATE + elif debate_score >= 1: + return DebateIntensity.LOW + else: + return DebateIntensity.NONE + + def _extract_arguments( + self, + doc: Dict[str, Any], + text_lower: str + ) -> Dict[str, List[str]]: + """Extract key arguments for and against.""" + arguments = { + "supporting": [], + "opposing": [] + } + + # Extract from motions and discussion + for motion in doc.get("motions", []): + motion_text = motion.get("text", "").lower() + + if any(ind in motion_text for ind in self.supportive_indicators): + arguments["supporting"].append(motion.get("text", "")) + elif any(ind in motion_text for ind in self.opposition_indicators): + arguments["opposing"].append(motion.get("text", "")) + + return arguments + + def _calculate_advocacy_urgency( + self, + stance: str, + intensity: str, + urgency_score: int + ) -> str: + """ + Calculate how urgent advocacy action is needed. + + Returns: "critical", "high", "medium", "low", or "none" + """ + # Critical: Vote imminent and debated/opposed + if intensity == DebateIntensity.CRITICAL: + if stance in [PolicyStance.DEBATED, PolicyStance.OPPOSED, PolicyStance.STRONGLY_OPPOSED]: + return "critical" + return "high" + + # High: Active debate with opposition + if intensity == DebateIntensity.HIGH: + if stance in [PolicyStance.OPPOSED, PolicyStance.STRONGLY_OPPOSED]: + return "high" + elif stance == PolicyStance.DEBATED: + return "high" + return "medium" + + # Medium: Moderate discussion or emerging issue + if intensity == DebateIntensity.MODERATE: + return "medium" + + # Low: Early stage or general mention + if intensity == DebateIntensity.LOW: + return "low" + + return "none" + + def _identify_advocacy_opportunities( + self, + documents: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """ + Identify advocacy opportunities across all analyzed documents. + + Args: + documents: All analyzed documents + + Returns: + List of advocacy opportunities + """ + opportunities = [] + + for doc in documents: + sentiment = doc.get("sentiment_analysis", {}) + urgency = sentiment.get("advocacy_urgency") + + # Only flag high and critical urgency items + if urgency in ["critical", "high"]: + opportunity = { + "document_id": doc["document_id"], + "municipality": doc["municipality"], + "state": doc["state"], + "meeting_date": doc["meeting_date"], + "source_url": doc["source_url"], + "topic": doc["classification"]["primary_topic"], + "stance": sentiment["stance"], + "intensity": sentiment["debate_intensity"], + "urgency": urgency, + "key_excerpts": doc["classification"].get("relevant_excerpts", []), + "recommended_action": self._recommend_action(sentiment, doc) + } + opportunities.append(opportunity) + + # Sort by urgency + urgency_order = {"critical": 0, "high": 1, "medium": 2, "low": 3} + opportunities.sort(key=lambda x: urgency_order.get(x["urgency"], 4)) + + return opportunities + + def _recommend_action( + self, + sentiment: Dict[str, Any], + doc: Dict[str, Any] + ) -> str: + """Recommend advocacy action based on analysis.""" + stance = sentiment.get("stance") + intensity = sentiment.get("debate_intensity") + + if intensity == DebateIntensity.CRITICAL: + if stance in [PolicyStance.OPPOSED, PolicyStance.STRONGLY_OPPOSED]: + return "URGENT: Contact officials immediately. Vote imminent." + elif stance == PolicyStance.DEBATED: + return "URGENT: Provide supporting testimony. Decision pending." + + if stance == PolicyStance.DEBATED: + return "Engage with stakeholders. Provide educational materials." + elif stance in [PolicyStance.OPPOSED, PolicyStance.STRONGLY_OPPOSED]: + return "Initiate dialogue with decision-makers. Address concerns." + elif stance == PolicyStance.NEUTRAL: + return "Introduce topic to agenda. Build awareness." + + return "Monitor situation. Prepare support materials." diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f36f9ec8c192765a57dc181c696be38b9a3573c2 --- /dev/null +++ b/api/__init__.py @@ -0,0 +1,4 @@ +"""API module for the Oral Health Policy Pulse system.""" +from api.main import app + +__all__ = ["app"] diff --git a/api/app.py b/api/app.py new file mode 100644 index 0000000000000000000000000000000000000000..a7a1b2eb50ef01d8c7951006e7ccd93a386985f8 --- /dev/null +++ b/api/app.py @@ -0,0 +1,711 @@ +""" +FastAPI application optimized for Databricks Apps deployment. +Serves React frontend and provides REST API for agent interactions. +""" +from typing import List, Dict, Any, Optional +from datetime import datetime +from pathlib import Path +from fastapi import FastAPI, HTTPException, Query, BackgroundTasks +from fastapi.responses import HTMLResponse, JSONResponse, FileResponse +from fastapi.staticfiles import StaticFiles +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel, Field +from loguru import logger +import os + +from agents.orchestrator import OrchestratorAgent +from pipeline.delta_lake import DeltaLakePipeline +from config import settings + +# Initialize FastAPI app +app = FastAPI( + title="Open Navigator", + description="AI-powered advocacy opportunity finder", + version="2.0.0", + docs_url="/api/docs", + redoc_url="/api/redoc", + openapi_url="/api/openapi.json" +) + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Initialize components +orchestrator = OrchestratorAgent() +pipeline = DeltaLakePipeline() + +# Pydantic models +class WorkflowRequest(BaseModel): + """Request to start a new analysis workflow.""" + targets: List[Dict[str, str]] + topics: Optional[List[str]] = None + +class OpportunityFilter(BaseModel): + """Filter criteria for advocacy opportunities.""" + state: Optional[str] = None + topic: Optional[str] = None + urgency: Optional[str] = None + min_confidence: Optional[float] = 0.7 + + +# API Routes +@app.get("/api/health") +async def health_check(): + """Health check endpoint.""" + return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()} + + +@app.get("/api/dashboard") +async def get_dashboard_stats(): + """Get dashboard statistics and recent opportunities.""" + try: + # Query Delta Lake for stats + stats = await pipeline.get_dashboard_stats() + return stats + except Exception as e: + logger.error(f"Dashboard stats error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/opportunities") +async def get_opportunities( + state: Optional[str] = Query(None), + topic: Optional[str] = Query(None), + urgency: Optional[str] = Query(None), + limit: int = Query(100, le=1000) +): + """Get advocacy opportunities with optional filters.""" + try: + opportunities = await pipeline.query_opportunities( + state=state, + topic=topic, + urgency=urgency, + limit=limit + ) + return {"opportunities": opportunities, "count": len(opportunities)} + except Exception as e: + logger.error(f"Query opportunities error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/documents") +async def get_documents( + search: Optional[str] = Query(None), + page: int = Query(1, ge=1), + limit: int = Query(20, le=100) +): + """Get analyzed documents with pagination.""" + try: + offset = (page - 1) * limit + documents = await pipeline.query_documents( + search=search, + limit=limit, + offset=offset + ) + total = await pipeline.count_documents(search=search) + return { + "documents": documents, + "page": page, + "limit": limit, + "total": total, + "total_pages": (total + limit - 1) // limit + } + except Exception as e: + logger.error(f"Query documents error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/search/") +async def search_all( + q: str = Query(..., min_length=1, description="Search query"), + types: Optional[str] = Query("contacts,organizations,causes", description="Comma-separated types to search"), + state: Optional[str] = Query(None, description="Filter by state code (e.g. MA, AL)"), + limit: int = Query(10, ge=1, le=100), + page: int = Query(1, ge=1), + ntee_code: Optional[str] = Query(None, description="NTEE category code") +): + """ + Unified search across contacts, organizations, causes, and jurisdictions. + Returns results grouped by type with pagination. + """ + try: + offset = (page - 1) * limit + search_types = [t.strip() for t in types.split(',') if t.strip()] + + # Initialize results structure + results = { + "contacts": [], + "organizations": [], + "causes": [], + "meetings": [], + "jurisdictions": [] + } + + # Search logic (placeholder - implement actual search) + # For now, return mock results for demonstration + if "contacts" in search_types: + results["contacts"] = [ + { + "type": "contact", + "title": f"Sample Contact matching '{q}'", + "subtitle": "Government Official", + "description": "Contact information for local official", + "url": "/contact/1", + "score": 0.9, + "metadata": {"state": state or "MA"} + } + ] + + if "organizations" in search_types: + results["organizations"] = [ + { + "type": "organization", + "title": f"Sample Organization matching '{q}'", + "subtitle": "Nonprofit Organization", + "description": "Community health organization", + "url": "/org/1", + "score": 0.85, + "metadata": {"state": state or "MA", "ntee": ntee_code} + } + ] + + if "causes" in search_types: + results["causes"] = [ + { + "type": "cause", + "title": f"Sample Cause matching '{q}'", + "subtitle": "Health & Wellness", + "description": "Advocacy for community health", + "url": "/cause/1", + "score": 0.8, + "metadata": {} + } + ] + + # Calculate total results + total_results = sum(len(v) for v in results.values()) + total_pages = max(1, (total_results + limit - 1) // limit) + + return { + "query": q, + "total_results": total_results, + "results": results, + "pagination": { + "page": page, + "limit": limit, + "offset": offset, + "total_pages": total_pages, + "has_next": page < total_pages, + "has_prev": page > 1 + }, + "filters": { + "state": state, + "ntee_code": ntee_code, + "types": search_types + } + } + except Exception as e: + logger.error(f"Search error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/workflow/start") +async def start_workflow(request: WorkflowRequest, background_tasks: BackgroundTasks): + """Start a new analysis workflow.""" + try: + workflow_id = f"workflow_{datetime.utcnow().timestamp()}" + + # Start workflow in background + background_tasks.add_task( + orchestrator.execute_pipeline, + workflow_id=workflow_id, + targets=request.targets, + topics=request.topics + ) + + return { + "workflow_id": workflow_id, + "status": "started", + "message": "Workflow started successfully" + } + except Exception as e: + logger.error(f"Workflow start error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/workflow/{workflow_id}/status") +async def get_workflow_status(workflow_id: str): + """Get status of a running workflow.""" + try: + status = await orchestrator.get_workflow_status(workflow_id) + return status + except Exception as e: + logger.error(f"Workflow status error: {e}") + raise HTTPException(status_code=404, detail="Workflow not found") + + +@app.post("/api/advocacy/email/{opportunity_id}") +async def generate_advocacy_email(opportunity_id: str): + """Generate advocacy email for an opportunity.""" + try: + opportunity = await pipeline.get_opportunity(opportunity_id) + if not opportunity: + raise HTTPException(status_code=404, detail="Opportunity not found") + + email_content = await orchestrator.generate_advocacy_email(opportunity) + return {"content": email_content} + except Exception as e: + logger.error(f"Generate email error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/settings") +async def get_settings(): + """Get current system settings.""" + return { + "target_states": settings.target_states or [], + "policy_topics": settings.policy_topics, + "min_confidence": 0.7, + "email_notifications": False, + "notification_email": "" + } + + +@app.put("/api/settings") +async def update_settings(new_settings: Dict[str, Any]): + """Update system settings.""" + try: + # In production, this would update configuration in Unity Catalog + logger.info(f"Settings update requested: {new_settings}") + return {"message": "Settings updated successfully"} + except Exception as e: + logger.error(f"Settings update error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/agents/status") +async def get_agents_status(): + """Get status of all agents.""" + try: + return { + "agents": [ + {"name": "Scraper", "status": "active", "uptime": "24h"}, + {"name": "Classifier", "status": "active", "uptime": "24h"}, + {"name": "Sentiment Analyzer", "status": "active", "uptime": "24h"}, + {"name": "Advocacy Writer", "status": "active", "uptime": "24h"} + ] + } + except Exception as e: + logger.error(f"Agent status error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/nonprofits") +async def search_nonprofits( + location: str = Query("Tuscaloosa, AL", description="City, State format"), + keyword: Optional[str] = Query(None, description="Service keyword (e.g., 'dental', 'health')"), + state: Optional[str] = Query(None, description="2-letter state code (e.g., 'AL')"), + ntee_code: Optional[str] = Query(None, description="NTEE code (e.g., 'E' for health)"), + source: Optional[str] = Query(None, description="Data source: 'propublica', 'everyorg', 'all'") +): + """ + Search for nonprofits using free open data APIs. + + Integrates data from: + - ProPublica Nonprofit Explorer (financial data, NTEE codes) + - Every.org (mission statements, logos) + - IRS TEOS (official tax-exempt status) + + Example: /api/nonprofits?location=Tuscaloosa,AL&keyword=dental&ntee_code=E + """ + try: + from discovery.nonprofit_discovery import NonprofitDiscovery + + discovery = NonprofitDiscovery() + results = [] + + # Parse location for state/city + location_parts = location.split(',') + city = location_parts[0].strip() if len(location_parts) > 0 else None + state_from_location = location_parts[1].strip() if len(location_parts) > 1 else None + state_code = state or state_from_location or "AL" + + # Determine which sources to query + sources_to_query = ['propublica', 'everyorg'] if source == 'all' or not source else [source] + + # Query ProPublica + if 'propublica' in sources_to_query: + try: + propublica_results = discovery.search_propublica( + state=state_code, + city=city, + ntee_code=ntee_code + ) + results.extend(propublica_results) + logger.info(f"ProPublica: Found {len(propublica_results)} organizations") + except Exception as e: + logger.warning(f"ProPublica search failed: {e}") + + # Query Every.org + if 'everyorg' in sources_to_query: + try: + causes = [] + if keyword: + # Map keywords to causes + keyword_lower = keyword.lower() + if 'health' in keyword_lower or 'dental' in keyword_lower or 'medical' in keyword_lower: + causes.append('health') + if 'education' in keyword_lower or 'school' in keyword_lower: + causes.append('education') + + everyorg_results = discovery.search_everyorg( + location=location, + causes=causes if causes else None + ) + results.extend(everyorg_results) + logger.info(f"Every.org: Found {len(everyorg_results)} organizations") + except Exception as e: + logger.warning(f"Every.org search failed: {e}") + + # Filter by keyword if provided + if keyword and results: + keyword_lower = keyword.lower() + filtered_results = [] + for org in results: + # Search in name, description, mission, ntee_description + searchable_text = ' '.join([ + str(org.get('name', '')), + str(org.get('description', '')), + str(org.get('mission', '')), + str(org.get('ntee_description', '')) + ]).lower() + + if keyword_lower in searchable_text: + filtered_results.append(org) + + results = filtered_results + + return { + "location": location, + "keyword": keyword, + "state": state_code, + "ntee_code": ntee_code, + "count": len(results), + "nonprofits": results, + "data_sources": { + "propublica": "https://projects.propublica.org/nonprofits/api", + "everyorg": "https://www.every.org/nonprofit-api", + "irs_teos": "https://www.irs.gov/charities-non-profits/tax-exempt-organization-search-bulk-data-downloads" + } + } + + except Exception as e: + logger.error(f"Nonprofit search error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/data/status") +async def get_data_status(): + """ + Get status of all reference data ingestions. + + Returns counts and last update times for: + - Census jurisdictions + - NCES school districts + - Nonprofit organizations + - Meeting datasets (MeetingBank, LocalView, etc.) + """ + try: + from pathlib import Path + from datetime import datetime + + status = { + "census_jurisdictions": { + "path": "data/bronze/census_jurisdictions", + "status": "not_ingested", + "count": 0, + "last_updated": None + }, + "nces_school_districts": { + "path": "data/bronze/nces_school_districts", + "status": "not_ingested", + "count": 0, + "last_updated": None + }, + "nonprofits": { + "path": "data/cache/nonprofits", + "status": "cached", + "count": 0, + "last_updated": None + }, + "meeting_datasets": { + "meetingbank": {"status": "available", "count": 1366}, + "city_scrapers": {"status": "available", "count": "100-500"}, + "open_states": {"status": "available", "count": "50+"} + } + } + + # Check each data directory + for key in ["census_jurisdictions", "nces_school_districts", "nonprofits"]: + data_path = Path(status[key]["path"]) + if data_path.exists(): + files = list(data_path.glob("**/*")) + status[key]["count"] = len(files) + status[key]["status"] = "ingested" if files else "empty" + if files: + latest_file = max(files, key=lambda f: f.stat().st_mtime if f.is_file() else 0) + if latest_file.is_file(): + status[key]["last_updated"] = datetime.fromtimestamp( + latest_file.stat().st_mtime + ).isoformat() + + return status + + except Exception as e: + logger.error(f"Data status error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/data/ingest/census") +async def ingest_census_data(background_tasks: BackgroundTasks): + """ + Trigger Census Bureau jurisdiction data ingestion. + + Downloads and processes: + - 3,144 counties + - 19,500+ municipalities + - 36,000+ townships + - 13,000+ school districts + + This is a long-running operation that runs in the background. + """ + try: + def run_census_ingestion(): + from discovery.census_ingestion import CensusGovernmentIngestion + import asyncio + + logger.info("Starting Census data ingestion...") + ingestor = CensusGovernmentIngestion() + + # Run async ingestion + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + result = loop.run_until_complete(ingestor.ingest_all_jurisdictions()) + loop.close() + + logger.success(f"Census ingestion complete: {result}") + + background_tasks.add_task(run_census_ingestion) + + return { + "message": "Census data ingestion started", + "status": "processing", + "check_status": "/api/data/status" + } + + except Exception as e: + logger.error(f"Census ingestion error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/data/ingest/nces") +async def ingest_nces_data(background_tasks: BackgroundTasks): + """ + Trigger NCES school district data ingestion. + + Downloads and processes 13,000+ school districts with: + - District names and addresses + - Contact information + - NCES IDs + - Enrollment data + """ + try: + def run_nces_ingestion(): + from discovery.nces_ingestion import NCESSchoolDistrictIngestion + import asyncio + + logger.info("Starting NCES data ingestion...") + ingestor = NCESSchoolDistrictIngestion() + + # Run async ingestion + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + result = loop.run_until_complete(ingestor.download_and_process()) + loop.close() + + logger.success(f"NCES ingestion complete: {result}") + + background_tasks.add_task(run_nces_ingestion) + + return { + "message": "NCES data ingestion started", + "status": "processing", + "check_status": "/api/data/status" + } + + except Exception as e: + logger.error(f"NCES ingestion error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/data/ingest/nonprofits") +async def ingest_nonprofits( + state: str = Query(..., description="2-letter state code"), + ntee_codes: Optional[List[str]] = Query(None, description="NTEE codes to ingest"), + background_tasks: BackgroundTasks = None +): + """ + Trigger nonprofit data ingestion for a specific state. + + Bulk downloads nonprofit data from ProPublica API and caches locally. + + Example: POST /api/data/ingest/nonprofits?state=AL&ntee_codes=E&ntee_codes=E20 + """ + try: + from discovery.nonprofit_discovery import NonprofitDiscovery + + discovery = NonprofitDiscovery() + ntee_list = ntee_codes or ["E"] # Default to health + + total_orgs = 0 + for ntee_code in ntee_list: + orgs = discovery.search_propublica(state=state, ntee_code=ntee_code) + total_orgs += len(orgs) + logger.info(f"Cached {len(orgs)} nonprofits for {state}/{ntee_code}") + + return { + "message": f"Nonprofit data ingestion complete for {state}", + "state": state, + "ntee_codes": ntee_list, + "organizations_cached": total_orgs, + "cache_location": "data/cache/nonprofits" + } + + except Exception as e: + logger.error(f"Nonprofit ingestion error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/jurisdictions") +async def get_jurisdictions( + state: Optional[str] = Query(None, description="2-letter state code"), + type: Optional[str] = Query(None, description="Type: county, municipality, township"), + limit: int = Query(100, le=1000) +): + """ + Query ingested Census jurisdiction data. + + Returns government entities with FIPS codes, coordinates, and population. + """ + try: + # This would query the Delta Lake census tables + # For now, return sample data + return { + "message": "Query census jurisdiction data from Delta Lake", + "filters": {"state": state, "type": type}, + "limit": limit, + "note": "Requires Census data ingestion first (POST /api/data/ingest/census)", + "example_data": [ + { + "name": "Tuscaloosa County", + "state": "AL", + "type": "county", + "fips": "01125", + "population": "209355" + } + ] + } + + except Exception as e: + logger.error(f"Jurisdiction query error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/school-districts") +async def get_school_districts( + state: Optional[str] = Query(None, description="2-letter state code"), + limit: int = Query(100, le=1000) +): + """ + Query ingested NCES school district data. + + Returns school districts with contact information and enrollment. + """ + try: + # This would query the Delta Lake NCES tables + return { + "message": "Query NCES school district data from Delta Lake", + "filters": {"state": state}, + "limit": limit, + "note": "Requires NCES data ingestion first (POST /api/data/ingest/nces)", + "example_data": [ + { + "name": "Tuscaloosa City Schools", + "state": "AL", + "nces_id": "0100123", + "phone": "(205) 759-3500", + "website": "https://www.tusc.k12.al.us/" + } + ] + } + + except Exception as e: + logger.error(f"School district query error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +# Serve React frontend +static_dir = Path(__file__).parent / "static" +if static_dir.exists(): + # Mount static files (JS, CSS, images) + app.mount("/assets", StaticFiles(directory=static_dir / "assets"), name="assets") + + # Serve index.html for all non-API routes (SPA routing) + @app.get("/{full_path:path}") + async def serve_react_app(full_path: str): + """Serve React app for all non-API routes.""" + if full_path.startswith("api/"): + raise HTTPException(status_code=404, detail="API endpoint not found") + + index_file = static_dir / "index.html" + if index_file.exists(): + return FileResponse(index_file) + else: + raise HTTPException(status_code=404, detail="Frontend not built") + + +@app.on_event("startup") +async def startup_event(): + """Initialize system on startup.""" + logger.info("Starting Oral Health Policy Pulse application...") + + # Initialize Delta Lake if not exists + try: + await pipeline.initialize_tables() + logger.info("Delta Lake tables initialized") + except Exception as e: + logger.warning(f"Delta Lake initialization skipped: {e}") + + logger.info("Application started successfully") + + +@app.on_event("shutdown") +async def shutdown_event(): + """Cleanup on shutdown.""" + logger.info("Shutting down application...") + + +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "api.app:app", + host="0.0.0.0", + port=8000, + reload=True + ) diff --git a/api/auth.py b/api/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..0c2e4ab0d2db44c0565fe69c81168278d92206f2 --- /dev/null +++ b/api/auth.py @@ -0,0 +1,153 @@ +""" +Authentication utilities - JWT tokens, password hashing, OAuth helpers +""" +import os +import secrets +from datetime import datetime, timedelta +from typing import Optional, Dict, Any +from dotenv import load_dotenv + +from jose import JWTError, jwt +from passlib.context import CryptContext +from fastapi import HTTPException, status, Depends +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.orm import Session + +from api.database import get_db +from api.models import User + +# Load environment variables +load_dotenv() + +# Security configuration +SECRET_KEY = os.getenv("JWT_SECRET_KEY", secrets.token_urlsafe(32)) +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days + +# Password hashing +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# HTTP Bearer token scheme +security = HTTPBearer(auto_error=False) + + +def hash_password(password: str) -> str: + """Hash a password using bcrypt""" + return pwd_context.hash(password) + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verify a password against its hash""" + return pwd_context.verify(plain_password, hashed_password) + + +def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: + """ + Create a JWT access token + + Args: + data: Payload to encode (usually {"sub": user_id}) + expires_delta: Optional custom expiration time + + Returns: + JWT token string + """ + to_encode = data.copy() + + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +def decode_access_token(token: str) -> Dict[str, Any]: + """ + Decode and validate a JWT token + + Args: + token: JWT token string + + Returns: + Decoded payload + + Raises: + HTTPException: If token is invalid or expired + """ + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + return payload + except JWTError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + +def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + db: Session = Depends(get_db) +) -> Optional[User]: + """ + Get current authenticated user from JWT token + + Usage: + @app.get("/protected") + def protected_route(user: User = Depends(get_current_user)): + return {"message": f"Hello {user.email}"} + + Returns: + User object if authenticated, None if optional auth + """ + if not credentials: + return None + + token = credentials.credentials + payload = decode_access_token(token) + + user_id: int = payload.get("sub") + if user_id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid authentication credentials" + ) + + user = db.query(User).filter(User.id == user_id).first() + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found" + ) + + # Update last login + user.last_login = datetime.utcnow() + db.commit() + + return user + + +def require_auth(user: User = Depends(get_current_user)) -> User: + """ + Require authentication (raises 401 if not authenticated) + + Usage: + @app.get("/protected") + def protected_route(user: User = Depends(require_auth)): + return {"message": f"Hello {user.email}"} + """ + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Authentication required", + headers={"WWW-Authenticate": "Bearer"}, + ) + return user + + +def generate_state_token() -> str: + """Generate a secure random state token for OAuth CSRF protection""" + return secrets.token_urlsafe(32) diff --git a/api/database.py b/api/database.py new file mode 100644 index 0000000000000000000000000000000000000000..29e20907910cea545863afa78d975a80e1709ac3 --- /dev/null +++ b/api/database.py @@ -0,0 +1,62 @@ +""" +Database connection and session management +""" +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, Session +from sqlalchemy.pool import StaticPool +from typing import Generator + +from api.models import Base + +# Database URL from environment or default to SQLite for development +DATABASE_URL = os.getenv( + "DATABASE_URL", + "sqlite:///./data/users.db" # Fallback to SQLite if no PostgreSQL configured +) + +# Handle PostgreSQL URL format for SQLAlchemy 2.0+ +if DATABASE_URL.startswith("postgres://"): + DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://", 1) + +# Create engine +if "sqlite" in DATABASE_URL: + # SQLite needs special handling for concurrent access + engine = create_engine( + DATABASE_URL, + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) +else: + # PostgreSQL configuration + engine = create_engine( + DATABASE_URL, + pool_pre_ping=True, + pool_size=10, + max_overflow=20, + ) + +# Create session factory +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +def init_db(): + """Create all tables""" + Base.metadata.create_all(bind=engine) + print(f"✅ Database initialized at: {DATABASE_URL}") + + +def get_db() -> Generator[Session, None, None]: + """ + Database session dependency for FastAPI + + Usage: + @app.get("/users") + def get_users(db: Session = Depends(get_db)): + return db.query(User).all() + """ + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/api/errors.py b/api/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..02a10129e1f7a608850c5b0b2d55831933823502 --- /dev/null +++ b/api/errors.py @@ -0,0 +1,154 @@ +""" +Structured error models for API responses. + +Provides user-friendly error messages with expandable technical details. +""" +from pydantic import BaseModel, Field +from typing import Optional, Dict, Any + + +class ErrorDetail(BaseModel): + """Structured error detail with user-friendly message and technical info.""" + + message: str = Field( + ..., + description="User-friendly error message" + ) + + error_type: str = Field( + ..., + description="Type of error (e.g., 'data_not_found', 'network_error', 'validation_error')" + ) + + technical_details: Optional[str] = Field( + None, + description="Technical error details for debugging (expandable in UI)" + ) + + suggestions: Optional[list[str]] = Field( + None, + description="Helpful suggestions for the user" + ) + + metadata: Optional[Dict[str, Any]] = Field( + None, + description="Additional context (state, dataset name, etc.)" + ) + + +def parse_error(exception: Exception, context: Optional[Dict[str, Any]] = None) -> ErrorDetail: + """ + Parse an exception into a structured ErrorDetail with user-friendly message. + + Args: + exception: The exception to parse + context: Additional context (state, dataset, etc.) + + Returns: + ErrorDetail with user-friendly message and technical details + """ + error_str = str(exception) + context = context or {} + + # Parse HuggingFace dataset not found errors + if "HTTP 404 Not Found" in error_str and "huggingface.co/datasets" in error_str: + # Extract dataset name from URL + import re + match = re.search(r'datasets/([^/]+/[^/]+)/', error_str) + dataset_name = match.group(1) if match else "unknown" + + # Extract state from dataset name or context + state = context.get('state', 'Unknown') + data_type = 'bills' if 'bills' in dataset_name else 'data' + + return ErrorDetail( + message=f"No {data_type} data available for {state.upper()}", + error_type="data_not_found", + technical_details=f"Dataset '{dataset_name}' not found on HuggingFace.\n\nFull error: {error_str}", + suggestions=[ + f"Try a different state - we have data for 50+ states", + f"Check /api/bills/map to see which states have {data_type} data", + "Contact support if you believe this data should be available" + ], + metadata={ + "dataset": dataset_name, + "state": state, + "data_type": data_type + } + ) + + # Parse file not found errors (local environment) + elif "No such file or directory" in error_str or "FileNotFoundError" in error_str: + state = context.get('state', 'Unknown') + data_type = context.get('data_type', 'data') + + return ErrorDetail( + message=f"No {data_type} available for {state.upper()}", + error_type="data_not_found", + technical_details=error_str, + suggestions=[ + f"This state may not have {data_type} in our database yet", + "Try a different state or check which states have data", + "Data is being continuously added - check back later" + ], + metadata={ + "state": state, + "data_type": data_type + } + ) + + # Parse DuckDB/SQL errors + elif "DuckDB" in error_str or "SYNTAX ERROR" in error_str or "LINE" in error_str: + return ErrorDetail( + message="Database query error - please check your search parameters", + error_type="query_error", + technical_details=error_str, + suggestions=[ + "Try simplifying your search query", + "Check that all parameters are valid", + "Contact support if the issue persists" + ], + metadata=context + ) + + # Parse network/timeout errors + elif "timeout" in error_str.lower() or "connection" in error_str.lower(): + return ErrorDetail( + message="Network request timed out - please try again", + error_type="network_error", + technical_details=error_str, + suggestions=[ + "Try again in a few seconds", + "Check your internet connection", + "The server may be temporarily busy" + ], + metadata=context + ) + + # Parse validation errors + elif "validation" in error_str.lower() or "invalid" in error_str.lower(): + return ErrorDetail( + message="Invalid request parameters", + error_type="validation_error", + technical_details=error_str, + suggestions=[ + "Check that all required parameters are provided", + "Verify parameter formats (e.g., state codes should be 2 letters)", + "See API documentation for valid parameter values" + ], + metadata=context + ) + + # Generic error fallback + else: + return ErrorDetail( + message="An unexpected error occurred", + error_type="server_error", + technical_details=error_str, + suggestions=[ + "Try again in a few moments", + "Contact support if the issue persists", + "Check the technical details for more information" + ], + metadata=context + ) diff --git a/api/main.py b/api/main.py new file mode 100644 index 0000000000000000000000000000000000000000..f744c848d2f23ff2578cdbee105455e94758d708 --- /dev/null +++ b/api/main.py @@ -0,0 +1,1288 @@ +""" +FastAPI application for the Oral Health Policy Pulse system. + +Provides REST API endpoints for: +- Initiating policy analysis workflows +- Querying advocacy opportunities +- Retrieving generated materials +- Accessing visualizations +- System status and monitoring +""" +from typing import List, Dict, Any, Optional +from datetime import datetime, date +import sys +import time +from pathlib import Path +from fastapi import FastAPI, HTTPException, Query, BackgroundTasks, Request +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.openapi.docs import get_swagger_ui_html +from pydantic import BaseModel, Field +from loguru import logger +import os + +from agents.orchestrator import OrchestratorAgent +from agents.scraper import ScraperAgent +from agents.parser import ParserAgent +from agents.classifier import ClassifierAgent +from agents.sentiment import SentimentAnalyzerAgent +from agents.advocacy import AdvocacyWriterAgent +from pipeline.delta_lake import DeltaLakePipeline +from visualization.heatmap import AdvocacyHeatmap +from config import settings + +# Configure logging with rotation and retention +# Output to both file (with rotation) and stderr (for HuggingFace container logs) +logger.remove() # Remove default handler + +# Add console output (shows in HuggingFace container logs) +logger.add( + sys.stderr, + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function} - {message}", + level=settings.log_level +) + +# Add file output with rotation and retention +logger.add( + settings.log_file, + rotation="500 MB", # Create new file when size exceeds 500MB + retention="10 days", # Delete logs older than 10 days + level=settings.log_level, + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function} - {message}" +) + +# Initialize FastAPI app +app = FastAPI( + title="Open Navigator API", + description="Multi-agent system for analyzing local government oral health policy discussions", + version="1.0.0", + docs_url=None, # Disable default docs to use custom + redoc_url="/redoc", # Keep ReDoc at /redoc + openapi_tags=[ + { + "name": "auth", + "description": "Authentication and user management" + }, + { + "name": "social", + "description": "Social features - follow users, leaders, organizations, and causes" + }, + { + "name": "workflows", + "description": "Policy analysis workflows" + }, + { + "name": "opportunities", + "description": "Advocacy opportunities" + } + ] +) + +# Custom OpenAPI schema with logo +def custom_openapi(): + if app.openapi_schema: + return app.openapi_schema + + from fastapi.openapi.utils import get_openapi + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + description=app.description, + routes=app.routes, + ) + + # Add custom logo + openapi_schema["info"]["x-logo"] = { + "url": "/static/communityone_logo.svg", + "altText": "CommunityOne Logo" + } + + app.openapi_schema = openapi_schema + return app.openapi_schema + +app.openapi = custom_openapi + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Request logging middleware +@app.middleware("http") +async def log_requests(request: Request, call_next): + """Log all API requests with timing and response info""" + start_time = time.time() + + # Get client info + client_host = request.client.host if request.client else "unknown" + + # Log incoming request + logger.info(f"➡️ {request.method} {request.url.path} - Client: {client_host}") + + # Process request + try: + response = await call_next(request) + + # Calculate duration + duration_ms = (time.time() - start_time) * 1000 + + # Get response size if available + response_size = response.headers.get("content-length", "unknown") + + # Log response with appropriate emoji based on status + if response.status_code < 400: + logger.info( + f"✅ {request.method} {request.url.path} - " + f"Status: {response.status_code} - " + f"Duration: {duration_ms:.2f}ms - " + f"Size: {response_size} bytes" + ) + elif response.status_code < 500: + logger.warning( + f"⚠️ {request.method} {request.url.path} - " + f"Status: {response.status_code} - " + f"Duration: {duration_ms:.2f}ms" + ) + else: + logger.error( + f"❌ {request.method} {request.url.path} - " + f"Status: {response.status_code} - " + f"Duration: {duration_ms:.2f}ms" + ) + + return response + + except Exception as e: + duration_ms = (time.time() - start_time) * 1000 + logger.error( + f"💥 {request.method} {request.url.path} - " + f"Error: {str(e)} - " + f"Duration: {duration_ms:.2f}ms" + ) + raise + +# Mount static files for logo +static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend", "public") +if os.path.exists(static_dir): + app.mount("/static", StaticFiles(directory=static_dir), name="static") +else: + logger.warning(f"Static directory not found: {static_dir}") + +# Include authentication routes +from api.routes import auth as auth_routes +from api.routes import social as social_routes +from api.routes import search as search_routes +# Use Neon database for fast stats queries (500x faster than parquet) +from api.routes import stats_neon as stats_routes # Was: stats +from api.routes import contact as contact_routes +# Use hybrid approach for bills: Neon for map, parquet for drill-down (saves space) +from api.routes import bills_neon as bills_routes # Was: bills +from api.database import init_db + +app.include_router(auth_routes.router) +app.include_router(social_routes.router) +app.include_router(search_routes.router) +app.include_router(stats_routes.router, prefix="/api", tags=["stats"]) +app.include_router(contact_routes.router) +app.include_router(bills_routes.router) + +# Custom Swagger UI with logo +@app.get("/docs", include_in_schema=False) +async def custom_swagger_ui_html(): + """Custom Swagger UI with CommunityOne logo""" + return get_swagger_ui_html( + openapi_url=app.openapi_url, + title=f"{app.title} - API Documentation", + swagger_favicon_url="/static/communityone_logo_64.png", + swagger_ui_parameters={ + "defaultModelsExpandDepth": -1, + "docExpansion": "list", + "filter": True, + "syntaxHighlight.theme": "monokai" + } + ) + +# Initialize database on startup +@app.on_event("startup") +async def init_database(): + """Initialize authentication database""" + try: + init_db() + logger.info("✅ Authentication database initialized") + except Exception as e: + logger.warning(f"⚠️ Database initialization skipped: {e}") + +# Initialize components +orchestrator = OrchestratorAgent() +pipeline = DeltaLakePipeline() +heatmap_generator = AdvocacyHeatmap() + + +# Pydantic models for API +class ScrapeTarget(BaseModel): + """Configuration for a scraping target.""" + url: str + municipality: str + state: str + platform: str = "generic" + + +class WorkflowRequest(BaseModel): + """Request to start a new analysis workflow.""" + scrape_targets: List[ScrapeTarget] + date_range: Optional[Dict[str, str]] = None + description: Optional[str] = None + + +class WorkflowResponse(BaseModel): + """Response for workflow operations.""" + workflow_id: str + status: str + message: str + + +class OpportunityFilter(BaseModel): + """Filters for querying opportunities.""" + state: Optional[str] = None + municipality: Optional[str] = None + topic: Optional[str] = None + urgency: Optional[str] = None + min_date: Optional[date] = None + max_date: Optional[date] = None + + +class OpportunityResponse(BaseModel): + """Response containing advocacy opportunities.""" + opportunities: List[Dict[str, Any]] + total_count: int + filters_applied: Dict[str, Any] + + +class SystemStatus(BaseModel): + """System status information.""" + status: str + active_workflows: int + agent_status: Dict[str, Any] + last_update: datetime + + +# API Endpoints + +@app.get("/", response_class=HTMLResponse) +async def root(): + """Root endpoint with API information.""" + html_content = """ + + + + Open Navigator API + + + +
+ +
+

Open Navigator API

+

CommunityOne: The open path to everything local

+
+
+ +

🔑 Key Endpoints:

+
+ POST /workflow/start - Start a new analysis workflow +
+
+ GET /opportunities - Query advocacy opportunities +
+
+ GET /heatmap - Get advocacy heatmap visualization +
+
+ GET /status - System status and health +
+
+ POST /auth/login/{provider} - OAuth login (HuggingFace, Google, Facebook, GitHub) +
+ +
📚 View Full API Documentation + + + """ + return HTMLResponse(content=html_content) + + +@app.post("/workflow/start", response_model=WorkflowResponse) +async def start_workflow( + request: WorkflowRequest, + background_tasks: BackgroundTasks +): + """ + Start a new policy analysis workflow. + + This initiates the full multi-agent pipeline: + 1. Scrape meeting minutes from specified sources + 2. Parse and structure the data + 3. Classify by oral health topics + 4. Analyze sentiment and policy stance + 5. Generate advocacy materials + """ + try: + # Register agents with orchestrator + orchestrator.register_agent(ScraperAgent()) + orchestrator.register_agent(ParserAgent()) + orchestrator.register_agent(ClassifierAgent()) + orchestrator.register_agent(SentimentAnalyzerAgent()) + orchestrator.register_agent(AdvocacyWriterAgent()) + + # Convert targets to dict format + targets = [target.dict() for target in request.scrape_targets] + + # Start workflow in background + background_tasks.add_task( + orchestrator.execute_pipeline, + targets, + request.date_range + ) + + return WorkflowResponse( + workflow_id="wf-" + datetime.utcnow().strftime("%Y%m%d-%H%M%S"), + status="started", + message=f"Workflow started with {len(targets)} targets" + ) + + except Exception as e: + logger.error(f"Error starting workflow: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/workflow/{workflow_id}/status") +async def get_workflow_status(workflow_id: str): + """Get the status of a specific workflow.""" + # Query workflow status from orchestrator + # This is a placeholder - would query actual workflow state + return { + "workflow_id": workflow_id, + "status": "running", + "stage": "classification", + "progress": 0.6 + } + + +@app.get("/opportunities", response_model=OpportunityResponse) +async def get_opportunities( + state: Optional[str] = Query(None, description="Filter by state code"), + municipality: Optional[str] = Query(None, description="Filter by municipality"), + topic: Optional[str] = Query(None, description="Filter by policy topic"), + urgency: Optional[str] = Query(None, description="Filter by urgency level"), + limit: int = Query(100, ge=1, le=1000, description="Maximum results to return") +): + """ + Query advocacy opportunities. + + Returns a list of identified opportunities for advocacy action + based on the specified filters. + """ + try: + # Query from Delta Lake + opportunities = pipeline.query_opportunities_by_state(state, urgency) + + # Apply additional filters + if municipality: + opportunities = [ + opp for opp in opportunities + if opp.get("municipality", "").lower() == municipality.lower() + ] + + if topic: + opportunities = [ + opp for opp in opportunities + if opp.get("topic") == topic + ] + + # Limit results + opportunities = opportunities[:limit] + + return OpportunityResponse( + opportunities=opportunities, + total_count=len(opportunities), + filters_applied={ + "state": state, + "municipality": municipality, + "topic": topic, + "urgency": urgency + } + ) + + except Exception as e: + logger.error(f"Error querying opportunities: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +# React frontend endpoints with /api/ prefix +@app.get("/api/opportunities") +async def get_api_opportunities( + state: Optional[str] = Query(None), + topic: Optional[str] = Query(None), + urgency: Optional[str] = Query(None), + limit: int = Query(100) +): + """API endpoint for React frontend opportunities page - returns fluoridation bills as advocacy opportunities.""" + try: + import duckdb + from pathlib import Path + import random + + # State center coordinates for mapping + STATE_COORDS = { + 'AL': (32.806671, -86.791130), + 'GA': (33.040619, -83.643074), + 'IN': (39.849426, -86.258278), + 'MA': (42.230171, -71.530106), + 'WA': (47.400902, -121.490494), + 'WI': (44.268543, -89.616508) + } + + # Build query for fluoridation-related bills + states = [state] if state else list(STATE_COORDS.keys()) + opportunities = [] + + for st in states: + parquet_path = Path(f"data/gold/states/{st}/bills_bills.parquet") + if not parquet_path.exists(): + continue + + # Query for fluoridation-related bills + query = f""" + SELECT + '{st}' as state, + title, + identifier, + session, + latest_action, + created_at, + updated_at + FROM read_parquet('{parquet_path}') + WHERE LOWER(title) LIKE '%fluorid%' + OR LOWER(title) LIKE '%dental%' + OR LOWER(title) LIKE '%oral health%' + OR LOWER(title) LIKE '%water treat%' + LIMIT {limit} + """ + + result = duckdb.query(query).fetchall() + + # Convert to opportunities format + for row in result: + state_code, title, identifier, session, latest_action, created_at, updated_at = row + + # Determine urgency based on keywords + title_lower = title.lower() if title else "" + # Check for fluoride topics (both pro and anti fluoride are critical) + if 'fluoride' in title_lower or 'fluorin' in title_lower or 'water' in title_lower: + urgency_level = 'critical' + confidence = 0.9 + topic_type = 'water_fluoridation' + elif 'dental' in title_lower: + urgency_level = 'high' + confidence = 0.75 + topic_type = 'school_dental_screening' + else: + urgency_level = 'medium' + confidence = 0.6 + topic_type = 'medicaid_dental_expansion' + + # Filter by topic if specified + if topic and topic_type != topic: + continue + + # Filter by urgency if specified + if urgency and urgency_level != urgency: + continue + + # Get state coordinates with slight random offset for multiple bills + base_lat, base_lon = STATE_COORDS[state_code] + lat_offset = random.uniform(-0.5, 0.5) + lon_offset = random.uniform(-0.5, 0.5) + + opportunities.append({ + 'state': state_code, + 'municipality': f'{state_code} Legislature', + 'latitude': base_lat + lat_offset, + 'longitude': base_lon + lon_offset, + 'topic': topic_type, + 'urgency': urgency_level, + 'confidence': confidence, + 'meeting_date': updated_at.isoformat() if updated_at else created_at.isoformat(), + 'title': title, + 'bill_id': identifier, + 'session': session, + 'latest_action': latest_action + }) + + return {"opportunities": opportunities[:limit]} + except Exception as e: + logger.error(f"Error: {e}") + return {"opportunities": []} + + +@app.get("/api/documents") +async def get_api_documents( + search: Optional[str] = Query(None), + page: int = Query(1), + limit: int = Query(20) +): + """API endpoint for React frontend documents page.""" + try: + # Get all opportunities (documents) + documents = pipeline.query_opportunities_by_state(None, None) + + # Apply search filter + if search: + search_lower = search.lower() + documents = [ + d for d in documents + if search_lower in d.get("title", "").lower() or + search_lower in d.get("municipality", "").lower() or + search_lower in d.get("content", "").lower() + ] + + # Paginate + start = (page - 1) * limit + end = start + limit + + return { + "documents": documents[start:end], + "total": len(documents), + "page": page, + "limit": limit + } + except Exception as e: + logger.error(f"Error: {e}") + return {"documents": [], "total": 0} + + +@app.get("/opportunities/{opportunity_id}") +async def get_opportunity_detail(opportunity_id: str): + """Get detailed information about a specific opportunity.""" + # Query specific opportunity + document = pipeline.get_document_by_id(opportunity_id) + + if not document: + raise HTTPException(status_code=404, detail="Opportunity not found") + + return document + + +@app.get("/opportunities/{opportunity_id}/materials") +async def get_advocacy_materials(opportunity_id: str): + """Get generated advocacy materials for an opportunity.""" + # Query materials from Delta Lake + # This is a placeholder + return { + "opportunity_id": opportunity_id, + "materials": { + "email": { + "subject": "Support Oral Health Policy", + "body": "..." + }, + "talking_points": [], + "social_media": {} + } + } + + +@app.get("/heatmap", response_class=HTMLResponse) +async def get_heatmap( + urgency: Optional[str] = Query(None, description="Filter by urgency level") +): + """ + Get interactive heatmap visualization. + + Returns an HTML page with an interactive map showing + advocacy opportunities across the country. + """ + try: + # Query opportunities + opportunities = pipeline.query_opportunities_by_state(None, urgency) + + # Generate map + m = heatmap_generator.create_folium_map( + opportunities, + title="Open Navigator - Advocacy Heatmap" + ) + + # Return HTML + return HTMLResponse(content=m._repr_html_()) + + except Exception as e: + logger.error(f"Error generating heatmap: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/dashboard") +async def get_dashboard(): + """ + Get complete dashboard data including statistics and visualizations. + """ + try: + # Query all opportunities + opportunities = pipeline.query_opportunities_by_state(None, None) + + # Generate dashboard + dashboard = heatmap_generator.create_dashboard(opportunities) + + # Convert visualizations to JSON-serializable format + return { + "statistics": dashboard["statistics"], + "topic_distribution": dashboard["topic_distribution"].to_json(), + "timeline": dashboard["timeline"].to_json() + } + + except Exception as e: + logger.error(f"Error generating dashboard: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/dashboard") +async def get_api_dashboard(): + """ + Get dashboard statistics for React frontend. + Returns data in format expected by frontend Dashboard component. + """ + try: + # Query all opportunities + opportunities = pipeline.query_opportunities_by_state(None, None) + + # Count topics + topics_count = {} + for opp in opportunities: + topic = opp.get("topic", "unknown") + topics_count[topic] = topics_count.get(topic, 0) + 1 + + # Get unique states + states = set(opp.get("state") for opp in opportunities if opp.get("state")) + + # Get recent opportunities (last 10) + recent = sorted( + opportunities, + key=lambda x: x.get("meeting_date", ""), + reverse=True + )[:10] + + return { + "total_documents": len(opportunities), + "total_opportunities": len(opportunities), + "states_monitored": len(states), + "topics": topics_count, + "recent_opportunities": recent + } + + except Exception as e: + logger.error(f"Error generating API dashboard: {e}") + # Return mock data if there's an error + return { + "total_documents": 0, + "total_opportunities": 0, + "states_monitored": 0, + "topics": {}, + "recent_opportunities": [] + } + + +@app.get("/topics") +async def get_topics(): + """Get list of all policy topics being tracked.""" + return { + "topics": settings.policy_topics, + "count": len(settings.policy_topics) + } + + +@app.get("/states") +async def get_states(): + """Get list of all states with active opportunities.""" + # Query distinct states from database + states = ["CA", "NY", "TX", "FL", "IL"] # Placeholder + + return { + "states": states, + "count": len(states) + } + + +@app.get("/status", response_model=SystemStatus) +async def get_system_status(): + """Get current system status and health.""" + try: + agent_status = orchestrator.get_all_agent_states() + + return SystemStatus( + status="operational", + active_workflows=len(orchestrator.active_workflows), + agent_status=agent_status, + last_update=datetime.utcnow() + ) + + except Exception as e: + logger.error(f"Error getting system status: {e}") + return SystemStatus( + status="error", + active_workflows=0, + agent_status={}, + last_update=datetime.utcnow() + ) + + +@app.get("/nonprofits") +async def search_nonprofits( + location: str = Query("Tuscaloosa, AL", description="City, State format"), + keyword: Optional[str] = Query(None, description="Service keyword (e.g., 'dental', 'health')"), + state: Optional[str] = Query(None, description="2-letter state code (e.g., 'AL')"), + ntee_code: Optional[str] = Query(None, description="NTEE code (e.g., 'E' for health)"), + source: Optional[str] = Query(None, description="Data source: 'propublica', 'everyorg', 'all'") +): + """ + Search for nonprofits using free open data APIs. + + Integrates data from: + - ProPublica Nonprofit Explorer (financial data, NTEE codes) + - Every.org (mission statements, logos) + - IRS TEOS (official tax-exempt status) + + Example: /nonprofits?location=Tuscaloosa,AL&keyword=dental&ntee_code=E + """ + try: + from discovery.nonprofit_discovery import NonprofitDiscovery + + discovery = NonprofitDiscovery() + results = [] + + # Parse location for state/city + location_parts = location.split(',') + city = location_parts[0].strip() if len(location_parts) > 0 else None + state_from_location = location_parts[1].strip() if len(location_parts) > 1 else None + state_code = state or state_from_location or "AL" + + # Determine which sources to query + sources_to_query = ['propublica', 'everyorg'] if source == 'all' or not source else [source] + + # Query ProPublica + if 'propublica' in sources_to_query: + try: + propublica_results = discovery.search_propublica( + state=state_code, + city=city, + ntee_code=ntee_code + ) + results.extend(propublica_results) + logger.info(f"ProPublica: Found {len(propublica_results)} organizations") + except Exception as e: + logger.warning(f"ProPublica search failed: {e}") + + # Query Every.org + if 'everyorg' in sources_to_query: + try: + causes = [] + if keyword: + # Map keywords to causes + keyword_lower = keyword.lower() + if 'health' in keyword_lower or 'dental' in keyword_lower or 'medical' in keyword_lower: + causes.append('health') + if 'education' in keyword_lower or 'school' in keyword_lower: + causes.append('education') + + everyorg_results = discovery.search_everyorg( + location=location, + causes=causes if causes else None + ) + results.extend(everyorg_results) + logger.info(f"Every.org: Found {len(everyorg_results)} organizations") + except Exception as e: + logger.warning(f"Every.org search failed: {e}") + + # Filter by keyword if provided + if keyword and results: + keyword_lower = keyword.lower() + filtered_results = [] + for org in results: + # Search in name, description, mission, ntee_description + searchable_text = ' '.join([ + str(org.get('name', '')), + str(org.get('description', '')), + str(org.get('mission', '')), + str(org.get('ntee_description', '')) + ]).lower() + + if keyword_lower in searchable_text: + filtered_results.append(org) + + results = filtered_results + + return { + "location": location, + "keyword": keyword, + "state": state_code, + "ntee_code": ntee_code, + "count": len(results), + "nonprofits": results, + "data_sources": { + "propublica": "https://projects.propublica.org/nonprofits/api", + "everyorg": "https://www.every.org/nonprofit-api", + "irs_teos": "https://www.irs.gov/charities-non-profits/tax-exempt-organization-search-bulk-data-downloads" + } + } + + except Exception as e: + logger.error(f"Nonprofit search error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/nonprofits") +async def search_nonprofits_api( + location: str = Query("Tuscaloosa, AL", description="City, State format"), + keyword: Optional[str] = Query(None, description="Service keyword (e.g., 'dental', 'health')"), + state: Optional[str] = Query(None, description="2-letter state code (e.g., 'AL')"), + ntee_code: Optional[str] = Query(None, description="NTEE code (e.g., 'E' for health)"), + source: Optional[str] = Query(None, description="Data source: 'propublica', 'everyorg', 'all'") +): + """ + Search for nonprofits using free open data APIs (API-prefixed endpoint for frontend). + + This is a duplicate of /nonprofits with /api prefix for frontend routing. + """ + return await search_nonprofits(location, keyword, state, ntee_code, source) + + +@app.get("/data/status") +async def get_data_ingestion_status(): + """ + Get status of reference data ingestions. + + Shows Census jurisdictions, NCES school districts, and nonprofit cache status. + """ + try: + from pathlib import Path + from datetime import datetime + + status = { + "census": { + "jurisdictions": 90000, + "counties": 3144, + "municipalities": 19500, + "status": "Check data/bronze/census_jurisdictions" + }, + "nces": { + "school_districts": 13000, + "status": "Check data/bronze/nces_school_districts" + }, + "nonprofits": { + "total_available": 3000000, + "cached_searches": 0, + "cache_path": "data/cache/nonprofits" + }, + "meetings": { + "meetingbank": 1366, + "city_scrapers": "100-500", + "open_states": "50+" + } + } + + # Check cache directories + cache_dir = Path("data/cache/nonprofits") + if cache_dir.exists(): + cached_files = list(cache_dir.glob("*.json")) + status["nonprofits"]["cached_searches"] = len(cached_files) + + return status + + except Exception as e: + logger.error(f"Data status error: {e}") + return {"error": str(e)} + + +@app.post("/data/ingest/nonprofits") +async def bulk_ingest_nonprofits( + state: str = Query(..., description="State code (e.g., AL)"), + ntee_code: Optional[str] = Query("E", description="NTEE code (default: E for Health)") +): + """ + Bulk ingest nonprofit data for a state. + + Caches ProPublica API results for offline use. + """ + try: + from discovery.nonprofit_discovery import NonprofitDiscovery + + discovery = NonprofitDiscovery() + orgs = discovery.search_propublica(state=state, ntee_code=ntee_code) + + return { + "message": f"Ingested {len(orgs)} nonprofits for {state}", + "state": state, + "ntee_code": ntee_code, + "count": len(orgs), + "cache_location": "data/cache/nonprofits" + } + + except Exception as e: + logger.error(f"Nonprofit ingestion error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/health") +async def health_check(): + """Health check endpoint for monitoring.""" + return { + "status": "healthy", + "timestamp": datetime.utcnow().isoformat(), + "version": "1.0.0" + } + + +@app.get("/api/health") +async def api_health_check(): + """Health check endpoint for monitoring (API path).""" + return { + "status": "healthy", + "timestamp": datetime.utcnow().isoformat(), + "version": "1.0.0" + } + + +@app.post("/admin/initialize") +async def initialize_system(): + """Initialize Delta Lake tables and system components.""" + try: + pipeline.initialize_tables() + + return { + "status": "success", + "message": "System initialized successfully" + } + + except Exception as e: + logger.error(f"Error initializing system: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +# Startup event +@app.post("/api/debate-grade") +async def grade_decision_with_debate_framework( + document_id: Optional[str] = Query(None, description="Document ID to grade"), + text: Optional[str] = Query(None, description="Text to grade directly"), + title: Optional[str] = Query("", description="Document title") +): + """ + Grade a government decision using debate framework (Harms/Solvency/Topicality). + + Translates debate concepts for laypeople: + - Harms → "The Problem": Why is this a crisis? + - Solvency → "The Fix": How does this solution work? + - Topicality → "The Scope": Does the government have authority? + + Example: /api/debate-grade?text=The city council approved funding for dental screening... + """ + try: + from agents.debate_grader import DebateGraderAgent + + grader = DebateGraderAgent() + + # Get document content + if document_id: + document = pipeline.get_document_by_id(document_id) + if not document: + raise HTTPException(status_code=404, detail="Document not found") + elif text: + document = { + "content": text, + "title": title, + "id": "custom_text" + } + else: + raise HTTPException(status_code=400, detail="Provide either document_id or text") + + # Grade the document + grade = await grader._grade_document(document) + + return { + "document_id": document.get("id"), + "title": document.get("title", ""), + "debate_grade": grade, + "explanation": { + "harms": "This measures how well the decision identifies and documents the problem using data and evidence", + "solvency": "This measures how clearly the solution is defined and whether it will actually fix the problem", + "topicality": "This measures whether the government body has the legal authority to take this action" + } + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Debate grading error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/debate-grade/batch") +async def grade_decisions_batch( + state: Optional[str] = Query(None, description="Filter by state"), + topic: Optional[str] = Query(None, description="Filter by topic"), + limit: int = Query(50, description="Number of documents to grade") +): + """ + Grade multiple government decisions using debate framework. + + Returns aggregate insights about decision quality across dimensions. + """ + try: + from agents.debate_grader import DebateGraderAgent + from agents.base import AgentMessage, MessageType, AgentRole + + grader = DebateGraderAgent() + + # Get documents to grade + documents = pipeline.query_opportunities_by_state(state, None) + + if topic: + documents = [d for d in documents if d.get("topic") == topic] + + documents = documents[:limit] + + # Create message and process + message = AgentMessage( + message_id=f"batch_grade_{datetime.utcnow().timestamp()}", + sender=AgentRole.ORCHESTRATOR, + recipient=AgentRole.DEBATE_GRADER, + message_type=MessageType.COMMAND, + payload={"documents": documents} + ) + + result = await grader.process(message) + graded_documents = result[0].payload.get("documents", []) + insights = result[0].payload.get("insights", {}) + + return { + "graded_count": len(graded_documents), + "documents": graded_documents, + "insights": insights, + "explanation": { + "average_scores": "Average scores across all three debate dimensions (out of 5)", + "strongest_dimension": "Which dimension governments perform best on", + "weakest_dimension": "Which dimension needs the most improvement" + } + } + + except Exception as e: + logger.error(f"Batch debate grading error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@app.on_event("startup") +async def startup_event(): + """Initialize system on startup with data validation.""" + logger.info("="*80) + logger.info("🚀 STARTING Open Navigator API") + logger.info("="*80) + logger.info(f"Configuration: {settings.catalog_name}.{settings.schema_name}") + logger.info(f"Log Level: {settings.log_level}") + logger.info(f"Log File: {settings.log_file}") + + # Check if running on HuggingFace Spaces + IS_HF_SPACES = os.getenv("HF_SPACES") == "1" + if IS_HF_SPACES: + logger.info(f"🤗 Running on HuggingFace Spaces") + else: + logger.info(f"💻 Running in local/standard environment") + + # Validate critical data files + logger.info("") + logger.info("📊 VALIDATING DATA AVAILABILITY...") + logger.info("-" * 80) + + data_dir = Path("data/gold") + critical_files = [] + optional_files = [] + + # Check reference data (critical) + reference_checks = [ + "reference/jurisdictions_cities.parquet", + "reference/jurisdictions_counties.parquet", + "reference/causes_ntee_codes.parquet", + ] + + for file_pattern in reference_checks: + file_path = data_dir / file_pattern + if file_path.exists(): + size_mb = file_path.stat().st_size / (1024 * 1024) + try: + import pandas as pd + df = pd.read_parquet(file_path) + logger.info(f" ✅ {file_pattern}: {len(df):,} records ({size_mb:.2f} MB)") + critical_files.append(file_pattern) + except Exception as e: + logger.error(f" ❌ {file_pattern}: ERROR - {e}") + else: + logger.warning(f" ⚠️ {file_pattern}: NOT FOUND") + + # Check state data (optional - shows what's available) + logger.info("") + logger.info("📍 STATE DATA AVAILABILITY:") + + states_dir = data_dir / "states" + if states_dir.exists(): + state_dirs = sorted([d for d in states_dir.iterdir() if d.is_dir()]) + states_with_data = [] + + for state_dir in state_dirs[:10]: # Show first 10 states + state = state_dir.name + files_found = [] + + # Check for key files + key_files = [ + "nonprofits_organizations.parquet", + "contacts_officials.parquet", + "events.parquet", + ] + + for filename in key_files: + file_path = state_dir / filename + if file_path.exists(): + files_found.append(filename.split('.')[0].split('_')[-1]) + + if files_found: + logger.info(f" ✅ {state}: {', '.join(files_found)}") + states_with_data.append(state) + + total_states = len(state_dirs) + if total_states > 10: + logger.info(f" ... and {total_states - 10} more states") + + logger.info(f"") + logger.info(f" 📊 Total states with data: {total_states}") + else: + logger.warning(" ⚠️ No state data directory found") + + # Validate HuggingFace datasets if running on HF Spaces + if IS_HF_SPACES: + logger.info("") + logger.info("🤗 VALIDATING HUGGINGFACE DATASETS...") + logger.info("-" * 80) + + # Check a sample of critical datasets + import requests + from api.routes.bills import get_hf_dataset_url + + test_datasets = [ + ("states-ma-bills-bills", "Massachusetts Bills"), + ("states-al-bills-bills", "Alabama Bills"), + ("states-ma-contacts-local-officials", "Massachusetts Local Officials"), + ] + + hf_datasets_ok = 0 + for dataset_name, display_name in test_datasets: + url = get_hf_dataset_url(dataset_name) + try: + response = requests.head(url, timeout=10, allow_redirects=True) + if response.status_code == 200: + logger.info(f" ✅ {display_name}: Accessible") + hf_datasets_ok += 1 + else: + logger.error(f" ❌ {display_name}: HTTP {response.status_code}") + logger.error(f" URL: {url}") + except Exception as e: + logger.error(f" ❌ {display_name}: {type(e).__name__} - {e}") + logger.error(f" URL: {url}") + + logger.info("") + logger.info(f" 📊 HuggingFace datasets validated: {hf_datasets_ok}/{len(test_datasets)}") + + if hf_datasets_ok < len(test_datasets): + logger.warning(" ⚠️ Some datasets are not accessible - API may have limited functionality") + + logger.info("") + logger.info("="*80) + logger.info(f"✅ API READY - {len(critical_files)}/{len(reference_checks)} critical files available") + if IS_HF_SPACES: + logger.info(f"✅ HuggingFace datasets validated") + logger.info("="*80) + logger.info("") + + +# Shutdown event +@app.on_event("shutdown") +async def shutdown_event(): + """Cleanup on shutdown.""" + logger.info("Shutting down Oral Health Policy Pulse API") + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run( + app, + host=settings.api_host, + port=settings.api_port, + workers=settings.api_workers + ) diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000000000000000000000000000000000000..b601e66f9ed9aaf3d738c7ce14ee6c532e7fdfce --- /dev/null +++ b/api/models.py @@ -0,0 +1,268 @@ +""" +Database models for authentication, user management, and social features +""" +from datetime import datetime +from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, ForeignKey, UniqueConstraint, Float +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship + +Base = declarative_base() + + +class User(Base): + """User account model""" + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + email = Column(String(255), unique=True, index=True, nullable=False) + username = Column(String(100), unique=True, index=True, nullable=True) + full_name = Column(String(255), nullable=True) + avatar_url = Column(String(500), nullable=True) + + # OAuth provider info + oauth_provider = Column(String(50), nullable=True) # 'huggingface', 'google', 'facebook', 'github' + oauth_id = Column(String(255), nullable=True) # Provider-specific user ID + + # Authentication + hashed_password = Column(String(255), nullable=True) # For email/password (optional) + is_active = Column(Boolean, default=True) + is_verified = Column(Boolean, default=False) + + # Location preferences + state = Column(String(100), nullable=True) # US State + county = Column(String(100), nullable=True) # County + city = Column(String(100), nullable=True) # City + school_board = Column(String(255), nullable=True) # School board/district + + # Profile completion + profile_completed = Column(Boolean, default=False) + + # Timestamps + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + last_login = Column(DateTime, nullable=True) + + # User preferences (JSON stored as text) + preferences = Column(Text, nullable=True) + + def __repr__(self): + return f"" + + +class OAuthState(Base): + """Temporary storage for OAuth state tokens (CSRF protection)""" + __tablename__ = "oauth_states" + + id = Column(Integer, primary_key=True, index=True) + state_token = Column(String(255), unique=True, index=True, nullable=False) + provider = Column(String(50), nullable=False) + redirect_uri = Column(String(500), nullable=True) + created_at = Column(DateTime, default=datetime.utcnow) + expires_at = Column(DateTime, nullable=False) + + def __repr__(self): + return f"" + + +# ============================================================================ +# SOCIAL FEATURES MODELS +# ============================================================================ + +class Organization(Base): + """Organizations (nonprofits, charities, government agencies, advocacy groups)""" + __tablename__ = "organizations" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(255), nullable=False, index=True) + slug = Column(String(255), unique=True, index=True, nullable=False) # URL-friendly identifier + description = Column(Text, nullable=True) + logo_url = Column(String(500), nullable=True) + website = Column(String(500), nullable=True) + + # Organization type + org_type = Column(String(50), nullable=True) # 'nonprofit', 'government', 'advocacy', 'charity' + + # Location + state = Column(String(100), nullable=True) + county = Column(String(100), nullable=True) + city = Column(String(100), nullable=True) + address = Column(Text, nullable=True) + + # Contact + email = Column(String(255), nullable=True) + phone = Column(String(50), nullable=True) + + # Nonprofit-specific (from IRS/ProPublica) + ein = Column(String(20), nullable=True, index=True) # Employer Identification Number + ntee_code = Column(String(10), nullable=True) # National Taxonomy of Exempt Entities + revenue = Column(Float, nullable=True) + + # Social stats + follower_count = Column(Integer, default=0) + + # Verification + is_verified = Column(Boolean, default=False) + verified_at = Column(DateTime, nullable=True) + + # Timestamps + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f"" + + +class Cause(Base): + """Causes/Topics/Issues (oral health, housing, education, climate, etc.)""" + __tablename__ = "causes" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(255), nullable=False, index=True) + slug = Column(String(255), unique=True, index=True, nullable=False) + description = Column(Text, nullable=True) + icon_url = Column(String(500), nullable=True) + color = Column(String(7), nullable=True) # Hex color code + + # Category + category = Column(String(100), nullable=True) # 'health', 'education', 'housing', 'environment', etc. + + # Social stats + follower_count = Column(Integer, default=0) + + # Timestamps + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f"" + + +class Official(Base): + """Public officials (elected, appointed) - renamed from Leader to match OpenStates""" + __tablename__ = "officials" + + id = Column(Integer, primary_key=True, index=True) + ocd_person_id = Column(String(255), unique=True, index=True, nullable=True) # OpenCivicData ID + name = Column(String(255), nullable=False, index=True) + slug = Column(String(255), unique=True, index=True, nullable=False) + family_name = Column(String(100), nullable=True) + given_name = Column(String(100), nullable=True) + sort_name = Column(String(255), nullable=True) + + # Bio and presentation + title = Column(String(255), nullable=True) # 'Mayor', 'State Senator', 'City Council Member' + bio = Column(Text, nullable=True) + photo_url = Column(String(500), nullable=True) + gender = Column(String(20), nullable=True) + birth_date = Column(DateTime, nullable=True) + + # Current role (primary position) + position_type = Column(String(100), nullable=True) # 'elected', 'appointed' + office = Column(String(255), nullable=True) # 'Office of the Mayor', 'State Senate District 12' + party = Column(String(100), nullable=True) # 'Democratic', 'Republican', 'Independent' + chamber = Column(String(50), nullable=True) # 'upper', 'lower', 'executive' + district = Column(String(50), nullable=True) # District number or name + + # Location/Jurisdiction + state = Column(String(100), nullable=True) + county = Column(String(100), nullable=True) + city = Column(String(100), nullable=True) + jurisdiction = Column(String(255), nullable=True) + + # Contact + email = Column(String(255), nullable=True) + phone = Column(String(50), nullable=True) + website = Column(String(500), nullable=True) + + # Social media + twitter = Column(String(255), nullable=True) + linkedin = Column(String(255), nullable=True) + facebook = Column(String(255), nullable=True) + + # Social stats + follower_count = Column(Integer, default=0) + + # Verification + is_verified = Column(Boolean, default=False) + verified_at = Column(DateTime, nullable=True) + + # Term dates + term_start_date = Column(DateTime, nullable=True) + term_end_date = Column(DateTime, nullable=True) + is_current = Column(Boolean, default=True) + + # Timestamps + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def __repr__(self): + return f"" + + +# ============================================================================ +# FOLLOW RELATIONSHIPS (Many-to-Many) +# ============================================================================ + +class UserFollow(Base): + """User following another user""" + __tablename__ = "user_follows" + __table_args__ = ( + UniqueConstraint('follower_id', 'following_id', name='unique_user_follow'), + ) + + id = Column(Integer, primary_key=True, index=True) + follower_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) + following_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) + created_at = Column(DateTime, default=datetime.utcnow) + + def __repr__(self): + return f" {self.following_id}>" + + +class OfficialFollow(Base): + """User following an official (renamed from LeaderFollow)""" + __tablename__ = "official_follows" + __table_args__ = ( + UniqueConstraint('user_id', 'official_id', name='unique_official_follow'), + ) + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) + official_id = Column(Integer, ForeignKey('officials.id', ondelete='CASCADE'), nullable=False, index=True) + created_at = Column(DateTime, default=datetime.utcnow) + + def __repr__(self): + return f" official:{self.official_id}>" + + +class OrganizationFollow(Base): + """User following an organization""" + __tablename__ = "organization_follows" + __table_args__ = ( + UniqueConstraint('user_id', 'organization_id', name='unique_org_follow'), + ) + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) + organization_id = Column(Integer, ForeignKey('organizations.id', ondelete='CASCADE'), nullable=False, index=True) + created_at = Column(DateTime, default=datetime.utcnow) + + def __repr__(self): + return f" org:{self.organization_id}>" + + +class CauseFollow(Base): + """User following a cause/topic""" + __tablename__ = "cause_follows" + __table_args__ = ( + UniqueConstraint('user_id', 'cause_id', name='unique_cause_follow'), + ) + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) + cause_id = Column(Integer, ForeignKey('causes.id', ondelete='CASCADE'), nullable=False, index=True) + created_at = Column(DateTime, default=datetime.utcnow) + + def __repr__(self): + return f" cause:{self.cause_id}>" + diff --git a/api/routes/__init__.py b/api/routes/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5e3fe99f43060c538b681f5dcdb1658b35359e27 --- /dev/null +++ b/api/routes/__init__.py @@ -0,0 +1,3 @@ +""" +API routes package +""" diff --git a/api/routes/auth.py b/api/routes/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..1573e6b21488f04adc4b04b19647eac5bccfc51e --- /dev/null +++ b/api/routes/auth.py @@ -0,0 +1,436 @@ +""" +OAuth authentication routes - HuggingFace, Google, Facebook, GitHub +""" +import os +import httpx +from datetime import datetime, timedelta +from typing import Optional +from urllib.parse import urlencode +from dotenv import load_dotenv + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.responses import RedirectResponse +from sqlalchemy.orm import Session +from pydantic import BaseModel + +from api.database import get_db +from api.models import User, OAuthState +from api.auth import create_access_token, generate_state_token + +# Load environment variables from .env file +load_dotenv() + +router = APIRouter(prefix="/auth", tags=["authentication"]) + +# OAuth provider configurations +OAUTH_PROVIDERS = { + 'huggingface': { + 'authorize_url': 'https://huggingface.co/oauth/authorize', + 'token_url': 'https://huggingface.co/oauth/token', + 'userinfo_url': 'https://huggingface.co/api/whoami-v2', + 'scope': 'openid profile email', + 'client_id_env': 'HUGGINGFACE_CLIENT_ID', + 'client_secret_env': 'HUGGINGFACE_CLIENT_SECRET', + }, + 'google': { + 'authorize_url': 'https://accounts.google.com/o/oauth2/v2/auth', + 'token_url': 'https://oauth2.googleapis.com/token', + 'userinfo_url': 'https://www.googleapis.com/oauth2/v2/userinfo', + 'scope': 'openid email profile', + 'client_id_env': 'GOOGLE_CLIENT_ID', + 'client_secret_env': 'GOOGLE_CLIENT_SECRET', + }, + 'facebook': { + 'authorize_url': 'https://www.facebook.com/v18.0/dialog/oauth', + 'token_url': 'https://graph.facebook.com/v18.0/oauth/access_token', + 'userinfo_url': 'https://graph.facebook.com/me?fields=id,name,email,picture', + 'scope': 'email public_profile', + 'client_id_env': 'FACEBOOK_APP_ID', + 'client_secret_env': 'FACEBOOK_APP_SECRET', + }, + 'github': { + 'authorize_url': 'https://github.com/login/oauth/authorize', + 'token_url': 'https://github.com/login/oauth/access_token', + 'userinfo_url': 'https://api.github.com/user', + 'scope': 'user:email', + 'client_id_env': 'GITHUB_CLIENT_ID', + 'client_secret_env': 'GITHUB_CLIENT_SECRET', + }, +} + + +# Response models +class TokenResponse(BaseModel): + access_token: str + token_type: str = "bearer" + user: dict + + +class UserResponse(BaseModel): + id: int + email: str + username: Optional[str] + full_name: Optional[str] + avatar_url: Optional[str] + oauth_provider: Optional[str] + state: Optional[str] + county: Optional[str] + city: Optional[str] + school_board: Optional[str] + profile_completed: Optional[bool] + + +# Helper functions +def get_or_create_user( + db: Session, + email: str, + provider: str, + oauth_id: str, + full_name: Optional[str] = None, + avatar_url: Optional[str] = None, + username: Optional[str] = None, +) -> User: + """Get existing user or create new one from OAuth data""" + + # Try to find existing user by OAuth ID first + user = db.query(User).filter( + User.oauth_provider == provider, + User.oauth_id == oauth_id + ).first() + + if user: + # Update user info if changed + user.full_name = full_name or user.full_name + user.avatar_url = avatar_url or user.avatar_url + user.username = username or user.username + user.last_login = datetime.utcnow() + db.commit() + return user + + # Try to find by email + user = db.query(User).filter(User.email == email).first() + + if user: + # Link OAuth account to existing user + user.oauth_provider = provider + user.oauth_id = oauth_id + user.full_name = full_name or user.full_name + user.avatar_url = avatar_url or user.avatar_url + user.username = username or user.username + user.last_login = datetime.utcnow() + user.is_verified = True # OAuth emails are verified + db.commit() + return user + + # Create new user + user = User( + email=email, + username=username, + full_name=full_name, + avatar_url=avatar_url, + oauth_provider=provider, + oauth_id=oauth_id, + is_verified=True, + is_active=True, + last_login=datetime.utcnow(), + ) + db.add(user) + db.commit() + db.refresh(user) + return user + + +# OAuth routes +@router.get("/login/{provider}") +async def oauth_login( + provider: str, + request: Request, + db: Session = Depends(get_db), + redirect_uri: Optional[str] = None +): + """ + Initiate OAuth login flow + + Supported providers: huggingface, google, facebook, github + """ + if provider not in OAUTH_PROVIDERS: + raise HTTPException(status_code=400, detail=f"Unsupported provider: {provider}") + + config = OAUTH_PROVIDERS[provider] + client_id = os.getenv(config['client_id_env']) + + if not client_id: + raise HTTPException( + status_code=500, + detail=f"OAuth not configured for {provider}. Missing {config['client_id_env']}" + ) + + # Generate state token for CSRF protection + state = generate_state_token() + + # Store state in database + oauth_state = OAuthState( + state_token=state, + provider=provider, + redirect_uri=redirect_uri, + expires_at=datetime.utcnow() + timedelta(minutes=10), + ) + db.add(oauth_state) + db.commit() + + # Build callback URL using API_BASE_URL to ensure correct protocol (http vs https) + base_url = os.getenv('API_BASE_URL', 'http://localhost:8000') + callback_url = f"{base_url}/auth/callback/{provider}" + + # Build authorization URL + params = { + 'client_id': client_id, + 'redirect_uri': callback_url, + 'scope': config['scope'], + 'state': state, + 'response_type': 'code', + } + + auth_url = f"{config['authorize_url']}?{urlencode(params)}" + return RedirectResponse(url=auth_url) + + +@router.get("/callback/{provider}", name="oauth_callback") +async def oauth_callback( + provider: str, + code: Optional[str] = None, + state: Optional[str] = None, + error: Optional[str] = None, + db: Session = Depends(get_db) +): + """OAuth callback handler""" + + if error: + raise HTTPException(status_code=400, detail=f"OAuth error: {error}") + + if not code or not state: + raise HTTPException(status_code=400, detail="Missing code or state parameter") + + if provider not in OAUTH_PROVIDERS: + raise HTTPException(status_code=400, detail=f"Unsupported provider: {provider}") + + # Verify state token + oauth_state = db.query(OAuthState).filter( + OAuthState.state_token == state, + OAuthState.provider == provider + ).first() + + if not oauth_state or oauth_state.expires_at < datetime.utcnow(): + raise HTTPException(status_code=400, detail="Invalid or expired state token") + + config = OAUTH_PROVIDERS[provider] + client_id = os.getenv(config['client_id_env']) + client_secret = os.getenv(config['client_secret_env']) + + # Build callback URL (must match the one sent to authorize) + from fastapi import Request + # We need to reconstruct the callback URL - for now use a simple approach + base_url = os.getenv('API_BASE_URL', 'http://localhost:8000') + callback_url = f"{base_url}/auth/callback/{provider}" + + # Exchange code for access token + async with httpx.AsyncClient() as client: + token_response = await client.post( + config['token_url'], + data={ + 'client_id': client_id, + 'client_secret': client_secret, + 'code': code, + 'redirect_uri': callback_url, + 'grant_type': 'authorization_code', + }, + headers={'Accept': 'application/json'} + ) + + if token_response.status_code != 200: + raise HTTPException( + status_code=400, + detail=f"Token exchange failed: {token_response.text}" + ) + + token_data = token_response.json() + access_token = token_data.get('access_token') + + if not access_token: + raise HTTPException(status_code=400, detail="No access token received") + + # Get user info from provider + user_info = await get_user_info(provider, access_token, config) + + if not user_info or not user_info.get('email'): + raise HTTPException(status_code=400, detail="Could not retrieve user email from provider") + + # Get or create user + user = get_or_create_user( + db=db, + email=user_info['email'], + provider=provider, + oauth_id=user_info['oauth_id'], + full_name=user_info.get('full_name'), + avatar_url=user_info.get('avatar_url'), + username=user_info.get('username'), + ) + + # Clean up state token + db.delete(oauth_state) + db.commit() + + # Create JWT token (sub must be string, not int) + jwt_token = create_access_token(data={"sub": str(user.id)}) + + # Redirect to frontend with token + # On HuggingFace/production, frontend and backend are same domain - use relative path + # On local dev, frontend is separate server - use FRONTEND_URL + frontend_url = os.getenv('FRONTEND_URL', '') + + # If FRONTEND_URL is localhost or not set, assume same-domain deployment (HuggingFace) + if not frontend_url or 'localhost' in frontend_url: + # Use relative redirect (works on HuggingFace where both are same domain) + redirect_url = oauth_state.redirect_uri or '/' + else: + # Use absolute URL for separate frontend server + redirect_url = oauth_state.redirect_uri or frontend_url + + # Append token as URL parameter + params = urlencode({'token': jwt_token}) + full_redirect_url = f"{redirect_url}?{params}" if '?' not in redirect_url else f"{redirect_url}&{params}" + + return RedirectResponse(url=full_redirect_url) + + +async def get_user_info(provider: str, access_token: str, config: dict) -> dict: + """Get user information from OAuth provider""" + + async with httpx.AsyncClient() as client: + headers = {'Authorization': f'Bearer {access_token}'} + + user_info = {} + + if provider == 'huggingface': + resp = await client.get(config['userinfo_url'], headers=headers) + data = resp.json() + user_info = { + 'email': data.get('email'), + 'oauth_id': str(data.get('id')), + 'full_name': data.get('fullname') or data.get('name'), + 'avatar_url': data.get('avatarUrl'), + 'username': data.get('name'), + } + + elif provider == 'google': + resp = await client.get(config['userinfo_url'], headers=headers) + data = resp.json() + user_info = { + 'email': data.get('email'), + 'oauth_id': data.get('id'), + 'full_name': data.get('name'), + 'avatar_url': data.get('picture'), + 'username': data.get('email', '').split('@')[0], + } + + elif provider == 'facebook': + resp = await client.get(config['userinfo_url'], headers={'Authorization': f'Bearer {access_token}'}) + data = resp.json() + user_info = { + 'email': data.get('email'), + 'oauth_id': str(data.get('id')), + 'full_name': data.get('name'), + 'avatar_url': data.get('picture', {}).get('data', {}).get('url') if isinstance(data.get('picture'), dict) else None, + 'username': data.get('name', '').replace(' ', '_').lower(), + } + + elif provider == 'github': + # Get user profile + resp = await client.get(config['userinfo_url'], headers=headers) + data = resp.json() + + # Get user email if not public + email = data.get('email') + if not email: + resp_emails = await client.get('https://api.github.com/user/emails', headers=headers) + emails = resp_emails.json() + email = next((e['email'] for e in emails if e.get('primary')), emails[0]['email'] if emails else None) + + user_info = { + 'email': email, + 'oauth_id': str(data.get('id')), + 'full_name': data.get('name'), + 'avatar_url': data.get('avatar_url'), + 'username': data.get('login'), + } + + return user_info + + +@router.get("/me", response_model=UserResponse) +def get_current_user_info( + request: Request, + db: Session = Depends(get_db) +): + """Get current authenticated user info""" + + # Get token from Authorization header + auth_header = request.headers.get('Authorization') + if not auth_header or not auth_header.startswith('Bearer '): + raise HTTPException(status_code=401, detail="Not authenticated") + + token = auth_header.split(' ')[1] + + # Decode token and get user + from api.auth import decode_access_token + payload = decode_access_token(token) + user_id = int(payload.get('sub')) # Convert back to int for DB query + + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + + return user + + +@router.patch("/profile", response_model=UserResponse) +def update_user_profile( + profile_data: dict, + request: Request, + db: Session = Depends(get_db) +): + """Update user profile (location preferences)""" + + # Get token from Authorization header + auth_header = request.headers.get('Authorization') + if not auth_header or not auth_header.startswith('Bearer '): + raise HTTPException(status_code=401, detail="Not authenticated") + + token = auth_header.split(' ')[1] + + # Decode token and get user + from api.auth import decode_access_token + payload = decode_access_token(token) + user_id = int(payload.get('sub')) + + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + + # Update allowed fields + allowed_fields = ['state', 'county', 'city', 'school_board', 'profile_completed'] + for field, value in profile_data.items(): + if field in allowed_fields and hasattr(user, field): + setattr(user, field, value) + + user.updated_at = datetime.utcnow() + db.commit() + db.refresh(user) + + return user + + +@router.post("/logout") +def logout(): + """Logout endpoint (client-side token removal)""" + return {"message": "Logged out successfully. Please remove the token from client."} diff --git a/api/routes/bills.py b/api/routes/bills.py new file mode 100644 index 0000000000000000000000000000000000000000..b3743b4fa7c1582db86e37e2a15e5a22a17e34c1 --- /dev/null +++ b/api/routes/bills.py @@ -0,0 +1,841 @@ +""" +Bills API Routes - Legislative bill data from OpenStates +""" +from fastapi import APIRouter, Query, HTTPException +from fastapi.responses import JSONResponse +from typing import Optional, List, Dict +import duckdb +import pandas as pd +from pathlib import Path +from loguru import logger +import re +import os +import sys +import traceback + +from api.errors import ErrorDetail, parse_error +from api.routes.search import load_parquet_cached + +router = APIRouter(prefix="/api/bills", tags=["bills"]) + +GOLD_DIR = Path("data/gold") +IS_HF_SPACES = os.getenv("HF_SPACES") == "1" +HF_ORGANIZATION = "CommunityOne" + +def get_hf_dataset_url(dataset_name: str) -> str: + """ + Convert dataset name to HuggingFace parquet URL. + + HuggingFace Datasets library stores parquet files in the standard format: + data/train-00000-of-00001.parquet + + Args: + dataset_name: Dataset name (e.g., 'states-ma-bills-bills') + + Returns: + Full URL to the parquet file + """ + return f"https://huggingface.co/datasets/{HF_ORGANIZATION}/{dataset_name}/resolve/main/data/train-00000-of-00001.parquet" + +def get_data_source(file_path: Path, use_remote: bool = False) -> str: + """Get data source (local path or remote URL) based on environment.""" + if not IS_HF_SPACES and not use_remote: + return str(file_path) + + # Convert local path to HuggingFace dataset name + parts = file_path.parts + + if 'states' in parts: + state_idx = parts.index('states') + state = parts[state_idx + 1].lower() + filename = parts[-1].replace('.parquet', '').replace('_', '-') + dataset_name = f"states-{state}-{filename}" + return get_hf_dataset_url(dataset_name) + + # Fallback to local + return str(file_path) + + +def classify_bill_type(title: str, classification: list, topic: Optional[str] = None) -> str: + """ + Classify bill based on topic-specific categories. + + Different topics use different classification schemes: + - Fluoridation: mandate, removal, funding, study + - Dental/Oral Health: coverage_expansion, screening, provider_access, funding + - Medicaid: expansion, coverage, reimbursement, eligibility + - Health (general): protection, restriction, funding, reform + - Education: requirement, funding, curriculum, reform + - Default: support, oppose, regulate, other + """ + title_lower = title.lower() + topic_lower = topic.lower() if topic else "" + + # EXCEPTION: Fluoride varnish/dental coverage bills (not water fluoridation) + # Check this BEFORE water fluoridation classification + if any(word in title_lower for word in ['varnish', 'sealant', 'dental', 'medicaid', 'medical assistance']) and 'fluoride' in title_lower: + if any(word in title_lower for word in ['coverage', 'expand', 'expansion', 'benefit']): + return 'coverage_expansion' + elif any(word in title_lower for word in ['screening', 'examination', 'check']): + return 'screening' + # If it mentions dental/varnish but unclear type, it's dental "other" not fluoridation + return 'other' + + # Fluoridation-specific classifications (WATER fluoridation only) + if 'fluoride' in topic_lower or 'fluoride' in title_lower: + # FIRST: Check for REMOVAL/BAN/PROHIBITION (negative sentiment) + # CRITICAL: Must check these BEFORE "mandate"/"require" to avoid misclassification + # e.g., "prohibit fluoride" should be "removal", not "mandate" + if any(word in title_lower for word in [ + 'prohibit', 'prohibition', 'prohibited', 'prohibiting', + 'ban', 'banning', 'banned', + 'discontinue', 'discontinuation', + 'cease', 'ceasing', + 'eliminate', 'elimination', + 'removal', 'remove', 'removing', + 'prevent', 'preventing', + 'repeal', 'repealing', 'repealed', + 'optional', 'opt-out', 'opt out', + 'fluoride-free', 'fluoride free' + ]): + # But check if it's "prohibit removal" (double negative = pro-fluoride) + if any(phrase in title_lower for phrase in ['prohibit removal', 'prevent removal', 'ban removal']): + return 'mandate' # Prohibiting removal = mandate to keep + return 'removal' + + # SECOND: Check for notification/monitoring (before "require" check) + # Bills like "notification required" are about monitoring, not mandating fluoridation + elif any(phrase in title_lower for phrase in [ + 'notification', 'notify', 'notifying', + 'report to', 'reporting', 'report when', + 'monitor', 'monitoring' + ]): + return 'study' + + # THIRD: Check for MANDATE/REQUIRE (positive sentiment) + # Be specific - just "require" alone isn't enough, need context + elif any(phrase in title_lower for phrase in [ + 'mandate', 'mandating', 'shall fluoridate', 'shall add fluoride', + 'must fluoridate', 'must add fluoride', + 'require fluoridation', 'require water system to fluoridate', + 'require addition of fluoride' + ]): + return 'mandate' + + # FOURTH: Check for funding + elif any(word in title_lower for word in ['fund', 'funding', 'appropriation', 'grant', 'reimburse', 'subsidy']): + return 'funding' + elif any(word in title_lower for word in ['study', 'research', 'analysis', 'assess', 'evaluate']): + return 'study' + else: + return 'other' + + # Dental/Oral Health-specific classifications + elif 'dental' in topic_lower or 'oral health' in topic_lower or 'dental' in title_lower: + if any(word in title_lower for word in ['expand', 'increase coverage', 'extend coverage', 'add coverage']): + return 'coverage_expansion' + elif any(word in title_lower for word in ['screen', 'examination', 'checkup', 'assessment']): + return 'screening' + elif any(word in title_lower for word in ['provider', 'dentist', 'hygienist', 'workforce', 'professional']): + return 'provider_access' + elif any(word in title_lower for word in ['fund', 'appropriation', 'grant', 'budget', 'reimburse']): + return 'funding' + else: + return 'other' + + # Medicaid-specific classifications + elif 'medicaid' in topic_lower or 'medicaid' in title_lower: + if any(word in title_lower for word in ['expand', 'expansion', 'extend', 'broaden']): + return 'expansion' + elif any(word in title_lower for word in ['coverage', 'benefit', 'service']): + return 'coverage' + elif any(word in title_lower for word in ['reimburse', 'payment', 'rate', 'compensation']): + return 'reimbursement' + elif any(word in title_lower for word in ['eligib', 'qualify', 'enroll']): + return 'eligibility' + else: + return 'other' + + # Education-specific classifications + elif 'education' in topic_lower or 'school' in topic_lower: + if any(word in title_lower for word in ['require', 'mandate', 'shall provide', 'must offer']): + return 'requirement' + elif any(word in title_lower for word in ['fund', 'appropriation', 'grant', 'budget']): + return 'funding' + elif any(word in title_lower for word in ['curriculum', 'course', 'instruction', 'program']): + return 'curriculum' + elif any(word in title_lower for word in ['reform', 'restructure', 'modernize', 'improve']): + return 'reform' + else: + return 'other' + + # General health classifications + elif 'health' in topic_lower or 'health' in title_lower: + if any(word in title_lower for word in ['protect', 'preserve', 'safeguard', 'ensure', 'guarantee', 'expand', 'increase', 'enhance', 'support']): + return 'protection' + elif any(word in title_lower for word in ['restrict', 'limit', 'regulate', 'control', 'impose', 'prohibit', 'ban']): + return 'restriction' + elif any(word in title_lower for word in ['fund', 'appropriation', 'grant', 'budget']): + return 'funding' + elif any(word in title_lower for word in ['reform', 'restructure', 'modernize', 'improve']): + return 'reform' + else: + return 'other' + + # Default general classifications + else: + if any(word in title_lower for word in ['support', 'promote', 'encourage', 'expand', 'increase', 'enhance', 'fund']): + return 'support' + elif any(word in title_lower for word in ['oppose', 'prohibit', 'ban', 'restrict', 'limit', 'prevent']): + return 'oppose' + elif any(word in title_lower for word in ['regulate', 'oversee', 'control', 'require', 'mandate']): + return 'regulate' + else: + return 'other' + + +def get_legend_for_topic(topic: Optional[str]) -> dict: + """ + Get appropriate legend labels based on topic. + """ + topic_lower = topic.lower() if topic else "" + + if 'fluoride' in topic_lower: + return { + "mandate": "Mandate Fluoridation", + "removal": "Remove Fluoridation", + "funding": "Funding/Grants", + "study": "Study/Research", + "other": "Other" + } + elif 'dental' in topic_lower or 'oral health' in topic_lower: + return { + "coverage_expansion": "Coverage Expansion", + "screening": "Screening Programs", + "provider_access": "Provider Access", + "funding": "Funding/Grants", + "other": "Other" + } + elif 'medicaid' in topic_lower: + return { + "expansion": "Program Expansion", + "coverage": "Coverage/Benefits", + "reimbursement": "Reimbursement", + "eligibility": "Eligibility", + "other": "Other" + } + elif 'education' in topic_lower or 'school' in topic_lower: + return { + "requirement": "Requirements", + "funding": "Funding", + "curriculum": "Curriculum", + "reform": "Reform", + "other": "Other" + } + elif 'health' in topic_lower: + return { + "protection": "Protection/Expansion", + "restriction": "Restriction", + "funding": "Funding", + "reform": "Reform", + "other": "Other" + } + else: + return { + "support": "Support/Promote", + "oppose": "Oppose/Restrict", + "regulate": "Regulate", + "other": "Other" + } + + +def determine_bill_status(latest_action: str, latest_date: str) -> str: + """ + Determine if bill was enacted, failed, or is pending. + """ + if not latest_action: + return 'pending' + + action_lower = latest_action.lower() + + # Enacted/Passed + if any(word in action_lower for word in ['signed', 'enacted', 'approved', 'passed both', 'became law']): + return 'enacted' + + # Failed + if any(word in action_lower for word in ['failed', 'defeated', 'rejected', 'died', 'withdrawn', 'vetoed']): + return 'failed' + + # Pending (default) + return 'pending' + + +@router.get("") +async def search_bills( + q: Optional[str] = Query(None, description="Search query for bill title or number"), + state: Optional[str] = Query("AL", description="State code (e.g., AL, GA, MA)"), + session: Optional[str] = Query(None, description="Legislative session (e.g., 2024rs)"), + limit: int = Query(20, ge=1, le=100, description="Maximum results"), + offset: int = Query(0, ge=0, description="Number of results to skip") +): + """ + Search legislative bills from OpenStates data. + + **Examples:** + - `/api/bills?state=AL&q=dental` - Search Alabama bills for "dental" + - `/api/bills?state=AL&session=2024rs` - Get all 2024 regular session bills + - `/api/bills?state=AL&limit=50` - Browse recent Alabama bills + """ + try: + # Build file path + bills_file = GOLD_DIR / "states" / state / "bills_bills.parquet" + + # Get data source (local or remote HuggingFace URL) + data_source = get_data_source(bills_file, use_remote=IS_HF_SPACES) + + # Connect to DuckDB + conn = duckdb.connect() + + # Build SQL query + where_clauses = [] + params = [] + + if q: + where_clauses.append("(LOWER(title) LIKE LOWER(?) OR LOWER(bill_number) LIKE LOWER(?))") + pattern = f'%{q}%' + params.extend([pattern, pattern]) + + if session: + where_clauses.append("session = ?") + params.append(session) + + where_clause = " AND ".join(where_clauses) if where_clauses else "1=1" + + # Count total + count_sql = f""" + SELECT COUNT(*) as total + FROM read_parquet(?) + WHERE {where_clause} + """ + count_params = [data_source] + params + total = conn.execute(count_sql, count_params).fetchone()[0] + + # Fetch bills + sql = f""" + SELECT + bill_id, + bill_number, + title, + classification, + session, + session_name, + first_action_date, + latest_action_date, + latest_action_description, + jurisdiction_name + FROM read_parquet(?) + WHERE {where_clause} + ORDER BY latest_action_date DESC NULLS LAST, bill_number DESC + LIMIT ? OFFSET ? + """ + + query_params = [data_source] + params + [limit, offset] + rows = conn.execute(sql, query_params).fetchall() + + bills = [] + for row in rows: + bills.append({ + "bill_id": row[0], + "bill_number": row[1], + "title": row[2], + "classification": row[3], + "session": row[4], + "session_name": row[5], + "first_action_date": row[6], + "latest_action_date": row[7], + "latest_action": row[8], + "jurisdiction": row[9] + }) + + conn.close() + + return { + "total": total, + "bills": bills, + "pagination": { + "limit": limit, + "offset": offset, + "has_more": offset + len(bills) < total + }, + "filters": { + "state": state, + "query": q, + "session": session + } + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Bills search error for state={state}: {e}") + + # Parse error into structured response + error_detail = parse_error(e, context={ + "state": state, + "data_type": "bills", + "query": q, + "session": session + }) + + # Return structured error response + return JSONResponse( + status_code=500, + content=error_detail.model_dump() + ) + + +@router.get("/sessions") +async def get_sessions( + state: str = Query("AL", description="State code") +): + """Get available legislative sessions for a state.""" + try: + bills_file = GOLD_DIR / "states" / state / "bills_bills.parquet" + + if not bills_file.exists(): + raise HTTPException( + status_code=404, + detail=f"No bills data found for state: {state}" + ) + + conn = duckdb.connect() + + sql = """ + SELECT DISTINCT + session, + session_name, + MIN(first_action_date) as start_date, + MAX(latest_action_date) as end_date, + COUNT(*) as bill_count + FROM read_parquet(?) + GROUP BY session, session_name + ORDER BY session DESC + """ + + rows = conn.execute(sql, [str(bills_file)]).fetchall() + + sessions = [] + for row in rows: + sessions.append({ + "session": row[0], + "session_name": row[1], + "start_date": row[2], + "end_date": row[3], + "bill_count": row[4] + }) + + conn.close() + + return { + "state": state, + "sessions": sessions, + "total_sessions": len(sessions) + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Sessions query error for state={state}: {e}") + + # Parse error into structured response + error_detail = parse_error(e, context={ + "state": state, + "data_type": "sessions" + }) + + return JSONResponse( + status_code=500, + content=error_detail.model_dump() + ) + + +@router.get("/map") +async def get_bill_map_data( + topic: Optional[str] = Query(None, description="Topic to filter (e.g., dental, health, education)"), + session: Optional[str] = Query(None, description="Legislative session") +): + """ + Get aggregated bill data for choropleth map visualization. + + Uses pre-computed national aggregates for instant loading. + Returns counts of bills by type and status for each state. + + **Examples:** + - `/api/bills/map?topic=fluorid` - Map fluoridation legislation + - `/api/bills/map?topic=dental` - Map dental legislation + """ + try: + # Use pre-aggregated national dataset + agg_file = GOLD_DIR / "national" / "bills_map_aggregates.parquet" + + # Fallback to on-demand aggregation if pre-computed file doesn't exist + if not agg_file.exists(): + logger.warning("Pre-aggregated bill data not found, using on-demand aggregation (slower)") + return await get_bill_map_data_on_demand(topic, session) + + # Load from cached aggregates (fast!) + df = load_parquet_cached(str(agg_file)) + + # Filter by topic + if topic: + df = df[df['topic'] == topic.lower()] + + # Convert to state_data dict + state_data = {} + + for _, row in df.iterrows(): + state_code = row['state'] + + # Reconstruct nested dicts (exclude type_status_counts which is already a dict) + type_cols = [c for c in df.columns if c.startswith('type_') and c != 'type_status_counts'] + status_cols = [c for c in df.columns if c.startswith('status_')] + + # Handle NaN values - convert to 0 + type_counts = {c.replace('type_', ''): int(row[c]) if not pd.isna(row[c]) else 0 for c in type_cols} + status_counts = {c.replace('status_', ''): int(row[c]) if not pd.isna(row[c]) else 0 for c in status_cols} + + # Extract sample_bills (stored as numpy array in parquet) + sample_bills = [] + if 'sample_bills' in row.index: + bills_data = row['sample_bills'] + # Pandas stores list columns as numpy arrays + if hasattr(bills_data, '__iter__') and not isinstance(bills_data, str): + try: + # Convert numpy array or list to Python list + sample_bills = [dict(bill) for bill in bills_data if bill] + except: + sample_bills = [] + elif isinstance(bills_data, str): + import json + try: + sample_bills = json.loads(bills_data) + except: + sample_bills = [] + + state_data[state_code] = { + "state": state_code, + "total_bills": int(row['total_bills']), + "type_counts": type_counts, + "status_counts": status_counts, + "primary_type": row['primary_type'], + "primary_status": row['primary_status'], + "map_category": row['map_category'], + "sample_bills": sample_bills, + "last_updated": str(row['last_updated']) if 'last_updated' in row.index else '' + } + + return { + "topic": topic, + "session": session, + "states": state_data, + "total_states": len(state_data), + "legend": { + "types": get_legend_for_topic(topic), + "statuses": { + "enacted": "Enacted", + "failed": "Failed", + "pending": "Pending" + } + }, + "cached": True + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Map data error: {e}") + + error_detail = parse_error(e, context={ + "data_type": "bill map", + "topic": topic, + "session": session + }) + + return JSONResponse( + status_code=500, + content=error_detail.model_dump() + ) + + +async def get_bill_map_data_on_demand( + topic: Optional[str] = None, + session: Optional[str] = None +): + """ + LEGACY: On-demand aggregation (slow - loads 50 state files). + Only used as fallback if pre-aggregated data doesn't exist. + """ + try: + # List of all US state codes to check + ALL_STATES = [ + "AL", "AK", "AZ", "AR", "CA", "CO", "CT", "DE", "FL", "GA", + "HI", "ID", "IL", "IN", "IA", "KS", "KY", "LA", "ME", "MD", + "MA", "MI", "MN", "MS", "MO", "MT", "NE", "NV", "NH", "NJ", + "NM", "NY", "NC", "ND", "OH", "OK", "OR", "PA", "RI", "SC", + "SD", "TN", "TX", "UT", "VT", "VA", "WA", "WV", "WI", "WY" + ] + + # In local environment, check available directories + # In HF Spaces, try all states (will skip missing datasets) + states_to_check = ALL_STATES + if not IS_HF_SPACES: + states_dir = GOLD_DIR / "states" + if states_dir.exists(): + states_to_check = [d.name for d in states_dir.iterdir() if d.is_dir()] + + state_data = {} + + # Iterate through states + for state_code in states_to_check: + try: + bills_file = GOLD_DIR / "states" / state_code / "bills_bills.parquet" + + # Get data source (local or remote HuggingFace URL) + data_source = get_data_source(bills_file, use_remote=IS_HF_SPACES) + + # Connect to DuckDB + conn = duckdb.connect() + + # Build query + where_clauses = ["1=1"] + params = [data_source] + + if topic: + where_clauses.append("LOWER(title) LIKE LOWER(?)") + params.append(f'%{topic}%') + + if session: + where_clauses.append("session = ?") + params.append(session) + + where_clause = " AND ".join(where_clauses) + + sql = f""" + SELECT + title, + classification, + latest_action_description + FROM read_parquet(?) + WHERE {where_clause} + """ + + rows = conn.execute(sql, params).fetchall() + conn.close() + + if not rows: + continue + + # Get topic-aware categories + legend_categories = get_legend_for_topic(topic) + + # Initialize type_counts with all possible categories for this topic + type_counts = {cat: 0 for cat in legend_categories.keys()} + status_counts = {'enacted': 0, 'failed': 0, 'pending': 0} + type_status_counts = {} + + for row in rows: + title = row[0] + classification = row[1] if row[1] else [] + latest_action = row[2] if row[2] else '' + + bill_type = classify_bill_type(title, classification, topic) + bill_status = determine_bill_status(latest_action, '') + + # Ensure bill_type exists in type_counts (fallback to 'other') + if bill_type not in type_counts: + bill_type = 'other' + + type_counts[bill_type] += 1 + status_counts[bill_status] += 1 + + # Track type+status combinations + key = f"{bill_type}_{bill_status}" + type_status_counts[key] = type_status_counts.get(key, 0) + 1 + + # Determine primary legislation type and status for map visualization + primary_type = max(type_counts, key=type_counts.get) + primary_status = max(status_counts, key=status_counts.get) + + state_data[state_code] = { + "state": state_code, + "total_bills": len(rows), + "type_counts": type_counts, + "status_counts": status_counts, + "type_status_counts": type_status_counts, + "primary_type": primary_type, + "primary_status": primary_status, + # For map visualization + "map_category": f"{primary_type}_{primary_status}" if type_counts[primary_type] > 0 else "none" + } + + except Exception as e: + # Skip states with missing or inaccessible data + logger.debug(f"Skipping state {state_code}: {str(e)}") + continue + + return { + "topic": topic, + "session": session, + "states": state_data, + "total_states": len(state_data), + "legend": { + "types": get_legend_for_topic(topic), + "statuses": { + "enacted": "Enacted", + "failed": "Failed", + "pending": "Pending" + } + } + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Map data error: {e}") + + # Parse error into structured response + error_detail = parse_error(e, context={ + "data_type": "bill map", + "topic": topic, + "session": session + }) + + return JSONResponse( + status_code=500, + content=error_detail.model_dump() + ) + + +@router.get("/{bill_id}") +async def get_bill_details(bill_id: str): + """ + Get detailed information about a specific bill from gold parquet files. + + Args: + bill_id: Bill identifier in format {state}-{bill_number} (e.g., "LA-SB 4") + + Returns: + Detailed bill information including actions, sponsors, sources + """ + try: + # Parse bill_id to extract state and bill number + if '-' not in bill_id: + raise HTTPException(status_code=400, detail="Invalid bill ID format. Expected: STATE-BILLNUMBER") + + parts = bill_id.split('-', 1) + state = parts[0].upper() + bill_number = parts[1] + + # Build file paths for bills data from gold layer + bills_file = GOLD_DIR / "states" / state / "bills_bills.parquet" + actions_file = GOLD_DIR / "states" / state / "bills_bill_actions.parquet" + sponsors_file = GOLD_DIR / "states" / state / "bills_bill_sponsorships.parquet" + + # Get data sources (local or remote HuggingFace URL) + bills_source = get_data_source(bills_file, use_remote=IS_HF_SPACES) + actions_source = get_data_source(actions_file, use_remote=IS_HF_SPACES) + sponsors_source = get_data_source(sponsors_file, use_remote=IS_HF_SPACES) + + # Connect to DuckDB for querying parquet files + conn = duckdb.connect() + + try: + # Query for the specific bill + bill_query = """ + SELECT + bill_id, + bill_number, + title, + classification, + latest_action_description, + latest_action_date, + first_action_date, + session, + session_name, + jurisdiction_name + FROM read_parquet(?) + WHERE bill_number = ? + LIMIT 1 + """ + + result = conn.execute(bill_query, [bills_source, bill_number]).fetchone() + + if not result: + conn.close() + raise HTTPException(status_code=404, detail=f"Bill {bill_number} not found in {state}") + + # Parse bill data + bill_data = { + "bill_id": result[0] if result[0] else bill_id, + "bill_number": result[1], + "title": result[2], + "classification": result[3] if result[3] else [], + "latest_action": result[4], + "latest_action_date": result[5], + "first_action_date": result[6], + "session": result[7], + "session_name": result[8], + "jurisdiction": result[9], + "state": state, + } + + # Get sponsors if available + try: + sponsor_query = """ + SELECT name, primary_sponsor, classification + FROM read_parquet(?) + WHERE bill_id = ? + ORDER BY primary_sponsor DESC + """ + sponsor_rows = conn.execute(sponsor_query, [sponsors_source, bill_data["bill_id"]]).fetchall() + + bill_data["sponsors"] = [ + {"name": s[0], "primary": bool(s[1]), "classification": s[2]} + for s in sponsor_rows + ] + except Exception as e: + logger.warning(f"Could not load sponsors for {bill_id}: {e}") + bill_data["sponsors"] = [] + + # Get actions if available + try: + actions_query = """ + SELECT description, date, classification + FROM read_parquet(?) + WHERE bill_id = ? + ORDER BY date DESC + LIMIT 10 + """ + action_rows = conn.execute(actions_query, [actions_source, bill_data["bill_id"]]).fetchall() + + bill_data["actions"] = [ + {"description": a[0], "date": a[1], "classification": a[2]} + for a in action_rows + ] + except Exception as e: + logger.warning(f"Could not load actions for {bill_id}: {e}") + bill_data["actions"] = [] + + conn.close() + return bill_data + + except Exception as e: + conn.close() + raise + + except HTTPException: + raise + except Exception as e: + logger.error(f"Bill details error: {e}") + logger.error(traceback.format_exc()) + raise HTTPException(status_code=500, detail=str(e)) diff --git a/api/routes/bills_neon.py b/api/routes/bills_neon.py new file mode 100644 index 0000000000000000000000000000000000000000..9c8e28d57e4651ca62a8e6a06fe06ef564229fc5 --- /dev/null +++ b/api/routes/bills_neon.py @@ -0,0 +1,481 @@ +""" +Bills API Routes - Hybrid approach using Neon + Parquet +- Map aggregates: Neon PostgreSQL (fast, lightweight) +- Detailed bills & sessions: Parquet files (saves Neon space) +""" +from fastapi import APIRouter, Query, HTTPException +from fastapi.responses import JSONResponse +from typing import Optional, List, Dict, Any +import asyncpg +import duckdb +import pandas as pd +from pathlib import Path +from loguru import logger +import os +from datetime import datetime, timedelta + +from api.errors import ErrorDetail, parse_error + +router = APIRouter(prefix="/api/bills", tags=["bills"]) + +# Database configuration (for map aggregates only) +NEON_DATABASE_URL_DEV = os.getenv("NEON_DATABASE_URL_DEV") +NEON_DATABASE_URL = os.getenv("NEON_DATABASE_URL") +DATABASE_URL = NEON_DATABASE_URL_DEV or NEON_DATABASE_URL + +# Parquet configuration (for detailed bills) +GOLD_DIR = Path("data/gold") +IS_HF_SPACES = os.getenv("HF_SPACES") == "1" +HF_ORGANIZATION = os.getenv('HF_ORGANIZATION', 'CommunityOne') + +if DATABASE_URL: + logger.info(f"🗄️ Bills map using: {'DEV' if NEON_DATABASE_URL_DEV else 'PROD'} database") + logger.info(f"📁 Bills details using: {'HuggingFace' if IS_HF_SPACES else 'local'} parquet") +else: + logger.warning("⚠️ No database URL configured. Map endpoint will not work.") + +# Connection pool +_pool = None + +# Cache for map data (TTL: 5 minutes) +_map_cache = {} +_map_cache_time = None +MAP_CACHE_DURATION = timedelta(minutes=5) + + +def get_hf_dataset_url(dataset_name: str) -> str: + """Convert dataset name to HuggingFace parquet URL.""" + return f"https://huggingface.co/datasets/{HF_ORGANIZATION}/{dataset_name}/resolve/main/data/train-00000-of-00001.parquet" + +def get_data_source(file_path: Path, use_remote: bool = False) -> str: + """Get data source (local path or remote URL) based on environment.""" + if not IS_HF_SPACES and not use_remote: + return str(file_path) + + # Convert local path to HuggingFace dataset name + parts = file_path.parts + + if 'states' in parts: + state_idx = parts.index('states') + state = parts[state_idx + 1].lower() + filename = parts[-1].replace('.parquet', '').replace('_', '-') + dataset_name = f"states-{state}-{filename}" + return get_hf_dataset_url(dataset_name) + + return str(file_path) + + +async def get_pool(): + """Get or create asyncpg connection pool.""" + global _pool + if _pool is None and DATABASE_URL: + _pool = await asyncpg.create_pool( + DATABASE_URL, + min_size=1, + max_size=10, + command_timeout=60 + ) + return _pool + + +async def fetch_bills_from_parquet( + state: str, + q: Optional[str] = None, + session: Optional[str] = None, + limit: int = 50, + offset: int = 0 +) -> Dict[str, Any]: + """Fetch bills from parquet files using DuckDB (detailed drill-down).""" + try: + # Build file path + bills_file = GOLD_DIR / "states" / state / "bills_bills.parquet" + + # Get data source (local or remote HuggingFace URL) + data_source = get_data_source(bills_file, use_remote=IS_HF_SPACES) + + # Connect to DuckDB + conn = duckdb.connect() + + # Build SQL query + where_clauses = [] + params = [] + + if q: + where_clauses.append("(LOWER(title) LIKE LOWER(?) OR LOWER(bill_number) LIKE LOWER(?))") + pattern = f'%{q}%' + params.extend([pattern, pattern]) + + if session: + where_clauses.append("session = ?") + params.append(session) + + where_clause = " AND ".join(where_clauses) if where_clauses else "1=1" + + # Count total + count_sql = f""" + SELECT COUNT(*) as total + FROM read_parquet(?) + WHERE {where_clause} + """ + count_params = [data_source] + params + total = conn.execute(count_sql, count_params).fetchone()[0] + + # Fetch bills + sql = f""" + SELECT + bill_id, + bill_number, + title, + classification, + session, + session_name, + first_action_date, + latest_action_date, + latest_action_description, + jurisdiction_name + FROM read_parquet(?) + WHERE {where_clause} + ORDER BY latest_action_date DESC NULLS LAST, bill_number DESC + LIMIT ? OFFSET ? + """ + + query_params = [data_source] + params + [limit, offset] + rows = conn.execute(sql, query_params).fetchall() + + bills = [] + for row in rows: + bills.append({ + "bill_id": row[0], + "bill_number": row[1], + "title": row[2], + "classification": list(row[3]) if row[3] else [], + "session": row[4], + "session_name": row[5], + "first_action_date": str(row[6]) if row[6] else None, + "latest_action_date": str(row[7]) if row[7] else None, + "latest_action_description": row[8], + "jurisdiction_name": row[9] + }) + + conn.close() + + return { + "state": state, + "query": q, + "session": session, + "bills": bills, + "total": total, + "limit": limit, + "offset": offset, + "source": "parquet" + } + + except Exception as e: + logger.error(f"Error fetching bills from parquet: {e}") + raise + + +async def fetch_sessions_from_parquet(state: str) -> Dict[str, Any]: + """Fetch sessions from parquet files using DuckDB.""" + try: + # Build file path + bills_file = GOLD_DIR / "states" / state / "bills_bills.parquet" + + # Get data source + data_source = get_data_source(bills_file, use_remote=IS_HF_SPACES) + + # Connect to DuckDB + conn = duckdb.connect() + + # Aggregate sessions + sql = """ + SELECT + session, + MAX(session_name) as session_name, + MIN(first_action_date) as start_date, + MAX(latest_action_date) as end_date, + COUNT(*) as bill_count + FROM read_parquet(?) + GROUP BY session, session_name + ORDER BY session DESC + """ + + rows = conn.execute(sql, [data_source]).fetchall() + + sessions = [] + for row in rows: + sessions.append({ + "session": row[0], + "session_name": row[1], + "start_date": str(row[2]) if row[2] else None, + "end_date": str(row[3]) if row[3] else None, + "bill_count": row[4] + }) + + conn.close() + + return { + "state": state, + "sessions": sessions, + "total_sessions": len(sessions), + "source": "parquet" + } + + except Exception as e: + logger.error(f"Error fetching sessions from parquet: {e}") + raise + + +async def fetch_map_data_from_neon( + topic: Optional[str] = None, + session: Optional[str] = None +) -> Dict[str, Any]: + """Fetch map aggregates from Neon PostgreSQL.""" + pool = await get_pool() + + # Use cache if available + global _map_cache, _map_cache_time + cache_key = f"{topic or 'all'}_{session or 'all'}" + + now = datetime.now() + if _map_cache_time and (now - _map_cache_time) < MAP_CACHE_DURATION: + if cache_key in _map_cache: + logger.debug(f"🚀 Map cache hit for {cache_key}") + return _map_cache[cache_key] + + async with pool.acquire() as conn: + # For now, we only support topic='all' (no topic filtering yet) + # Session filtering would require aggregating bills on-the-fly + + sql = """ + SELECT + state_code, + topic, + total_bills, + type_bill, + type_resolution, + type_concurrent_resolution, + type_joint_resolution, + type_constitutional_amendment, + status_enacted, + status_failed, + status_pending, + primary_type, + primary_status, + map_category, + sample_bills, + last_updated + FROM bills_map_aggregates + WHERE topic = $1 + """ + + requested_topic = topic.lower() if topic else 'all' + rows = await conn.fetch(sql, requested_topic) + + # If topic-specific data not found, return empty (don't fallback) + if not rows: + logger.warning(f"📊 No pre-computed data for topic '{requested_topic}'") + return { + "topic": requested_topic, + "session": session, + "states": {}, + "total_states": 0, + "message": f"No data available for topic '{requested_topic}'. Try 'all' or pre-compute aggregates for this topic.", + "source": "neon" + } + + state_data = {} + for row in rows: + state_code = row['state_code'] + + # Parse sample bills JSON + sample_bills = row['sample_bills'] or [] + if isinstance(sample_bills, str): + import json + sample_bills = json.loads(sample_bills) + + state_data[state_code] = { + "state": state_code, + "total_bills": row['total_bills'], + "type_counts": { + "bill": row['type_bill'], + "resolution": row['type_resolution'], + "concurrent_resolution": row['type_concurrent_resolution'], + "joint_resolution": row['type_joint_resolution'], + "constitutional_amendment": row['type_constitutional_amendment'] + }, + "status_counts": { + "enacted": row['status_enacted'] or 0, + "failed": row['status_failed'] or 0, + "pending": row['status_pending'] or 0 + }, + "primary_type": row['primary_type'], + "primary_status": row['primary_status'], + "map_category": row['map_category'], + "sample_bills": sample_bills, + "last_updated": row['last_updated'].isoformat() if row['last_updated'] else None + } + + # Build dynamic legend based on actual data + unique_types = set() + for state in state_data.values(): + if state['primary_type']: + unique_types.add(state['primary_type']) + + # Map types to user-friendly names + type_labels = { + 'mandate': 'Mandate', + 'removal': 'Removal', + 'study': 'Study', + 'funding': 'Funding', + 'coverage_expansion': 'Coverage Expansion', + 'screening': 'Screening', + 'provider_access': 'Provider Access', + 'expansion': 'Expansion', + 'coverage': 'Coverage', + 'reimbursement': 'Reimbursement', + 'eligibility': 'Eligibility', + 'requirement': 'Requirement', + 'curriculum': 'Curriculum', + 'reform': 'Reform', + 'protection': 'Protection', + 'restriction': 'Restriction', + 'other': 'Other' + } + + legend_types = {t: type_labels.get(t, t.replace('_', ' ').title()) for t in unique_types} + + result = { + "topic": requested_topic, + "session": session, + "states": state_data, + "total_states": len(state_data), + "legend": { + "types": legend_types, + "statuses": { + "enacted": "Enacted", + "failed": "Failed", + "pending": "Pending" + } + }, + "cached": True, + "source": "neon" + } + + # Update cache + _map_cache[cache_key] = result + _map_cache_time = now + + return result + + +@router.get("") +async def get_bills( + state: str = Query(..., description="State abbreviation (e.g., MA, AL)"), + q: Optional[str] = Query(None, description="Search query (bill number or title)"), + session: Optional[str] = Query(None, description="Legislative session"), + limit: int = Query(50, ge=1, le=500), + offset: int = Query(0, ge=0) +): + """ + Search legislative bills using parquet files (detailed drill-down). + + **Examples:** + - `/api/bills?state=AL&q=dental` - Search Alabama bills for "dental" + - `/api/bills?state=AL&session=2024rs` - Get all 2024 regular session bills + - `/api/bills?state=AL&limit=50` - Browse recent Alabama bills + """ + try: + result = await fetch_bills_from_parquet( + state=state.upper(), + q=q, + session=session, + limit=limit, + offset=offset + ) + + return result + + except HTTPException: + raise + except Exception as e: + logger.error(f"Bills query error for state={state}: {e}") + + error_detail = parse_error(e, context={ + "state": state, + "query": q, + "session": session + }) + + return JSONResponse( + status_code=500, + content=error_detail.model_dump() + ) + + +@router.get("/sessions") +async def get_sessions( + state: str = Query(..., description="State abbreviation (e.g., MA, AL)") +): + """ + Get legislative sessions for a state using parquet files. + + **Examples:** + - `/api/bills/sessions?state=MA` - Get all Massachusetts sessions + """ + try: + result = await fetch_sessions_from_parquet(state=state.upper()) + + return result + + except HTTPException: + raise + except Exception as e: + logger.error(f"Sessions query error for state={state}: {e}") + + error_detail = parse_error(e, context={ + "state": state + }) + + return JSONResponse( + status_code=500, + content=error_detail.model_dump() + ) + + +@router.get("/map") +async def get_bill_map_data( + topic: Optional[str] = Query(None, description="Topic to filter (e.g., dental, health, education)"), + session: Optional[str] = Query(None, description="Legislative session") +): + """ + Get aggregated bill data for choropleth map using Neon PostgreSQL. + + Returns pre-computed state-level aggregates for instant visualization. + + **Examples:** + - `/api/bills/map` - Get national bill map data + - `/api/bills/map?topic=dental` - Map dental legislation (not yet implemented) + """ + try: + if not DATABASE_URL: + raise HTTPException(status_code=503, detail="Database not configured") + + result = await fetch_map_data_from_neon(topic=topic, session=session) + + return result + + except HTTPException: + raise + except Exception as e: + logger.error(f"Map data query error: {e}") + + error_detail = parse_error(e, context={ + "topic": topic, + "session": session + }) + + return JSONResponse( + status_code=500, + content=error_detail.model_dump() + ) diff --git a/api/routes/contact.py b/api/routes/contact.py new file mode 100644 index 0000000000000000000000000000000000000000..706515e26c385d0362b476e32191fe66fb960c80 --- /dev/null +++ b/api/routes/contact.py @@ -0,0 +1,118 @@ +""" +Contact routes for submitting feedback and issues via GitHub. +""" +from fastapi import APIRouter, HTTPException, status +from pydantic import BaseModel, EmailStr, Field +from typing import Optional +import httpx +import os +from loguru import logger + +router = APIRouter(prefix="/api/contact", tags=["contact"]) + + +class ContactRequest(BaseModel): + """Contact form submission""" + name: str = Field(..., min_length=1, max_length=100, description="Name of the person contacting") + email: EmailStr = Field(..., description="Email address for follow-up") + subject: str = Field(..., min_length=1, max_length=200, description="Subject of the message") + message: str = Field(..., min_length=10, max_length=5000, description="Detailed message") + category: Optional[str] = Field(default="feedback", description="Type of contact: feedback, bug, feature, question") + + +class ContactResponse(BaseModel): + """Response after submitting contact form""" + success: bool + message: str + issue_url: Optional[str] = None + + +@router.post("/submit", response_model=ContactResponse, status_code=status.HTTP_201_CREATED) +async def submit_contact(request: ContactRequest): + """ + Submit a contact form message as a GitHub issue. + + Creates an issue in the GitHub repository with the contact information. + Requires GITHUB_TOKEN and GITHUB_REPO environment variables. + """ + github_token = os.getenv("GITHUB_TOKEN") + github_repo = os.getenv("GITHUB_REPO", "getcommunityone/open-navigator") + + if not github_token: + logger.warning("GitHub token not configured - contact form submission failed") + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="Contact form is not configured. Please email us directly or submit an issue on GitHub." + ) + + # Determine label based on category + category_labels = { + "bug": "bug", + "feature": "enhancement", + "question": "question", + "feedback": "feedback" + } + label = category_labels.get(request.category, "feedback") + + # Create issue title and body + issue_title = f"[Contact Form] {request.subject}" + issue_body = f"""**From:** {request.name} ({request.email}) +**Category:** {request.category} + +--- + +{request.message} + +--- +*This issue was automatically created from the contact form.* +""" + + # Create GitHub issue + github_api_url = f"https://api.github.com/repos/{github_repo}/issues" + headers = { + "Authorization": f"token {github_token}", + "Accept": "application/vnd.github.v3+json" + } + payload = { + "title": issue_title, + "body": issue_body, + "labels": [label, "contact-form"] + } + + try: + async with httpx.AsyncClient() as client: + response = await client.post( + github_api_url, + json=payload, + headers=headers, + timeout=10.0 + ) + + if response.status_code == 201: + issue_data = response.json() + issue_url = issue_data.get("html_url") + logger.info(f"Contact form submitted successfully: {issue_url}") + return ContactResponse( + success=True, + message="Thank you for contacting us! We've received your message and will get back to you soon.", + issue_url=issue_url + ) + else: + logger.error(f"GitHub API error: {response.status_code} - {response.text}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to submit contact form. Please try again later." + ) + + except httpx.TimeoutException: + logger.error("GitHub API timeout") + raise HTTPException( + status_code=status.HTTP_504_GATEWAY_TIMEOUT, + detail="Request timed out. Please try again." + ) + except httpx.RequestError as e: + logger.error(f"GitHub API request error: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to submit contact form. Please try again later." + ) diff --git a/api/routes/hf_search.py b/api/routes/hf_search.py new file mode 100644 index 0000000000000000000000000000000000000000..ff9af3d3620679948f0012b32d6d2eeaebac5162 --- /dev/null +++ b/api/routes/hf_search.py @@ -0,0 +1,182 @@ +""" +HuggingFace Datasets Search API Integration + +Fast server-side text search using HuggingFace's indexed datasets. +Falls back to DuckDB if dataset not indexed or search unavailable. +""" +import httpx +from typing import Optional, List, Dict, Any +from loguru import logger +import os + +HF_SEARCH_API = "https://datasets-server.huggingface.co/search" +HF_ORGANIZATION = os.getenv('HF_ORGANIZATION', 'CommunityOne') +REQUEST_TIMEOUT = 5 # seconds + + +def is_dataset_indexed(dataset_name: str) -> bool: + """ + Check if a dataset is indexed and searchable. + + Args: + dataset_name: Full repo ID (e.g., 'CommunityOne/states-ma-contacts-local-officials') + + Returns: + True if dataset supports search, False otherwise + """ + try: + response = httpx.get( + "https://datasets-server.huggingface.co/is-valid", + params={"dataset": dataset_name}, + timeout=REQUEST_TIMEOUT + ) + if response.status_code == 200: + data = response.json() + return data.get("search", False) + except Exception as e: + logger.debug(f"Could not check if {dataset_name} is indexed: {e}") + + return False + + +def search_hf_dataset( + dataset_name: str, + query: str, + config: str = "default", + split: str = "train", + limit: int = 100 +) -> Optional[List[Dict[str, Any]]]: + """ + Search a HuggingFace dataset using server-side indexed search. + + Args: + dataset_name: Full repo ID (e.g., 'CommunityOne/states-ma-contacts') + query: Search text (searches across all string columns) + config: Dataset configuration name + split: Dataset split to search + limit: Maximum results to return + + Returns: + List of matching rows, or None if search unavailable + """ + try: + response = httpx.get( + HF_SEARCH_API, + params={ + "dataset": dataset_name, + "config": config, + "split": split, + "query": query, + "offset": 0, + "length": limit + }, + timeout=REQUEST_TIMEOUT + ) + + if response.status_code == 200: + data = response.json() + + # Check for errors + if "error" in data: + logger.debug(f"HF Search API error for {dataset_name}: {data['error']}") + return None + + # Extract rows + rows = data.get("rows", []) + logger.info(f"✅ HF Search API: Found {len(rows)} results for '{query}' in {dataset_name}") + + # Convert to list of dicts (row['row'] contains actual data) + results = [row.get("row", {}) for row in rows] + return results + + elif response.status_code == 404: + logger.debug(f"Dataset {dataset_name} not found on HuggingFace") + return None + + else: + logger.debug(f"HF Search API returned status {response.status_code}") + return None + + except httpx.TimeoutException: + logger.debug(f"HF Search API timeout for {dataset_name}") + return None + + except Exception as e: + logger.debug(f"HF Search API error: {e}") + return None + + +def search_contacts_hf(query: str, state: Optional[str] = None, limit: int = 10) -> Optional[List[Dict[str, Any]]]: + """ + Search contacts (local officials + nonprofit officers) using HF Search API. + + Args: + query: Search text (name, title, jurisdiction, etc.) + state: 2-letter state code (e.g., 'MA') + limit: Maximum results + + Returns: + List of contact dicts, or None if search unavailable + """ + results = [] + + # Search local officials + if state: + dataset = f"{HF_ORGANIZATION}/states-{state.lower()}-contacts-local-officials" + local_results = search_hf_dataset(dataset, query, limit=limit) + if local_results: + for row in local_results: + row['source'] = 'local_officials' + results.append(row) + + # Search nonprofit officers + if state and len(results) < limit: + dataset = f"{HF_ORGANIZATION}/states-{state.lower()}-contacts-nonprofit-officers" + nonprofit_results = search_hf_dataset(dataset, query, limit=limit - len(results)) + if nonprofit_results: + for row in nonprofit_results: + row['source'] = 'nonprofit_officers' + results.append(row) + + return results if results else None + + +def search_organizations_hf(query: str, state: Optional[str] = None, limit: int = 10) -> Optional[List[Dict[str, Any]]]: + """ + Search nonprofit organizations using HF Search API. + + Args: + query: Search text (organization name, EIN, etc.) + state: 2-letter state code + limit: Maximum results + + Returns: + List of organization dicts, or None if search unavailable + """ + if state: + dataset = f"{HF_ORGANIZATION}/states-{state.lower()}-nonprofits-organizations" + else: + dataset = f"{HF_ORGANIZATION}/national-nonprofits-organizations" + + return search_hf_dataset(dataset, query, limit=limit) + + +def search_jurisdictions_hf(query: str, jurisdiction_type: Optional[str] = None, limit: int = 10) -> Optional[List[Dict[str, Any]]]: + """ + Search jurisdictions (cities, counties, townships, school districts) using HF Search API. + + Args: + query: Search text (city name, county name, etc.) + jurisdiction_type: 'cities', 'counties', 'townships', 'school_districts' + limit: Maximum results + + Returns: + List of jurisdiction dicts, or None if search unavailable + """ + if jurisdiction_type: + dataset = f"{HF_ORGANIZATION}/reference-jurisdictions-{jurisdiction_type.replace('_', '-')}" + else: + # Try cities first (most common) + dataset = f"{HF_ORGANIZATION}/reference-jurisdictions-cities" + + return search_hf_dataset(dataset, query, limit=limit) diff --git a/api/routes/search.py b/api/routes/search.py new file mode 100644 index 0000000000000000000000000000000000000000..b6f30f99c0c701adc44c2e55393072b8cdecbe3e --- /dev/null +++ b/api/routes/search.py @@ -0,0 +1,1685 @@ +""" +Unified Search API +LinkedIn-style search across contacts, meetings, organizations, and causes +Uses hybrid approach: PostgreSQL (primary, fast) + HuggingFace Search API + DuckDB (fallback) +""" +from fastapi import APIRouter, Query, HTTPException +from fastapi.responses import JSONResponse +from typing import Optional, List, Dict, Any +from pathlib import Path +import pandas as pd +import duckdb +from loguru import logger +import re +import os +import sys +import requests +from functools import lru_cache +from datetime import datetime, timedelta + +from api.errors import ErrorDetail, parse_error + +# Import PostgreSQL search functions (primary) +from api.routes import search_postgres + +# Import HuggingFace Search helpers +from api.routes.hf_search import ( + search_contacts_hf, + search_organizations_hf, + search_jurisdictions_hf, + is_dataset_indexed +) + +router = APIRouter(tags=["search"]) + +# Paths to gold datasets +GOLD_DIR = Path("data/gold") + +# Detect deployment environment +IS_HF_SPACES = os.getenv("HF_SPACES") == "1" +HF_ORGANIZATION = os.getenv('HF_ORGANIZATION', 'CommunityOne') + +# Cache for count queries (TTL: 1 hour) +_count_cache = {} +_count_cache_ttl = {} + +# In-memory DataFrame cache for HuggingFace datasets (TTL: 5 minutes) +# Reduces remote HTTP requests from 2-3s to <10ms per search +_dataframe_cache: Dict[str, pd.DataFrame] = {} +_dataframe_cache_ttl: Dict[str, datetime] = {} +DATAFRAME_CACHE_TTL = timedelta(minutes=5) + +# Every.org API config (fallback only) +EVERYORG_API_KEY = os.getenv('EVERYORG_API_KEY', '') +EVERYORG_API_BASE = "https://partners.every.org/v0.2" + + +def load_parquet_cached(url: str) -> pd.DataFrame: + """ + Load parquet file with in-memory caching to avoid repeated HTTP requests. + + Cache TTL: 5 minutes (balances speed vs freshness) + Reduces search latency from 2-3s to <10ms per query. + + Args: + url: URL to parquet file (local path or HuggingFace URL) + + Returns: + pandas DataFrame + """ + now = datetime.now() + + # Check cache + if url in _dataframe_cache: + cache_time = _dataframe_cache_ttl.get(url) + if cache_time and (now - cache_time) < DATAFRAME_CACHE_TTL: + logger.debug(f"🚀 Cache hit for {url}") + return _dataframe_cache[url] + + # Cache miss - load from source + logger.info(f"📥 Loading parquet from {url}") + df = pd.read_parquet(url) + + # Store in cache + _dataframe_cache[url] = df + _dataframe_cache_ttl[url] = now + logger.debug(f"💾 Cached {len(df)} rows from {url}") + + return df + + +def get_hf_dataset_url(dataset_name: str) -> str: + """ + Convert dataset name to HuggingFace parquet URL. + + HuggingFace Datasets library stores parquet files in the standard format: + data/train-00000-of-00001.parquet + + Examples: + states-ma-contacts-local-officials -> + https://huggingface.co/datasets/CommunityOne/states-ma-contacts-local-officials/resolve/main/data/train-00000-of-00001.parquet + """ + return f"https://huggingface.co/datasets/{HF_ORGANIZATION}/{dataset_name}/resolve/main/data/train-00000-of-00001.parquet" + + +def get_data_source(file_path: Path, use_remote: bool = False) -> str: + """ + Get data source (local path or remote URL) based on environment. + + Args: + file_path: Local file path (e.g., data/gold/states/MA/contacts_local_officials.parquet) + use_remote: Force remote URL even in local environment + + Returns: + File path string (local) or HuggingFace URL (remote) + """ + if not IS_HF_SPACES and not use_remote: + return str(file_path) + + # Convert local path to HuggingFace dataset name + # data/gold/states/MA/contacts_local_officials.parquet -> states-ma-contacts-local-officials + parts = file_path.parts + + if 'states' in parts: + state_idx = parts.index('states') + state = parts[state_idx + 1].lower() + filename = parts[-1].replace('.parquet', '').replace('_', '-') + dataset_name = f"states-{state}-{filename}" + elif 'national' in parts: + filename = parts[-1].replace('.parquet', '').replace('_', '-') + dataset_name = f"national-{filename}" + elif 'reference' in parts: + filename = parts[-1].replace('.parquet', '').replace('_', '-') + dataset_name = f"reference-{filename}" + else: + # Fallback to local + return str(file_path) + + return get_hf_dataset_url(dataset_name) + + +@lru_cache(maxsize=5000) +def fetch_form990_data(ein: str) -> Optional[Dict[str, Any]]: + """ + Fetch enrichment data from ProPublica Nonprofit Explorer (FREE!) + Uses their API to get website and mission from Form 990 filings + """ + if not ein: + return None + + try: + clean_ein = str(ein).replace('-', '').zfill(9) + url = f"https://projects.propublica.org/nonprofits/api/v2/organizations/{clean_ein}.json" + + response = requests.get(url, timeout=3) + + if response.status_code == 200: + data = response.json() + org = data.get('organization', {}) + filings = data.get('filings_with_data', []) + + # Get most recent filing data + website = None + mission = None + + if filings: + # ProPublica provides website from most recent filing + latest = filings[0] + # Note: ProPublica API doesn't directly expose website field + # but we can use their organization name and data as fallback + pass + + return { + 'website': website, # ProPublica doesn't expose this in API + 'mission': None, # Would need to parse PDF + 'source': 'propublica', + 'last_updated': datetime.now().isoformat(), + 'tax_year': filings[0].get('tax_prd_yr') if filings else None + } + except Exception as e: + logger.debug(f"ProPublica lookup failed for EIN {ein}: {e}") + + return None + + +@lru_cache(maxsize=5000) +def fetch_everyorg_data(ein: str) -> Optional[Dict[str, Any]]: + """Fetch enrichment data from Every.org API (cached) - FALLBACK ONLY""" + if not EVERYORG_API_KEY or not ein: + return None + + try: + # Format EIN (remove dashes, ensure 9 digits) + clean_ein = str(ein).replace('-', '').zfill(9) + + url = f"{EVERYORG_API_BASE}/nonprofit/{clean_ein}" + headers = { + "Authorization": f"Bearer {EVERYORG_API_KEY}", + "Accept": "application/json" + } + + response = requests.get(url, headers=headers, timeout=3) + + if response.status_code == 200: + data = response.json() + if data and 'data' in data and 'nonprofit' in data['data']: + nonprofit = data['data']['nonprofit'] + tags = data['data'].get('nonprofitTags', []) + causes = [tag.get('tagName') for tag in tags if tag.get('tagName')] + + return { + 'mission': nonprofit.get('description') or nonprofit.get('descriptionLong'), + 'website': nonprofit.get('websiteUrl'), + 'logo_url': nonprofit.get('logoUrl'), + 'profile_url': nonprofit.get('profileUrl'), + 'causes': causes[:5], # Limit to top 5 causes + 'source': 'everyorg', + 'last_updated': datetime.now().isoformat() + } + except Exception as e: + logger.debug(f"Every.org lookup failed for EIN {ein}: {e}") + + return None + + +def get_enrichment_data(ein: str, existing_data: Optional[Dict] = None) -> Dict[str, Any]: + """ + Get enrichment data with intelligent backfill strategy + + Priority: + 1. Existing form_990_* data (if recent) + 2. GivingTuesday 990 XML (future: direct S3 access) + 3. ProPublica API (current fallback) + 4. Every.org API (last resort) + + Tracks source and update time for incremental processing + """ + result = { + 'website': None, + 'mission': None, + 'logo_url': None, + 'profile_url': None, + 'causes': [], + 'data_sources': [] + } + + # Check existing data first (skip if older than 30 days) + if existing_data: + cutoff_date = datetime.now() - timedelta(days=30) + + # Check enrichment data (from any source: form_990, bigquery, etc.) + if existing_data.get('enrichment_website'): + last_updated = existing_data.get('enrichment_last_updated') + if not last_updated or (isinstance(last_updated, str) and datetime.fromisoformat(last_updated) > cutoff_date): + result['website'] = existing_data['enrichment_website'] + result['data_sources'].append('cached') + + if existing_data.get('enrichment_mission'): + result['mission'] = existing_data['enrichment_mission'] + if 'cached' not in result['data_sources']: + result['data_sources'].append('cached') + + # Try Every.org for missing fields (keeps logo and causes which 990 doesn't have) + if not result['website'] or not result['mission']: + everyorg_data = fetch_everyorg_data(ein) + if everyorg_data: + if not result['website'] and everyorg_data.get('website'): + result['website'] = everyorg_data['website'] + result['data_sources'].append('everyorg') + + if not result['mission'] and everyorg_data.get('mission'): + result['mission'] = everyorg_data['mission'] + result['data_sources'].append('everyorg') + + # Always get logo and causes from Every.org + result['logo_url'] = everyorg_data.get('logo_url') + result['profile_url'] = everyorg_data.get('profile_url') + result['causes'] = everyorg_data.get('causes', []) + if result['logo_url'] or result['causes']: + if 'everyorg' not in result['data_sources']: + result['data_sources'].append('everyorg') + + return result + +class SearchResult: + """Unified search result""" + + def __init__(self, + result_type: str, + title: str, + subtitle: str, + description: str, + url: str, + score: float, + metadata: Dict[str, Any]): + self.result_type = result_type + self.title = title + self.subtitle = subtitle + self.description = description + self.url = url + self.score = score + self.metadata = metadata + + def to_dict(self): + return { + "type": self.result_type, + "title": self.title, + "subtitle": self.subtitle, + "description": self.description, + "url": self.url, + "score": self.score, + "metadata": self.metadata + } + + +def convert_pg_result(pg_result: search_postgres.SearchResult) -> 'SearchResult': + """Convert PostgreSQL SearchResult dataclass to SearchResult class""" + return SearchResult( + result_type=pg_result.result_type, + title=pg_result.title, + subtitle=pg_result.subtitle, + description=pg_result.description, + url=pg_result.url, + score=pg_result.score, + metadata=pg_result.metadata + ) + + +def calculate_relevance_score(text: str, query: str) -> float: + """Calculate relevance score for text matching query""" + if not text or not query: + return 0.0 + + text_lower = text.lower() + query_lower = query.lower() + + # Exact match gets highest score + if query_lower in text_lower: + score = 1.0 + # Boost if it's at the start + if text_lower.startswith(query_lower): + score += 0.5 + return min(score, 2.0) + + # Partial word matches + query_words = query_lower.split() + text_words = text_lower.split() + + matches = sum(1 for qw in query_words if any(qw in tw for tw in text_words)) + return matches / len(query_words) if query_words else 0.0 + + +def search_contacts_duckdb(query: str, state: Optional[str] = None, limit: int = 10) -> List[SearchResult]: + """ + Search contacts using DuckDB (supports local files or remote HTTP parquet). + This is the fallback when HF Search API is unavailable. + Supports browse mode when query is empty. + """ + results = [] + + # Determine if this is browse mode (no query) or search mode + is_browse_mode = not query or query.strip() == '' + + try: + # Initialize DuckDB connection + conn = duckdb.connect() + + # Search 1: State Officials (OpenStates - state legislators, mayors, etc.) + if state: + officials_file_path = GOLD_DIR / "states" / state / "contacts_officials.parquet" + officials_file_paths = [officials_file_path] + else: + officials_file_paths = list(GOLD_DIR.glob("states/*/contacts_officials.parquet"))[:5] + + logger.info(f"Searching {len(officials_file_paths)} state official contact files (OpenStates) - browse_mode={is_browse_mode}") + + for file_path in officials_file_paths: + if not file_path.exists(): + continue + + # Get data source (local or remote URL) + data_source = get_data_source(file_path, use_remote=IS_HF_SPACES) + + try: + if is_browse_mode: + # Browse mode: return all officials, prioritize mayors + sql = """ + SELECT + full_name as name, + role_type as title, + city_jurisdiction as jurisdiction, + state, + email, + phone, + CASE + WHEN LOWER(role_type) = 'mayor' THEN 2.0 + WHEN LOWER(role_type) LIKE '%council%' THEN 1.8 + WHEN LOWER(role_type) LIKE '%commission%' THEN 1.7 + ELSE 1.5 + END as score + FROM read_parquet(?) + ORDER BY score DESC, full_name ASC + LIMIT ? + """ + + rows = conn.execute(sql, [data_source, limit]).fetchall() + else: + # Search mode: relevance scoring + sql = """ + SELECT + full_name as name, + role_type as title, + city_jurisdiction as jurisdiction, + state, + email, + phone, + GREATEST( + CASE + WHEN LOWER(full_name) LIKE LOWER(?) THEN 1.5 + WHEN LOWER(full_name) LIKE LOWER(?) THEN 1.0 + ELSE 0.0 + END, + CASE + WHEN LOWER(role_type) LIKE LOWER(?) THEN 1.5 + WHEN LOWER(role_type) LIKE LOWER(?) THEN 1.0 + ELSE 0.0 + END, + CASE + WHEN LOWER(city_jurisdiction) LIKE LOWER(?) THEN 1.5 + WHEN LOWER(city_jurisdiction) LIKE LOWER(?) THEN 1.0 + ELSE 0.0 + END, + CASE + WHEN LOWER(jurisdiction_name) LIKE LOWER(?) THEN 1.5 + WHEN LOWER(jurisdiction_name) LIKE LOWER(?) THEN 1.0 + ELSE 0.0 + END + ) as score + FROM read_parquet(?) + WHERE LOWER(full_name) LIKE LOWER(?) + OR LOWER(role_type) LIKE LOWER(?) + OR LOWER(city_jurisdiction) LIKE LOWER(?) + OR LOWER(jurisdiction_name) LIKE LOWER(?) + ORDER BY score DESC + LIMIT ? + """ + + query_pattern = f'%{query}%' + query_start = f'{query}%' + + rows = conn.execute(sql, [ + query_start, query_pattern, # name scoring + query_start, query_pattern, # role_type scoring + query_start, query_pattern, # city_jurisdiction scoring + query_start, query_pattern, # jurisdiction_name scoring + data_source, # file path or URL + query_pattern, query_pattern, query_pattern, query_pattern, # WHERE clause + limit + ]).fetchall() + + # Convert to SearchResult objects + for row in rows: + name, title, jurisdiction, state_code, email, phone, score = row + + if score > 0.3: # Relevance threshold + contact_info = [] + if email: + contact_info.append(f"📧 {email}") + if phone: + contact_info.append(f"📞 {phone}") + + description = f"State official in {jurisdiction}" if jurisdiction else f"State official in {state_code}" + if contact_info: + description += f" • {' • '.join(contact_info)}" + + results.append(SearchResult( + result_type="contact", + title=name if name else "Unknown", + subtitle=f"{title.title() if title else 'Official'} - {jurisdiction or state_code}", + description=description, + url=f"/people/{name.replace(' ', '-') if name else 'unknown'}", + score=score, + metadata={ + "title": title, + "jurisdiction": jurisdiction, + "state": state_code, + "name": name, + "email": email, + "phone": phone, + "contact_type": "state_official", + "data_source": "OpenStates" + } + )) + + except Exception as e: + logger.debug(f"Error searching state officials {file_path}: {e}") + + # Search 2: Local Officials (from meeting transcripts) + if state: + local_file_path = GOLD_DIR / "states" / state / "contacts_local_officials.parquet" + local_file_paths = [local_file_path] + else: + local_file_paths = list(GOLD_DIR.glob("states/*/contacts_local_officials.parquet"))[:5] + + logger.info(f"Searching {len(local_file_paths)} local official contact files (meeting transcripts)") + + for file_path in local_file_paths: + # Get data source (local or remote URL) + data_source = get_data_source(file_path, use_remote=IS_HF_SPACES) + + try: + # SQL query with relevance scoring across name, title, jurisdiction + sql = """ + SELECT + name, + title, + jurisdiction, + state, + GREATEST( + CASE + WHEN LOWER(name) LIKE LOWER(?) THEN 1.5 + WHEN LOWER(name) LIKE LOWER(?) THEN 1.0 + ELSE 0.0 + END, + CASE + WHEN LOWER(title) LIKE LOWER(?) THEN 1.5 + WHEN LOWER(title) LIKE LOWER(?) THEN 1.0 + ELSE 0.0 + END, + CASE + WHEN LOWER(jurisdiction) LIKE LOWER(?) THEN 1.5 + WHEN LOWER(jurisdiction) LIKE LOWER(?) THEN 1.0 + ELSE 0.0 + END + ) as score + FROM read_parquet(?) + WHERE LOWER(name) LIKE LOWER(?) + OR LOWER(title) LIKE LOWER(?) + OR LOWER(jurisdiction) LIKE LOWER(?) + ORDER BY score DESC + LIMIT ? + """ + + query_pattern = f'%{query}%' + query_start = f'{query}%' + + rows = conn.execute(sql, [ + query_start, query_pattern, # name scoring + query_start, query_pattern, # title scoring + query_start, query_pattern, # jurisdiction scoring + data_source, # file path or URL + query_pattern, query_pattern, query_pattern, # WHERE clause + limit + ]).fetchall() + + # Convert to SearchResult objects + for row in rows: + name, title, jurisdiction, state_code, score = row + + if score > 0.3: # Relevance threshold + results.append(SearchResult( + result_type="contact", + title=name if name else "Unknown", + subtitle=f"{title} - {jurisdiction}, {state_code}", + description=f"Local official in {jurisdiction}", + url=f"/people/{name.replace(' ', '-') if name else 'unknown'}", + score=score, + metadata={ + "title": title, + "jurisdiction": jurisdiction, + "state": state_code, + "name": name + } + )) + + except Exception as e: + logger.debug(f"Error searching {file_path}: {e}") + + # Search 3: Nonprofit Officers from state directories + nonprofit_files = [] + + # If state specified, search that state's directory + if state: + state_nonprofit_file = GOLD_DIR / "states" / state / "contacts_nonprofit_officers.parquet" + nonprofit_files.append(state_nonprofit_file) + else: + # Search all state directories + for state_dir in (GOLD_DIR / "states").glob("*/"): + state_file = state_dir / "contacts_nonprofit_officers.parquet" + nonprofit_files.append(state_file) + + for nonprofit_file in nonprofit_files: + # Get data source (local or remote URL) + nonprofit_source = get_data_source(nonprofit_file, use_remote=IS_HF_SPACES) + + try: + logger.info(f"Searching nonprofit officers: {nonprofit_source}") + + officer_sql = """ + SELECT + name, + title, + organization_name, + city, + state, + compensation, + GREATEST( + CASE + WHEN LOWER(name) LIKE LOWER(?) THEN 1.5 + WHEN LOWER(name) LIKE LOWER(?) THEN 1.0 + ELSE 0.0 + END, + CASE + WHEN LOWER(title) LIKE LOWER(?) THEN 1.0 + WHEN LOWER(title) LIKE LOWER(?) THEN 0.5 + ELSE 0.0 + END, + CASE + WHEN LOWER(organization_name) LIKE LOWER(?) THEN 1.0 + WHEN LOWER(organization_name) LIKE LOWER(?) THEN 0.5 + ELSE 0.0 + END + ) AS score + FROM read_parquet(?) + WHERE (LOWER(name) LIKE LOWER(?) + OR LOWER(title) LIKE LOWER(?) + OR LOWER(organization_name) LIKE LOWER(?)) + """ + + query_pattern = f'%{query}%' + query_start = f'{query}%' + params = [ + query_start, query_pattern, # name scoring + query_start, query_pattern, # title scoring + query_start, query_pattern, # organization scoring + nonprofit_source, # file path or URL + query_pattern, query_pattern, query_pattern # WHERE clause + ] + + if state: + officer_sql += " AND LOWER(state) = LOWER(?)" + params.append(state) + + officer_sql += " ORDER BY score DESC, compensation DESC NULLS LAST LIMIT ?" + params.append(limit) + + officer_rows = conn.execute(officer_sql, params).fetchall() + + for row in officer_rows: + name, title, org_name, city, state_code, compensation, score = row + + if score > 0.3: + comp_text = f" (${compensation:,.0f})" if compensation else "" + + results.append(SearchResult( + result_type="contact", + title=name if name else "Unknown", + subtitle=f"{title} - {org_name}{comp_text}", + description=f"Nonprofit officer in {city}, {state_code}", + url=f"/nonprofits?name={org_name.replace(' ', '-') if org_name else 'unknown'}", + score=score, + metadata={ + "title": title, + "organization": org_name, + "city": city, + "state": state_code, + "compensation": compensation, + "contact_type": "nonprofit_officer" + } + )) + + logger.info(f"Found {len([r for r in results if r.metadata.get('contact_type') == 'nonprofit_officer'])} nonprofit officer results") + + except Exception as e: + logger.debug(f"Error searching nonprofit officers: {e}") + + conn.close() + + # Sort all results by score and limit + results.sort(key=lambda x: x.score, reverse=True) + logger.info(f"DuckDB search found {len(results)} contacts for query '{query}'") + return results[:limit] + + except Exception as e: + logger.error(f"Contact search error: {e}") + return results + + +def search_contacts(query: str, state: Optional[str] = None, limit: int = 10) -> List[SearchResult]: + """ + HYBRID SEARCH: Search local officials AND nonprofit officers. + + Strategy: + 1. Try HuggingFace Search API first (fast, server-side indexed) - HF Spaces only + 2. Fall back to DuckDB (local files or remote HTTP parquet) + + Args: + query: Search text (name, title, organization, etc.) + state: Optional 2-letter state code filter + limit: Maximum results to return + + Returns: + List of SearchResult objects sorted by relevance + """ + logger.info(f"🔎 search_contacts() called - query={query!r}, state={state!r}, limit={limit}, IS_HF_SPACES={IS_HF_SPACES}") + + # STRATEGY 1: Try HuggingFace Search API (fast text search) + if query and IS_HF_SPACES: + logger.info(f"🔍 Trying HF Search API for '{query}' (state={state})") + try: + hf_results = search_contacts_hf(query, state, limit=limit) + + if hf_results: + logger.info(f"✅ HF Search API returned {len(hf_results)} results") + # Convert HF results to SearchResult objects + results = [] + for row in hf_results: + source_type = row.get('source', 'contact') + name = row.get('name', 'Unknown') + title = row.get('title', '') + jurisdiction = row.get('jurisdiction', row.get('organization_name', '')) + state_code = row.get('state', state or '') + + results.append(SearchResult( + result_type="contact", + title=name, + subtitle=f"{title} - {jurisdiction}, {state_code}", + description=f"{'Local official' if source_type == 'local_officials' else 'Nonprofit officer'} in {jurisdiction}", + url=f"/people/{name.replace(' ', '-')}", + score=1.0, + metadata={ + "title": title, + "jurisdiction": jurisdiction, + "state": state_code, + "name": name, + "source": source_type + } + )) + return results[:limit] + except Exception as e: + logger.warning(f"HF Search API failed, falling back to DuckDB: {e}") + + # STRATEGY 2: Fall back to DuckDB (works with local or remote parquet) + logger.info(f"🔍 Using DuckDB {'remote' if IS_HF_SPACES else 'local'} search for '{query}'") + return search_contacts_duckdb(query, state, limit) + + +def search_meetings(query: str, state: Optional[str] = None, limit: int = 10) -> List[SearchResult]: + """Search meeting transcripts and agendas""" + results = [] + + try: + # Search state event/meeting files (try new naming first, fallback to old) + if state: + meeting_files = list(GOLD_DIR.glob(f"states/{state}/events.parquet")) + if not meeting_files: + meeting_files = list(GOLD_DIR.glob(f"states/{state}/events_events.parquet")) + if not meeting_files: + meeting_files = list(GOLD_DIR.glob(f"states/{state}/meetings.parquet")) + else: + meeting_files = list(GOLD_DIR.glob("states/*/events.parquet")) + if not meeting_files: + meeting_files = list(GOLD_DIR.glob("states/*/events_events.parquet")) + if not meeting_files: + meeting_files = list(GOLD_DIR.glob("states/*/meetings.parquet")) + + for file_path in meeting_files[:5]: # Limit for performance + try: + df = pd.read_parquet(file_path) + state_code = file_path.parent.name + + # Detect schema - different files have different column names + columns = set(df.columns) + + # Map column names (handle LocalView vs CityScrapers vs other formats) + title_col = 'vid_title' if 'vid_title' in columns else ('event_title' if 'event_title' in columns else 'title') + body_col = 'caption_text_clean' if 'caption_text_clean' in columns else ('caption_text' if 'caption_text' in columns else ('full_text' if 'full_text' in columns else 'body')) + jurisdiction_col = 'place_name' if 'place_name' in columns else ('jurisdiction_name' if 'jurisdiction_name' in columns else 'jurisdiction') + date_col = 'meeting_date' if 'meeting_date' in columns else 'date' + id_col = 'vid_id' if 'vid_id' in columns else ('meeting_id' if 'meeting_id' in columns else 'id') + + # Search in title, body, jurisdiction + for _, row in df.iterrows(): + title = str(row.get(title_col, '')) + body = str(row.get(body_col, ''))[:500] # First 500 chars + jurisdiction = str(row.get(jurisdiction_col, '')) + meeting_date = str(row.get(date_col, '')) + meeting_id = str(row.get(id_col, '')) + + score = max( + calculate_relevance_score(title, query), + calculate_relevance_score(body, query) * 0.8, # Body matches slightly lower + calculate_relevance_score(jurisdiction, query) * 0.6 + ) + + if score > 0.3: + # Extract snippet around query + snippet = body[:200] + "..." if len(body) > 200 else body + + results.append(SearchResult( + result_type="meeting", + title=title, + subtitle=f"{jurisdiction}, {state_code} - {meeting_date}", + description=snippet, + url=f"/documents?meeting_id={meeting_id}", + score=score, + metadata={ + "jurisdiction": jurisdiction, + "state": state_code, + "date": meeting_date, + "meeting_id": meeting_id + } + )) + except Exception as e: + logger.debug(f"Error searching {file_path}: {e}") + + except Exception as e: + logger.error(f"Meeting search error: {e}") + + results.sort(key=lambda x: x.score, reverse=True) + return results[:limit] + + +def count_organizations(state: Optional[str] = None, ntee_code: Optional[str] = None, query: Optional[str] = None) -> int: + """Count total organizations matching criteria (for pagination) - cached""" + # Create cache key + cache_key = f"count_{state}_{ntee_code}_{query}" + + # Check cache (1 hour TTL) + now = datetime.now() + if cache_key in _count_cache: + cached_time = _count_cache_ttl.get(cache_key) + if cached_time and (now - cached_time).total_seconds() < 3600: + return _count_cache[cache_key] + + try: + # Determine file path + if state: + file_pattern = f"{GOLD_DIR}/states/{state}/nonprofits_organizations.parquet" + else: + file_pattern = f"{GOLD_DIR}/national/nonprofits_organizations.parquet" + + file_path = Path(file_pattern) + if not file_path.exists(): + return 0 + + conn = duckdb.connect() + + # Detect schema + columns_query = f"DESCRIBE SELECT * FROM '{file_path}' LIMIT 0" + available_columns = set([row[0] for row in conn.execute(columns_query).fetchall()]) + name_col = 'organization_name' if 'organization_name' in available_columns else 'name' + ntee_col = 'ntee_code' if 'ntee_code' in available_columns else 'ntee_cd' + + # Build WHERE clause + where_clauses = [] + params = [] + + if query and query.strip(): + where_clauses.append(f"LOWER({name_col}) LIKE LOWER(?)") + params.append(f'%{query}%') + + if ntee_code and ntee_col in available_columns: + where_clauses.append(f"{ntee_col} LIKE ?") + params.append(f'{ntee_code}%') + + where_sql = " AND ".join(where_clauses) if where_clauses else "TRUE" + + # Count query + count_sql = f"SELECT COUNT(*) FROM '{data_source}' WHERE {where_sql}" + result = conn.execute(count_sql, params).fetchone() + conn.close() + + count = result[0] if result else 0 + + # Cache the result + _count_cache[cache_key] = count + _count_cache_ttl[cache_key] = now + + return count + except Exception as e: + logger.error(f"Count error: {e}") + return 0 + + +def search_organizations(query: str, state: Optional[str] = None, ntee_code: Optional[str] = None, limit: int = 10, offset: int = 0, enrich: bool = False, sort: str = 'relevance', ein: Optional[str] = None) -> List[SearchResult]: + """Search nonprofit organizations using DuckDB for fast Parquet queries + + Args: + enrich: If True, fetch additional data from Every.org API (slower) + sort: Sort order - 'relevance', 'name-asc', 'name-desc', 'revenue-asc', 'revenue-desc', 'assets-asc', 'assets-desc' + ein: If provided, filter to exact EIN match (for direct organization links) + """ + results = [] + + try: + # Determine file path + if state: + file_pattern = f"{GOLD_DIR}/states/{state}/nonprofits_organizations.parquet" + else: + file_pattern = f"{GOLD_DIR}/national/nonprofits_organizations.parquet" + + # Get data source (local or remote HuggingFace URL) + file_path = Path(file_pattern) + data_source = get_data_source(file_path, use_remote=IS_HF_SPACES) + + # Load parquet with caching (speeds up from 2-3s to <10ms) + df = load_parquet_cached(data_source) + + # Initialize DuckDB connection + conn = duckdb.connect() + + # Query the DataFrame directly (DuckDB can query pandas DataFrames) + available_columns = set(df.columns) + + # Detect column name variations (handle different schemas) + name_col = 'organization_name' if 'organization_name' in available_columns else 'name' + ntee_col = 'ntee_code' if 'ntee_code' in available_columns else 'ntee_cd' + revenue_col = 'form_990_total_revenue' if 'form_990_total_revenue' in available_columns else 'revenue_amt' + asset_col = 'form_990_total_assets' if 'form_990_total_assets' in available_columns else 'asset_amt' + income_col = 'form_990_net_income' if 'form_990_net_income' in available_columns else 'income_amt' + + # Build WHERE clauses using detected column names + where_clauses = [] + params = [] + + # EIN filter (exact match - highest priority for direct organization links) + if ein and ein.strip(): + where_clauses.append("ein = ?") + params.append(ein.strip()) + + # Search query (case-insensitive LIKE) - only if query provided and no EIN + if query and query.strip() and not ein: + where_clauses.append(f"LOWER({name_col}) LIKE LOWER(?)") + params.append(f'%{query}%') + + # State filter (if using national file) + if state and not file_pattern.startswith(f"{GOLD_DIR}/states/"): + where_clauses.append("state = ?") + params.append(state) + + # NTEE code filter + if ntee_code and ntee_col in available_columns: + where_clauses.append(f"{ntee_col} LIKE ?") + params.append(f'{ntee_code}%') + + # Default to TRUE if no filters (browse all) + where_sql = " AND ".join(where_clauses) if where_clauses else "TRUE" + + # Build column list with proper NULL handling for missing columns + select_columns = [] + + # Add core columns (with aliases for consistency) + select_columns.append(f'{name_col} as name') + select_columns.append('city') + select_columns.append('state') + select_columns.append(f'{ntee_col} as ntee_cd' if ntee_col in available_columns else 'NULL as ntee_cd') + select_columns.append('ein') + select_columns.append(f'{revenue_col} as revenue_amt' if revenue_col in available_columns else 'NULL as revenue_amt') + select_columns.append(f'{asset_col} as asset_amt' if asset_col in available_columns else 'NULL as asset_amt') + select_columns.append(f'{income_col} as income_amt' if income_col in available_columns else 'NULL as income_amt') + select_columns.append('tax_period' if 'tax_period' in available_columns else 'NULL as tax_period') + + # Track enrichment columns (form_990 and bigquery) + enrichment_cols = [] + enrichment_col_map = {} + + # Check for website columns (multiple possible names) - ALWAYS add if exists + website_col_added = False + for col_name in ['bigquery_website', 'form_990_website', 'website', 'everyorg_website']: + if col_name in available_columns: + select_columns.append(f'{col_name} as enrichment_website') + enrichment_cols.append('enrichment_website') + enrichment_col_map['enrichment_website'] = col_name + website_col_added = True + logger.debug(f"Added website column: {col_name}") + break + + # Check for mission columns + mission_col_added = False + for col_name in ['bigquery_mission', 'form_990_mission', 'mission', 'everyorg_mission']: + if col_name in available_columns: + select_columns.append(f'{col_name} as enrichment_mission') + enrichment_cols.append('enrichment_mission') + enrichment_col_map['enrichment_mission'] = col_name + mission_col_added = True + logger.debug(f"Added mission column: {col_name}") + break + + # Check for logo columns + logo_col_added = False + for col_name in ['logodev_logo_url', 'everyorg_logo_url', 'logo_url']: + if col_name in available_columns: + select_columns.append(f'{col_name} as enrichment_logo') + enrichment_cols.append('enrichment_logo') + enrichment_col_map['enrichment_logo'] = col_name + logo_col_added = True + logger.debug(f"Added logo column: {col_name}") + break + + # Last updated timestamp + for col_name in ['bigquery_updated_date', 'form_990_last_updated', 'everyorg_last_updated']: + if col_name in available_columns: + select_columns.append(f'{col_name} as enrichment_last_updated') + enrichment_cols.append('enrichment_last_updated') + enrichment_col_map['enrichment_last_updated'] = col_name + logger.debug(f"Added timestamp column: {col_name}") + break + + columns_sql = ', '.join(select_columns) + + # Log what we're selecting + logger.info(f"🔍 Enrichment columns to select: {enrichment_cols}") + logger.info(f"📋 Full SQL columns: {columns_sql}") + + # Build ORDER BY clause based on sort parameter + order_by_clauses = [] + + if sort == 'name-asc': + order_by_clauses.append(f"{name_col} ASC") + elif sort == 'name-desc': + order_by_clauses.append(f"{name_col} DESC") + elif sort == 'revenue-desc': + order_by_clauses.append(f"COALESCE(TRY_CAST({revenue_col} AS BIGINT), 0) DESC") + elif sort == 'revenue-asc': + # Low to high: Show positive values first (smallest to largest), then zeros, then negatives + order_by_clauses.append(f""" + CASE + WHEN TRY_CAST({revenue_col} AS BIGINT) IS NULL THEN 3 + WHEN TRY_CAST({revenue_col} AS BIGINT) <= 0 THEN 2 + ELSE 1 + END ASC, + ABS(COALESCE(TRY_CAST({revenue_col} AS BIGINT), 0)) ASC + """) + elif sort == 'assets-desc': + order_by_clauses.append(f"COALESCE(TRY_CAST({asset_col} AS BIGINT), 0) DESC") + elif sort == 'assets-asc': + # Low to high: Show positive values first (smallest to largest), then zeros, then negatives + order_by_clauses.append(f""" + CASE + WHEN TRY_CAST({asset_col} AS BIGINT) IS NULL THEN 3 + WHEN TRY_CAST({asset_col} AS BIGINT) <= 0 THEN 2 + ELSE 1 + END ASC, + ABS(COALESCE(TRY_CAST({asset_col} AS BIGINT), 0)) ASC + """) + elif query and query.strip(): + # Relevance sort (only for search mode) + order_by_clauses.append("score DESC") + order_by_clauses.append(f"COALESCE(TRY_CAST({revenue_col} AS BIGINT), 0) DESC") + else: + # Default browse mode: sort by revenue/assets + order_by_clauses.append(f"COALESCE(TRY_CAST({revenue_col} AS BIGINT), 0) DESC") + order_by_clauses.append(f"COALESCE(TRY_CAST({asset_col} AS BIGINT), 0) DESC") + + # Always add name as final sort for consistency + if 'name' not in sort: + order_by_clauses.append(f"{name_col}") + + order_by_sql = ', '.join(order_by_clauses) + + # SQL query with relevance scoring (browse mode if no query) + if query and query.strip(): + # Search mode: score by text match + sql = f""" + SELECT + {columns_sql}, + CASE + WHEN LOWER({name_col}) LIKE LOWER(?) THEN 1.5 + WHEN LOWER({name_col}) LIKE LOWER(?) THEN 1.0 + ELSE 0.5 + END as score + FROM df + WHERE {where_sql} + ORDER BY {order_by_sql} + LIMIT ? OFFSET ? + """ + # Execute query with scoring parameters + query_params = [f'{query}%', f'%{query}%'] + params + [limit, offset] + else: + # Browse mode: sort by size/activity + sql = f""" + SELECT + {columns_sql}, + 1.0 as score + FROM df + WHERE {where_sql} + ORDER BY {order_by_sql} + LIMIT ? OFFSET ? + """ + # Execute query without scoring parameters + query_params = params + [limit, offset] + + rows = conn.execute(sql, query_params).fetchall() + + # NTEE code descriptions for better context + ntee_descriptions = { + 'E': 'Health Services', + 'E60': 'Health Support Services', + 'E61': 'Blood Supply', + 'E62': 'Emergency Medical Services', + 'E65': 'Organ & Tissue Banks', + 'E70': 'Public Health', + 'E80': 'Health Treatment - Primary Care', + 'E90': 'Nursing Services', + 'E20': 'Hospitals & Primary Medical Care', + 'E30': 'Ambulatory & Primary Health Care', + 'E32': 'Clinics & Community Health Centers', + 'P': 'Human Services', + 'B': 'Education', + 'X': 'Religion-Related', + 'A': 'Arts, Culture & Humanities', + } + + # Convert to SearchResult objects with intelligent enrichment + for row in rows: + # Unpack base columns (now includes tax_period) + org_name, city, state_code, ntee, ein, revenue, assets, income, tax_period = row[:9] + + # Unpack optional enrichment columns if present + existing_data = {} + idx = 9 + + if 'enrichment_website' in enrichment_cols: + existing_data['enrichment_website'] = row[idx] + # Only log non-null websites to reduce spam + if row[idx] and str(row[idx]) != 'nan': + logger.debug(f"✅ Website: {row[idx]}") + idx += 1 + if 'enrichment_mission' in enrichment_cols: + existing_data['enrichment_mission'] = row[idx] + idx += 1 + if 'enrichment_logo' in enrichment_cols: + existing_data['enrichment_logo'] = row[idx] + idx += 1 + if 'enrichment_last_updated' in enrichment_cols: + existing_data['enrichment_last_updated'] = row[idx] + idx += 1 + + score = row[-1] # Score is always last + + # Parse tax year from tax_period (format: YYYYMM) + tax_year = None + if tax_period and str(tax_period).isdigit() and len(str(tax_period)) >= 4: + tax_year = int(str(tax_period)[:4]) + + # Get enriched data with intelligent backfill (only if requested) + enrichment = get_enrichment_data(ein, existing_data) if (ein and enrich) else {} + + # Build a more informative description + ntee_desc = None + if ntee: + # Try exact match first, then prefix match + ntee_desc = ntee_descriptions.get(ntee) + if not ntee_desc: + # Try first character (major category) + ntee_desc = ntee_descriptions.get(ntee[0]) if ntee else None + + # Use enriched mission as primary description, fallback to NTEE + financial + description = enrichment.get('mission') if enrichment.get('mission') else None + + # Validate mission: if it contains a different org name, it's stale data + if description and org_name: + # Check if mission mentions a completely different org name + # (e.g., "Catalyst Institute" when org name is "CAREQUEST INSTITUTE") + mission_lower = description.lower() + name_words = set(org_name.lower().split()) + + # If mission starts with an org name that's not in our actual org name, skip it + first_sentence = description.split('.')[0].lower() + if ' is a nonprofit' in first_sentence or ' is an nonprofit' in first_sentence: + # Extract the subject (organization name before "is a nonprofit") + subject = first_sentence.split(' is a')[0].strip() + subject_words = set(subject.split()) + + # If the subject shares NO significant words with our org name, it's stale + # (e.g., "catalyst institute" vs "carequest institute") + significant_words = subject_words - {'the', 'a', 'an', 'of', 'for', 'and', 'inc', 'llc'} + name_significant = name_words - {'the', 'a', 'an', 'of', 'for', 'and', 'inc', 'llc', 'institute'} + + if significant_words and not (significant_words & name_significant): + # Stale data - mission talks about a different org + logger.warning(f"Stale mission data for {org_name}: '{subject}' != '{org_name}'") + description = None + + if not description: + description_parts = [] + if ntee_desc: + description_parts.append(ntee_desc) + + # Convert financial data to numbers (handle None and string types) + try: + revenue_num = float(revenue) if revenue else 0 + assets_num = float(assets) if assets else 0 + except (ValueError, TypeError): + revenue_num = 0 + assets_num = 0 + + if revenue_num > 0: + description_parts.append(f"Revenue: ${revenue_num:,.0f}") + elif assets_num > 0: + description_parts.append(f"Assets: ${assets_num:,.0f}") + + if not description_parts: + description_parts.append(f"Nonprofit serving {city}") + + description = " • ".join(description_parts) + + # Build metadata with enriched fields + metadata = { + "ein": ein, + "city": city, + "state": state_code, + "ntee_code": ntee, + "revenue": revenue, + "assets": assets, + "income": income, + "tax_year": tax_year, + "data_sources": [] + } + + # ALWAYS add enrichment from parquet columns (existing_data) - no enrich flag needed + if existing_data.get('enrichment_website'): + metadata['website'] = existing_data['enrichment_website'] + metadata['data_sources'].append('cached') + + if existing_data.get('enrichment_mission'): + metadata['mission'] = existing_data['enrichment_mission'] + if 'cached' not in metadata['data_sources']: + metadata['data_sources'].append('cached') + + if existing_data.get('enrichment_logo'): + metadata['logo_url'] = existing_data['enrichment_logo'] + if 'cached' not in metadata['data_sources']: + metadata['data_sources'].append('cached') + + # Add API enrichment if requested (enrich=true) + if enrichment: + if enrichment.get('website') and not metadata.get('website'): + metadata['website'] = enrichment['website'] + if enrichment.get('logo_url'): + metadata['logo_url'] = enrichment['logo_url'] + if enrichment.get('profile_url'): + metadata['profile_url'] = enrichment['profile_url'] + if enrichment.get('causes'): + metadata['causes'] = enrichment['causes'] + # Add API data sources + for source in enrichment.get('data_sources', []): + if source not in metadata['data_sources']: + metadata['data_sources'].append(source) + + results.append(SearchResult( + result_type="organization", + title=org_name if org_name else "Unknown", + subtitle=f"{city}, {state_code}" + (f" - NTEE: {ntee}" if ntee else ""), + description=description, + url=f"/search?types=organizations&state={state_code}&ein={ein}", + score=score, + metadata=metadata + )) + + conn.close() + logger.info(f"DuckDB search found {len(results)} organizations for query '{query}'") + + except Exception as e: + logger.error(f"Organization search error: {e}") + + return results + + +def search_causes(query: str, limit: int = 10) -> List[SearchResult]: + """Search causes and NTEE categories - supports browse mode""" + results = [] + + try: + # Get data source (local or remote HuggingFace URL) + ntee_file = GOLD_DIR / "reference" / "causes_ntee_codes.parquet" + data_source = get_data_source(ntee_file, use_remote=IS_HF_SPACES) + + # Load with caching + df = load_parquet_cached(data_source) + logger.debug(f"Loaded {len(df)} NTEE codes from cache") + + for _, row in df.iterrows(): + code = str(row.get('ntee_code', '')) + description = str(row.get('description', '')) + ntee_type = str(row.get('ntee_type', '')) + + # Browse mode: return all causes + # Search mode: filter by relevance + if query and query.strip(): + score = max( + calculate_relevance_score(description, query), + calculate_relevance_score(code, query) + ) + if score <= 0.3: + continue # Skip low relevance results + else: + score = 1.0 # Default score for browse mode + + results.append(SearchResult( + result_type="cause", + title=description, + subtitle=f"NTEE Code: {code}", + description=f"Category type: {ntee_type}", + url=f"/nonprofits?ntee_code={code}", + score=score, + metadata={ + "ntee_code": code, + "ntee_type": ntee_type + } + )) + + logger.info(f"Found {len(results)} cause results for query '{query}'") + + except Exception as e: + logger.error(f"Cause search error: {e}") + + results.sort(key=lambda x: x.score, reverse=True) + return results[:limit] + + +def search_jurisdictions(query: str, state: Optional[str] = None, city: Optional[str] = None, jurisdiction_levels: Optional[List[str]] = None, limit: int = 10, offset: int = 0) -> List[SearchResult]: + """Search cities, counties, townships, and school districts using DuckDB""" + all_results = [] + + try: + conn = duckdb.connect() + + # Map frontend level IDs to file keys + level_mapping = { + 'city': 'city', + 'county': 'county', + 'town': 'township', + 'village': 'township', + 'school_district': 'school district', + 'special_district': 'school district', # Use school district as proxy + 'state': None # States handled separately if needed + } + + # Define jurisdiction files with priority scores + jurisdiction_files = { + 'county': (f"{GOLD_DIR}/reference/jurisdictions_counties.parquet", 1.3), # Boost counties + 'city': (f"{GOLD_DIR}/reference/jurisdictions_cities.parquet", 1.0), + 'school district': (f"{GOLD_DIR}/reference/jurisdictions_school_districts.parquet", 1.1), # Boost school districts + 'township': (f"{GOLD_DIR}/reference/jurisdictions_townships.parquet", 0.9) + } + + # Filter jurisdiction files based on selected levels + if jurisdiction_levels: + # Map selected levels to file keys + selected_file_keys = set() + for level in jurisdiction_levels: + file_key = level_mapping.get(level) + if file_key: + selected_file_keys.add(file_key) + + # Filter to only selected types + if selected_file_keys: + jurisdiction_files = { + k: v for k, v in jurisdiction_files.items() + if k in selected_file_keys + } + + # Fetch enough results from each type to ensure diversity + # Even with small limits, we want representation from each type + per_type_limit = max(limit, 15) + + for jtype, (file_path, type_score) in jurisdiction_files.items(): + file_path_obj = Path(file_path) + if not file_path_obj.exists(): + continue + + try: + # Build SQL query - use state column (lowercase) + where_clauses = [] + params = [] + + if state: + where_clauses.append("state = ?") + params.append(state) + + if city and query: + # If city is specified, search for jurisdictions matching the city name + where_clauses.append("LOWER(NAME) LIKE LOWER(?)") + params.append(f"%{city}%") + elif query: + # General search across jurisdiction names + where_clauses.append("LOWER(NAME) LIKE LOWER(?)") + params.append(f"%{query}%") + + where_clause = " AND ".join(where_clauses) if where_clauses else "1=1" + + # Calculate name match score if query provided + score_expr = f"{type_score}" + if query: + score_expr = f"""CASE + WHEN LOWER(NAME) = LOWER('{query}') THEN {type_score} * 2.0 + WHEN LOWER(NAME) LIKE LOWER('{query}%') THEN {type_score} * 1.5 + ELSE {type_score} + END""" + + sql = f""" + SELECT + NAME as name, + state, + GEOID as geoid, + jurisdiction_type, + {score_expr} as score + FROM read_parquet(?) + WHERE {where_clause} + ORDER BY score DESC, NAME ASC + LIMIT ? + """ + + query_params = [str(file_path_obj)] + params + [per_type_limit] + df = conn.execute(sql, query_params).fetchdf() + + for _, row in df.iterrows(): + jurisdiction_label = row['jurisdiction_type'].replace('_', ' ').title() + all_results.append(SearchResult( + result_type='jurisdiction', + title=f"{row['name']}", + subtitle=f"{jurisdiction_label}", + description=f"{jurisdiction_label} in {row['state']}", + url=f"/jurisdictions/{row['geoid']}", + score=float(row['score']), + metadata={ + 'state': row['state'], + 'geoid': row['geoid'], + 'type': row['jurisdiction_type'] + } + )) + + except Exception as e: + logger.error(f"Error searching {jtype} jurisdictions: {e}") + continue + + except Exception as e: + logger.error(f"Jurisdiction search error: {e}") + + # Sort all results by score, then apply pagination + all_results.sort(key=lambda x: (x.score, x.title), reverse=True) + return all_results[offset:offset + limit] + + +@router.get("/api/search") +@router.get("/api/search/", include_in_schema=False) +async def unified_search( + q: Optional[str] = Query(None, description="Search query (optional - browse by filters if omitted)"), + types: Optional[str] = Query(None, description="Comma-separated result types: contacts,meetings,organizations,causes,jurisdictions"), + state: Optional[str] = Query(None, description="Filter by state (2-letter code)"), + city: Optional[str] = Query(None, description="Filter by city name"), + jurisdiction_levels: Optional[str] = Query(None, description="Comma-separated jurisdiction levels: city,county,town,village,school_district,special_district,state"), + ntee_code: Optional[str] = Query(None, description="Filter organizations by NTEE code"), + ein: Optional[str] = Query(None, description="Filter organizations by exact EIN (for direct organization links)"), + limit: int = Query(20, ge=1, le=100, description="Maximum results per type"), + offset: int = Query(0, ge=0, description="Number of results to skip (for pagination)"), + page: int = Query(1, ge=1, description="Page number (alternative to offset)"), + enrich: bool = Query(False, description="Enable API enrichment (slower - fetches logos, causes from Every.org)"), + sort: str = Query('relevance', description="Sort order: relevance, name-asc, name-desc, revenue-asc, revenue-desc, assets-asc, assets-desc") +): + """ + Unified search across all data types + + Search for contacts, meetings, organizations, and causes in one query. + **NEW:** Query is now optional - you can browse by state/type without searching! + + **Pagination:** + - Use `offset` to skip results: `offset=20` skips first 20 results + - Or use `page` with `limit`: `page=2&limit=20` gets results 21-40 + - `offset` takes precedence if both are provided + + **Examples:** + - `/api/search?q=dental` - Search everything for "dental" + - `/api/search?types=organizations&state=GA` - Browse all orgs in Georgia + - `/api/search?q=budget&types=meetings` - Search only meetings + - `/api/search?q=health&state=AL` - Search in Alabama only + - `/api/search?q=education&types=organizations,causes` - Search orgs and causes + - `/api/search?q=health&state=MA&page=2&limit=20` - Page 2 of MA health results + """ + # 🔍 DEBUG LOGGING - Log all incoming request parameters + logger.info(f"🔍 SEARCH REQUEST: q={q!r}, types={types!r}, state={state!r}, city={city!r}, jurisdiction_levels={jurisdiction_levels!r}, ntee_code={ntee_code!r}, ein={ein!r}, limit={limit}, offset={offset}, page={page}, enrich={enrich}, sort={sort!r}") + + try: + # Calculate offset from page if offset not explicitly provided + if offset == 0 and page > 1: + offset = (page - 1) * limit + + # Parse requested types + if types: + requested_types = [t.strip() for t in types.split(',')] + else: + requested_types = ['contacts', 'meetings', 'organizations', 'causes', 'jurisdictions'] + + # Parse jurisdiction levels if provided + jurisdiction_levels_list = None + if jurisdiction_levels: + jurisdiction_levels_list = [level.strip() for level in jurisdiction_levels.split(',')] + + logger.info(f"📋 Requested types: {requested_types}, calculated offset: {offset}") + + all_results = [] + + # Optimize for single-type browse mode (no query) + # Let database handle pagination for efficiency + use_db_pagination = not q and len(requested_types) == 1 + + if use_db_pagination: + # Single-type browse: pass offset to DB for efficient pagination + search_limit = limit + search_offset = offset + else: + # Multi-type or search mode: fetch extra for mixing/sorting + search_limit = offset + limit + 100 + search_offset = 0 + + if 'contacts' in requested_types: + # Use PostgreSQL for fast indexed search + contact_results_pg = await search_postgres.search_contacts_pg(q, state, limit=search_limit) + contact_results = [convert_pg_result(r) for r in contact_results_pg] + logger.info(f"👤 Contacts search returned {len(contact_results)} results") + all_results.extend(contact_results) + + if 'meetings' in requested_types: + # Use PostgreSQL for fast indexed search + meeting_results_pg = await search_postgres.search_events_pg(q, state, limit=search_limit) + meeting_results = [convert_pg_result(r) for r in meeting_results_pg] + logger.info(f"📅 Meetings search returned {len(meeting_results)} results") + all_results.extend(meeting_results) + + if 'organizations' in requested_types: + # Use PostgreSQL for fast indexed search + org_results_pg = await search_postgres.search_organizations_pg(q, state, ntee_code, ein, limit=search_limit, offset=search_offset, sort=sort) + org_results = [convert_pg_result(r) for r in org_results_pg] + logger.info(f"🏢 Organizations search returned {len(org_results)} results") + all_results.extend(org_results) + + if 'causes' in requested_types: + cause_results = search_causes(q or "", limit=search_limit) + logger.info(f"🎯 Causes search returned {len(cause_results)} results") + all_results.extend(cause_results) + + if 'jurisdictions' in requested_types: + # Use PostgreSQL for fast indexed search + jurisdiction_results_pg = await search_postgres.search_jurisdictions_pg(q, state, city, jurisdiction_levels_list, limit=search_limit, offset=search_offset) + jurisdiction_results = [convert_pg_result(r) for r in jurisdiction_results_pg] + logger.info(f"🏛️ Jurisdictions search returned {len(jurisdiction_results)} results") + all_results.extend(jurisdiction_results) + + # Sort all results by score + all_results.sort(key=lambda x: x.score, reverse=True) + + logger.info(f"📊 Total combined results: {len(all_results)}, applying pagination (offset={offset}, limit={limit})") + + # Apply pagination + if use_db_pagination: + # DB already paginated - use all results + paginated_results = all_results + else: + # Paginate in-memory from combined results + paginated_results = all_results[offset:offset + limit] + + logger.info(f"✂️ Paginated results: {len(paginated_results)} items") + + # Group by type for response + grouped_results = { + 'contacts': [r.to_dict() for r in paginated_results if r.result_type == 'contact'], + 'meetings': [r.to_dict() for r in paginated_results if r.result_type == 'meeting'], + 'organizations': [r.to_dict() for r in paginated_results if r.result_type == 'organization'], + 'causes': [r.to_dict() for r in paginated_results if r.result_type == 'cause'], + 'jurisdictions': [r.to_dict() for r in paginated_results if r.result_type == 'jurisdiction'], + } + + logger.info(f"📦 Grouped results - contacts:{len(grouped_results['contacts'])}, meetings:{len(grouped_results['meetings'])}, organizations:{len(grouped_results['organizations'])}, causes:{len(grouped_results['causes'])}, jurisdictions:{len(grouped_results['jurisdictions'])}") + + # Calculate total results per type (from all_results before pagination) + type_totals = { + 'contacts': len([r for r in all_results if r.result_type == 'contact']), + 'meetings': len([r for r in all_results if r.result_type == 'meeting']), + 'organizations': len([r for r in all_results if r.result_type == 'organization']), + 'causes': len([r for r in all_results if r.result_type == 'cause']), + 'jurisdictions': len([r for r in all_results if r.result_type == 'jurisdiction']), + } + + # Calculate total results + # For single-type browse mode, get accurate count from database + if not q and len(requested_types) == 1: + # Browse mode: count total matching records in DB + if 'organizations' in requested_types: + total_results = count_organizations(state=state, ntee_code=ntee_code, query=q) + type_totals['organizations'] = total_results # Use accurate DB count + else: + # Fallback to fetched results for other types + total_results = len(all_results) + else: + # Search/multi-type mode: use fetched results + total_results = len(all_results) + + total_pages = (total_results + limit - 1) // limit # Ceiling division + + response_data = { + "query": q or "", + "total_results": total_results, + "type_totals": type_totals, # Add per-type totals + "results": grouped_results, + "pagination": { + "page": page if offset == 0 or offset == (page - 1) * limit else (offset // limit) + 1, + "limit": limit, + "offset": offset, + "total_pages": total_pages, + "has_next": offset + limit < total_results, + "has_prev": offset > 0 + }, + "filters": { + "state": state, + "ntee_code": ntee_code, + "types": requested_types, + "sort": sort + } + } + + logger.info(f"✅ Search complete - returning {total_results} total results, {len(paginated_results)} on this page") + return response_data + + except Exception as e: + logger.error(f"❌ Search error: {type(e).__name__}: {e}") + logger.exception("Full traceback:") + + # Parse error into structured response + error_detail = parse_error(e, context={ + "query": q, + "state": state, + "types": types, + "data_type": "search" + }) + + return JSONResponse( + status_code=500, + content=error_detail.model_dump() + ) + + +@router.get("/api/search/suggest") +async def search_suggestions( + q: str = Query(..., min_length=1, description="Partial search query"), + limit: int = Query(5, ge=1, le=20, description="Maximum suggestions") +): + """ + Get search suggestions/autocomplete + + Returns quick suggestions as user types + """ + try: + suggestions = [] + + # Common search terms + common_terms = [ + "dental health", "oral health", "affordable housing", "public transit", + "school funding", "budget", "water quality", "parks", "zoning", + "police", "fire department", "mental health", "food assistance", + "senior services", "youth programs", "employment", "job training" + ] + + # Filter suggestions + q_lower = q.lower() + suggestions = [term for term in common_terms if q_lower in term.lower()] + + return { + "query": q, + "suggestions": suggestions[:limit] + } + + except Exception as e: + logger.error(f"Suggestion error: {e}") + + # Parse error into structured response + error_detail = parse_error(e, context={ + "query": q, + "data_type": "suggestions" + }) + + return JSONResponse( + status_code=500, + content=error_detail.model_dump() + ) diff --git a/api/routes/search_postgres.py b/api/routes/search_postgres.py new file mode 100644 index 0000000000000000000000000000000000000000..6469f7ce9afc1d80dcb5a72d3e081797174c00fb --- /dev/null +++ b/api/routes/search_postgres.py @@ -0,0 +1,535 @@ +""" +PostgreSQL-based search functions +Uses indexed search tables for fast queries (10-100x faster than parquet) +""" +from typing import Optional, List +from loguru import logger +import asyncpg +import os +from datetime import datetime +from dataclasses import dataclass + +# Database configuration +# Priority: NEON_DATABASE_URL_DEV (local) > NEON_DATABASE_URL (production) +NEON_DATABASE_URL_DEV = os.getenv('NEON_DATABASE_URL_DEV') +NEON_DATABASE_URL = os.getenv('NEON_DATABASE_URL') + +# Use dev database for local development, production database for deployed environments +DATABASE_URL = NEON_DATABASE_URL_DEV or NEON_DATABASE_URL + +# Connection pool (created on first request) +_db_pool = None + + +@dataclass +class SearchResult: + """Search result data class""" + result_type: str + title: str + subtitle: str + description: str + url: str + score: float + metadata: dict + + +async def get_db_pool(): + """Get or create database connection pool""" + global _db_pool + if _db_pool is None: + if not DATABASE_URL: + raise ValueError("DATABASE_URL not configured") + + db_type = "Development (Local PostgreSQL)" if NEON_DATABASE_URL_DEV else "Production (Neon)" + logger.info(f"🗄️ Creating connection pool to {db_type}") + + _db_pool = await asyncpg.create_pool(DATABASE_URL, min_size=2, max_size=20) + return _db_pool + + +async def search_jurisdictions_pg( + query: Optional[str] = None, + state: Optional[str] = None, + city: Optional[str] = None, + jurisdiction_levels: Optional[List[str]] = None, + limit: int = 10, + offset: int = 0 +) -> List[SearchResult]: + """ + Search jurisdictions using PostgreSQL full-text search + + Args: + query: Search text (jurisdiction name) + state: Filter by state code (e.g., 'MA') + city: Filter by city name + jurisdiction_levels: Filter by types (city, county, town, school_district, etc.) + limit: Max results + offset: Pagination offset + + Returns: + List of SearchResult objects + """ + try: + pool = await get_db_pool() + + # Map frontend level IDs to database types + level_mapping = { + 'city': 'city', + 'county': 'county', + 'town': 'town', + 'village': 'village', + 'school_district': 'school_district', + 'special_district': 'special_district', + 'state': 'state' + } + + # Build SQL query + where_clauses = [] + params = [] + param_idx = 1 + has_query = query and query.strip() + + # Text search filter first (if present) - must be $1 for score calculation + score_param_idx = None + if has_query: + where_clauses.append(f"to_tsvector('english', name) @@ plainto_tsquery('english', ${param_idx})") + params.append(query) + score_param_idx = param_idx + param_idx += 1 + + # State filter + if state: + where_clauses.append(f"state = ${param_idx}") + params.append(state.upper()) + param_idx += 1 + + # City filter + if city: + where_clauses.append(f"LOWER(name) LIKE LOWER(${param_idx})") + params.append(f"%{city}%") + param_idx += 1 + + # Jurisdiction level filter + if jurisdiction_levels: + db_types = [level_mapping.get(level) for level in jurisdiction_levels if level_mapping.get(level)] + if db_types: + placeholders = ','.join([f"${param_idx + i}" for i in range(len(db_types))]) + where_clauses.append(f"type IN ({placeholders})") + params.extend(db_types) + param_idx += len(db_types) + + # Build final WHERE clause + where_sql = " AND ".join(where_clauses) if where_clauses else "TRUE" + + # Select clause and order by + if has_query: + select_score = f"ts_rank(to_tsvector('english', name), plainto_tsquery('english', ${score_param_idx})) as score" + order_by = f"score DESC, name ASC" + else: + select_score = "1.0 as score" + order_by = "name ASC" + + # Build complete query + sql = f""" + SELECT + name, + type, + state, + county, + geoid, + population, + {select_score} + FROM jurisdictions_search + WHERE {where_sql} + ORDER BY {order_by} + LIMIT ${param_idx} + OFFSET ${param_idx + 1} + """ + + # Add limit and offset + params.append(limit) + params.append(offset) + + async with pool.acquire() as conn: + rows = await conn.fetch(sql, *params) + + results = [] + for row in rows: + jurisdiction_label = row['type'].replace('_', ' ').title() + + results.append(SearchResult( + result_type='jurisdiction', + title=row['name'], + subtitle=f"{jurisdiction_label}", + description=f"{jurisdiction_label} in {row['state']}" + (f" • Pop: {row['population']:,}" if row['population'] else ""), + url=f"/jurisdictions/{row['geoid']}" if row['geoid'] else f"/jurisdictions/{row['name']}", + score=float(row.get('score', 1.0)) if query else 1.0, + metadata={ + 'state': row['state'], + 'geoid': row['geoid'], + 'type': row['type'], + 'county': row['county'], + 'population': row['population'] + } + )) + + logger.info(f"🏛️ PostgreSQL jurisdictions search: {len(results)} results") + return results + + except Exception as e: + logger.error(f"PostgreSQL jurisdictions search error: {e}") + return [] + + +async def search_contacts_pg( + query: Optional[str] = None, + state: Optional[str] = None, + limit: int = 10 +) -> List[SearchResult]: + """ + Search contacts (nonprofit officers, local officials) using PostgreSQL + + Args: + query: Search text (name, title, organization) + state: Filter by state code + limit: Max results + + Returns: + List of SearchResult objects + """ + try: + pool = await get_db_pool() + + # Build WHERE clauses + where_clauses = [] + params = [] + param_idx = 1 + + if state: + where_clauses.append(f"state = ${param_idx}") + params.append(state.upper()) + param_idx += 1 + + # Text search across name and organization + if query and query.strip(): + where_clauses.append(f"""( + to_tsvector('english', name) @@ plainto_tsquery('english', ${param_idx}) + OR to_tsvector('english', COALESCE(organization_name, '')) @@ plainto_tsquery('english', ${param_idx}) + OR LOWER(title) LIKE LOWER(${param_idx + 1}) + )""") + params.append(query) + params.append(f"%{query}%") + param_idx += 2 + + # Rank by relevance + order_by = f""" + GREATEST( + ts_rank(to_tsvector('english', name), plainto_tsquery('english', ${param_idx - 2})), + ts_rank(to_tsvector('english', COALESCE(organization_name, '')), plainto_tsquery('english', ${param_idx - 2})) + ) DESC, name ASC + """ + else: + order_by = "name ASC" + + where_sql = " AND ".join(where_clauses) if where_clauses else "TRUE" + + sql = f""" + SELECT + name, + title, + organization_name, + organization_ein, + city, + state, + role_type, + compensation, + source + FROM contacts_search + WHERE {where_sql} + ORDER BY {order_by} + LIMIT ${param_idx} + """ + params.append(limit) + + async with pool.acquire() as conn: + rows = await conn.fetch(sql, *params) + + results = [] + for row in rows: + org_display = row['organization_name'] or 'Unknown Organization' + location = f"{row['city']}, {row['state']}" if row['city'] and row['state'] else (row['state'] or '') + + results.append(SearchResult( + result_type='contact', + title=row['name'], + subtitle=f"{row['title'] or 'Officer'} - {org_display}", + description=f"{row['role_type'] or 'Contact'} in {location}", + url=f"/people/{row['name'].replace(' ', '-')}", + score=1.0, + metadata={ + 'title': row['title'], + 'organization': org_display, + 'organization_ein': row['organization_ein'], + 'state': row['state'], + 'city': row['city'], + 'role_type': row['role_type'], + 'compensation': row['compensation'], + 'source': row['source'] + } + )) + + logger.info(f"👤 PostgreSQL contacts search: {len(results)} results") + return results + + except Exception as e: + logger.error(f"PostgreSQL contacts search error: {e}") + return [] + + +async def search_organizations_pg( + query: Optional[str] = None, + state: Optional[str] = None, + ntee_code: Optional[str] = None, + ein: Optional[str] = None, + limit: int = 10, + offset: int = 0, + sort: str = 'relevance' +) -> List[SearchResult]: + """ + Search nonprofit organizations using PostgreSQL + + Args: + query: Search text (organization name) + state: Filter by state code + ntee_code: Filter by NTEE code prefix + ein: Exact EIN match + limit: Max results + offset: Pagination offset + sort: Sort order (relevance, name-asc, name-desc, revenue-asc, revenue-desc, assets-asc, assets-desc) + + Returns: + List of SearchResult objects + """ + try: + pool = await get_db_pool() + + # Build WHERE clauses + where_clauses = [] + params = [] + param_idx = 1 + + # EIN exact match (highest priority) + if ein: + where_clauses.append(f"ein = ${param_idx}") + params.append(ein.strip()) + param_idx += 1 + + # State filter + if state: + where_clauses.append(f"state = ${param_idx}") + params.append(state.upper()) + param_idx += 1 + + # NTEE code filter + if ntee_code: + where_clauses.append(f"ntee_code LIKE ${param_idx}") + params.append(f"{ntee_code}%") + param_idx += 1 + + # Text search (if no EIN specified) + if query and query.strip() and not ein: + where_clauses.append(f"to_tsvector('english', name) @@ plainto_tsquery('english', ${param_idx})") + params.append(query) + param_idx += 1 + + where_sql = " AND ".join(where_clauses) if where_clauses else "TRUE" + + # Determine sort order + if sort == 'name-asc': + order_by = "name ASC" + elif sort == 'name-desc': + order_by = "name DESC" + elif sort == 'revenue-asc': + order_by = "revenue ASC NULLS LAST" + elif sort == 'revenue-desc': + order_by = "revenue DESC NULLS LAST" + elif sort == 'assets-asc': + order_by = "assets ASC NULLS LAST" + elif sort == 'assets-desc': + order_by = "assets DESC NULLS LAST" + elif query and query.strip() and not ein: + # Relevance ranking for text search + order_by = f"ts_rank(to_tsvector('english', name), plainto_tsquery('english', ${param_idx - 1})) DESC, name ASC" + else: + order_by = "name ASC" + + sql = f""" + SELECT + ein, + name, + city, + state, + county, + ntee_code, + ntee_description, + revenue, + assets, + income, + tax_period + FROM nonprofits_search + WHERE {where_sql} + ORDER BY {order_by} + LIMIT ${param_idx} + OFFSET ${param_idx + 1} + """ + params.append(limit) + params.append(offset) + + async with pool.acquire() as conn: + rows = await conn.fetch(sql, *params) + + results = [] + for row in rows: + location = f"{row['city']}, {row['state']}" if row['city'] and row['state'] else (row['state'] or '') + + # Format financials + financials = [] + if row['revenue']: + financials.append(f"Revenue: ${row['revenue']:,}") + if row['assets']: + financials.append(f"Assets: ${row['assets']:,}") + + description = f"{row['ntee_description'] or 'Nonprofit organization'}" + if financials: + description += " • " + " • ".join(financials) + + results.append(SearchResult( + result_type='organization', + title=row['name'], + subtitle=location, + description=description, + url=f"/organizations/{row['ein']}", + score=1.0, + metadata={ + 'ein': row['ein'], + 'state': row['state'], + 'city': row['city'], + 'county': row['county'], + 'ntee_code': row['ntee_code'], + 'ntee_description': row['ntee_description'], + 'revenue': row['revenue'], + 'assets': row['assets'], + 'income': row['income'], + 'tax_period': row['tax_period'] + } + )) + + logger.info(f"🏢 PostgreSQL organizations search: {len(results)} results") + return results + + except Exception as e: + logger.error(f"PostgreSQL organizations search error: {e}") + return [] + + +async def search_events_pg( + query: Optional[str] = None, + state: Optional[str] = None, + limit: int = 10 +) -> List[SearchResult]: + """ + Search meetings/events using PostgreSQL + + Args: + query: Search text (title, jurisdiction, description) + state: Filter by state code + limit: Max results + + Returns: + List of SearchResult objects + """ + try: + pool = await get_db_pool() + + # Build WHERE clauses + where_clauses = [] + params = [] + param_idx = 1 + + if state: + where_clauses.append(f"state = ${param_idx}") + params.append(state.upper()) + param_idx += 1 + + # Text search + if query and query.strip(): + where_clauses.append(f"""( + to_tsvector('english', title) @@ plainto_tsquery('english', ${param_idx}) + OR LOWER(jurisdiction_name) LIKE LOWER(${param_idx + 1}) + )""") + params.append(query) + params.append(f"%{query}%") + param_idx += 2 + + order_by = f"ts_rank(to_tsvector('english', title), plainto_tsquery('english', ${param_idx - 2})) DESC, event_date DESC" + else: + order_by = "event_date DESC" + + where_sql = " AND ".join(where_clauses) if where_clauses else "TRUE" + + sql = f""" + SELECT + id, + title, + description, + event_date, + jurisdiction_name, + jurisdiction_type, + state, + city, + video_url, + agenda_url + FROM events_search + WHERE {where_sql} + ORDER BY {order_by} + LIMIT ${param_idx} + """ + params.append(limit) + + async with pool.acquire() as conn: + rows = await conn.fetch(sql, *params) + + results = [] + for row in rows: + location = f"{row['jurisdiction_name']}, {row['state']}" if row['jurisdiction_name'] and row['state'] else '' + date_str = row['event_date'].strftime('%Y-%m-%d') if row['event_date'] else '' + + description = (row['description'] or '')[:200] + if len(description) == 200: + description += "..." + + results.append(SearchResult( + result_type='meeting', + title=row['title'], + subtitle=f"{location} - {date_str}", + description=description, + url=f"/documents?meeting_id={row['id']}", + score=1.0, + metadata={ + 'jurisdiction': row['jurisdiction_name'], + 'jurisdiction_type': row['jurisdiction_type'], + 'state': row['state'], + 'city': row['city'], + 'date': date_str, + 'meeting_id': row['id'], + 'video_url': row['video_url'], + 'agenda_url': row['agenda_url'] + } + )) + + logger.info(f"📅 PostgreSQL events search: {len(results)} results") + return results + + except Exception as e: + logger.error(f"PostgreSQL events search error: {e}") + return [] diff --git a/api/routes/social.py b/api/routes/social.py new file mode 100644 index 0000000000000000000000000000000000000000..46aa35c0f596314afa4ae26a613a00d4cd713c76 --- /dev/null +++ b/api/routes/social.py @@ -0,0 +1,544 @@ +""" +Social features API routes - following, followers, feeds +""" +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from sqlalchemy import func, or_, and_ +from typing import List, Optional +from pydantic import BaseModel +from datetime import datetime + +from api.database import get_db +from api.auth import get_current_user +from api.models import ( + User, Official, Organization, Cause, + UserFollow, OfficialFollow, OrganizationFollow, CauseFollow +) + +router = APIRouter(prefix="/api/social", tags=["social"]) + + +# ============================================================================ +# PYDANTIC MODELS +# ============================================================================ + +class FollowResponse(BaseModel): + """Response after follow/unfollow action""" + success: bool + following: bool + follower_count: int + message: str + + +class FollowerStats(BaseModel): + """Follower/following statistics""" + followers: int + following: int + following_users: int + following_officials: int + following_organizations: int + following_causes: int + + +class UserSummary(BaseModel): + """Brief user info for lists""" + id: int + username: Optional[str] + full_name: Optional[str] + avatar_url: Optional[str] + created_at: datetime + + class Config: + from_attributes = True + + +class OfficialSummary(BaseModel): + """Brief official info for lists""" + id: int + name: str + slug: str + title: Optional[str] + photo_url: Optional[str] + office: Optional[str] + city: Optional[str] + state: Optional[str] + follower_count: int + is_verified: bool + + class Config: + from_attributes = True + + +class OrganizationSummary(BaseModel): + """Brief organization info for lists""" + id: int + name: str + slug: str + description: Optional[str] + logo_url: Optional[str] + org_type: Optional[str] + city: Optional[str] + state: Optional[str] + follower_count: int + is_verified: bool + + class Config: + from_attributes = True + + +class CauseSummary(BaseModel): + """Brief cause info for lists""" + id: int + name: str + slug: str + description: Optional[str] + icon_url: Optional[str] + color: Optional[str] + category: Optional[str] + follower_count: int + + class Config: + from_attributes = True + + +# ============================================================================ +# FOLLOW/UNFOLLOW ACTIONS +# ============================================================================ + +@router.post("/follow/user/{user_id}") +async def follow_user( + user_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +) -> FollowResponse: + """Follow another user""" + + if current_user.id == user_id: + raise HTTPException(status_code=400, detail="Cannot follow yourself") + + # Check if target user exists + target_user = db.query(User).filter(User.id == user_id).first() + if not target_user: + raise HTTPException(status_code=404, detail="User not found") + + # Check if already following + existing = db.query(UserFollow).filter( + UserFollow.follower_id == current_user.id, + UserFollow.following_id == user_id + ).first() + + if existing: + return FollowResponse( + success=True, + following=True, + follower_count=db.query(UserFollow).filter(UserFollow.following_id == user_id).count(), + message="Already following this user" + ) + + # Create follow + follow = UserFollow(follower_id=current_user.id, following_id=user_id) + db.add(follow) + db.commit() + + follower_count = db.query(UserFollow).filter(UserFollow.following_id == user_id).count() + + return FollowResponse( + success=True, + following=True, + follower_count=follower_count, + message="Successfully followed user" + ) + + +@router.delete("/follow/user/{user_id}") +async def unfollow_user( + user_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +) -> FollowResponse: + """Unfollow a user""" + + follow = db.query(UserFollow).filter( + UserFollow.follower_id == current_user.id, + UserFollow.following_id == user_id + ).first() + + if not follow: + return FollowResponse( + success=True, + following=False, + follower_count=db.query(UserFollow).filter(UserFollow.following_id == user_id).count(), + message="Not following this user" + ) + + db.delete(follow) + db.commit() + + follower_count = db.query(UserFollow).filter(UserFollow.following_id == user_id).count() + + return FollowResponse( + success=True, + following=False, + follower_count=follower_count, + message="Successfully unfollowed user" + ) + + +@router.post("/follow/official/{official_id}") +async def follow_official( + official_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +) -> FollowResponse: + """Follow an official""" + + # Check if official exists + official = db.query(Official).filter(Official.id == official_id).first() + if not official: + raise HTTPException(status_code=404, detail="Official not found") + + # Check if already following + existing = db.query(OfficialFollow).filter( + OfficialFollow.user_id == current_user.id, + OfficialFollow.official_id == official_id + ).first() + + if existing: + return FollowResponse( + success=True, + following=True, + follower_count=official.follower_count, + message="Already following this official" + ) + + # Create follow + follow = OfficialFollow(user_id=current_user.id, official_id=official_id) + db.add(follow) + + # Update follower count + official.follower_count += 1 + db.commit() + + return FollowResponse( + success=True, + following=True, + follower_count=official.follower_count, + message="Successfully followed official" + ) + + +@router.delete("/follow/official/{official_id}") +async def unfollow_official( + official_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +) -> FollowResponse: + """Unfollow an official""" + + official = db.query(Official).filter(Official.id == official_id).first() + if not official: + raise HTTPException(status_code=404, detail="Official not found") + + follow = db.query(OfficialFollow).filter( + OfficialFollow.user_id == current_user.id, + OfficialFollow.official_id == official_id + ).first() + + if not follow: + return FollowResponse( + success=True, + following=False, + follower_count=official.follower_count, + message="Not following this official" + ) + + db.delete(follow) + official.follower_count = max(0, official.follower_count - 1) + db.commit() + + return FollowResponse( + success=True, + following=False, + follower_count=official.follower_count, + message="Successfully unfollowed official" + ) + + +@router.post("/follow/organization/{org_id}") +async def follow_organization( + org_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +) -> FollowResponse: + """Follow an organization""" + + org = db.query(Organization).filter(Organization.id == org_id).first() + if not org: + raise HTTPException(status_code=404, detail="Organization not found") + + existing = db.query(OrganizationFollow).filter( + OrganizationFollow.user_id == current_user.id, + OrganizationFollow.organization_id == org_id + ).first() + + if existing: + return FollowResponse( + success=True, + following=True, + follower_count=org.follower_count, + message="Already following this organization" + ) + + follow = OrganizationFollow(user_id=current_user.id, organization_id=org_id) + db.add(follow) + org.follower_count += 1 + db.commit() + + return FollowResponse( + success=True, + following=True, + follower_count=org.follower_count, + message="Successfully followed organization" + ) + + +@router.delete("/follow/organization/{org_id}") +async def unfollow_organization( + org_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +) -> FollowResponse: + """Unfollow an organization""" + + org = db.query(Organization).filter(Organization.id == org_id).first() + if not org: + raise HTTPException(status_code=404, detail="Organization not found") + + follow = db.query(OrganizationFollow).filter( + OrganizationFollow.user_id == current_user.id, + OrganizationFollow.organization_id == org_id + ).first() + + if not follow: + return FollowResponse( + success=True, + following=False, + follower_count=org.follower_count, + message="Not following this organization" + ) + + db.delete(follow) + org.follower_count = max(0, org.follower_count - 1) + db.commit() + + return FollowResponse( + success=True, + following=False, + follower_count=org.follower_count, + message="Successfully unfollowed organization" + ) + + +@router.post("/follow/cause/{cause_id}") +async def follow_cause( + cause_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +) -> FollowResponse: + """Follow a cause/topic""" + + cause = db.query(Cause).filter(Cause.id == cause_id).first() + if not cause: + raise HTTPException(status_code=404, detail="Cause not found") + + existing = db.query(CauseFollow).filter( + CauseFollow.user_id == current_user.id, + CauseFollow.cause_id == cause_id + ).first() + + if existing: + return FollowResponse( + success=True, + following=True, + follower_count=cause.follower_count, + message="Already following this cause" + ) + + follow = CauseFollow(user_id=current_user.id, cause_id=cause_id) + db.add(follow) + cause.follower_count += 1 + db.commit() + + return FollowResponse( + success=True, + following=True, + follower_count=cause.follower_count, + message="Successfully followed cause" + ) + + +@router.delete("/follow/cause/{cause_id}") +async def unfollow_cause( + cause_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +) -> FollowResponse: + """Unfollow a cause/topic""" + + cause = db.query(Cause).filter(Cause.id == cause_id).first() + if not cause: + raise HTTPException(status_code=404, detail="Cause not found") + + follow = db.query(CauseFollow).filter( + CauseFollow.user_id == current_user.id, + CauseFollow.cause_id == cause_id + ).first() + + if not follow: + return FollowResponse( + success=True, + following=False, + follower_count=cause.follower_count, + message="Not following this cause" + ) + + db.delete(follow) + cause.follower_count = max(0, cause.follower_count - 1) + db.commit() + + return FollowResponse( + success=True, + following=False, + follower_count=cause.follower_count, + message="Successfully unfollowed cause" + ) + + +# ============================================================================ +# CHECK FOLLOW STATUS +# ============================================================================ + +@router.get("/following/status") +async def check_following_status( + user_id: Optional[int] = None, + leader_id: Optional[int] = None, + org_id: Optional[int] = None, + cause_id: Optional[int] = None, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +) -> dict: + """Check if current user is following various entities""" + + result = {} + + if user_id: + result['user'] = db.query(UserFollow).filter( + UserFollow.follower_id == current_user.id, + UserFollow.following_id == user_id + ).first() is not None + + if leader_id: + result['official'] = db.query(OfficialFollow).filter( + OfficialFollow.user_id == current_user.id, + OfficialFollow.official_id == leader_id + ).first() is not None + + if org_id: + result['organization'] = db.query(OrganizationFollow).filter( + OrganizationFollow.user_id == current_user.id, + OrganizationFollow.organization_id == org_id + ).first() is not None + + if cause_id: + result['cause'] = db.query(CauseFollow).filter( + CauseFollow.user_id == current_user.id, + CauseFollow.cause_id == cause_id + ).first() is not None + + return result + + +# ============================================================================ +# FOLLOWER/FOLLOWING LISTS +# ============================================================================ + +@router.get("/stats") +async def get_follower_stats( + user_id: Optional[int] = None, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +) -> FollowerStats: + """Get follower/following statistics for a user""" + + target_id = user_id if user_id else current_user.id + + # Count followers (people following this user) + followers = db.query(UserFollow).filter(UserFollow.following_id == target_id).count() + + # Count following (users this person follows) + following_users = db.query(UserFollow).filter(UserFollow.follower_id == target_id).count() + following_officials = db.query(OfficialFollow).filter(OfficialFollow.user_id == target_id).count() + following_orgs = db.query(OrganizationFollow).filter(OrganizationFollow.user_id == target_id).count() + following_causes = db.query(CauseFollow).filter(CauseFollow.user_id == target_id).count() + + total_following = following_users + following_officials + following_orgs + following_causes + + return FollowerStats( + followers=followers, + following=total_following, + following_users=following_users, + following_officials=following_officials, + following_organizations=following_orgs, + following_causes=following_causes + ) + + +@router.get("/following/officials") +async def get_following_officials( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +) -> List[OfficialSummary]: + """Get list of officials the current user is following""" + + officials = db.query(Official).join( + OfficialFollow, + OfficialFollow.official_id == Official.id + ).filter( + OfficialFollow.user_id == current_user.id + ).all() + + return [OfficialSummary.from_orm(official) for official in officials] + + +@router.get("/following/organizations") +async def get_following_organizations( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +) -> List[OrganizationSummary]: + """Get list of organizations the current user is following""" + + orgs = db.query(Organization).join( + OrganizationFollow, + OrganizationFollow.organization_id == Organization.id + ).filter( + OrganizationFollow.user_id == current_user.id + ).all() + + return [OrganizationSummary.from_orm(org) for org in orgs] + + +@router.get("/following/causes") +async def get_following_causes( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +) -> List[CauseSummary]: + """Get list of causes the current user is following""" + + causes = db.query(Cause).join( + CauseFollow, + CauseFollow.cause_id == Cause.id + ).filter( + CauseFollow.user_id == current_user.id + ).all() + + return [CauseSummary.from_orm(cause) for cause in causes] diff --git a/api/routes/stats.py b/api/routes/stats.py new file mode 100644 index 0000000000000000000000000000000000000000..587e7bd06b8057a61c5438f16a8e33c38ea35d8e --- /dev/null +++ b/api/routes/stats.py @@ -0,0 +1,453 @@ +""" +Statistics endpoint with cached metrics from real data at multiple geographic levels +""" +from fastapi import APIRouter, HTTPException, Query +from pathlib import Path +import pandas as pd +from datetime import datetime, timedelta +from typing import Dict, Any, Optional +from loguru import logger + +router = APIRouter() + +# Multi-level cache: {cache_key: {stats_data, timestamp}} +# Cache key format: "national" or "state:MA" or "county:MA:Suffolk" or "city:MA:Boston" +STATS_CACHE: Dict[str, Dict[str, Any]] = {} +CACHE_DURATION = timedelta(hours=1) + + +def count_parquet_records(pattern: str, filter_func=None) -> int: + """ + Count total records across matching parquet files + + Args: + pattern: Glob pattern for files + filter_func: Optional function to filter DataFrame rows + """ + files = list(Path('data/gold').glob(pattern)) + total = 0 + for file in files: + try: + df = pd.read_parquet(file) + if filter_func: + df = df[filter_func(df)] + total += len(df) + except Exception as e: + print(f"Warning: Could not read {file}: {e}") + return total + + +def calculate_stats(state: Optional[str] = None, + county: Optional[str] = None, + city: Optional[str] = None) -> Dict[str, Any]: + """ + Calculate statistics from parquet files with optional geographic filtering + + Args: + state: Two-letter state code (e.g., 'MA') + county: County name (e.g., 'Suffolk County') + city: City name (e.g., 'Boston') + """ + + # Determine geographic level + if city and state: + level = 'city' + if county: + location_display = f"{city}, {county}, {state}" + else: + location_display = f"{city}, {state}" + elif county and state: + level = 'county' + location_display = f"{county}, {state}" + elif state: + level = 'state' + location_display = state + else: + level = 'national' + location_display = 'United States' + + # Count jurisdictions (cities, counties, townships, school districts) + if state: + # Filter to specific state's jurisdictions + def filter_state(df): + state_col = 'state' if 'state' in df.columns else 'STATE' + if state_col not in df.columns: + return pd.Series([False] * len(df)) + return df[state_col].str.upper() == state.upper() + + # For city level, show just that city (1 jurisdiction) + if city: + # When a city is selected, show 4 jurisdictions: + # 1. City, 2. County, 3. State, 4. School District + jurisdictions = 4 # City, County, State, School District + elif county: + # Count cities/townships in this county + cities_file = Path('data/gold/reference/jurisdictions_cities.parquet') + townships_file = Path('data/gold/reference/jurisdictions_townships.parquet') + count = 0 + + if cities_file.exists(): + df = pd.read_parquet(cities_file) + state_col = 'state' if 'state' in df.columns else 'STATE' + if state_col in df.columns: + df = df[df[state_col].str.upper() == state.upper()] + # Filter by county name (NAME column contains county info in some cases) + # For now, count all in state - proper county filtering needs geocoding + count += len(df) + + if townships_file.exists(): + df = pd.read_parquet(townships_file) + state_col = 'state' if 'state' in df.columns else 'STATE' + if state_col in df.columns: + df = df[df[state_col].str.upper() == state.upper()] + count += len(df) + + jurisdictions = count if count > 0 else 1 # At least the county itself + else: + # State level - count all jurisdictions + jurisdictions = count_parquet_records('reference/jurisdictions_*.parquet', filter_state) + + school_districts = count_parquet_records('reference/jurisdictions_school_districts.parquet', filter_state) + else: + jurisdictions = count_parquet_records('reference/jurisdictions_*.parquet') + school_districts = count_parquet_records('reference/jurisdictions_school_districts.parquet') + + # Count nonprofits + if state: + # Read specific state's nonprofit file + state_file = Path(f'data/gold/states/{state}/nonprofits_organizations.parquet') + if state_file.exists(): + df = pd.read_parquet(state_file) + + # Filter by county if specified + if county: + county_col = 'COUNTY' if 'COUNTY' in df.columns else 'county' + if county_col in df.columns: + df = df[df[county_col].str.contains(county, case=False, na=False)] + + # Filter by city if specified + if city: + city_col = 'CITY' if 'CITY' in df.columns else 'city' + if city_col in df.columns: + df = df[df[city_col].str.contains(city, case=False, na=False)] + + nonprofits = len(df) + else: + nonprofits = 0 + else: + nonprofits = count_parquet_records('states/*/nonprofits_organizations.parquet') + + # Count events/meetings (try new naming first, fallback to old) + if state: + # Try new naming first + event_pattern = f'states/{state}/events.parquet' + event_file = Path(f'data/gold/{event_pattern}') + + if not event_file.exists(): + # Try old events_events naming + event_pattern = f'states/{state}/events_events.parquet' + event_file = Path(f'data/gold/{event_pattern}') + + if not event_file.exists(): + # Fallback to original meetings naming + event_pattern = f'states/{state}/meetings.parquet' + event_file = Path(f'data/gold/{event_pattern}') + + if city and event_file.exists(): + # Filter by city + df = pd.read_parquet(event_file) + place_col = 'place_name' if 'place_name' in df.columns else ('jurisdiction_name' if 'jurisdiction_name' in df.columns else 'jurisdiction') + if place_col in df.columns: + # Match city name (case-insensitive) + df = df[df[place_col].str.contains(city, case=False, na=False)] + meetings = len(df) + else: + meetings = count_parquet_records(event_pattern) + else: + # Try new naming first for all states + meetings = count_parquet_records('states/*/events.parquet') + if meetings == 0: + # Try old events_events naming + meetings = count_parquet_records('states/*/events_events.parquet') + if meetings == 0: + # Fallback to original meetings naming + meetings = count_parquet_records('states/*/meetings.parquet') + + # Count contacts + if state: + contact_pattern = f'states/{state}/contacts_*.parquet' + contact_files = list(Path('data/gold/states').glob(f'{state}/contacts_*.parquet')) + + if city and contact_files: + # Filter by city across all contact files + contacts = 0 + for contact_file in contact_files: + try: + df = pd.read_parquet(contact_file) + jurisdiction_col = 'jurisdiction' if 'jurisdiction' in df.columns else 'city' + if jurisdiction_col in df.columns: + df = df[df[jurisdiction_col].str.contains(city, case=False, na=False)] + contacts += len(df) + except Exception as e: + logger.error(f"Error filtering contacts by city in {contact_file}: {e}") + continue + else: + contacts = count_parquet_records(contact_pattern) + else: + contacts = count_parquet_records('states/*/contacts_*.parquet') + + # Count causes (NTEE codes - always national) + causes = count_parquet_records('reference/causes_ntee_codes.parquet') + + # Count states with data + states_with_data = len(list(Path('data/gold/states').glob('*/'))) + + # Count domains + domains = count_parquet_records('reference/domains_*.parquet') + + # Format display values - use ACTUAL counts only, no extrapolation + # Don't make up numbers we don't have + nonprofits_display = f'{nonprofits:,}' + meetings_display = f'{meetings:,}' + contacts_display = f'{contacts:,}' + + # Build jurisdictions breakdown for city-level views + jurisdictions_breakdown = None + if city and state: + jurisdictions_breakdown = [ + {'type': 'City', 'name': city}, + {'type': 'County', 'name': county if county else 'County (TBD)'}, + {'type': 'State', 'name': state}, + {'type': 'School District', 'name': f'{city} School District'} + ] + + return { + 'level': level, + 'location': location_display, + 'state': state, + 'county': county, + 'city': city, + + # Core counts + 'jurisdictions': jurisdictions, + 'jurisdictions_display': f'{jurisdictions:,}', + 'jurisdictions_breakdown': jurisdictions_breakdown, # List of jurisdiction types for city-level + 'school_districts': school_districts, + 'school_districts_display': f'{school_districts:,}', + + # Nonprofits (actual counts only) + 'nonprofits_current': nonprofits, + 'nonprofits_display': nonprofits_display, + + # Meetings (actual counts only) + 'meetings_current': meetings, + 'meetings_display': meetings_display, + + # Contacts (actual counts only) + 'contacts_current': contacts, + 'contacts_display': contacts_display, + + # Other metrics + 'causes': causes, + 'causes_display': f'{causes}', + 'states_with_data': states_with_data, + 'domains': domains, + 'last_updated': datetime.now().isoformat(), + + # Calculated metrics (use N/A for unavailable data) + 'budget_tracked': 'N/A', + 'fact_checks': 'N/A', + 'grant_opportunities': '1,000s', + 'churches': f'{int(nonprofits * 0.1):,}' if nonprofits > 0 else '4,372', + 'policy_decisions': 'N/A', + 'states_total': states_with_data, + } + + +def get_cached_stats(state: Optional[str] = None, + county: Optional[str] = None, + city: Optional[str] = None) -> Dict[str, Any]: + """Get stats with multi-level caching""" + global STATS_CACHE + + # Build cache key based on geographic level + if city and state: + # City level (county is optional) + if county: + cache_key = f"city:{state}:{county}:{city}" + else: + cache_key = f"city:{state}:{city}" + elif county and state: + cache_key = f"county:{state}:{county}" + elif state: + cache_key = f"state:{state}" + else: + cache_key = "national" + + now = datetime.now() + + # Check if cached stats exist and are still valid + if cache_key in STATS_CACHE: + cached_entry = STATS_CACHE[cache_key] + cache_timestamp = cached_entry.get('_cache_timestamp') + + if cache_timestamp and (now - cache_timestamp) < CACHE_DURATION: + # Return cached stats (remove internal timestamp before returning) + stats = cached_entry.copy() + stats.pop('_cache_timestamp', None) + return stats + + # Calculate fresh stats + try: + stats = calculate_stats(state=state, county=county, city=city) + + # Add to cache with timestamp + cache_entry = stats.copy() + cache_entry['_cache_timestamp'] = now + STATS_CACHE[cache_key] = cache_entry + + return stats + except Exception as e: + print(f"Error calculating stats for {cache_key}: {e}") + + # Return fallback stats if calculation fails (use real numbers only) + return { + 'level': 'national' if not state else ('state' if not county else ('county' if not city else 'city')), + 'location': state or 'United States', + 'jurisdictions_display': '925', + 'nonprofits_display': '43,726', + 'meetings_display': '6,913', + 'contacts_display': '362', + 'school_districts_display': '306', + 'causes_display': '196', + 'churches': '4,372', + 'budget_tracked': 'N/A', + 'fact_checks': 'N/A', + 'grant_opportunities': '1,000s', + 'policy_decisions': 'N/A', + 'states_with_data': 5, + 'states_total': 5, + 'last_updated': now.isoformat(), + 'error': str(e) + } + + +@router.get("/stats") +async def get_stats( + state: Optional[str] = Query(None, description="Two-letter state code (e.g., 'MA')"), + county: Optional[str] = Query(None, description="County name (e.g., 'Suffolk County')"), + city: Optional[str] = Query(None, description="City name (e.g., 'Boston')") +): + """ + Get platform statistics from real data with optional geographic filtering + + **Examples:** + - `/api/stats` - National statistics + - `/api/stats?state=MA` - Massachusetts statistics + - `/api/stats?state=MA&county=Suffolk` - Suffolk County, MA statistics + - `/api/stats?state=MA&county=Suffolk&city=Boston` - Boston, MA statistics + + **Returns:** Cached metrics calculated from parquet files: + - Jurisdictions tracked (cities, counties, townships, school districts) + - Nonprofits monitored + - Meetings analyzed + - Officials and contacts tracked + - Causes and NTEE codes + + **Cache duration:** 1 hour per geographic level + """ + try: + stats = get_cached_stats(state=state, county=county, city=city) + return { + 'success': True, + 'data': stats + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error fetching stats: {str(e)}") + + +@router.get("/stats/detailed") +async def get_detailed_stats( + state: Optional[str] = Query(None, description="Two-letter state code (e.g., 'MA')") +): + """ + Get detailed statistics including breakdowns by state + + Returns: + - Overall totals + - Per-state breakdowns (if no state specified) + - Data quality metrics + """ + try: + stats = get_cached_stats(state=state) + + # Add state-by-state breakdown (only for national view) + if not state: + states = {} + for state_dir in Path('data/gold/states').glob('*/'): + state_code = state_dir.name + state_stats = {} + + # Count each data type for this state + for data_type in ['nonprofits_organizations', 'meetings', 'contacts_nonprofit_officers']: + file = state_dir / f'{data_type}.parquet' + if file.exists(): + try: + df = pd.read_parquet(file) + state_stats[data_type] = len(df) + except: + pass + + if state_stats: + states[state_code] = state_stats + + return { + 'success': True, + 'data': { + **stats, + 'state_breakdown': states + } + } + else: + return { + 'success': True, + 'data': stats + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error fetching detailed stats: {str(e)}") + + +@router.post("/stats/refresh") +async def refresh_stats( + state: Optional[str] = Query(None, description="State to refresh (or all if not specified)") +): + """ + Force refresh of statistics cache + + Useful after data updates or imports. + Can refresh a specific state or all levels. + """ + global STATS_CACHE + + try: + if state: + # Clear cache for specific state and its derivatives + keys_to_remove = [k for k in STATS_CACHE.keys() if k.startswith(f'state:{state}') or k.startswith(f'county:{state}') or k.startswith(f'city:{state}')] + for key in keys_to_remove: + STATS_CACHE.pop(key, None) + message = f'Statistics cache refreshed for {state}' + else: + # Clear all cache + STATS_CACHE = {} + message = 'All statistics cache refreshed' + + # Recalculate to warm cache + stats = get_cached_stats(state=state) + + return { + 'success': True, + 'message': message, + 'data': stats + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error refreshing stats: {str(e)}") diff --git a/api/routes/stats_neon.py b/api/routes/stats_neon.py new file mode 100644 index 0000000000000000000000000000000000000000..652cf14c645237fce4cfd99636db16418594a2f6 --- /dev/null +++ b/api/routes/stats_neon.py @@ -0,0 +1,322 @@ +""" +Statistics endpoint using Neon Postgres (fast!) +Replaces parquet file scanning with indexed database queries +""" +from fastapi import APIRouter, HTTPException, Query +from typing import Dict, Any, Optional +from loguru import logger +import os +import asyncpg +from datetime import datetime, timedelta + +router = APIRouter() + +# Cache for stats (TTL: 5 minutes - data in Neon changes infrequently) +STATS_CACHE: Dict[str, Dict[str, Any]] = {} +CACHE_DURATION = timedelta(minutes=5) + +# Get database URL from environment +# Priority: NEON_DATABASE_URL_DEV (local) > NEON_DATABASE_URL (production) +NEON_DATABASE_URL_DEV = os.getenv('NEON_DATABASE_URL_DEV') +NEON_DATABASE_URL = os.getenv('NEON_DATABASE_URL') + +# Use dev database for local development, production database for deployed environments +DATABASE_URL = NEON_DATABASE_URL_DEV or NEON_DATABASE_URL + +# Connection pool (created on first request) +_db_pool = None + + +async def get_db_pool(): + """Get or create database connection pool""" + global _db_pool + if _db_pool is None: + if not DATABASE_URL: + raise ValueError("DATABASE_URL not configured (set NEON_DATABASE_URL_DEV or NEON_DATABASE_URL)") + + # Log which database we're using + db_type = "Development (Local PostgreSQL)" if NEON_DATABASE_URL_DEV else "Production (Neon)" + logger.info(f"🗄️ [Stats] Connecting to {db_type}: {DATABASE_URL[:50]}...") + + _db_pool = await asyncpg.create_pool(DATABASE_URL, min_size=1, max_size=10) + return _db_pool + + +@router.get("/stats") +async def get_stats( + state: Optional[str] = Query(None, description="Two-letter state code (e.g., MA)"), + county: Optional[str] = Query(None, description="County name (e.g., Suffolk County)"), + city: Optional[str] = Query(None, description="City name (e.g., Boston)") +): + """ + Get statistics from Neon Postgres database + + **Performance**: ~10-50ms (vs 3-10 seconds with parquet files) + + - **National**: GET /api/stats + - **State**: GET /api/stats?state=MA + - **County**: GET /api/stats?state=MA&county=Suffolk%20County + - **City**: GET /api/stats?state=MA&city=Boston + + Returns comprehensive statistics including: + - Jurisdiction counts (cities, counties, school districts) + - Nonprofit counts and financials + - Event/meeting counts + - Contact/officer counts + """ + + try: + # Determine cache key and query parameters + if city and state: + cache_key = f"city:{state}:{city}" + level = 'city' + location_display = f"{city}, {state}" + elif county and state: + cache_key = f"county:{state}:{county}" + level = 'county' + location_display = f"{county}, {state}" + elif state: + cache_key = f"state:{state}" + level = 'state' + location_display = state + else: + cache_key = "national" + level = 'national' + location_display = 'United States' + + # Check cache + if cache_key in STATS_CACHE: + cached = STATS_CACHE[cache_key] + if datetime.now() - cached['timestamp'] < CACHE_DURATION: + logger.debug(f"🚀 Cache hit for {cache_key}") + return cached['stats'] + + # Query Neon database + logger.info(f"📊 Fetching stats from Neon: {cache_key}") + stats = await fetch_stats_from_neon(level, state, county, city) + + if not stats: + # No data found - return empty stats + stats = { + 'location': location_display, + 'level': level, + 'state': state, + 'county': county, + 'city': city, + 'jurisdictions': 0, + 'school_districts': 0, + 'nonprofits': 0, + 'events': 0, + 'bills': 0, + 'contacts': 0, + 'total_revenue': 0, + 'total_assets': 0, + 'last_updated': None, + 'source': 'neon', + 'note': 'No data available for this location' + } + else: + # Format response + stats = { + 'location': location_display, + 'level': level, + 'state': state, + 'county': county, + 'city': city, + 'jurisdictions': stats.get('jurisdictions_count', 0), + 'school_districts': stats.get('school_districts_count', 0), + 'nonprofits': stats.get('nonprofits_count', 0), + 'events': stats.get('events_count', 0), + 'bills': stats.get('bills_count', 0), + 'contacts': stats.get('contacts_count', 0), + 'total_revenue': stats.get('total_revenue', 0), + 'total_assets': stats.get('total_assets', 0), + 'last_updated': stats.get('last_updated'), + 'source': 'neon' + } + + # Cache result + STATS_CACHE[cache_key] = { + 'stats': stats, + 'timestamp': datetime.now() + } + + return stats + + except Exception as e: + logger.error(f"❌ Error fetching stats: {e}") + raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") + + +async def fetch_stats_from_neon( + level: str, + state: Optional[str] = None, + county: Optional[str] = None, + city: Optional[str] = None +) -> Optional[Dict[str, Any]]: + """ + Fetch statistics from Neon database + + Args: + level: 'national', 'state', 'county', or 'city' + state: State code (if applicable) + county: County name (if applicable) + city: City name (if applicable) + + Returns: + Dictionary with stats or None if not found + """ + try: + pool = await get_db_pool() + + async with pool.acquire() as conn: + # Build query based on level + if level == 'national': + query = """ + SELECT * FROM stats_aggregates + WHERE level = 'national' + LIMIT 1 + """ + result = await conn.fetchrow(query) + + elif level == 'state': + query = """ + SELECT * FROM stats_aggregates + WHERE level = 'state' AND UPPER(state) = UPPER($1) + LIMIT 1 + """ + result = await conn.fetchrow(query, state) + + elif level == 'county': + # Try county-level stats first + query = """ + SELECT * FROM stats_aggregates + WHERE level = 'county' + AND UPPER(state) = UPPER($1) + AND county ILIKE $2 + LIMIT 1 + """ + result = await conn.fetchrow(query, state, f"%{county}%") + + # Fall back to state-level if county not found + if not result and state: + logger.info(f"County '{county}' not found in stats, falling back to state '{state}'") + query = """ + SELECT * FROM stats_aggregates + WHERE level = 'state' AND UPPER(state) = UPPER($1) + LIMIT 1 + """ + result = await conn.fetchrow(query, state) + + elif level == 'city': + # Try city-level stats first + query = """ + SELECT * FROM stats_aggregates + WHERE level = 'city' + AND UPPER(state) = UPPER($1) + AND city ILIKE $2 + LIMIT 1 + """ + result = await conn.fetchrow(query, state, f"%{city}%") + + # NEVER fall back to county stats for city requests + # If city stats not found, go straight to state-level + if not result and state: + logger.info(f"City '{city}' not found in stats, falling back to state '{state}' (skipping county)") + query = """ + SELECT * FROM stats_aggregates + WHERE level = 'state' AND UPPER(state) = UPPER($1) + LIMIT 1 + """ + result = await conn.fetchrow(query, state) + + else: + return None + + if result: + return dict(result) + return None + + except Exception as e: + logger.error(f"Database query error: {e}") + raise + + +@router.get("/stats/search") +async def search_stats( + query: str = Query(..., description="Search query"), + limit: int = Query(10, ge=1, le=100, description="Max results") +): + """ + Search for locations (cities, counties, states) with statistics + + Example: GET /api/stats/search?query=boston&limit=5 + + Returns matching locations with their statistics + """ + try: + pool = await get_db_pool() + + async with pool.acquire() as conn: + # Search across all geographic levels + results = await conn.fetch(""" + SELECT + level, + state, + county, + city, + jurisdictions_count, + nonprofits_count, + events_count, + total_revenue + FROM stats_aggregates + WHERE + (city ILIKE $1 OR county ILIKE $1 OR state ILIKE $1) + AND level != 'national' + ORDER BY + CASE level + WHEN 'city' THEN 1 + WHEN 'county' THEN 2 + WHEN 'state' THEN 3 + END, + nonprofits_count DESC + LIMIT $2 + """, f"%{query}%", limit) + + return [{ + 'level': row['level'], + 'location': format_location(row), + 'state': row['state'], + 'county': row['county'], + 'city': row['city'], + 'jurisdictions': row['jurisdictions_count'], + 'nonprofits': row['nonprofits_count'], + 'events': row['events_count'], + 'total_revenue': row['total_revenue'] + } for row in results] + + except Exception as e: + logger.error(f"Search error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +def format_location(row) -> str: + """Format location string from database row""" + if row['city']: + if row['county']: + return f"{row['city']}, {row['county']}, {row['state']}" + return f"{row['city']}, {row['state']}" + elif row['county']: + return f"{row['county']}, {row['state']}" + elif row['state']: + return row['state'] + return 'Unknown' + + +@router.on_event("shutdown") +async def shutdown_db_pool(): + """Close database connection pool on shutdown""" + global _db_pool + if _db_pool: + await _db_pool.close() + _db_pool = None diff --git a/api/static/assets/index-BIH9Tona.css b/api/static/assets/index-BIH9Tona.css new file mode 100644 index 0000000000000000000000000000000000000000..878262d9193ce72d268d0aa25756e5f2c432506d --- /dev/null +++ b/api/static/assets/index-BIH9Tona.css @@ -0,0 +1 @@ +.leaflet-pane,.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-tile-container,.leaflet-pane>svg,.leaflet-pane>canvas,.leaflet-zoom-box,.leaflet-image-layer,.leaflet-layer{position:absolute;left:0;top:0}.leaflet-container{overflow:hidden}.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow{-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-user-drag:none}.leaflet-tile::-moz-selection{background:transparent}.leaflet-tile::selection{background:transparent}.leaflet-safari .leaflet-tile{image-rendering:-webkit-optimize-contrast}.leaflet-safari .leaflet-tile-container{width:1600px;height:1600px;-webkit-transform-origin:0 0}.leaflet-marker-icon,.leaflet-marker-shadow{display:block}.leaflet-container .leaflet-overlay-pane svg{max-width:none!important;max-height:none!important}.leaflet-container .leaflet-marker-pane img,.leaflet-container .leaflet-shadow-pane img,.leaflet-container .leaflet-tile-pane img,.leaflet-container img.leaflet-image-layer,.leaflet-container .leaflet-tile{max-width:none!important;max-height:none!important;width:auto;padding:0}.leaflet-container img.leaflet-tile{mix-blend-mode:plus-lighter}.leaflet-container.leaflet-touch-zoom{touch-action:pan-x pan-y}.leaflet-container.leaflet-touch-drag{touch-action:none;touch-action:pinch-zoom}.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom{touch-action:none}.leaflet-container{-webkit-tap-highlight-color:transparent}.leaflet-container a{-webkit-tap-highlight-color:rgba(51,181,229,.4)}.leaflet-tile{filter:inherit;visibility:hidden}.leaflet-tile-loaded{visibility:inherit}.leaflet-zoom-box{width:0;height:0;box-sizing:border-box;z-index:800}.leaflet-overlay-pane svg{-moz-user-select:none}.leaflet-pane{z-index:400}.leaflet-tile-pane{z-index:200}.leaflet-overlay-pane{z-index:400}.leaflet-shadow-pane{z-index:500}.leaflet-marker-pane{z-index:600}.leaflet-tooltip-pane{z-index:650}.leaflet-popup-pane{z-index:700}.leaflet-map-pane canvas{z-index:100}.leaflet-map-pane svg{z-index:200}.leaflet-vml-shape{width:1px;height:1px}.lvml{behavior:url(#default#VML);display:inline-block;position:absolute}.leaflet-control{position:relative;z-index:800;pointer-events:visiblePainted;pointer-events:auto}.leaflet-top,.leaflet-bottom{position:absolute;z-index:1000;pointer-events:none}.leaflet-top{top:0}.leaflet-right{right:0}.leaflet-bottom{bottom:0}.leaflet-left{left:0}.leaflet-control{float:left;clear:both}.leaflet-right .leaflet-control{float:right}.leaflet-top .leaflet-control{margin-top:10px}.leaflet-bottom .leaflet-control{margin-bottom:10px}.leaflet-left .leaflet-control{margin-left:10px}.leaflet-right .leaflet-control{margin-right:10px}.leaflet-fade-anim .leaflet-popup{opacity:0;transition:opacity .2s linear}.leaflet-fade-anim .leaflet-map-pane .leaflet-popup{opacity:1}.leaflet-zoom-animated{transform-origin:0 0}svg.leaflet-zoom-animated{will-change:transform}.leaflet-zoom-anim .leaflet-zoom-animated{transition:transform .25s cubic-bezier(0,0,.25,1)}.leaflet-zoom-anim .leaflet-tile,.leaflet-pan-anim .leaflet-tile{transition:none}.leaflet-zoom-anim .leaflet-zoom-hide{visibility:hidden}.leaflet-interactive{cursor:pointer}.leaflet-grab{cursor:grab}.leaflet-crosshair,.leaflet-crosshair .leaflet-interactive{cursor:crosshair}.leaflet-popup-pane,.leaflet-control{cursor:auto}.leaflet-dragging .leaflet-grab,.leaflet-dragging .leaflet-grab .leaflet-interactive,.leaflet-dragging .leaflet-marker-draggable{cursor:move;cursor:grabbing}.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-image-layer,.leaflet-pane>svg path,.leaflet-tile-container{pointer-events:none}.leaflet-marker-icon.leaflet-interactive,.leaflet-image-layer.leaflet-interactive,.leaflet-pane>svg path.leaflet-interactive,svg.leaflet-image-layer.leaflet-interactive path{pointer-events:visiblePainted;pointer-events:auto}.leaflet-container{background:#ddd;outline-offset:1px}.leaflet-container a{color:#0078a8}.leaflet-zoom-box{border:2px dotted #38f;background:#ffffff80}.leaflet-container{font-family:Helvetica Neue,Arial,Helvetica,sans-serif;font-size:12px;font-size:.75rem;line-height:1.5}.leaflet-bar{box-shadow:0 1px 5px #000000a6;border-radius:4px}.leaflet-bar a{background-color:#fff;border-bottom:1px solid #ccc;width:26px;height:26px;line-height:26px;display:block;text-align:center;text-decoration:none;color:#000}.leaflet-bar a,.leaflet-control-layers-toggle{background-position:50% 50%;background-repeat:no-repeat;display:block}.leaflet-bar a:hover,.leaflet-bar a:focus{background-color:#f4f4f4}.leaflet-bar a:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.leaflet-bar a:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom:none}.leaflet-bar a.leaflet-disabled{cursor:default;background-color:#f4f4f4;color:#bbb}.leaflet-touch .leaflet-bar a{width:30px;height:30px;line-height:30px}.leaflet-touch .leaflet-bar a:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.leaflet-touch .leaflet-bar a:last-child{border-bottom-left-radius:2px;border-bottom-right-radius:2px}.leaflet-control-zoom-in,.leaflet-control-zoom-out{font:700 18px Lucida Console,Monaco,monospace;text-indent:1px}.leaflet-touch .leaflet-control-zoom-in,.leaflet-touch .leaflet-control-zoom-out{font-size:22px}.leaflet-control-layers{box-shadow:0 1px 5px #0006;background:#fff;border-radius:5px}.leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAQAAAADQ4RFAAACf0lEQVR4AY1UM3gkARTePdvdoTxXKc+qTl3aU5U6b2Kbkz3Gtq3Zw6ziLGNPzrYx7946Tr6/ee/XeCQ4D3ykPtL5tHno4n0d/h3+xfuWHGLX81cn7r0iTNzjr7LrlxCqPtkbTQEHeqOrTy4Yyt3VCi/IOB0v7rVC7q45Q3Gr5K6jt+3Gl5nCoDD4MtO+j96Wu8atmhGqcNGHObuf8OM/x3AMx38+4Z2sPqzCxRFK2aF2e5Jol56XTLyggAMTL56XOMoS1W4pOyjUcGGQdZxU6qRh7B9Zp+PfpOFlqt0zyDZckPi1ttmIp03jX8gyJ8a/PG2yutpS/Vol7peZIbZcKBAEEheEIAgFbDkz5H6Zrkm2hVWGiXKiF4Ycw0RWKdtC16Q7qe3X4iOMxruonzegJzWaXFrU9utOSsLUmrc0YjeWYjCW4PDMADElpJSSQ0vQvA1Tm6/JlKnqFs1EGyZiFCqnRZTEJJJiKRYzVYzJck2Rm6P4iH+cmSY0YzimYa8l0EtTODFWhcMIMVqdsI2uiTvKmTisIDHJ3od5GILVhBCarCfVRmo4uTjkhrhzkiBV7SsaqS+TzrzM1qpGGUFt28pIySQHR6h7F6KSwGWm97ay+Z+ZqMcEjEWebE7wxCSQwpkhJqoZA5ivCdZDjJepuJ9IQjGGUmuXJdBFUygxVqVsxFsLMbDe8ZbDYVCGKxs+W080max1hFCarCfV+C1KATwcnvE9gRRuMP2prdbWGowm1KB1y+zwMMENkM755cJ2yPDtqhTI6ED1M/82yIDtC/4j4BijjeObflpO9I9MwXTCsSX8jWAFeHr05WoLTJ5G8IQVS/7vwR6ohirYM7f6HzYpogfS3R2OAAAAAElFTkSuQmCC);width:36px;height:36px}.leaflet-retina .leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAQAAABvcdNgAAAEsklEQVR4AWL4TydIhpZK1kpWOlg0w3ZXP6D2soBtG42jeI6ZmQTHzAxiTbSJsYLjO9HhP+WOmcuhciVnmHVQcJnp7DFvScowZorad/+V/fVzMdMT2g9Cv9guXGv/7pYOrXh2U+RRR3dSd9JRx6bIFc/ekqHI29JC6pJ5ZEh1yWkhkbcFeSjxgx3L2m1cb1C7bceyxA+CNjT/Ifff+/kDk2u/w/33/IeCMOSaWZ4glosqT3DNnNZQ7Cs58/3Ce5HL78iZH/vKVIaYlqzfdLu8Vi7dnvUbEza5Idt36tquZFldl6N5Z/POLof0XLK61mZCmJSWjVF9tEjUluu74IUXvgttuVIHE7YxSkaYhJZam7yiM9Pv82JYfl9nptxZaxMJE4YSPty+vF0+Y2up9d3wwijfjZbabqm/3bZ9ecKHsiGmRflnn1MW4pjHf9oLufyn2z3y1D6n8g8TZhxyzipLNPnAUpsOiuWimg52psrTZYnOWYNDTMuWBWa0tJb4rgq1UvmutpaYEbZlwU3CLJm/ayYjHW5/h7xWLn9Hh1vepDkyf7dE7MtT5LR4e7yYpHrkhOUpEfssBLq2pPhAqoSWKUkk7EDqkmK6RrCEzqDjhNDWNE+XSMvkJRDWlZTmCW0l0PHQGRZY5t1L83kT0Y3l2SItk5JAWHl2dCOBm+fPu3fo5/3v61RMCO9Jx2EEYYhb0rmNQMX/vm7gqOEJLcXTGw3CAuRNeyaPWwjR8PRqKQ1PDA/dpv+on9Shox52WFnx0KY8onHayrJzm87i5h9xGw/tfkev0jGsQizqezUKjk12hBMKJ4kbCqGPVNXudyyrShovGw5CgxsRICxF6aRmSjlBnHRzg7Gx8fKqEubI2rahQYdR1YgDIRQO7JvQyD52hoIQx0mxa0ODtW2Iozn1le2iIRdzwWewedyZzewidueOGqlsn1MvcnQpuVwLGG3/IR1hIKxCjelIDZ8ldqWz25jWAsnldEnK0Zxro19TGVb2ffIZEsIO89EIEDvKMPrzmBOQcKQ+rroye6NgRRxqR4U8EAkz0CL6uSGOm6KQCdWjvjRiSP1BPalCRS5iQYiEIvxuBMJEWgzSoHADcVMuN7IuqqTeyUPq22qFimFtxDyBBJEwNyt6TM88blFHao/6tWWhuuOM4SAK4EI4QmFHA+SEyWlp4EQoJ13cYGzMu7yszEIBOm2rVmHUNqwAIQabISNMRstmdhNWcFLsSm+0tjJH1MdRxO5Nx0WDMhCtgD6OKgZeljJqJKc9po8juskR9XN0Y1lZ3mWjLR9JCO1jRDMd0fpYC2VnvjBSEFg7wBENc0R9HFlb0xvF1+TBEpF68d+DHR6IOWVv2BECtxo46hOFUBd/APU57WIoEwJhIi2CdpyZX0m93BZicktMj1AS9dClteUFAUNUIEygRZCtik5zSxI9MubTBH1GOiHsiLJ3OCoSZkILa9PxiN0EbvhsAo8tdAf9Seepd36lGWHmtNANTv5Jd0z4QYyeo/UEJqxKRpg5LZx6btLPsOaEmdMyxYdlc8LMaJnikDlhclqmPiQnTEpLUIZEwkRagjYkEibQErwhkTAKCLQEbUgkzJQWc/0PstHHcfEdQ+UAAAAASUVORK5CYII=);background-size:26px 26px}.leaflet-touch .leaflet-control-layers-toggle{width:44px;height:44px}.leaflet-control-layers .leaflet-control-layers-list,.leaflet-control-layers-expanded .leaflet-control-layers-toggle{display:none}.leaflet-control-layers-expanded .leaflet-control-layers-list{display:block;position:relative}.leaflet-control-layers-expanded{padding:6px 10px 6px 6px;color:#333;background:#fff}.leaflet-control-layers-scrollbar{overflow-y:scroll;overflow-x:hidden;padding-right:5px}.leaflet-control-layers-selector{margin-top:2px;position:relative;top:1px}.leaflet-control-layers label{display:block;font-size:13px;font-size:1.08333em}.leaflet-control-layers-separator{height:0;border-top:1px solid #ddd;margin:5px -10px 5px -6px}.leaflet-default-icon-path{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YaZtr2962HUzbDNpjszW24mRt28p47v7zq/bXZtrp/lWnXr337j3nPCe85NcypgSFdugCpW5YoDAMRaIMqRi6aKq5E3YqDQO3qAwjVWrD8Ncq/RBpykd8oZUb/kaJutow8r1aP9II0WmLKLIsJyv1w/kqw9Ch2MYdB++12Onxee/QMwvf4/Dk/Lfp/i4nxTXtOoQ4pW5Aj7wpici1A9erdAN2OH64x8OSP9j3Ft3b7aWkTg/Fm91siTra0f9on5sQr9INejH6CUUUpavjFNq1B+Oadhxmnfa8RfEmN8VNAsQhPqF55xHkMzz3jSmChWU6f7/XZKNH+9+hBLOHYozuKQPxyMPUKkrX/K0uWnfFaJGS1QPRtZsOPtr3NsW0uyh6NNCOkU3Yz+bXbT3I8G3xE5EXLXtCXbbqwCO9zPQYPRTZ5vIDXD7U+w7rFDEoUUf7ibHIR4y6bLVPXrz8JVZEql13trxwue/uDivd3fkWRbS6/IA2bID4uk0UpF1N8qLlbBlXs4Ee7HLTfV1j54APvODnSfOWBqtKVvjgLKzF5YdEk5ewRkGlK0i33Eofffc7HT56jD7/6U+qH3Cx7SBLNntH5YIPvODnyfIXZYRVDPqgHtLs5ABHD3YzLuespb7t79FY34DjMwrVrcTuwlT55YMPvOBnRrJ4VXTdNnYug5ucHLBjEpt30701A3Ts+HEa73u6dT3FNWwflY86eMHPk+Yu+i6pzUpRrW7SNDg5JHR4KapmM5Wv2E8Tfcb1HoqqHMHU+uWDD7zg54mz5/2BSnizi9T1Dg4QQXLToGNCkb6tb1NU+QAlGr1++eADrzhn/u8Q2YZhQVlZ5+CAOtqfbhmaUCS1ezNFVm2imDbPmPng5wmz+gwh+oHDce0eUtQ6OGDIyR0uUhUsoO3vfDmmgOezH0mZN59x7MBi++WDL1g/eEiU3avlidO671bkLfwbw5XV2P8Pzo0ydy4t2/0eu33xYSOMOD8hTf4CrBtGMSoXfPLchX+J0ruSePw3LZeK0juPJbYzrhkH0io7B3k164hiGvawhOKMLkrQLyVpZg8rHFW7E2uHOL888IBPlNZ1FPzstSJM694fWr6RwpvcJK60+0HCILTBzZLFNdtAzJaohze60T8qBzyh5ZuOg5e7uwQppofEmf2++DYvmySqGBuKaicF1blQjhuHdvCIMvp8whTTfZzI7RldpwtSzL+F1+wkdZ2TBOW2gIF88PBTzD/gpeREAMEbxnJcaJHNHrpzji0gQCS6hdkEeYt9DF/2qPcEC8RM28Hwmr3sdNyht00byAut2k3gufWNtgtOEOFGUwcXWNDbdNbpgBGxEvKkOQsxivJx33iow0Vw5S6SVTrpVq11ysA2Rp7gTfPfktc6zhtXBBC+adRLshf6sG2RfHPZ5EAc4sVZ83yCN00Fk/4kggu40ZTvIEm5g24qtU4KjBrx/BTTH8ifVASAG7gKrnWxJDcU7x8X6Ecczhm3o6YicvsLXWfh3Ch1W0k8x0nXF+0fFxgt4phz8QvypiwCCFKMqXCnqXExjq10beH+UUA7+nG6mdG/Pu0f3LgFcGrl2s0kNNjpmoJ9o4B29CMO8dMT4Q5ox8uitF6fqsrJOr8qnwNbRzv6hSnG5wP+64C7h9lp30hKNtKdWjtdkbuPA19nJ7Tz3zR/ibgARbhb4AlhavcBebmTHcFl2fvYEnW0ox9xMxKBS8btJ+KiEbq9zA4RthQXDhPa0T9TEe69gWupwc6uBUphquXgf+/FrIjweHQS4/pduMe5ERUMHUd9xv8ZR98CxkS4F2n3EUrUZ10EYNw7BWm9x1GiPssi3GgiGRDKWRYZfXlON+dfNbM+GgIwYdwAAAAASUVORK5CYII=)}.leaflet-container .leaflet-control-attribution{background:#fff;background:#fffc;margin:0}.leaflet-control-attribution,.leaflet-control-scale-line{padding:0 5px;color:#333;line-height:1.4}.leaflet-control-attribution a{text-decoration:none}.leaflet-control-attribution a:hover,.leaflet-control-attribution a:focus{text-decoration:underline}.leaflet-attribution-flag{display:inline!important;vertical-align:baseline!important;width:1em;height:.6669em}.leaflet-left .leaflet-control-scale{margin-left:5px}.leaflet-bottom .leaflet-control-scale{margin-bottom:5px}.leaflet-control-scale-line{border:2px solid #777;border-top:none;line-height:1.1;padding:2px 5px 1px;white-space:nowrap;box-sizing:border-box;background:#fffc;text-shadow:1px 1px #fff}.leaflet-control-scale-line:not(:first-child){border-top:2px solid #777;border-bottom:none;margin-top:-2px}.leaflet-control-scale-line:not(:first-child):not(:last-child){border-bottom:2px solid #777}.leaflet-touch .leaflet-control-attribution,.leaflet-touch .leaflet-control-layers,.leaflet-touch .leaflet-bar{box-shadow:none}.leaflet-touch .leaflet-control-layers,.leaflet-touch .leaflet-bar{border:2px solid rgba(0,0,0,.2);background-clip:padding-box}.leaflet-popup{position:absolute;text-align:center;margin-bottom:20px}.leaflet-popup-content-wrapper{padding:1px;text-align:left;border-radius:12px}.leaflet-popup-content{margin:13px 24px 13px 20px;line-height:1.3;font-size:13px;font-size:1.08333em;min-height:1px}.leaflet-popup-content p{margin:1.3em 0}.leaflet-popup-tip-container{width:40px;height:20px;position:absolute;left:50%;margin-top:-1px;margin-left:-20px;overflow:hidden;pointer-events:none}.leaflet-popup-tip{width:17px;height:17px;padding:1px;margin:-10px auto 0;pointer-events:auto;transform:rotate(45deg)}.leaflet-popup-content-wrapper,.leaflet-popup-tip{background:#fff;color:#333;box-shadow:0 3px 14px #0006}.leaflet-container a.leaflet-popup-close-button{position:absolute;top:0;right:0;border:none;text-align:center;width:24px;height:24px;font:16px/24px Tahoma,Verdana,sans-serif;color:#757575;text-decoration:none;background:transparent}.leaflet-container a.leaflet-popup-close-button:hover,.leaflet-container a.leaflet-popup-close-button:focus{color:#585858}.leaflet-popup-scrolled{overflow:auto}.leaflet-oldie .leaflet-popup-content-wrapper{-ms-zoom:1}.leaflet-oldie .leaflet-popup-tip{width:24px;margin:0 auto;-ms-filter:"progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";filter:progid:DXImageTransform.Microsoft.Matrix(M11=.70710678,M12=.70710678,M21=-.70710678,M22=.70710678)}.leaflet-oldie .leaflet-control-zoom,.leaflet-oldie .leaflet-control-layers,.leaflet-oldie .leaflet-popup-content-wrapper,.leaflet-oldie .leaflet-popup-tip{border:1px solid #999}.leaflet-div-icon{background:#fff;border:1px solid #666}.leaflet-tooltip{position:absolute;padding:6px;background-color:#fff;border:1px solid #fff;border-radius:3px;color:#222;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:none;box-shadow:0 1px 3px #0006}.leaflet-tooltip.leaflet-interactive{cursor:pointer;pointer-events:auto}.leaflet-tooltip-top:before,.leaflet-tooltip-bottom:before,.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{position:absolute;pointer-events:none;border:6px solid transparent;background:transparent;content:""}.leaflet-tooltip-bottom{margin-top:6px}.leaflet-tooltip-top{margin-top:-6px}.leaflet-tooltip-bottom:before,.leaflet-tooltip-top:before{left:50%;margin-left:-6px}.leaflet-tooltip-top:before{bottom:0;margin-bottom:-12px;border-top-color:#fff}.leaflet-tooltip-bottom:before{top:0;margin-top:-12px;margin-left:-6px;border-bottom-color:#fff}.leaflet-tooltip-left{margin-left:-6px}.leaflet-tooltip-right{margin-left:6px}.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{top:50%;margin-top:-6px}.leaflet-tooltip-left:before{right:0;margin-right:-12px;border-left-color:#fff}.leaflet-tooltip-right:before{left:0;margin-left:-12px;border-right-color:#fff}@media print{.leaflet-control{-webkit-print-color-adjust:exact;print-color-adjust:exact}}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}h1{font-size:2.25rem;line-height:2.5rem;font-weight:700}h2{font-size:1.875rem;line-height:2.25rem;font-weight:600}h3{font-size:1.5rem;line-height:2rem;font-weight:600}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.card{border-radius:.5rem;--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding:1.5rem;--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.btn-primary{border-radius:.5rem;--tw-bg-opacity: 1;background-color:rgb(53 79 82 / var(--tw-bg-opacity, 1));padding:.5rem 1rem;font-weight:500;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.btn-primary:hover{--tw-bg-opacity: 1;background-color:rgb(46 67 70 / var(--tw-bg-opacity, 1))}.btn-secondary{border-radius:.5rem;--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1));padding:.5rem 1rem;font-weight:500;--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.btn-secondary:hover{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1))}.pointer-events-auto{pointer-events:auto}.visible{visibility:visible}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.inset-y-0{top:0;bottom:0}.-bottom-6{bottom:-1.5rem}.bottom-0{bottom:0}.bottom-4{bottom:1rem}.left-0{left:0}.left-1\/2{left:50%}.left-3{left:.75rem}.left-4{left:1rem}.right-0{right:0}.right-2{right:.5rem}.right-4{right:1rem}.top-0{top:0}.top-1\.5{top:.375rem}.top-1\/2{top:50%}.top-16{top:4rem}.top-2{top:.5rem}.top-2\.5{top:.625rem}.top-3\.5{top:.875rem}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[1\]{z-index:1}.mx-8{margin-left:2rem;margin-right:2rem}.mx-auto{margin-left:auto;margin-right:auto}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-12{margin-bottom:3rem}.mb-16{margin-bottom:4rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.mr-2{margin-right:.5rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1{height:.25rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-20{height:5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-96{height:24rem}.h-\[600px\]{height:600px}.h-\[calc\(100vh-10rem\)\]{height:calc(100vh - 10rem)}.h-auto{height:auto}.h-screen{height:100vh}.max-h-48{max-height:12rem}.max-h-60{max-height:15rem}.max-h-96{max-height:24rem}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-3{width:.75rem}.w-32{width:8rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-full{width:100%}.min-w-0{min-width:0px}.min-w-\[150px\]{min-width:150px}.min-w-\[200px\]{min-width:200px}.min-w-\[250px\]{min-width:250px}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-md{max-width:28rem}.max-w-sm{max-width:24rem}.max-w-xs{max-width:20rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.grow{flex-grow:1}.origin-left{transform-origin:left}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-x-full{--tw-translate-x: -100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-full{--tw-translate-y: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-95{--tw-scale-x: .95;--tw-scale-y: .95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-x-0{--tw-scale-x: 0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.animate-\[slideUp_0\.6s_ease-out\]{animation:slideUp .6s ease-out}.animate-\[slideUp_0\.8s_ease-out_0\.2s_both\]{animation:slideUp .8s ease-out .2s both}.animate-\[slideUp_0\.8s_ease-out_0\.4s_both\]{animation:slideUp .8s ease-out .4s both}.animate-\[slideUp_0\.8s_ease-out_0\.6s_both\]{animation:slideUp .8s ease-out .6s both}.animate-\[slideUp_0\.8s_ease-out_0\.8s_both\]{animation:slideUp .8s ease-out .8s both}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity, 1))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l-2{border-left-width:2px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-\[\#354F52\]{--tw-border-opacity: 1;border-color:rgb(53 79 82 / var(--tw-border-opacity, 1))}.border-amber-200{--tw-border-opacity: 1;border-color:rgb(253 230 138 / var(--tw-border-opacity, 1))}.border-amber-500{--tw-border-opacity: 1;border-color:rgb(245 158 11 / var(--tw-border-opacity, 1))}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity, 1))}.border-blue-400{--tw-border-opacity: 1;border-color:rgb(96 165 250 / var(--tw-border-opacity, 1))}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-blue-600{--tw-border-opacity: 1;border-color:rgb(37 99 235 / var(--tw-border-opacity, 1))}.border-current{border-color:currentColor}.border-emerald-500{--tw-border-opacity: 1;border-color:rgb(16 185 129 / var(--tw-border-opacity, 1))}.border-gray-100{--tw-border-opacity: 1;border-color:rgb(243 244 246 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-gray-900{--tw-border-opacity: 1;border-color:rgb(17 24 39 / var(--tw-border-opacity, 1))}.border-green-200{--tw-border-opacity: 1;border-color:rgb(187 247 208 / var(--tw-border-opacity, 1))}.border-green-500{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.border-green-500\/30{border-color:#22c55e4d}.border-green-500\/50{border-color:#22c55e80}.border-orange-200{--tw-border-opacity: 1;border-color:rgb(254 215 170 / var(--tw-border-opacity, 1))}.border-pink-200{--tw-border-opacity: 1;border-color:rgb(251 207 232 / var(--tw-border-opacity, 1))}.border-primary-500{--tw-border-opacity: 1;border-color:rgb(53 79 82 / var(--tw-border-opacity, 1))}.border-primary-600{--tw-border-opacity: 1;border-color:rgb(46 67 70 / var(--tw-border-opacity, 1))}.border-purple-200{--tw-border-opacity: 1;border-color:rgb(233 213 255 / var(--tw-border-opacity, 1))}.border-purple-500{--tw-border-opacity: 1;border-color:rgb(168 85 247 / var(--tw-border-opacity, 1))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-red-300{--tw-border-opacity: 1;border-color:rgb(252 165 165 / var(--tw-border-opacity, 1))}.border-red-400{--tw-border-opacity: 1;border-color:rgb(248 113 113 / var(--tw-border-opacity, 1))}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.border-red-500\/30{border-color:#ef44444d}.border-red-500\/50{border-color:#ef444480}.border-teal-200{--tw-border-opacity: 1;border-color:rgb(153 246 228 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.border-white{--tw-border-opacity: 1;border-color:rgb(255 255 255 / var(--tw-border-opacity, 1))}.border-yellow-200{--tw-border-opacity: 1;border-color:rgb(254 240 138 / var(--tw-border-opacity, 1))}.border-yellow-500\/30{border-color:#eab3084d}.border-yellow-500\/50{border-color:#eab30880}.border-t-primary-600{--tw-border-opacity: 1;border-top-color:rgb(46 67 70 / var(--tw-border-opacity, 1))}.border-t-transparent{border-top-color:transparent}.bg-\[\#354F52\]{--tw-bg-opacity: 1;background-color:rgb(53 79 82 / var(--tw-bg-opacity, 1))}.bg-\[\#E8EFEA\]{--tw-bg-opacity: 1;background-color:rgb(232 239 234 / var(--tw-bg-opacity, 1))}.bg-amber-100{--tw-bg-opacity: 1;background-color:rgb(254 243 199 / var(--tw-bg-opacity, 1))}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-amber-600{--tw-bg-opacity: 1;background-color:rgb(217 119 6 / var(--tw-bg-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-emerald-100{--tw-bg-opacity: 1;background-color:rgb(209 250 229 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-800\/50{background-color:#1f293780}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-500\/10{background-color:#22c55e1a}.bg-green-500\/20{background-color:#22c55e33}.bg-green-500\/30{background-color:#22c55e4d}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-neutral-600{--tw-bg-opacity: 1;background-color:rgb(53 79 82 / var(--tw-bg-opacity, 1))}.bg-orange-100{--tw-bg-opacity: 1;background-color:rgb(255 237 213 / var(--tw-bg-opacity, 1))}.bg-orange-50{--tw-bg-opacity: 1;background-color:rgb(255 247 237 / var(--tw-bg-opacity, 1))}.bg-pink-100{--tw-bg-opacity: 1;background-color:rgb(252 231 243 / var(--tw-bg-opacity, 1))}.bg-primary-100{--tw-bg-opacity: 1;background-color:rgb(197 202 206 / var(--tw-bg-opacity, 1))}.bg-primary-50{--tw-bg-opacity: 1;background-color:rgb(232 234 235 / var(--tw-bg-opacity, 1))}.bg-primary-500{--tw-bg-opacity: 1;background-color:rgb(53 79 82 / var(--tw-bg-opacity, 1))}.bg-primary-600{--tw-bg-opacity: 1;background-color:rgb(46 67 70 / var(--tw-bg-opacity, 1))}.bg-purple-100{--tw-bg-opacity: 1;background-color:rgb(243 232 255 / var(--tw-bg-opacity, 1))}.bg-purple-50{--tw-bg-opacity: 1;background-color:rgb(250 245 255 / var(--tw-bg-opacity, 1))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-500\/20{background-color:#ef444433}.bg-red-500\/30{background-color:#ef44444d}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-teal-100{--tw-bg-opacity: 1;background-color:rgb(204 251 241 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-white\/10{background-color:#ffffff1a}.bg-white\/95{background-color:#fffffff2}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity, 1))}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity, 1))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.bg-yellow-500\/10{background-color:#eab3081a}.bg-yellow-500\/20{background-color:#eab30833}.bg-yellow-500\/30{background-color:#eab3084d}.bg-opacity-10{--tw-bg-opacity: .1}.bg-opacity-25{--tw-bg-opacity: .25}.bg-opacity-50{--tw-bg-opacity: .5}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-\[\#52796F\]{--tw-gradient-from: #52796F var(--tw-gradient-from-position);--tw-gradient-to: rgb(82 121 111 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-blue-50{--tw-gradient-from: #eff6ff var(--tw-gradient-from-position);--tw-gradient-to: rgb(239 246 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-gray-50{--tw-gradient-from: #f9fafb var(--tw-gradient-from-position);--tw-gradient-to: rgb(249 250 251 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-primary-50{--tw-gradient-from: #e8eaeb var(--tw-gradient-from-position);--tw-gradient-to: rgb(232 234 235 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-primary-500{--tw-gradient-from: #354F52 var(--tw-gradient-from-position);--tw-gradient-to: rgb(53 79 82 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-primary-600{--tw-gradient-from: #2e4346 var(--tw-gradient-from-position);--tw-gradient-to: rgb(46 67 70 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-purple-600{--tw-gradient-from: #9333ea var(--tw-gradient-from-position);--tw-gradient-to: rgb(147 51 234 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-red-500{--tw-gradient-from: #ef4444 var(--tw-gradient-from-position);--tw-gradient-to: rgb(239 68 68 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.from-teal-50{--tw-gradient-from: #f0fdfa var(--tw-gradient-from-position);--tw-gradient-to: rgb(240 253 250 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-\[\#84A98C\]{--tw-gradient-to: #84A98C var(--tw-gradient-to-position)}.to-blue-50{--tw-gradient-to: #eff6ff var(--tw-gradient-to-position)}.to-gray-100{--tw-gradient-to: #f3f4f6 var(--tw-gradient-to-position)}.to-indigo-50{--tw-gradient-to: #eef2ff var(--tw-gradient-to-position)}.to-pink-600{--tw-gradient-to: #db2777 var(--tw-gradient-to-position)}.to-primary-100{--tw-gradient-to: #c5cace var(--tw-gradient-to-position)}.to-primary-600{--tw-gradient-to: #2e4346 var(--tw-gradient-to-position)}.to-primary-700{--tw-gradient-to: #27383a var(--tw-gradient-to-position)}.to-red-600{--tw-gradient-to: #dc2626 var(--tw-gradient-to-position)}.to-white{--tw-gradient-to: #fff var(--tw-gradient-to-position)}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0\.5{padding:.125rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-12{padding-left:3rem;padding-right:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-20{padding-top:5rem;padding-bottom:5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-12{padding-bottom:3rem}.pb-16{padding-bottom:4rem}.pb-20{padding-bottom:5rem}.pb-4{padding-bottom:1rem}.pb-6{padding-bottom:1.5rem}.pl-10{padding-left:2.5rem}.pl-12{padding-left:3rem}.pl-4{padding-left:1rem}.pt-16{padding-top:4rem}.pt-2{padding-top:.5rem}.pt-20{padding-top:5rem}.pt-3{padding-top:.75rem}.pt-32{padding-top:8rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.text-center{text-align:center}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-6xl{font-size:3.75rem;line-height:1}.text-\[10px\]{font-size:10px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.leading-5{line-height:1.25rem}.leading-6{line-height:1.5rem}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-\[\#354F52\]{--tw-text-opacity: 1;color:rgb(53 79 82 / var(--tw-text-opacity, 1))}.text-\[\#52796F\]{--tw-text-opacity: 1;color:rgb(82 121 111 / var(--tw-text-opacity, 1))}.text-amber-600{--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity, 1))}.text-amber-700{--tw-text-opacity: 1;color:rgb(180 83 9 / var(--tw-text-opacity, 1))}.text-amber-800{--tw-text-opacity: 1;color:rgb(146 64 14 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-blue-700{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.text-blue-900{--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity, 1))}.text-cyan-600{--tw-text-opacity: 1;color:rgb(8 145 178 / var(--tw-text-opacity, 1))}.text-emerald-600{--tw-text-opacity: 1;color:rgb(5 150 105 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-green-300\/70{color:#86efacb3}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-green-900{--tw-text-opacity: 1;color:rgb(20 83 45 / var(--tw-text-opacity, 1))}.text-orange-600{--tw-text-opacity: 1;color:rgb(234 88 12 / var(--tw-text-opacity, 1))}.text-orange-700{--tw-text-opacity: 1;color:rgb(194 65 12 / var(--tw-text-opacity, 1))}.text-orange-800{--tw-text-opacity: 1;color:rgb(154 52 18 / var(--tw-text-opacity, 1))}.text-orange-900{--tw-text-opacity: 1;color:rgb(124 45 18 / var(--tw-text-opacity, 1))}.text-pink-600{--tw-text-opacity: 1;color:rgb(219 39 119 / var(--tw-text-opacity, 1))}.text-pink-700{--tw-text-opacity: 1;color:rgb(190 24 93 / var(--tw-text-opacity, 1))}.text-primary-50{--tw-text-opacity: 1;color:rgb(232 234 235 / var(--tw-text-opacity, 1))}.text-primary-600{--tw-text-opacity: 1;color:rgb(46 67 70 / var(--tw-text-opacity, 1))}.text-primary-700{--tw-text-opacity: 1;color:rgb(39 56 58 / var(--tw-text-opacity, 1))}.text-purple-50{--tw-text-opacity: 1;color:rgb(250 245 255 / var(--tw-text-opacity, 1))}.text-purple-600{--tw-text-opacity: 1;color:rgb(147 51 234 / var(--tw-text-opacity, 1))}.text-purple-700{--tw-text-opacity: 1;color:rgb(126 34 206 / var(--tw-text-opacity, 1))}.text-purple-800{--tw-text-opacity: 1;color:rgb(107 33 168 / var(--tw-text-opacity, 1))}.text-purple-900{--tw-text-opacity: 1;color:rgb(88 28 135 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-300\/70{color:#fca5a5b3}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.text-red-900{--tw-text-opacity: 1;color:rgb(127 29 29 / var(--tw-text-opacity, 1))}.text-teal-600{--tw-text-opacity: 1;color:rgb(13 148 136 / var(--tw-text-opacity, 1))}.text-teal-700{--tw-text-opacity: 1;color:rgb(15 118 110 / var(--tw-text-opacity, 1))}.text-teal-800{--tw-text-opacity: 1;color:rgb(17 94 89 / var(--tw-text-opacity, 1))}.text-transparent{color:transparent}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-white\/90{color:#ffffffe6}.text-yellow-300{--tw-text-opacity: 1;color:rgb(253 224 71 / var(--tw-text-opacity, 1))}.text-yellow-300\/70{color:#fde047b3}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity, 1))}.text-yellow-700{--tw-text-opacity: 1;color:rgb(161 98 7 / var(--tw-text-opacity, 1))}.text-yellow-800{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.no-underline{text-decoration-line:none}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-60{opacity:.6}.opacity-90{opacity:.9}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-75{transition-duration:75ms}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#f1f5f9}html{scroll-behavior:smooth}body{margin:0;min-height:100vh}@keyframes slideUp{0%{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}.last\:rounded-b-lg:last-child{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.last\:border-b-0:last-child{border-bottom-width:0px}.hover\:-translate-y-1:hover{--tw-translate-y: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:scale-105:hover{--tw-scale-x: 1.05;--tw-scale-y: 1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-2:hover{border-width:2px}.hover\:border-\[\#354F52\]:hover{--tw-border-opacity: 1;border-color:rgb(53 79 82 / var(--tw-border-opacity, 1))}.hover\:border-amber-500:hover{--tw-border-opacity: 1;border-color:rgb(245 158 11 / var(--tw-border-opacity, 1))}.hover\:border-blue-500:hover{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.hover\:border-gray-200:hover{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.hover\:border-gray-300:hover{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.hover\:border-gray-400:hover{--tw-border-opacity: 1;border-color:rgb(156 163 175 / var(--tw-border-opacity, 1))}.hover\:border-green-500:hover{--tw-border-opacity: 1;border-color:rgb(34 197 94 / var(--tw-border-opacity, 1))}.hover\:border-primary-500:hover{--tw-border-opacity: 1;border-color:rgb(53 79 82 / var(--tw-border-opacity, 1))}.hover\:border-purple-500:hover{--tw-border-opacity: 1;border-color:rgb(168 85 247 / var(--tw-border-opacity, 1))}.hover\:bg-\[\#d9e5db\]:hover{--tw-bg-opacity: 1;background-color:rgb(217 229 219 / var(--tw-bg-opacity, 1))}.hover\:bg-amber-200:hover{--tw-bg-opacity: 1;background-color:rgb(253 230 138 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-200:hover{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-100:hover{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-300:hover{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700\/50:hover{background-color:#37415180}.hover\:bg-green-200:hover{--tw-bg-opacity: 1;background-color:rgb(187 247 208 / var(--tw-bg-opacity, 1))}.hover\:bg-green-700:hover{--tw-bg-opacity: 1;background-color:rgb(21 128 61 / var(--tw-bg-opacity, 1))}.hover\:bg-neutral-700:hover{--tw-bg-opacity: 1;background-color:rgb(46 67 70 / var(--tw-bg-opacity, 1))}.hover\:bg-primary-100:hover{--tw-bg-opacity: 1;background-color:rgb(197 202 206 / var(--tw-bg-opacity, 1))}.hover\:bg-primary-50:hover{--tw-bg-opacity: 1;background-color:rgb(232 234 235 / var(--tw-bg-opacity, 1))}.hover\:bg-primary-700:hover{--tw-bg-opacity: 1;background-color:rgb(39 56 58 / var(--tw-bg-opacity, 1))}.hover\:bg-purple-200:hover{--tw-bg-opacity: 1;background-color:rgb(233 213 255 / var(--tw-bg-opacity, 1))}.hover\:bg-red-100:hover{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:bg-teal-200:hover{--tw-bg-opacity: 1;background-color:rgb(153 246 228 / var(--tw-bg-opacity, 1))}.hover\:bg-white\/20:hover{background-color:#fff3}.hover\:text-\[\#354F52\]:hover{--tw-text-opacity: 1;color:rgb(53 79 82 / var(--tw-text-opacity, 1))}.hover\:text-amber-900:hover{--tw-text-opacity: 1;color:rgb(120 53 15 / var(--tw-text-opacity, 1))}.hover\:text-blue-600:hover{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.hover\:text-blue-700:hover{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.hover\:text-blue-800:hover{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.hover\:text-gray-500:hover{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.hover\:text-gray-600:hover{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.hover\:text-gray-700:hover{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.hover\:text-primary-100:hover{--tw-text-opacity: 1;color:rgb(197 202 206 / var(--tw-text-opacity, 1))}.hover\:text-primary-600:hover{--tw-text-opacity: 1;color:rgb(46 67 70 / var(--tw-text-opacity, 1))}.hover\:text-primary-700:hover{--tw-text-opacity: 1;color:rgb(39 56 58 / var(--tw-text-opacity, 1))}.hover\:text-red-700:hover{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:decoration-2:hover{text-decoration-thickness:2px}.hover\:opacity-75:hover{opacity:.75}.hover\:opacity-90:hover{opacity:.9}.hover\:shadow-2xl:hover{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-md:hover{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.hover\:shadow-xl:hover{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.focus\:border-primary-500:focus{--tw-border-opacity: 1;border-color:rgb(53 79 82 / var(--tw-border-opacity, 1))}.focus\:border-transparent:focus{border-color:transparent}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-\[\#354F52\]:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(53 79 82 / var(--tw-ring-opacity, 1))}.focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}.focus\:ring-primary-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(53 79 82 / var(--tw-ring-opacity, 1))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:bg-gray-400:disabled{--tw-bg-opacity: 1;background-color:rgb(156 163 175 / var(--tw-bg-opacity, 1))}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:translate-x-1{--tw-translate-x: .25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-110{--tw-scale-x: 1.1;--tw-scale-y: 1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-x-100{--tw-scale-x: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:bg-amber-200{--tw-bg-opacity: 1;background-color:rgb(253 230 138 / var(--tw-bg-opacity, 1))}.group:hover .group-hover\:bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity, 1))}.group:hover .group-hover\:bg-green-200{--tw-bg-opacity: 1;background-color:rgb(187 247 208 / var(--tw-bg-opacity, 1))}.group:hover .group-hover\:bg-purple-200{--tw-bg-opacity: 1;background-color:rgb(233 213 255 / var(--tw-bg-opacity, 1))}.group:hover .group-hover\:text-\[\#354F52\]{--tw-text-opacity: 1;color:rgb(53 79 82 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:text-amber-600{--tw-text-opacity: 1;color:rgb(217 119 6 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:text-blue-200{--tw-text-opacity: 1;color:rgb(191 219 254 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:text-primary-600{--tw-text-opacity: 1;color:rgb(46 67 70 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:text-purple-600{--tw-text-opacity: 1;color:rgb(147 51 234 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width: 640px){.sm\:w-auto{width:auto}.sm\:flex-row{flex-direction:row}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width: 768px){.md\:block{display:block}.md\:inline{display:inline}.md\:flex{display:flex}.md\:hidden{display:none}.md\:h-12{height:3rem}.md\:translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:gap-2{gap:.5rem}.md\:gap-3{gap:.75rem}.md\:gap-4{gap:1rem}.md\:px-4{padding-left:1rem;padding-right:1rem}.md\:px-6{padding-left:1.5rem;padding-right:1.5rem}.md\:pl-64{padding-left:16rem}.md\:text-2xl{font-size:1.5rem;line-height:2rem}.md\:text-5xl{font-size:3rem;line-height:1}.md\:text-7xl{font-size:4.5rem;line-height:1}.md\:text-base{font-size:1rem;line-height:1.5rem}}@media (min-width: 1024px){.lg\:col-span-2{grid-column:span 2 / span 2}.lg\:col-span-3{grid-column:span 3 / span 3}.lg\:col-span-7{grid-column:span 7 / span 7}.lg\:flex{display:flex}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width: 1280px){.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}} diff --git a/api/static/assets/index-DoIJncqg.js b/api/static/assets/index-DoIJncqg.js new file mode 100644 index 0000000000000000000000000000000000000000..76442c039c9d7251ab0e284abeb60a267e30a2b7 --- /dev/null +++ b/api/static/assets/index-DoIJncqg.js @@ -0,0 +1,193 @@ +var r7=Object.defineProperty;var RN=e=>{throw TypeError(e)};var i7=(e,t,n)=>t in e?r7(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var FN=(e,t,n)=>i7(e,typeof t!="symbol"?t+"":t,n),Fv=(e,t,n)=>t.has(e)||RN("Cannot "+n);var q=(e,t,n)=>(Fv(e,t,"read from private field"),n?n.call(e):t.get(e)),xe=(e,t,n)=>t.has(e)?RN("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),fe=(e,t,n,r)=>(Fv(e,t,"write to private field"),r?r.call(e,n):t.set(e,n),n),Me=(e,t,n)=>(Fv(e,t,"access private method"),n);var Ph=(e,t,n,r)=>({set _(i){fe(e,t,i,n)},get _(){return q(e,t,r)}});function a7(e,t){for(var n=0;nr[i]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const a of i)if(a.type==="childList")for(const s of a.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(i){const a={};return i.integrity&&(a.integrity=i.integrity),i.referrerPolicy&&(a.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?a.credentials="include":i.crossOrigin==="anonymous"?a.credentials="omit":a.credentials="same-origin",a}function r(i){if(i.ep)return;i.ep=!0;const a=n(i);fetch(i.href,a)}})();var zu=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Qe(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Pk={exports:{}},Rg={},Ek={exports:{}},Be={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Ff=Symbol.for("react.element"),o7=Symbol.for("react.portal"),s7=Symbol.for("react.fragment"),l7=Symbol.for("react.strict_mode"),c7=Symbol.for("react.profiler"),u7=Symbol.for("react.provider"),d7=Symbol.for("react.context"),f7=Symbol.for("react.forward_ref"),h7=Symbol.for("react.suspense"),m7=Symbol.for("react.memo"),p7=Symbol.for("react.lazy"),DN=Symbol.iterator;function g7(e){return e===null||typeof e!="object"?null:(e=DN&&e[DN]||e["@@iterator"],typeof e=="function"?e:null)}var Ok={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},kk=Object.assign,Ck={};function Uc(e,t,n){this.props=e,this.context=t,this.refs=Ck,this.updater=n||Ok}Uc.prototype.isReactComponent={};Uc.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Uc.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function Ak(){}Ak.prototype=Uc.prototype;function $w(e,t,n){this.props=e,this.context=t,this.refs=Ck,this.updater=n||Ok}var Iw=$w.prototype=new Ak;Iw.constructor=$w;kk(Iw,Uc.prototype);Iw.isPureReactComponent=!0;var BN=Array.isArray,Tk=Object.prototype.hasOwnProperty,Rw={current:null},Mk={key:!0,ref:!0,__self:!0,__source:!0};function Lk(e,t,n){var r,i={},a=null,s=null;if(t!=null)for(r in t.ref!==void 0&&(s=t.ref),t.key!==void 0&&(a=""+t.key),t)Tk.call(t,r)&&!Mk.hasOwnProperty(r)&&(i[r]=t[r]);var c=arguments.length-2;if(c===1)i.children=n;else if(1>>1,Y=R[U];if(0>>1;Ui(ee,W))cei(Ne,ee)?(R[U]=Ne,R[ce]=W,U=ce):(R[U]=ee,R[ae]=W,U=ae);else if(cei(Ne,W))R[U]=Ne,R[ce]=W,U=ce;else break e}}return K}function i(R,K){var W=R.sortIndex-K.sortIndex;return W!==0?W:R.id-K.id}if(typeof performance=="object"&&typeof performance.now=="function"){var a=performance;e.unstable_now=function(){return a.now()}}else{var s=Date,c=s.now();e.unstable_now=function(){return s.now()-c}}var u=[],d=[],h=1,m=null,p=3,v=!1,_=!1,x=!1,y=typeof setTimeout=="function"?setTimeout:null,w=typeof clearTimeout=="function"?clearTimeout:null,b=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function j(R){for(var K=n(d);K!==null;){if(K.callback===null)r(d);else if(K.startTime<=R)r(d),K.sortIndex=K.expirationTime,t(u,K);else break;K=n(d)}}function E(R){if(x=!1,j(R),!_)if(n(u)!==null)_=!0,B(P);else{var K=n(d);K!==null&&G(E,K.startTime-R)}}function P(R,K){_=!1,x&&(x=!1,w(A),A=-1),v=!0;var W=p;try{for(j(K),m=n(u);m!==null&&(!(m.expirationTime>K)||R&&!z());){var U=m.callback;if(typeof U=="function"){m.callback=null,p=m.priorityLevel;var Y=U(m.expirationTime<=K);K=e.unstable_now(),typeof Y=="function"?m.callback=Y:m===n(u)&&r(u),j(K)}else r(u);m=n(u)}if(m!==null)var ne=!0;else{var ae=n(d);ae!==null&&G(E,ae.startTime-K),ne=!1}return ne}finally{m=null,p=W,v=!1}}var O=!1,C=null,A=-1,T=5,$=-1;function z(){return!(e.unstable_now()-$R||125U?(R.sortIndex=W,t(d,R),n(u)===null&&R===n(d)&&(x?(w(A),A=-1):x=!0,G(E,W-U))):(R.sortIndex=Y,t(u,R),_||v||(_=!0,B(P))),R},e.unstable_shouldYield=z,e.unstable_wrapCallback=function(R){var K=p;return function(){var W=p;p=K;try{return R.apply(this,arguments)}finally{p=W}}}})(Dk);Fk.exports=Dk;var E7=Fk.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var O7=N,vr=E7;function le(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ax=Object.prototype.hasOwnProperty,k7=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,UN={},WN={};function C7(e){return ax.call(WN,e)?!0:ax.call(UN,e)?!1:k7.test(e)?WN[e]=!0:(UN[e]=!0,!1)}function A7(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function T7(e,t,n,r){if(t===null||typeof t>"u"||A7(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function Un(e,t,n,r,i,a,s){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=i,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=a,this.removeEmptyString=s}var mn={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){mn[e]=new Un(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];mn[t]=new Un(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){mn[e]=new Un(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){mn[e]=new Un(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){mn[e]=new Un(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){mn[e]=new Un(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){mn[e]=new Un(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){mn[e]=new Un(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){mn[e]=new Un(e,5,!1,e.toLowerCase(),null,!1,!1)});var Dw=/[\-:]([a-z])/g;function Bw(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Dw,Bw);mn[t]=new Un(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Dw,Bw);mn[t]=new Un(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Dw,Bw);mn[t]=new Un(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){mn[e]=new Un(e,1,!1,e.toLowerCase(),null,!1,!1)});mn.xlinkHref=new Un("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){mn[e]=new Un(e,1,!1,e.toLowerCase(),null,!0,!0)});function zw(e,t,n,r){var i=mn.hasOwnProperty(t)?mn[t]:null;(i!==null?i.type!==0:r||!(2c||i[s]!==a[c]){var u=` +`+i[s].replace(" at new "," at ");return e.displayName&&u.includes("")&&(u=u.replace("",e.displayName)),u}while(1<=s&&0<=c);break}}}finally{zv=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Uu(e):""}function M7(e){switch(e.tag){case 5:return Uu(e.type);case 16:return Uu("Lazy");case 13:return Uu("Suspense");case 19:return Uu("SuspenseList");case 0:case 2:case 15:return e=Uv(e.type,!1),e;case 11:return e=Uv(e.type.render,!1),e;case 1:return e=Uv(e.type,!0),e;default:return""}}function cx(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case hl:return"Fragment";case fl:return"Portal";case ox:return"Profiler";case Uw:return"StrictMode";case sx:return"Suspense";case lx:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Uk:return(e.displayName||"Context")+".Consumer";case zk:return(e._context.displayName||"Context")+".Provider";case Ww:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Hw:return t=e.displayName||null,t!==null?t:cx(e.type)||"Memo";case Ha:t=e._payload,e=e._init;try{return cx(e(t))}catch{}}return null}function L7(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return cx(t);case 8:return t===Uw?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function So(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Hk(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function $7(e){var t=Hk(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var i=n.get,a=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(s){r=""+s,a.call(this,s)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(s){r=""+s},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function kh(e){e._valueTracker||(e._valueTracker=$7(e))}function Vk(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Hk(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Rm(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ux(e,t){var n=t.checked;return St({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function VN(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=So(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function qk(e,t){t=t.checked,t!=null&&zw(e,"checked",t,!1)}function dx(e,t){qk(e,t);var n=So(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?fx(e,t.type,n):t.hasOwnProperty("defaultValue")&&fx(e,t.type,So(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function qN(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function fx(e,t,n){(t!=="number"||Rm(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Wu=Array.isArray;function Tl(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i"+t.valueOf().toString()+"",t=Ch.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Nd(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var rd={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},I7=["Webkit","ms","Moz","O"];Object.keys(rd).forEach(function(e){I7.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),rd[t]=rd[e]})});function Yk(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||rd.hasOwnProperty(e)&&rd[e]?(""+t).trim():t+"px"}function Xk(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,i=Yk(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,i):e[n]=i}}var R7=St({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function px(e,t){if(t){if(R7[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(le(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(le(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(le(61))}if(t.style!=null&&typeof t.style!="object")throw Error(le(62))}}function gx(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var vx=null;function Vw(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var yx=null,Ml=null,Ll=null;function KN(e){if(e=zf(e)){if(typeof yx!="function")throw Error(le(280));var t=e.stateNode;t&&(t=Ug(t),yx(e.stateNode,e.type,t))}}function Qk(e){Ml?Ll?Ll.push(e):Ll=[e]:Ml=e}function Jk(){if(Ml){var e=Ml,t=Ll;if(Ll=Ml=null,KN(e),t)for(e=0;e>>=0,e===0?32:31-(G7(e)/K7|0)|0}var Ah=64,Th=4194304;function Hu(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function zm(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,i=e.suspendedLanes,a=e.pingedLanes,s=n&268435455;if(s!==0){var c=s&~i;c!==0?r=Hu(c):(a&=s,a!==0&&(r=Hu(a)))}else s=n&~i,s!==0?r=Hu(s):a!==0&&(r=Hu(a));if(r===0)return 0;if(t!==0&&t!==r&&!(t&i)&&(i=r&-r,a=t&-t,i>=a||i===16&&(a&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Df(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-di(t),e[t]=n}function J7(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=ad),iS=" ",aS=!1;function xC(e,t){switch(e){case"keyup":return E8.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function bC(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var ml=!1;function k8(e,t){switch(e){case"compositionend":return bC(t);case"keypress":return t.which!==32?null:(aS=!0,iS);case"textInput":return e=t.data,e===iS&&aS?null:e;default:return null}}function C8(e,t){if(ml)return e==="compositionend"||!Jw&&xC(e,t)?(e=vC(),ym=Yw=so=null,ml=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=cS(n)}}function NC(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?NC(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function SC(){for(var e=window,t=Rm();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Rm(e.document)}return t}function e_(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function D8(e){var t=SC(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&NC(n.ownerDocument.documentElement,n)){if(r!==null&&e_(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var i=n.textContent.length,a=Math.min(r.start,i);r=r.end===void 0?a:Math.min(r.end,i),!e.extend&&a>r&&(i=r,r=a,a=i),i=uS(n,a);var s=uS(n,r);i&&s&&(e.rangeCount!==1||e.anchorNode!==i.node||e.anchorOffset!==i.offset||e.focusNode!==s.node||e.focusOffset!==s.offset)&&(t=t.createRange(),t.setStart(i.node,i.offset),e.removeAllRanges(),a>r?(e.addRange(t),e.extend(s.node,s.offset)):(t.setEnd(s.node,s.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,pl=null,Nx=null,sd=null,Sx=!1;function dS(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Sx||pl==null||pl!==Rm(r)||(r=pl,"selectionStart"in r&&e_(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),sd&&Cd(sd,r)||(sd=r,r=Hm(Nx,"onSelect"),0yl||(e.current=Ax[yl],Ax[yl]=null,yl--)}function lt(e,t){yl++,Ax[yl]=e.current,e.current=t}var Po={},Sn=ko(Po),Yn=ko(!1),_s=Po;function rc(e,t){var n=e.type.contextTypes;if(!n)return Po;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var i={},a;for(a in n)i[a]=t[a];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function Xn(e){return e=e.childContextTypes,e!=null}function qm(){pt(Yn),pt(Sn)}function yS(e,t,n){if(Sn.current!==Po)throw Error(le(168));lt(Sn,t),lt(Yn,n)}function LC(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var i in r)if(!(i in t))throw Error(le(108,L7(e)||"Unknown",i));return St({},n,r)}function Zm(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Po,_s=Sn.current,lt(Sn,e),lt(Yn,Yn.current),!0}function xS(e,t,n){var r=e.stateNode;if(!r)throw Error(le(169));n?(e=LC(e,t,_s),r.__reactInternalMemoizedMergedChildContext=e,pt(Yn),pt(Sn),lt(Sn,e)):pt(Yn),lt(Yn,n)}var na=null,Wg=!1,ny=!1;function $C(e){na===null?na=[e]:na.push(e)}function X8(e){Wg=!0,$C(e)}function Co(){if(!ny&&na!==null){ny=!0;var e=0,t=Xe;try{var n=na;for(Xe=1;e>=s,i-=s,da=1<<32-di(t)+i|n<A?(T=C,C=null):T=C.sibling;var $=p(w,C,j[A],E);if($===null){C===null&&(C=T);break}e&&C&&$.alternate===null&&t(w,C),b=a($,b,A),O===null?P=$:O.sibling=$,O=$,C=T}if(A===j.length)return n(w,C),gt&&qo(w,A),P;if(C===null){for(;AA?(T=C,C=null):T=C.sibling;var z=p(w,C,$.value,E);if(z===null){C===null&&(C=T);break}e&&C&&z.alternate===null&&t(w,C),b=a(z,b,A),O===null?P=z:O.sibling=z,O=z,C=T}if($.done)return n(w,C),gt&&qo(w,A),P;if(C===null){for(;!$.done;A++,$=j.next())$=m(w,$.value,E),$!==null&&(b=a($,b,A),O===null?P=$:O.sibling=$,O=$);return gt&&qo(w,A),P}for(C=r(w,C);!$.done;A++,$=j.next())$=v(C,w,A,$.value,E),$!==null&&(e&&$.alternate!==null&&C.delete($.key===null?A:$.key),b=a($,b,A),O===null?P=$:O.sibling=$,O=$);return e&&C.forEach(function(D){return t(w,D)}),gt&&qo(w,A),P}function y(w,b,j,E){if(typeof j=="object"&&j!==null&&j.type===hl&&j.key===null&&(j=j.props.children),typeof j=="object"&&j!==null){switch(j.$$typeof){case Oh:e:{for(var P=j.key,O=b;O!==null;){if(O.key===P){if(P=j.type,P===hl){if(O.tag===7){n(w,O.sibling),b=i(O,j.props.children),b.return=w,w=b;break e}}else if(O.elementType===P||typeof P=="object"&&P!==null&&P.$$typeof===Ha&&_S(P)===O.type){n(w,O.sibling),b=i(O,j.props),b.ref=Pu(w,O,j),b.return=w,w=b;break e}n(w,O);break}else t(w,O);O=O.sibling}j.type===hl?(b=xs(j.props.children,w.mode,E,j.key),b.return=w,w=b):(E=Pm(j.type,j.key,j.props,null,w.mode,E),E.ref=Pu(w,b,j),E.return=w,w=E)}return s(w);case fl:e:{for(O=j.key;b!==null;){if(b.key===O)if(b.tag===4&&b.stateNode.containerInfo===j.containerInfo&&b.stateNode.implementation===j.implementation){n(w,b.sibling),b=i(b,j.children||[]),b.return=w,w=b;break e}else{n(w,b);break}else t(w,b);b=b.sibling}b=uy(j,w.mode,E),b.return=w,w=b}return s(w);case Ha:return O=j._init,y(w,b,O(j._payload),E)}if(Wu(j))return _(w,b,j,E);if(wu(j))return x(w,b,j,E);Dh(w,j)}return typeof j=="string"&&j!==""||typeof j=="number"?(j=""+j,b!==null&&b.tag===6?(n(w,b.sibling),b=i(b,j),b.return=w,w=b):(n(w,b),b=cy(j,w.mode,E),b.return=w,w=b),s(w)):n(w,b)}return y}var ac=DC(!0),BC=DC(!1),Ym=ko(null),Xm=null,wl=null,i_=null;function a_(){i_=wl=Xm=null}function o_(e){var t=Ym.current;pt(Ym),e._currentValue=t}function Lx(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function Il(e,t){Xm=e,i_=wl=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(Kn=!0),e.firstContext=null)}function Br(e){var t=e._currentValue;if(i_!==e)if(e={context:e,memoizedValue:t,next:null},wl===null){if(Xm===null)throw Error(le(308));wl=e,Xm.dependencies={lanes:0,firstContext:e}}else wl=wl.next=e;return t}var ts=null;function s_(e){ts===null?ts=[e]:ts.push(e)}function zC(e,t,n,r){var i=t.interleaved;return i===null?(n.next=n,s_(t)):(n.next=i.next,i.next=n),t.interleaved=n,Na(e,r)}function Na(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var Va=!1;function l_(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function UC(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function ya(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function vo(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,He&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,Na(e,n)}return i=r.interleaved,i===null?(t.next=t,s_(r)):(t.next=i.next,i.next=t),r.interleaved=t,Na(e,n)}function bm(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Zw(e,n)}}function jS(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var s={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};a===null?i=a=s:a=a.next=s,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function Qm(e,t,n,r){var i=e.updateQueue;Va=!1;var a=i.firstBaseUpdate,s=i.lastBaseUpdate,c=i.shared.pending;if(c!==null){i.shared.pending=null;var u=c,d=u.next;u.next=null,s===null?a=d:s.next=d,s=u;var h=e.alternate;h!==null&&(h=h.updateQueue,c=h.lastBaseUpdate,c!==s&&(c===null?h.firstBaseUpdate=d:c.next=d,h.lastBaseUpdate=u))}if(a!==null){var m=i.baseState;s=0,h=d=u=null,c=a;do{var p=c.lane,v=c.eventTime;if((r&p)===p){h!==null&&(h=h.next={eventTime:v,lane:0,tag:c.tag,payload:c.payload,callback:c.callback,next:null});e:{var _=e,x=c;switch(p=t,v=n,x.tag){case 1:if(_=x.payload,typeof _=="function"){m=_.call(v,m,p);break e}m=_;break e;case 3:_.flags=_.flags&-65537|128;case 0:if(_=x.payload,p=typeof _=="function"?_.call(v,m,p):_,p==null)break e;m=St({},m,p);break e;case 2:Va=!0}}c.callback!==null&&c.lane!==0&&(e.flags|=64,p=i.effects,p===null?i.effects=[c]:p.push(c))}else v={eventTime:v,lane:p,tag:c.tag,payload:c.payload,callback:c.callback,next:null},h===null?(d=h=v,u=m):h=h.next=v,s|=p;if(c=c.next,c===null){if(c=i.shared.pending,c===null)break;p=c,c=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(!0);if(h===null&&(u=m),i.baseState=u,i.firstBaseUpdate=d,i.lastBaseUpdate=h,t=i.shared.interleaved,t!==null){i=t;do s|=i.lane,i=i.next;while(i!==t)}else a===null&&(i.shared.lanes=0);Ss|=s,e.lanes=s,e.memoizedState=m}}function NS(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=iy.transition;iy.transition={};try{e(!1),t()}finally{Xe=n,iy.transition=r}}function a4(){return zr().memoizedState}function tR(e,t,n){var r=xo(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},o4(e))s4(t,n);else if(n=zC(e,t,n,r),n!==null){var i=In();fi(n,e,r,i),l4(n,t,r)}}function nR(e,t,n){var r=xo(e),i={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(o4(e))s4(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var s=t.lastRenderedState,c=a(s,n);if(i.hasEagerState=!0,i.eagerState=c,mi(c,s)){var u=t.interleaved;u===null?(i.next=i,s_(t)):(i.next=u.next,u.next=i),t.interleaved=i;return}}catch{}finally{}n=zC(e,t,i,r),n!==null&&(i=In(),fi(n,e,r,i),l4(n,t,r))}}function o4(e){var t=e.alternate;return e===Nt||t!==null&&t===Nt}function s4(e,t){ld=ep=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function l4(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Zw(e,n)}}var tp={readContext:Br,useCallback:gn,useContext:gn,useEffect:gn,useImperativeHandle:gn,useInsertionEffect:gn,useLayoutEffect:gn,useMemo:gn,useReducer:gn,useRef:gn,useState:gn,useDebugValue:gn,useDeferredValue:gn,useTransition:gn,useMutableSource:gn,useSyncExternalStore:gn,useId:gn,unstable_isNewReconciler:!1},rR={readContext:Br,useCallback:function(e,t){return Si().memoizedState=[e,t===void 0?null:t],e},useContext:Br,useEffect:PS,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,_m(4194308,4,e4.bind(null,t,e),n)},useLayoutEffect:function(e,t){return _m(4194308,4,e,t)},useInsertionEffect:function(e,t){return _m(4,2,e,t)},useMemo:function(e,t){var n=Si();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Si();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=tR.bind(null,Nt,e),[r.memoizedState,e]},useRef:function(e){var t=Si();return e={current:e},t.memoizedState=e},useState:SS,useDebugValue:g_,useDeferredValue:function(e){return Si().memoizedState=e},useTransition:function(){var e=SS(!1),t=e[0];return e=eR.bind(null,e[1]),Si().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Nt,i=Si();if(gt){if(n===void 0)throw Error(le(407));n=n()}else{if(n=t(),nn===null)throw Error(le(349));Ns&30||qC(r,t,n)}i.memoizedState=n;var a={value:n,getSnapshot:t};return i.queue=a,PS(GC.bind(null,r,a,e),[e]),r.flags|=2048,Fd(9,ZC.bind(null,r,a,n,t),void 0,null),n},useId:function(){var e=Si(),t=nn.identifierPrefix;if(gt){var n=fa,r=da;n=(r&~(1<<32-di(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=Id++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=s.createElement(n,{is:r.is}):(e=s.createElement(n),n==="select"&&(s=e,r.multiple?s.multiple=!0:r.size&&(s.size=r.size))):e=s.createElementNS(e,n),e[ki]=t,e[Md]=r,y4(e,t,!1,!1),t.stateNode=e;e:{switch(s=gx(n,r),n){case"dialog":ft("cancel",e),ft("close",e),i=r;break;case"iframe":case"object":case"embed":ft("load",e),i=r;break;case"video":case"audio":for(i=0;ilc&&(t.flags|=128,r=!0,Eu(a,!1),t.lanes=4194304)}else{if(!r)if(e=Jm(s),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Eu(a,!0),a.tail===null&&a.tailMode==="hidden"&&!s.alternate&&!gt)return vn(t),null}else 2*Lt()-a.renderingStartTime>lc&&n!==1073741824&&(t.flags|=128,r=!0,Eu(a,!1),t.lanes=4194304);a.isBackwards?(s.sibling=t.child,t.child=s):(n=a.last,n!==null?n.sibling=s:t.child=s,a.last=s)}return a.tail!==null?(t=a.tail,a.rendering=t,a.tail=t.sibling,a.renderingStartTime=Lt(),t.sibling=null,n=_t.current,lt(_t,r?n&1|2:n&1),t):(vn(t),null);case 22:case 23:return __(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?ar&1073741824&&(vn(t),t.subtreeFlags&6&&(t.flags|=8192)):vn(t),null;case 24:return null;case 25:return null}throw Error(le(156,t.tag))}function dR(e,t){switch(n_(t),t.tag){case 1:return Xn(t.type)&&qm(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return oc(),pt(Yn),pt(Sn),d_(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return u_(t),null;case 13:if(pt(_t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(le(340));ic()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return pt(_t),null;case 4:return oc(),null;case 10:return o_(t.type._context),null;case 22:case 23:return __(),null;case 24:return null;default:return null}}var zh=!1,bn=!1,fR=typeof WeakSet=="function"?WeakSet:Set,pe=null;function _l(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){kt(e,t,r)}else n.current=null}function Wx(e,t,n){try{n()}catch(r){kt(e,t,r)}}var RS=!1;function hR(e,t){if(Px=Um,e=SC(),e_(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var i=r.anchorOffset,a=r.focusNode;r=r.focusOffset;try{n.nodeType,a.nodeType}catch{n=null;break e}var s=0,c=-1,u=-1,d=0,h=0,m=e,p=null;t:for(;;){for(var v;m!==n||i!==0&&m.nodeType!==3||(c=s+i),m!==a||r!==0&&m.nodeType!==3||(u=s+r),m.nodeType===3&&(s+=m.nodeValue.length),(v=m.firstChild)!==null;)p=m,m=v;for(;;){if(m===e)break t;if(p===n&&++d===i&&(c=s),p===a&&++h===r&&(u=s),(v=m.nextSibling)!==null)break;m=p,p=m.parentNode}m=v}n=c===-1||u===-1?null:{start:c,end:u}}else n=null}n=n||{start:0,end:0}}else n=null;for(Ex={focusedElem:e,selectionRange:n},Um=!1,pe=t;pe!==null;)if(t=pe,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,pe=e;else for(;pe!==null;){t=pe;try{var _=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(_!==null){var x=_.memoizedProps,y=_.memoizedState,w=t.stateNode,b=w.getSnapshotBeforeUpdate(t.elementType===t.type?x:Jr(t.type,x),y);w.__reactInternalSnapshotBeforeUpdate=b}break;case 3:var j=t.stateNode.containerInfo;j.nodeType===1?j.textContent="":j.nodeType===9&&j.documentElement&&j.removeChild(j.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(le(163))}}catch(E){kt(t,t.return,E)}if(e=t.sibling,e!==null){e.return=t.return,pe=e;break}pe=t.return}return _=RS,RS=!1,_}function cd(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var i=r=r.next;do{if((i.tag&e)===e){var a=i.destroy;i.destroy=void 0,a!==void 0&&Wx(t,n,a)}i=i.next}while(i!==r)}}function qg(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Hx(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function w4(e){var t=e.alternate;t!==null&&(e.alternate=null,w4(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[ki],delete t[Md],delete t[Cx],delete t[K8],delete t[Y8])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function _4(e){return e.tag===5||e.tag===3||e.tag===4}function FS(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||_4(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Vx(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Vm));else if(r!==4&&(e=e.child,e!==null))for(Vx(e,t,n),e=e.sibling;e!==null;)Vx(e,t,n),e=e.sibling}function qx(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(qx(e,t,n),e=e.sibling;e!==null;)qx(e,t,n),e=e.sibling}var un=null,ti=!1;function za(e,t,n){for(n=n.child;n!==null;)j4(e,t,n),n=n.sibling}function j4(e,t,n){if(Ii&&typeof Ii.onCommitFiberUnmount=="function")try{Ii.onCommitFiberUnmount(Fg,n)}catch{}switch(n.tag){case 5:bn||_l(n,t);case 6:var r=un,i=ti;un=null,za(e,t,n),un=r,ti=i,un!==null&&(ti?(e=un,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):un.removeChild(n.stateNode));break;case 18:un!==null&&(ti?(e=un,n=n.stateNode,e.nodeType===8?ty(e.parentNode,n):e.nodeType===1&&ty(e,n),Od(e)):ty(un,n.stateNode));break;case 4:r=un,i=ti,un=n.stateNode.containerInfo,ti=!0,za(e,t,n),un=r,ti=i;break;case 0:case 11:case 14:case 15:if(!bn&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){i=r=r.next;do{var a=i,s=a.destroy;a=a.tag,s!==void 0&&(a&2||a&4)&&Wx(n,t,s),i=i.next}while(i!==r)}za(e,t,n);break;case 1:if(!bn&&(_l(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(c){kt(n,t,c)}za(e,t,n);break;case 21:za(e,t,n);break;case 22:n.mode&1?(bn=(r=bn)||n.memoizedState!==null,za(e,t,n),bn=r):za(e,t,n);break;default:za(e,t,n)}}function DS(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new fR),t.forEach(function(r){var i=_R.bind(null,e,r);n.has(r)||(n.add(r),r.then(i,i))})}}function Xr(e,t){var n=t.deletions;if(n!==null)for(var r=0;ri&&(i=s),r&=~a}if(r=i,r=Lt()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*pR(r/1960))-r,10e?16:e,lo===null)var r=!1;else{if(e=lo,lo=null,ip=0,He&6)throw Error(le(331));var i=He;for(He|=4,pe=e.current;pe!==null;){var a=pe,s=a.child;if(pe.flags&16){var c=a.deletions;if(c!==null){for(var u=0;uLt()-b_?ys(e,0):x_|=n),Qn(e,t)}function A4(e,t){t===0&&(e.mode&1?(t=Th,Th<<=1,!(Th&130023424)&&(Th=4194304)):t=1);var n=In();e=Na(e,t),e!==null&&(Df(e,t,n),Qn(e,n))}function wR(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),A4(e,n)}function _R(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,i=e.memoizedState;i!==null&&(n=i.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(le(314))}r!==null&&r.delete(t),A4(e,n)}var T4;T4=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Yn.current)Kn=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return Kn=!1,cR(e,t,n);Kn=!!(e.flags&131072)}else Kn=!1,gt&&t.flags&1048576&&IC(t,Km,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;jm(e,t),e=t.pendingProps;var i=rc(t,Sn.current);Il(t,n),i=h_(null,t,r,e,i,n);var a=m_();return t.flags|=1,typeof i=="object"&&i!==null&&typeof i.render=="function"&&i.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Xn(r)?(a=!0,Zm(t)):a=!1,t.memoizedState=i.state!==null&&i.state!==void 0?i.state:null,l_(t),i.updater=Vg,t.stateNode=i,i._reactInternals=t,Ix(t,r,e,n),t=Dx(null,t,r,!0,a,n)):(t.tag=0,gt&&a&&t_(t),On(null,t,i,n),t=t.child),t;case 16:r=t.elementType;e:{switch(jm(e,t),e=t.pendingProps,i=r._init,r=i(r._payload),t.type=r,i=t.tag=NR(r),e=Jr(r,e),i){case 0:t=Fx(null,t,r,e,n);break e;case 1:t=LS(null,t,r,e,n);break e;case 11:t=TS(null,t,r,e,n);break e;case 14:t=MS(null,t,r,Jr(r.type,e),n);break e}throw Error(le(306,r,""))}return t;case 0:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:Jr(r,i),Fx(e,t,r,i,n);case 1:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:Jr(r,i),LS(e,t,r,i,n);case 3:e:{if(p4(t),e===null)throw Error(le(387));r=t.pendingProps,a=t.memoizedState,i=a.element,UC(e,t),Qm(t,r,null,n);var s=t.memoizedState;if(r=s.element,a.isDehydrated)if(a={element:r,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=a,t.memoizedState=a,t.flags&256){i=sc(Error(le(423)),t),t=$S(e,t,r,n,i);break e}else if(r!==i){i=sc(Error(le(424)),t),t=$S(e,t,r,n,i);break e}else for(hr=go(t.stateNode.containerInfo.firstChild),pr=t,gt=!0,ii=null,n=BC(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(ic(),r===i){t=Sa(e,t,n);break e}On(e,t,r,n)}t=t.child}return t;case 5:return WC(t),e===null&&Mx(t),r=t.type,i=t.pendingProps,a=e!==null?e.memoizedProps:null,s=i.children,Ox(r,i)?s=null:a!==null&&Ox(r,a)&&(t.flags|=32),m4(e,t),On(e,t,s,n),t.child;case 6:return e===null&&Mx(t),null;case 13:return g4(e,t,n);case 4:return c_(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=ac(t,null,r,n):On(e,t,r,n),t.child;case 11:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:Jr(r,i),TS(e,t,r,i,n);case 7:return On(e,t,t.pendingProps,n),t.child;case 8:return On(e,t,t.pendingProps.children,n),t.child;case 12:return On(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,i=t.pendingProps,a=t.memoizedProps,s=i.value,lt(Ym,r._currentValue),r._currentValue=s,a!==null)if(mi(a.value,s)){if(a.children===i.children&&!Yn.current){t=Sa(e,t,n);break e}}else for(a=t.child,a!==null&&(a.return=t);a!==null;){var c=a.dependencies;if(c!==null){s=a.child;for(var u=c.firstContext;u!==null;){if(u.context===r){if(a.tag===1){u=ya(-1,n&-n),u.tag=2;var d=a.updateQueue;if(d!==null){d=d.shared;var h=d.pending;h===null?u.next=u:(u.next=h.next,h.next=u),d.pending=u}}a.lanes|=n,u=a.alternate,u!==null&&(u.lanes|=n),Lx(a.return,n,t),c.lanes|=n;break}u=u.next}}else if(a.tag===10)s=a.type===t.type?null:a.child;else if(a.tag===18){if(s=a.return,s===null)throw Error(le(341));s.lanes|=n,c=s.alternate,c!==null&&(c.lanes|=n),Lx(s,n,t),s=a.sibling}else s=a.child;if(s!==null)s.return=a;else for(s=a;s!==null;){if(s===t){s=null;break}if(a=s.sibling,a!==null){a.return=s.return,s=a;break}s=s.return}a=s}On(e,t,i.children,n),t=t.child}return t;case 9:return i=t.type,r=t.pendingProps.children,Il(t,n),i=Br(i),r=r(i),t.flags|=1,On(e,t,r,n),t.child;case 14:return r=t.type,i=Jr(r,t.pendingProps),i=Jr(r.type,i),MS(e,t,r,i,n);case 15:return f4(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:Jr(r,i),jm(e,t),t.tag=1,Xn(r)?(e=!0,Zm(t)):e=!1,Il(t,n),c4(t,r,i),Ix(t,r,i,n),Dx(null,t,r,!0,e,n);case 19:return v4(e,t,n);case 22:return h4(e,t,n)}throw Error(le(156,t.tag))};function M4(e,t){return oC(e,t)}function jR(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Mr(e,t,n,r){return new jR(e,t,n,r)}function N_(e){return e=e.prototype,!(!e||!e.isReactComponent)}function NR(e){if(typeof e=="function")return N_(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Ww)return 11;if(e===Hw)return 14}return 2}function bo(e,t){var n=e.alternate;return n===null?(n=Mr(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Pm(e,t,n,r,i,a){var s=2;if(r=e,typeof e=="function")N_(e)&&(s=1);else if(typeof e=="string")s=5;else e:switch(e){case hl:return xs(n.children,i,a,t);case Uw:s=8,i|=8;break;case ox:return e=Mr(12,n,t,i|2),e.elementType=ox,e.lanes=a,e;case sx:return e=Mr(13,n,t,i),e.elementType=sx,e.lanes=a,e;case lx:return e=Mr(19,n,t,i),e.elementType=lx,e.lanes=a,e;case Wk:return Gg(n,i,a,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case zk:s=10;break e;case Uk:s=9;break e;case Ww:s=11;break e;case Hw:s=14;break e;case Ha:s=16,r=null;break e}throw Error(le(130,e==null?e:typeof e,""))}return t=Mr(s,n,t,i),t.elementType=e,t.type=r,t.lanes=a,t}function xs(e,t,n,r){return e=Mr(7,e,r,t),e.lanes=n,e}function Gg(e,t,n,r){return e=Mr(22,e,r,t),e.elementType=Wk,e.lanes=n,e.stateNode={isHidden:!1},e}function cy(e,t,n){return e=Mr(6,e,null,t),e.lanes=n,e}function uy(e,t,n){return t=Mr(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function SR(e,t,n,r,i){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Hv(0),this.expirationTimes=Hv(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Hv(0),this.identifierPrefix=r,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function S_(e,t,n,r,i,a,s,c,u){return e=new SR(e,t,n,c,u),t===1?(t=1,a===!0&&(t|=8)):t=0,a=Mr(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},l_(a),e}function PR(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(R4)}catch(e){console.error(e)}}R4(),Rk.exports=yr;var F4=Rk.exports,ZS=F4;ix.createRoot=ZS.createRoot,ix.hydrateRoot=ZS.hydrateRoot;/** + * @remix-run/router v1.23.2 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Bd(){return Bd=Object.assign?Object.assign.bind():function(e){for(var t=1;t"u")throw new Error(t)}function k_(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function TR(){return Math.random().toString(36).substr(2,8)}function KS(e,t){return{usr:e.state,key:e.key,idx:t}}function Xx(e,t,n,r){return n===void 0&&(n=null),Bd({pathname:typeof e=="string"?e:e.pathname,search:"",hash:""},typeof t=="string"?Vc(t):t,{state:n,key:t&&t.key||r||TR()})}function sp(e){let{pathname:t="/",search:n="",hash:r=""}=e;return n&&n!=="?"&&(t+=n.charAt(0)==="?"?n:"?"+n),r&&r!=="#"&&(t+=r.charAt(0)==="#"?r:"#"+r),t}function Vc(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substr(n),e=e.substr(0,n));let r=e.indexOf("?");r>=0&&(t.search=e.substr(r),e=e.substr(0,r)),e&&(t.pathname=e)}return t}function MR(e,t,n,r){r===void 0&&(r={});let{window:i=document.defaultView,v5Compat:a=!1}=r,s=i.history,c=co.Pop,u=null,d=h();d==null&&(d=0,s.replaceState(Bd({},s.state,{idx:d}),""));function h(){return(s.state||{idx:null}).idx}function m(){c=co.Pop;let y=h(),w=y==null?null:y-d;d=y,u&&u({action:c,location:x.location,delta:w})}function p(y,w){c=co.Push;let b=Xx(x.location,y,w);d=h()+1;let j=KS(b,d),E=x.createHref(b);try{s.pushState(j,"",E)}catch(P){if(P instanceof DOMException&&P.name==="DataCloneError")throw P;i.location.assign(E)}a&&u&&u({action:c,location:x.location,delta:1})}function v(y,w){c=co.Replace;let b=Xx(x.location,y,w);d=h();let j=KS(b,d),E=x.createHref(b);s.replaceState(j,"",E),a&&u&&u({action:c,location:x.location,delta:0})}function _(y){let w=i.location.origin!=="null"?i.location.origin:i.location.href,b=typeof y=="string"?y:sp(y);return b=b.replace(/ $/,"%20"),Bt(w,"No window.location.(origin|href) available to create URL for href: "+b),new URL(b,w)}let x={get action(){return c},get location(){return e(i,s)},listen(y){if(u)throw new Error("A history only accepts one active listener");return i.addEventListener(GS,m),u=y,()=>{i.removeEventListener(GS,m),u=null}},createHref(y){return t(i,y)},createURL:_,encodeLocation(y){let w=_(y);return{pathname:w.pathname,search:w.search,hash:w.hash}},push:p,replace:v,go(y){return s.go(y)}};return x}var YS;(function(e){e.data="data",e.deferred="deferred",e.redirect="redirect",e.error="error"})(YS||(YS={}));function LR(e,t,n){return n===void 0&&(n="/"),$R(e,t,n)}function $R(e,t,n,r){let i=typeof t=="string"?Vc(t):t,a=C_(i.pathname||"/",n);if(a==null)return null;let s=D4(e);IR(s);let c=null;for(let u=0;c==null&&u{let u={relativePath:c===void 0?a.path||"":c,caseSensitive:a.caseSensitive===!0,childrenIndex:s,route:a};u.relativePath.startsWith("/")&&(Bt(u.relativePath.startsWith(r),'Absolute route path "'+u.relativePath+'" nested under path '+('"'+r+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),u.relativePath=u.relativePath.slice(r.length));let d=wo([r,u.relativePath]),h=n.concat(u);a.children&&a.children.length>0&&(Bt(a.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+d+'".')),D4(a.children,t,h,d)),!(a.path==null&&!a.index)&&t.push({path:d,score:WR(d,a.index),routesMeta:h})};return e.forEach((a,s)=>{var c;if(a.path===""||!((c=a.path)!=null&&c.includes("?")))i(a,s);else for(let u of B4(a.path))i(a,s,u)}),t}function B4(e){let t=e.split("/");if(t.length===0)return[];let[n,...r]=t,i=n.endsWith("?"),a=n.replace(/\?$/,"");if(r.length===0)return i?[a,""]:[a];let s=B4(r.join("/")),c=[];return c.push(...s.map(u=>u===""?a:[a,u].join("/"))),i&&c.push(...s),c.map(u=>e.startsWith("/")&&u===""?"/":u)}function IR(e){e.sort((t,n)=>t.score!==n.score?n.score-t.score:HR(t.routesMeta.map(r=>r.childrenIndex),n.routesMeta.map(r=>r.childrenIndex)))}const RR=/^:[\w-]+$/,FR=3,DR=2,BR=1,zR=10,UR=-2,XS=e=>e==="*";function WR(e,t){let n=e.split("/"),r=n.length;return n.some(XS)&&(r+=UR),t&&(r+=DR),n.filter(i=>!XS(i)).reduce((i,a)=>i+(RR.test(a)?FR:a===""?BR:zR),r)}function HR(e,t){return e.length===t.length&&e.slice(0,-1).every((r,i)=>r===t[i])?e[e.length-1]-t[t.length-1]:0}function VR(e,t,n){let{routesMeta:r}=e,i={},a="/",s=[];for(let c=0;c{let{paramName:p,isOptional:v}=h;if(p==="*"){let x=c[m]||"";s=a.slice(0,a.length-x.length).replace(/(.)\/+$/,"$1")}const _=c[m];return v&&!_?d[p]=void 0:d[p]=(_||"").replace(/%2F/g,"/"),d},{}),pathname:a,pathnameBase:s,pattern:e}}function ZR(e,t,n){t===void 0&&(t=!1),n===void 0&&(n=!0),k_(e==="*"||!e.endsWith("*")||e.endsWith("/*"),'Route path "'+e+'" will be treated as if it were '+('"'+e.replace(/\*$/,"/*")+'" because the `*` character must ')+"always follow a `/` in the pattern. To get rid of this warning, "+('please change the route path to "'+e.replace(/\*$/,"/*")+'".'));let r=[],i="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(s,c,u)=>(r.push({paramName:c,isOptional:u!=null}),u?"/?([^\\/]+)?":"/([^\\/]+)"));return e.endsWith("*")?(r.push({paramName:"*"}),i+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):n?i+="\\/*$":e!==""&&e!=="/"&&(i+="(?:(?=\\/|$))"),[new RegExp(i,t?void 0:"i"),r]}function GR(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return k_(!1,'The URL path "'+e+'" could not be decoded because it is is a malformed URL segment. This is probably due to a bad percent '+("encoding ("+t+").")),e}}function C_(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith("/")?t.length-1:t.length,r=e.charAt(n);return r&&r!=="/"?null:e.slice(n)||"/"}const KR=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,YR=e=>KR.test(e);function XR(e,t){t===void 0&&(t="/");let{pathname:n,search:r="",hash:i=""}=typeof e=="string"?Vc(e):e,a;if(n)if(YR(n))a=n;else{if(n.includes("//")){let s=n;n=n.replace(/\/\/+/g,"/"),k_(!1,"Pathnames cannot have embedded double slashes - normalizing "+(s+" -> "+n))}n.startsWith("/")?a=QS(n.substring(1),"/"):a=QS(n,t)}else a=t;return{pathname:a,search:e9(r),hash:t9(i)}}function QS(e,t){let n=t.replace(/\/+$/,"").split("/");return e.split("/").forEach(i=>{i===".."?n.length>1&&n.pop():i!=="."&&n.push(i)}),n.length>1?n.join("/"):"/"}function dy(e,t,n,r){return"Cannot include a '"+e+"' character in a manually specified "+("`to."+t+"` field ["+JSON.stringify(r)+"]. Please separate it out to the ")+("`to."+n+"` field. Alternatively you may provide the full path as ")+'a string in and the router will parse it for you.'}function QR(e){return e.filter((t,n)=>n===0||t.route.path&&t.route.path.length>0)}function z4(e,t){let n=QR(e);return t?n.map((r,i)=>i===n.length-1?r.pathname:r.pathnameBase):n.map(r=>r.pathnameBase)}function U4(e,t,n,r){r===void 0&&(r=!1);let i;typeof e=="string"?i=Vc(e):(i=Bd({},e),Bt(!i.pathname||!i.pathname.includes("?"),dy("?","pathname","search",i)),Bt(!i.pathname||!i.pathname.includes("#"),dy("#","pathname","hash",i)),Bt(!i.search||!i.search.includes("#"),dy("#","search","hash",i)));let a=e===""||i.pathname==="",s=a?"/":i.pathname,c;if(s==null)c=n;else{let m=t.length-1;if(!r&&s.startsWith("..")){let p=s.split("/");for(;p[0]==="..";)p.shift(),m-=1;i.pathname=p.join("/")}c=m>=0?t[m]:"/"}let u=XR(i,c),d=s&&s!=="/"&&s.endsWith("/"),h=(a||s===".")&&n.endsWith("/");return!u.pathname.endsWith("/")&&(d||h)&&(u.pathname+="/"),u}const wo=e=>e.join("/").replace(/\/\/+/g,"/"),JR=e=>e.replace(/\/+$/,"").replace(/^\/*/,"/"),e9=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,t9=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e;function n9(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}const W4=["post","put","patch","delete"];new Set(W4);const r9=["get",...W4];new Set(r9);/** + * React Router v6.30.3 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function zd(){return zd=Object.assign?Object.assign.bind():function(e){for(var t=1;t{c.current=!0}),N.useCallback(function(d,h){if(h===void 0&&(h={}),!c.current)return;if(typeof d=="number"){r.go(d);return}let m=U4(d,JSON.parse(s),a,h.relative==="path");e==null&&t!=="/"&&(m.pathname=m.pathname==="/"?t:wo([t,m.pathname])),(h.replace?r.replace:r.push)(m,h.state,h)},[t,r,s,a,e])}const s9=N.createContext(null);function l9(e){let t=N.useContext(La).outlet;return t&&N.createElement(s9.Provider,{value:e},t)}function c9(){let{matches:e}=N.useContext(La),t=e[e.length-1];return t?t.params:{}}function q4(e,t){let{relative:n}=t===void 0?{}:t,{future:r}=N.useContext(Bs),{matches:i}=N.useContext(La),{pathname:a}=zs(),s=JSON.stringify(z4(i,r.v7_relativeSplatPath));return N.useMemo(()=>U4(e,JSON.parse(s),a,n==="path"),[e,s,a,n])}function u9(e,t){return d9(e,t)}function d9(e,t,n,r){Wf()||Bt(!1);let{navigator:i}=N.useContext(Bs),{matches:a}=N.useContext(La),s=a[a.length-1],c=s?s.params:{};s&&s.pathname;let u=s?s.pathnameBase:"/";s&&s.route;let d=zs(),h;if(t){var m;let y=typeof t=="string"?Vc(t):t;u==="/"||(m=y.pathname)!=null&&m.startsWith(u)||Bt(!1),h=y}else h=d;let p=h.pathname||"/",v=p;if(u!=="/"){let y=u.replace(/^\//,"").split("/");v="/"+p.replace(/^\//,"").split("/").slice(y.length).join("/")}let _=LR(e,{pathname:v}),x=g9(_&&_.map(y=>Object.assign({},y,{params:Object.assign({},c,y.params),pathname:wo([u,i.encodeLocation?i.encodeLocation(y.pathname).pathname:y.pathname]),pathnameBase:y.pathnameBase==="/"?u:wo([u,i.encodeLocation?i.encodeLocation(y.pathnameBase).pathname:y.pathnameBase])})),a,n,r);return t&&x?N.createElement(Jg.Provider,{value:{location:zd({pathname:"/",search:"",hash:"",state:null,key:"default"},h),navigationType:co.Pop}},x):x}function f9(){let e=b9(),t=n9(e)?e.status+" "+e.statusText:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,i={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return N.createElement(N.Fragment,null,N.createElement("h2",null,"Unexpected Application Error!"),N.createElement("h3",{style:{fontStyle:"italic"}},t),n?N.createElement("pre",{style:i},n):null,null)}const h9=N.createElement(f9,null);class m9 extends N.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,n){return n.location!==t.location||n.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:n.error,location:n.location,revalidation:t.revalidation||n.revalidation}}componentDidCatch(t,n){console.error("React Router caught the following error during render",t,n)}render(){return this.state.error!==void 0?N.createElement(La.Provider,{value:this.props.routeContext},N.createElement(H4.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function p9(e){let{routeContext:t,match:n,children:r}=e,i=N.useContext(A_);return i&&i.static&&i.staticContext&&(n.route.errorElement||n.route.ErrorBoundary)&&(i.staticContext._deepestRenderedBoundaryId=n.route.id),N.createElement(La.Provider,{value:t},r)}function g9(e,t,n,r){var i;if(t===void 0&&(t=[]),n===void 0&&(n=null),r===void 0&&(r=null),e==null){var a;if(!n)return null;if(n.errors)e=n.matches;else if((a=r)!=null&&a.v7_partialHydration&&t.length===0&&!n.initialized&&n.matches.length>0)e=n.matches;else return null}let s=e,c=(i=n)==null?void 0:i.errors;if(c!=null){let h=s.findIndex(m=>m.route.id&&(c==null?void 0:c[m.route.id])!==void 0);h>=0||Bt(!1),s=s.slice(0,Math.min(s.length,h+1))}let u=!1,d=-1;if(n&&r&&r.v7_partialHydration)for(let h=0;h=0?s=s.slice(0,d+1):s=[s[0]];break}}}return s.reduceRight((h,m,p)=>{let v,_=!1,x=null,y=null;n&&(v=c&&m.route.id?c[m.route.id]:void 0,x=m.route.errorElement||h9,u&&(d<0&&p===0?(_9("route-fallback"),_=!0,y=null):d===p&&(_=!0,y=m.route.hydrateFallbackElement||null)));let w=t.concat(s.slice(0,p+1)),b=()=>{let j;return v?j=x:_?j=y:m.route.Component?j=N.createElement(m.route.Component,null):m.route.element?j=m.route.element:j=h,N.createElement(p9,{match:m,routeContext:{outlet:h,matches:w,isDataRoute:n!=null},children:j})};return n&&(m.route.ErrorBoundary||m.route.errorElement||p===0)?N.createElement(m9,{location:n.location,revalidation:n.revalidation,component:x,error:v,children:b(),routeContext:{outlet:null,matches:w,isDataRoute:!0}}):b()},null)}var Z4=function(e){return e.UseBlocker="useBlocker",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e}(Z4||{}),G4=function(e){return e.UseBlocker="useBlocker",e.UseLoaderData="useLoaderData",e.UseActionData="useActionData",e.UseRouteError="useRouteError",e.UseNavigation="useNavigation",e.UseRouteLoaderData="useRouteLoaderData",e.UseMatches="useMatches",e.UseRevalidator="useRevalidator",e.UseNavigateStable="useNavigate",e.UseRouteId="useRouteId",e}(G4||{});function v9(e){let t=N.useContext(A_);return t||Bt(!1),t}function y9(e){let t=N.useContext(i9);return t||Bt(!1),t}function x9(e){let t=N.useContext(La);return t||Bt(!1),t}function K4(e){let t=x9(),n=t.matches[t.matches.length-1];return n.route.id||Bt(!1),n.route.id}function b9(){var e;let t=N.useContext(H4),n=y9(),r=K4();return t!==void 0?t:(e=n.errors)==null?void 0:e[r]}function w9(){let{router:e}=v9(Z4.UseNavigateStable),t=K4(G4.UseNavigateStable),n=N.useRef(!1);return V4(()=>{n.current=!0}),N.useCallback(function(i,a){a===void 0&&(a={}),n.current&&(typeof i=="number"?e.navigate(i):e.navigate(i,zd({fromRouteId:t},a)))},[e,t])}const JS={};function _9(e,t,n){JS[e]||(JS[e]=!0)}function j9(e,t){e==null||e.v7_startTransition,e==null||e.v7_relativeSplatPath}function N9(e){return l9(e.context)}function et(e){Bt(!1)}function S9(e){let{basename:t="/",children:n=null,location:r,navigationType:i=co.Pop,navigator:a,static:s=!1,future:c}=e;Wf()&&Bt(!1);let u=t.replace(/^\/*/,"/"),d=N.useMemo(()=>({basename:u,navigator:a,static:s,future:zd({v7_relativeSplatPath:!1},c)}),[u,c,a,s]);typeof r=="string"&&(r=Vc(r));let{pathname:h="/",search:m="",hash:p="",state:v=null,key:_="default"}=r,x=N.useMemo(()=>{let y=C_(h,u);return y==null?null:{location:{pathname:y,search:m,hash:p,state:v,key:_},navigationType:i}},[u,h,m,p,v,_,i]);return x==null?null:N.createElement(Bs.Provider,{value:d},N.createElement(Jg.Provider,{children:n,value:x}))}function P9(e){let{children:t,location:n}=e;return u9(Qx(t),n)}new Promise(()=>{});function Qx(e,t){t===void 0&&(t=[]);let n=[];return N.Children.forEach(e,(r,i)=>{if(!N.isValidElement(r))return;let a=[...t,i];if(r.type===N.Fragment){n.push.apply(n,Qx(r.props.children,a));return}r.type!==et&&Bt(!1),!r.props.index||!r.props.children||Bt(!1);let s={id:r.props.id||a.join("-"),caseSensitive:r.props.caseSensitive,element:r.props.element,Component:r.props.Component,index:r.props.index,path:r.props.path,loader:r.props.loader,action:r.props.action,errorElement:r.props.errorElement,ErrorBoundary:r.props.ErrorBoundary,hasErrorBoundary:r.props.ErrorBoundary!=null||r.props.errorElement!=null,shouldRevalidate:r.props.shouldRevalidate,handle:r.props.handle,lazy:r.props.lazy};r.props.children&&(s.children=Qx(r.props.children,a)),n.push(s)}),n}/** + * React Router DOM v6.30.3 + * + * Copyright (c) Remix Software Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE.md file in the root directory of this source tree. + * + * @license MIT + */function Jx(){return Jx=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&(n[i]=e[i]);return n}function O9(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function k9(e,t){return e.button===0&&(!t||t==="_self")&&!O9(e)}function eb(e){return e===void 0&&(e=""),new URLSearchParams(typeof e=="string"||Array.isArray(e)||e instanceof URLSearchParams?e:Object.keys(e).reduce((t,n)=>{let r=e[n];return t.concat(Array.isArray(r)?r.map(i=>[n,i]):[[n,r]])},[]))}function C9(e,t){let n=eb(e);return t&&t.forEach((r,i)=>{n.has(i)||t.getAll(i).forEach(a=>{n.append(i,a)})}),n}const A9=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],T9="6";try{window.__reactRouterVersion=T9}catch{}const M9="startTransition",e5=rx[M9];function L9(e){let{basename:t,children:n,future:r,window:i}=e,a=N.useRef();a.current==null&&(a.current=AR({window:i,v5Compat:!0}));let s=a.current,[c,u]=N.useState({action:s.action,location:s.location}),{v7_startTransition:d}=r||{},h=N.useCallback(m=>{d&&e5?e5(()=>u(m)):u(m)},[u,d]);return N.useLayoutEffect(()=>s.listen(h),[s,h]),N.useEffect(()=>j9(r),[r]),N.createElement(S9,{basename:t,children:n,location:c.location,navigationType:c.action,navigator:s,future:r})}const $9=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",I9=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,ke=N.forwardRef(function(t,n){let{onClick:r,relative:i,reloadDocument:a,replace:s,state:c,target:u,to:d,preventScrollReset:h,viewTransition:m}=t,p=E9(t,A9),{basename:v}=N.useContext(Bs),_,x=!1;if(typeof d=="string"&&I9.test(d)&&(_=d,$9))try{let j=new URL(window.location.href),E=d.startsWith("//")?new URL(j.protocol+d):new URL(d),P=C_(E.pathname,v);E.origin===j.origin&&P!=null?d=P+E.search+E.hash:x=!0}catch{}let y=a9(d,{relative:i}),w=R9(d,{replace:s,state:c,target:u,preventScrollReset:h,relative:i,viewTransition:m});function b(j){r&&r(j),j.defaultPrevented||w(j)}return N.createElement("a",Jx({},p,{href:_||y,onClick:x||a?r:b,ref:n,target:u}))});var t5;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmit="useSubmit",e.UseSubmitFetcher="useSubmitFetcher",e.UseFetcher="useFetcher",e.useViewTransitionState="useViewTransitionState"})(t5||(t5={}));var n5;(function(e){e.UseFetcher="useFetcher",e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(n5||(n5={}));function R9(e,t){let{target:n,replace:r,state:i,preventScrollReset:a,relative:s,viewTransition:c}=t===void 0?{}:t,u=qc(),d=zs(),h=q4(e,{relative:s});return N.useCallback(m=>{if(k9(m,n)){m.preventDefault();let p=r!==void 0?r:sp(d)===sp(h);u(e,{replace:p,state:i,preventScrollReset:a,relative:s,viewTransition:c})}},[d,u,h,r,i,n,e,a,s,c])}function Us(e){let t=N.useRef(eb(e)),n=N.useRef(!1),r=zs(),i=N.useMemo(()=>C9(r.search,n.current?null:t.current),[r.search]),a=qc(),s=N.useCallback((c,u)=>{const d=eb(typeof c=="function"?c(i):c);n.current=!0,a("?"+d,u)},[a,i]);return[i,s]}var Zc=class{constructor(){this.listeners=new Set,this.subscribe=this.subscribe.bind(this)}subscribe(e){return this.listeners.add(e),this.onSubscribe(),()=>{this.listeners.delete(e),this.onUnsubscribe()}}hasListeners(){return this.listeners.size>0}onSubscribe(){}onUnsubscribe(){}},cs,Qa,Wl,pk,F9=(pk=class extends Zc{constructor(){super();xe(this,cs);xe(this,Qa);xe(this,Wl);fe(this,Wl,t=>{if(typeof window<"u"&&window.addEventListener){const n=()=>t();return window.addEventListener("visibilitychange",n,!1),()=>{window.removeEventListener("visibilitychange",n)}}})}onSubscribe(){q(this,Qa)||this.setEventListener(q(this,Wl))}onUnsubscribe(){var t;this.hasListeners()||((t=q(this,Qa))==null||t.call(this),fe(this,Qa,void 0))}setEventListener(t){var n;fe(this,Wl,t),(n=q(this,Qa))==null||n.call(this),fe(this,Qa,t(r=>{typeof r=="boolean"?this.setFocused(r):this.onFocus()}))}setFocused(t){q(this,cs)!==t&&(fe(this,cs,t),this.onFocus())}onFocus(){const t=this.isFocused();this.listeners.forEach(n=>{n(t)})}isFocused(){var t;return typeof q(this,cs)=="boolean"?q(this,cs):((t=globalThis.document)==null?void 0:t.visibilityState)!=="hidden"}},cs=new WeakMap,Qa=new WeakMap,Wl=new WeakMap,pk),T_=new F9,D9={setTimeout:(e,t)=>setTimeout(e,t),clearTimeout:e=>clearTimeout(e),setInterval:(e,t)=>setInterval(e,t),clearInterval:e=>clearInterval(e)},Ja,Lw,gk,B9=(gk=class{constructor(){xe(this,Ja,D9);xe(this,Lw,!1)}setTimeoutProvider(e){fe(this,Ja,e)}setTimeout(e,t){return q(this,Ja).setTimeout(e,t)}clearTimeout(e){q(this,Ja).clearTimeout(e)}setInterval(e,t){return q(this,Ja).setInterval(e,t)}clearInterval(e){q(this,Ja).clearInterval(e)}},Ja=new WeakMap,Lw=new WeakMap,gk),rs=new B9;function z9(e){setTimeout(e,0)}var U9=typeof window>"u"||"Deno"in globalThis;function kn(){}function W9(e,t){return typeof e=="function"?e(t):e}function tb(e){return typeof e=="number"&&e>=0&&e!==1/0}function Y4(e,t){return Math.max(e+(t||0)-Date.now(),0)}function _o(e,t){return typeof e=="function"?e(t):e}function cr(e,t){return typeof e=="function"?e(t):e}function r5(e,t){const{type:n="all",exact:r,fetchStatus:i,predicate:a,queryKey:s,stale:c}=e;if(s){if(r){if(t.queryHash!==M_(s,t.options))return!1}else if(!Ud(t.queryKey,s))return!1}if(n!=="all"){const u=t.isActive();if(n==="active"&&!u||n==="inactive"&&u)return!1}return!(typeof c=="boolean"&&t.isStale()!==c||i&&i!==t.state.fetchStatus||a&&!a(t))}function i5(e,t){const{exact:n,status:r,predicate:i,mutationKey:a}=e;if(a){if(!t.options.mutationKey)return!1;if(n){if(Es(t.options.mutationKey)!==Es(a))return!1}else if(!Ud(t.options.mutationKey,a))return!1}return!(r&&t.state.status!==r||i&&!i(t))}function M_(e,t){return((t==null?void 0:t.queryKeyHashFn)||Es)(e)}function Es(e){return JSON.stringify(e,(t,n)=>nb(n)?Object.keys(n).sort().reduce((r,i)=>(r[i]=n[i],r),{}):n)}function Ud(e,t){return e===t?!0:typeof e!=typeof t?!1:e&&t&&typeof e=="object"&&typeof t=="object"?Object.keys(t).every(n=>Ud(e[n],t[n])):!1}var H9=Object.prototype.hasOwnProperty;function X4(e,t,n=0){if(e===t)return e;if(n>500)return t;const r=a5(e)&&a5(t);if(!r&&!(nb(e)&&nb(t)))return t;const a=(r?e:Object.keys(e)).length,s=r?t:Object.keys(t),c=s.length,u=r?new Array(c):{};let d=0;for(let h=0;h{rs.setTimeout(t,e)})}function rb(e,t,n){return typeof n.structuralSharing=="function"?n.structuralSharing(e,t):n.structuralSharing!==!1?X4(e,t):t}function q9(e,t,n=0){const r=[...e,t];return n&&r.length>n?r.slice(1):r}function Z9(e,t,n=0){const r=[t,...e];return n&&r.length>n?r.slice(0,-1):r}var L_=Symbol();function Q4(e,t){return!e.queryFn&&(t!=null&&t.initialPromise)?()=>t.initialPromise:!e.queryFn||e.queryFn===L_?()=>Promise.reject(new Error(`Missing queryFn: '${e.queryHash}'`)):e.queryFn}function $_(e,t){return typeof e=="function"?e(...t):!!e}function G9(e,t,n){let r=!1,i;return Object.defineProperty(e,"signal",{enumerable:!0,get:()=>(i??(i=t()),r||(r=!0,i.aborted?n():i.addEventListener("abort",n,{once:!0})),i)}),e}var Wd=(()=>{let e=()=>U9;return{isServer(){return e()},setIsServer(t){e=t}}})();function ib(){let e,t;const n=new Promise((i,a)=>{e=i,t=a});n.status="pending",n.catch(()=>{});function r(i){Object.assign(n,i),delete n.resolve,delete n.reject}return n.resolve=i=>{r({status:"fulfilled",value:i}),e(i)},n.reject=i=>{r({status:"rejected",reason:i}),t(i)},n}var K9=z9;function Y9(){let e=[],t=0,n=c=>{c()},r=c=>{c()},i=K9;const a=c=>{t?e.push(c):i(()=>{n(c)})},s=()=>{const c=e;e=[],c.length&&i(()=>{r(()=>{c.forEach(u=>{n(u)})})})};return{batch:c=>{let u;t++;try{u=c()}finally{t--,t||s()}return u},batchCalls:c=>(...u)=>{a(()=>{c(...u)})},schedule:a,setNotifyFunction:c=>{n=c},setBatchNotifyFunction:c=>{r=c},setScheduler:c=>{i=c}}}var qt=Y9(),Hl,eo,Vl,vk,X9=(vk=class extends Zc{constructor(){super();xe(this,Hl,!0);xe(this,eo);xe(this,Vl);fe(this,Vl,t=>{if(typeof window<"u"&&window.addEventListener){const n=()=>t(!0),r=()=>t(!1);return window.addEventListener("online",n,!1),window.addEventListener("offline",r,!1),()=>{window.removeEventListener("online",n),window.removeEventListener("offline",r)}}})}onSubscribe(){q(this,eo)||this.setEventListener(q(this,Vl))}onUnsubscribe(){var t;this.hasListeners()||((t=q(this,eo))==null||t.call(this),fe(this,eo,void 0))}setEventListener(t){var n;fe(this,Vl,t),(n=q(this,eo))==null||n.call(this),fe(this,eo,t(this.setOnline.bind(this)))}setOnline(t){q(this,Hl)!==t&&(fe(this,Hl,t),this.listeners.forEach(r=>{r(t)}))}isOnline(){return q(this,Hl)}},Hl=new WeakMap,eo=new WeakMap,Vl=new WeakMap,vk),cp=new X9;function Q9(e){return Math.min(1e3*2**e,3e4)}function J4(e){return(e??"online")==="online"?cp.isOnline():!0}var ab=class extends Error{constructor(e){super("CancelledError"),this.revert=e==null?void 0:e.revert,this.silent=e==null?void 0:e.silent}};function eA(e){let t=!1,n=0,r;const i=ib(),a=()=>i.status!=="pending",s=x=>{var y;if(!a()){const w=new ab(x);p(w),(y=e.onCancel)==null||y.call(e,w)}},c=()=>{t=!0},u=()=>{t=!1},d=()=>T_.isFocused()&&(e.networkMode==="always"||cp.isOnline())&&e.canRun(),h=()=>J4(e.networkMode)&&e.canRun(),m=x=>{a()||(r==null||r(),i.resolve(x))},p=x=>{a()||(r==null||r(),i.reject(x))},v=()=>new Promise(x=>{var y;r=w=>{(a()||d())&&x(w)},(y=e.onPause)==null||y.call(e)}).then(()=>{var x;r=void 0,a()||(x=e.onContinue)==null||x.call(e)}),_=()=>{if(a())return;let x;const y=n===0?e.initialPromise:void 0;try{x=y??e.fn()}catch(w){x=Promise.reject(w)}Promise.resolve(x).then(m).catch(w=>{var O;if(a())return;const b=e.retry??(Wd.isServer()?0:3),j=e.retryDelay??Q9,E=typeof j=="function"?j(n,w):j,P=b===!0||typeof b=="number"&&nd()?void 0:v()).then(()=>{t?p(w):_()})})};return{promise:i,status:()=>i.status,cancel:s,continue:()=>(r==null||r(),i),cancelRetry:c,continueRetry:u,canStart:h,start:()=>(h()?_():v().then(_),i)}}var us,yk,tA=(yk=class{constructor(){xe(this,us)}destroy(){this.clearGcTimeout()}scheduleGc(){this.clearGcTimeout(),tb(this.gcTime)&&fe(this,us,rs.setTimeout(()=>{this.optionalRemove()},this.gcTime))}updateGcTime(e){this.gcTime=Math.max(this.gcTime||0,e??(Wd.isServer()?1/0:5*60*1e3))}clearGcTimeout(){q(this,us)!==void 0&&(rs.clearTimeout(q(this,us)),fe(this,us,void 0))}},us=new WeakMap,yk);function J9(e){return{onFetch:(t,n)=>{var h,m,p,v,_;const r=t.options,i=(p=(m=(h=t.fetchOptions)==null?void 0:h.meta)==null?void 0:m.fetchMore)==null?void 0:p.direction,a=((v=t.state.data)==null?void 0:v.pages)||[],s=((_=t.state.data)==null?void 0:_.pageParams)||[];let c={pages:[],pageParams:[]},u=0;const d=async()=>{let x=!1;const y=j=>{G9(j,()=>t.signal,()=>x=!0)},w=Q4(t.options,t.fetchOptions),b=async(j,E,P)=>{if(x)return Promise.reject(t.signal.reason);if(E==null&&j.pages.length)return Promise.resolve(j);const C=(()=>{const z={client:t.client,queryKey:t.queryKey,pageParam:E,direction:P?"backward":"forward",meta:t.options.meta};return y(z),z})(),A=await w(C),{maxPages:T}=t.options,$=P?Z9:q9;return{pages:$(j.pages,A,T),pageParams:$(j.pageParams,E,T)}};if(i&&a.length){const j=i==="backward",E=j?eF:s5,P={pages:a,pageParams:s},O=E(r,P);c=await b(P,O,j)}else{const j=e??a.length;do{const E=u===0?s[0]??r.initialPageParam:s5(r,c);if(u>0&&E==null)break;c=await b(c,E),u++}while(u{var x,y;return(y=(x=t.options).persister)==null?void 0:y.call(x,d,{client:t.client,queryKey:t.queryKey,meta:t.options.meta,signal:t.signal},n)}:t.fetchFn=d}}}function s5(e,{pages:t,pageParams:n}){const r=t.length-1;return t.length>0?e.getNextPageParam(t[r],t,n[r],n):void 0}function eF(e,{pages:t,pageParams:n}){var r;return t.length>0?(r=e.getPreviousPageParam)==null?void 0:r.call(e,t[0],t,n[0],n):void 0}var ql,ds,Zl,Er,fs,Xt,Mf,hs,lr,nA,Ji,xk,tF=(xk=class extends tA{constructor(t){super();xe(this,lr);xe(this,ql);xe(this,ds);xe(this,Zl);xe(this,Er);xe(this,fs);xe(this,Xt);xe(this,Mf);xe(this,hs);fe(this,hs,!1),fe(this,Mf,t.defaultOptions),this.setOptions(t.options),this.observers=[],fe(this,fs,t.client),fe(this,Er,q(this,fs).getQueryCache()),this.queryKey=t.queryKey,this.queryHash=t.queryHash,fe(this,ds,c5(this.options)),this.state=t.state??q(this,ds),this.scheduleGc()}get meta(){return this.options.meta}get queryType(){return q(this,ql)}get promise(){var t;return(t=q(this,Xt))==null?void 0:t.promise}setOptions(t){if(this.options={...q(this,Mf),...t},t!=null&&t._type&&fe(this,ql,t._type),this.updateGcTime(this.options.gcTime),this.state&&this.state.data===void 0){const n=c5(this.options);n.data!==void 0&&(this.setState(l5(n.data,n.dataUpdatedAt)),fe(this,ds,n))}}optionalRemove(){!this.observers.length&&this.state.fetchStatus==="idle"&&q(this,Er).remove(this)}setData(t,n){const r=rb(this.state.data,t,this.options);return Me(this,lr,Ji).call(this,{data:r,type:"success",dataUpdatedAt:n==null?void 0:n.updatedAt,manual:n==null?void 0:n.manual}),r}setState(t){Me(this,lr,Ji).call(this,{type:"setState",state:t})}cancel(t){var r,i;const n=(r=q(this,Xt))==null?void 0:r.promise;return(i=q(this,Xt))==null||i.cancel(t),n?n.then(kn).catch(kn):Promise.resolve()}destroy(){super.destroy(),this.cancel({silent:!0})}get resetState(){return q(this,ds)}reset(){this.destroy(),this.setState(this.resetState)}isActive(){return this.observers.some(t=>cr(t.options.enabled,this)!==!1)}isDisabled(){return this.getObserversCount()>0?!this.isActive():this.options.queryFn===L_||!this.isFetched()}isFetched(){return this.state.dataUpdateCount+this.state.errorUpdateCount>0}isStatic(){return this.getObserversCount()>0?this.observers.some(t=>_o(t.options.staleTime,this)==="static"):!1}isStale(){return this.getObserversCount()>0?this.observers.some(t=>t.getCurrentResult().isStale):this.state.data===void 0||this.state.isInvalidated}isStaleByTime(t=0){return this.state.data===void 0?!0:t==="static"?!1:this.state.isInvalidated?!0:!Y4(this.state.dataUpdatedAt,t)}onFocus(){var n;const t=this.observers.find(r=>r.shouldFetchOnWindowFocus());t==null||t.refetch({cancelRefetch:!1}),(n=q(this,Xt))==null||n.continue()}onOnline(){var n;const t=this.observers.find(r=>r.shouldFetchOnReconnect());t==null||t.refetch({cancelRefetch:!1}),(n=q(this,Xt))==null||n.continue()}addObserver(t){this.observers.includes(t)||(this.observers.push(t),this.clearGcTimeout(),q(this,Er).notify({type:"observerAdded",query:this,observer:t}))}removeObserver(t){this.observers.includes(t)&&(this.observers=this.observers.filter(n=>n!==t),this.observers.length||(q(this,Xt)&&(q(this,hs)||Me(this,lr,nA).call(this)?q(this,Xt).cancel({revert:!0}):q(this,Xt).cancelRetry()),this.scheduleGc()),q(this,Er).notify({type:"observerRemoved",query:this,observer:t}))}getObserversCount(){return this.observers.length}invalidate(){this.state.isInvalidated||Me(this,lr,Ji).call(this,{type:"invalidate"})}async fetch(t,n){var d,h,m,p,v,_,x,y,w,b,j;if(this.state.fetchStatus!=="idle"&&((d=q(this,Xt))==null?void 0:d.status())!=="rejected"){if(this.state.data!==void 0&&(n!=null&&n.cancelRefetch))this.cancel({silent:!0});else if(q(this,Xt))return q(this,Xt).continueRetry(),q(this,Xt).promise}if(t&&this.setOptions(t),!this.options.queryFn){const E=this.observers.find(P=>P.options.queryFn);E&&this.setOptions(E.options)}const r=new AbortController,i=E=>{Object.defineProperty(E,"signal",{enumerable:!0,get:()=>(fe(this,hs,!0),r.signal)})},a=()=>{const E=Q4(this.options,n),O=(()=>{const C={client:q(this,fs),queryKey:this.queryKey,meta:this.meta};return i(C),C})();return fe(this,hs,!1),this.options.persister?this.options.persister(E,O,this):E(O)},c=(()=>{const E={fetchOptions:n,options:this.options,queryKey:this.queryKey,client:q(this,fs),state:this.state,fetchFn:a};return i(E),E})(),u=q(this,ql)==="infinite"?J9(this.options.pages):this.options.behavior;u==null||u.onFetch(c,this),fe(this,Zl,this.state),(this.state.fetchStatus==="idle"||this.state.fetchMeta!==((h=c.fetchOptions)==null?void 0:h.meta))&&Me(this,lr,Ji).call(this,{type:"fetch",meta:(m=c.fetchOptions)==null?void 0:m.meta}),fe(this,Xt,eA({initialPromise:n==null?void 0:n.initialPromise,fn:c.fetchFn,onCancel:E=>{E instanceof ab&&E.revert&&this.setState({...q(this,Zl),fetchStatus:"idle"}),r.abort()},onFail:(E,P)=>{Me(this,lr,Ji).call(this,{type:"failed",failureCount:E,error:P})},onPause:()=>{Me(this,lr,Ji).call(this,{type:"pause"})},onContinue:()=>{Me(this,lr,Ji).call(this,{type:"continue"})},retry:c.options.retry,retryDelay:c.options.retryDelay,networkMode:c.options.networkMode,canRun:()=>!0}));try{const E=await q(this,Xt).start();if(E===void 0)throw new Error(`${this.queryHash} data is undefined`);return this.setData(E),(v=(p=q(this,Er).config).onSuccess)==null||v.call(p,E,this),(x=(_=q(this,Er).config).onSettled)==null||x.call(_,E,this.state.error,this),E}catch(E){if(E instanceof ab){if(E.silent)return q(this,Xt).promise;if(E.revert){if(this.state.data===void 0)throw E;return this.state.data}}throw Me(this,lr,Ji).call(this,{type:"error",error:E}),(w=(y=q(this,Er).config).onError)==null||w.call(y,E,this),(j=(b=q(this,Er).config).onSettled)==null||j.call(b,this.state.data,E,this),E}finally{this.scheduleGc()}}},ql=new WeakMap,ds=new WeakMap,Zl=new WeakMap,Er=new WeakMap,fs=new WeakMap,Xt=new WeakMap,Mf=new WeakMap,hs=new WeakMap,lr=new WeakSet,nA=function(){return this.state.fetchStatus==="paused"&&this.state.status==="pending"},Ji=function(t){const n=r=>{switch(t.type){case"failed":return{...r,fetchFailureCount:t.failureCount,fetchFailureReason:t.error};case"pause":return{...r,fetchStatus:"paused"};case"continue":return{...r,fetchStatus:"fetching"};case"fetch":return{...r,...rA(r.data,this.options),fetchMeta:t.meta??null};case"success":const i={...r,...l5(t.data,t.dataUpdatedAt),dataUpdateCount:r.dataUpdateCount+1,...!t.manual&&{fetchStatus:"idle",fetchFailureCount:0,fetchFailureReason:null}};return fe(this,Zl,t.manual?i:void 0),i;case"error":const a=t.error;return{...r,error:a,errorUpdateCount:r.errorUpdateCount+1,errorUpdatedAt:Date.now(),fetchFailureCount:r.fetchFailureCount+1,fetchFailureReason:a,fetchStatus:"idle",status:"error",isInvalidated:!0};case"invalidate":return{...r,isInvalidated:!0};case"setState":return{...r,...t.state}}};this.state=n(this.state),qt.batch(()=>{this.observers.forEach(r=>{r.onQueryUpdate()}),q(this,Er).notify({query:this,type:"updated",action:t})})},xk);function rA(e,t){return{fetchFailureCount:0,fetchFailureReason:null,fetchStatus:J4(t.networkMode)?"fetching":"paused",...e===void 0&&{error:null,status:"pending"}}}function l5(e,t){return{data:e,dataUpdatedAt:t??Date.now(),error:null,isInvalidated:!1,status:"success"}}function c5(e){const t=typeof e.initialData=="function"?e.initialData():e.initialData,n=t!==void 0,r=n?typeof e.initialDataUpdatedAt=="function"?e.initialDataUpdatedAt():e.initialDataUpdatedAt:0;return{data:t,dataUpdateCount:0,dataUpdatedAt:n?r??Date.now():0,error:null,errorUpdateCount:0,errorUpdatedAt:0,fetchFailureCount:0,fetchFailureReason:null,fetchMeta:null,isInvalidated:!1,status:n?"success":"pending",fetchStatus:"idle"}}var Vn,ze,Lf,En,ms,Gl,ia,to,$f,Kl,Yl,ps,gs,no,Xl,Ge,qu,ob,sb,lb,cb,ub,db,fb,iA,bk,nF=(bk=class extends Zc{constructor(t,n){super();xe(this,Ge);xe(this,Vn);xe(this,ze);xe(this,Lf);xe(this,En);xe(this,ms);xe(this,Gl);xe(this,ia);xe(this,to);xe(this,$f);xe(this,Kl);xe(this,Yl);xe(this,ps);xe(this,gs);xe(this,no);xe(this,Xl,new Set);this.options=n,fe(this,Vn,t),fe(this,to,null),fe(this,ia,ib()),this.bindMethods(),this.setOptions(n)}bindMethods(){this.refetch=this.refetch.bind(this)}onSubscribe(){this.listeners.size===1&&(q(this,ze).addObserver(this),u5(q(this,ze),this.options)?Me(this,Ge,qu).call(this):this.updateResult(),Me(this,Ge,cb).call(this))}onUnsubscribe(){this.hasListeners()||this.destroy()}shouldFetchOnReconnect(){return hb(q(this,ze),this.options,this.options.refetchOnReconnect)}shouldFetchOnWindowFocus(){return hb(q(this,ze),this.options,this.options.refetchOnWindowFocus)}destroy(){this.listeners=new Set,Me(this,Ge,ub).call(this),Me(this,Ge,db).call(this),q(this,ze).removeObserver(this)}setOptions(t){const n=this.options,r=q(this,ze);if(this.options=q(this,Vn).defaultQueryOptions(t),this.options.enabled!==void 0&&typeof this.options.enabled!="boolean"&&typeof this.options.enabled!="function"&&typeof cr(this.options.enabled,q(this,ze))!="boolean")throw new Error("Expected enabled to be a boolean or a callback that returns a boolean");Me(this,Ge,fb).call(this),q(this,ze).setOptions(this.options),n._defaulted&&!lp(this.options,n)&&q(this,Vn).getQueryCache().notify({type:"observerOptionsUpdated",query:q(this,ze),observer:this});const i=this.hasListeners();i&&d5(q(this,ze),r,this.options,n)&&Me(this,Ge,qu).call(this),this.updateResult(),i&&(q(this,ze)!==r||cr(this.options.enabled,q(this,ze))!==cr(n.enabled,q(this,ze))||_o(this.options.staleTime,q(this,ze))!==_o(n.staleTime,q(this,ze)))&&Me(this,Ge,ob).call(this);const a=Me(this,Ge,sb).call(this);i&&(q(this,ze)!==r||cr(this.options.enabled,q(this,ze))!==cr(n.enabled,q(this,ze))||a!==q(this,no))&&Me(this,Ge,lb).call(this,a)}getOptimisticResult(t){const n=q(this,Vn).getQueryCache().build(q(this,Vn),t),r=this.createResult(n,t);return iF(this,r)&&(fe(this,En,r),fe(this,Gl,this.options),fe(this,ms,q(this,ze).state)),r}getCurrentResult(){return q(this,En)}trackResult(t,n){return new Proxy(t,{get:(r,i)=>(this.trackProp(i),n==null||n(i),i==="promise"&&(this.trackProp("data"),!this.options.experimental_prefetchInRender&&q(this,ia).status==="pending"&&q(this,ia).reject(new Error("experimental_prefetchInRender feature flag is not enabled"))),Reflect.get(r,i))})}trackProp(t){q(this,Xl).add(t)}getCurrentQuery(){return q(this,ze)}refetch({...t}={}){return this.fetch({...t})}fetchOptimistic(t){const n=q(this,Vn).defaultQueryOptions(t),r=q(this,Vn).getQueryCache().build(q(this,Vn),n);return r.fetch().then(()=>this.createResult(r,n))}fetch(t){return Me(this,Ge,qu).call(this,{...t,cancelRefetch:t.cancelRefetch??!0}).then(()=>(this.updateResult(),q(this,En)))}createResult(t,n){var T;const r=q(this,ze),i=this.options,a=q(this,En),s=q(this,ms),c=q(this,Gl),d=t!==r?t.state:q(this,Lf),{state:h}=t;let m={...h},p=!1,v;if(n._optimisticResults){const $=this.hasListeners(),z=!$&&u5(t,n),D=$&&d5(t,r,n,i);(z||D)&&(m={...m,...rA(h.data,t.options)}),n._optimisticResults==="isRestoring"&&(m.fetchStatus="idle")}let{error:_,errorUpdatedAt:x,status:y}=m;v=m.data;let w=!1;if(n.placeholderData!==void 0&&v===void 0&&y==="pending"){let $;a!=null&&a.isPlaceholderData&&n.placeholderData===(c==null?void 0:c.placeholderData)?($=a.data,w=!0):$=typeof n.placeholderData=="function"?n.placeholderData((T=q(this,Yl))==null?void 0:T.state.data,q(this,Yl)):n.placeholderData,$!==void 0&&(y="success",v=rb(a==null?void 0:a.data,$,n),p=!0)}if(n.select&&v!==void 0&&!w)if(a&&v===(s==null?void 0:s.data)&&n.select===q(this,$f))v=q(this,Kl);else try{fe(this,$f,n.select),v=n.select(v),v=rb(a==null?void 0:a.data,v,n),fe(this,Kl,v),fe(this,to,null)}catch($){fe(this,to,$)}q(this,to)&&(_=q(this,to),v=q(this,Kl),x=Date.now(),y="error");const b=m.fetchStatus==="fetching",j=y==="pending",E=y==="error",P=j&&b,O=v!==void 0,A={status:y,fetchStatus:m.fetchStatus,isPending:j,isSuccess:y==="success",isError:E,isInitialLoading:P,isLoading:P,data:v,dataUpdatedAt:m.dataUpdatedAt,error:_,errorUpdatedAt:x,failureCount:m.fetchFailureCount,failureReason:m.fetchFailureReason,errorUpdateCount:m.errorUpdateCount,isFetched:t.isFetched(),isFetchedAfterMount:m.dataUpdateCount>d.dataUpdateCount||m.errorUpdateCount>d.errorUpdateCount,isFetching:b,isRefetching:b&&!j,isLoadingError:E&&!O,isPaused:m.fetchStatus==="paused",isPlaceholderData:p,isRefetchError:E&&O,isStale:I_(t,n),refetch:this.refetch,promise:q(this,ia),isEnabled:cr(n.enabled,t)!==!1};if(this.options.experimental_prefetchInRender){const $=A.data!==void 0,z=A.status==="error"&&!$,D=F=>{z?F.reject(A.error):$&&F.resolve(A.data)},Z=()=>{const F=fe(this,ia,A.promise=ib());D(F)},I=q(this,ia);switch(I.status){case"pending":t.queryHash===r.queryHash&&D(I);break;case"fulfilled":(z||A.data!==I.value)&&Z();break;case"rejected":(!z||A.error!==I.reason)&&Z();break}}return A}updateResult(){const t=q(this,En),n=this.createResult(q(this,ze),this.options);if(fe(this,ms,q(this,ze).state),fe(this,Gl,this.options),q(this,ms).data!==void 0&&fe(this,Yl,q(this,ze)),lp(n,t))return;fe(this,En,n);const r=()=>{if(!t)return!0;const{notifyOnChangeProps:i}=this.options,a=typeof i=="function"?i():i;if(a==="all"||!a&&!q(this,Xl).size)return!0;const s=new Set(a??q(this,Xl));return this.options.throwOnError&&s.add("error"),Object.keys(q(this,En)).some(c=>{const u=c;return q(this,En)[u]!==t[u]&&s.has(u)})};Me(this,Ge,iA).call(this,{listeners:r()})}onQueryUpdate(){this.updateResult(),this.hasListeners()&&Me(this,Ge,cb).call(this)}},Vn=new WeakMap,ze=new WeakMap,Lf=new WeakMap,En=new WeakMap,ms=new WeakMap,Gl=new WeakMap,ia=new WeakMap,to=new WeakMap,$f=new WeakMap,Kl=new WeakMap,Yl=new WeakMap,ps=new WeakMap,gs=new WeakMap,no=new WeakMap,Xl=new WeakMap,Ge=new WeakSet,qu=function(t){Me(this,Ge,fb).call(this);let n=q(this,ze).fetch(this.options,t);return t!=null&&t.throwOnError||(n=n.catch(kn)),n},ob=function(){Me(this,Ge,ub).call(this);const t=_o(this.options.staleTime,q(this,ze));if(Wd.isServer()||q(this,En).isStale||!tb(t))return;const r=Y4(q(this,En).dataUpdatedAt,t)+1;fe(this,ps,rs.setTimeout(()=>{q(this,En).isStale||this.updateResult()},r))},sb=function(){return(typeof this.options.refetchInterval=="function"?this.options.refetchInterval(q(this,ze)):this.options.refetchInterval)??!1},lb=function(t){Me(this,Ge,db).call(this),fe(this,no,t),!(Wd.isServer()||cr(this.options.enabled,q(this,ze))===!1||!tb(q(this,no))||q(this,no)===0)&&fe(this,gs,rs.setInterval(()=>{(this.options.refetchIntervalInBackground||T_.isFocused())&&Me(this,Ge,qu).call(this)},q(this,no)))},cb=function(){Me(this,Ge,ob).call(this),Me(this,Ge,lb).call(this,Me(this,Ge,sb).call(this))},ub=function(){q(this,ps)!==void 0&&(rs.clearTimeout(q(this,ps)),fe(this,ps,void 0))},db=function(){q(this,gs)!==void 0&&(rs.clearInterval(q(this,gs)),fe(this,gs,void 0))},fb=function(){const t=q(this,Vn).getQueryCache().build(q(this,Vn),this.options);if(t===q(this,ze))return;const n=q(this,ze);fe(this,ze,t),fe(this,Lf,t.state),this.hasListeners()&&(n==null||n.removeObserver(this),t.addObserver(this))},iA=function(t){qt.batch(()=>{t.listeners&&this.listeners.forEach(n=>{n(q(this,En))}),q(this,Vn).getQueryCache().notify({query:q(this,ze),type:"observerResultsUpdated"})})},bk);function rF(e,t){return cr(t.enabled,e)!==!1&&e.state.data===void 0&&!(e.state.status==="error"&&cr(t.retryOnMount,e)===!1)}function u5(e,t){return rF(e,t)||e.state.data!==void 0&&hb(e,t,t.refetchOnMount)}function hb(e,t,n){if(cr(t.enabled,e)!==!1&&_o(t.staleTime,e)!=="static"){const r=typeof n=="function"?n(e):n;return r==="always"||r!==!1&&I_(e,t)}return!1}function d5(e,t,n,r){return(e!==t||cr(r.enabled,e)===!1)&&(!n.suspense||e.state.status!=="error")&&I_(e,n)}function I_(e,t){return cr(t.enabled,e)!==!1&&e.isStaleByTime(_o(t.staleTime,e))}function iF(e,t){return!lp(e.getCurrentResult(),t)}var If,Pi,yn,vs,Ei,Wa,wk,aF=(wk=class extends tA{constructor(t){super();xe(this,Ei);xe(this,If);xe(this,Pi);xe(this,yn);xe(this,vs);fe(this,If,t.client),this.mutationId=t.mutationId,fe(this,yn,t.mutationCache),fe(this,Pi,[]),this.state=t.state||aA(),this.setOptions(t.options),this.scheduleGc()}setOptions(t){this.options=t,this.updateGcTime(this.options.gcTime)}get meta(){return this.options.meta}addObserver(t){q(this,Pi).includes(t)||(q(this,Pi).push(t),this.clearGcTimeout(),q(this,yn).notify({type:"observerAdded",mutation:this,observer:t}))}removeObserver(t){fe(this,Pi,q(this,Pi).filter(n=>n!==t)),this.scheduleGc(),q(this,yn).notify({type:"observerRemoved",mutation:this,observer:t})}optionalRemove(){q(this,Pi).length||(this.state.status==="pending"?this.scheduleGc():q(this,yn).remove(this))}continue(){var t;return((t=q(this,vs))==null?void 0:t.continue())??this.execute(this.state.variables)}async execute(t){var s,c,u,d,h,m,p,v,_,x,y,w,b,j,E,P,O,C;const n=()=>{Me(this,Ei,Wa).call(this,{type:"continue"})},r={client:q(this,If),meta:this.options.meta,mutationKey:this.options.mutationKey};fe(this,vs,eA({fn:()=>this.options.mutationFn?this.options.mutationFn(t,r):Promise.reject(new Error("No mutationFn found")),onFail:(A,T)=>{Me(this,Ei,Wa).call(this,{type:"failed",failureCount:A,error:T})},onPause:()=>{Me(this,Ei,Wa).call(this,{type:"pause"})},onContinue:n,retry:this.options.retry??0,retryDelay:this.options.retryDelay,networkMode:this.options.networkMode,canRun:()=>q(this,yn).canRun(this)}));const i=this.state.status==="pending",a=!q(this,vs).canStart();try{if(i)n();else{Me(this,Ei,Wa).call(this,{type:"pending",variables:t,isPaused:a}),q(this,yn).config.onMutate&&await q(this,yn).config.onMutate(t,this,r);const T=await((c=(s=this.options).onMutate)==null?void 0:c.call(s,t,r));T!==this.state.context&&Me(this,Ei,Wa).call(this,{type:"pending",context:T,variables:t,isPaused:a})}const A=await q(this,vs).start();return await((d=(u=q(this,yn).config).onSuccess)==null?void 0:d.call(u,A,t,this.state.context,this,r)),await((m=(h=this.options).onSuccess)==null?void 0:m.call(h,A,t,this.state.context,r)),await((v=(p=q(this,yn).config).onSettled)==null?void 0:v.call(p,A,null,this.state.variables,this.state.context,this,r)),await((x=(_=this.options).onSettled)==null?void 0:x.call(_,A,null,t,this.state.context,r)),Me(this,Ei,Wa).call(this,{type:"success",data:A}),A}catch(A){try{await((w=(y=q(this,yn).config).onError)==null?void 0:w.call(y,A,t,this.state.context,this,r))}catch(T){Promise.reject(T)}try{await((j=(b=this.options).onError)==null?void 0:j.call(b,A,t,this.state.context,r))}catch(T){Promise.reject(T)}try{await((P=(E=q(this,yn).config).onSettled)==null?void 0:P.call(E,void 0,A,this.state.variables,this.state.context,this,r))}catch(T){Promise.reject(T)}try{await((C=(O=this.options).onSettled)==null?void 0:C.call(O,void 0,A,t,this.state.context,r))}catch(T){Promise.reject(T)}throw Me(this,Ei,Wa).call(this,{type:"error",error:A}),A}finally{q(this,yn).runNext(this)}}},If=new WeakMap,Pi=new WeakMap,yn=new WeakMap,vs=new WeakMap,Ei=new WeakSet,Wa=function(t){const n=r=>{switch(t.type){case"failed":return{...r,failureCount:t.failureCount,failureReason:t.error};case"pause":return{...r,isPaused:!0};case"continue":return{...r,isPaused:!1};case"pending":return{...r,context:t.context,data:void 0,failureCount:0,failureReason:null,error:null,isPaused:t.isPaused,status:"pending",variables:t.variables,submittedAt:Date.now()};case"success":return{...r,data:t.data,failureCount:0,failureReason:null,error:null,status:"success",isPaused:!1};case"error":return{...r,data:void 0,error:t.error,failureCount:r.failureCount+1,failureReason:t.error,isPaused:!1,status:"error"}}};this.state=n(this.state),qt.batch(()=>{q(this,Pi).forEach(r=>{r.onMutationUpdate(t)}),q(this,yn).notify({mutation:this,type:"updated",action:t})})},wk);function aA(){return{context:void 0,data:void 0,error:null,failureCount:0,failureReason:null,isPaused:!1,status:"idle",variables:void 0,submittedAt:0}}var aa,ei,Rf,_k,oF=(_k=class extends Zc{constructor(t={}){super();xe(this,aa);xe(this,ei);xe(this,Rf);this.config=t,fe(this,aa,new Set),fe(this,ei,new Map),fe(this,Rf,0)}build(t,n,r){const i=new aF({client:t,mutationCache:this,mutationId:++Ph(this,Rf)._,options:t.defaultMutationOptions(n),state:r});return this.add(i),i}add(t){q(this,aa).add(t);const n=Hh(t);if(typeof n=="string"){const r=q(this,ei).get(n);r?r.push(t):q(this,ei).set(n,[t])}this.notify({type:"added",mutation:t})}remove(t){if(q(this,aa).delete(t)){const n=Hh(t);if(typeof n=="string"){const r=q(this,ei).get(n);if(r)if(r.length>1){const i=r.indexOf(t);i!==-1&&r.splice(i,1)}else r[0]===t&&q(this,ei).delete(n)}}this.notify({type:"removed",mutation:t})}canRun(t){const n=Hh(t);if(typeof n=="string"){const r=q(this,ei).get(n),i=r==null?void 0:r.find(a=>a.state.status==="pending");return!i||i===t}else return!0}runNext(t){var r;const n=Hh(t);if(typeof n=="string"){const i=(r=q(this,ei).get(n))==null?void 0:r.find(a=>a!==t&&a.state.isPaused);return(i==null?void 0:i.continue())??Promise.resolve()}else return Promise.resolve()}clear(){qt.batch(()=>{q(this,aa).forEach(t=>{this.notify({type:"removed",mutation:t})}),q(this,aa).clear(),q(this,ei).clear()})}getAll(){return Array.from(q(this,aa))}find(t){const n={exact:!0,...t};return this.getAll().find(r=>i5(n,r))}findAll(t={}){return this.getAll().filter(n=>i5(t,n))}notify(t){qt.batch(()=>{this.listeners.forEach(n=>{n(t)})})}resumePausedMutations(){const t=this.getAll().filter(n=>n.state.isPaused);return qt.batch(()=>Promise.all(t.map(n=>n.continue().catch(kn))))}},aa=new WeakMap,ei=new WeakMap,Rf=new WeakMap,_k);function Hh(e){var t;return(t=e.options.scope)==null?void 0:t.id}var oa,ro,qn,sa,wa,Em,mb,jk,sF=(jk=class extends Zc{constructor(n,r){super();xe(this,wa);xe(this,oa);xe(this,ro);xe(this,qn);xe(this,sa);fe(this,oa,n),this.setOptions(r),this.bindMethods(),Me(this,wa,Em).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(n){var i;const r=this.options;this.options=q(this,oa).defaultMutationOptions(n),lp(this.options,r)||q(this,oa).getMutationCache().notify({type:"observerOptionsUpdated",mutation:q(this,qn),observer:this}),r!=null&&r.mutationKey&&this.options.mutationKey&&Es(r.mutationKey)!==Es(this.options.mutationKey)?this.reset():((i=q(this,qn))==null?void 0:i.state.status)==="pending"&&q(this,qn).setOptions(this.options)}onUnsubscribe(){var n;this.hasListeners()||(n=q(this,qn))==null||n.removeObserver(this)}onMutationUpdate(n){Me(this,wa,Em).call(this),Me(this,wa,mb).call(this,n)}getCurrentResult(){return q(this,ro)}reset(){var n;(n=q(this,qn))==null||n.removeObserver(this),fe(this,qn,void 0),Me(this,wa,Em).call(this),Me(this,wa,mb).call(this)}mutate(n,r){var i;return fe(this,sa,r),(i=q(this,qn))==null||i.removeObserver(this),fe(this,qn,q(this,oa).getMutationCache().build(q(this,oa),this.options)),q(this,qn).addObserver(this),q(this,qn).execute(n)}},oa=new WeakMap,ro=new WeakMap,qn=new WeakMap,sa=new WeakMap,wa=new WeakSet,Em=function(){var r;const n=((r=q(this,qn))==null?void 0:r.state)??aA();fe(this,ro,{...n,isPending:n.status==="pending",isSuccess:n.status==="success",isError:n.status==="error",isIdle:n.status==="idle",mutate:this.mutate,reset:this.reset})},mb=function(n){qt.batch(()=>{var r,i,a,s,c,u,d,h;if(q(this,sa)&&this.hasListeners()){const m=q(this,ro).variables,p=q(this,ro).context,v={client:q(this,oa),meta:this.options.meta,mutationKey:this.options.mutationKey};if((n==null?void 0:n.type)==="success"){try{(i=(r=q(this,sa)).onSuccess)==null||i.call(r,n.data,m,p,v)}catch(_){Promise.reject(_)}try{(s=(a=q(this,sa)).onSettled)==null||s.call(a,n.data,null,m,p,v)}catch(_){Promise.reject(_)}}else if((n==null?void 0:n.type)==="error"){try{(u=(c=q(this,sa)).onError)==null||u.call(c,n.error,m,p,v)}catch(_){Promise.reject(_)}try{(h=(d=q(this,sa)).onSettled)==null||h.call(d,void 0,n.error,m,p,v)}catch(_){Promise.reject(_)}}}this.listeners.forEach(m=>{m(q(this,ro))})})},jk),Oi,Nk,lF=(Nk=class extends Zc{constructor(t={}){super();xe(this,Oi);this.config=t,fe(this,Oi,new Map)}build(t,n,r){const i=n.queryKey,a=n.queryHash??M_(i,n);let s=this.get(a);return s||(s=new tF({client:t,queryKey:i,queryHash:a,options:t.defaultQueryOptions(n),state:r,defaultOptions:t.getQueryDefaults(i)}),this.add(s)),s}add(t){q(this,Oi).has(t.queryHash)||(q(this,Oi).set(t.queryHash,t),this.notify({type:"added",query:t}))}remove(t){const n=q(this,Oi).get(t.queryHash);n&&(t.destroy(),n===t&&q(this,Oi).delete(t.queryHash),this.notify({type:"removed",query:t}))}clear(){qt.batch(()=>{this.getAll().forEach(t=>{this.remove(t)})})}get(t){return q(this,Oi).get(t)}getAll(){return[...q(this,Oi).values()]}find(t){const n={exact:!0,...t};return this.getAll().find(r=>r5(n,r))}findAll(t={}){const n=this.getAll();return Object.keys(t).length>0?n.filter(r=>r5(t,r)):n}notify(t){qt.batch(()=>{this.listeners.forEach(n=>{n(t)})})}onFocus(){qt.batch(()=>{this.getAll().forEach(t=>{t.onFocus()})})}onOnline(){qt.batch(()=>{this.getAll().forEach(t=>{t.onOnline()})})}},Oi=new WeakMap,Nk),Et,io,ao,Ql,Jl,oo,ec,tc,Sk,cF=(Sk=class{constructor(e={}){xe(this,Et);xe(this,io);xe(this,ao);xe(this,Ql);xe(this,Jl);xe(this,oo);xe(this,ec);xe(this,tc);fe(this,Et,e.queryCache||new lF),fe(this,io,e.mutationCache||new oF),fe(this,ao,e.defaultOptions||{}),fe(this,Ql,new Map),fe(this,Jl,new Map),fe(this,oo,0)}mount(){Ph(this,oo)._++,q(this,oo)===1&&(fe(this,ec,T_.subscribe(async e=>{e&&(await this.resumePausedMutations(),q(this,Et).onFocus())})),fe(this,tc,cp.subscribe(async e=>{e&&(await this.resumePausedMutations(),q(this,Et).onOnline())})))}unmount(){var e,t;Ph(this,oo)._--,q(this,oo)===0&&((e=q(this,ec))==null||e.call(this),fe(this,ec,void 0),(t=q(this,tc))==null||t.call(this),fe(this,tc,void 0))}isFetching(e){return q(this,Et).findAll({...e,fetchStatus:"fetching"}).length}isMutating(e){return q(this,io).findAll({...e,status:"pending"}).length}getQueryData(e){var n;const t=this.defaultQueryOptions({queryKey:e});return(n=q(this,Et).get(t.queryHash))==null?void 0:n.state.data}ensureQueryData(e){const t=this.defaultQueryOptions(e),n=q(this,Et).build(this,t),r=n.state.data;return r===void 0?this.fetchQuery(e):(e.revalidateIfStale&&n.isStaleByTime(_o(t.staleTime,n))&&this.prefetchQuery(t),Promise.resolve(r))}getQueriesData(e){return q(this,Et).findAll(e).map(({queryKey:t,state:n})=>{const r=n.data;return[t,r]})}setQueryData(e,t,n){const r=this.defaultQueryOptions({queryKey:e}),i=q(this,Et).get(r.queryHash),a=i==null?void 0:i.state.data,s=W9(t,a);if(s!==void 0)return q(this,Et).build(this,r).setData(s,{...n,manual:!0})}setQueriesData(e,t,n){return qt.batch(()=>q(this,Et).findAll(e).map(({queryKey:r})=>[r,this.setQueryData(r,t,n)]))}getQueryState(e){var n;const t=this.defaultQueryOptions({queryKey:e});return(n=q(this,Et).get(t.queryHash))==null?void 0:n.state}removeQueries(e){const t=q(this,Et);qt.batch(()=>{t.findAll(e).forEach(n=>{t.remove(n)})})}resetQueries(e,t){const n=q(this,Et);return qt.batch(()=>(n.findAll(e).forEach(r=>{r.reset()}),this.refetchQueries({type:"active",...e},t)))}cancelQueries(e,t={}){const n={revert:!0,...t},r=qt.batch(()=>q(this,Et).findAll(e).map(i=>i.cancel(n)));return Promise.all(r).then(kn).catch(kn)}invalidateQueries(e,t={}){return qt.batch(()=>(q(this,Et).findAll(e).forEach(n=>{n.invalidate()}),(e==null?void 0:e.refetchType)==="none"?Promise.resolve():this.refetchQueries({...e,type:(e==null?void 0:e.refetchType)??(e==null?void 0:e.type)??"active"},t)))}refetchQueries(e,t={}){const n={...t,cancelRefetch:t.cancelRefetch??!0},r=qt.batch(()=>q(this,Et).findAll(e).filter(i=>!i.isDisabled()&&!i.isStatic()).map(i=>{let a=i.fetch(void 0,n);return n.throwOnError||(a=a.catch(kn)),i.state.fetchStatus==="paused"?Promise.resolve():a}));return Promise.all(r).then(kn)}fetchQuery(e){const t=this.defaultQueryOptions(e);t.retry===void 0&&(t.retry=!1);const n=q(this,Et).build(this,t);return n.isStaleByTime(_o(t.staleTime,n))?n.fetch(t):Promise.resolve(n.state.data)}prefetchQuery(e){return this.fetchQuery(e).then(kn).catch(kn)}fetchInfiniteQuery(e){return e._type="infinite",this.fetchQuery(e)}prefetchInfiniteQuery(e){return this.fetchInfiniteQuery(e).then(kn).catch(kn)}ensureInfiniteQueryData(e){return e._type="infinite",this.ensureQueryData(e)}resumePausedMutations(){return cp.isOnline()?q(this,io).resumePausedMutations():Promise.resolve()}getQueryCache(){return q(this,Et)}getMutationCache(){return q(this,io)}getDefaultOptions(){return q(this,ao)}setDefaultOptions(e){fe(this,ao,e)}setQueryDefaults(e,t){q(this,Ql).set(Es(e),{queryKey:e,defaultOptions:t})}getQueryDefaults(e){const t=[...q(this,Ql).values()],n={};return t.forEach(r=>{Ud(e,r.queryKey)&&Object.assign(n,r.defaultOptions)}),n}setMutationDefaults(e,t){q(this,Jl).set(Es(e),{mutationKey:e,defaultOptions:t})}getMutationDefaults(e){const t=[...q(this,Jl).values()],n={};return t.forEach(r=>{Ud(e,r.mutationKey)&&Object.assign(n,r.defaultOptions)}),n}defaultQueryOptions(e){if(e._defaulted)return e;const t={...q(this,ao).queries,...this.getQueryDefaults(e.queryKey),...e,_defaulted:!0};return t.queryHash||(t.queryHash=M_(t.queryKey,t)),t.refetchOnReconnect===void 0&&(t.refetchOnReconnect=t.networkMode!=="always"),t.throwOnError===void 0&&(t.throwOnError=!!t.suspense),!t.networkMode&&t.persister&&(t.networkMode="offlineFirst"),t.queryFn===L_&&(t.enabled=!1),t}defaultMutationOptions(e){return e!=null&&e._defaulted?e:{...q(this,ao).mutations,...(e==null?void 0:e.mutationKey)&&this.getMutationDefaults(e.mutationKey),...e,_defaulted:!0}}clear(){q(this,Et).clear(),q(this,io).clear()}},Et=new WeakMap,io=new WeakMap,ao=new WeakMap,Ql=new WeakMap,Jl=new WeakMap,oo=new WeakMap,ec=new WeakMap,tc=new WeakMap,Sk),oA=N.createContext(void 0),R_=e=>{const t=N.useContext(oA);if(!t)throw new Error("No QueryClient set, use QueryClientProvider to set one");return t},uF=({client:e,children:t})=>(N.useEffect(()=>(e.mount(),()=>{e.unmount()}),[e]),o.jsx(oA.Provider,{value:e,children:t})),sA=N.createContext(!1),dF=()=>N.useContext(sA);sA.Provider;function fF(){let e=!1;return{clearReset:()=>{e=!1},reset:()=>{e=!0},isReset:()=>e}}var hF=N.createContext(fF()),mF=()=>N.useContext(hF),pF=(e,t,n)=>{const r=n!=null&&n.state.error&&typeof e.throwOnError=="function"?$_(e.throwOnError,[n.state.error,n]):e.throwOnError;(e.suspense||e.experimental_prefetchInRender||r)&&(t.isReset()||(e.retryOnMount=!1))},gF=e=>{N.useEffect(()=>{e.clearReset()},[e])},vF=({result:e,errorResetBoundary:t,throwOnError:n,query:r,suspense:i})=>e.isError&&!t.isReset()&&!e.isFetching&&r&&(i&&e.data===void 0||$_(n,[e.error,r])),yF=e=>{if(e.suspense){const n=i=>i==="static"?i:Math.max(i??1e3,1e3),r=e.staleTime;e.staleTime=typeof r=="function"?(...i)=>n(r(...i)):n(r),typeof e.gcTime=="number"&&(e.gcTime=Math.max(e.gcTime,1e3))}},xF=(e,t)=>e.isLoading&&e.isFetching&&!t,bF=(e,t)=>(e==null?void 0:e.suspense)&&t.isPending,f5=(e,t,n)=>t.fetchOptimistic(e).catch(()=>{n.clearReset()});function wF(e,t,n){var p,v,_,x;const r=dF(),i=mF(),a=R_(),s=a.defaultQueryOptions(e);(v=(p=a.getDefaultOptions().queries)==null?void 0:p._experimental_beforeQuery)==null||v.call(p,s);const c=a.getQueryCache().get(s.queryHash);s._optimisticResults=r?"isRestoring":"optimistic",yF(s),pF(s,i,c),gF(i);const u=!a.getQueryCache().get(s.queryHash),[d]=N.useState(()=>new t(a,s)),h=d.getOptimisticResult(s),m=!r&&e.subscribed!==!1;if(N.useSyncExternalStore(N.useCallback(y=>{const w=m?d.subscribe(qt.batchCalls(y)):kn;return d.updateResult(),w},[d,m]),()=>d.getCurrentResult(),()=>d.getCurrentResult()),N.useEffect(()=>{d.setOptions(s)},[s,d]),bF(s,h))throw f5(s,d,i);if(vF({result:h,errorResetBoundary:i,throwOnError:s.throwOnError,query:c,suspense:s.suspense}))throw h.error;if((x=(_=a.getDefaultOptions().queries)==null?void 0:_._experimental_afterQuery)==null||x.call(_,s,h),s.experimental_prefetchInRender&&!Wd.isServer()&&xF(h,r)){const y=u?f5(s,d,i):c==null?void 0:c.promise;y==null||y.catch(kn).finally(()=>{d.updateResult()})}return s.notifyOnChangeProps?h:d.trackResult(h)}function zt(e,t){return wF(e,nF)}function h5(e,t){const n=R_(),[r]=N.useState(()=>new sF(n,e));N.useEffect(()=>{r.setOptions(e)},[r,e]);const i=N.useSyncExternalStore(N.useCallback(s=>r.subscribe(qt.batchCalls(s)),[r]),()=>r.getCurrentResult(),()=>r.getCurrentResult()),a=N.useCallback((s,c)=>{r.mutate(s,c).catch(kn)},[r]);if(i.error&&$_(r.options.throwOnError,[i.error]))throw i.error;return{...i,mutate:a,mutateAsync:i.mutate}}const lA=N.createContext(void 0),_F=({children:e})=>{const[t,n]=N.useState(null),[r,i]=N.useState(null),[a,s]=N.useState(!0),c="/api";N.useEffect(()=>{const p=new URLSearchParams(window.location.search).get("token");if(p){localStorage.setItem("auth_token",p),i(p),u(p),window.history.replaceState({},document.title,window.location.pathname);return}const v=localStorage.getItem("auth_token");v?(i(v),u(v)):s(!1)},[]);const u=async m=>{try{const p=await fetch(`${c}/auth/me`,{headers:{Authorization:`Bearer ${m}`}});if(p.ok){const v=await p.json();n(v)}else localStorage.removeItem("auth_token"),i(null)}catch(p){console.error("Error fetching user:",p),localStorage.removeItem("auth_token"),i(null)}finally{s(!1)}},d=m=>{const p=encodeURIComponent(window.location.origin),v=`${c}/auth/login/${m}?redirect_uri=${p}`;window.location.href=v},h=()=>{localStorage.removeItem("auth_token"),i(null),n(null)};return o.jsx(lA.Provider,{value:{user:t,token:r,login:d,logout:h,isAuthenticated:!!t,isLoading:a},children:e})},e0=()=>{const e=N.useContext(lA);if(e===void 0)throw new Error("useAuth must be used within an AuthProvider");return e},Vh={Alabama:"AL",Alaska:"AK",Arizona:"AZ",Arkansas:"AR",California:"CA",Colorado:"CO",Connecticut:"CT",Delaware:"DE",Florida:"FL",Georgia:"GA",Hawaii:"HI",Idaho:"ID",Illinois:"IL",Indiana:"IN",Iowa:"IA",Kansas:"KS",Kentucky:"KY",Louisiana:"LA",Maine:"ME",Maryland:"MD",Massachusetts:"MA",Michigan:"MI",Minnesota:"MN",Mississippi:"MS",Missouri:"MO",Montana:"MT",Nebraska:"NE",Nevada:"NV","New Hampshire":"NH","New Jersey":"NJ","New Mexico":"NM","New York":"NY","North Carolina":"NC","North Dakota":"ND",Ohio:"OH",Oklahoma:"OK",Oregon:"OR",Pennsylvania:"PA","Rhode Island":"RI","South Carolina":"SC","South Dakota":"SD",Tennessee:"TN",Texas:"TX",Utah:"UT",Vermont:"VT",Virginia:"VA",Washington:"WA","West Virginia":"WV",Wisconsin:"WI",Wyoming:"WY","District of Columbia":"DC","Puerto Rico":"PR"};function pb(e){if(e&&e.length===2&&e===e.toUpperCase())return e;if(Vh[e])return Vh[e];const t=Object.keys(Vh).find(n=>n.toLowerCase()===e.toLowerCase());return t?Vh[t]:(console.warn(`State name "${e}" not found in mapping`),e)}const cA=N.createContext(void 0),jF=({children:e})=>{const{user:t,isAuthenticated:n}=e0(),[r,i]=N.useState(null);N.useEffect(()=>{if(n&&t){if(t.state&&t.city){const u=pb(t.state);i({state:u,county:t.county||"",city:t.city,school_board:t.school_board})}}else{const u=localStorage.getItem("user_location");if(u)try{const d=JSON.parse(u);d.state&&(d.state=pb(d.state)),i(d),localStorage.setItem("user_location",JSON.stringify(d))}catch(d){console.error("Failed to parse saved location:",d)}}},[t,n]);const a=u=>{i(u),n||localStorage.setItem("user_location",JSON.stringify(u))},s=()=>{i(null),localStorage.removeItem("user_location")},c=r!==null&&!!r.state&&!!r.city;return o.jsx(cA.Provider,{value:{location:r,setLocation:a,clearLocation:s,hasLocation:c},children:e})},Hf=()=>{const e=N.useContext(cA);if(e===void 0)throw new Error("useLocation must be used within a LocationProvider");return e};function NF(){const{pathname:e}=zs();return N.useEffect(()=>{window.scrollTo(0,0)},[e]),null}var SF=Object.defineProperty,PF=(e,t,n)=>t in e?SF(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,fy=(e,t,n)=>(PF(e,typeof t!="symbol"?t+"":t,n),n);let EF=class{constructor(){fy(this,"current",this.detect()),fy(this,"handoffState","pending"),fy(this,"currentId",0)}set(t){this.current!==t&&(this.handoffState="pending",this.currentId=0,this.current=t)}reset(){this.set(this.detect())}nextId(){return++this.currentId}get isServer(){return this.current==="server"}get isClient(){return this.current==="client"}detect(){return typeof window>"u"||typeof document>"u"?"server":"client"}handoff(){this.handoffState==="pending"&&(this.handoffState="complete")}get isHandoffComplete(){return this.handoffState==="complete"}},bs=new EF,jn=(e,t)=>{bs.isServer?N.useEffect(e,t):N.useLayoutEffect(e,t)};function Lr(e){let t=N.useRef(e);return jn(()=>{t.current=e},[e]),t}let Ke=function(e){let t=Lr(e);return H.useCallback((...n)=>t.current(...n),[t])};function uA(e){typeof queueMicrotask=="function"?queueMicrotask(e):Promise.resolve().then(e).catch(t=>setTimeout(()=>{throw t}))}function Pa(){let e=[],t={addEventListener(n,r,i,a){return n.addEventListener(r,i,a),t.add(()=>n.removeEventListener(r,i,a))},requestAnimationFrame(...n){let r=requestAnimationFrame(...n);return t.add(()=>cancelAnimationFrame(r))},nextFrame(...n){return t.requestAnimationFrame(()=>t.requestAnimationFrame(...n))},setTimeout(...n){let r=setTimeout(...n);return t.add(()=>clearTimeout(r))},microTask(...n){let r={current:!0};return uA(()=>{r.current&&n[0]()}),t.add(()=>{r.current=!1})},style(n,r,i){let a=n.style.getPropertyValue(r);return Object.assign(n.style,{[r]:i}),this.add(()=>{Object.assign(n.style,{[r]:a})})},group(n){let r=Pa();return n(r),this.add(()=>r.dispose())},add(n){return e.push(n),()=>{let r=e.indexOf(n);if(r>=0)for(let i of e.splice(r,1))i()}},dispose(){for(let n of e.splice(0))n()}};return t}function t0(){let[e]=N.useState(Pa);return N.useEffect(()=>()=>e.dispose(),[e]),e}function OF(){let e=typeof document>"u";return"useSyncExternalStore"in rx?(t=>t.useSyncExternalStore)(rx)(()=>()=>{},()=>!1,()=>!e):!1}function F_(){let e=OF(),[t,n]=N.useState(bs.isHandoffComplete);return t&&bs.isHandoffComplete===!1&&n(!1),N.useEffect(()=>{t!==!0&&n(!0)},[t]),N.useEffect(()=>bs.handoff(),[]),e?!1:t}var m5;let Vf=(m5=H.useId)!=null?m5:function(){let e=F_(),[t,n]=H.useState(e?()=>bs.nextId():null);return jn(()=>{t===null&&n(bs.nextId())},[t]),t!=null?""+t:void 0};function tn(e,t,...n){if(e in t){let i=t[e];return typeof i=="function"?i(...n):i}let r=new Error(`Tried to handle "${e}" but there is no handler defined. Only defined handlers are: ${Object.keys(t).map(i=>`"${i}"`).join(", ")}.`);throw Error.captureStackTrace&&Error.captureStackTrace(r,tn),r}function qf(e){return bs.isServer?null:e instanceof Node?e.ownerDocument:e!=null&&e.hasOwnProperty("current")&&e.current instanceof Node?e.current.ownerDocument:document}let gb=["[contentEditable=true]","[tabindex]","a[href]","area[href]","button:not([disabled])","iframe","input:not([disabled])","select:not([disabled])","textarea:not([disabled])"].map(e=>`${e}:not([tabindex='-1'])`).join(",");var rr=(e=>(e[e.First=1]="First",e[e.Previous=2]="Previous",e[e.Next=4]="Next",e[e.Last=8]="Last",e[e.WrapAround=16]="WrapAround",e[e.NoScroll=32]="NoScroll",e))(rr||{}),Zu=(e=>(e[e.Error=0]="Error",e[e.Overflow=1]="Overflow",e[e.Success=2]="Success",e[e.Underflow=3]="Underflow",e))(Zu||{}),kF=(e=>(e[e.Previous=-1]="Previous",e[e.Next=1]="Next",e))(kF||{});function dA(e=document.body){return e==null?[]:Array.from(e.querySelectorAll(gb)).sort((t,n)=>Math.sign((t.tabIndex||Number.MAX_SAFE_INTEGER)-(n.tabIndex||Number.MAX_SAFE_INTEGER)))}var D_=(e=>(e[e.Strict=0]="Strict",e[e.Loose=1]="Loose",e))(D_||{});function B_(e,t=0){var n;return e===((n=qf(e))==null?void 0:n.body)?!1:tn(t,{0(){return e.matches(gb)},1(){let r=e;for(;r!==null;){if(r.matches(gb))return!0;r=r.parentElement}return!1}})}function fA(e){let t=qf(e);Pa().nextFrame(()=>{t&&!B_(t.activeElement,0)&&AF(e)})}var CF=(e=>(e[e.Keyboard=0]="Keyboard",e[e.Mouse=1]="Mouse",e))(CF||{});typeof window<"u"&&typeof document<"u"&&(document.addEventListener("keydown",e=>{e.metaKey||e.altKey||e.ctrlKey||(document.documentElement.dataset.headlessuiFocusVisible="")},!0),document.addEventListener("click",e=>{e.detail===1?delete document.documentElement.dataset.headlessuiFocusVisible:e.detail===0&&(document.documentElement.dataset.headlessuiFocusVisible="")},!0));function AF(e){e==null||e.focus({preventScroll:!0})}let TF=["textarea","input"].join(",");function MF(e){var t,n;return(n=(t=e==null?void 0:e.matches)==null?void 0:t.call(e,TF))!=null?n:!1}function is(e,t=n=>n){return e.slice().sort((n,r)=>{let i=t(n),a=t(r);if(i===null||a===null)return 0;let s=i.compareDocumentPosition(a);return s&Node.DOCUMENT_POSITION_FOLLOWING?-1:s&Node.DOCUMENT_POSITION_PRECEDING?1:0})}function LF(e,t){return Go(dA(),t,{relativeTo:e})}function Go(e,t,{sorted:n=!0,relativeTo:r=null,skipElements:i=[]}={}){let a=Array.isArray(e)?e.length>0?e[0].ownerDocument:document:e.ownerDocument,s=Array.isArray(e)?n?is(e):e:dA(e);i.length>0&&s.length>1&&(s=s.filter(v=>!i.includes(v))),r=r??a.activeElement;let c=(()=>{if(t&5)return 1;if(t&10)return-1;throw new Error("Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last")})(),u=(()=>{if(t&1)return 0;if(t&2)return Math.max(0,s.indexOf(r))-1;if(t&4)return Math.max(0,s.indexOf(r))+1;if(t&8)return s.length-1;throw new Error("Missing Focus.First, Focus.Previous, Focus.Next or Focus.Last")})(),d=t&32?{preventScroll:!0}:{},h=0,m=s.length,p;do{if(h>=m||h+m<=0)return 0;let v=u+h;if(t&16)v=(v+m)%m;else{if(v<0)return 3;if(v>=m)return 1}p=s[v],p==null||p.focus(d),h+=c}while(p!==a.activeElement);return t&6&&MF(p)&&p.select(),2}function $F(){return/iPhone/gi.test(window.navigator.platform)||/Mac/gi.test(window.navigator.platform)&&window.navigator.maxTouchPoints>0}function IF(){return/Android/gi.test(window.navigator.userAgent)}function RF(){return $F()||IF()}function qh(e,t,n){let r=Lr(t);N.useEffect(()=>{function i(a){r.current(a)}return document.addEventListener(e,i,n),()=>document.removeEventListener(e,i,n)},[e,n])}function FF(e,t,n){let r=Lr(t);N.useEffect(()=>{function i(a){r.current(a)}return window.addEventListener(e,i,n),()=>window.removeEventListener(e,i,n)},[e,n])}function DF(e,t,n=!0){let r=N.useRef(!1);N.useEffect(()=>{requestAnimationFrame(()=>{r.current=n})},[n]);function i(s,c){if(!r.current||s.defaultPrevented)return;let u=c(s);if(u===null||!u.getRootNode().contains(u)||!u.isConnected)return;let d=function h(m){return typeof m=="function"?h(m()):Array.isArray(m)||m instanceof Set?m:[m]}(e);for(let h of d){if(h===null)continue;let m=h instanceof HTMLElement?h:h.current;if(m!=null&&m.contains(u)||s.composed&&s.composedPath().includes(m))return}return!B_(u,D_.Loose)&&u.tabIndex!==-1&&s.preventDefault(),t(s,u)}let a=N.useRef(null);qh("pointerdown",s=>{var c,u;r.current&&(a.current=((u=(c=s.composedPath)==null?void 0:c.call(s))==null?void 0:u[0])||s.target)},!0),qh("mousedown",s=>{var c,u;r.current&&(a.current=((u=(c=s.composedPath)==null?void 0:c.call(s))==null?void 0:u[0])||s.target)},!0),qh("click",s=>{RF()||a.current&&(i(s,()=>a.current),a.current=null)},!0),qh("touchend",s=>i(s,()=>s.target instanceof HTMLElement?s.target:null),!0),FF("blur",s=>i(s,()=>window.document.activeElement instanceof HTMLIFrameElement?window.document.activeElement:null),!0)}function BF(...e){return N.useMemo(()=>qf(...e),[...e])}function p5(e){var t;if(e.type)return e.type;let n=(t=e.as)!=null?t:"button";if(typeof n=="string"&&n.toLowerCase()==="button")return"button"}function hA(e,t){let[n,r]=N.useState(()=>p5(e));return jn(()=>{r(p5(e))},[e.type,e.as]),jn(()=>{n||t.current&&t.current instanceof HTMLButtonElement&&!t.current.hasAttribute("type")&&r("button")},[n,t]),n}let zF=Symbol();function pi(...e){let t=N.useRef(e);N.useEffect(()=>{t.current=e},[e]);let n=Ke(r=>{for(let i of t.current)i!=null&&(typeof i=="function"?i(r):i.current=r)});return e.every(r=>r==null||(r==null?void 0:r[zF]))?void 0:n}function g5(e){return[e.screenX,e.screenY]}function UF(){let e=N.useRef([-1,-1]);return{wasMoved(t){let n=g5(t);return e.current[0]===n[0]&&e.current[1]===n[1]?!1:(e.current=n,!0)},update(t){e.current=g5(t)}}}function WF({container:e,accept:t,walk:n,enabled:r=!0}){let i=N.useRef(t),a=N.useRef(n);N.useEffect(()=>{i.current=t,a.current=n},[t,n]),jn(()=>{if(!e||!r)return;let s=qf(e);if(!s)return;let c=i.current,u=a.current,d=Object.assign(m=>c(m),{acceptNode:c}),h=s.createTreeWalker(e,NodeFilter.SHOW_ELEMENT,d,!1);for(;h.nextNode();)u(h.currentNode)},[e,r,i,a])}function up(...e){return Array.from(new Set(e.flatMap(t=>typeof t=="string"?t.split(" "):[]))).filter(Boolean).join(" ")}var cc=(e=>(e[e.None=0]="None",e[e.RenderStrategy=1]="RenderStrategy",e[e.Static=2]="Static",e))(cc||{}),uo=(e=>(e[e.Unmount=0]="Unmount",e[e.Hidden=1]="Hidden",e))(uo||{});function Hr({ourProps:e,theirProps:t,slot:n,defaultTag:r,features:i,visible:a=!0,name:s,mergeRefs:c}){c=c??HF;let u=mA(t,e);if(a)return Zh(u,n,r,s,c);let d=i??0;if(d&2){let{static:h=!1,...m}=u;if(h)return Zh(m,n,r,s,c)}if(d&1){let{unmount:h=!0,...m}=u;return tn(h?0:1,{0(){return null},1(){return Zh({...m,hidden:!0,style:{display:"none"}},n,r,s,c)}})}return Zh(u,n,r,s,c)}function Zh(e,t={},n,r,i){let{as:a=n,children:s,refName:c="ref",...u}=hy(e,["unmount","static"]),d=e.ref!==void 0?{[c]:e.ref}:{},h=typeof s=="function"?s(t):s;"className"in u&&u.className&&typeof u.className=="function"&&(u.className=u.className(t));let m={};if(t){let p=!1,v=[];for(let[_,x]of Object.entries(t))typeof x=="boolean"&&(p=!0),x===!0&&v.push(_);p&&(m["data-headlessui-state"]=v.join(" "))}if(a===N.Fragment&&Object.keys(v5(u)).length>0){if(!N.isValidElement(h)||Array.isArray(h)&&h.length>1)throw new Error(['Passing props on "Fragment"!',"",`The current component <${r} /> is rendering a "Fragment".`,"However we need to passthrough the following props:",Object.keys(u).map(x=>` - ${x}`).join(` +`),"","You can apply a few solutions:",['Add an `as="..."` prop, to ensure that we render an actual element instead of a "Fragment".',"Render a single element as the child so that we can forward the props onto that element."].map(x=>` - ${x}`).join(` +`)].join(` +`));let p=h.props,v=typeof(p==null?void 0:p.className)=="function"?(...x)=>up(p==null?void 0:p.className(...x),u.className):up(p==null?void 0:p.className,u.className),_=v?{className:v}:{};return N.cloneElement(h,Object.assign({},mA(h.props,v5(hy(u,["ref"]))),m,d,{ref:i(h.ref,d.ref)},_))}return N.createElement(a,Object.assign({},hy(u,["ref"]),a!==N.Fragment&&d,a!==N.Fragment&&m),h)}function HF(...e){return e.every(t=>t==null)?void 0:t=>{for(let n of e)n!=null&&(typeof n=="function"?n(t):n.current=t)}}function mA(...e){if(e.length===0)return{};if(e.length===1)return e[0];let t={},n={};for(let r of e)for(let i in r)i.startsWith("on")&&typeof r[i]=="function"?(n[i]!=null||(n[i]=[]),n[i].push(r[i])):t[i]=r[i];if(t.disabled||t["aria-disabled"])return Object.assign(t,Object.fromEntries(Object.keys(n).map(r=>[r,void 0])));for(let r in n)Object.assign(t,{[r](i,...a){let s=n[r];for(let c of s){if((i instanceof Event||(i==null?void 0:i.nativeEvent)instanceof Event)&&i.defaultPrevented)return;c(i,...a)}}});return t}function br(e){var t;return Object.assign(N.forwardRef(e),{displayName:(t=e.displayName)!=null?t:e.name})}function v5(e){let t=Object.assign({},e);for(let n in t)t[n]===void 0&&delete t[n];return t}function hy(e,t=[]){let n=Object.assign({},e);for(let r of t)r in n&&delete n[r];return n}let VF="div";var pA=(e=>(e[e.None=1]="None",e[e.Focusable=2]="Focusable",e[e.Hidden=4]="Hidden",e))(pA||{});function qF(e,t){var n;let{features:r=1,...i}=e,a={ref:t,"aria-hidden":(r&2)===2?!0:(n=i["aria-hidden"])!=null?n:void 0,hidden:(r&4)===4?!0:void 0,style:{position:"fixed",top:1,left:1,width:1,height:0,padding:0,margin:-1,overflow:"hidden",clip:"rect(0, 0, 0, 0)",whiteSpace:"nowrap",borderWidth:"0",...(r&4)===4&&(r&2)!==2&&{display:"none"}}};return Hr({ourProps:a,theirProps:i,slot:{},defaultTag:VF,name:"Hidden"})}let gA=br(qF),z_=N.createContext(null);z_.displayName="OpenClosedContext";var ur=(e=>(e[e.Open=1]="Open",e[e.Closed=2]="Closed",e[e.Closing=4]="Closing",e[e.Opening=8]="Opening",e))(ur||{});function U_(){return N.useContext(z_)}function vA({value:e,children:t}){return H.createElement(z_.Provider,{value:e},t)}function ZF(e){let t=e.parentElement,n=null;for(;t&&!(t instanceof HTMLFieldSetElement);)t instanceof HTMLLegendElement&&(n=t),t=t.parentElement;let r=(t==null?void 0:t.getAttribute("disabled"))==="";return r&&GF(n)?!1:r}function GF(e){if(!e)return!1;let t=e.previousElementSibling;for(;t!==null;){if(t instanceof HTMLLegendElement)return!1;t=t.previousElementSibling}return!0}function KF(e){throw new Error("Unexpected object: "+e)}var li=(e=>(e[e.First=0]="First",e[e.Previous=1]="Previous",e[e.Next=2]="Next",e[e.Last=3]="Last",e[e.Specific=4]="Specific",e[e.Nothing=5]="Nothing",e))(li||{});function YF(e,t){let n=t.resolveItems();if(n.length<=0)return null;let r=t.resolveActiveIndex(),i=r??-1;switch(e.focus){case 0:{for(let a=0;a=0;--a)if(!t.resolveDisabled(n[a],a,n))return a;return r}case 2:{for(let a=i+1;a=0;--a)if(!t.resolveDisabled(n[a],a,n))return a;return r}case 4:{for(let a=0;a(e.Space=" ",e.Enter="Enter",e.Escape="Escape",e.Backspace="Backspace",e.Delete="Delete",e.ArrowLeft="ArrowLeft",e.ArrowUp="ArrowUp",e.ArrowRight="ArrowRight",e.ArrowDown="ArrowDown",e.Home="Home",e.End="End",e.PageUp="PageUp",e.PageDown="PageDown",e.Tab="Tab",e))(nt||{});function n0(){let e=N.useRef(!1);return jn(()=>(e.current=!0,()=>{e.current=!1}),[]),e}let y5=/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g;function x5(e){var t,n;let r=(t=e.innerText)!=null?t:"",i=e.cloneNode(!0);if(!(i instanceof HTMLElement))return r;let a=!1;for(let c of i.querySelectorAll('[hidden],[aria-hidden],[role="img"]'))c.remove(),a=!0;let s=a?(n=i.innerText)!=null?n:"":r;return y5.test(s)&&(s=s.replace(y5,"")),s}function XF(e){let t=e.getAttribute("aria-label");if(typeof t=="string")return t.trim();let n=e.getAttribute("aria-labelledby");if(n){let r=n.split(" ").map(i=>{let a=document.getElementById(i);if(a){let s=a.getAttribute("aria-label");return typeof s=="string"?s.trim():x5(a).trim()}return null}).filter(Boolean);if(r.length>0)return r.join(", ")}return x5(e).trim()}function QF(e){let t=N.useRef(""),n=N.useRef("");return Ke(()=>{let r=e.current;if(!r)return"";let i=r.innerText;if(t.current===i)return n.current;let a=XF(r).trim().toLowerCase();return t.current=i,n.current=a,a})}var JF=(e=>(e[e.Open=0]="Open",e[e.Closed=1]="Closed",e))(JF||{}),eD=(e=>(e[e.Pointer=0]="Pointer",e[e.Other=1]="Other",e))(eD||{}),tD=(e=>(e[e.OpenMenu=0]="OpenMenu",e[e.CloseMenu=1]="CloseMenu",e[e.GoToItem=2]="GoToItem",e[e.Search=3]="Search",e[e.ClearSearch=4]="ClearSearch",e[e.RegisterItem=5]="RegisterItem",e[e.UnregisterItem=6]="UnregisterItem",e))(tD||{});function my(e,t=n=>n){let n=e.activeItemIndex!==null?e.items[e.activeItemIndex]:null,r=is(t(e.items.slice()),a=>a.dataRef.current.domRef.current),i=n?r.indexOf(n):null;return i===-1&&(i=null),{items:r,activeItemIndex:i}}let nD={1(e){return e.menuState===1?e:{...e,activeItemIndex:null,menuState:1}},0(e){return e.menuState===0?e:{...e,__demoMode:!1,menuState:0}},2:(e,t)=>{var n;let r=my(e),i=YF(t,{resolveItems:()=>r.items,resolveActiveIndex:()=>r.activeItemIndex,resolveId:a=>a.id,resolveDisabled:a=>a.dataRef.current.disabled});return{...e,...r,searchQuery:"",activeItemIndex:i,activationTrigger:(n=t.trigger)!=null?n:1}},3:(e,t)=>{let n=e.searchQuery!==""?0:1,r=e.searchQuery+t.value.toLowerCase(),i=(e.activeItemIndex!==null?e.items.slice(e.activeItemIndex+n).concat(e.items.slice(0,e.activeItemIndex+n)):e.items).find(s=>{var c;return((c=s.dataRef.current.textValue)==null?void 0:c.startsWith(r))&&!s.dataRef.current.disabled}),a=i?e.items.indexOf(i):-1;return a===-1||a===e.activeItemIndex?{...e,searchQuery:r}:{...e,searchQuery:r,activeItemIndex:a,activationTrigger:1}},4(e){return e.searchQuery===""?e:{...e,searchQuery:"",searchActiveItemIndex:null}},5:(e,t)=>{let n=my(e,r=>[...r,{id:t.id,dataRef:t.dataRef}]);return{...e,...n}},6:(e,t)=>{let n=my(e,r=>{let i=r.findIndex(a=>a.id===t.id);return i!==-1&&r.splice(i,1),r});return{...e,...n,activationTrigger:1}}},W_=N.createContext(null);W_.displayName="MenuContext";function r0(e){let t=N.useContext(W_);if(t===null){let n=new Error(`<${e} /> is missing a parent component.`);throw Error.captureStackTrace&&Error.captureStackTrace(n,r0),n}return t}function rD(e,t){return tn(t.type,nD,e,t)}let iD=N.Fragment;function aD(e,t){let{__demoMode:n=!1,...r}=e,i=N.useReducer(rD,{__demoMode:n,menuState:n?0:1,buttonRef:N.createRef(),itemsRef:N.createRef(),items:[],searchQuery:"",activeItemIndex:null,activationTrigger:1}),[{menuState:a,itemsRef:s,buttonRef:c},u]=i,d=pi(t);DF([c,s],(v,_)=>{var x;u({type:1}),B_(_,D_.Loose)||(v.preventDefault(),(x=c.current)==null||x.focus())},a===0);let h=Ke(()=>{u({type:1})}),m=N.useMemo(()=>({open:a===0,close:h}),[a,h]),p={ref:d};return H.createElement(W_.Provider,{value:i},H.createElement(vA,{value:tn(a,{0:ur.Open,1:ur.Closed})},Hr({ourProps:p,theirProps:r,slot:m,defaultTag:iD,name:"Menu"})))}let oD="button";function sD(e,t){var n;let r=Vf(),{id:i=`headlessui-menu-button-${r}`,...a}=e,[s,c]=r0("Menu.Button"),u=pi(s.buttonRef,t),d=t0(),h=Ke(x=>{switch(x.key){case nt.Space:case nt.Enter:case nt.ArrowDown:x.preventDefault(),x.stopPropagation(),c({type:0}),d.nextFrame(()=>c({type:2,focus:li.First}));break;case nt.ArrowUp:x.preventDefault(),x.stopPropagation(),c({type:0}),d.nextFrame(()=>c({type:2,focus:li.Last}));break}}),m=Ke(x=>{switch(x.key){case nt.Space:x.preventDefault();break}}),p=Ke(x=>{if(ZF(x.currentTarget))return x.preventDefault();e.disabled||(s.menuState===0?(c({type:1}),d.nextFrame(()=>{var y;return(y=s.buttonRef.current)==null?void 0:y.focus({preventScroll:!0})})):(x.preventDefault(),c({type:0})))}),v=N.useMemo(()=>({open:s.menuState===0}),[s]),_={ref:u,id:i,type:hA(e,s.buttonRef),"aria-haspopup":"menu","aria-controls":(n=s.itemsRef.current)==null?void 0:n.id,"aria-expanded":s.menuState===0,onKeyDown:h,onKeyUp:m,onClick:p};return Hr({ourProps:_,theirProps:a,slot:v,defaultTag:oD,name:"Menu.Button"})}let lD="div",cD=cc.RenderStrategy|cc.Static;function uD(e,t){var n,r;let i=Vf(),{id:a=`headlessui-menu-items-${i}`,...s}=e,[c,u]=r0("Menu.Items"),d=pi(c.itemsRef,t),h=BF(c.itemsRef),m=t0(),p=U_(),v=p!==null?(p&ur.Open)===ur.Open:c.menuState===0;N.useEffect(()=>{let b=c.itemsRef.current;b&&c.menuState===0&&b!==(h==null?void 0:h.activeElement)&&b.focus({preventScroll:!0})},[c.menuState,c.itemsRef,h]),WF({container:c.itemsRef.current,enabled:c.menuState===0,accept(b){return b.getAttribute("role")==="menuitem"?NodeFilter.FILTER_REJECT:b.hasAttribute("role")?NodeFilter.FILTER_SKIP:NodeFilter.FILTER_ACCEPT},walk(b){b.setAttribute("role","none")}});let _=Ke(b=>{var j,E;switch(m.dispose(),b.key){case nt.Space:if(c.searchQuery!=="")return b.preventDefault(),b.stopPropagation(),u({type:3,value:b.key});case nt.Enter:if(b.preventDefault(),b.stopPropagation(),u({type:1}),c.activeItemIndex!==null){let{dataRef:P}=c.items[c.activeItemIndex];(E=(j=P.current)==null?void 0:j.domRef.current)==null||E.click()}fA(c.buttonRef.current);break;case nt.ArrowDown:return b.preventDefault(),b.stopPropagation(),u({type:2,focus:li.Next});case nt.ArrowUp:return b.preventDefault(),b.stopPropagation(),u({type:2,focus:li.Previous});case nt.Home:case nt.PageUp:return b.preventDefault(),b.stopPropagation(),u({type:2,focus:li.First});case nt.End:case nt.PageDown:return b.preventDefault(),b.stopPropagation(),u({type:2,focus:li.Last});case nt.Escape:b.preventDefault(),b.stopPropagation(),u({type:1}),Pa().nextFrame(()=>{var P;return(P=c.buttonRef.current)==null?void 0:P.focus({preventScroll:!0})});break;case nt.Tab:b.preventDefault(),b.stopPropagation(),u({type:1}),Pa().nextFrame(()=>{LF(c.buttonRef.current,b.shiftKey?rr.Previous:rr.Next)});break;default:b.key.length===1&&(u({type:3,value:b.key}),m.setTimeout(()=>u({type:4}),350));break}}),x=Ke(b=>{switch(b.key){case nt.Space:b.preventDefault();break}}),y=N.useMemo(()=>({open:c.menuState===0}),[c]),w={"aria-activedescendant":c.activeItemIndex===null||(n=c.items[c.activeItemIndex])==null?void 0:n.id,"aria-labelledby":(r=c.buttonRef.current)==null?void 0:r.id,id:a,onKeyDown:_,onKeyUp:x,role:"menu",tabIndex:0,ref:d};return Hr({ourProps:w,theirProps:s,slot:y,defaultTag:lD,features:cD,visible:v,name:"Menu.Items"})}let dD=N.Fragment;function fD(e,t){let n=Vf(),{id:r=`headlessui-menu-item-${n}`,disabled:i=!1,...a}=e,[s,c]=r0("Menu.Item"),u=s.activeItemIndex!==null?s.items[s.activeItemIndex].id===r:!1,d=N.useRef(null),h=pi(t,d);jn(()=>{if(s.__demoMode||s.menuState!==0||!u||s.activationTrigger===0)return;let P=Pa();return P.requestAnimationFrame(()=>{var O,C;(C=(O=d.current)==null?void 0:O.scrollIntoView)==null||C.call(O,{block:"nearest"})}),P.dispose},[s.__demoMode,d,u,s.menuState,s.activationTrigger,s.activeItemIndex]);let m=QF(d),p=N.useRef({disabled:i,domRef:d,get textValue(){return m()}});jn(()=>{p.current.disabled=i},[p,i]),jn(()=>(c({type:5,id:r,dataRef:p}),()=>c({type:6,id:r})),[p,r]);let v=Ke(()=>{c({type:1})}),_=Ke(P=>{if(i)return P.preventDefault();c({type:1}),fA(s.buttonRef.current)}),x=Ke(()=>{if(i)return c({type:2,focus:li.Nothing});c({type:2,focus:li.Specific,id:r})}),y=UF(),w=Ke(P=>y.update(P)),b=Ke(P=>{y.wasMoved(P)&&(i||u||c({type:2,focus:li.Specific,id:r,trigger:0}))}),j=Ke(P=>{y.wasMoved(P)&&(i||u&&c({type:2,focus:li.Nothing}))}),E=N.useMemo(()=>({active:u,disabled:i,close:v}),[u,i,v]);return Hr({ourProps:{id:r,ref:h,role:"menuitem",tabIndex:i===!0?void 0:-1,"aria-disabled":i===!0?!0:void 0,disabled:void 0,onClick:_,onFocus:x,onPointerEnter:w,onMouseEnter:w,onPointerMove:b,onMouseMove:b,onPointerLeave:j,onMouseLeave:j},theirProps:a,slot:E,defaultTag:dD,name:"Menu.Item"})}let hD=br(aD),mD=br(sD),pD=br(uD),gD=br(fD),ol=Object.assign(hD,{Button:mD,Items:pD,Item:gD});function vD(e=0){let[t,n]=N.useState(e),r=n0(),i=N.useCallback(u=>{r.current&&n(d=>d|u)},[t,r]),a=N.useCallback(u=>!!(t&u),[t]),s=N.useCallback(u=>{r.current&&n(d=>d&~u)},[n,r]),c=N.useCallback(u=>{r.current&&n(d=>d^u)},[n]);return{flags:t,addFlag:i,hasFlag:a,removeFlag:s,toggleFlag:c}}function yD({onFocus:e}){let[t,n]=N.useState(!0),r=n0();return t?H.createElement(gA,{as:"button",type:"button",features:pA.Focusable,onFocus:i=>{i.preventDefault();let a,s=50;function c(){if(s--<=0){a&&cancelAnimationFrame(a);return}if(e()){if(cancelAnimationFrame(a),!r.current)return;n(!1);return}a=requestAnimationFrame(c)}a=requestAnimationFrame(c)}}):null}const yA=N.createContext(null);function xD(){return{groups:new Map,get(e,t){var n;let r=this.groups.get(e);r||(r=new Map,this.groups.set(e,r));let i=(n=r.get(t))!=null?n:0;r.set(t,i+1);let a=Array.from(r.keys()).indexOf(t);function s(){let c=r.get(t);c>1?r.set(t,c-1):r.delete(t)}return[a,s]}}}function bD({children:e}){let t=N.useRef(xD());return N.createElement(yA.Provider,{value:t},e)}function xA(e){let t=N.useContext(yA);if(!t)throw new Error("You must wrap your component in a ");let n=wD(),[r,i]=t.current.get(e,n);return N.useEffect(()=>i,[]),r}function wD(){var e,t,n;let r=(n=(t=(e=N.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED)==null?void 0:e.ReactCurrentOwner)==null?void 0:t.current)!=null?n:null;if(!r)return Symbol();let i=[],a=r;for(;a;)i.push(a.index),a=a.return;return"$."+i.join(".")}var _D=(e=>(e[e.Forwards=0]="Forwards",e[e.Backwards=1]="Backwards",e))(_D||{}),jD=(e=>(e[e.Less=-1]="Less",e[e.Equal=0]="Equal",e[e.Greater=1]="Greater",e))(jD||{}),ND=(e=>(e[e.SetSelectedIndex=0]="SetSelectedIndex",e[e.RegisterTab=1]="RegisterTab",e[e.UnregisterTab=2]="UnregisterTab",e[e.RegisterPanel=3]="RegisterPanel",e[e.UnregisterPanel=4]="UnregisterPanel",e))(ND||{});let SD={0(e,t){var n;let r=is(e.tabs,h=>h.current),i=is(e.panels,h=>h.current),a=r.filter(h=>{var m;return!((m=h.current)!=null&&m.hasAttribute("disabled"))}),s={...e,tabs:r,panels:i};if(t.index<0||t.index>r.length-1){let h=tn(Math.sign(t.index-e.selectedIndex),{[-1]:()=>1,0:()=>tn(Math.sign(t.index),{[-1]:()=>0,0:()=>0,1:()=>1}),1:()=>0});if(a.length===0)return s;let m=tn(h,{0:()=>r.indexOf(a[0]),1:()=>r.indexOf(a[a.length-1])});return{...s,selectedIndex:m===-1?e.selectedIndex:m}}let c=r.slice(0,t.index),u=[...r.slice(t.index),...c].find(h=>a.includes(h));if(!u)return s;let d=(n=r.indexOf(u))!=null?n:e.selectedIndex;return d===-1&&(d=e.selectedIndex),{...s,selectedIndex:d}},1(e,t){if(e.tabs.includes(t.tab))return e;let n=e.tabs[e.selectedIndex],r=is([...e.tabs,t.tab],a=>a.current),i=e.selectedIndex;return e.info.current.isControlled||(i=r.indexOf(n),i===-1&&(i=e.selectedIndex)),{...e,tabs:r,selectedIndex:i}},2(e,t){return{...e,tabs:e.tabs.filter(n=>n!==t.tab)}},3(e,t){return e.panels.includes(t.panel)?e:{...e,panels:is([...e.panels,t.panel],n=>n.current)}},4(e,t){return{...e,panels:e.panels.filter(n=>n!==t.panel)}}},H_=N.createContext(null);H_.displayName="TabsDataContext";function uc(e){let t=N.useContext(H_);if(t===null){let n=new Error(`<${e} /> is missing a parent component.`);throw Error.captureStackTrace&&Error.captureStackTrace(n,uc),n}return t}let V_=N.createContext(null);V_.displayName="TabsActionsContext";function q_(e){let t=N.useContext(V_);if(t===null){let n=new Error(`<${e} /> is missing a parent component.`);throw Error.captureStackTrace&&Error.captureStackTrace(n,q_),n}return t}function PD(e,t){return tn(t.type,SD,e,t)}let ED=N.Fragment;function OD(e,t){let{defaultIndex:n=0,vertical:r=!1,manual:i=!1,onChange:a,selectedIndex:s=null,...c}=e;const u=r?"vertical":"horizontal",d=i?"manual":"auto";let h=s!==null,m=Lr({isControlled:h}),p=pi(t),[v,_]=N.useReducer(PD,{info:m,selectedIndex:s??n,tabs:[],panels:[]}),x=N.useMemo(()=>({selectedIndex:v.selectedIndex}),[v.selectedIndex]),y=Lr(a||(()=>{})),w=Lr(v.tabs),b=N.useMemo(()=>({orientation:u,activation:d,...v}),[u,d,v]),j=Ke(T=>(_({type:1,tab:T}),()=>_({type:2,tab:T}))),E=Ke(T=>(_({type:3,panel:T}),()=>_({type:4,panel:T}))),P=Ke(T=>{O.current!==T&&y.current(T),h||_({type:0,index:T})}),O=Lr(h?e.selectedIndex:v.selectedIndex),C=N.useMemo(()=>({registerTab:j,registerPanel:E,change:P}),[]);jn(()=>{_({type:0,index:s??n})},[s]),jn(()=>{if(O.current===void 0||v.tabs.length<=0)return;let T=is(v.tabs,$=>$.current);T.some(($,z)=>v.tabs[z]!==$)&&P(T.indexOf(v.tabs[O.current]))});let A={ref:p};return H.createElement(bD,null,H.createElement(V_.Provider,{value:C},H.createElement(H_.Provider,{value:b},b.tabs.length<=0&&H.createElement(yD,{onFocus:()=>{var T,$;for(let z of w.current)if(((T=z.current)==null?void 0:T.tabIndex)===0)return($=z.current)==null||$.focus(),!0;return!1}}),Hr({ourProps:A,theirProps:c,slot:x,defaultTag:ED,name:"Tabs"}))))}let kD="div";function CD(e,t){let{orientation:n,selectedIndex:r}=uc("Tab.List"),i=pi(t);return Hr({ourProps:{ref:i,role:"tablist","aria-orientation":n},theirProps:e,slot:{selectedIndex:r},defaultTag:kD,name:"Tabs.List"})}let AD="button";function TD(e,t){var n,r;let i=Vf(),{id:a=`headlessui-tabs-tab-${i}`,...s}=e,{orientation:c,activation:u,selectedIndex:d,tabs:h,panels:m}=uc("Tab"),p=q_("Tab"),v=uc("Tab"),_=N.useRef(null),x=pi(_,t);jn(()=>p.registerTab(_),[p,_]);let y=xA("tabs"),w=h.indexOf(_);w===-1&&(w=y);let b=w===d,j=Ke($=>{var z;let D=$();if(D===Zu.Success&&u==="auto"){let Z=(z=qf(_))==null?void 0:z.activeElement,I=v.tabs.findIndex(F=>F.current===Z);I!==-1&&p.change(I)}return D}),E=Ke($=>{let z=h.map(D=>D.current).filter(Boolean);if($.key===nt.Space||$.key===nt.Enter){$.preventDefault(),$.stopPropagation(),p.change(w);return}switch($.key){case nt.Home:case nt.PageUp:return $.preventDefault(),$.stopPropagation(),j(()=>Go(z,rr.First));case nt.End:case nt.PageDown:return $.preventDefault(),$.stopPropagation(),j(()=>Go(z,rr.Last))}if(j(()=>tn(c,{vertical(){return $.key===nt.ArrowUp?Go(z,rr.Previous|rr.WrapAround):$.key===nt.ArrowDown?Go(z,rr.Next|rr.WrapAround):Zu.Error},horizontal(){return $.key===nt.ArrowLeft?Go(z,rr.Previous|rr.WrapAround):$.key===nt.ArrowRight?Go(z,rr.Next|rr.WrapAround):Zu.Error}}))===Zu.Success)return $.preventDefault()}),P=N.useRef(!1),O=Ke(()=>{var $;P.current||(P.current=!0,($=_.current)==null||$.focus({preventScroll:!0}),p.change(w),uA(()=>{P.current=!1}))}),C=Ke($=>{$.preventDefault()}),A=N.useMemo(()=>{var $;return{selected:b,disabled:($=e.disabled)!=null?$:!1}},[b,e.disabled]),T={ref:x,onKeyDown:E,onMouseDown:C,onClick:O,id:a,role:"tab",type:hA(e,_),"aria-controls":(r=(n=m[w])==null?void 0:n.current)==null?void 0:r.id,"aria-selected":b,tabIndex:b?0:-1};return Hr({ourProps:T,theirProps:s,slot:A,defaultTag:AD,name:"Tabs.Tab"})}let MD="div";function LD(e,t){let{selectedIndex:n}=uc("Tab.Panels"),r=pi(t),i=N.useMemo(()=>({selectedIndex:n}),[n]);return Hr({ourProps:{ref:r},theirProps:e,slot:i,defaultTag:MD,name:"Tabs.Panels"})}let $D="div",ID=cc.RenderStrategy|cc.Static;function RD(e,t){var n,r,i,a;let s=Vf(),{id:c=`headlessui-tabs-panel-${s}`,tabIndex:u=0,...d}=e,{selectedIndex:h,tabs:m,panels:p}=uc("Tab.Panel"),v=q_("Tab.Panel"),_=N.useRef(null),x=pi(_,t);jn(()=>v.registerPanel(_),[v,_,c]);let y=xA("panels"),w=p.indexOf(_);w===-1&&(w=y);let b=w===h,j=N.useMemo(()=>({selected:b}),[b]),E={ref:x,id:c,role:"tabpanel","aria-labelledby":(r=(n=m[w])==null?void 0:n.current)==null?void 0:r.id,tabIndex:b?u:-1};return!b&&((i=d.unmount)==null||i)&&!((a=d.static)!=null&&a)?H.createElement(gA,{as:"span","aria-hidden":"true",...E}):Hr({ourProps:E,theirProps:d,slot:j,defaultTag:$D,features:ID,visible:b,name:"Tabs.Panel"})}let FD=br(TD),DD=br(OD),BD=br(CD),zD=br(LD),UD=br(RD),wt=Object.assign(FD,{Group:DD,List:BD,Panels:zD,Panel:UD});function WD(e){let t={called:!1};return(...n)=>{if(!t.called)return t.called=!0,e(...n)}}function py(e,...t){e&&t.length>0&&e.classList.add(...t)}function gy(e,...t){e&&t.length>0&&e.classList.remove(...t)}function HD(e,t){let n=Pa();if(!e)return n.dispose;let{transitionDuration:r,transitionDelay:i}=getComputedStyle(e),[a,s]=[r,i].map(u=>{let[d=0]=u.split(",").filter(Boolean).map(h=>h.includes("ms")?parseFloat(h):parseFloat(h)*1e3).sort((h,m)=>m-h);return d}),c=a+s;if(c!==0){n.group(d=>{d.setTimeout(()=>{t(),d.dispose()},c),d.addEventListener(e,"transitionrun",h=>{h.target===h.currentTarget&&d.dispose()})});let u=n.addEventListener(e,"transitionend",d=>{d.target===d.currentTarget&&(t(),u())})}else t();return n.add(()=>t()),n.dispose}function VD(e,t,n,r){let i=n?"enter":"leave",a=Pa(),s=r!==void 0?WD(r):()=>{};i==="enter"&&(e.removeAttribute("hidden"),e.style.display="");let c=tn(i,{enter:()=>t.enter,leave:()=>t.leave}),u=tn(i,{enter:()=>t.enterTo,leave:()=>t.leaveTo}),d=tn(i,{enter:()=>t.enterFrom,leave:()=>t.leaveFrom});return gy(e,...t.base,...t.enter,...t.enterTo,...t.enterFrom,...t.leave,...t.leaveFrom,...t.leaveTo,...t.entered),py(e,...t.base,...c,...d),a.nextFrame(()=>{gy(e,...t.base,...c,...d),py(e,...t.base,...c,...u),HD(e,()=>(gy(e,...t.base,...c),py(e,...t.base,...t.entered),s()))}),a.dispose}function qD({immediate:e,container:t,direction:n,classes:r,onStart:i,onStop:a}){let s=n0(),c=t0(),u=Lr(n);jn(()=>{e&&(u.current="enter")},[e]),jn(()=>{let d=Pa();c.add(d.dispose);let h=t.current;if(h&&u.current!=="idle"&&s.current)return d.dispose(),i.current(u.current),d.add(VD(h,r.current,u.current==="enter",()=>{d.dispose(),a.current(u.current)})),d.dispose},[n])}function Ua(e=""){return e.split(/\s+/).filter(t=>t.length>1)}let i0=N.createContext(null);i0.displayName="TransitionContext";var ZD=(e=>(e.Visible="visible",e.Hidden="hidden",e))(ZD||{});function GD(){let e=N.useContext(i0);if(e===null)throw new Error("A is used but it is missing a parent or .");return e}function KD(){let e=N.useContext(a0);if(e===null)throw new Error("A is used but it is missing a parent or .");return e}let a0=N.createContext(null);a0.displayName="NestingContext";function o0(e){return"children"in e?o0(e.children):e.current.filter(({el:t})=>t.current!==null).filter(({state:t})=>t==="visible").length>0}function bA(e,t){let n=Lr(e),r=N.useRef([]),i=n0(),a=t0(),s=Ke((v,_=uo.Hidden)=>{let x=r.current.findIndex(({el:y})=>y===v);x!==-1&&(tn(_,{[uo.Unmount](){r.current.splice(x,1)},[uo.Hidden](){r.current[x].state="hidden"}}),a.microTask(()=>{var y;!o0(r)&&i.current&&((y=n.current)==null||y.call(n))}))}),c=Ke(v=>{let _=r.current.find(({el:x})=>x===v);return _?_.state!=="visible"&&(_.state="visible"):r.current.push({el:v,state:"visible"}),()=>s(v,uo.Unmount)}),u=N.useRef([]),d=N.useRef(Promise.resolve()),h=N.useRef({enter:[],leave:[],idle:[]}),m=Ke((v,_,x)=>{u.current.splice(0),t&&(t.chains.current[_]=t.chains.current[_].filter(([y])=>y!==v)),t==null||t.chains.current[_].push([v,new Promise(y=>{u.current.push(y)})]),t==null||t.chains.current[_].push([v,new Promise(y=>{Promise.all(h.current[_].map(([w,b])=>b)).then(()=>y())})]),_==="enter"?d.current=d.current.then(()=>t==null?void 0:t.wait.current).then(()=>x(_)):x(_)}),p=Ke((v,_,x)=>{Promise.all(h.current[_].splice(0).map(([y,w])=>w)).then(()=>{var y;(y=u.current.shift())==null||y()}).then(()=>x(_))});return N.useMemo(()=>({children:r,register:c,unregister:s,onStart:m,onStop:p,wait:d,chains:h}),[c,s,r,m,p,h,d])}function YD(){}let XD=["beforeEnter","afterEnter","beforeLeave","afterLeave"];function b5(e){var t;let n={};for(let r of XD)n[r]=(t=e[r])!=null?t:YD;return n}function QD(e){let t=N.useRef(b5(e));return N.useEffect(()=>{t.current=b5(e)},[e]),t}let JD="div",wA=cc.RenderStrategy;function eB(e,t){var n,r;let{beforeEnter:i,afterEnter:a,beforeLeave:s,afterLeave:c,enter:u,enterFrom:d,enterTo:h,entered:m,leave:p,leaveFrom:v,leaveTo:_,...x}=e,y=N.useRef(null),w=pi(y,t),b=(n=x.unmount)==null||n?uo.Unmount:uo.Hidden,{show:j,appear:E,initial:P}=GD(),[O,C]=N.useState(j?"visible":"hidden"),A=KD(),{register:T,unregister:$}=A;N.useEffect(()=>T(y),[T,y]),N.useEffect(()=>{if(b===uo.Hidden&&y.current){if(j&&O!=="visible"){C("visible");return}return tn(O,{hidden:()=>$(y),visible:()=>T(y)})}},[O,y,T,$,j,b]);let z=Lr({base:Ua(x.className),enter:Ua(u),enterFrom:Ua(d),enterTo:Ua(h),entered:Ua(m),leave:Ua(p),leaveFrom:Ua(v),leaveTo:Ua(_)}),D=QD({beforeEnter:i,afterEnter:a,beforeLeave:s,afterLeave:c}),Z=F_();N.useEffect(()=>{if(Z&&O==="visible"&&y.current===null)throw new Error("Did you forget to passthrough the `ref` to the actual DOM node?")},[y,O,Z]);let I=P&&!E,F=E&&j&&P,B=!Z||I?"idle":j?"enter":"leave",G=vD(0),R=Ke(ae=>tn(ae,{enter:()=>{G.addFlag(ur.Opening),D.current.beforeEnter()},leave:()=>{G.addFlag(ur.Closing),D.current.beforeLeave()},idle:()=>{}})),K=Ke(ae=>tn(ae,{enter:()=>{G.removeFlag(ur.Opening),D.current.afterEnter()},leave:()=>{G.removeFlag(ur.Closing),D.current.afterLeave()},idle:()=>{}})),W=bA(()=>{C("hidden"),$(y)},A),U=N.useRef(!1);qD({immediate:F,container:y,classes:z,direction:B,onStart:Lr(ae=>{U.current=!0,W.onStart(y,ae,R)}),onStop:Lr(ae=>{U.current=!1,W.onStop(y,ae,K),ae==="leave"&&!o0(W)&&(C("hidden"),$(y))})});let Y=x,ne={ref:w};return F?Y={...Y,className:up(x.className,...z.current.enter,...z.current.enterFrom)}:U.current&&(Y.className=up(x.className,(r=y.current)==null?void 0:r.className),Y.className===""&&delete Y.className),H.createElement(a0.Provider,{value:W},H.createElement(vA,{value:tn(O,{visible:ur.Open,hidden:ur.Closed})|G.flags},Hr({ourProps:ne,theirProps:Y,defaultTag:JD,features:wA,visible:O==="visible",name:"Transition.Child"})))}function tB(e,t){let{show:n,appear:r=!1,unmount:i=!0,...a}=e,s=N.useRef(null),c=pi(s,t);F_();let u=U_();if(n===void 0&&u!==null&&(n=(u&ur.Open)===ur.Open),![!0,!1].includes(n))throw new Error("A is used but it is missing a `show={true | false}` prop.");let[d,h]=N.useState(n?"visible":"hidden"),m=bA(()=>{h("hidden")}),[p,v]=N.useState(!0),_=N.useRef([n]);jn(()=>{p!==!1&&_.current[_.current.length-1]!==n&&(_.current.push(n),v(!1))},[_,n]);let x=N.useMemo(()=>({show:n,appear:r,initial:p}),[n,r,p]);N.useEffect(()=>{if(n)h("visible");else if(!o0(m))h("hidden");else{let j=s.current;if(!j)return;let E=j.getBoundingClientRect();E.x===0&&E.y===0&&E.width===0&&E.height===0&&h("hidden")}},[n,m]);let y={unmount:i},w=Ke(()=>{var j;p&&v(!1),(j=e.beforeEnter)==null||j.call(e)}),b=Ke(()=>{var j;p&&v(!1),(j=e.beforeLeave)==null||j.call(e)});return H.createElement(a0.Provider,{value:m},H.createElement(i0.Provider,{value:x},Hr({ourProps:{...y,as:N.Fragment,children:H.createElement(_A,{ref:c,...y,...a,beforeEnter:w,beforeLeave:b})},theirProps:{},defaultTag:N.Fragment,features:wA,visible:d==="visible",name:"Transition"})))}function nB(e,t){let n=N.useContext(i0)!==null,r=U_()!==null;return H.createElement(H.Fragment,null,!n&&r?H.createElement(vb,{ref:t,...e}):H.createElement(_A,{ref:t,...e}))}let vb=br(tB),_A=br(eB),rB=br(nB),iB=Object.assign(vb,{Child:rB,Root:vb});function aB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M4.26 10.147a60.438 60.438 0 0 0-.491 6.347A48.62 48.62 0 0 1 12 20.904a48.62 48.62 0 0 1 8.232-4.41 60.46 60.46 0 0 0-.491-6.347m-15.482 0a50.636 50.636 0 0 0-2.658-.813A59.906 59.906 0 0 1 12 3.493a59.903 59.903 0 0 1 10.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.717 50.717 0 0 1 12 13.489a50.702 50.702 0 0 1 7.74-3.342M6.75 15a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm0 0v-3.675A55.378 55.378 0 0 1 12 8.443m-7.007 11.55A5.981 5.981 0 0 0 6.75 15.75v-1.5"}))}const Eo=N.forwardRef(aB);function oB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75"}))}const jA=N.forwardRef(oB);function sB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"}))}const w5=N.forwardRef(sB);function lB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9"}))}const cB=N.forwardRef(lB);function uB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"}))}const Nl=N.forwardRef(uB);function dB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"}))}const yb=N.forwardRef(dB);function fB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M2.25 18 9 11.25l4.306 4.306a11.95 11.95 0 0 1 5.814-5.518l2.74-1.22m0 0-5.94-2.281m5.94 2.28-2.28 5.941"}))}const hB=N.forwardRef(fB);function mB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"}))}const NA=N.forwardRef(mB);function pB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0M3.124 7.5A8.969 8.969 0 0 1 5.292 3m13.416 0a8.969 8.969 0 0 1 2.168 4.5"}))}const Hd=N.forwardRef(pB);function gB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"}))}const Zf=N.forwardRef(gB);function vB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M20.25 14.15v4.25c0 1.094-.787 2.036-1.872 2.18-2.087.277-4.216.42-6.378.42s-4.291-.143-6.378-.42c-1.085-.144-1.872-1.086-1.872-2.18v-4.25m16.5 0a2.18 2.18 0 0 0 .75-1.661V8.706c0-1.081-.768-2.015-1.837-2.175a48.114 48.114 0 0 0-3.413-.387m4.5 8.006c-.194.165-.42.295-.673.38A23.978 23.978 0 0 1 12 15.75c-2.648 0-5.195-.429-7.577-1.22a2.016 2.016 0 0 1-.673-.38m0 0A2.18 2.18 0 0 1 3 12.489V8.706c0-1.081.768-2.015 1.837-2.175a48.111 48.111 0 0 1 3.413-.387m7.5 0V5.25A2.25 2.25 0 0 0 13.5 3h-3a2.25 2.25 0 0 0-2.25 2.25v.894m7.5 0a48.667 48.667 0 0 0-7.5 0M12 12.75h.008v.008H12v-.008Z"}))}const SA=N.forwardRef(vB);function yB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M12 21v-8.25M15.75 21v-8.25M8.25 21v-8.25M3 9l9-6 9 6m-1.5 12V10.332A48.36 48.36 0 0 0 12 9.75c-2.551 0-5.056.2-7.5.582V21M3 21h18M12 6.75h.008v.008H12V6.75Z"}))}const dc=N.forwardRef(yB);function xB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m3-6H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21"}))}const Ci=N.forwardRef(xB);function bB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5"}))}const ha=N.forwardRef(bB);function wB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z"}))}const fc=N.forwardRef(wB);function _B({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155"}))}const jB=N.forwardRef(_B);function NB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.39-1.593 3.068a3.745 3.745 0 0 1-1.043 3.296 3.745 3.745 0 0 1-3.296 1.043A3.745 3.745 0 0 1 12 21c-1.268 0-2.39-.63-3.068-1.593a3.746 3.746 0 0 1-3.296-1.043 3.745 3.745 0 0 1-1.043-3.296A3.745 3.745 0 0 1 3 12c0-1.268.63-2.39 1.593-3.068a3.745 3.745 0 0 1 1.043-3.296 3.746 3.746 0 0 1 3.296-1.043A3.746 3.746 0 0 1 12 3c1.268 0 2.39.63 3.068 1.593a3.746 3.746 0 0 1 3.296 1.043 3.746 3.746 0 0 1 1.043 3.296A3.745 3.745 0 0 1 21 12Z"}))}const SB=N.forwardRef(NB);function PB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"}))}const ci=N.forwardRef(PB);function EB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"m4.5 12.75 6 6 9-13.5"}))}const xb=N.forwardRef(EB);function OB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"m19.5 8.25-7.5 7.5-7.5-7.5"}))}const Vd=N.forwardRef(OB);function kB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"m4.5 15.75 7.5-7.5 7.5 7.5"}))}const bb=N.forwardRef(kB);function CB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125"}))}const _5=N.forwardRef(CB);function AB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"}))}const j5=N.forwardRef(AB);function TB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5"}))}const $r=N.forwardRef(TB);function MB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"}),N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"}))}const Z_=N.forwardRef(MB);function LB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"}))}const $B=N.forwardRef(LB);function IB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9"}))}const RB=N.forwardRef(IB);function FB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M12 6v12m-3-2.818.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"}))}const PA=N.forwardRef(FB);function DB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"}))}const BB=N.forwardRef(DB);function zB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v16.5c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Zm3.75 11.625a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"}))}const UB=N.forwardRef(zB);function WB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z"}))}const Ws=N.forwardRef(WB);function HB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"}))}const N5=N.forwardRef(HB);function VB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"}))}const qB=N.forwardRef(VB);function ZB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"}))}const wb=N.forwardRef(ZB);function GB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"}))}const $t=N.forwardRef(GB);function KB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205 3 1m1.5.5-1.5-.5M6.75 7.364V3h-3v18m3-13.636 10.5-3.819"}))}const YB=N.forwardRef(KB);function XB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"}))}const G_=N.forwardRef(XB);function QB({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 1 0-7.517 0c.85.493 1.509 1.333 1.509 2.316V18"}))}const JB=N.forwardRef(QB);function ez({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"}))}const tz=N.forwardRef(ez);function nz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"}))}const fn=N.forwardRef(nz);function rz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"}),N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z"}))}const Rn=N.forwardRef(rz);function iz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M9 6.75V15m6-6v8.25m.503 3.498 4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 0 0-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0Z"}))}const la=N.forwardRef(iz);function az({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 1 1 0-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 0 1-1.44-4.282m3.102.069a18.03 18.03 0 0 1-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 0 1 8.835 2.535M10.34 6.66a23.847 23.847 0 0 0 8.835-2.535m0 0A23.74 23.74 0 0 0 18.795 3m.38 1.125a23.91 23.91 0 0 1 1.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 0 0 1.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 0 1 0 3.46"}))}const oz=N.forwardRef(az);function sz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M12 18.75a6 6 0 0 0 6-6v-1.5m-6 7.5a6 6 0 0 1-6-6v-1.5m6 7.5v3.75m-3.75 0h7.5M12 15.75a3 3 0 0 1-3-3V4.5a3 3 0 1 1 6 0v8.25a3 3 0 0 1-3 3Z"}))}const _b=N.forwardRef(sz);function lz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 0 0 2.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.44-.11-.902.055-1.173.417l-.97 1.293c-.282.376-.769.542-1.21.38a12.035 12.035 0 0 1-7.143-7.143c-.162-.441.004-.928.38-1.21l1.293-.97c.363-.271.527-.734.417-1.173L6.963 3.102a1.125 1.125 0 0 0-1.091-.852H4.5A2.25 2.25 0 0 0 2.25 4.5v2.25Z"}))}const EA=N.forwardRef(lz);function cz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"}))}const OA=N.forwardRef(cz);function uz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M15.59 14.37a6 6 0 0 1-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 0 0 6.16-12.12A14.98 14.98 0 0 0 9.631 8.41m5.96 5.96a14.926 14.926 0 0 1-5.841 2.58m-.119-8.54a6 6 0 0 0-7.381 5.84h4.8m2.581-5.84a14.927 14.927 0 0 0-2.58 5.84m2.699 2.7c-.103.021-.207.041-.311.06a15.09 15.09 0 0 1-2.448-2.448 14.9 14.9 0 0 1 .06-.312m-2.24 2.39a4.493 4.493 0 0 0-1.757 4.306 4.493 4.493 0 0 0 4.306-1.758M16.5 9a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z"}))}const jo=N.forwardRef(uz);function dz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M12 3v17.25m0 0c-1.472 0-2.882.265-4.185.75M12 20.25c1.472 0 2.882.265 4.185.75M18.75 4.97A48.416 48.416 0 0 0 12 4.5c-2.291 0-4.545.16-6.75.47m13.5 0c1.01.143 2.01.317 3 .52m-3-.52 2.62 10.726c.122.499-.106 1.028-.589 1.202a5.988 5.988 0 0 1-2.031.352 5.988 5.988 0 0 1-2.031-.352c-.483-.174-.711-.703-.59-1.202L18.75 4.971Zm-16.5.52c.99-.203 1.99-.377 3-.52m0 0 2.62 10.726c.122.499-.106 1.028-.589 1.202a5.989 5.989 0 0 1-2.031.352 5.989 5.989 0 0 1-2.031-.352c-.483-.174-.711-.703-.59-1.202L5.25 4.971Z"}))}const fz=N.forwardRef(dz);function hz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M21.75 17.25v-.228a4.5 4.5 0 0 0-.12-1.03l-2.268-9.64a3.375 3.375 0 0 0-3.285-2.602H7.923a3.375 3.375 0 0 0-3.285 2.602l-2.268 9.64a4.5 4.5 0 0 0-.12 1.03v.228m19.5 0a3 3 0 0 1-3 3H5.25a3 3 0 0 1-3-3m19.5 0a3 3 0 0 0-3-3H5.25a3 3 0 0 0-3 3m16.5 0h.008v.008h-.008v-.008Zm-3 0h.008v.008h-.008v-.008Z"}))}const mz=N.forwardRef(hz);function pz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z"}))}const gz=N.forwardRef(pz);function vz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z"}))}const kA=N.forwardRef(vz);function yz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z"}))}const CA=N.forwardRef(yz);function xz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z"}))}const bz=N.forwardRef(xz);function wz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M16.5 18.75h-9m9 0a3 3 0 0 1 3 3h-15a3 3 0 0 1 3-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 0 1-.982-3.172M9.497 14.25a7.454 7.454 0 0 0 .981-3.172M5.25 4.236c-.982.143-1.954.317-2.916.52A6.003 6.003 0 0 0 7.73 9.728M5.25 4.236V4.5c0 2.108.966 3.99 2.48 5.228M5.25 4.236V2.721C7.456 2.41 9.71 2.25 12 2.25c2.291 0 4.545.16 6.75.47v1.516M7.73 9.728a6.726 6.726 0 0 0 2.748 1.35m8.272-6.842V4.5c0 2.108-.966 3.99-2.48 5.228m2.48-5.492a46.32 46.32 0 0 1 2.916.52 6.003 6.003 0 0 1-5.395 4.972m0 0a6.726 6.726 0 0 1-2.749 1.35m0 0a6.772 6.772 0 0 1-3.044 0"}))}const _z=N.forwardRef(wz);function jz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M8.25 18.75a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 0 1-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m3 0h1.125c.621 0 1.129-.504 1.09-1.124a17.902 17.902 0 0 0-3.213-9.193 2.056 2.056 0 0 0-1.58-.86H14.25M16.5 18.75h-2.25m0-11.177v-.958c0-.568-.422-1.048-.987-1.106a48.554 48.554 0 0 0-10.026 0 1.106 1.106 0 0 0-.987 1.106v7.635m12-6.677v6.677m0 4.5v-4.5m0 0h-12"}))}const Nz=N.forwardRef(jz);function Sz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"}))}const jb=N.forwardRef(Sz);function Pz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z"}))}const Ur=N.forwardRef(Pz);function Ez({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M22 10.5h-6m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM4 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 10.374 21c-2.331 0-4.512-.645-6.374-1.766Z"}))}const S5=N.forwardRef(Ez);function Oz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"}))}const Nb=N.forwardRef(Oz);function kz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"}))}const Li=N.forwardRef(kz);function Cz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"}))}const Az=N.forwardRef(Cz);function Tz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z"}))}const AA=N.forwardRef(Tz);function Mz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"}))}const Sb=N.forwardRef(Mz);function Lz({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M6 18 18 6M6 6l12 12"}))}const Cr=N.forwardRef(Lz),$z=[{name:"Home",href:"/",icon:G_},{name:"Explore Data",href:"/explore",icon:fn},{name:"Search",href:"/search",icon:fn},{name:"Jurisdictions",href:"/jurisdictions",icon:Rn},{section:"Families & Individuals",items:[{name:"Community Events",href:"/events",icon:Zf},{name:"Services & Resources",href:"/services",icon:$t}]},{section:"Policy & Government",items:[{name:"Policy Decisions",href:"/documents",icon:Ws},{name:"Budget Analysis",href:"/analytics",icon:fc},{name:"Elected Officials",href:"/people",icon:Ur},{name:"Policy Map",href:"/policy-map",icon:la}]},{section:"Community & Advocacy",items:[{name:"Nonprofits",href:"/nonprofits",icon:dc},{name:"Advocacy Topics",href:"/advocacy-topics",icon:Hd},{name:"Fact-Checking",href:"/fact-checking",icon:Eo}]},{section:"Developers",items:[{name:"Open Source",href:"/opensource",icon:$r},{name:"Hackathons",href:"/hackathons",icon:Eo}]},{name:"Settings",href:"/settings",icon:Z_}];function P5(){const e=zs(),t=qc(),[n,r]=N.useState(""),[i,a]=N.useState(!1),[s,c]=N.useState(!1),{user:u,isAuthenticated:d,login:h,logout:m,isLoading:p}=e0(),{location:v,hasLocation:_}=Hf(),x="https://www.communityone.com/docs/intro",y="https://www.communityone.com/api/docs",w=b=>{b.preventDefault(),n.trim()&&t(`/search?q=${encodeURIComponent(n)}`)};return o.jsxs("div",{className:"min-h-screen",style:{backgroundColor:"#F1F5F9"},children:[o.jsx("div",{className:"fixed top-0 left-0 right-0 bg-white border-b border-gray-200 z-50",children:o.jsxs("div",{className:"flex items-center justify-between px-4 md:px-6 py-3",children:[o.jsxs("div",{className:"flex items-center gap-3",children:[o.jsx("button",{onClick:()=>a(!i),className:"md:hidden p-2 rounded-lg hover:bg-gray-100 text-gray-700","aria-label":"Toggle menu",children:i?o.jsx(Cr,{className:"h-6 w-6"}):o.jsx(NA,{className:"h-6 w-6"})}),o.jsxs(ke,{to:"/",className:"flex items-center gap-2 md:gap-3",children:[o.jsx("img",{src:"/communityone_logo.svg",alt:"CommunityOne Logo",className:"h-10 md:h-12"}),o.jsx("h1",{className:"text-lg md:text-2xl font-bold",style:{color:"#354F52"},children:"Open Navigator"})]})]}),e.pathname!=="/"&&o.jsx("form",{onSubmit:w,className:"hidden md:flex flex-1 max-w-2xl mx-8",children:o.jsxs("div",{className:"relative w-full",children:[o.jsx("input",{type:"text",placeholder:"Search people, meetings, organizations, causes...",value:n,onChange:b=>r(b.target.value),className:"w-full px-4 py-2 pl-10 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"}),o.jsx(fn,{className:"absolute left-3 top-2.5 h-5 w-5 text-gray-400"})]})}),o.jsxs("div",{className:"flex items-center gap-2 md:gap-4",children:[_&&v&&o.jsxs("div",{className:"hidden lg:flex items-center gap-2 px-3 py-1.5 bg-primary-50 border border-primary-200 rounded-lg",children:[o.jsx(Rn,{className:"h-4 w-4 text-primary-600 flex-shrink-0"}),o.jsxs("div",{className:"text-xs",children:[o.jsxs("div",{className:"font-semibold text-gray-900",children:[v.city,", ",v.state]}),v.county&&o.jsx("div",{className:"text-gray-700",children:v.county})]}),o.jsx("button",{onClick:()=>t("/?tab=community"),className:"text-xs text-primary-600 hover:text-primary-700 font-medium underline ml-2 flex-shrink-0",children:"Change"})]}),p?o.jsx("div",{className:"px-3 py-2",children:o.jsx("div",{className:"animate-spin h-8 w-8 border-3 border-gray-300 border-t-primary-600 rounded-full"})}):d&&u?o.jsxs(ol,{as:"div",className:"relative",children:[o.jsxs(ol.Button,{className:"flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors",children:[u.avatar_url?o.jsx("img",{src:u.avatar_url,alt:u.full_name||u.email,className:"h-9 w-9 rounded-full border-2 border-primary-500 shadow-sm",onError:b=>{b.currentTarget.style.display="none";const j=b.currentTarget.nextElementSibling;j&&(j.style.display="flex")}}):null,o.jsx("div",{className:"h-9 w-9 rounded-full bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center text-white font-bold text-sm shadow-sm",style:{display:u.avatar_url?"none":"flex"},children:(u.full_name||u.username||u.email).charAt(0).toUpperCase()}),o.jsx("span",{className:"hidden md:inline text-sm font-medium text-gray-700",children:u.full_name||u.username||u.email.split("@")[0]}),o.jsx(Vd,{className:"hidden md:block h-4 w-4 text-gray-600"})]}),o.jsx(iB,{as:N.Fragment,enter:"transition ease-out duration-100",enterFrom:"transform opacity-0 scale-95",enterTo:"transform opacity-100 scale-100",leave:"transition ease-in duration-75",leaveFrom:"transform opacity-100 scale-100",leaveTo:"transform opacity-0 scale-95",children:o.jsxs(ol.Items,{className:"absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 focus:outline-none z-50",children:[o.jsxs("div",{className:"px-4 py-3 border-b border-gray-200",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-2",children:[u.avatar_url?o.jsx("img",{src:u.avatar_url,alt:u.full_name||u.email,className:"h-12 w-12 rounded-full border-2 border-primary-500",onError:b=>{b.currentTarget.style.display="none";const j=b.currentTarget.nextElementSibling;j&&(j.style.display="flex")}}):null,o.jsx("div",{className:"h-12 w-12 rounded-full bg-gradient-to-br from-primary-500 to-primary-600 flex items-center justify-center text-white font-bold text-lg",style:{display:u.avatar_url?"none":"flex"},children:(u.full_name||u.username||u.email).charAt(0).toUpperCase()}),o.jsxs("div",{children:[o.jsx("p",{className:"text-sm font-semibold text-gray-900",children:u.full_name||u.username||u.email.split("@")[0]}),o.jsx("p",{className:"text-xs text-gray-500 truncate",children:u.email})]})]}),u.oauth_provider&&o.jsxs("div",{className:"flex items-center gap-1 text-xs text-gray-400",children:[o.jsx("span",{children:"Signed in via"}),o.jsx("span",{className:"font-medium capitalize",children:u.oauth_provider})]})]}),o.jsxs("div",{className:"py-1",children:[o.jsx(ol.Item,{children:({active:b})=>o.jsxs("button",{onClick:()=>t("/profile"),className:`${b?"bg-gray-50":""} flex items-center gap-3 w-full px-4 py-2.5 text-sm text-gray-700 hover:text-gray-900`,children:[o.jsx(jb,{className:"h-5 w-5"}),o.jsx("span",{children:"My Profile"})]})}),o.jsx(ol.Item,{children:({active:b})=>o.jsxs("button",{onClick:()=>t("/settings"),className:`${b?"bg-gray-50":""} flex items-center gap-3 w-full px-4 py-2.5 text-sm text-gray-700 hover:text-gray-900`,children:[o.jsx(Z_,{className:"h-5 w-5"}),o.jsx("span",{children:"Settings"})]})}),o.jsx(ol.Item,{children:({active:b})=>o.jsxs("button",{onClick:m,className:`${b?"bg-red-50":""} flex items-center gap-3 w-full px-4 py-2.5 text-sm text-red-600 hover:text-red-700 border-t border-gray-100 mt-1`,children:[o.jsx(cB,{className:"h-5 w-5"}),o.jsx("span",{className:"font-medium",children:"Sign out"})]})})]})]})})]}):o.jsxs("div",{className:"relative",children:[o.jsxs("button",{onClick:()=>c(!s),className:"px-3 md:px-4 py-2 text-white rounded-lg transition-colors text-sm md:text-base font-medium flex items-center gap-2",style:{backgroundColor:"#354F52"},onMouseEnter:b=>b.currentTarget.style.backgroundColor="#2e4346",onMouseLeave:b=>b.currentTarget.style.backgroundColor="#354F52",children:[o.jsx(jb,{className:"h-5 w-5"}),o.jsx("span",{className:"hidden md:inline",children:"Register"}),o.jsx(Vd,{className:"h-4 w-4"})]}),s&&o.jsxs("div",{className:"absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50",children:[o.jsx("div",{className:"px-4 py-2 border-b border-gray-200",children:o.jsx("p",{className:"text-sm font-medium text-gray-900",children:"Sign in with:"})}),o.jsxs("button",{onClick:()=>{h("google"),c(!1)},className:"flex items-center gap-3 w-full px-4 py-3 hover:bg-gray-100 transition-colors",children:[o.jsx("div",{className:"w-6 h-6 flex items-center justify-center",children:o.jsxs("svg",{viewBox:"0 0 24 24",className:"w-5 h-5",children:[o.jsx("path",{fill:"#4285F4",d:"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"}),o.jsx("path",{fill:"#34A853",d:"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"}),o.jsx("path",{fill:"#FBBC05",d:"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"}),o.jsx("path",{fill:"#EA4335",d:"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"})]})}),o.jsx("span",{className:"text-sm font-medium text-gray-700",children:"Google"})]}),o.jsxs("button",{onClick:()=>{h("facebook"),c(!1)},className:"flex items-center gap-3 w-full px-4 py-3 hover:bg-gray-100 transition-colors",children:[o.jsx("div",{className:"w-6 h-6 flex items-center justify-center",children:o.jsx("svg",{viewBox:"0 0 24 24",className:"w-5 h-5",fill:"#1877F2",children:o.jsx("path",{d:"M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"})})}),o.jsx("span",{className:"text-sm font-medium text-gray-700",children:"Facebook"})]}),o.jsxs("button",{onClick:()=>{h("github"),c(!1)},className:"flex items-center gap-3 w-full px-4 py-3 hover:bg-gray-100 transition-colors",children:[o.jsx("div",{className:"w-6 h-6 flex items-center justify-center",children:o.jsx("svg",{viewBox:"0 0 24 24",className:"w-5 h-5",fill:"#181717",children:o.jsx("path",{d:"M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z"})})}),o.jsx("span",{className:"text-sm font-medium text-gray-700",children:"GitHub"})]}),o.jsx("div",{className:"border-t border-gray-100 my-1"}),o.jsxs("button",{onClick:()=>{h("huggingface"),c(!1)},className:"flex items-center gap-3 w-full px-4 py-3 hover:bg-gray-100 transition-colors",children:[o.jsx("div",{className:"w-6 h-6 flex items-center justify-center",children:o.jsx("span",{className:"text-2xl",children:"🤗"})}),o.jsx("span",{className:"text-sm font-medium text-gray-700",children:"HuggingFace"})]})]})]}),o.jsxs("a",{href:x,target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-1 md:gap-2 px-2 md:px-4 py-2 text-gray-700 hover:text-primary-600 transition-colors",children:[o.jsx(Zf,{className:"h-5 w-5"}),o.jsx("span",{className:"hidden md:inline font-medium",children:"Docs"})]}),o.jsx("a",{href:y,target:"_blank",rel:"noopener noreferrer",className:"px-2 md:px-4 py-2 text-white rounded-lg transition-colors text-sm md:text-base",style:{backgroundColor:"#354F52"},onMouseEnter:b=>b.currentTarget.style.backgroundColor="#2e4346",onMouseLeave:b=>b.currentTarget.style.backgroundColor="#354F52",children:"API"})]})]})}),o.jsxs("div",{className:` + fixed top-16 inset-y-0 left-0 w-64 bg-white border-r border-gray-200 z-40 + transform transition-transform duration-200 ease-in-out + ${i?"translate-x-0":"-translate-x-full md:translate-x-0"} + `,children:[o.jsx("nav",{className:"mt-6 px-4 overflow-y-auto h-[calc(100vh-10rem)]",children:$z.map((b,j)=>{if("section"in b&&b.section&&b.items)return o.jsxs("div",{className:"mb-6",children:[o.jsx("div",{className:"px-4 py-2 text-xs font-semibold text-gray-500 uppercase tracking-wider",children:b.section}),b.items.map(E=>{const P=e.pathname===E.href,O="external"in E&&E.external,C=` + flex items-center gap-3 px-4 py-3 mb-1 rounded-lg transition-colors + ${P?"bg-primary-50 text-primary-700 font-medium":"text-gray-700 hover:bg-gray-100"} + `;return O?o.jsxs("a",{href:E.href,target:"_blank",rel:"noopener noreferrer",className:C,children:[o.jsx(E.icon,{className:"h-5 w-5"}),o.jsx("span",{className:"text-sm",children:E.name})]},E.name):o.jsxs(ke,{to:E.href,onClick:()=>a(!1),className:C,children:[o.jsx(E.icon,{className:"h-5 w-5"}),o.jsx("span",{className:"text-sm",children:E.name})]},E.name)})]},j);if("href"in b&&b.href){const E=e.pathname===b.href;return o.jsxs(ke,{to:b.href,onClick:()=>a(!1),className:` + flex items-center gap-3 px-4 py-3 mb-2 rounded-lg transition-colors + ${E?"bg-primary-50 text-primary-700 font-medium":"text-gray-700 hover:bg-gray-100"} + `,children:[o.jsx(b.icon,{className:"h-6 w-6"}),o.jsx("span",{children:b.name})]},b.name)}return null})}),o.jsx("div",{className:"absolute bottom-0 left-0 right-0 p-4 border-t border-gray-200 bg-white",children:o.jsxs("div",{className:"text-sm text-gray-600",children:[o.jsx("div",{className:"font-medium mb-1",children:"Open Data Sources"}),o.jsxs("div",{className:"text-xs",children:["• ",o.jsx(ke,{to:"/jurisdictions",className:"hover:text-primary-600 hover:underline",children:"925 Jurisdictions"}),o.jsx("br",{}),"• ",o.jsx(ke,{to:"/search?types=organizations",className:"hover:text-primary-600 hover:underline",children:"43,726 Nonprofits"}),o.jsx("br",{}),"• 6,913 Meeting Pages",o.jsx("br",{}),"• ",o.jsx(ke,{to:"/search?types=contacts",className:"hover:text-primary-600 hover:underline",children:"362 Officials"})]}),o.jsx("div",{className:"mt-3 pt-3 border-t border-gray-100",children:o.jsx(ke,{to:"/#contact",className:"text-xs text-primary-600 hover:text-primary-700 hover:underline font-medium",children:"📍 Request Jurisdiction Coverage"})})]})})]}),i&&o.jsx("div",{className:"fixed inset-0 bg-black bg-opacity-50 z-30 md:hidden",onClick:()=>a(!1)}),o.jsx("div",{className:"md:pl-64 pt-16",children:o.jsx("main",{children:o.jsx(N9,{})})})]})}let dp;dp="/api",console.log("🌐 [API] Production mode: HARDCODED relative path:",dp),console.log("🚨 [API] Ignoring all environment variables (nuclear option enabled)");console.log("📡 [API] Final base URL:",dp);console.log("🔒 [API] Page protocol:",typeof window<"u"?window.location.protocol:"N/A");class Iz{constructor(t){FN(this,"baseURL");this.baseURL=t}async request(t,n={}){const r=t.startsWith("http")?t:`${this.baseURL}${t}`;if(r.startsWith("http://")){const s=r.replace("http://","https://");throw console.error("❌ [API] BLOCKED insecure HTTP request in production:",r),console.error("❌ [API] This would cause Mixed Content errors"),console.error("❌ [API] Upgrading to HTTPS:",s),new Error(`BLOCKED: Attempted to make insecure HTTP request in production: ${r}`)}console.log("🔍 [FETCH] Request URL:",r),console.log("🔍 [FETCH] Method:",n.method||"GET");const i=localStorage.getItem("auth_token"),a={"Content-Type":"application/json",...n.headers};i&&(a.Authorization=`Bearer ${i}`);try{const s=await fetch(r,{...n,headers:a});s.status===401&&localStorage.removeItem("auth_token");let c;const u=s.headers.get("content-type");if(u&&u.includes("application/json")?c=await s.json():c=await s.text(),!s.ok)throw{response:{data:c,status:s.status,statusText:s.statusText},message:`HTTP ${s.status}: ${s.statusText}`};return console.log("✅ [FETCH] Success:",s.status),{data:c,status:s.status,statusText:s.statusText}}catch(s){throw console.error("❌ [FETCH] Error:",s),s}}async get(t,n){let r=t;if(n!=null&&n.params){const i=new URLSearchParams;Object.entries(n.params).forEach(([s,c])=>{c!=null&&i.append(s,String(c))});const a=i.toString();a&&(r=`${t}?${a}`)}return this.request(r,{method:"GET"})}async post(t,n){return this.request(t,{method:"POST",body:n?JSON.stringify(n):void 0})}async put(t,n){return this.request(t,{method:"PUT",body:n?JSON.stringify(n):void 0})}async delete(t){return this.request(t,{method:"DELETE"})}async patch(t,n){return this.request(t,{method:"PATCH",body:n?JSON.stringify(n):void 0})}}const vt=new Iz(dp);function K_({onLocationFound:e,initialAddress:t="",compact:n=!1}){const{clearLocation:r}=Hf(),[i,a]=N.useState(t),[s,c]=N.useState(!1),[u,d]=N.useState(null),[h,m]=N.useState([]),[p,v]=N.useState(null),[_,x]=N.useState(!1),[y,w]=N.useState(-1),b=N.useRef(null),j=N.useRef(null),E=async D=>{if(D.trim().length<3){m([]),x(!1);return}try{const Z=await fetch(`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(D)}&format=json&addressdetails=1&countrycodes=us&limit=5`,{headers:{"User-Agent":"CommunityOne-Navigator/1.0"}});if(!Z.ok)return;const F=(await Z.json()).reduce((B,G)=>{const R=`${G.osm_type}_${G.osm_id}`;return B.some(W=>`${W.osm_type}_${W.osm_id}`===R)||B.push(G),B},[]);m(F),x(F.length>0),w(-1)}catch(Z){console.error("Autocomplete error:",Z)}},P=D=>{a(D),d(null),b.current&&clearTimeout(b.current),b.current=setTimeout(()=>{E(D)},300)};N.useEffect(()=>()=>{b.current&&clearTimeout(b.current)},[]);const O=async D=>{if(!D.trim()){d("Please enter an address");return}c(!0),d(null),m([]),x(!1);try{const Z=await fetch(`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(D)}&format=json&addressdetails=1&countrycodes=us&limit=5`,{headers:{"User-Agent":"CommunityOne-Navigator/1.0"}});if(!Z.ok)throw new Error("Failed to lookup address");const I=await Z.json();if(I.length===0){d("Address not found. Please try a different address or be more specific.");return}const F=I.reduce((B,G)=>{const R=`${G.osm_type}_${G.osm_id}`;return B.some(W=>`${W.osm_type}_${W.osm_id}`===R)||B.push(G),B},[]);if(F.length>1){m(F),x(!0);return}C(F[0])}catch(Z){console.error("Address lookup error:",Z),d("Failed to lookup address. Please try again.")}finally{c(!1)}},C=D=>{const Z=D.address,I=Z.state||"",F=pb(I);console.log(`🗺️ [AddressLookup] State conversion: "${I}" → "${F}"`);const B={address:D.display_name,state:F,county:Z.county||"",city:Z.city||Z.town||Z.village||Z.municipality||"",latitude:parseFloat(D.lat),longitude:parseFloat(D.lon)};if(!B.state||!B.city){d("Could not determine city and state from this address. Please be more specific."),m([]),x(!1);return}console.log("📍 [AddressLookup] Location found:",B),m([]),x(!1),v(B),e(B)},A=D=>{D.preventDefault(),y>=0&&h[y]?C(h[y]):O(i)},T=D=>{a(D.display_name),C(D)},$=D=>{if(!(!_||h.length===0))switch(D.key){case"ArrowDown":D.preventDefault(),w(Z=>ZZ>0?Z-1:-1);break;case"Enter":y>=0&&(D.preventDefault(),C(h[y]));break;case"Escape":x(!1),w(-1);break}},z=()=>{if(!navigator.geolocation){d("Geolocation is not supported by your browser");return}c(!0),d(null),m([]),navigator.geolocation.getCurrentPosition(async D=>{const{latitude:Z,longitude:I}=D.coords;try{const F=await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${Z}&lon=${I}&format=json&addressdetails=1`,{headers:{"User-Agent":"CommunityOne-Navigator/1.0"}});if(!F.ok)throw new Error("Failed to reverse geocode location");const B=await F.json();a(B.display_name),C(B)}catch(F){console.error("Reverse geocoding error:",F),d("Failed to determine your location. Please enter your address manually.")}finally{c(!1)}},D=>{switch(console.error("Geolocation error:",D),c(!1),D.code){case D.PERMISSION_DENIED:d("Location access denied. Please enter your address manually or enable location permissions.");break;case D.POSITION_UNAVAILABLE:d("Location information unavailable. Please enter your address manually.");break;case D.TIMEOUT:d("Location request timed out. Please try again or enter your address manually.");break;default:d("An error occurred while getting your location. Please enter your address manually.")}},{enableHighAccuracy:!1,timeout:5e3,maximumAge:3e4})};return n?o.jsxs("form",{onSubmit:A,className:"w-full",children:[o.jsxs("div",{className:"relative",children:[o.jsx("input",{ref:j,type:"text",value:i,onChange:D=>P(D.target.value),onKeyDown:$,placeholder:"Enter your address...",className:"w-full px-4 py-2 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-900",disabled:s,autoComplete:"off"},"address-input-compact"),o.jsx(Rn,{className:"absolute left-3 top-2.5 h-5 w-5 text-gray-400"}),o.jsx("button",{type:"submit",disabled:s,className:"absolute right-2 top-1.5 px-3 py-1 text-white rounded-md transition-colors text-sm disabled:opacity-50",style:{backgroundColor:"#354F52"},onMouseEnter:D=>!s&&(D.currentTarget.style.backgroundColor="#2e4346"),onMouseLeave:D=>!s&&(D.currentTarget.style.backgroundColor="#354F52"),children:s?"Finding...":"Find"}),_&&h.length>0&&o.jsx("div",{className:"absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto",children:h.map((D,Z)=>{const I=D.address,F=I.city||I.town||I.village||I.county||"Unknown";return o.jsxs("button",{type:"button",onClick:()=>T(D),className:`w-full px-4 py-2 text-left hover:bg-gray-100 transition-colors ${Z===y?"bg-gray-100":""}`,children:[o.jsx("p",{className:"text-sm font-medium text-gray-900",children:D.display_name}),o.jsxs("p",{className:"text-xs text-gray-500",children:[F,", ",I.state]})]},`${D.osm_type}_${D.osm_id}`)})})]}),u&&o.jsx("p",{className:"mt-2 text-sm text-red-600",children:u})]}):o.jsxs("div",{className:"w-full",children:[o.jsxs("form",{onSubmit:A,className:"space-y-4",children:[o.jsxs("div",{children:[o.jsx("label",{htmlFor:"address",className:"block text-sm font-medium text-gray-700 mb-2",children:o.jsxs("span",{className:"flex items-center gap-2",children:[o.jsx(Rn,{className:"h-5 w-5"}),"Enter Your Address"]})}),o.jsxs("div",{className:"relative",children:[o.jsx("input",{ref:j,type:"text",id:"address",name:"addresslookup",value:i,onChange:D=>P(D.target.value),onKeyDown:$,placeholder:"123 Main St, Los Angeles, CA 90001",className:"w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-base text-gray-900",disabled:s,autoComplete:"off"},"address-input"),_&&h.length>0&&o.jsx("div",{className:"absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto",children:h.map((D,Z)=>{const I=D.address,F=I.city||I.town||I.village||I.county||"Unknown";return o.jsxs("button",{type:"button",onClick:()=>T(D),className:`w-full px-4 py-3 text-left hover:bg-gray-100 transition-colors border-b border-gray-100 last:border-b-0 ${Z===y?"bg-gray-100":""}`,children:[o.jsx("p",{className:"text-sm font-medium text-gray-900",children:D.display_name}),o.jsxs("p",{className:"text-xs text-gray-500 mt-1",children:[F,", ",I.state]})]},`${D.osm_type}_${D.osm_id}`)})})]}),o.jsx("p",{className:"mt-1 text-xs text-gray-500",children:"We'll find your local organizations based on your address"}),o.jsx("div",{className:"mt-3",children:o.jsxs("button",{type:"button",onClick:z,disabled:s,className:"w-full px-4 py-2 bg-white border-2 border-primary-300 text-primary-700 rounded-lg hover:bg-primary-50 hover:border-primary-500 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2",children:[o.jsxs("svg",{className:"h-5 w-5",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:[o.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"}),o.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M15 11a3 3 0 11-6 0 3 3 0 016 0z"})]}),o.jsx("span",{children:"Use My Current Location"})]})}),o.jsxs("div",{className:"relative my-4",children:[o.jsx("div",{className:"absolute inset-0 flex items-center",children:o.jsx("div",{className:"w-full border-t border-gray-300"})}),o.jsx("div",{className:"relative flex justify-center text-xs",children:o.jsx("span",{className:"px-2 bg-white text-gray-500",children:"or enter manually"})})]})]}),o.jsx("button",{type:"submit",disabled:s,className:"w-full px-6 py-3 text-white rounded-lg transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2",style:{backgroundColor:"#354F52"},onMouseEnter:D=>!s&&(D.currentTarget.style.backgroundColor="#2e4346"),onMouseLeave:D=>!s&&(D.currentTarget.style.backgroundColor="#354F52"),children:s?o.jsxs(o.Fragment,{children:[o.jsx("div",{className:"animate-spin h-5 w-5 border-2 border-white border-t-transparent rounded-full"}),o.jsx("span",{children:"Looking up address..."})]}):o.jsxs(o.Fragment,{children:[o.jsx(fn,{className:"h-5 w-5"}),o.jsx("span",{children:"Find My Community"})]})})]}),u&&o.jsx("div",{className:"mt-4 p-4 bg-red-50 border border-red-200 rounded-lg",children:o.jsx("p",{className:"text-sm text-red-800",children:u})}),p&&!n&&o.jsxs("div",{className:"mt-6 border-2 border-primary-200 rounded-lg overflow-hidden bg-primary-50",children:[o.jsxs("div",{className:"bg-primary-600 px-4 py-3 flex items-center justify-between",children:[o.jsxs("h3",{className:"text-lg font-semibold text-white flex items-center gap-2",children:[o.jsx(Rn,{className:"h-5 w-5"}),"Your Local Community"]}),o.jsx("button",{onClick:()=>window.location.href="/",className:"text-sm text-white hover:text-primary-100 underline font-medium",children:"← Back to Home"})]}),o.jsxs("div",{className:"p-6 space-y-4",children:[o.jsx("p",{className:"text-sm text-gray-700 mb-4",children:"Select a jurisdiction level below to explore organizations, meeting minutes, and contacts:"}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-4",children:[p.city&&o.jsx("button",{onClick:()=>{window.location.href="/?scope=city"},className:"bg-white rounded-lg p-4 shadow-sm hover:shadow-md hover:border-2 hover:border-blue-500 transition-all text-left w-full group",children:o.jsxs("div",{className:"flex items-start gap-3",children:[o.jsx("div",{className:"p-2 bg-blue-100 rounded-lg group-hover:bg-blue-200 transition-colors",children:o.jsx("svg",{className:"h-6 w-6 text-blue-600",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:o.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"})})}),o.jsxs("div",{className:"flex-1",children:[o.jsx("p",{className:"text-xs font-medium text-gray-500 uppercase tracking-wider",children:"City"}),o.jsx("p",{className:"text-lg font-semibold text-gray-900 mt-1 group-hover:text-blue-600",children:p.city}),o.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"City Council"}),o.jsx("p",{className:"text-xs text-blue-600 mt-2 opacity-0 group-hover:opacity-100 transition-opacity",children:"Click to explore →"})]})]})}),p.county&&o.jsx("button",{onClick:()=>{window.location.href="/?scope=county"},className:"bg-white rounded-lg p-4 shadow-sm hover:shadow-md hover:border-2 hover:border-green-500 transition-all text-left w-full group",children:o.jsxs("div",{className:"flex items-start gap-3",children:[o.jsx("div",{className:"p-2 bg-green-100 rounded-lg group-hover:bg-green-200 transition-colors",children:o.jsx("svg",{className:"h-6 w-6 text-green-600",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:o.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"})})}),o.jsxs("div",{className:"flex-1",children:[o.jsx("p",{className:"text-xs font-medium text-gray-500 uppercase tracking-wider",children:"County"}),o.jsx("p",{className:"text-lg font-semibold text-gray-900 mt-1 group-hover:text-green-600",children:p.county}),o.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"County Board"}),o.jsx("p",{className:"text-xs text-green-600 mt-2 opacity-0 group-hover:opacity-100 transition-opacity",children:"Click to explore →"})]})]})}),p.state&&o.jsx("button",{onClick:()=>{window.location.href="/?scope=state"},className:"bg-white rounded-lg p-4 shadow-sm hover:shadow-md hover:border-2 hover:border-purple-500 transition-all text-left w-full group",children:o.jsxs("div",{className:"flex items-start gap-3",children:[o.jsx("div",{className:"p-2 bg-purple-100 rounded-lg group-hover:bg-purple-200 transition-colors",children:o.jsx("svg",{className:"h-6 w-6 text-purple-600",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:o.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M3 21v-4m0 0V5a2 2 0 012-2h6.5l1 1H21l-3 6 3 6h-8.5l-1-1H5a2 2 0 00-2 2zm9-13.5V9"})})}),o.jsxs("div",{className:"flex-1",children:[o.jsx("p",{className:"text-xs font-medium text-gray-500 uppercase tracking-wider",children:"State"}),o.jsx("p",{className:"text-lg font-semibold text-gray-900 mt-1 group-hover:text-purple-600",children:p.state}),o.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"State Legislature"}),o.jsx("p",{className:"text-xs text-purple-600 mt-2 opacity-0 group-hover:opacity-100 transition-opacity",children:"Click to explore →"})]})]})}),p.city&&o.jsx("button",{onClick:()=>{window.location.href="/?scope=community"},className:"bg-white rounded-lg p-4 shadow-sm hover:shadow-md hover:border-2 hover:border-amber-500 transition-all text-left w-full group",children:o.jsxs("div",{className:"flex items-start gap-3",children:[o.jsx("div",{className:"p-2 bg-amber-100 rounded-lg group-hover:bg-amber-200 transition-colors",children:o.jsx("svg",{className:"h-6 w-6 text-amber-600",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:o.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"})})}),o.jsxs("div",{className:"flex-1",children:[o.jsx("p",{className:"text-xs font-medium text-gray-500 uppercase tracking-wider",children:"School District"}),o.jsxs("p",{className:"text-lg font-semibold text-gray-900 mt-1 group-hover:text-amber-600",children:[p.city," Unified"]}),o.jsx("p",{className:"text-sm text-gray-600 mt-1",children:"School Board"}),o.jsx("p",{className:"text-xs text-amber-600 mt-2 opacity-0 group-hover:opacity-100 transition-opacity",children:"Click to explore →"})]})]})})]}),o.jsxs("div",{className:"pt-4 border-t border-primary-200",children:[o.jsx("p",{className:"text-sm text-gray-600 mb-3",children:"Quick access to all local resources:"}),o.jsxs("div",{className:"flex flex-wrap gap-3",children:[o.jsx("button",{onClick:()=>{window.location.href=`/documents?state=${p.state}&city=${p.city}`},className:"flex-1 min-w-[200px] px-4 py-2 bg-white border-2 border-primary-600 text-primary-700 rounded-lg hover:bg-primary-50 transition-colors font-medium",children:"📄 All Meeting Minutes"}),o.jsx("button",{onClick:()=>{window.location.href=`/nonprofits?state=${p.state}&city=${p.city}`},className:"flex-1 min-w-[200px] px-4 py-2 bg-white border-2 border-primary-600 text-primary-700 rounded-lg hover:bg-primary-50 transition-colors font-medium",children:"🏢 All Local Organizations"})]})]}),o.jsx("div",{className:"text-center pt-2",children:o.jsx("button",{onClick:()=>{v(null),a(""),d(null),r()},className:"text-sm text-primary-600 hover:text-primary-700 font-medium underline",children:"Search Different Address"})})]})]})]})}function Rz(){const e=qc(),[t]=Us(),[n,r]=N.useState(""),[i,a]=N.useState("city"),[s,c]=N.useState(0),[u,d]=N.useState(!1),{location:h,setLocation:m}=Hf(),p="https://www.communityone.com/docs/intro",{data:v,isLoading:_,error:x}=zt({queryKey:["search-preview-home",n,h==null?void 0:h.state],queryFn:async()=>{if(console.log("🔍 [Home] Fetching preview for:",n,"in state:",h==null?void 0:h.state),!n||n.length<2)return console.log("⚠️ [Home] Query too short, skipping"),null;const C="/search/",A={q:n,types:"causes,contacts,organizations",limit:3};h&&h.state&&(A.state=h.state,console.log("📍 [Home] Filtering by state:",h.state)),console.log("📤 [Home] API Request:",C,A);const T=await vt.get(C,{params:A});return console.log("📥 [Home] API Response:",T.data),console.log("📊 [Home] Total results:",T.data.total_results),T.data},enabled:n.length>=2&&u,staleTime:1e3});N.useEffect(()=>{console.log("🔄 [Home] Preview results updated:",{hasResults:!!v,totalResults:v==null?void 0:v.total_results,showSuggestions:u,keyword:n,isLoading:_,error:x})},[v,u,n,_,x]),N.useEffect(()=>{t.get("tab")==="community"&&c(1)},[t]),N.useEffect(()=>{h&&i==="community"&&a("city")},[h,i]);const y=C=>{const A=C.target.value;r(A),d(A.length>=2)},w=C=>{r(C),d(!1)},b=C=>{if(n.trim().length>=2){const A=new URLSearchParams;A.set("q",n),A.set("types",C),h&&h.state&&A.set("state",h.state),e(`/search?${A.toString()}`)}},j=C=>{if(C.preventDefault(),n||h){const A=new URLSearchParams;n&&A.set("search",n),i&&A.set("scope",i),h&&((i==="state"||i==="county"||i==="city"||i==="community")&&A.set("state",h.state),(i==="county"||i==="city"||i==="community")&&h.county&&A.set("county",h.county),(i==="city"||i==="community")&&A.set("city",h.city)),e(`/documents?${A.toString()}`)}},E=C=>{m({address:C.address,state:C.state,county:C.county,city:C.city,latitude:C.latitude,longitude:C.longitude})},P=[{name:"People",icon:Ur,query:"",route:"/people"},{name:"Community",icon:$r,query:"community engagement"},{name:"Budget",icon:PA,query:"budget funding"},{name:"Housing",icon:G_,query:"housing affordable"},{name:"Transport",icon:Nz,query:"transportation transit"},{name:"Health",icon:$t,query:"health dental"},{name:"Education",icon:Eo,query:"education school"},{name:"Jobs",icon:SA,query:"employment jobs"},{name:"Legal",icon:fz,query:"legal services"},{name:"Charities",icon:dc,query:"",route:"/nonprofits"}],O=C=>{C.route?C.route.startsWith("http")?window.open(C.route,"_blank"):e(C.route):C.query&&e(`/search?q=${encodeURIComponent(C.query)}`)};return o.jsxs("div",{className:"min-h-screen",style:{backgroundColor:"#F1F5F9"},children:[o.jsx("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-16",children:o.jsxs("div",{className:"text-center",children:[o.jsx("h1",{className:"text-5xl font-bold mb-6",style:{color:"#354F52"},children:"Open Navigator"}),o.jsxs("p",{className:"text-xl mb-12 max-w-3xl mx-auto",style:{color:"#354F52"},children:["Track what local governments and charities say, spend—and block.",o.jsx("br",{}),"Find leaders by name. Discover causes."," ",o.jsx(ke,{to:"/jurisdictions",className:"font-semibold hover:underline",children:"925 jurisdictions"}),"."," ",o.jsx(ke,{to:"/search?types=organizations",className:"font-semibold hover:underline",children:"43,726 nonprofits"}),". All free."]}),o.jsx("div",{className:"max-w-5xl mx-auto mb-8",children:o.jsxs(wt.Group,{selectedIndex:s,onChange:c,children:[o.jsxs(wt.List,{className:"flex space-x-2 rounded-xl bg-white p-2 shadow-lg mb-6",children:[o.jsx(wt,{as:N.Fragment,children:({selected:C})=>o.jsx("button",{className:`w-full rounded-lg py-3 px-4 text-base font-medium leading-5 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${C?"text-white shadow":"text-gray-700 hover:bg-gray-100"}`,style:C?{backgroundColor:"#354F52"}:{},children:"🔍 Search Topics"})}),o.jsx(wt,{as:N.Fragment,children:({selected:C})=>o.jsx("button",{className:`w-full rounded-lg py-3 px-4 text-base font-medium leading-5 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${C?"text-white shadow":"text-gray-700 hover:bg-gray-100"}`,style:C?{backgroundColor:"#354F52"}:{},children:"📍 Find My Local Community"})})]}),o.jsxs(wt.Panels,{children:[o.jsx(wt.Panel,{children:o.jsx("div",{className:"bg-white rounded-xl shadow-lg p-8",children:o.jsx("form",{onSubmit:j,children:o.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-12 gap-4 items-end",children:[o.jsxs("div",{className:"lg:col-span-7",children:[o.jsx("label",{className:"block text-left text-sm font-medium text-gray-700 mb-2",children:"Search for topics, people, organizations, or causes"}),o.jsxs("div",{className:"relative",children:[o.jsx("input",{type:"text",placeholder:"Try: mayor, dental clinic, food bank, affordable housing...",value:n,onChange:y,onBlur:()=>setTimeout(()=>d(!1),200),onFocus:()=>{n.length>=2&&d(!0)},className:"w-full px-4 py-3 text-lg border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent text-gray-900"}),u&&v&&v.total_results>0&&o.jsxs("div",{className:"absolute z-10 w-full mt-2 bg-white border border-gray-200 rounded-lg shadow-xl max-h-96 overflow-y-auto",children:[v.results.causes.length>0&&o.jsxs("div",{className:"border-b border-gray-200",children:[o.jsxs("div",{className:"px-4 py-2 bg-gray-50 flex items-center justify-between",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx($t,{className:"h-4 w-4 text-gray-500"}),o.jsx("span",{className:"text-xs font-semibold text-gray-700 uppercase",children:"Causes"})]}),o.jsx("button",{type:"button",onClick:()=>b("causes"),className:"text-xs text-primary-600 hover:text-primary-700 font-medium",children:"View All"})]}),v.results.causes.slice(0,3).map((C,A)=>o.jsxs("button",{type:"button",onClick:()=>w(C.title),className:"w-full text-left px-4 py-2 hover:bg-gray-50 flex items-start gap-3 transition-colors",children:[o.jsx($t,{className:"h-5 w-5 text-gray-600 mt-0.5 flex-shrink-0"}),o.jsxs("div",{className:"flex-1 min-w-0",children:[o.jsx("div",{className:"font-medium text-gray-900 truncate",children:C.title}),o.jsx("div",{className:"text-sm text-gray-600 truncate",children:C.subtitle})]})]},A))]}),v.results.contacts.length>0&&o.jsxs("div",{className:"border-b border-gray-200",children:[o.jsxs("div",{className:"px-4 py-2 bg-gray-50 flex items-center justify-between",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Li,{className:"h-4 w-4 text-gray-500"}),o.jsx("span",{className:"text-xs font-semibold text-gray-700 uppercase",children:"People"})]}),o.jsx("button",{type:"button",onClick:()=>b("contacts"),className:"text-xs text-primary-600 hover:text-primary-700 font-medium",children:"View All"})]}),v.results.contacts.slice(0,3).map((C,A)=>o.jsxs("button",{type:"button",onClick:()=>w(C.title),className:"w-full text-left px-4 py-2 hover:bg-gray-50 flex items-start gap-3 transition-colors",children:[o.jsx(Li,{className:"h-5 w-5 text-gray-600 mt-0.5 flex-shrink-0"}),o.jsxs("div",{className:"flex-1 min-w-0",children:[o.jsx("div",{className:"font-medium text-gray-900 truncate",children:C.title}),o.jsx("div",{className:"text-sm text-gray-600 truncate",children:C.subtitle})]})]},A))]}),v.results.organizations.length>0&&o.jsxs("div",{children:[o.jsxs("div",{className:"px-4 py-2 bg-gray-50 flex items-center justify-between",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Ci,{className:"h-4 w-4 text-gray-500"}),o.jsx("span",{className:"text-xs font-semibold text-gray-700 uppercase",children:"Organizations"})]}),o.jsx("button",{type:"button",onClick:()=>b("organizations"),className:"text-xs text-primary-600 hover:text-primary-700 font-medium",children:"View All"})]}),v.results.organizations.slice(0,3).map((C,A)=>{var T,$;return o.jsxs("button",{type:"button",onClick:()=>w(C.title),className:"w-full text-left px-4 py-2 hover:bg-gray-50 flex items-start gap-3 transition-colors last:rounded-b-lg",children:[(T=C.metadata)!=null&&T.logo_url?o.jsx("img",{src:C.metadata.logo_url,alt:`${C.title} logo`,className:"h-5 w-5 rounded object-contain mt-0.5 flex-shrink-0",onError:z=>{var D;z.currentTarget.style.display="none",(D=z.currentTarget.nextElementSibling)==null||D.classList.remove("hidden")}}):null,o.jsx(Ci,{className:`h-5 w-5 text-gray-600 mt-0.5 flex-shrink-0 ${($=C.metadata)!=null&&$.logo_url?"hidden":""}`}),o.jsxs("div",{className:"flex-1 min-w-0",children:[o.jsx("div",{className:"font-medium text-gray-900 truncate",children:C.title}),o.jsx("div",{className:"text-sm text-gray-600 truncate",children:C.subtitle})]})]},A)})]}),o.jsx("div",{className:"px-4 py-2 bg-gray-50 text-center border-t border-gray-200",children:o.jsxs("button",{type:"submit",className:"text-sm text-primary-600 hover:text-primary-700 font-medium",children:["See all ",v.total_results," results →"]})})]})]})]}),o.jsxs("div",{className:"lg:col-span-3",children:[o.jsx("label",{className:"block text-left text-sm font-medium text-gray-700 mb-2",children:"Search In"}),h?o.jsxs("div",{className:"relative",children:[o.jsxs("select",{value:i,onChange:C=>a(C.target.value),className:"w-full px-4 py-3 text-lg border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent bg-white text-gray-900",children:[o.jsxs("option",{value:"city",children:["My City (",h.city,")"]}),o.jsxs("option",{value:"county",children:["My County (",h.county||"County",")"]}),o.jsxs("option",{value:"state",children:["My State (",h.state,")"]}),o.jsxs("option",{value:"community",children:["School Board (",h.city,")"]})]}),o.jsxs("button",{type:"button",onClick:()=>e("/?tab=community"),className:"absolute -bottom-6 left-0 right-0 text-xs text-primary-600 hover:text-primary-700 font-medium underline flex items-center justify-center gap-1",children:[o.jsx(Rn,{className:"h-3 w-3"}),"Change Location"]})]}):o.jsxs("button",{type:"button",onClick:()=>e("/?tab=community"),className:"w-full px-4 py-3 text-lg border-2 border-primary-600 rounded-lg bg-primary-50 text-primary-700 hover:bg-primary-100 transition-colors font-semibold flex items-center justify-center gap-2",children:[o.jsx(Rn,{className:"h-5 w-5"}),"Set Your Location First"]})]}),o.jsxs("div",{className:"lg:col-span-2",children:[o.jsx("label",{className:"block text-left text-sm font-medium text-gray-700 mb-2 invisible",children:"Search"}),o.jsxs("button",{type:"submit",className:"w-full text-white px-6 py-3 rounded-lg transition-colors text-lg font-semibold flex items-center justify-center gap-2",style:{backgroundColor:"#354F52"},onMouseEnter:C=>C.currentTarget.style.backgroundColor="#2e4346",onMouseLeave:C=>C.currentTarget.style.backgroundColor="#354F52",children:[o.jsx(fn,{className:"h-6 w-6"}),"Search"]})]})]})})})}),o.jsx(wt.Panel,{children:o.jsxs("div",{className:"bg-white rounded-xl shadow-lg p-8",children:[o.jsx("h2",{className:"text-2xl font-bold mb-3 text-center",style:{color:"#354F52"},children:"What's Happening in Your Community?"}),o.jsx("p",{className:"text-gray-600 text-center mb-6",children:"Enter your address to find local organizations, city councils, county boards, school districts, and charities near you"}),o.jsx(K_,{onLocationFound:E}),h&&o.jsxs("div",{className:"mt-8 p-6 bg-green-50 border-2 border-green-200 rounded-xl",children:[o.jsxs("div",{className:"flex items-start gap-3 mb-4",children:[o.jsx(ci,{className:"h-6 w-6 text-green-600 flex-shrink-0 mt-0.5"}),o.jsxs("div",{className:"flex-1",children:[o.jsx("h3",{className:"text-lg font-bold text-green-900 mb-1",children:"Location Set Successfully!"}),o.jsxs("p",{className:"text-green-700",children:["You're all set for ",o.jsxs("strong",{children:[h.city,", ",h.state]}),". Now you can search for topics in your community."]})]})]}),o.jsxs("button",{onClick:()=>c(0),className:"w-full bg-green-600 hover:bg-green-700 text-white px-6 py-4 rounded-lg font-semibold text-lg flex items-center justify-center gap-2 transition-all shadow-lg hover:shadow-xl",children:[o.jsx(fn,{className:"h-6 w-6"}),"Search Topics in My Community",o.jsx(Nl,{className:"h-5 w-5"})]})]})]})})]})]})}),o.jsxs("div",{className:"max-w-5xl mx-auto mb-6",children:[o.jsx("p",{className:"text-sm text-gray-600 text-center mb-3",children:"Popular topics:"}),o.jsx("div",{className:"flex flex-wrap justify-center gap-3",children:P.map(C=>o.jsxs("button",{onClick:()=>O(C),className:"inline-flex items-center gap-2 px-4 py-2 bg-white border-2 border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-colors",children:[o.jsx(C.icon,{className:"h-5 w-5 text-gray-600"}),o.jsx("span",{className:"font-medium text-gray-700",children:C.name})]},C.name))})]}),o.jsxs("p",{className:"text-sm text-gray-500",children:["By continuing, you agree to the"," ",o.jsx("a",{href:"#",className:"text-primary-600 hover:underline",children:"Terms"})," & ",o.jsx("a",{href:"#",className:"text-primary-600 hover:underline",children:"Privacy"}),"."]})]})}),o.jsx("div",{className:"py-16",style:{backgroundColor:"#354F52"},children:o.jsxs("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:[o.jsx("h2",{className:"text-3xl font-bold text-center mb-12 text-white",children:"Explore the Platform"}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8",children:[o.jsx(ke,{to:"/analytics",className:"group",children:o.jsxs("div",{className:"bg-white border-2 border-gray-200 rounded-lg p-6 hover:border-primary-500 transition-colors",children:[o.jsx(fc,{className:"h-10 w-10 text-primary-600 mb-4"}),o.jsx("h3",{className:"text-xl font-semibold mb-2",style:{color:"#354F52"},children:"Data & Trends"}),o.jsx("p",{className:"text-gray-600 mb-4",children:"Statistics, charts, and insights. See what's happening across communities."}),o.jsxs("span",{className:"text-primary-600 font-medium inline-flex items-center",children:["View Data ",o.jsx(Nl,{className:"h-4 w-4 ml-2"})]})]})}),o.jsx(ke,{to:"/documents",className:"group",children:o.jsxs("div",{className:"bg-white border-2 border-gray-200 rounded-lg p-6 hover:border-primary-500 transition-colors",children:[o.jsx(Ws,{className:"h-10 w-10 text-primary-600 mb-4"}),o.jsx("h3",{className:"text-xl font-semibold mb-2",style:{color:"#354F52"},children:"Meeting Minutes"}),o.jsx("p",{className:"text-gray-600 mb-4",children:"See what local governments are discussing, deciding, and spending"}),o.jsxs("span",{className:"text-primary-600 font-medium inline-flex items-center",children:["Browse Minutes ",o.jsx(Nl,{className:"h-4 w-4 ml-2"})]})]})}),o.jsx("a",{href:p,target:"_blank",rel:"noopener noreferrer",className:"group",children:o.jsxs("div",{className:"bg-white border-2 border-gray-200 rounded-lg p-6 hover:border-primary-500 transition-colors",children:[o.jsx(Zf,{className:"h-10 w-10 text-primary-600 mb-4"}),o.jsx("h3",{className:"text-xl font-semibold mb-2",style:{color:"#354F52"},children:"Learn More"}),o.jsx("p",{className:"text-gray-600 mb-4",children:"Discover how to track local decisions and find charities"}),o.jsxs("span",{className:"text-primary-600 font-medium inline-flex items-center",children:["Getting Started ",o.jsx(Nl,{className:"h-4 w-4 ml-2"})]})]})})]})]})}),o.jsx("div",{className:"py-16",style:{backgroundColor:"#F1F5F9"},children:o.jsx("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 text-center",children:[o.jsxs("div",{children:[o.jsx("div",{className:"text-4xl font-bold text-primary-600 mb-2",children:"2,500+"}),o.jsx("div",{className:"text-gray-600",children:"Causes Tracked"})]}),o.jsxs("div",{children:[o.jsx("div",{className:"text-4xl font-bold text-emerald-600 mb-2",children:"15,000+"}),o.jsx("div",{className:"text-gray-600",children:"Government Decisions"})]}),o.jsxs("div",{children:[o.jsx("div",{className:"text-4xl font-bold text-amber-600 mb-2",children:"8 Years"}),o.jsx("div",{className:"text-gray-600",children:"Historical Coverage"})]}),o.jsxs("div",{children:[o.jsx("div",{className:"text-4xl font-bold text-cyan-600 mb-2",children:"5,000+"}),o.jsx("div",{className:"text-gray-600",children:"Meeting Records"})]}),o.jsxs("div",{children:[o.jsx("div",{className:"text-4xl font-bold mb-2",style:{color:"#354F52"},children:"12,000+"}),o.jsx("div",{className:"text-gray-600",children:"Hours of Video"})]}),o.jsxs("div",{children:[o.jsx("div",{className:"text-4xl font-bold text-purple-600 mb-2",children:"100%"}),o.jsx("div",{className:"text-gray-600",children:"Free & Open"})]})]})})})]})}function Fz(){const e=qc(),[t]=Us(),[n,r]=N.useState(""),[i,a]=N.useState(""),[s,c]=N.useState("hero"),[u,d]=N.useState(0),[h,m]=N.useState("city"),[p,v]=N.useState(!1),[_,x]=N.useState(!1),{location:y,setLocation:w}=Hf(),b=N.useRef(null);N.useEffect(()=>{const U=setTimeout(()=>{console.log("⏱️ [HomeModern] Debounced keyword update:",n),a(n)},300);return()=>{clearTimeout(U)}},[n]),N.useEffect(()=>{const U=Y=>{b.current&&!b.current.contains(Y.target)&&v(!1)};if(p)return document.addEventListener("mousedown",U),()=>{document.removeEventListener("mousedown",U)}},[p]);let j="https://www.communityone.com/docs",E="";typeof window<"u"&&window.location.protocol==="https:"&&(j.startsWith("http://")&&(j=j.replace("http://","https://")),E.startsWith("http://")&&(E=E.replace("http://","https://")));const{data:P,isLoading:O}=zt({queryKey:["platform-stats-all",y==null?void 0:y.state,y==null?void 0:y.county,y==null?void 0:y.city],queryFn:async()=>{if(!y||!y.state)return console.log("📊 [HomeModern] No location set, skipping stats fetch"),null;console.log("📊 [HomeModern] Fetching stats for location:",y);const[U,Y,ne]=await Promise.all([y.city?vt.get("/stats",{params:{state:y.state,county:y.county,city:y.city}}).then(ee=>(console.log("📊 [HomeModern] City stats:",ee.data),ee.data)).catch(ee=>{var ce,Ne,Pe;return console.error("❌ [HomeModern] City stats error:",((ce=ee.response)==null?void 0:ce.data)||ee.message),{error:((Pe=(Ne=ee.response)==null?void 0:Ne.data)==null?void 0:Pe.detail)||ee.message}}):Promise.resolve(null),y.county?vt.get("/stats",{params:{state:y.state,county:y.county}}).then(ee=>(console.log("📊 [HomeModern] County stats:",ee.data),ee.data)).catch(ee=>{var ce,Ne,Pe;return console.error("❌ [HomeModern] County stats error:",((ce=ee.response)==null?void 0:ce.data)||ee.message),{error:((Pe=(Ne=ee.response)==null?void 0:Ne.data)==null?void 0:Pe.detail)||ee.message}}):Promise.resolve(null),vt.get("/stats",{params:{state:y.state}}).then(ee=>(console.log("📊 [HomeModern] State stats:",ee.data),ee.data)).catch(ee=>{var ce,Ne,Pe;return console.error("❌ [HomeModern] State stats error:",((ce=ee.response)==null?void 0:ce.data)||ee.message),{error:((Pe=(Ne=ee.response)==null?void 0:Ne.data)==null?void 0:Pe.detail)||ee.message}})]),ae={city:U,county:Y,state:ne,community:U};return console.log("📊 [HomeModern] All stats loaded:",ae),ae},enabled:!!(y&&y.state),staleTime:1e3*60*60,refetchOnWindowFocus:!1}),C=U=>U==null||U===0?"0":U>=1e6?`${(U/1e6).toFixed(1)}M`:U>=1e4?`${(U/1e3).toFixed(1)}K`:U.toLocaleString(),A=P==null?void 0:P[h],T=A?{...A,jurisdictions_display:C(A.jurisdictions),nonprofits_display:C(A.nonprofits),contacts_display:C(A.contacts),causes_display:"650+",school_districts_display:C(A.school_districts)}:null;console.log("📊 [HomeModern] Current scope:",h,"Stats data:",T,"Loading:",O);const{data:$,isLoading:z,error:D}=zt({queryKey:["search-preview-home",i,y==null?void 0:y.state],queryFn:async()=>{var ne,ae;if(console.log("🔍 [HomeModern] Fetching preview for:",i,"in state:",y==null?void 0:y.state),!i||i.length<2)return console.log("⚠️ [HomeModern] Query too short, skipping"),null;const U="/search/",Y={q:i,types:"causes,contacts,organizations",limit:3};y&&y.state&&(Y.state=y.state,console.log("📍 [HomeModern] Filtering by state:",y.state)),console.log("📤 [HomeModern] API Request:",U,Y);try{const ee=await vt.get(U,{params:Y});return console.log("📥 [HomeModern] API Response:",ee.data),console.log("📊 [HomeModern] Total results:",ee.data.total_results),console.log("🎯 [HomeModern] Causes:",ee.data.results.causes.length),console.log("👥 [HomeModern] Contacts:",ee.data.results.contacts.length),console.log("🏢 [HomeModern] Organizations:",ee.data.results.organizations.length),ee.data}catch(ee){throw console.error("❌ [HomeModern] API Error:",ee),console.error("❌ [HomeModern] Error message:",ee.message),console.error("❌ [HomeModern] Error response:",(ne=ee.response)==null?void 0:ne.data),console.error("❌ [HomeModern] Error status:",(ae=ee.response)==null?void 0:ae.status),ee}},enabled:i.length>=2&&p,staleTime:1e3,retry:!1});N.useEffect(()=>{console.log("🔄 [HomeModern] Preview results updated:",{hasResults:!!$,totalResults:$==null?void 0:$.total_results,showSuggestions:p,keyword:n,isLoading:z,error:D})},[$,p,n,z,D]),N.useEffect(()=>{t.get("tab")==="community"&&d(1)},[t]),N.useEffect(()=>{const U=t.get("scope");U&&["city","county","state","community","national"].includes(U)&&(m(U),d(0))},[t]),N.useEffect(()=>{y&&h==="community"&&m("city")},[y,h]);const Z=U=>{const Y=U.target.value;console.log("⌨️ [HomeModern] Keyword changed:",Y),r(Y),v(Y.length>=2),console.log("👁️ [HomeModern] Show suggestions:",Y.length>=2)},I=U=>{r(U),v(!1);const Y=new URLSearchParams;Y.set("q",U),y&&y.state&&Y.set("state",y.state),e(`/search?${Y.toString()}`)},F=U=>{if(n.trim().length>=2){const Y=new URLSearchParams;Y.set("q",n),Y.set("types",U),y&&y.state&&Y.set("state",y.state),e(`/search?${Y.toString()}`)}},B=U=>{const Y=document.getElementById(U);if(Y){const ee=Y.getBoundingClientRect().top+window.pageYOffset-80;window.scrollTo({top:ee,behavior:"smooth"})}};N.useEffect(()=>{const U=()=>{const Y=["hero","features","how-it-works","stats","get-started"],ne=window.scrollY+100;for(const ae of Y){const ee=document.getElementById(ae);if(ee){const{offsetTop:ce,offsetHeight:Ne}=ee;if(ne>=ce&&newindow.removeEventListener("scroll",U)},[]);const G=U=>{if(U.preventDefault(),n||y){const Y=new URLSearchParams;n&&Y.set("q",n),y&&y.state&&Y.set("state",y.state),e(`/search?${Y.toString()}`)}},R=U=>{w({address:U.address,state:U.state,county:U.county,city:U.city,latitude:U.latitude,longitude:U.longitude})},K=[{id:"hero",label:"Home"},{id:"features",label:"Features"},{id:"how-it-works",label:"How It Works"},{id:"stats",label:"Impact"},{id:"get-started",label:"Documentation"},{id:"contact",label:"Contact"}],W=[{icon:Ws,title:"Policy Decisions",description:"Track 500K+ meeting pages with decision analysis, deferral patterns, and stakeholder positions",link:"/documents",color:"#354F52"},{icon:fc,title:"Budget Analysis",description:"Compare budget rhetoric to reality with $2T+ in tracked spending and delta analysis",link:"/analytics",color:"#52796F"},{icon:Ur,title:"Elected Officials",description:"Follow 362 officials across 925 jurisdictions with voting records and decision patterns",link:"/people",color:"#84A98C"},{icon:la,title:"Policy Map",description:"Track state legislation and bills across all sessions. Search 13,000+ bills by topic and status",link:"/policy-map",color:"#4A90E2"},{icon:dc,title:"Nonprofits & Churches",description:"43,726 nonprofits including 4,372 churches with financial data from 5 states",link:"/nonprofits",color:"#9B59B6"},{icon:Hd,title:"Fact-Checking",description:"Verify claims with integrated PolitiFact, FactCheck.org, and Google Fact Check data",link:"/debate-grader",color:"#E74C3C"}];return o.jsxs("div",{className:"min-h-screen bg-white",children:[o.jsxs("nav",{className:"fixed top-0 left-0 right-0 z-50 bg-white/95 backdrop-blur-sm border-b border-gray-200 shadow-sm",children:[o.jsx("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:o.jsxs("div",{className:"flex items-center justify-between h-20",children:[o.jsxs(ke,{to:"/",className:"flex items-center gap-3",children:[o.jsx("img",{src:"/communityone_logo.svg",alt:"CommunityOne Logo",className:"h-12"}),o.jsx("span",{className:"text-xl font-bold",style:{color:"#354F52"},children:"Open Navigator"})]}),o.jsx("div",{className:"hidden md:flex items-center gap-8",children:K.map(U=>o.jsx("button",{onClick:()=>B(U.id),className:`text-sm font-medium transition-colors ${s===U.id?"text-[#354F52] border-b-2 border-[#354F52]":"text-gray-600 hover:text-[#354F52]"} pb-1`,children:U.label},U.id))}),o.jsx(ke,{to:"/explore",className:"hidden md:block px-6 py-2.5 rounded-lg text-white font-semibold hover:shadow-lg transition-all",style:{backgroundColor:"#354F52"},children:"Explore Now"}),o.jsx("button",{onClick:()=>x(!_),className:"md:hidden p-2 rounded-lg text-gray-600 hover:bg-gray-100 transition-colors","aria-label":"Toggle menu",children:_?o.jsx(Cr,{className:"h-6 w-6"}):o.jsx(NA,{className:"h-6 w-6"})})]})}),_&&o.jsx("div",{className:"md:hidden border-t border-gray-200 bg-white",children:o.jsxs("div",{className:"px-4 py-3 space-y-1",children:[K.map(U=>o.jsx("button",{onClick:()=>{B(U.id),x(!1)},className:`block w-full text-left px-4 py-3 rounded-lg text-base font-medium transition-colors ${s===U.id?"bg-[#354F52] text-white":"text-gray-700 hover:bg-gray-100"}`,children:U.label},U.id)),o.jsx(ke,{to:"/explore",className:"block w-full text-center px-4 py-3 mt-2 rounded-lg text-white font-semibold",style:{backgroundColor:"#354F52"},onClick:()=>x(!1),children:"Explore Now"})]})})]}),o.jsx("section",{id:"hero",className:"pt-32 pb-20 px-4",style:{background:"linear-gradient(135deg, #F1F5F9 0%, #E8EEF2 100%)"},children:o.jsxs("div",{className:"max-w-7xl mx-auto text-center",children:[o.jsxs("div",{className:"inline-flex items-center gap-2 px-4 py-2 bg-white rounded-full shadow-sm mb-6 animate-[slideUp_0.6s_ease-out]",children:[o.jsx(CA,{className:"h-5 w-5",style:{color:"#354F52"}}),o.jsx("span",{className:"text-sm font-medium",style:{color:"#354F52"},children:"The open path to everything local"})]}),o.jsxs("h1",{className:"text-6xl md:text-7xl font-bold mb-6 animate-[slideUp_0.8s_ease-out_0.2s_both]",style:{color:"#354F52"},children:["Track Local Decisions.",o.jsx("br",{}),o.jsx("span",{className:"bg-gradient-to-r from-[#52796F] to-[#84A98C] bg-clip-text text-transparent",children:"Take Action."})]}),o.jsxs("p",{className:"text-xl md:text-2xl text-gray-600 mb-12 max-w-3xl mx-auto animate-[slideUp_0.8s_ease-out_0.4s_both]",children:["Follow leaders, charities, and causes in your community.",o.jsx("br",{}),n.length>=2&&$&&$.total_results>0?o.jsxs(o.Fragment,{children:[o.jsxs("span",{className:"font-semibold text-[#52796F]",children:[$.total_results.toLocaleString()," result",$.total_results!==1?"s":""]}),' matching "',o.jsx("span",{className:"font-bold text-[#354F52]",children:n}),'"',y&&o.jsxs(o.Fragment,{children:[" in ",o.jsx("span",{className:"font-semibold text-[#52796F]",children:y.city||y.county||y.state})]})]}):n.length>=2&&$&&$.total_results===0?o.jsx(o.Fragment,{children:o.jsxs("span",{className:"text-gray-500",children:['No results found for "',n,'"',y&&` in ${y.city||y.county||y.state}`]})}):T!=null&&T.error?o.jsxs("span",{className:"text-amber-600",children:["⚠️ Stats unavailable (",T.error,"). Using default counts."]}):T?T.level==="state"||T.level==="county"||T.level==="city"||T.level==="community"?o.jsxs(o.Fragment,{children:[T.level==="city"&&T.jurisdictions_breakdown?o.jsxs(ke,{to:`/jurisdictions?state=${T.state}&jurisdiction_details=${encodeURIComponent(JSON.stringify(T.jurisdictions_breakdown))}`,className:"font-semibold text-[#52796F] hover:text-[#354F52] no-underline hover:underline hover:decoration-2 transition-all duration-200",children:[T.jurisdictions_display," jurisdictions"]}):o.jsxs(ke,{to:`/jurisdictions?state=${T.state}${T.county?`&county=${encodeURIComponent(T.county)}`:""}${T.city?`&city=${encodeURIComponent(T.city)}`:""}`,className:"font-semibold text-[#52796F] hover:text-[#354F52] no-underline hover:underline hover:decoration-2 transition-all duration-200",children:[T.jurisdictions_display," jurisdictions"]})," • ",o.jsxs(ke,{to:`/search?types=organizations&state=${T.state}${T.county?`&county=${encodeURIComponent(T.county)}`:""}${T.city?`&city=${encodeURIComponent(T.city)}`:""}`,className:"font-semibold text-[#52796F] hover:text-[#354F52] no-underline hover:underline hover:decoration-2 transition-all duration-200",children:[T.nonprofits_display," nonprofits"]})," • ",o.jsxs(ke,{to:`/search?types=contacts&state=${T.state}${T.county?`&county=${encodeURIComponent(T.county)}`:""}${T.city?`&city=${encodeURIComponent(T.city)}`:""}`,className:"font-semibold text-[#52796F] hover:text-[#354F52] no-underline hover:underline hover:decoration-2 transition-all duration-200",children:[T.contacts_display," leaders"]})," • ",o.jsxs(ke,{to:`/search?types=causes&state=${T.state}${T.county?`&county=${encodeURIComponent(T.county)}`:""}${T.city?`&city=${encodeURIComponent(T.city)}`:""}`,className:"font-semibold text-[#52796F] hover:text-[#354F52] no-underline hover:underline hover:decoration-2 transition-all duration-200",children:[T.causes_display," causes"]})," in ",T.location," • 100% free"]}):o.jsxs(o.Fragment,{children:[o.jsxs(ke,{to:`/jurisdictions${T.state?`?state=${T.state}`:""}`,className:"font-semibold text-[#52796F] hover:text-[#354F52] no-underline hover:underline hover:decoration-2 transition-all duration-200",children:[T.jurisdictions_display," jurisdictions"]})," • ",o.jsxs(ke,{to:"/search?types=organizations",className:"font-semibold text-[#52796F] hover:text-[#354F52] no-underline hover:underline hover:decoration-2 transition-all duration-200",children:[T.nonprofits_display," nonprofits"]})," • ",o.jsxs(ke,{to:"/search?types=contacts",className:"font-semibold text-[#52796F] hover:text-[#354F52] no-underline hover:underline hover:decoration-2 transition-all duration-200",children:[T.contacts_display," leaders"]})," • ",o.jsxs(ke,{to:"/search?types=causes",className:"font-semibold text-[#52796F] hover:text-[#354F52] no-underline hover:underline hover:decoration-2 transition-all duration-200",children:[T.causes_display," causes"]})," • 100% free"]}):o.jsxs(o.Fragment,{children:[o.jsx(ke,{to:"/jurisdictions",className:"font-semibold text-[#52796F] hover:text-[#354F52] no-underline hover:underline hover:decoration-2 transition-all duration-200",children:"925 jurisdictions"})," • ",o.jsx(ke,{to:"/search?types=organizations",className:"font-semibold text-[#52796F] hover:text-[#354F52] no-underline hover:underline hover:decoration-2 transition-all duration-200",children:"43,726 nonprofits"})," • ",o.jsx(ke,{to:"/search?types=contacts",className:"font-semibold text-[#52796F] hover:text-[#354F52] no-underline hover:underline hover:decoration-2 transition-all duration-200",children:"10,000+ leaders"})," • ",o.jsx(ke,{to:"/search?types=causes",className:"font-semibold text-[#52796F] hover:text-[#354F52] no-underline hover:underline hover:decoration-2 transition-all duration-200",children:"650+ causes"})," • 100% free"]})]}),o.jsx("div",{className:"relative z-20 max-w-5xl mx-auto mb-8 animate-[slideUp_0.8s_ease-out_0.6s_both]",children:o.jsxs(wt.Group,{selectedIndex:u,onChange:d,children:[o.jsxs(wt.List,{className:"flex space-x-2 rounded-xl bg-white p-2 shadow-lg mb-6 max-w-2xl mx-auto",children:[o.jsx(wt,{as:N.Fragment,children:({selected:U})=>o.jsx("button",{className:`w-full rounded-lg py-3 px-6 text-base font-medium leading-5 focus:outline-none transition-all ${U?"text-white shadow-md":"text-gray-700 hover:bg-gray-100"}`,style:U?{backgroundColor:"#354F52"}:{},children:"🔍 Search Topics"})}),o.jsx(wt,{as:N.Fragment,children:({selected:U})=>o.jsx("button",{className:`w-full rounded-lg py-3 px-6 text-base font-medium leading-5 focus:outline-none transition-all ${U?"text-white shadow-md":"text-gray-700 hover:bg-gray-100"}`,style:U?{backgroundColor:"#354F52"}:{},children:"📍 Find My Community"})})]}),o.jsxs(wt.Panels,{children:[o.jsx(wt.Panel,{children:o.jsx("div",{className:"relative z-10 bg-white rounded-2xl shadow-xl p-8",children:o.jsx("form",{onSubmit:G,children:o.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-12 gap-4 items-end",children:[o.jsxs("div",{className:"lg:col-span-7",children:[o.jsx("label",{className:"block text-left text-sm font-medium text-gray-900 mb-2",children:"Search for topics, people, organizations, or causes"}),o.jsxs("div",{className:"relative",ref:b,children:[o.jsx("input",{type:"text",placeholder:"Try: mayor, dental clinic, food bank, affordable housing...",value:n,onChange:Z,onFocus:()=>{n.length>=2&&v(!0)},className:"w-full px-4 py-3 text-lg border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#354F52] focus:border-transparent bg-white text-gray-900"}),p&&!$&&!D&&o.jsx("div",{className:"absolute z-50 w-full mt-2 border-2 border-gray-300 rounded-lg shadow-2xl",style:{backgroundColor:"#ffffff"},children:o.jsxs("div",{className:"px-4 py-8 flex flex-col items-center justify-center gap-3",children:[o.jsx("div",{className:"animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"}),o.jsx("p",{className:"text-sm text-gray-600",children:"Searching..."})]})}),p&&D&&o.jsx("div",{className:"absolute z-50 w-full mt-2 border-2 border-red-300 rounded-lg shadow-2xl",style:{backgroundColor:"#FEF2F2"},children:o.jsxs("div",{className:"px-4 py-3 flex items-start gap-3",children:[o.jsx("svg",{className:"h-5 w-5 text-red-500 mt-0.5 flex-shrink-0",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:o.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"})}),o.jsxs("div",{className:"flex-1",children:[o.jsx("h3",{className:"text-sm font-semibold text-red-800 mb-1",children:"Unable to Load Results"}),o.jsx("p",{className:"text-sm text-red-600",children:"We're having trouble connecting to our search service. Please try again in a moment."})]})]})}),p&&!D&&$&&$.total_results>0&&o.jsxs("div",{className:"absolute z-50 w-full mt-2 border-2 border-gray-300 rounded-lg shadow-2xl max-h-96 overflow-y-auto",style:{backgroundColor:"#ffffff"},children:[$.results.causes.length>0&&o.jsxs("div",{className:"border-b border-gray-200",children:[o.jsxs("div",{className:"px-4 py-2 bg-gray-50 flex items-center justify-between",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx($t,{className:"h-4 w-4 text-gray-500"}),o.jsx("span",{className:"text-xs font-semibold text-gray-700 uppercase",children:"Causes"})]}),o.jsx("button",{type:"button",onMouseDown:U=>{U.preventDefault(),F("causes")},className:"text-xs text-primary-600 hover:text-primary-700 font-medium",children:"View All"})]}),$.results.causes.slice(0,3).map((U,Y)=>o.jsxs("button",{type:"button",onMouseDown:ne=>{ne.preventDefault(),I(U.title)},className:"w-full text-left px-4 py-2 bg-white hover:bg-gray-50 flex items-start gap-3 transition-colors",children:[o.jsx($t,{className:"h-5 w-5 text-gray-600 mt-0.5 flex-shrink-0"}),o.jsxs("div",{className:"flex-1 min-w-0",children:[o.jsx("div",{className:"font-medium text-gray-900 truncate",children:U.title}),o.jsx("div",{className:"text-sm text-gray-600 truncate",children:U.subtitle})]})]},Y))]}),$.results.contacts.length>0&&o.jsxs("div",{className:"border-b border-gray-200",children:[o.jsxs("div",{className:"px-4 py-2 bg-gray-50 flex items-center justify-between",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Li,{className:"h-4 w-4 text-gray-500"}),o.jsx("span",{className:"text-xs font-semibold text-gray-700 uppercase",children:"People"})]}),o.jsx("button",{type:"button",onMouseDown:U=>{U.preventDefault(),F("contacts")},className:"text-xs text-primary-600 hover:text-primary-700 font-medium",children:"View All"})]}),$.results.contacts.slice(0,3).map((U,Y)=>o.jsxs("button",{type:"button",onMouseDown:ne=>{ne.preventDefault(),I(U.title)},className:"w-full text-left px-4 py-2 bg-white hover:bg-gray-50 flex items-start gap-3 transition-colors",children:[o.jsx(Li,{className:"h-5 w-5 text-gray-600 mt-0.5 flex-shrink-0"}),o.jsxs("div",{className:"flex-1 min-w-0",children:[o.jsx("div",{className:"font-medium text-gray-900 truncate",children:U.title}),o.jsx("div",{className:"text-sm text-gray-600 truncate",children:U.subtitle})]})]},Y))]}),$.results.organizations.length>0&&o.jsxs("div",{children:[o.jsxs("div",{className:"px-4 py-2 bg-gray-50 flex items-center justify-between",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Ci,{className:"h-4 w-4 text-gray-500"}),o.jsx("span",{className:"text-xs font-semibold text-gray-700 uppercase",children:"Organizations"})]}),o.jsx("button",{type:"button",onMouseDown:U=>{U.preventDefault(),F("organizations")},className:"text-xs text-primary-600 hover:text-primary-700 font-medium",children:"View All"})]}),$.results.organizations.slice(0,3).map((U,Y)=>{var ne,ae;return o.jsxs("button",{type:"button",onMouseDown:ee=>{ee.preventDefault(),I(U.title)},className:"w-full text-left px-4 py-2 bg-white hover:bg-gray-50 flex items-start gap-3 transition-colors last:rounded-b-lg",children:[(ne=U.metadata)!=null&&ne.logo_url?o.jsx("img",{src:U.metadata.logo_url,alt:`${U.title} logo`,className:"h-5 w-5 rounded object-contain mt-0.5 flex-shrink-0",onError:ee=>{var ce;ee.currentTarget.style.display="none",(ce=ee.currentTarget.nextElementSibling)==null||ce.classList.remove("hidden")}}):null,o.jsx(Ci,{className:`h-5 w-5 text-gray-600 mt-0.5 flex-shrink-0 ${(ae=U.metadata)!=null&&ae.logo_url?"hidden":""}`}),o.jsxs("div",{className:"flex-1 min-w-0",children:[o.jsx("div",{className:"font-medium text-gray-900 truncate",children:U.title}),o.jsx("div",{className:"text-sm text-gray-600 truncate",children:U.subtitle})]})]},Y)})]}),o.jsx("div",{className:"px-4 py-2 bg-gray-50 text-center border-t border-gray-200",children:o.jsxs("button",{type:"submit",className:"text-sm text-primary-600 hover:text-primary-700 font-medium",children:["See all ",$.total_results," results →"]})})]}),p&&!D&&!z&&$&&$.total_results===0&&o.jsx("div",{className:"absolute z-50 w-full mt-2 border-2 border-gray-300 rounded-lg shadow-2xl",style:{backgroundColor:"#ffffff"},children:o.jsxs("div",{className:"px-4 py-6 text-center",children:[o.jsx(fn,{className:"h-12 w-12 text-gray-400 mx-auto mb-3"}),o.jsx("p",{className:"text-sm font-medium text-gray-900 mb-1",children:"No results found"}),o.jsx("p",{className:"text-xs text-gray-600",children:"Try different keywords or check your spelling"})]})})]})]}),o.jsxs("div",{className:"lg:col-span-3",children:[o.jsx("label",{className:"block text-left text-sm font-medium text-gray-900 mb-2",children:"Search In"}),y?o.jsxs("div",{className:"relative",children:[o.jsxs("select",{value:h,onChange:U=>m(U.target.value),className:"w-full px-4 py-3 text-lg border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#354F52] focus:border-transparent bg-white text-gray-900",children:[o.jsxs("option",{value:"city",className:"text-gray-900 bg-white",children:["My City (",y.city,")"]}),o.jsxs("option",{value:"county",className:"text-gray-900 bg-white",children:["My County (",y.county||"County",")"]}),o.jsxs("option",{value:"state",className:"text-gray-900 bg-white",children:["My State (",y.state,")"]}),o.jsxs("option",{value:"community",className:"text-gray-900 bg-white",children:["School Board (",y.city,")"]})]}),o.jsxs("button",{type:"button",onClick:()=>d(1),className:"absolute -bottom-6 left-0 right-0 text-xs text-primary-600 hover:text-primary-700 font-medium underline flex items-center justify-center gap-1",children:[o.jsx(la,{className:"h-3 w-3"}),"Change Location"]})]}):o.jsxs("button",{type:"button",onClick:()=>d(1),className:"w-full px-4 py-3 text-lg border-2 border-[#354F52] rounded-lg bg-[#E8EFEA] text-[#354F52] hover:bg-[#d9e5db] transition-colors font-semibold flex items-center justify-center gap-2",children:[o.jsx(la,{className:"h-5 w-5"}),"Set Your Location First"]})]}),o.jsxs("div",{className:"lg:col-span-2",children:[o.jsx("label",{className:"block text-left text-sm font-medium text-gray-900 mb-2 invisible",children:"Search"}),o.jsxs("button",{type:"submit",className:"w-full text-white px-6 py-3 rounded-lg transition-all text-lg font-semibold flex items-center justify-center gap-2 hover:shadow-lg",style:{backgroundColor:"#354F52"},children:[o.jsx(fn,{className:"h-6 w-6"}),"Search"]})]})]})})})}),o.jsx(wt.Panel,{children:o.jsxs("div",{className:"bg-white rounded-2xl shadow-xl p-8",children:[o.jsx("h2",{className:"text-2xl font-bold mb-3 text-center",style:{color:"#354F52"},children:"What's Happening in Your Community?"}),o.jsx("p",{className:"text-gray-600 text-center mb-6",children:"Enter your address to find local organizations, city councils, county boards, school districts, and charities near you"}),o.jsx(K_,{onLocationFound:R}),y&&o.jsxs("div",{className:"mt-8 p-6 bg-green-50 border-2 border-green-200 rounded-xl",children:[o.jsxs("div",{className:"flex items-start gap-3 mb-4",children:[o.jsx(ci,{className:"h-6 w-6 text-green-600 flex-shrink-0 mt-0.5"}),o.jsxs("div",{className:"flex-1",children:[o.jsx("h3",{className:"text-lg font-bold text-green-900 mb-1",children:"Location Set Successfully!"}),o.jsxs("p",{className:"text-green-700",children:["You're all set for ",o.jsxs("strong",{children:[y.city,", ",y.state]}),". Now you can search for topics in your community."]})]})]}),o.jsxs("button",{onClick:()=>d(0),className:"w-full bg-green-600 hover:bg-green-700 text-white px-6 py-4 rounded-lg font-semibold text-lg flex items-center justify-center gap-2 transition-all shadow-lg hover:shadow-xl",children:[o.jsx(fn,{className:"h-6 w-6"}),"Search Topics in My Community",o.jsx(Nl,{className:"h-5 w-5"})]})]})]})})]})]})}),o.jsxs("div",{className:"relative z-[1] flex flex-wrap justify-center gap-8 text-sm text-gray-600 animate-[slideUp_0.8s_ease-out_0.8s_both]",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(ci,{className:"h-5 w-5 text-green-500"}),o.jsxs("span",{children:[(T==null?void 0:T.jurisdictions_display)||"90,000+"," Jurisdictions"]})]}),o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(ci,{className:"h-5 w-5 text-green-500"}),o.jsxs("span",{children:[(T==null?void 0:T.nonprofits_display)||"43,726"," Nonprofits"]})]}),o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(ci,{className:"h-5 w-5 text-green-500"}),o.jsxs("span",{children:[(T==null?void 0:T.meetings_display)||"500K+"," Meetings Analyzed"]})]})]})]})}),o.jsx("section",{id:"jurisdictions",className:"py-20 px-4 bg-gradient-to-br from-gray-50 to-blue-50",children:o.jsx("div",{className:"max-w-4xl mx-auto",children:o.jsxs("div",{className:"bg-white rounded-2xl shadow-xl p-12 text-center",children:[o.jsx(la,{className:"h-16 w-16 mx-auto mb-6",style:{color:"#354F52"}}),o.jsx("h2",{className:"text-4xl md:text-5xl font-bold mb-4",style:{color:"#354F52"},children:"Explore Jurisdictions"}),o.jsx("p",{className:"text-xl text-gray-600 mb-8",children:"Search and filter through 32,000+ cities, counties, and school districts across all 50 states. View meeting schedules, discovery status, and available data sources."}),o.jsxs("div",{className:"flex flex-col sm:flex-row gap-4 justify-center",children:[o.jsxs(ke,{to:"/jurisdictions",className:"inline-flex items-center justify-center px-8 py-4 rounded-lg text-white font-semibold transition-all hover:shadow-lg",style:{backgroundColor:"#354F52"},children:[o.jsx(fn,{className:"h-5 w-5 mr-2"}),"Browse Jurisdictions"]}),o.jsx("a",{href:"#contact",className:"inline-flex items-center justify-center px-8 py-4 rounded-lg bg-white border-2 text-[#354F52] font-semibold transition-all hover:bg-gray-50",style:{borderColor:"#354F52"},children:"Request Coverage"})]})]})})}),o.jsx("section",{id:"features",className:"py-20 px-4 bg-white",children:o.jsxs("div",{className:"max-w-7xl mx-auto",children:[o.jsxs("div",{className:"text-center mb-16",children:[o.jsx("h2",{className:"text-4xl md:text-5xl font-bold mb-4",style:{color:"#354F52"},children:"Everything You Need"}),o.jsx("p",{className:"text-xl text-gray-600",children:"Powerful tools to stay informed and engaged with the most impactful details"})]}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8",children:W.map((U,Y)=>o.jsxs(ke,{to:U.link,className:"group bg-gradient-to-br from-gray-50 to-white border-2 border-gray-200 rounded-2xl p-8 hover:border-[#354F52] hover:shadow-xl transition-all",children:[o.jsx("div",{className:"w-14 h-14 rounded-xl flex items-center justify-center mb-4 group-hover:scale-110 transition-transform",style:{backgroundColor:`${U.color}15`},children:o.jsx(U.icon,{className:"h-7 w-7",style:{color:U.color}})}),o.jsx("h3",{className:"text-xl font-bold mb-3",style:{color:"#354F52"},children:U.title}),o.jsx("p",{className:"text-gray-600 mb-4",children:U.description}),o.jsxs("span",{className:"inline-flex items-center gap-2 text-sm font-medium",style:{color:U.color},children:["Learn more ",o.jsx(Nl,{className:"h-4 w-4 group-hover:translate-x-1 transition-transform"})]})]},Y))})]})}),o.jsx("section",{id:"how-it-works",className:"py-20 px-4",style:{backgroundColor:"#F1F5F9"},children:o.jsxs("div",{className:"max-w-7xl mx-auto",children:[o.jsxs("div",{className:"text-center mb-16",children:[o.jsx("h2",{className:"text-4xl md:text-5xl font-bold mb-4",style:{color:"#354F52"},children:"How It Works"}),o.jsx("p",{className:"text-xl text-gray-600",children:"Get started in three simple steps"})]}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-12",children:[{step:"1",title:"Set Your Location",description:"Tell us where you live to see local leaders, meetings, and charities",icon:la},{step:"2",title:"Follow What Matters",description:"Follow leaders, organizations, and causes you care about",icon:$t},{step:"3",title:"Stay Informed",description:"Get updates on local decisions and opportunities to take action",icon:Hd}].map((U,Y)=>o.jsxs("div",{className:"text-center",children:[o.jsx("div",{className:"w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6 text-3xl font-bold text-white",style:{backgroundColor:"#354F52"},children:U.step}),o.jsx(U.icon,{className:"h-12 w-12 mx-auto mb-4",style:{color:"#52796F"}}),o.jsx("h3",{className:"text-2xl font-bold mb-3",style:{color:"#354F52"},children:U.title}),o.jsx("p",{className:"text-gray-600 text-lg",children:U.description})]},Y))})]})}),o.jsx("section",{id:"stats",className:"py-20 px-4 bg-white",children:o.jsxs("div",{className:"max-w-7xl mx-auto",children:[o.jsxs("div",{className:"text-center mb-16",children:[o.jsx("h2",{className:"text-4xl md:text-5xl font-bold mb-4",style:{color:"#354F52"},children:(T==null?void 0:T.level)==="state"?`Our Impact in ${T.location}`:"Our Impact"}),o.jsxs("p",{className:"text-xl text-gray-600",children:[(T==null?void 0:T.level)==="state"?`Real numbers for ${T.location} from live data tables`:"Real numbers from real data tables",(T==null?void 0:T.last_updated)&&o.jsxs("span",{className:"text-sm text-gray-500 ml-2",children:["(updated ",new Date(T.last_updated).toLocaleDateString(),")"]})]})]}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8",children:[{value:(T==null?void 0:T.jurisdictions_display)||"925",label:"Jurisdictions Tracked",description:"Cities, counties, states, and tribal governments",color:"#354F52"},{value:(T==null?void 0:T.nonprofits_display)||"43,726",label:"Nonprofits & Churches",description:T?`${T.states_with_data} states with full IRS BMF data`:"5 states with full IRS BMF data",color:"#52796F"},{value:(T==null?void 0:T.meetings_display)||"6,913",label:"Meeting Pages Analyzed",description:"AI-extracted decisions and budget items",color:"#84A98C"},{value:(T==null?void 0:T.budget_tracked)||"N/A",label:"Budget Dollars",description:"Real-time tracking and delta analysis",color:"#4A90E2"},{value:(T==null?void 0:T.contacts_display)||"362",label:"Elected Officials",description:"Voting records and decision patterns",color:"#9B59B6"},{value:(T==null?void 0:T.churches)||"4,372",label:"Churches & Congregations",description:"Community-based organizations mapped",color:"#6B8E23"},{value:(T==null?void 0:T.policy_decisions)||"N/A",label:"Policy Decisions",description:"With deferral tracking and stakeholder positions",color:"#DC143C"},{value:(T==null?void 0:T.school_districts_display)||"306",label:"School Districts",description:"NCES-validated educational boundaries",color:"#8B4513"},{value:T!=null&&T.states_with_data?`${T.states_with_data} State${T.states_with_data!==1?"s":""}`:"5 States",label:"State Coverage",description:"Including territories and tribal nations",color:"#2E8B57"},{value:(T==null?void 0:T.grant_opportunities)||"1,000s",label:"Grant Opportunities",description:"Federal, state, and foundation funding",color:"#FF6B6B"},{value:(T==null?void 0:T.fact_checks)||"N/A",label:"Fact-Checked Claims",description:"PolitiFact, FactCheck.org integration",color:"#4ECDC4"},{value:"100%",label:"Free & Open Source",description:"MIT License, HuggingFace datasets",color:"#95E1D3"}].map((Y,ne)=>o.jsxs("div",{className:"text-center p-8 rounded-2xl bg-gradient-to-br from-gray-50 to-white shadow-md hover:shadow-xl transition-shadow group",children:[o.jsx("div",{className:"text-5xl font-bold mb-2 group-hover:scale-110 transition-transform",style:{color:Y.color},children:Y.value}),o.jsx("div",{className:"text-gray-800 font-semibold mb-1",children:Y.label}),o.jsx("div",{className:"text-sm text-gray-600",children:Y.description})]},ne))})]})}),o.jsx("section",{id:"get-started",className:"py-20 px-4",style:{background:"linear-gradient(135deg, #354F52 0%, #52796F 100%)"},children:o.jsxs("div",{className:"max-w-7xl mx-auto",children:[o.jsxs("div",{className:"text-center mb-12",children:[o.jsx(Zf,{className:"h-16 w-16 mx-auto mb-6 text-white"}),o.jsx("h2",{className:"text-4xl md:text-5xl font-bold mb-6 text-white",children:"Documentation & Resources"}),o.jsx("p",{className:"text-xl text-white/90",children:"Choose your path based on your role and technical expertise"})]}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-6",children:[o.jsxs("a",{href:`${j}/intro`,target:"_blank",rel:"noopener noreferrer",className:"bg-white rounded-2xl p-8 shadow-xl hover:shadow-2xl transform hover:-translate-y-1 transition-all group",children:[o.jsx(jo,{className:"h-12 w-12 mb-4 group-hover:scale-110 transition-transform",style:{color:"#354F52"}}),o.jsx("h3",{className:"text-2xl font-bold mb-3",style:{color:"#354F52"},children:"Getting Started"}),o.jsx("p",{className:"text-gray-600 mb-4",children:"New here? Start with our quick introduction and dashboard overview."}),o.jsx("div",{className:"text-sm font-semibold",style:{color:"#354F52"},children:"For Everyone →"})]}),o.jsxs("a",{href:`${j}/for-families`,target:"_blank",rel:"noopener noreferrer",className:"bg-white rounded-2xl p-8 shadow-xl hover:shadow-2xl transform hover:-translate-y-1 transition-all group",children:[o.jsx($t,{className:"h-12 w-12 mb-4 group-hover:scale-110 transition-transform",style:{color:"#354F52"}}),o.jsx("h3",{className:"text-2xl font-bold mb-3",style:{color:"#354F52"},children:"Families & Individuals"}),o.jsx("p",{className:"text-gray-600 mb-4",children:"Community events, voter registration, services, and how to engage locally."}),o.jsx("div",{className:"text-sm font-semibold",style:{color:"#354F52"},children:"Community Resources →"})]}),o.jsxs("a",{href:`${j}/for-advocates`,target:"_blank",rel:"noopener noreferrer",className:"bg-white rounded-2xl p-8 shadow-xl hover:shadow-2xl transform hover:-translate-y-1 transition-all group",children:[o.jsx(Eo,{className:"h-12 w-12 mb-4 group-hover:scale-110 transition-transform",style:{color:"#354F52"}}),o.jsx("h3",{className:"text-2xl font-bold mb-3",style:{color:"#354F52"},children:"Policy Makers"}),o.jsx("p",{className:"text-gray-600 mb-4",children:"Case studies, data insights, and how to use the platform for advocacy."}),o.jsx("div",{className:"text-sm font-semibold",style:{color:"#354F52"},children:"Non-Technical →"})]}),o.jsxs("a",{href:`${j}/for-developers`,target:"_blank",rel:"noopener noreferrer",className:"bg-white rounded-2xl p-8 shadow-xl hover:shadow-2xl transform hover:-translate-y-1 transition-all group",children:[o.jsx($r,{className:"h-12 w-12 mb-4 group-hover:scale-110 transition-transform",style:{color:"#354F52"}}),o.jsx("h3",{className:"text-2xl font-bold mb-3",style:{color:"#354F52"},children:"Developers"}),o.jsx("p",{className:"text-gray-600 mb-4",children:"Setup guides, API docs, deployment instructions, and integrations."}),o.jsx("div",{className:"text-sm font-semibold",style:{color:"#354F52"},children:"Technical →"})]}),o.jsxs("a",{href:`${j}/intro`,target:"_blank",rel:"noopener noreferrer",className:"bg-white rounded-2xl p-8 shadow-xl hover:shadow-2xl transform hover:-translate-y-1 transition-all group",children:[o.jsx($B,{className:"h-12 w-12 mb-4 group-hover:scale-110 transition-transform",style:{color:"#354F52"}}),o.jsx("h3",{className:"text-2xl font-bold mb-3",style:{color:"#354F52"},children:"Full Docs"}),o.jsx("p",{className:"text-gray-600 mb-4",children:"Complete documentation including data sources, guides, and more."}),o.jsx("div",{className:"text-sm font-semibold",style:{color:"#354F52"},children:"Browse All →"})]})]}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-6 mt-12 max-w-4xl mx-auto",children:[o.jsx(ke,{to:"/people",className:"px-8 py-4 bg-white rounded-xl font-semibold hover:shadow-xl transition-all text-center",style:{color:"#354F52"},children:"Browse Leaders →"}),o.jsx(ke,{to:"/nonprofits",className:"px-8 py-4 bg-white/10 border-2 border-white rounded-xl text-white font-semibold hover:bg-white/20 transition-all text-center",children:"Explore Charities →"})]})]})}),o.jsx("section",{id:"contact",className:"py-20 px-4 bg-gradient-to-br from-gray-50 to-blue-50",children:o.jsx("div",{className:"max-w-4xl mx-auto",children:o.jsxs("div",{className:"bg-white rounded-2xl shadow-xl p-12 text-center",children:[o.jsx(N5,{className:"h-16 w-16 mx-auto mb-6",style:{color:"#354F52"}}),o.jsx("h2",{className:"text-4xl md:text-5xl font-bold mb-4",style:{color:"#354F52"},children:"Get in Touch"}),o.jsx("p",{className:"text-xl text-gray-600 mb-8",children:"Questions, feedback, or ideas? We'd love to hear from you. Report bugs, request features, or ask questions about jurisdiction coverage."}),o.jsxs("div",{className:"flex flex-col sm:flex-row gap-4 justify-center",children:[o.jsxs("a",{href:"https://github.com/getcommunityone/open-navigator-for-engagement/issues/new",target:"_blank",rel:"noopener noreferrer",className:"inline-flex items-center justify-center px-8 py-4 rounded-lg text-white font-semibold transition-all hover:shadow-lg",style:{backgroundColor:"#354F52"},children:[o.jsx(N5,{className:"h-5 w-5 mr-2"}),"Contact Us on GitHub"]}),o.jsx("a",{href:"mailto:johnbowyer@communityone.com",className:"inline-flex items-center justify-center px-8 py-4 rounded-lg bg-white border-2 text-[#354F52] font-semibold transition-all hover:bg-gray-50",style:{borderColor:"#354F52"},children:"Email Us Directly"})]}),o.jsx("p",{className:"text-sm text-gray-500 mt-6",children:"Your feedback helps us improve the platform for everyone."})]})})}),o.jsx("footer",{className:"py-12 px-4 bg-gray-900 text-white",children:o.jsxs("div",{className:"max-w-7xl mx-auto text-center",children:[o.jsxs("div",{className:"flex items-center justify-center gap-3 mb-6",children:[o.jsx("img",{src:"/communityone_logo.svg",alt:"CommunityOne Logo",className:"h-10 opacity-90"}),o.jsx("span",{className:"text-xl font-bold",children:"Open Navigator"})]}),o.jsx("p",{className:"text-gray-400 mb-6",children:"Making community impact accessible to everyone"}),o.jsxs("div",{className:"flex justify-center gap-6 mb-8",children:[o.jsx("a",{href:"https://www.instagram.com/getcommunityone/",target:"_blank",rel:"noopener noreferrer",className:"text-gray-400 hover:text-white transition-colors","aria-label":"Instagram",children:o.jsx("svg",{className:"w-6 h-6",fill:"currentColor",viewBox:"0 0 24 24",children:o.jsx("path",{d:"M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"})})}),o.jsx("a",{href:"https://www.facebook.com/getcommunityone",target:"_blank",rel:"noopener noreferrer",className:"text-gray-400 hover:text-white transition-colors","aria-label":"Facebook",children:o.jsx("svg",{className:"w-6 h-6",fill:"currentColor",viewBox:"0 0 24 24",children:o.jsx("path",{d:"M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"})})}),o.jsx("a",{href:"https://x.com/getcommunityone/",target:"_blank",rel:"noopener noreferrer",className:"text-gray-400 hover:text-white transition-colors","aria-label":"X (Twitter)",children:o.jsx("svg",{className:"w-6 h-6",fill:"currentColor",viewBox:"0 0 24 24",children:o.jsx("path",{d:"M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"})})}),o.jsx("a",{href:"https://www.linkedin.com/company/getcommunityone",target:"_blank",rel:"noopener noreferrer",className:"text-gray-400 hover:text-white transition-colors","aria-label":"LinkedIn",children:o.jsx("svg",{className:"w-6 h-6",fill:"currentColor",viewBox:"0 0 24 24",children:o.jsx("path",{d:"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"})})}),o.jsx("a",{href:"https://github.com/getcommunityone/open-navigator-for-engagement",target:"_blank",rel:"noopener noreferrer",className:"text-gray-400 hover:text-white transition-colors","aria-label":"GitHub",children:o.jsx("svg",{className:"w-6 h-6",fill:"currentColor",viewBox:"0 0 24 24",children:o.jsx("path",{d:"M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"})})})]}),o.jsxs("div",{className:"flex justify-center gap-8 text-sm text-gray-400",children:[o.jsx("a",{href:`${j}/intro`,target:"_blank",rel:"noopener noreferrer",className:"hover:text-white transition-colors",children:"Documentation"}),o.jsx("a",{href:`${E}/api/docs`,target:"_blank",rel:"noopener noreferrer",className:"hover:text-white transition-colors",children:"API"}),o.jsx(ke,{to:"/explore",className:"hover:text-white transition-colors",children:"Explore"})]}),o.jsx("p",{className:"text-gray-500 text-sm mt-6",children:"© 2026 CommunityOne. All rights reserved."})]})})]})}function TA(e){var t,n,r="";if(typeof e=="string"||typeof e=="number")r+=e;else if(typeof e=="object")if(Array.isArray(e)){var i=e.length;for(t=0;t-1}var RW=IW,FW=l0;function DW(e,t){var n=this.__data__,r=FW(n,e);return r<0?(++this.size,n.push([e,t])):n[r][1]=t,this}var BW=DW,zW=jW,UW=AW,WW=LW,HW=RW,VW=BW;function Xc(e){var t=-1,n=e==null?0:e.length;for(this.clear();++t0?1:-1},as=function(t){return Os(t)&&t.indexOf("%")===t.length-1},ue=function(t){return dV(t)&&!Kf(t)},pV=function(t){return De(t)},Gt=function(t){return ue(t)||Os(t)},gV=0,Yf=function(t){var n=++gV;return"".concat(t||"").concat(n)},Ln=function(t,n){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:0,i=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!1;if(!ue(t)&&!Os(t))return r;var a;if(as(t)){var s=t.indexOf("%");a=n*parseFloat(t.slice(0,s))/100}else a=+t;return Kf(a)&&(a=r),i&&a>n&&(a=n),a},Xa=function(t){if(!t)return null;var n=Object.keys(t);return n&&n.length?t[n[0]]:null},vV=function(t){if(!Array.isArray(t))return!1;for(var n=t.length,r={},i=0;i=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function NV(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}var F5={click:"onClick",mousedown:"onMouseDown",mouseup:"onMouseUp",mouseover:"onMouseOver",mousemove:"onMouseMove",mouseout:"onMouseOut",mouseenter:"onMouseEnter",mouseleave:"onMouseLeave",touchcancel:"onTouchCancel",touchend:"onTouchEnd",touchmove:"onTouchMove",touchstart:"onTouchStart",contextmenu:"onContextMenu",dblclick:"onDoubleClick"},xa=function(t){return typeof t=="string"?t:t?t.displayName||t.name||"Component":""},D5=null,xy=null,s2=function e(t){if(t===D5&&Array.isArray(xy))return xy;var n=[];return N.Children.forEach(t,function(r){De(r)||(oV.isFragment(r)?n=n.concat(e(r.props.children)):n.push(r))}),xy=n,D5=t,n};function Dr(e,t){var n=[],r=[];return Array.isArray(t)?r=t.map(function(i){return xa(i)}):r=[xa(t)],s2(e).forEach(function(i){var a=gr(i,"type.displayName")||gr(i,"type.name");r.indexOf(a)!==-1&&n.push(i)}),n}function or(e,t){var n=Dr(e,t);return n&&n[0]}var B5=function(t){if(!t||!t.props)return!1;var n=t.props,r=n.width,i=n.height;return!(!ue(r)||r<=0||!ue(i)||i<=0)},SV=["a","altGlyph","altGlyphDef","altGlyphItem","animate","animateColor","animateMotion","animateTransform","circle","clipPath","color-profile","cursor","defs","desc","ellipse","feBlend","feColormatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","font","font-face","font-face-format","font-face-name","font-face-url","foreignObject","g","glyph","glyphRef","hkern","image","line","lineGradient","marker","mask","metadata","missing-glyph","mpath","path","pattern","polygon","polyline","radialGradient","rect","script","set","stop","style","svg","switch","symbol","text","textPath","title","tref","tspan","use","view","vkern"],PV=function(t){return t&&t.type&&Os(t.type)&&SV.indexOf(t.type)>=0},EV=function(t,n,r,i){var a,s=(a=yy==null?void 0:yy[i])!==null&&a!==void 0?a:[];return n.startsWith("data-")||!Ce(t)&&(i&&s.includes(n)||bV.includes(n))||r&&o2.includes(n)},Oe=function(t,n,r){if(!t||typeof t=="function"||typeof t=="boolean")return null;var i=t;if(N.isValidElement(t)&&(i=t.props),!Kc(i))return null;var a={};return Object.keys(i).forEach(function(s){var c;EV((c=i)===null||c===void 0?void 0:c[s],s,n,r)&&(a[s]=i[s])}),a},Ob=function e(t,n){if(t===n)return!0;var r=N.Children.count(t);if(r!==N.Children.count(n))return!1;if(r===0)return!0;if(r===1)return z5(Array.isArray(t)?t[0]:t,Array.isArray(n)?n[0]:n);for(var i=0;i=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function TV(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}function Cb(e){var t=e.children,n=e.width,r=e.height,i=e.viewBox,a=e.className,s=e.style,c=e.title,u=e.desc,d=AV(e,CV),h=i||{width:n,height:r,x:0,y:0},m=Ie("recharts-surface",a);return H.createElement("svg",kb({},Oe(d,!0,"svg"),{className:m,width:n,height:r,style:s,viewBox:"".concat(h.x," ").concat(h.y," ").concat(h.width," ").concat(h.height)}),H.createElement("title",null,c),H.createElement("desc",null,u),t)}var MV=["children","className"];function Ab(){return Ab=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function $V(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}var Ye=H.forwardRef(function(e,t){var n=e.children,r=e.className,i=LV(e,MV),a=Ie("recharts-layer",r);return H.createElement("g",Ab({className:a},Oe(i,!0),{ref:t}),n)}),hi=function(t,n){for(var r=arguments.length,i=new Array(r>2?r-2:0),a=2;ai?0:i+t),n=n>i?i:n,n<0&&(n+=i),i=t>n?0:n-t>>>0,t>>>=0;for(var a=Array(i);++r=r?e:FV(e,t,n)}var BV=DV,zV="\\ud800-\\udfff",UV="\\u0300-\\u036f",WV="\\ufe20-\\ufe2f",HV="\\u20d0-\\u20ff",VV=UV+WV+HV,qV="\\ufe0e\\ufe0f",ZV="\\u200d",GV=RegExp("["+ZV+zV+VV+qV+"]");function KV(e){return GV.test(e)}var VA=KV;function YV(e){return e.split("")}var XV=YV,qA="\\ud800-\\udfff",QV="\\u0300-\\u036f",JV="\\ufe20-\\ufe2f",eq="\\u20d0-\\u20ff",tq=QV+JV+eq,nq="\\ufe0e\\ufe0f",rq="["+qA+"]",Tb="["+tq+"]",Mb="\\ud83c[\\udffb-\\udfff]",iq="(?:"+Tb+"|"+Mb+")",ZA="[^"+qA+"]",GA="(?:\\ud83c[\\udde6-\\uddff]){2}",KA="[\\ud800-\\udbff][\\udc00-\\udfff]",aq="\\u200d",YA=iq+"?",XA="["+nq+"]?",oq="(?:"+aq+"(?:"+[ZA,GA,KA].join("|")+")"+XA+YA+")*",sq=XA+YA+oq,lq="(?:"+[ZA+Tb+"?",Tb,GA,KA,rq].join("|")+")",cq=RegExp(Mb+"(?="+Mb+")|"+lq+sq,"g");function uq(e){return e.match(cq)||[]}var dq=uq,fq=XV,hq=VA,mq=dq;function pq(e){return hq(e)?mq(e):fq(e)}var gq=pq,vq=BV,yq=VA,xq=gq,bq=DA;function wq(e){return function(t){t=bq(t);var n=yq(t)?xq(t):void 0,r=n?n[0]:t.charAt(0),i=n?vq(n,1).join(""):t.slice(1);return r[e]()+i}}var _q=wq,jq=_q,Nq=jq("toUpperCase"),Sq=Nq;const _0=Qe(Sq);function st(e){return function(){return e}}const QA=Math.cos,hp=Math.sin,gi=Math.sqrt,mp=Math.PI,j0=2*mp,Lb=Math.PI,$b=2*Lb,Ko=1e-6,Pq=$b-Ko;function JA(e){this._+=e[0];for(let t=1,n=e.length;t=0))throw new Error(`invalid digits: ${e}`);if(t>15)return JA;const n=10**t;return function(r){this._+=r[0];for(let i=1,a=r.length;iKo)if(!(Math.abs(m*u-d*h)>Ko)||!a)this._append`L${this._x1=t},${this._y1=n}`;else{let v=r-s,_=i-c,x=u*u+d*d,y=v*v+_*_,w=Math.sqrt(x),b=Math.sqrt(p),j=a*Math.tan((Lb-Math.acos((x+p-y)/(2*w*b)))/2),E=j/b,P=j/w;Math.abs(E-1)>Ko&&this._append`L${t+E*h},${n+E*m}`,this._append`A${a},${a},0,0,${+(m*v>h*_)},${this._x1=t+P*u},${this._y1=n+P*d}`}}arc(t,n,r,i,a,s){if(t=+t,n=+n,r=+r,s=!!s,r<0)throw new Error(`negative radius: ${r}`);let c=r*Math.cos(i),u=r*Math.sin(i),d=t+c,h=n+u,m=1^s,p=s?i-a:a-i;this._x1===null?this._append`M${d},${h}`:(Math.abs(this._x1-d)>Ko||Math.abs(this._y1-h)>Ko)&&this._append`L${d},${h}`,r&&(p<0&&(p=p%$b+$b),p>Pq?this._append`A${r},${r},0,1,${m},${t-c},${n-u}A${r},${r},0,1,${m},${this._x1=d},${this._y1=h}`:p>Ko&&this._append`A${r},${r},0,${+(p>=Lb)},${m},${this._x1=t+r*Math.cos(a)},${this._y1=n+r*Math.sin(a)}`)}rect(t,n,r,i){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+n}h${r=+r}v${+i}h${-r}Z`}toString(){return this._}}function l2(e){let t=3;return e.digits=function(n){if(!arguments.length)return t;if(n==null)t=null;else{const r=Math.floor(n);if(!(r>=0))throw new RangeError(`invalid digits: ${n}`);t=r}return e},()=>new Oq(t)}function c2(e){return typeof e=="object"&&"length"in e?e:Array.from(e)}function eT(e){this._context=e}eT.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;default:this._context.lineTo(e,t);break}}};function N0(e){return new eT(e)}function tT(e){return e[0]}function nT(e){return e[1]}function rT(e,t){var n=st(!0),r=null,i=N0,a=null,s=l2(c);e=typeof e=="function"?e:e===void 0?tT:st(e),t=typeof t=="function"?t:t===void 0?nT:st(t);function c(u){var d,h=(u=c2(u)).length,m,p=!1,v;for(r==null&&(a=i(v=s())),d=0;d<=h;++d)!(d=v;--_)c.point(j[_],E[_]);c.lineEnd(),c.areaEnd()}w&&(j[p]=+e(y,p,m),E[p]=+t(y,p,m),c.point(r?+r(y,p,m):j[p],n?+n(y,p,m):E[p]))}if(b)return c=null,b+""||null}function h(){return rT().defined(i).curve(s).context(a)}return d.x=function(m){return arguments.length?(e=typeof m=="function"?m:st(+m),r=null,d):e},d.x0=function(m){return arguments.length?(e=typeof m=="function"?m:st(+m),d):e},d.x1=function(m){return arguments.length?(r=m==null?null:typeof m=="function"?m:st(+m),d):r},d.y=function(m){return arguments.length?(t=typeof m=="function"?m:st(+m),n=null,d):t},d.y0=function(m){return arguments.length?(t=typeof m=="function"?m:st(+m),d):t},d.y1=function(m){return arguments.length?(n=m==null?null:typeof m=="function"?m:st(+m),d):n},d.lineX0=d.lineY0=function(){return h().x(e).y(t)},d.lineY1=function(){return h().x(e).y(n)},d.lineX1=function(){return h().x(r).y(t)},d.defined=function(m){return arguments.length?(i=typeof m=="function"?m:st(!!m),d):i},d.curve=function(m){return arguments.length?(s=m,a!=null&&(c=s(a)),d):s},d.context=function(m){return arguments.length?(m==null?a=c=null:c=s(a=m),d):a},d}class iT{constructor(t,n){this._context=t,this._x=n}areaStart(){this._line=0}areaEnd(){this._line=NaN}lineStart(){this._point=0}lineEnd(){(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line}point(t,n){switch(t=+t,n=+n,this._point){case 0:{this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break}case 1:this._point=2;default:{this._x?this._context.bezierCurveTo(this._x0=(this._x0+t)/2,this._y0,this._x0,n,t,n):this._context.bezierCurveTo(this._x0,this._y0=(this._y0+n)/2,t,this._y0,t,n);break}}this._x0=t,this._y0=n}}function kq(e){return new iT(e,!0)}function Cq(e){return new iT(e,!1)}const u2={draw(e,t){const n=gi(t/mp);e.moveTo(n,0),e.arc(0,0,n,0,j0)}},Aq={draw(e,t){const n=gi(t/5)/2;e.moveTo(-3*n,-n),e.lineTo(-n,-n),e.lineTo(-n,-3*n),e.lineTo(n,-3*n),e.lineTo(n,-n),e.lineTo(3*n,-n),e.lineTo(3*n,n),e.lineTo(n,n),e.lineTo(n,3*n),e.lineTo(-n,3*n),e.lineTo(-n,n),e.lineTo(-3*n,n),e.closePath()}},aT=gi(1/3),Tq=aT*2,Mq={draw(e,t){const n=gi(t/Tq),r=n*aT;e.moveTo(0,-n),e.lineTo(r,0),e.lineTo(0,n),e.lineTo(-r,0),e.closePath()}},Lq={draw(e,t){const n=gi(t),r=-n/2;e.rect(r,r,n,n)}},$q=.8908130915292852,oT=hp(mp/10)/hp(7*mp/10),Iq=hp(j0/10)*oT,Rq=-QA(j0/10)*oT,Fq={draw(e,t){const n=gi(t*$q),r=Iq*n,i=Rq*n;e.moveTo(0,-n),e.lineTo(r,i);for(let a=1;a<5;++a){const s=j0*a/5,c=QA(s),u=hp(s);e.lineTo(u*n,-c*n),e.lineTo(c*r-u*i,u*r+c*i)}e.closePath()}},by=gi(3),Dq={draw(e,t){const n=-gi(t/(by*3));e.moveTo(0,n*2),e.lineTo(-by*n,-n),e.lineTo(by*n,-n),e.closePath()}},Nr=-.5,Sr=gi(3)/2,Ib=1/gi(12),Bq=(Ib/2+1)*3,zq={draw(e,t){const n=gi(t/Bq),r=n/2,i=n*Ib,a=r,s=n*Ib+n,c=-a,u=s;e.moveTo(r,i),e.lineTo(a,s),e.lineTo(c,u),e.lineTo(Nr*r-Sr*i,Sr*r+Nr*i),e.lineTo(Nr*a-Sr*s,Sr*a+Nr*s),e.lineTo(Nr*c-Sr*u,Sr*c+Nr*u),e.lineTo(Nr*r+Sr*i,Nr*i-Sr*r),e.lineTo(Nr*a+Sr*s,Nr*s-Sr*a),e.lineTo(Nr*c+Sr*u,Nr*u-Sr*c),e.closePath()}};function Uq(e,t){let n=null,r=l2(i);e=typeof e=="function"?e:st(e||u2),t=typeof t=="function"?t:st(t===void 0?64:+t);function i(){let a;if(n||(n=a=r()),e.apply(this,arguments).draw(n,+t.apply(this,arguments)),a)return n=null,a+""||null}return i.type=function(a){return arguments.length?(e=typeof a=="function"?a:st(a),i):e},i.size=function(a){return arguments.length?(t=typeof a=="function"?a:st(+a),i):t},i.context=function(a){return arguments.length?(n=a??null,i):n},i}function pp(){}function gp(e,t,n){e._context.bezierCurveTo((2*e._x0+e._x1)/3,(2*e._y0+e._y1)/3,(e._x0+2*e._x1)/3,(e._y0+2*e._y1)/3,(e._x0+4*e._x1+t)/6,(e._y0+4*e._y1+n)/6)}function sT(e){this._context=e}sT.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:gp(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1);break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:gp(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function Wq(e){return new sT(e)}function lT(e){this._context=e}lT.prototype={areaStart:pp,areaEnd:pp,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:{this._context.moveTo(this._x2,this._y2),this._context.closePath();break}case 2:{this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break}case 3:{this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4);break}}},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._x2=e,this._y2=t;break;case 1:this._point=2,this._x3=e,this._y3=t;break;case 2:this._point=3,this._x4=e,this._y4=t,this._context.moveTo((this._x0+4*this._x1+e)/6,(this._y0+4*this._y1+t)/6);break;default:gp(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function Hq(e){return new lT(e)}function cT(e){this._context=e}cT.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||this._line!==0&&this._point===3)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var n=(this._x0+4*this._x1+e)/6,r=(this._y0+4*this._y1+t)/6;this._line?this._context.lineTo(n,r):this._context.moveTo(n,r);break;case 3:this._point=4;default:gp(this,e,t);break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t}};function Vq(e){return new cT(e)}function uT(e){this._context=e}uT.prototype={areaStart:pp,areaEnd:pp,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(e,t){e=+e,t=+t,this._point?this._context.lineTo(e,t):(this._point=1,this._context.moveTo(e,t))}};function qq(e){return new uT(e)}function W5(e){return e<0?-1:1}function H5(e,t,n){var r=e._x1-e._x0,i=t-e._x1,a=(e._y1-e._y0)/(r||i<0&&-0),s=(n-e._y1)/(i||r<0&&-0),c=(a*i+s*r)/(r+i);return(W5(a)+W5(s))*Math.min(Math.abs(a),Math.abs(s),.5*Math.abs(c))||0}function V5(e,t){var n=e._x1-e._x0;return n?(3*(e._y1-e._y0)/n-t)/2:t}function wy(e,t,n){var r=e._x0,i=e._y0,a=e._x1,s=e._y1,c=(a-r)/3;e._context.bezierCurveTo(r+c,i+c*t,a-c,s-c*n,a,s)}function vp(e){this._context=e}vp.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:wy(this,this._t0,V5(this,this._t0));break}(this._line||this._line!==0&&this._point===1)&&this._context.closePath(),this._line=1-this._line},point:function(e,t){var n=NaN;if(e=+e,t=+t,!(e===this._x1&&t===this._y1)){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;break;case 2:this._point=3,wy(this,V5(this,n=H5(this,e,t)),n);break;default:wy(this,this._t0,n=H5(this,e,t));break}this._x0=this._x1,this._x1=e,this._y0=this._y1,this._y1=t,this._t0=n}}};function dT(e){this._context=new fT(e)}(dT.prototype=Object.create(vp.prototype)).point=function(e,t){vp.prototype.point.call(this,t,e)};function fT(e){this._context=e}fT.prototype={moveTo:function(e,t){this._context.moveTo(t,e)},closePath:function(){this._context.closePath()},lineTo:function(e,t){this._context.lineTo(t,e)},bezierCurveTo:function(e,t,n,r,i,a){this._context.bezierCurveTo(t,e,r,n,a,i)}};function Zq(e){return new vp(e)}function Gq(e){return new dT(e)}function hT(e){this._context=e}hT.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var e=this._x,t=this._y,n=e.length;if(n)if(this._line?this._context.lineTo(e[0],t[0]):this._context.moveTo(e[0],t[0]),n===2)this._context.lineTo(e[1],t[1]);else for(var r=q5(e),i=q5(t),a=0,s=1;s=0;--t)i[t]=(s[t]-i[t+1])/a[t];for(a[n-1]=(e[n]+i[n-1])/2,t=0;t=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(e,t){switch(e=+e,t=+t,this._point){case 0:this._point=1,this._line?this._context.lineTo(e,t):this._context.moveTo(e,t);break;case 1:this._point=2;default:{if(this._t<=0)this._context.lineTo(this._x,t),this._context.lineTo(e,t);else{var n=this._x*(1-this._t)+e*this._t;this._context.lineTo(n,this._y),this._context.lineTo(n,t)}break}}this._x=e,this._y=t}};function Yq(e){return new S0(e,.5)}function Xq(e){return new S0(e,0)}function Qq(e){return new S0(e,1)}function hc(e,t){if((s=e.length)>1)for(var n=1,r,i,a=e[t[0]],s,c=a.length;n=0;)n[t]=t;return n}function Jq(e,t){return e[t]}function eZ(e){const t=[];return t.key=e,t}function tZ(){var e=st([]),t=Rb,n=hc,r=Jq;function i(a){var s=Array.from(e.apply(this,arguments),eZ),c,u=s.length,d=-1,h;for(const m of a)for(c=0,++d;c0){for(var n,r,i=0,a=e[0].length,s;i0){for(var n=0,r=e[t[0]],i,a=r.length;n0)||!((a=(i=e[t[0]]).length)>0))){for(var n=0,r=1,i,a,s;r=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function uZ(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}var mT={symbolCircle:u2,symbolCross:Aq,symbolDiamond:Mq,symbolSquare:Lq,symbolStar:Fq,symbolTriangle:Dq,symbolWye:zq},dZ=Math.PI/180,fZ=function(t){var n="symbol".concat(_0(t));return mT[n]||u2},hZ=function(t,n,r){if(n==="area")return t;switch(r){case"cross":return 5*t*t/9;case"diamond":return .5*t*t/Math.sqrt(3);case"square":return t*t;case"star":{var i=18*dZ;return 1.25*t*t*(Math.tan(i)-Math.tan(i*2)*Math.pow(Math.tan(i),2))}case"triangle":return Math.sqrt(3)*t*t/4;case"wye":return(21-10*Math.sqrt(3))*t*t/8;default:return Math.PI*t*t/4}},mZ=function(t,n){mT["symbol".concat(_0(t))]=n},d2=function(t){var n=t.type,r=n===void 0?"circle":n,i=t.size,a=i===void 0?64:i,s=t.sizeType,c=s===void 0?"area":s,u=cZ(t,aZ),d=G5(G5({},u),{},{type:r,size:a,sizeType:c}),h=function(){var y=fZ(r),w=Uq().type(y).size(hZ(a,c,r));return w()},m=d.className,p=d.cx,v=d.cy,_=Oe(d,!0);return p===+p&&v===+v&&a===+a?H.createElement("path",Fb({},_,{className:Ie("recharts-symbols",m),transform:"translate(".concat(p,", ").concat(v,")"),d:h()})):null};d2.registerSymbol=mZ;function mc(e){"@babel/helpers - typeof";return mc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},mc(e)}function Db(){return Db=Object.assign?Object.assign.bind():function(e){for(var t=1;t`);var b=v.inactive?d:v.color;return H.createElement("li",Db({className:y,style:m,key:"legend-item-".concat(_)},ks(r.props,v,_)),H.createElement(Cb,{width:s,height:s,viewBox:h,style:p},r.renderIcon(v)),H.createElement("span",{className:"recharts-legend-item-text",style:{color:b}},x?x(w,v,_):w))})}},{key:"render",value:function(){var r=this.props,i=r.payload,a=r.layout,s=r.align;if(!i||!i.length)return null;var c={padding:0,margin:0,textAlign:a==="horizontal"?s:"left"};return H.createElement("ul",{className:"recharts-default-legend",style:c},this.renderItems())}}])}(N.PureComponent);Zd(f2,"displayName","Legend");Zd(f2,"defaultProps",{iconSize:14,layout:"horizontal",align:"center",verticalAlign:"middle",inactiveColor:"#ccc"});var NZ=c0;function SZ(){this.__data__=new NZ,this.size=0}var PZ=SZ;function EZ(e){var t=this.__data__,n=t.delete(e);return this.size=t.size,n}var OZ=EZ;function kZ(e){return this.__data__.get(e)}var CZ=kZ;function AZ(e){return this.__data__.has(e)}var TZ=AZ,MZ=c0,LZ=J_,$Z=e2,IZ=200;function RZ(e,t){var n=this.__data__;if(n instanceof MZ){var r=n.__data__;if(!LZ||r.lengthc))return!1;var d=a.get(e),h=a.get(t);if(d&&h)return d==t&&h==e;var m=-1,p=!0,v=n&aG?new tG:void 0;for(a.set(e,t),a.set(t,e);++m-1&&e%1==0&&e-1&&e%1==0&&e<=cK}var g2=uK,dK=$a,fK=g2,hK=Ia,mK="[object Arguments]",pK="[object Array]",gK="[object Boolean]",vK="[object Date]",yK="[object Error]",xK="[object Function]",bK="[object Map]",wK="[object Number]",_K="[object Object]",jK="[object RegExp]",NK="[object Set]",SK="[object String]",PK="[object WeakMap]",EK="[object ArrayBuffer]",OK="[object DataView]",kK="[object Float32Array]",CK="[object Float64Array]",AK="[object Int8Array]",TK="[object Int16Array]",MK="[object Int32Array]",LK="[object Uint8Array]",$K="[object Uint8ClampedArray]",IK="[object Uint16Array]",RK="[object Uint32Array]",ht={};ht[kK]=ht[CK]=ht[AK]=ht[TK]=ht[MK]=ht[LK]=ht[$K]=ht[IK]=ht[RK]=!0;ht[mK]=ht[pK]=ht[EK]=ht[gK]=ht[OK]=ht[vK]=ht[yK]=ht[xK]=ht[bK]=ht[wK]=ht[_K]=ht[jK]=ht[NK]=ht[SK]=ht[PK]=!1;function FK(e){return hK(e)&&fK(e.length)&&!!ht[dK(e)]}var DK=FK;function BK(e){return function(t){return e(t)}}var ST=BK,wp={exports:{}};wp.exports;(function(e,t){var n=MA,r=t&&!t.nodeType&&t,i=r&&!0&&e&&!e.nodeType&&e,a=i&&i.exports===r,s=a&&n.process,c=function(){try{var u=i&&i.require&&i.require("util").types;return u||s&&s.binding&&s.binding("util")}catch{}}();e.exports=c})(wp,wp.exports);var zK=wp.exports,UK=DK,WK=ST,tP=zK,nP=tP&&tP.isTypedArray,HK=nP?WK(nP):UK,PT=HK,VK=GG,qK=m2,ZK=tr,GK=NT,KK=p2,YK=PT,XK=Object.prototype,QK=XK.hasOwnProperty;function JK(e,t){var n=ZK(e),r=!n&&qK(e),i=!n&&!r&&GK(e),a=!n&&!r&&!i&&YK(e),s=n||r||i||a,c=s?VK(e.length,String):[],u=c.length;for(var d in e)(t||QK.call(e,d))&&!(s&&(d=="length"||i&&(d=="offset"||d=="parent")||a&&(d=="buffer"||d=="byteLength"||d=="byteOffset")||KK(d,u)))&&c.push(d);return c}var eY=JK,tY=Object.prototype;function nY(e){var t=e&&e.constructor,n=typeof t=="function"&&t.prototype||tY;return e===n}var rY=nY;function iY(e,t){return function(n){return e(t(n))}}var ET=iY,aY=ET,oY=aY(Object.keys,Object),sY=oY,lY=rY,cY=sY,uY=Object.prototype,dY=uY.hasOwnProperty;function fY(e){if(!lY(e))return cY(e);var t=[];for(var n in Object(e))dY.call(e,n)&&n!="constructor"&&t.push(n);return t}var hY=fY,mY=X_,pY=g2;function gY(e){return e!=null&&pY(e.length)&&!mY(e)}var Xf=gY,vY=eY,yY=hY,xY=Xf;function bY(e){return xY(e)?vY(e):yY(e)}var P0=bY,wY=IG,_Y=qG,jY=P0;function NY(e){return wY(e,jY,_Y)}var SY=NY,rP=SY,PY=1,EY=Object.prototype,OY=EY.hasOwnProperty;function kY(e,t,n,r,i,a){var s=n&PY,c=rP(e),u=c.length,d=rP(t),h=d.length;if(u!=h&&!s)return!1;for(var m=u;m--;){var p=c[m];if(!(s?p in t:OY.call(t,p)))return!1}var v=a.get(e),_=a.get(t);if(v&&_)return v==t&&_==e;var x=!0;a.set(e,t),a.set(t,e);for(var y=s;++m-1}var EQ=PQ;function OQ(e,t,n){for(var r=-1,i=e==null?0:e.length;++r=WQ){var d=t?null:zQ(e);if(d)return UQ(d);s=!1,i=BQ,u=new RQ}else u=t?[]:c;e:for(;++r=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function aJ(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}function oJ(e){return e.value}function sJ(e,t){if(H.isValidElement(e))return H.cloneElement(e,t);if(typeof e=="function")return H.createElement(e,t);t.ref;var n=iJ(t,YQ);return H.createElement(f2,n)}var xP=1,Dl=function(e){function t(){var n;XQ(this,t);for(var r=arguments.length,i=new Array(r),a=0;axP||Math.abs(i.height-this.lastBoundingBox.height)>xP)&&(this.lastBoundingBox.width=i.width,this.lastBoundingBox.height=i.height,r&&r(i)):(this.lastBoundingBox.width!==-1||this.lastBoundingBox.height!==-1)&&(this.lastBoundingBox.width=-1,this.lastBoundingBox.height=-1,r&&r(null))}},{key:"getBBoxSnapshot",value:function(){return this.lastBoundingBox.width>=0&&this.lastBoundingBox.height>=0?Qi({},this.lastBoundingBox):{width:0,height:0}}},{key:"getDefaultPosition",value:function(r){var i=this.props,a=i.layout,s=i.align,c=i.verticalAlign,u=i.margin,d=i.chartWidth,h=i.chartHeight,m,p;if(!r||(r.left===void 0||r.left===null)&&(r.right===void 0||r.right===null))if(s==="center"&&a==="vertical"){var v=this.getBBoxSnapshot();m={left:((d||0)-v.width)/2}}else m=s==="right"?{right:u&&u.right||0}:{left:u&&u.left||0};if(!r||(r.top===void 0||r.top===null)&&(r.bottom===void 0||r.bottom===null))if(c==="middle"){var _=this.getBBoxSnapshot();p={top:((h||0)-_.height)/2}}else p=c==="bottom"?{bottom:u&&u.bottom||0}:{top:u&&u.top||0};return Qi(Qi({},m),p)}},{key:"render",value:function(){var r=this,i=this.props,a=i.content,s=i.width,c=i.height,u=i.wrapperStyle,d=i.payloadUniqBy,h=i.payload,m=Qi(Qi({position:"absolute",width:s||"auto",height:c||"auto"},this.getDefaultPosition(u)),u);return H.createElement("div",{className:"recharts-legend-wrapper",style:m,ref:function(v){r.wrapperNode=v}},sJ(a,Qi(Qi({},this.props),{},{payload:LT(h,d,oJ)})))}}],[{key:"getWithHeight",value:function(r,i){var a=Qi(Qi({},this.defaultProps),r.props),s=a.layout;return s==="vertical"&&ue(r.props.height)?{height:r.props.height}:s==="horizontal"?{width:r.props.width||i}:null}}])}(N.PureComponent);E0(Dl,"displayName","Legend");E0(Dl,"defaultProps",{iconSize:14,layout:"horizontal",align:"center",verticalAlign:"bottom"});var bP=Gf,lJ=m2,cJ=tr,wP=bP?bP.isConcatSpreadable:void 0;function uJ(e){return cJ(e)||lJ(e)||!!(wP&&e&&e[wP])}var dJ=uJ,fJ=_T,hJ=dJ;function RT(e,t,n,r,i){var a=-1,s=e.length;for(n||(n=hJ),i||(i=[]);++a0&&n(c)?t>1?RT(c,t-1,n,r,i):fJ(i,c):r||(i[i.length]=c)}return i}var FT=RT;function mJ(e){return function(t,n,r){for(var i=-1,a=Object(t),s=r(t),c=s.length;c--;){var u=s[e?c:++i];if(n(a[u],u,a)===!1)break}return t}}var pJ=mJ,gJ=pJ,vJ=gJ(),yJ=vJ,xJ=yJ,bJ=P0;function wJ(e,t){return e&&xJ(e,t,bJ)}var DT=wJ,_J=Xf;function jJ(e,t){return function(n,r){if(n==null)return n;if(!_J(n))return e(n,r);for(var i=n.length,a=t?i:-1,s=Object(n);(t?a--:++at||a&&s&&u&&!c&&!d||r&&s&&u||!n&&u||!i)return 1;if(!r&&!a&&!d&&e=c)return u;var d=n[r];return u*(d=="desc"?-1:1)}}return e.index-t.index}var RJ=IJ,Sy=n2,FJ=r2,DJ=Wi,BJ=BT,zJ=TJ,UJ=ST,WJ=RJ,HJ=tu,VJ=tr;function qJ(e,t,n){t.length?t=Sy(t,function(a){return VJ(a)?function(s){return FJ(s,a.length===1?a[0]:a)}:a}):t=[HJ];var r=-1;t=Sy(t,UJ(DJ));var i=BJ(e,function(a,s,c){var u=Sy(t,function(d){return d(a)});return{criteria:u,index:++r,value:a}});return zJ(i,function(a,s){return WJ(a,s,n)})}var ZJ=qJ;function GJ(e,t,n){switch(n.length){case 0:return e.call(t);case 1:return e.call(t,n[0]);case 2:return e.call(t,n[0],n[1]);case 3:return e.call(t,n[0],n[1],n[2])}return e.apply(t,n)}var KJ=GJ,YJ=KJ,jP=Math.max;function XJ(e,t,n){return t=jP(t===void 0?e.length-1:t,0),function(){for(var r=arguments,i=-1,a=jP(r.length-t,0),s=Array(a);++i0){if(++t>=see)return arguments[0]}else t=0;return e.apply(void 0,arguments)}}var dee=uee,fee=oee,hee=dee,mee=hee(fee),pee=mee,gee=tu,vee=QJ,yee=pee;function xee(e,t){return yee(vee(e,t,gee),e+"")}var bee=xee,wee=Q_,_ee=Xf,jee=p2,Nee=Ao;function See(e,t,n){if(!Nee(n))return!1;var r=typeof t;return(r=="number"?_ee(n)&&jee(t,n.length):r=="string"&&t in n)?wee(n[t],e):!1}var O0=See,Pee=FT,Eee=ZJ,Oee=bee,SP=O0,kee=Oee(function(e,t){if(e==null)return[];var n=t.length;return n>1&&SP(e,t[0],t[1])?t=[]:n>2&&SP(t[0],t[1],t[2])&&(t=[t[0]]),Eee(e,Pee(t,1),[])}),Cee=kee;const x2=Qe(Cee);function Gd(e){"@babel/helpers - typeof";return Gd=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Gd(e)}function Zb(){return Zb=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n=t.x),"".concat(Cu,"-left"),ue(n)&&t&&ue(t.x)&&n=t.y),"".concat(Cu,"-top"),ue(r)&&t&&ue(t.y)&&rx?Math.max(h,u[r]):Math.max(m,u[r])}function Vee(e){var t=e.translateX,n=e.translateY,r=e.useTranslate3d;return{transform:r?"translate3d(".concat(t,"px, ").concat(n,"px, 0)"):"translate(".concat(t,"px, ").concat(n,"px)")}}function qee(e){var t=e.allowEscapeViewBox,n=e.coordinate,r=e.offsetTopLeft,i=e.position,a=e.reverseDirection,s=e.tooltipBox,c=e.useTranslate3d,u=e.viewBox,d,h,m;return s.height>0&&s.width>0&&n?(h=OP({allowEscapeViewBox:t,coordinate:n,key:"x",offsetTopLeft:r,position:i,reverseDirection:a,tooltipDimension:s.width,viewBox:u,viewBoxDimension:u.width}),m=OP({allowEscapeViewBox:t,coordinate:n,key:"y",offsetTopLeft:r,position:i,reverseDirection:a,tooltipDimension:s.height,viewBox:u,viewBoxDimension:u.height}),d=Vee({translateX:h,translateY:m,useTranslate3d:c})):d=Wee,{cssProperties:d,cssClasses:Hee({translateX:h,translateY:m,coordinate:n})}}function gc(e){"@babel/helpers - typeof";return gc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},gc(e)}function kP(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function CP(e){for(var t=1;tAP||Math.abs(r.height-this.state.lastBoundingBox.height)>AP)&&this.setState({lastBoundingBox:{width:r.width,height:r.height}})}else(this.state.lastBoundingBox.width!==-1||this.state.lastBoundingBox.height!==-1)&&this.setState({lastBoundingBox:{width:-1,height:-1}})}},{key:"componentDidMount",value:function(){document.addEventListener("keydown",this.handleKeyDown),this.updateBBox()}},{key:"componentWillUnmount",value:function(){document.removeEventListener("keydown",this.handleKeyDown)}},{key:"componentDidUpdate",value:function(){var r,i;this.props.active&&this.updateBBox(),this.state.dismissed&&(((r=this.props.coordinate)===null||r===void 0?void 0:r.x)!==this.state.dismissedAtCoordinate.x||((i=this.props.coordinate)===null||i===void 0?void 0:i.y)!==this.state.dismissedAtCoordinate.y)&&(this.state.dismissed=!1)}},{key:"render",value:function(){var r=this,i=this.props,a=i.active,s=i.allowEscapeViewBox,c=i.animationDuration,u=i.animationEasing,d=i.children,h=i.coordinate,m=i.hasPayload,p=i.isAnimationActive,v=i.offset,_=i.position,x=i.reverseDirection,y=i.useTranslate3d,w=i.viewBox,b=i.wrapperStyle,j=qee({allowEscapeViewBox:s,coordinate:h,offsetTopLeft:v,position:_,reverseDirection:x,tooltipBox:this.state.lastBoundingBox,useTranslate3d:y,viewBox:w}),E=j.cssClasses,P=j.cssProperties,O=CP(CP({transition:p&&a?"transform ".concat(c,"ms ").concat(u):void 0},P),{},{pointerEvents:"none",visibility:!this.state.dismissed&&a&&m?"visible":"hidden",position:"absolute",top:0,left:0},b);return H.createElement("div",{tabIndex:-1,className:E,style:O,ref:function(A){r.wrapperNode=A}},d)}}])}(N.PureComponent),nte=function(){return!(typeof window<"u"&&window.document&&window.document.createElement&&window.setTimeout)},nu={isSsr:nte()};function vc(e){"@babel/helpers - typeof";return vc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},vc(e)}function TP(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function MP(e){for(var t=1;t0;return H.createElement(tte,{allowEscapeViewBox:s,animationDuration:c,animationEasing:u,isAnimationActive:p,active:a,coordinate:h,hasPayload:O,offset:v,position:y,reverseDirection:w,useTranslate3d:b,viewBox:j,wrapperStyle:E},fte(d,MP(MP({},this.props),{},{payload:P})))}}])}(N.PureComponent);b2(ni,"displayName","Tooltip");b2(ni,"defaultProps",{accessibilityLayer:!1,allowEscapeViewBox:{x:!1,y:!1},animationDuration:400,animationEasing:"ease",contentStyle:{},coordinate:{x:0,y:0},cursor:!0,cursorStyle:{},filterNull:!0,isAnimationActive:!nu.isSsr,itemStyle:{},labelStyle:{},offset:10,reverseDirection:{x:!1,y:!1},separator:" : ",trigger:"hover",useTranslate3d:!1,viewBox:{x:0,y:0,height:0,width:0},wrapperStyle:{}});var hte=Ui,mte=function(){return hte.Date.now()},pte=mte,gte=/\s/;function vte(e){for(var t=e.length;t--&>e.test(e.charAt(t)););return t}var yte=vte,xte=yte,bte=/^\s+/;function wte(e){return e&&e.slice(0,xte(e)+1).replace(bte,"")}var _te=wte,jte=_te,LP=Ao,Nte=Gc,$P=NaN,Ste=/^[-+]0x[0-9a-f]+$/i,Pte=/^0b[01]+$/i,Ete=/^0o[0-7]+$/i,Ote=parseInt;function kte(e){if(typeof e=="number")return e;if(Nte(e))return $P;if(LP(e)){var t=typeof e.valueOf=="function"?e.valueOf():e;e=LP(t)?t+"":t}if(typeof e!="string")return e===0?e:+e;e=jte(e);var n=Pte.test(e);return n||Ete.test(e)?Ote(e.slice(2),n?2:8):Ste.test(e)?$P:+e}var qT=kte,Cte=Ao,Ey=pte,IP=qT,Ate="Expected a function",Tte=Math.max,Mte=Math.min;function Lte(e,t,n){var r,i,a,s,c,u,d=0,h=!1,m=!1,p=!0;if(typeof e!="function")throw new TypeError(Ate);t=IP(t)||0,Cte(n)&&(h=!!n.leading,m="maxWait"in n,a=m?Tte(IP(n.maxWait)||0,t):a,p="trailing"in n?!!n.trailing:p);function v(O){var C=r,A=i;return r=i=void 0,d=O,s=e.apply(A,C),s}function _(O){return d=O,c=setTimeout(w,t),h?v(O):s}function x(O){var C=O-u,A=O-d,T=t-C;return m?Mte(T,a-A):T}function y(O){var C=O-u,A=O-d;return u===void 0||C>=t||C<0||m&&A>=a}function w(){var O=Ey();if(y(O))return b(O);c=setTimeout(w,x(O))}function b(O){return c=void 0,p&&r?v(O):(r=i=void 0,s)}function j(){c!==void 0&&clearTimeout(c),d=0,r=u=i=c=void 0}function E(){return c===void 0?s:b(Ey())}function P(){var O=Ey(),C=y(O);if(r=arguments,i=this,u=O,C){if(c===void 0)return _(u);if(m)return clearTimeout(c),c=setTimeout(w,t),v(u)}return c===void 0&&(c=setTimeout(w,t)),s}return P.cancel=j,P.flush=E,P}var $te=Lte,Ite=$te,Rte=Ao,Fte="Expected a function";function Dte(e,t,n){var r=!0,i=!0;if(typeof e!="function")throw new TypeError(Fte);return Rte(n)&&(r="leading"in n?!!n.leading:r,i="trailing"in n?!!n.trailing:i),Ite(e,t,{leading:r,maxWait:t,trailing:i})}var Bte=Dte;const ZT=Qe(Bte);function Yd(e){"@babel/helpers - typeof";return Yd=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Yd(e)}function RP(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function Xh(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&(Z=ZT(Z,x,{trailing:!0,leading:!1}));var I=new ResizeObserver(Z),F=P.current.getBoundingClientRect(),B=F.width,G=F.height;return z(B,G),I.observe(P.current),function(){I.disconnect()}},[z,x]);var D=N.useMemo(function(){var Z=T.containerWidth,I=T.containerHeight;if(Z<0||I<0)return null;hi(as(s)||as(u),`The width(%s) and height(%s) are both fixed numbers, + maybe you don't need to use a ResponsiveContainer.`,s,u),hi(!n||n>0,"The aspect(%s) must be greater than zero.",n);var F=as(s)?Z:s,B=as(u)?I:u;n&&n>0&&(F?B=F/n:B&&(F=B*n),p&&B>p&&(B=p)),hi(F>0||B>0,`The width(%s) and height(%s) of chart should be greater than 0, + please check the style of container, or the props width(%s) and height(%s), + or add a minWidth(%s) or minHeight(%s) or use aspect(%s) to control the + height and width.`,F,B,s,u,h,m,n);var G=!Array.isArray(v)&&xa(v.type).endsWith("Chart");return H.Children.map(v,function(R){return H.isValidElement(R)?N.cloneElement(R,Xh({width:F,height:B},G?{style:Xh({height:"100%",width:"100%",maxHeight:B,maxWidth:F},R.props.style)}:{})):R})},[n,v,u,p,m,h,T,s]);return H.createElement("div",{id:y?"".concat(y):void 0,className:Ie("recharts-responsive-container",w),style:Xh(Xh({},E),{},{width:s,height:u,minWidth:h,minHeight:m,maxHeight:p}),ref:P},D)}),k0=function(t){return null};k0.displayName="Cell";function Xd(e){"@babel/helpers - typeof";return Xd=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Xd(e)}function BP(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function Xb(e){for(var t=1;t1&&arguments[1]!==void 0?arguments[1]:{};if(t==null||nu.isSsr)return{width:0,height:0};var r=ene(n),i=JSON.stringify({text:t,copyStyle:r});if(sl.widthCache[i])return sl.widthCache[i];try{var a=document.getElementById(zP);a||(a=document.createElement("span"),a.setAttribute("id",zP),a.setAttribute("aria-hidden","true"),document.body.appendChild(a));var s=Xb(Xb({},Jte),r);Object.assign(a.style,s),a.textContent="".concat(t);var c=a.getBoundingClientRect(),u={width:c.width,height:c.height};return sl.widthCache[i]=u,++sl.cacheCount>Qte&&(sl.cacheCount=0,sl.widthCache={}),u}catch{return{width:0,height:0}}},tne=function(t){return{top:t.top+window.scrollY-document.documentElement.clientTop,left:t.left+window.scrollX-document.documentElement.clientLeft}};function Qd(e){"@babel/helpers - typeof";return Qd=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Qd(e)}function Sp(e,t){return ane(e)||ine(e,t)||rne(e,t)||nne()}function nne(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function rne(e,t){if(e){if(typeof e=="string")return UP(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);if(n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set")return Array.from(e);if(n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return UP(e,t)}}function UP(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function xne(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}function GP(e,t){return jne(e)||_ne(e,t)||wne(e,t)||bne()}function bne(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function wne(e,t){if(e){if(typeof e=="string")return KP(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);if(n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set")return Array.from(e);if(n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return KP(e,t)}}function KP(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&arguments[0]!==void 0?arguments[0]:[];return F.reduce(function(B,G){var R=G.word,K=G.width,W=B[B.length-1];if(W&&(i==null||a||W.width+K+rG.width?B:G})};if(!h)return v;for(var x="…",y=function(F){var B=m.slice(0,F),G=XT({breakAll:d,style:u,children:B+x}).wordsWithComputedWidth,R=p(G),K=R.length>s||_(R).width>Number(i);return[K,R]},w=0,b=m.length-1,j=0,E;w<=b&&j<=m.length-1;){var P=Math.floor((w+b)/2),O=P-1,C=y(O),A=GP(C,2),T=A[0],$=A[1],z=y(P),D=GP(z,1),Z=D[0];if(!T&&!Z&&(w=P+1),T&&Z&&(b=P-1),!T&&Z){E=$;break}j++}return E||v},YP=function(t){var n=De(t)?[]:t.toString().split(YT);return[{words:n}]},Sne=function(t){var n=t.width,r=t.scaleToFit,i=t.children,a=t.style,s=t.breakAll,c=t.maxLines;if((n||r)&&!nu.isSsr){var u,d,h=XT({breakAll:s,children:i,style:a});if(h){var m=h.wordsWithComputedWidth,p=h.spaceWidth;u=m,d=p}else return YP(i);return Nne({breakAll:s,children:i,maxLines:c,style:a},u,d,n,r)}return YP(i)},XP="#808080",Cs=function(t){var n=t.x,r=n===void 0?0:n,i=t.y,a=i===void 0?0:i,s=t.lineHeight,c=s===void 0?"1em":s,u=t.capHeight,d=u===void 0?"0.71em":u,h=t.scaleToFit,m=h===void 0?!1:h,p=t.textAnchor,v=p===void 0?"start":p,_=t.verticalAnchor,x=_===void 0?"end":_,y=t.fill,w=y===void 0?XP:y,b=ZP(t,vne),j=N.useMemo(function(){return Sne({breakAll:b.breakAll,children:b.children,maxLines:b.maxLines,scaleToFit:m,style:b.style,width:b.width})},[b.breakAll,b.children,b.maxLines,m,b.style,b.width]),E=b.dx,P=b.dy,O=b.angle,C=b.className,A=b.breakAll,T=ZP(b,yne);if(!Gt(r)||!Gt(a))return null;var $=r+(ue(E)?E:0),z=a+(ue(P)?P:0),D;switch(x){case"start":D=Oy("calc(".concat(d,")"));break;case"middle":D=Oy("calc(".concat((j.length-1)/2," * -").concat(c," + (").concat(d," / 2))"));break;default:D=Oy("calc(".concat(j.length-1," * -").concat(c,")"));break}var Z=[];if(m){var I=j[0].width,F=b.width;Z.push("scale(".concat((ue(F)?F/I:1)/I,")"))}return O&&Z.push("rotate(".concat(O,", ").concat($,", ").concat(z,")")),Z.length&&(T.transform=Z.join(" ")),H.createElement("text",Qb({},Oe(T,!0),{x:$,y:z,className:Ie("recharts-text",C),textAnchor:v,fill:w.includes("url")?XP:w}),j.map(function(B,G){var R=B.words.join(A?"":" ");return H.createElement("tspan",{x:$,dy:G===0?D:c,key:"".concat(R,"-").concat(G)},R)}))};function Qf(e,t){return et?1:e>=t?0:NaN}function w2(e){let t=e,n=e;e.length===1&&(t=(s,c)=>e(s)-c,n=Pne(e));function r(s,c,u,d){for(u==null&&(u=0),d==null&&(d=s.length);u>>1;n(s[h],c)<0?u=h+1:d=h}return u}function i(s,c,u,d){for(u==null&&(u=0),d==null&&(d=s.length);u>>1;n(s[h],c)>0?d=h:u=h+1}return u}function a(s,c,u,d){u==null&&(u=0),d==null&&(d=s.length);const h=r(s,c,u,d-1);return h>u&&t(s[h-1],c)>-t(s[h],c)?h-1:h}return{left:r,center:a,right:i}}function Pne(e){return(t,n)=>Qf(e(t),n)}function QT(e){return e===null?NaN:+e}function*Ene(e,t){for(let n of e)n!=null&&(n=+n)>=n&&(yield n)}const One=w2(Qf),Jf=One.right;w2(QT).center;class Nn{constructor(){this._partials=new Float64Array(32),this._n=0}add(t){const n=this._partials;let r=0;for(let i=0;i0){for(s=t[--n];n>0&&(r=s,i=t[--n],s=r+i,a=i-(s-r),!a););n>0&&(a<0&&t[n-1]<0||a>0&&t[n-1]>0)&&(i=a*2,r=s+i,i==r-s&&(s=r))}return s}}class QP extends Map{constructor(t,n=Ane){if(super(),Object.defineProperties(this,{_intern:{value:new Map},_key:{value:n}}),t!=null)for(const[r,i]of t)this.set(r,i)}get(t){return super.get(JP(this,t))}has(t){return super.has(JP(this,t))}set(t,n){return super.set(kne(this,t),n)}delete(t){return super.delete(Cne(this,t))}}function JP({_intern:e,_key:t},n){const r=t(n);return e.has(r)?e.get(r):n}function kne({_intern:e,_key:t},n){const r=t(n);return e.has(r)?e.get(r):(e.set(r,n),n)}function Cne({_intern:e,_key:t},n){const r=t(n);return e.has(r)&&(n=e.get(n),e.delete(r)),n}function Ane(e){return e!==null&&typeof e=="object"?e.valueOf():e}var Jb=Math.sqrt(50),e1=Math.sqrt(10),t1=Math.sqrt(2);function n1(e,t,n){var r,i=-1,a,s,c;if(t=+t,e=+e,n=+n,e===t&&n>0)return[e];if((r=t0){let u=Math.round(e/c),d=Math.round(t/c);for(u*ct&&--d,s=new Array(a=d-u+1);++it&&--d,s=new Array(a=d-u+1);++i=0?(a>=Jb?10:a>=e1?5:a>=t1?2:1)*Math.pow(10,i):-Math.pow(10,-i)/(a>=Jb?10:a>=e1?5:a>=t1?2:1)}function r1(e,t,n){var r=Math.abs(t-e)/Math.max(0,n),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),a=r/i;return a>=Jb?i*=10:a>=e1?i*=5:a>=t1&&(i*=2),t=r)&&(n=r);return n}function t3(e,t){let n;for(const r of e)r!=null&&(n>r||n===void 0&&r>=r)&&(n=r);return n}function e6(e,t,n=0,r=e.length-1,i=Qf){for(;r>n;){if(r-n>600){const u=r-n+1,d=t-n+1,h=Math.log(u),m=.5*Math.exp(2*h/3),p=.5*Math.sqrt(h*m*(u-m)/u)*(d-u/2<0?-1:1),v=Math.max(n,Math.floor(t-d*m/u+p)),_=Math.min(r,Math.floor(t+(u-d)*m/u+p));e6(e,t,v,_,i)}const a=e[t];let s=n,c=r;for(Au(e,n,t),i(e[r],a)>0&&Au(e,n,r);s0;)--c}i(e[n],a)===0?Au(e,n,c):(++c,Au(e,c,r)),c<=t&&(n=c+1),t<=c&&(r=c-1)}return e}function Au(e,t,n){const r=e[t];e[t]=e[n],e[n]=r}function Tne(e,t,n){if(e=Float64Array.from(Ene(e)),!!(r=e.length)){if((t=+t)<=0||r<2)return t3(e);if(t>=1)return e3(e);var r,i=(r-1)*t,a=Math.floor(i),s=e3(e6(e,a).subarray(0,a+1)),c=t3(e.subarray(a+1));return s+(c-s)*(i-a)}}function Mne(e,t,n=QT){if(r=e.length){if((t=+t)<=0||r<2)return+n(e[0],0,e);if(t>=1)return+n(e[r-1],r-1,e);var r,i=(r-1)*t,a=Math.floor(i),s=+n(e[a],a,e),c=+n(e[a+1],a+1,e);return s+(c-s)*(i-a)}}function*Lne(e){for(const t of e)yield*t}function t6(e){return Array.from(Lne(e))}function os(e,t,n){e=+e,t=+t,n=(i=arguments.length)<2?(t=e,e=0,1):i<3?1:+n;for(var r=-1,i=Math.max(0,Math.ceil((t-e)/n))|0,a=new Array(i);++r>8&15|t>>4&240,t>>4&15|t&240,(t&15)<<4|t&15,1):n===8?Jh(t>>24&255,t>>16&255,t>>8&255,(t&255)/255):n===4?Jh(t>>12&15|t>>8&240,t>>8&15|t>>4&240,t>>4&15|t&240,((t&15)<<4|t&15)/255):null):(t=Ine.exec(e))?new mr(t[1],t[2],t[3],1):(t=Rne.exec(e))?new mr(t[1]*255/100,t[2]*255/100,t[3]*255/100,1):(t=Fne.exec(e))?Jh(t[1],t[2],t[3],t[4]):(t=Dne.exec(e))?Jh(t[1]*255/100,t[2]*255/100,t[3]*255/100,t[4]):(t=Bne.exec(e))?l3(t[1],t[2]/100,t[3]/100,1):(t=zne.exec(e))?l3(t[1],t[2]/100,t[3]/100,t[4]):n3.hasOwnProperty(e)?a3(n3[e]):e==="transparent"?new mr(NaN,NaN,NaN,0):null}function a3(e){return new mr(e>>16&255,e>>8&255,e&255,1)}function Jh(e,t,n,r){return r<=0&&(e=t=n=NaN),new mr(e,t,n,r)}function Wne(e){return e instanceof eh||(e=As(e)),e?(e=e.rgb(),new mr(e.r,e.g,e.b,e.opacity)):new mr}function a1(e,t,n,r){return arguments.length===1?Wne(e):new mr(e,t,n,r??1)}function mr(e,t,n,r){this.r=+e,this.g=+t,this.b=+n,this.opacity=+r}j2(mr,a1,r6(eh,{brighter:function(e){return e=e==null?Pp:Math.pow(Pp,e),new mr(this.r*e,this.g*e,this.b*e,this.opacity)},darker:function(e){return e=e==null?ef:Math.pow(ef,e),new mr(this.r*e,this.g*e,this.b*e,this.opacity)},rgb:function(){return this},displayable:function(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:o3,formatHex:o3,formatRgb:s3,toString:s3}));function o3(){return"#"+ky(this.r)+ky(this.g)+ky(this.b)}function s3(){var e=this.opacity;return e=isNaN(e)?1:Math.max(0,Math.min(1,e)),(e===1?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(e===1?")":", "+e+")")}function ky(e){return e=Math.max(0,Math.min(255,Math.round(e)||0)),(e<16?"0":"")+e.toString(16)}function l3(e,t,n,r){return r<=0?e=t=n=NaN:n<=0||n>=1?e=t=NaN:t<=0&&(e=NaN),new Ai(e,t,n,r)}function i6(e){if(e instanceof Ai)return new Ai(e.h,e.s,e.l,e.opacity);if(e instanceof eh||(e=As(e)),!e)return new Ai;if(e instanceof Ai)return e;e=e.rgb();var t=e.r/255,n=e.g/255,r=e.b/255,i=Math.min(t,n,r),a=Math.max(t,n,r),s=NaN,c=a-i,u=(a+i)/2;return c?(t===a?s=(n-r)/c+(n0&&u<1?0:s,new Ai(s,c,u,e.opacity)}function Hne(e,t,n,r){return arguments.length===1?i6(e):new Ai(e,t,n,r??1)}function Ai(e,t,n,r){this.h=+e,this.s=+t,this.l=+n,this.opacity=+r}j2(Ai,Hne,r6(eh,{brighter:function(e){return e=e==null?Pp:Math.pow(Pp,e),new Ai(this.h,this.s,this.l*e,this.opacity)},darker:function(e){return e=e==null?ef:Math.pow(ef,e),new Ai(this.h,this.s,this.l*e,this.opacity)},rgb:function(){var e=this.h%360+(this.h<0)*360,t=isNaN(e)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*t,i=2*n-r;return new mr(Cy(e>=240?e-240:e+120,i,r),Cy(e,i,r),Cy(e<120?e+240:e-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl:function(){var e=this.opacity;return e=isNaN(e)?1:Math.max(0,Math.min(1,e)),(e===1?"hsl(":"hsla(")+(this.h||0)+", "+(this.s||0)*100+"%, "+(this.l||0)*100+"%"+(e===1?")":", "+e+")")}}));function Cy(e,t,n){return(e<60?t+(n-t)*e/60:e<180?n:e<240?t+(n-t)*(240-e)/60:t)*255}const N2=e=>()=>e;function Vne(e,t){return function(n){return e+n*t}}function qne(e,t,n){return e=Math.pow(e,n),t=Math.pow(t,n)-e,n=1/n,function(r){return Math.pow(e+r*t,n)}}function Zne(e){return(e=+e)==1?a6:function(t,n){return n-t?qne(t,n,e):N2(isNaN(t)?n:t)}}function a6(e,t){var n=t-e;return n?Vne(e,n):N2(isNaN(e)?t:e)}const Ep=function e(t){var n=Zne(t);function r(i,a){var s=n((i=a1(i)).r,(a=a1(a)).r),c=n(i.g,a.g),u=n(i.b,a.b),d=a6(i.opacity,a.opacity);return function(h){return i.r=s(h),i.g=c(h),i.b=u(h),i.opacity=d(h),i+""}}return r.gamma=e,r}(1);function Gne(e,t){t||(t=[]);var n=e?Math.min(t.length,e.length):0,r=t.slice(),i;return function(a){for(i=0;in&&(a=t.slice(n,a),c[s]?c[s]+=a:c[++s]=a),(r=r[0])===(i=i[0])?c[s]?c[s]+=i:c[++s]=i:(c[++s]=null,u.push({i:s,x:ai(r,i)})),n=Ay.lastIndex;return n180?h+=360:h-d>180&&(d+=360),p.push({i:m.push(i(m)+"rotate(",null,r)-2,x:ai(d,h)})):h&&m.push(i(m)+"rotate("+h+r)}function c(d,h,m,p){d!==h?p.push({i:m.push(i(m)+"skewX(",null,r)-2,x:ai(d,h)}):h&&m.push(i(m)+"skewX("+h+r)}function u(d,h,m,p,v,_){if(d!==m||h!==p){var x=v.push(i(v)+"scale(",null,",",null,")");_.push({i:x-4,x:ai(d,m)},{i:x-2,x:ai(h,p)})}else(m!==1||p!==1)&&v.push(i(v)+"scale("+m+","+p+")")}return function(d,h){var m=[],p=[];return d=e(d),h=e(h),a(d.translateX,d.translateY,h.translateX,h.translateY,m,p),s(d.rotate,h.rotate,m,p),c(d.skewX,h.skewX,m,p),u(d.scaleX,d.scaleY,h.scaleX,h.scaleY,m,p),d=h=null,function(v){for(var _=-1,x=p.length,y;++_t&&(n=e,e=t,t=n),function(r){return Math.max(e,Math.min(t,r))}}function fre(e,t,n){var r=e[0],i=e[1],a=t[0],s=t[1];return i2?hre:fre,u=d=null,m}function m(p){return p==null||isNaN(p=+p)?a:(u||(u=c(e.map(r),t,n)))(r(s(p)))}return m.invert=function(p){return s(i((d||(d=c(t,e.map(r),ai)))(p)))},m.domain=function(p){return arguments.length?(e=Array.from(p,Op),h()):e.slice()},m.range=function(p){return arguments.length?(t=Array.from(p),h()):t.slice()},m.rangeRound=function(p){return t=Array.from(p),n=S2,h()},m.clamp=function(p){return arguments.length?(s=p?!0:$n,h()):s!==$n},m.interpolate=function(p){return arguments.length?(n=p,h()):n},m.unknown=function(p){return arguments.length?(a=p,m):a},function(p,v){return r=p,i=v,h()}}function P2(){return C0()($n,$n)}function mre(e){return Math.abs(e=Math.round(e))>=1e21?e.toLocaleString("en").replace(/,/g,""):e.toString(10)}function kp(e,t){if(!isFinite(e)||e===0)return null;var n=(e=t?e.toExponential(t-1):e.toExponential()).indexOf("e"),r=e.slice(0,n);return[r.length>1?r[0]+r.slice(2):r,+e.slice(n+1)]}function yc(e){return e=kp(Math.abs(e)),e?e[1]:NaN}function pre(e,t){return function(n,r){for(var i=n.length,a=[],s=0,c=e[0],u=0;i>0&&c>0&&(u+c+1>r&&(c=Math.max(1,r-u)),a.push(n.substring(i-=c,i+c)),!((u+=c+1)>r));)c=e[s=(s+1)%e.length];return a.reverse().join(t)}}function gre(e){return function(t){return t.replace(/[0-9]/g,function(n){return e[+n]})}}var vre=/^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i;function nf(e){if(!(t=vre.exec(e)))throw new Error("invalid format: "+e);var t;return new E2({fill:t[1],align:t[2],sign:t[3],symbol:t[4],zero:t[5],width:t[6],comma:t[7],precision:t[8]&&t[8].slice(1),trim:t[9],type:t[10]})}nf.prototype=E2.prototype;function E2(e){this.fill=e.fill===void 0?" ":e.fill+"",this.align=e.align===void 0?">":e.align+"",this.sign=e.sign===void 0?"-":e.sign+"",this.symbol=e.symbol===void 0?"":e.symbol+"",this.zero=!!e.zero,this.width=e.width===void 0?void 0:+e.width,this.comma=!!e.comma,this.precision=e.precision===void 0?void 0:+e.precision,this.trim=!!e.trim,this.type=e.type===void 0?"":e.type+""}E2.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(this.width===void 0?"":Math.max(1,this.width|0))+(this.comma?",":"")+(this.precision===void 0?"":"."+Math.max(0,this.precision|0))+(this.trim?"~":"")+this.type};function yre(e){e:for(var t=e.length,n=1,r=-1,i;n0&&(r=0);break}return r>0?e.slice(0,r)+e.slice(i+1):e}var Cp;function xre(e,t){var n=kp(e,t);if(!n)return Cp=void 0,e.toPrecision(t);var r=n[0],i=n[1],a=i-(Cp=Math.max(-8,Math.min(8,Math.floor(i/3)))*3)+1,s=r.length;return a===s?r:a>s?r+new Array(a-s+1).join("0"):a>0?r.slice(0,a)+"."+r.slice(a):"0."+new Array(1-a).join("0")+kp(e,Math.max(0,t+a-1))[0]}function f3(e,t){var n=kp(e,t);if(!n)return e+"";var r=n[0],i=n[1];return i<0?"0."+new Array(-i).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}const h3={"%":(e,t)=>(e*100).toFixed(t),b:e=>Math.round(e).toString(2),c:e=>e+"",d:mre,e:(e,t)=>e.toExponential(t),f:(e,t)=>e.toFixed(t),g:(e,t)=>e.toPrecision(t),o:e=>Math.round(e).toString(8),p:(e,t)=>f3(e*100,t),r:f3,s:xre,X:e=>Math.round(e).toString(16).toUpperCase(),x:e=>Math.round(e).toString(16)};function m3(e){return e}var p3=Array.prototype.map,g3=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];function bre(e){var t=e.grouping===void 0||e.thousands===void 0?m3:pre(p3.call(e.grouping,Number),e.thousands+""),n=e.currency===void 0?"":e.currency[0]+"",r=e.currency===void 0?"":e.currency[1]+"",i=e.decimal===void 0?".":e.decimal+"",a=e.numerals===void 0?m3:gre(p3.call(e.numerals,String)),s=e.percent===void 0?"%":e.percent+"",c=e.minus===void 0?"−":e.minus+"",u=e.nan===void 0?"NaN":e.nan+"";function d(m,p){m=nf(m);var v=m.fill,_=m.align,x=m.sign,y=m.symbol,w=m.zero,b=m.width,j=m.comma,E=m.precision,P=m.trim,O=m.type;O==="n"?(j=!0,O="g"):h3[O]||(E===void 0&&(E=12),P=!0,O="g"),(w||v==="0"&&_==="=")&&(w=!0,v="0",_="=");var C=(p&&p.prefix!==void 0?p.prefix:"")+(y==="$"?n:y==="#"&&/[boxX]/.test(O)?"0"+O.toLowerCase():""),A=(y==="$"?r:/[%p]/.test(O)?s:"")+(p&&p.suffix!==void 0?p.suffix:""),T=h3[O],$=/[defgprs%]/.test(O);E=E===void 0?6:/[gprs]/.test(O)?Math.max(1,Math.min(21,E)):Math.max(0,Math.min(20,E));function z(D){var Z=C,I=A,F,B,G;if(O==="c")I=T(D)+I,D="";else{D=+D;var R=D<0||1/D<0;if(D=isNaN(D)?u:T(Math.abs(D),E),P&&(D=yre(D)),R&&+D==0&&x!=="+"&&(R=!1),Z=(R?x==="("?x:c:x==="-"||x==="("?"":x)+Z,I=(O==="s"&&!isNaN(D)&&Cp!==void 0?g3[8+Cp/3]:"")+I+(R&&x==="("?")":""),$){for(F=-1,B=D.length;++FG||G>57){I=(G===46?i+D.slice(F+1):D.slice(F))+I,D=D.slice(0,F);break}}}j&&!w&&(D=t(D,1/0));var K=Z.length+D.length+I.length,W=K>1)+Z+D+I+W.slice(K);break;default:D=W+Z+D+I;break}return a(D)}return z.toString=function(){return m+""},z}function h(m,p){var v=Math.max(-8,Math.min(8,Math.floor(yc(p)/3)))*3,_=Math.pow(10,-v),x=d((m=nf(m),m.type="f",m),{suffix:g3[8+v/3]});return function(y){return x(_*y)}}return{format:d,formatPrefix:h}}var tm,O2,c6;wre({thousands:",",grouping:[3],currency:["$",""]});function wre(e){return tm=bre(e),O2=tm.format,c6=tm.formatPrefix,tm}function _re(e){return Math.max(0,-yc(Math.abs(e)))}function jre(e,t){return Math.max(0,Math.max(-8,Math.min(8,Math.floor(yc(t)/3)))*3-yc(Math.abs(e)))}function Nre(e,t){return e=Math.abs(e),t=Math.abs(t)-e,Math.max(0,yc(t)-yc(e))+1}function u6(e,t,n,r){var i=r1(e,t,n),a;switch(r=nf(r??",f"),r.type){case"s":{var s=Math.max(Math.abs(e),Math.abs(t));return r.precision==null&&!isNaN(a=jre(i,s))&&(r.precision=a),c6(r,s)}case"":case"e":case"g":case"p":case"r":{r.precision==null&&!isNaN(a=Nre(i,Math.max(Math.abs(e),Math.abs(t))))&&(r.precision=a-(r.type==="e"));break}case"f":case"%":{r.precision==null&&!isNaN(a=_re(i))&&(r.precision=a-(r.type==="%")*2);break}}return O2(r)}function To(e){var t=e.domain;return e.ticks=function(n){var r=t();return n1(r[0],r[r.length-1],n??10)},e.tickFormat=function(n,r){var i=t();return u6(i[0],i[i.length-1],n??10,r)},e.nice=function(n){n==null&&(n=10);var r=t(),i=0,a=r.length-1,s=r[i],c=r[a],u,d,h=10;for(c0;){if(d=JT(s,c,n),d===u)return r[i]=s,r[a]=c,t(r);if(d>0)s=Math.floor(s/d)*d,c=Math.ceil(c/d)*d;else if(d<0)s=Math.ceil(s*d)/d,c=Math.floor(c*d)/d;else break;u=d}return e},e}function Ap(){var e=P2();return e.copy=function(){return th(e,Ap())},qr.apply(e,arguments),To(e)}function d6(e){var t;function n(r){return r==null||isNaN(r=+r)?t:r}return n.invert=n,n.domain=n.range=function(r){return arguments.length?(e=Array.from(r,Op),n):e.slice()},n.unknown=function(r){return arguments.length?(t=r,n):t},n.copy=function(){return d6(e).unknown(t)},e=arguments.length?Array.from(e,Op):[0,1],To(n)}function f6(e,t){e=e.slice();var n=0,r=e.length-1,i=e[n],a=e[r],s;return aMath.pow(e,t)}function kre(e){return e===Math.E?Math.log:e===10&&Math.log10||e===2&&Math.log2||(e=Math.log(e),t=>Math.log(t)/e)}function x3(e){return(t,n)=>-e(-t,n)}function k2(e){const t=e(v3,y3),n=t.domain;let r=10,i,a;function s(){return i=kre(r),a=Ore(r),n()[0]<0?(i=x3(i),a=x3(a),e(Sre,Pre)):e(v3,y3),t}return t.base=function(c){return arguments.length?(r=+c,s()):r},t.domain=function(c){return arguments.length?(n(c),s()):n()},t.ticks=c=>{const u=n();let d=u[0],h=u[u.length-1];const m=h0){for(;p<=v;++p)for(_=1;_h)break;w.push(x)}}else for(;p<=v;++p)for(_=r-1;_>=1;--_)if(x=p>0?_/a(-p):_*a(p),!(xh)break;w.push(x)}w.length*2{if(c==null&&(c=10),u==null&&(u=r===10?"s":","),typeof u!="function"&&(!(r%1)&&(u=nf(u)).precision==null&&(u.trim=!0),u=O2(u)),c===1/0)return u;const d=Math.max(1,r*c/t.ticks().length);return h=>{let m=h/a(Math.round(i(h)));return m*rn(f6(n(),{floor:c=>a(Math.floor(i(c))),ceil:c=>a(Math.ceil(i(c)))})),t}function h6(){const e=k2(C0()).domain([1,10]);return e.copy=()=>th(e,h6()).base(e.base()),qr.apply(e,arguments),e}function b3(e){return function(t){return Math.sign(t)*Math.log1p(Math.abs(t/e))}}function w3(e){return function(t){return Math.sign(t)*Math.expm1(Math.abs(t))*e}}function C2(e){var t=1,n=e(b3(t),w3(t));return n.constant=function(r){return arguments.length?e(b3(t=+r),w3(t)):t},To(n)}function m6(){var e=C2(C0());return e.copy=function(){return th(e,m6()).constant(e.constant())},qr.apply(e,arguments)}function _3(e){return function(t){return t<0?-Math.pow(-t,e):Math.pow(t,e)}}function Cre(e){return e<0?-Math.sqrt(-e):Math.sqrt(e)}function Are(e){return e<0?-e*e:e*e}function A2(e){var t=e($n,$n),n=1;function r(){return n===1?e($n,$n):n===.5?e(Cre,Are):e(_3(n),_3(1/n))}return t.exponent=function(i){return arguments.length?(n=+i,r()):n},To(t)}function T2(){var e=A2(C0());return e.copy=function(){return th(e,T2()).exponent(e.exponent())},qr.apply(e,arguments),e}function Tre(){return T2.apply(null,arguments).exponent(.5)}function j3(e){return Math.sign(e)*e*e}function Mre(e){return Math.sign(e)*Math.sqrt(Math.abs(e))}function p6(){var e=P2(),t=[0,1],n=!1,r;function i(a){var s=Mre(e(a));return isNaN(s)?r:n?Math.round(s):s}return i.invert=function(a){return e.invert(j3(a))},i.domain=function(a){return arguments.length?(e.domain(a),i):e.domain()},i.range=function(a){return arguments.length?(e.range((t=Array.from(a,Op)).map(j3)),i):t.slice()},i.rangeRound=function(a){return i.range(a).round(!0)},i.round=function(a){return arguments.length?(n=!!a,i):n},i.clamp=function(a){return arguments.length?(e.clamp(a),i):e.clamp()},i.unknown=function(a){return arguments.length?(r=a,i):r},i.copy=function(){return p6(e.domain(),t).round(n).clamp(e.clamp()).unknown(r)},qr.apply(i,arguments),To(i)}function g6(){var e=[],t=[],n=[],r;function i(){var s=0,c=Math.max(1,t.length);for(n=new Array(c-1);++s0?n[c-1]:e[0],c=n?[r[n-1],t]:[r[d-1],r[d]]},s.unknown=function(u){return arguments.length&&(a=u),s},s.thresholds=function(){return r.slice()},s.copy=function(){return v6().domain([e,t]).range(i).unknown(a)},qr.apply(To(s),arguments)}function y6(){var e=[.5],t=[0,1],n,r=1;function i(a){return a!=null&&a<=a?t[Jf(e,a,0,r)]:n}return i.domain=function(a){return arguments.length?(e=Array.from(a),r=Math.min(e.length,t.length-1),i):e.slice()},i.range=function(a){return arguments.length?(t=Array.from(a),r=Math.min(e.length,t.length-1),i):t.slice()},i.invertExtent=function(a){var s=t.indexOf(a);return[e[s-1],e[s]]},i.unknown=function(a){return arguments.length?(n=a,i):n},i.copy=function(){return y6().domain(e).range(t).unknown(n)},qr.apply(i,arguments)}const Ty=new Date,My=new Date;function Kt(e,t,n,r){function i(a){return e(a=arguments.length===0?new Date:new Date(+a)),a}return i.floor=a=>(e(a=new Date(+a)),a),i.ceil=a=>(e(a=new Date(a-1)),t(a,1),e(a),a),i.round=a=>{const s=i(a),c=i.ceil(a);return a-s(t(a=new Date(+a),s==null?1:Math.floor(s)),a),i.range=(a,s,c)=>{const u=[];if(a=i.ceil(a),c=c==null?1:Math.floor(c),!(a0))return u;let d;do u.push(d=new Date(+a)),t(a,c),e(a);while(dKt(s=>{if(s>=s)for(;e(s),!a(s);)s.setTime(s-1)},(s,c)=>{if(s>=s)if(c<0)for(;++c<=0;)for(;t(s,-1),!a(s););else for(;--c>=0;)for(;t(s,1),!a(s););}),n&&(i.count=(a,s)=>(Ty.setTime(+a),My.setTime(+s),e(Ty),e(My),Math.floor(n(Ty,My))),i.every=a=>(a=Math.floor(a),!isFinite(a)||!(a>0)?null:a>1?i.filter(r?s=>r(s)%a===0:s=>i.count(0,s)%a===0):i)),i}const Tp=Kt(()=>{},(e,t)=>{e.setTime(+e+t)},(e,t)=>t-e);Tp.every=e=>(e=Math.floor(e),!isFinite(e)||!(e>0)?null:e>1?Kt(t=>{t.setTime(Math.floor(t/e)*e)},(t,n)=>{t.setTime(+t+n*e)},(t,n)=>(n-t)/e):Tp);Tp.range;const ma=1e3,Ir=ma*60,pa=Ir*60,Ea=pa*24,M2=Ea*7,N3=Ea*30,Ly=Ea*365,ss=Kt(e=>{e.setTime(e-e.getMilliseconds())},(e,t)=>{e.setTime(+e+t*ma)},(e,t)=>(t-e)/ma,e=>e.getUTCSeconds());ss.range;const L2=Kt(e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*ma)},(e,t)=>{e.setTime(+e+t*Ir)},(e,t)=>(t-e)/Ir,e=>e.getMinutes());L2.range;const $2=Kt(e=>{e.setUTCSeconds(0,0)},(e,t)=>{e.setTime(+e+t*Ir)},(e,t)=>(t-e)/Ir,e=>e.getUTCMinutes());$2.range;const I2=Kt(e=>{e.setTime(e-e.getMilliseconds()-e.getSeconds()*ma-e.getMinutes()*Ir)},(e,t)=>{e.setTime(+e+t*pa)},(e,t)=>(t-e)/pa,e=>e.getHours());I2.range;const R2=Kt(e=>{e.setUTCMinutes(0,0,0)},(e,t)=>{e.setTime(+e+t*pa)},(e,t)=>(t-e)/pa,e=>e.getUTCHours());R2.range;const nh=Kt(e=>e.setHours(0,0,0,0),(e,t)=>e.setDate(e.getDate()+t),(e,t)=>(t-e-(t.getTimezoneOffset()-e.getTimezoneOffset())*Ir)/Ea,e=>e.getDate()-1);nh.range;const A0=Kt(e=>{e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCDate(e.getUTCDate()+t)},(e,t)=>(t-e)/Ea,e=>e.getUTCDate()-1);A0.range;const x6=Kt(e=>{e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCDate(e.getUTCDate()+t)},(e,t)=>(t-e)/Ea,e=>Math.floor(e/Ea));x6.range;function Vs(e){return Kt(t=>{t.setDate(t.getDate()-(t.getDay()+7-e)%7),t.setHours(0,0,0,0)},(t,n)=>{t.setDate(t.getDate()+n*7)},(t,n)=>(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Ir)/M2)}const T0=Vs(0),Mp=Vs(1),Lre=Vs(2),$re=Vs(3),xc=Vs(4),Ire=Vs(5),Rre=Vs(6);T0.range;Mp.range;Lre.range;$re.range;xc.range;Ire.range;Rre.range;function qs(e){return Kt(t=>{t.setUTCDate(t.getUTCDate()-(t.getUTCDay()+7-e)%7),t.setUTCHours(0,0,0,0)},(t,n)=>{t.setUTCDate(t.getUTCDate()+n*7)},(t,n)=>(n-t)/M2)}const M0=qs(0),Lp=qs(1),Fre=qs(2),Dre=qs(3),bc=qs(4),Bre=qs(5),zre=qs(6);M0.range;Lp.range;Fre.range;Dre.range;bc.range;Bre.range;zre.range;const F2=Kt(e=>{e.setDate(1),e.setHours(0,0,0,0)},(e,t)=>{e.setMonth(e.getMonth()+t)},(e,t)=>t.getMonth()-e.getMonth()+(t.getFullYear()-e.getFullYear())*12,e=>e.getMonth());F2.range;const D2=Kt(e=>{e.setUTCDate(1),e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCMonth(e.getUTCMonth()+t)},(e,t)=>t.getUTCMonth()-e.getUTCMonth()+(t.getUTCFullYear()-e.getUTCFullYear())*12,e=>e.getUTCMonth());D2.range;const Oa=Kt(e=>{e.setMonth(0,1),e.setHours(0,0,0,0)},(e,t)=>{e.setFullYear(e.getFullYear()+t)},(e,t)=>t.getFullYear()-e.getFullYear(),e=>e.getFullYear());Oa.every=e=>!isFinite(e=Math.floor(e))||!(e>0)?null:Kt(t=>{t.setFullYear(Math.floor(t.getFullYear()/e)*e),t.setMonth(0,1),t.setHours(0,0,0,0)},(t,n)=>{t.setFullYear(t.getFullYear()+n*e)});Oa.range;const ka=Kt(e=>{e.setUTCMonth(0,1),e.setUTCHours(0,0,0,0)},(e,t)=>{e.setUTCFullYear(e.getUTCFullYear()+t)},(e,t)=>t.getUTCFullYear()-e.getUTCFullYear(),e=>e.getUTCFullYear());ka.every=e=>!isFinite(e=Math.floor(e))||!(e>0)?null:Kt(t=>{t.setUTCFullYear(Math.floor(t.getUTCFullYear()/e)*e),t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},(t,n)=>{t.setUTCFullYear(t.getUTCFullYear()+n*e)});ka.range;function b6(e,t,n,r,i,a){const s=[[ss,1,ma],[ss,5,5*ma],[ss,15,15*ma],[ss,30,30*ma],[a,1,Ir],[a,5,5*Ir],[a,15,15*Ir],[a,30,30*Ir],[i,1,pa],[i,3,3*pa],[i,6,6*pa],[i,12,12*pa],[r,1,Ea],[r,2,2*Ea],[n,1,M2],[t,1,N3],[t,3,3*N3],[e,1,Ly]];function c(d,h,m){const p=hy).right(s,p);if(v===s.length)return e.every(r1(d/Ly,h/Ly,m));if(v===0)return Tp.every(Math.max(r1(d,h,m),1));const[_,x]=s[p/s[v-1][2]53)return null;"w"in ie||(ie.w=1),"Z"in ie?(Re=Iy(Tu(ie.y,0,1)),ut=Re.getUTCDay(),Re=ut>4||ut===0?Lp.ceil(Re):Lp(Re),Re=A0.offset(Re,(ie.V-1)*7),ie.y=Re.getUTCFullYear(),ie.m=Re.getUTCMonth(),ie.d=Re.getUTCDate()+(ie.w+6)%7):(Re=$y(Tu(ie.y,0,1)),ut=Re.getDay(),Re=ut>4||ut===0?Mp.ceil(Re):Mp(Re),Re=nh.offset(Re,(ie.V-1)*7),ie.y=Re.getFullYear(),ie.m=Re.getMonth(),ie.d=Re.getDate()+(ie.w+6)%7)}else("W"in ie||"U"in ie)&&("w"in ie||(ie.w="u"in ie?ie.u%7:"W"in ie?1:0),ut="Z"in ie?Iy(Tu(ie.y,0,1)).getUTCDay():$y(Tu(ie.y,0,1)).getDay(),ie.m=0,ie.d="W"in ie?(ie.w+6)%7+ie.W*7-(ut+5)%7:ie.w+ie.U*7-(ut+6)%7);return"Z"in ie?(ie.H+=ie.Z/100|0,ie.M+=ie.Z%100,Iy(ie)):$y(ie)}}function A(se,ye,je,ie){for(var Ve=0,Re=ye.length,ut=je.length,dt,Tt;Ve=ut)return-1;if(dt=ye.charCodeAt(Ve++),dt===37){if(dt=ye.charAt(Ve++),Tt=P[dt in S3?ye.charAt(Ve++):dt],!Tt||(ie=Tt(se,je,ie))<0)return-1}else if(dt!=je.charCodeAt(ie++))return-1}return ie}function T(se,ye,je){var ie=d.exec(ye.slice(je));return ie?(se.p=h.get(ie[0].toLowerCase()),je+ie[0].length):-1}function $(se,ye,je){var ie=v.exec(ye.slice(je));return ie?(se.w=_.get(ie[0].toLowerCase()),je+ie[0].length):-1}function z(se,ye,je){var ie=m.exec(ye.slice(je));return ie?(se.w=p.get(ie[0].toLowerCase()),je+ie[0].length):-1}function D(se,ye,je){var ie=w.exec(ye.slice(je));return ie?(se.m=b.get(ie[0].toLowerCase()),je+ie[0].length):-1}function Z(se,ye,je){var ie=x.exec(ye.slice(je));return ie?(se.m=y.get(ie[0].toLowerCase()),je+ie[0].length):-1}function I(se,ye,je){return A(se,t,ye,je)}function F(se,ye,je){return A(se,n,ye,je)}function B(se,ye,je){return A(se,r,ye,je)}function G(se){return s[se.getDay()]}function R(se){return a[se.getDay()]}function K(se){return u[se.getMonth()]}function W(se){return c[se.getMonth()]}function U(se){return i[+(se.getHours()>=12)]}function Y(se){return 1+~~(se.getMonth()/3)}function ne(se){return s[se.getUTCDay()]}function ae(se){return a[se.getUTCDay()]}function ee(se){return u[se.getUTCMonth()]}function ce(se){return c[se.getUTCMonth()]}function Ne(se){return i[+(se.getUTCHours()>=12)]}function Pe(se){return 1+~~(se.getUTCMonth()/3)}return{format:function(se){var ye=O(se+="",j);return ye.toString=function(){return se},ye},parse:function(se){var ye=C(se+="",!1);return ye.toString=function(){return se},ye},utcFormat:function(se){var ye=O(se+="",E);return ye.toString=function(){return se},ye},utcParse:function(se){var ye=C(se+="",!0);return ye.toString=function(){return se},ye}}}var S3={"-":"",_:" ",0:"0"},an=/^\s*\d+/,Zre=/^%/,Gre=/[\\^$*+?|[\]().{}]/g;function Ze(e,t,n){var r=e<0?"-":"",i=(r?-e:e)+"",a=i.length;return r+(a[t.toLowerCase(),n]))}function Yre(e,t,n){var r=an.exec(t.slice(n,n+1));return r?(e.w=+r[0],n+r[0].length):-1}function Xre(e,t,n){var r=an.exec(t.slice(n,n+1));return r?(e.u=+r[0],n+r[0].length):-1}function Qre(e,t,n){var r=an.exec(t.slice(n,n+2));return r?(e.U=+r[0],n+r[0].length):-1}function Jre(e,t,n){var r=an.exec(t.slice(n,n+2));return r?(e.V=+r[0],n+r[0].length):-1}function eie(e,t,n){var r=an.exec(t.slice(n,n+2));return r?(e.W=+r[0],n+r[0].length):-1}function P3(e,t,n){var r=an.exec(t.slice(n,n+4));return r?(e.y=+r[0],n+r[0].length):-1}function E3(e,t,n){var r=an.exec(t.slice(n,n+2));return r?(e.y=+r[0]+(+r[0]>68?1900:2e3),n+r[0].length):-1}function tie(e,t,n){var r=/^(Z)|([+-]\d\d)(?::?(\d\d))?/.exec(t.slice(n,n+6));return r?(e.Z=r[1]?0:-(r[2]+(r[3]||"00")),n+r[0].length):-1}function nie(e,t,n){var r=an.exec(t.slice(n,n+1));return r?(e.q=r[0]*3-3,n+r[0].length):-1}function rie(e,t,n){var r=an.exec(t.slice(n,n+2));return r?(e.m=r[0]-1,n+r[0].length):-1}function O3(e,t,n){var r=an.exec(t.slice(n,n+2));return r?(e.d=+r[0],n+r[0].length):-1}function iie(e,t,n){var r=an.exec(t.slice(n,n+3));return r?(e.m=0,e.d=+r[0],n+r[0].length):-1}function k3(e,t,n){var r=an.exec(t.slice(n,n+2));return r?(e.H=+r[0],n+r[0].length):-1}function aie(e,t,n){var r=an.exec(t.slice(n,n+2));return r?(e.M=+r[0],n+r[0].length):-1}function oie(e,t,n){var r=an.exec(t.slice(n,n+2));return r?(e.S=+r[0],n+r[0].length):-1}function sie(e,t,n){var r=an.exec(t.slice(n,n+3));return r?(e.L=+r[0],n+r[0].length):-1}function lie(e,t,n){var r=an.exec(t.slice(n,n+6));return r?(e.L=Math.floor(r[0]/1e3),n+r[0].length):-1}function cie(e,t,n){var r=Zre.exec(t.slice(n,n+1));return r?n+r[0].length:-1}function uie(e,t,n){var r=an.exec(t.slice(n));return r?(e.Q=+r[0],n+r[0].length):-1}function die(e,t,n){var r=an.exec(t.slice(n));return r?(e.s=+r[0],n+r[0].length):-1}function C3(e,t){return Ze(e.getDate(),t,2)}function fie(e,t){return Ze(e.getHours(),t,2)}function hie(e,t){return Ze(e.getHours()%12||12,t,2)}function mie(e,t){return Ze(1+nh.count(Oa(e),e),t,3)}function w6(e,t){return Ze(e.getMilliseconds(),t,3)}function pie(e,t){return w6(e,t)+"000"}function gie(e,t){return Ze(e.getMonth()+1,t,2)}function vie(e,t){return Ze(e.getMinutes(),t,2)}function yie(e,t){return Ze(e.getSeconds(),t,2)}function xie(e){var t=e.getDay();return t===0?7:t}function bie(e,t){return Ze(T0.count(Oa(e)-1,e),t,2)}function _6(e){var t=e.getDay();return t>=4||t===0?xc(e):xc.ceil(e)}function wie(e,t){return e=_6(e),Ze(xc.count(Oa(e),e)+(Oa(e).getDay()===4),t,2)}function _ie(e){return e.getDay()}function jie(e,t){return Ze(Mp.count(Oa(e)-1,e),t,2)}function Nie(e,t){return Ze(e.getFullYear()%100,t,2)}function Sie(e,t){return e=_6(e),Ze(e.getFullYear()%100,t,2)}function Pie(e,t){return Ze(e.getFullYear()%1e4,t,4)}function Eie(e,t){var n=e.getDay();return e=n>=4||n===0?xc(e):xc.ceil(e),Ze(e.getFullYear()%1e4,t,4)}function Oie(e){var t=e.getTimezoneOffset();return(t>0?"-":(t*=-1,"+"))+Ze(t/60|0,"0",2)+Ze(t%60,"0",2)}function A3(e,t){return Ze(e.getUTCDate(),t,2)}function kie(e,t){return Ze(e.getUTCHours(),t,2)}function Cie(e,t){return Ze(e.getUTCHours()%12||12,t,2)}function Aie(e,t){return Ze(1+A0.count(ka(e),e),t,3)}function j6(e,t){return Ze(e.getUTCMilliseconds(),t,3)}function Tie(e,t){return j6(e,t)+"000"}function Mie(e,t){return Ze(e.getUTCMonth()+1,t,2)}function Lie(e,t){return Ze(e.getUTCMinutes(),t,2)}function $ie(e,t){return Ze(e.getUTCSeconds(),t,2)}function Iie(e){var t=e.getUTCDay();return t===0?7:t}function Rie(e,t){return Ze(M0.count(ka(e)-1,e),t,2)}function N6(e){var t=e.getUTCDay();return t>=4||t===0?bc(e):bc.ceil(e)}function Fie(e,t){return e=N6(e),Ze(bc.count(ka(e),e)+(ka(e).getUTCDay()===4),t,2)}function Die(e){return e.getUTCDay()}function Bie(e,t){return Ze(Lp.count(ka(e)-1,e),t,2)}function zie(e,t){return Ze(e.getUTCFullYear()%100,t,2)}function Uie(e,t){return e=N6(e),Ze(e.getUTCFullYear()%100,t,2)}function Wie(e,t){return Ze(e.getUTCFullYear()%1e4,t,4)}function Hie(e,t){var n=e.getUTCDay();return e=n>=4||n===0?bc(e):bc.ceil(e),Ze(e.getUTCFullYear()%1e4,t,4)}function Vie(){return"+0000"}function T3(){return"%"}function M3(e){return+e}function L3(e){return Math.floor(+e/1e3)}var ll,S6,P6;qie({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});function qie(e){return ll=qre(e),S6=ll.format,ll.parse,P6=ll.utcFormat,ll.utcParse,ll}function Zie(e){return new Date(e)}function Gie(e){return e instanceof Date?+e:+new Date(+e)}function B2(e,t,n,r,i,a,s,c,u,d){var h=P2(),m=h.invert,p=h.domain,v=d(".%L"),_=d(":%S"),x=d("%I:%M"),y=d("%I %p"),w=d("%a %d"),b=d("%b %d"),j=d("%B"),E=d("%Y");function P(O){return(u(O)t(i/(e.length-1)))},n.quantiles=function(r){return Array.from({length:r+1},(i,a)=>Tne(e,a/r))},n.copy=function(){return C6(t).domain(e)},Ra.apply(n,arguments)}function $0(){var e=0,t=.5,n=1,r=1,i,a,s,c,u,d=$n,h,m=!1,p;function v(x){return isNaN(x=+x)?p:(x=.5+((x=+h(x))-a)*(r*xt}var L6=tae,nae=I0,rae=L6,iae=tu;function aae(e){return e&&e.length?nae(e,iae,rae):void 0}var oae=aae;const R0=Qe(oae);function sae(e,t){return ee.e^a.s<0?1:-1;for(r=a.d.length,i=e.d.length,t=0,n=re.d[t]^a.s<0?1:-1;return r===i?0:r>i^a.s<0?1:-1};ge.decimalPlaces=ge.dp=function(){var e=this,t=e.d.length-1,n=(t-e.e)*mt;if(t=e.d[t],t)for(;t%10==0;t/=10)n--;return n<0?0:n};ge.dividedBy=ge.div=function(e){return ba(this,new this.constructor(e))};ge.dividedToIntegerBy=ge.idiv=function(e){var t=this,n=t.constructor;return it(ba(t,new n(e),0,1),n.precision)};ge.equals=ge.eq=function(e){return!this.cmp(e)};ge.exponent=function(){return Ut(this)};ge.greaterThan=ge.gt=function(e){return this.cmp(e)>0};ge.greaterThanOrEqualTo=ge.gte=function(e){return this.cmp(e)>=0};ge.isInteger=ge.isint=function(){return this.e>this.d.length-2};ge.isNegative=ge.isneg=function(){return this.s<0};ge.isPositive=ge.ispos=function(){return this.s>0};ge.isZero=function(){return this.s===0};ge.lessThan=ge.lt=function(e){return this.cmp(e)<0};ge.lessThanOrEqualTo=ge.lte=function(e){return this.cmp(e)<1};ge.logarithm=ge.log=function(e){var t,n=this,r=n.constructor,i=r.precision,a=i+5;if(e===void 0)e=new r(10);else if(e=new r(e),e.s<1||e.eq(fr))throw Error(Wr+"NaN");if(n.s<1)throw Error(Wr+(n.s?"NaN":"-Infinity"));return n.eq(fr)?new r(0):(yt=!1,t=ba(rf(n,a),rf(e,a),a),yt=!0,it(t,i))};ge.minus=ge.sub=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?D6(t,e):R6(t,(e.s=-e.s,e))};ge.modulo=ge.mod=function(e){var t,n=this,r=n.constructor,i=r.precision;if(e=new r(e),!e.s)throw Error(Wr+"NaN");return n.s?(yt=!1,t=ba(n,e,0,1).times(e),yt=!0,n.minus(t)):it(new r(n),i)};ge.naturalExponential=ge.exp=function(){return F6(this)};ge.naturalLogarithm=ge.ln=function(){return rf(this)};ge.negated=ge.neg=function(){var e=new this.constructor(this);return e.s=-e.s||0,e};ge.plus=ge.add=function(e){var t=this;return e=new t.constructor(e),t.s==e.s?R6(t,e):D6(t,(e.s=-e.s,e))};ge.precision=ge.sd=function(e){var t,n,r,i=this;if(e!==void 0&&e!==!!e&&e!==1&&e!==0)throw Error(ws+e);if(t=Ut(i)+1,r=i.d.length-1,n=r*mt+1,r=i.d[r],r){for(;r%10==0;r/=10)n--;for(r=i.d[0];r>=10;r/=10)n++}return e&&t>n?t:n};ge.squareRoot=ge.sqrt=function(){var e,t,n,r,i,a,s,c=this,u=c.constructor;if(c.s<1){if(!c.s)return new u(0);throw Error(Wr+"NaN")}for(e=Ut(c),yt=!1,i=Math.sqrt(+c),i==0||i==1/0?(t=$i(c.d),(t.length+e)%2==0&&(t+="0"),i=Math.sqrt(t),e=au((e+1)/2)-(e<0||e%2),i==1/0?t="5e"+e:(t=i.toExponential(),t=t.slice(0,t.indexOf("e")+1)+e),r=new u(t)):r=new u(i.toString()),n=u.precision,i=s=n+3;;)if(a=r,r=a.plus(ba(c,a,s+2)).times(.5),$i(a.d).slice(0,s)===(t=$i(r.d)).slice(0,s)){if(t=t.slice(s-3,s+1),i==s&&t=="4999"){if(it(a,n+1,0),a.times(a).eq(c)){r=a;break}}else if(t!="9999")break;s+=4}return yt=!0,it(r,n)};ge.times=ge.mul=function(e){var t,n,r,i,a,s,c,u,d,h=this,m=h.constructor,p=h.d,v=(e=new m(e)).d;if(!h.s||!e.s)return new m(0);for(e.s*=h.s,n=h.e+e.e,u=p.length,d=v.length,u=0;){for(t=0,i=u+r;i>r;)c=a[i]+v[r]*p[i-r-1]+t,a[i--]=c%Qt|0,t=c/Qt|0;a[i]=(a[i]+t)%Qt|0}for(;!a[--s];)a.pop();return t?++n:a.shift(),e.d=a,e.e=n,yt?it(e,m.precision):e};ge.toDecimalPlaces=ge.todp=function(e,t){var n=this,r=n.constructor;return n=new r(n),e===void 0?n:(Bi(e,0,iu),t===void 0?t=r.rounding:Bi(t,0,8),it(n,e+Ut(n)+1,t))};ge.toExponential=function(e,t){var n,r=this,i=r.constructor;return e===void 0?n=Ts(r,!0):(Bi(e,0,iu),t===void 0?t=i.rounding:Bi(t,0,8),r=it(new i(r),e+1,t),n=Ts(r,!0,e+1)),n};ge.toFixed=function(e,t){var n,r,i=this,a=i.constructor;return e===void 0?Ts(i):(Bi(e,0,iu),t===void 0?t=a.rounding:Bi(t,0,8),r=it(new a(i),e+Ut(i)+1,t),n=Ts(r.abs(),!1,e+Ut(r)+1),i.isneg()&&!i.isZero()?"-"+n:n)};ge.toInteger=ge.toint=function(){var e=this,t=e.constructor;return it(new t(e),Ut(e)+1,t.rounding)};ge.toNumber=function(){return+this};ge.toPower=ge.pow=function(e){var t,n,r,i,a,s,c=this,u=c.constructor,d=12,h=+(e=new u(e));if(!e.s)return new u(fr);if(c=new u(c),!c.s){if(e.s<1)throw Error(Wr+"Infinity");return c}if(c.eq(fr))return c;if(r=u.precision,e.eq(fr))return it(c,r);if(t=e.e,n=e.d.length-1,s=t>=n,a=c.s,s){if((n=h<0?-h:h)<=I6){for(i=new u(fr),t=Math.ceil(r/mt+4),yt=!1;n%2&&(i=i.times(c),R3(i.d,t)),n=au(n/2),n!==0;)c=c.times(c),R3(c.d,t);return yt=!0,e.s<0?new u(fr).div(i):it(i,r)}}else if(a<0)throw Error(Wr+"NaN");return a=a<0&&e.d[Math.max(t,n)]&1?-1:1,c.s=1,yt=!1,i=e.times(rf(c,r+d)),yt=!0,i=F6(i),i.s=a,i};ge.toPrecision=function(e,t){var n,r,i=this,a=i.constructor;return e===void 0?(n=Ut(i),r=Ts(i,n<=a.toExpNeg||n>=a.toExpPos)):(Bi(e,1,iu),t===void 0?t=a.rounding:Bi(t,0,8),i=it(new a(i),e,t),n=Ut(i),r=Ts(i,e<=n||n<=a.toExpNeg,e)),r};ge.toSignificantDigits=ge.tosd=function(e,t){var n=this,r=n.constructor;return e===void 0?(e=r.precision,t=r.rounding):(Bi(e,1,iu),t===void 0?t=r.rounding:Bi(t,0,8)),it(new r(n),e,t)};ge.toString=ge.valueOf=ge.val=ge.toJSON=ge[Symbol.for("nodejs.util.inspect.custom")]=function(){var e=this,t=Ut(e),n=e.constructor;return Ts(e,t<=n.toExpNeg||t>=n.toExpPos)};function R6(e,t){var n,r,i,a,s,c,u,d,h=e.constructor,m=h.precision;if(!e.s||!t.s)return t.s||(t=new h(e)),yt?it(t,m):t;if(u=e.d,d=t.d,s=e.e,i=t.e,u=u.slice(),a=s-i,a){for(a<0?(r=u,a=-a,c=d.length):(r=d,i=s,c=u.length),s=Math.ceil(m/mt),c=s>c?s+1:c+1,a>c&&(a=c,r.length=1),r.reverse();a--;)r.push(0);r.reverse()}for(c=u.length,a=d.length,c-a<0&&(a=c,r=d,d=u,u=r),n=0;a;)n=(u[--a]=u[a]+d[a]+n)/Qt|0,u[a]%=Qt;for(n&&(u.unshift(n),++i),c=u.length;u[--c]==0;)u.pop();return t.d=u,t.e=i,yt?it(t,m):t}function Bi(e,t,n){if(e!==~~e||en)throw Error(ws+e)}function $i(e){var t,n,r,i=e.length-1,a="",s=e[0];if(i>0){for(a+=s,t=1;ts?1:-1;else for(c=u=0;ci[c]?1:-1;break}return u}function n(r,i,a){for(var s=0;a--;)r[a]-=s,s=r[a]1;)r.shift()}return function(r,i,a,s){var c,u,d,h,m,p,v,_,x,y,w,b,j,E,P,O,C,A,T=r.constructor,$=r.s==i.s?1:-1,z=r.d,D=i.d;if(!r.s)return new T(r);if(!i.s)throw Error(Wr+"Division by zero");for(u=r.e-i.e,C=D.length,P=z.length,v=new T($),_=v.d=[],d=0;D[d]==(z[d]||0);)++d;if(D[d]>(z[d]||0)&&--u,a==null?b=a=T.precision:s?b=a+(Ut(r)-Ut(i))+1:b=a,b<0)return new T(0);if(b=b/mt+2|0,d=0,C==1)for(h=0,D=D[0],b++;(d1&&(D=e(D,h),z=e(z,h),C=D.length,P=z.length),E=C,x=z.slice(0,C),y=x.length;y=Qt/2&&++O;do h=0,c=t(D,x,C,y),c<0?(w=x[0],C!=y&&(w=w*Qt+(x[1]||0)),h=w/O|0,h>1?(h>=Qt&&(h=Qt-1),m=e(D,h),p=m.length,y=x.length,c=t(m,x,p,y),c==1&&(h--,n(m,C16)throw Error(W2+Ut(e));if(!e.s)return new h(fr);for(yt=!1,c=m,s=new h(.03125);e.abs().gte(.1);)e=e.times(s),d+=5;for(r=Math.log(Xo(2,d))/Math.LN10*2+5|0,c+=r,n=i=a=new h(fr),h.precision=c;;){if(i=it(i.times(e),c),n=n.times(++u),s=a.plus(ba(i,n,c)),$i(s.d).slice(0,c)===$i(a.d).slice(0,c)){for(;d--;)a=it(a.times(a),c);return h.precision=m,t==null?(yt=!0,it(a,m)):a}a=s}}function Ut(e){for(var t=e.e*mt,n=e.d[0];n>=10;n/=10)t++;return t}function Ry(e,t,n){if(t>e.LN10.sd())throw yt=!0,n&&(e.precision=n),Error(Wr+"LN10 precision limit exceeded");return it(new e(e.LN10),t)}function Za(e){for(var t="";e--;)t+="0";return t}function rf(e,t){var n,r,i,a,s,c,u,d,h,m=1,p=10,v=e,_=v.d,x=v.constructor,y=x.precision;if(v.s<1)throw Error(Wr+(v.s?"NaN":"-Infinity"));if(v.eq(fr))return new x(0);if(t==null?(yt=!1,d=y):d=t,v.eq(10))return t==null&&(yt=!0),Ry(x,d);if(d+=p,x.precision=d,n=$i(_),r=n.charAt(0),a=Ut(v),Math.abs(a)<15e14){for(;r<7&&r!=1||r==1&&n.charAt(1)>3;)v=v.times(e),n=$i(v.d),r=n.charAt(0),m++;a=Ut(v),r>1?(v=new x("0."+n),a++):v=new x(r+"."+n.slice(1))}else return u=Ry(x,d+2,y).times(a+""),v=rf(new x(r+"."+n.slice(1)),d-p).plus(u),x.precision=y,t==null?(yt=!0,it(v,y)):v;for(c=s=v=ba(v.minus(fr),v.plus(fr),d),h=it(v.times(v),d),i=3;;){if(s=it(s.times(h),d),u=c.plus(ba(s,new x(i),d)),$i(u.d).slice(0,d)===$i(c.d).slice(0,d))return c=c.times(2),a!==0&&(c=c.plus(Ry(x,d+2,y).times(a+""))),c=ba(c,new x(m),d),x.precision=y,t==null?(yt=!0,it(c,y)):c;c=u,i+=2}}function I3(e,t){var n,r,i;for((n=t.indexOf("."))>-1&&(t=t.replace(".","")),(r=t.search(/e/i))>0?(n<0&&(n=r),n+=+t.slice(r+1),t=t.substring(0,r)):n<0&&(n=t.length),r=0;t.charCodeAt(r)===48;)++r;for(i=t.length;t.charCodeAt(i-1)===48;)--i;if(t=t.slice(r,i),t){if(i-=r,n=n-r-1,e.e=au(n/mt),e.d=[],r=(n+1)%mt,n<0&&(r+=mt),r$p||e.e<-$p))throw Error(W2+n)}else e.s=0,e.e=0,e.d=[0];return e}function it(e,t,n){var r,i,a,s,c,u,d,h,m=e.d;for(s=1,a=m[0];a>=10;a/=10)s++;if(r=t-s,r<0)r+=mt,i=t,d=m[h=0];else{if(h=Math.ceil((r+1)/mt),a=m.length,h>=a)return e;for(d=a=m[h],s=1;a>=10;a/=10)s++;r%=mt,i=r-mt+s}if(n!==void 0&&(a=Xo(10,s-i-1),c=d/a%10|0,u=t<0||m[h+1]!==void 0||d%a,u=n<4?(c||u)&&(n==0||n==(e.s<0?3:2)):c>5||c==5&&(n==4||u||n==6&&(r>0?i>0?d/Xo(10,s-i):0:m[h-1])%10&1||n==(e.s<0?8:7))),t<1||!m[0])return u?(a=Ut(e),m.length=1,t=t-a-1,m[0]=Xo(10,(mt-t%mt)%mt),e.e=au(-t/mt)||0):(m.length=1,m[0]=e.e=e.s=0),e;if(r==0?(m.length=h,a=1,h--):(m.length=h+1,a=Xo(10,mt-r),m[h]=i>0?(d/Xo(10,s-i)%Xo(10,i)|0)*a:0),u)for(;;)if(h==0){(m[0]+=a)==Qt&&(m[0]=1,++e.e);break}else{if(m[h]+=a,m[h]!=Qt)break;m[h--]=0,a=1}for(r=m.length;m[--r]===0;)m.pop();if(yt&&(e.e>$p||e.e<-$p))throw Error(W2+Ut(e));return e}function D6(e,t){var n,r,i,a,s,c,u,d,h,m,p=e.constructor,v=p.precision;if(!e.s||!t.s)return t.s?t.s=-t.s:t=new p(e),yt?it(t,v):t;if(u=e.d,m=t.d,r=t.e,d=e.e,u=u.slice(),s=d-r,s){for(h=s<0,h?(n=u,s=-s,c=m.length):(n=m,r=d,c=u.length),i=Math.max(Math.ceil(v/mt),c)+2,s>i&&(s=i,n.length=1),n.reverse(),i=s;i--;)n.push(0);n.reverse()}else{for(i=u.length,c=m.length,h=i0;--i)u[c++]=0;for(i=m.length;i>s;){if(u[--i]0?a=a.charAt(0)+"."+a.slice(1)+Za(r):s>1&&(a=a.charAt(0)+"."+a.slice(1)),a=a+(i<0?"e":"e+")+i):i<0?(a="0."+Za(-i-1)+a,n&&(r=n-s)>0&&(a+=Za(r))):i>=s?(a+=Za(i+1-s),n&&(r=n-i-1)>0&&(a=a+"."+Za(r))):((r=i+1)0&&(i+1===s&&(a+="."),a+=Za(r))),e.s<0?"-"+a:a}function R3(e,t){if(e.length>t)return e.length=t,!0}function B6(e){var t,n,r;function i(a){var s=this;if(!(s instanceof i))return new i(a);if(s.constructor=i,a instanceof i){s.s=a.s,s.e=a.e,s.d=(a=a.d)?a.slice():a;return}if(typeof a=="number"){if(a*0!==0)throw Error(ws+a);if(a>0)s.s=1;else if(a<0)a=-a,s.s=-1;else{s.s=0,s.e=0,s.d=[0];return}if(a===~~a&&a<1e7){s.e=0,s.d=[a];return}return I3(s,a.toString())}else if(typeof a!="string")throw Error(ws+a);if(a.charCodeAt(0)===45?(a=a.slice(1),s.s=-1):s.s=1,Oae.test(a))I3(s,a);else throw Error(ws+a)}if(i.prototype=ge,i.ROUND_UP=0,i.ROUND_DOWN=1,i.ROUND_CEIL=2,i.ROUND_FLOOR=3,i.ROUND_HALF_UP=4,i.ROUND_HALF_DOWN=5,i.ROUND_HALF_EVEN=6,i.ROUND_HALF_CEIL=7,i.ROUND_HALF_FLOOR=8,i.clone=B6,i.config=i.set=kae,e===void 0&&(e={}),e)for(r=["precision","rounding","toExpNeg","toExpPos","LN10"],t=0;t=i[t+1]&&r<=i[t+2])this[n]=r;else throw Error(ws+n+": "+r);if((r=e[n="LN10"])!==void 0)if(r==Math.LN10)this[n]=new this(r);else throw Error(ws+n+": "+r);return this}var H2=B6(Eae);fr=new H2(1);const tt=H2;function Cae(e){return Lae(e)||Mae(e)||Tae(e)||Aae()}function Aae(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Tae(e,t){if(e){if(typeof e=="string")return c1(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);if(n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set")return Array.from(e);if(n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return c1(e,t)}}function Mae(e){if(typeof Symbol<"u"&&Symbol.iterator in Object(e))return Array.from(e)}function Lae(e){if(Array.isArray(e))return c1(e)}function c1(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=t?n.apply(void 0,i):e(t-s,F3(function(){for(var c=arguments.length,u=new Array(c),d=0;de.length)&&(t=e.length);for(var n=0,r=new Array(t);n"u"||!(Symbol.iterator in Object(e)))){var n=[],r=!0,i=!1,a=void 0;try{for(var s=e[Symbol.iterator](),c;!(r=(c=s.next()).done)&&(n.push(c.value),!(t&&n.length===t));r=!0);}catch(u){i=!0,a=u}finally{try{!r&&s.return!=null&&s.return()}finally{if(i)throw a}}return n}}function Kae(e){if(Array.isArray(e))return e}function V6(e){var t=af(e,2),n=t[0],r=t[1],i=n,a=r;return n>r&&(i=r,a=n),[i,a]}function q6(e,t,n){if(e.lte(0))return new tt(0);var r=z0.getDigitCount(e.toNumber()),i=new tt(10).pow(r),a=e.div(i),s=r!==1?.05:.1,c=new tt(Math.ceil(a.div(s).toNumber())).add(n).mul(s),u=c.mul(i);return t?u:new tt(Math.ceil(u))}function Yae(e,t,n){var r=1,i=new tt(e);if(!i.isint()&&n){var a=Math.abs(e);a<1?(r=new tt(10).pow(z0.getDigitCount(e)-1),i=new tt(Math.floor(i.div(r).toNumber())).mul(r)):a>1&&(i=new tt(Math.floor(e)))}else e===0?i=new tt(Math.floor((t-1)/2)):n||(i=new tt(Math.floor(e)));var s=Math.floor((t-1)/2),c=Fae(Rae(function(u){return i.add(new tt(u-s).mul(r)).toNumber()}),u1);return c(0,t)}function Z6(e,t,n,r){var i=arguments.length>4&&arguments[4]!==void 0?arguments[4]:0;if(!Number.isFinite((t-e)/(n-1)))return{step:new tt(0),tickMin:new tt(0),tickMax:new tt(0)};var a=q6(new tt(t).sub(e).div(n-1),r,i),s;e<=0&&t>=0?s=new tt(0):(s=new tt(e).add(t).div(2),s=s.sub(new tt(s).mod(a)));var c=Math.ceil(s.sub(e).div(a).toNumber()),u=Math.ceil(new tt(t).sub(s).div(a).toNumber()),d=c+u+1;return d>n?Z6(e,t,n,r,i+1):(d0?u+(n-d):u,c=t>0?c:c+(n-d)),{step:a,tickMin:s.sub(new tt(c).mul(a)),tickMax:s.add(new tt(u).mul(a))})}function Xae(e){var t=af(e,2),n=t[0],r=t[1],i=arguments.length>1&&arguments[1]!==void 0?arguments[1]:6,a=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0,s=Math.max(i,2),c=V6([n,r]),u=af(c,2),d=u[0],h=u[1];if(d===-1/0||h===1/0){var m=h===1/0?[d].concat(f1(u1(0,i-1).map(function(){return 1/0}))):[].concat(f1(u1(0,i-1).map(function(){return-1/0})),[h]);return n>r?d1(m):m}if(d===h)return Yae(d,i,a);var p=Z6(d,h,s,a),v=p.step,_=p.tickMin,x=p.tickMax,y=z0.rangeStep(_,x.add(new tt(.1).mul(v)),v);return n>r?d1(y):y}function Qae(e,t){var n=af(e,2),r=n[0],i=n[1],a=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0,s=V6([r,i]),c=af(s,2),u=c[0],d=c[1];if(u===-1/0||d===1/0)return[r,i];if(u===d)return[u];var h=Math.max(t,2),m=q6(new tt(d).sub(u).div(h-1),a,0),p=[].concat(f1(z0.rangeStep(new tt(u),new tt(d).sub(new tt(.99).mul(m)),m)),[d]);return r>i?d1(p):p}var Jae=W6(Xae),eoe=W6(Qae),toe="Invariant failed";function Ms(e,t){throw new Error(toe)}var noe=["offset","layout","width","dataKey","data","dataPointFormatter","xAxis","yAxis"];function wc(e){"@babel/helpers - typeof";return wc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},wc(e)}function Ip(){return Ip=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function coe(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}function uoe(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function doe(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n1&&arguments[1]!==void 0?arguments[1]:[],i=arguments.length>2?arguments[2]:void 0,a=arguments.length>3?arguments[3]:void 0,s=-1,c=(n=r==null?void 0:r.length)!==null&&n!==void 0?n:0;if(c<=1)return 0;if(a&&a.axisType==="angleAxis"&&Math.abs(Math.abs(a.range[1]-a.range[0])-360)<=1e-6)for(var u=a.range,d=0;d0?i[d-1].coordinate:i[c-1].coordinate,m=i[d].coordinate,p=d>=c-1?i[0].coordinate:i[d+1].coordinate,v=void 0;if(Mn(m-h)!==Mn(p-m)){var _=[];if(Mn(p-m)===Mn(u[1]-u[0])){v=p;var x=m+u[1]-u[0];_[0]=Math.min(x,(x+h)/2),_[1]=Math.max(x,(x+h)/2)}else{v=h;var y=p+u[1]-u[0];_[0]=Math.min(m,(y+m)/2),_[1]=Math.max(m,(y+m)/2)}var w=[Math.min(m,(v+m)/2),Math.max(m,(v+m)/2)];if(t>w[0]&&t<=w[1]||t>=_[0]&&t<=_[1]){s=i[d].index;break}}else{var b=Math.min(h,p),j=Math.max(h,p);if(t>(b+m)/2&&t<=(j+m)/2){s=i[d].index;break}}}else for(var E=0;E0&&E(r[E].coordinate+r[E-1].coordinate)/2&&t<=(r[E].coordinate+r[E+1].coordinate)/2||E===c-1&&t>(r[E].coordinate+r[E-1].coordinate)/2){s=r[E].index;break}return s},V2=function(t){var n,r=t,i=r.type.displayName,a=(n=t.type)!==null&&n!==void 0&&n.defaultProps?Ct(Ct({},t.type.defaultProps),t.props):t.props,s=a.stroke,c=a.fill,u;switch(i){case"Line":u=s;break;case"Area":case"Radar":u=s&&s!=="none"?s:c;break;default:u=c;break}return u},Ooe=function(t){var n=t.barSize,r=t.totalSize,i=t.stackGroups,a=i===void 0?{}:i;if(!a)return{};for(var s={},c=Object.keys(a),u=0,d=c.length;u=0});if(w&&w.length){var b=w[0].type.defaultProps,j=b!==void 0?Ct(Ct({},b),w[0].props):w[0].props,E=j.barSize,P=j[y];s[P]||(s[P]=[]);var O=De(E)?n:E;s[P].push({item:w[0],stackList:w.slice(1),barSize:De(O)?void 0:Ln(O,r,0)})}}return s},koe=function(t){var n=t.barGap,r=t.barCategoryGap,i=t.bandSize,a=t.sizeList,s=a===void 0?[]:a,c=t.maxBarSize,u=s.length;if(u<1)return null;var d=Ln(n,i,0,!0),h,m=[];if(s[0].barSize===+s[0].barSize){var p=!1,v=i/u,_=s.reduce(function(E,P){return E+P.barSize||0},0);_+=(u-1)*d,_>=i&&(_-=(u-1)*d,d=0),_>=i&&v>0&&(p=!0,v*=.9,_=u*v);var x=(i-_)/2>>0,y={offset:x-d,size:0};h=s.reduce(function(E,P){var O={item:P.item,position:{offset:y.offset+y.size+d,size:p?v:P.barSize}},C=[].concat(z3(E),[O]);return y=C[C.length-1].position,P.stackList&&P.stackList.length&&P.stackList.forEach(function(A){C.push({item:A,position:y})}),C},m)}else{var w=Ln(r,i,0,!0);i-2*w-(u-1)*d<=0&&(d=0);var b=(i-2*w-(u-1)*d)/u;b>1&&(b>>=0);var j=c===+c?Math.min(b,c):b;h=s.reduce(function(E,P,O){var C=[].concat(z3(E),[{item:P.item,position:{offset:w+(b+d)*O+(b-j)/2,size:j}}]);return P.stackList&&P.stackList.length&&P.stackList.forEach(function(A){C.push({item:A,position:C[C.length-1].position})}),C},m)}return h},Coe=function(t,n,r,i){var a=r.children,s=r.width,c=r.margin,u=s-(c.left||0)-(c.right||0),d=X6({children:a,legendWidth:u});if(d){var h=i||{},m=h.width,p=h.height,v=d.align,_=d.verticalAlign,x=d.layout;if((x==="vertical"||x==="horizontal"&&_==="middle")&&v!=="center"&&ue(t[v]))return Ct(Ct({},t),{},zl({},v,t[v]+(m||0)));if((x==="horizontal"||x==="vertical"&&v==="center")&&_!=="middle"&&ue(t[_]))return Ct(Ct({},t),{},zl({},_,t[_]+(p||0)))}return t},Aoe=function(t,n,r){return De(n)?!0:t==="horizontal"?n==="yAxis":t==="vertical"||r==="x"?n==="xAxis":r==="y"?n==="yAxis":!0},Q6=function(t,n,r,i,a){var s=n.props.children,c=Dr(s,U0).filter(function(d){return Aoe(i,a,d.props.direction)});if(c&&c.length){var u=c.map(function(d){return d.props.dataKey});return t.reduce(function(d,h){var m=_n(h,r);if(De(m))return d;var p=Array.isArray(m)?[F0(m),R0(m)]:[m,m],v=u.reduce(function(_,x){var y=_n(h,x,0),w=p[0]-Math.abs(Array.isArray(y)?y[0]:y),b=p[1]+Math.abs(Array.isArray(y)?y[1]:y);return[Math.min(w,_[0]),Math.max(b,_[1])]},[1/0,-1/0]);return[Math.min(v[0],d[0]),Math.max(v[1],d[1])]},[1/0,-1/0])}return null},Toe=function(t,n,r,i,a){var s=n.map(function(c){return Q6(t,c,r,a,i)}).filter(function(c){return!De(c)});return s&&s.length?s.reduce(function(c,u){return[Math.min(c[0],u[0]),Math.max(c[1],u[1])]},[1/0,-1/0]):null},J6=function(t,n,r,i,a){var s=n.map(function(u){var d=u.props.dataKey;return r==="number"&&d&&Q6(t,u,d,i)||md(t,d,r,a)});if(r==="number")return s.reduce(function(u,d){return[Math.min(u[0],d[0]),Math.max(u[1],d[1])]},[1/0,-1/0]);var c={};return s.reduce(function(u,d){for(var h=0,m=d.length;h=2?Mn(c[0]-c[1])*2*d:d,n&&(t.ticks||t.niceTicks)){var h=(t.ticks||t.niceTicks).map(function(m){var p=a?a.indexOf(m):m;return{coordinate:i(p)+d,value:m,offset:d}});return h.filter(function(m){return!Kf(m.coordinate)})}return t.isCategorical&&t.categoricalDomain?t.categoricalDomain.map(function(m,p){return{coordinate:i(m)+d,value:m,index:p,offset:d}}):i.ticks&&!r?i.ticks(t.tickCount).map(function(m){return{coordinate:i(m)+d,value:m,offset:d}}):i.domain().map(function(m,p){return{coordinate:i(m)+d,value:a?a[m]:m,index:p,offset:d}})},Fy=new WeakMap,nm=function(t,n){if(typeof n!="function")return t;Fy.has(t)||Fy.set(t,new WeakMap);var r=Fy.get(t);if(r.has(n))return r.get(n);var i=function(){t.apply(void 0,arguments),n.apply(void 0,arguments)};return r.set(n,i),i},nM=function(t,n,r){var i=t.scale,a=t.type,s=t.layout,c=t.axisType;if(i==="auto")return s==="radial"&&c==="radiusAxis"?{scale:Jd(),realScaleType:"band"}:s==="radial"&&c==="angleAxis"?{scale:Ap(),realScaleType:"linear"}:a==="category"&&n&&(n.indexOf("LineChart")>=0||n.indexOf("AreaChart")>=0||n.indexOf("ComposedChart")>=0&&!r)?{scale:hd(),realScaleType:"point"}:a==="category"?{scale:Jd(),realScaleType:"band"}:{scale:Ap(),realScaleType:"linear"};if(Os(i)){var u="scale".concat(_0(i));return{scale:($3[u]||hd)(),realScaleType:$3[u]?u:"point"}}return Ce(i)?{scale:i}:{scale:hd(),realScaleType:"point"}},W3=1e-4,rM=function(t){var n=t.domain();if(!(!n||n.length<=2)){var r=n.length,i=t.range(),a=Math.min(i[0],i[1])-W3,s=Math.max(i[0],i[1])+W3,c=t(n[0]),u=t(n[r-1]);(cs||us)&&t.domain([n[0],n[r-1]])}},Moe=function(t,n){if(!t)return null;for(var r=0,i=t.length;ri)&&(a[1]=i),a[0]>i&&(a[0]=i),a[1]=0?(t[c][r][0]=a,t[c][r][1]=a+u,a=t[c][r][1]):(t[c][r][0]=s,t[c][r][1]=s+u,s=t[c][r][1])}},Ioe=function(t){var n=t.length;if(!(n<=0))for(var r=0,i=t[0].length;r=0?(t[s][r][0]=a,t[s][r][1]=a+c,a=t[s][r][1]):(t[s][r][0]=0,t[s][r][1]=0)}},Roe={sign:$oe,expand:nZ,none:hc,silhouette:rZ,wiggle:iZ,positive:Ioe},Foe=function(t,n,r){var i=n.map(function(c){return c.props.dataKey}),a=Roe[r],s=tZ().keys(i).value(function(c,u){return+_n(c,u,0)}).order(Rb).offset(a);return s(t)},Doe=function(t,n,r,i,a,s){if(!t)return null;var c=s?n.reverse():n,u={},d=c.reduce(function(m,p){var v,_=(v=p.type)!==null&&v!==void 0&&v.defaultProps?Ct(Ct({},p.type.defaultProps),p.props):p.props,x=_.stackId,y=_.hide;if(y)return m;var w=_[r],b=m[w]||{hasStack:!1,stackGroups:{}};if(Gt(x)){var j=b.stackGroups[x]||{numericAxisId:r,cateAxisId:i,items:[]};j.items.push(p),b.hasStack=!0,b.stackGroups[x]=j}else b.stackGroups[Yf("_stackId_")]={numericAxisId:r,cateAxisId:i,items:[p]};return Ct(Ct({},m),{},zl({},w,b))},u),h={};return Object.keys(d).reduce(function(m,p){var v=d[p];if(v.hasStack){var _={};v.stackGroups=Object.keys(v.stackGroups).reduce(function(x,y){var w=v.stackGroups[y];return Ct(Ct({},x),{},zl({},y,{numericAxisId:r,cateAxisId:i,items:w.items,stackedData:Foe(t,w.items,a)}))},_)}return Ct(Ct({},m),{},zl({},p,v))},h)},iM=function(t,n){var r=n.realScaleType,i=n.type,a=n.tickCount,s=n.originalDomain,c=n.allowDecimals,u=r||n.scale;if(u!=="auto"&&u!=="linear")return null;if(a&&i==="number"&&s&&(s[0]==="auto"||s[1]==="auto")){var d=t.domain();if(!d.length)return null;var h=Jae(d,a,c);return t.domain([F0(h),R0(h)]),{niceTicks:h}}if(a&&i==="number"){var m=t.domain(),p=eoe(m,a,c);return{niceTicks:p}}return null},H3=function(t){var n=t.axis,r=t.ticks,i=t.offset,a=t.bandSize,s=t.entry,c=t.index;if(n.type==="category")return r[c]?r[c].coordinate+i:null;var u=_n(s,n.dataKey,n.domain[c]);return De(u)?null:n.scale(u)-a/2+i},Boe=function(t){var n=t.numericAxis,r=n.scale.domain();if(n.type==="number"){var i=Math.min(r[0],r[1]),a=Math.max(r[0],r[1]);return i<=0&&a>=0?0:a<0?a:i}return r[0]},zoe=function(t,n){var r,i=(r=t.type)!==null&&r!==void 0&&r.defaultProps?Ct(Ct({},t.type.defaultProps),t.props):t.props,a=i.stackId;if(Gt(a)){var s=n[a];if(s){var c=s.items.indexOf(t);return c>=0?s.stackedData[c]:null}}return null},Uoe=function(t){return t.reduce(function(n,r){return[F0(r.concat([n[0]]).filter(ue)),R0(r.concat([n[1]]).filter(ue))]},[1/0,-1/0])},aM=function(t,n,r){return Object.keys(t).reduce(function(i,a){var s=t[a],c=s.stackedData,u=c.reduce(function(d,h){var m=Uoe(h.slice(n,r+1));return[Math.min(d[0],m[0]),Math.max(d[1],m[1])]},[1/0,-1/0]);return[Math.min(u[0],i[0]),Math.max(u[1],i[1])]},[1/0,-1/0]).map(function(i){return i===1/0||i===-1/0?0:i})},V3=/^dataMin[\s]*-[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/,q3=/^dataMax[\s]*\+[\s]*([0-9]+([.]{1}[0-9]+){0,1})$/,g1=function(t,n,r){if(Ce(t))return t(n,r);if(!Array.isArray(t))return n;var i=[];if(ue(t[0]))i[0]=r?t[0]:Math.min(t[0],n[0]);else if(V3.test(t[0])){var a=+V3.exec(t[0])[1];i[0]=n[0]-a}else Ce(t[0])?i[0]=t[0](n[0]):i[0]=n[0];if(ue(t[1]))i[1]=r?t[1]:Math.max(t[1],n[1]);else if(q3.test(t[1])){var s=+q3.exec(t[1])[1];i[1]=n[1]+s}else Ce(t[1])?i[1]=t[1](n[1]):i[1]=n[1];return i},Fp=function(t,n,r){if(t&&t.scale&&t.scale.bandwidth){var i=t.scale.bandwidth();if(!r||i>0)return i}if(t&&n&&n.length>=2){for(var a=x2(n,function(m){return m.coordinate}),s=1/0,c=1,u=a.length;ce.length)&&(t=e.length);for(var n=0,r=new Array(t);n2&&arguments[2]!==void 0?arguments[2]:{top:0,right:0,bottom:0,left:0};return Math.min(Math.abs(t-(r.left||0)-(r.right||0)),Math.abs(n-(r.top||0)-(r.bottom||0)))/2},Xoe=function(t,n,r,i,a){var s=t.width,c=t.height,u=t.startAngle,d=t.endAngle,h=Ln(t.cx,s,s/2),m=Ln(t.cy,c,c/2),p=lM(s,c,r),v=Ln(t.innerRadius,p,0),_=Ln(t.outerRadius,p,p*.8),x=Object.keys(n);return x.reduce(function(y,w){var b=n[w],j=b.domain,E=b.reversed,P;if(De(b.range))i==="angleAxis"?P=[u,d]:i==="radiusAxis"&&(P=[v,_]),E&&(P=[P[1],P[0]]);else{P=b.range;var O=P,C=Voe(O,2);u=C[0],d=C[1]}var A=nM(b,a),T=A.realScaleType,$=A.scale;$.domain(j).range(P),rM($);var z=iM($,ta(ta({},b),{},{realScaleType:T})),D=ta(ta(ta({},b),z),{},{range:P,radius:_,realScaleType:T,scale:$,cx:h,cy:m,innerRadius:v,outerRadius:_,startAngle:u,endAngle:d});return ta(ta({},y),{},sM({},w,D))},{})},Qoe=function(t,n){var r=t.x,i=t.y,a=n.x,s=n.y;return Math.sqrt(Math.pow(r-a,2)+Math.pow(i-s,2))},Joe=function(t,n){var r=t.x,i=t.y,a=n.cx,s=n.cy,c=Qoe({x:r,y:i},{x:a,y:s});if(c<=0)return{radius:c};var u=(r-a)/c,d=Math.acos(u);return i>s&&(d=2*Math.PI-d),{radius:c,angle:Yoe(d),angleInRadian:d}},ese=function(t){var n=t.startAngle,r=t.endAngle,i=Math.floor(n/360),a=Math.floor(r/360),s=Math.min(i,a);return{startAngle:n-s*360,endAngle:r-s*360}},tse=function(t,n){var r=n.startAngle,i=n.endAngle,a=Math.floor(r/360),s=Math.floor(i/360),c=Math.min(a,s);return t+c*360},Y3=function(t,n){var r=t.x,i=t.y,a=Joe({x:r,y:i},n),s=a.radius,c=a.angle,u=n.innerRadius,d=n.outerRadius;if(sd)return!1;if(s===0)return!0;var h=ese(n),m=h.startAngle,p=h.endAngle,v=c,_;if(m<=p){for(;v>p;)v-=360;for(;v=m&&v<=p}else{for(;v>m;)v-=360;for(;v=p&&v<=m}return _?ta(ta({},n),{},{radius:s,angle:tse(v,n)}):null},cM=function(t){return!N.isValidElement(t)&&!Ce(t)&&typeof t!="boolean"?t.className:""};function cf(e){"@babel/helpers - typeof";return cf=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},cf(e)}var nse=["offset"];function rse(e){return sse(e)||ose(e)||ase(e)||ise()}function ise(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function ase(e,t){if(e){if(typeof e=="string")return v1(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);if(n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set")return Array.from(e);if(n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return v1(e,t)}}function ose(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function sse(e){if(Array.isArray(e))return v1(e)}function v1(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function cse(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}function X3(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function Ht(e){for(var t=1;t=0?1:-1,j,E;i==="insideStart"?(j=v+b*s,E=x):i==="insideEnd"?(j=_-b*s,E=!x):i==="end"&&(j=_+b*s,E=x),E=w<=0?E:!E;var P=ct(d,h,y,j),O=ct(d,h,y,j+(E?1:-1)*359),C="M".concat(P.x,",").concat(P.y,` + A`).concat(y,",").concat(y,",0,1,").concat(E?0:1,`, + `).concat(O.x,",").concat(O.y),A=De(t.id)?Yf("recharts-radial-line-"):t.id;return H.createElement("text",uf({},r,{dominantBaseline:"central",className:Ie("recharts-radial-bar-label",c)}),H.createElement("defs",null,H.createElement("path",{id:A,d:C})),H.createElement("textPath",{xlinkHref:"#".concat(A)},n))},gse=function(t){var n=t.viewBox,r=t.offset,i=t.position,a=n,s=a.cx,c=a.cy,u=a.innerRadius,d=a.outerRadius,h=a.startAngle,m=a.endAngle,p=(h+m)/2;if(i==="outside"){var v=ct(s,c,d+r,p),_=v.x,x=v.y;return{x:_,y:x,textAnchor:_>=s?"start":"end",verticalAnchor:"middle"}}if(i==="center")return{x:s,y:c,textAnchor:"middle",verticalAnchor:"middle"};if(i==="centerTop")return{x:s,y:c,textAnchor:"middle",verticalAnchor:"start"};if(i==="centerBottom")return{x:s,y:c,textAnchor:"middle",verticalAnchor:"end"};var y=(u+d)/2,w=ct(s,c,y,p),b=w.x,j=w.y;return{x:b,y:j,textAnchor:"middle",verticalAnchor:"middle"}},vse=function(t){var n=t.viewBox,r=t.parentViewBox,i=t.offset,a=t.position,s=n,c=s.x,u=s.y,d=s.width,h=s.height,m=h>=0?1:-1,p=m*i,v=m>0?"end":"start",_=m>0?"start":"end",x=d>=0?1:-1,y=x*i,w=x>0?"end":"start",b=x>0?"start":"end";if(a==="top"){var j={x:c+d/2,y:u-m*i,textAnchor:"middle",verticalAnchor:v};return Ht(Ht({},j),r?{height:Math.max(u-r.y,0),width:d}:{})}if(a==="bottom"){var E={x:c+d/2,y:u+h+p,textAnchor:"middle",verticalAnchor:_};return Ht(Ht({},E),r?{height:Math.max(r.y+r.height-(u+h),0),width:d}:{})}if(a==="left"){var P={x:c-y,y:u+h/2,textAnchor:w,verticalAnchor:"middle"};return Ht(Ht({},P),r?{width:Math.max(P.x-r.x,0),height:h}:{})}if(a==="right"){var O={x:c+d+y,y:u+h/2,textAnchor:b,verticalAnchor:"middle"};return Ht(Ht({},O),r?{width:Math.max(r.x+r.width-O.x,0),height:h}:{})}var C=r?{width:d,height:h}:{};return a==="insideLeft"?Ht({x:c+y,y:u+h/2,textAnchor:b,verticalAnchor:"middle"},C):a==="insideRight"?Ht({x:c+d-y,y:u+h/2,textAnchor:w,verticalAnchor:"middle"},C):a==="insideTop"?Ht({x:c+d/2,y:u+p,textAnchor:"middle",verticalAnchor:_},C):a==="insideBottom"?Ht({x:c+d/2,y:u+h-p,textAnchor:"middle",verticalAnchor:v},C):a==="insideTopLeft"?Ht({x:c+y,y:u+p,textAnchor:b,verticalAnchor:_},C):a==="insideTopRight"?Ht({x:c+d-y,y:u+p,textAnchor:w,verticalAnchor:_},C):a==="insideBottomLeft"?Ht({x:c+y,y:u+h-p,textAnchor:b,verticalAnchor:v},C):a==="insideBottomRight"?Ht({x:c+d-y,y:u+h-p,textAnchor:w,verticalAnchor:v},C):Kc(a)&&(ue(a.x)||as(a.x))&&(ue(a.y)||as(a.y))?Ht({x:c+Ln(a.x,d),y:u+Ln(a.y,h),textAnchor:"end",verticalAnchor:"end"},C):Ht({x:c+d/2,y:u+h/2,textAnchor:"middle",verticalAnchor:"middle"},C)},yse=function(t){return"cx"in t&&ue(t.cx)};function en(e){var t=e.offset,n=t===void 0?5:t,r=lse(e,nse),i=Ht({offset:n},r),a=i.viewBox,s=i.position,c=i.value,u=i.children,d=i.content,h=i.className,m=h===void 0?"":h,p=i.textBreakAll;if(!a||De(c)&&De(u)&&!N.isValidElement(d)&&!Ce(d))return null;if(N.isValidElement(d))return N.cloneElement(d,i);var v;if(Ce(d)){if(v=N.createElement(d,i),N.isValidElement(v))return v}else v=hse(i);var _=yse(a),x=Oe(i,!0);if(_&&(s==="insideStart"||s==="insideEnd"||s==="end"))return pse(i,v,x);var y=_?gse(i):vse(i);return H.createElement(Cs,uf({className:Ie("recharts-label",m)},x,y,{breakAll:p}),v)}en.displayName="Label";var uM=function(t){var n=t.cx,r=t.cy,i=t.angle,a=t.startAngle,s=t.endAngle,c=t.r,u=t.radius,d=t.innerRadius,h=t.outerRadius,m=t.x,p=t.y,v=t.top,_=t.left,x=t.width,y=t.height,w=t.clockWise,b=t.labelViewBox;if(b)return b;if(ue(x)&&ue(y)){if(ue(m)&&ue(p))return{x:m,y:p,width:x,height:y};if(ue(v)&&ue(_))return{x:v,y:_,width:x,height:y}}return ue(m)&&ue(p)?{x:m,y:p,width:0,height:0}:ue(n)&&ue(r)?{cx:n,cy:r,startAngle:a||i||0,endAngle:s||i||0,innerRadius:d||0,outerRadius:h||u||c||0,clockWise:w}:t.viewBox?t.viewBox:{}},xse=function(t,n){return t?t===!0?H.createElement(en,{key:"label-implicit",viewBox:n}):Gt(t)?H.createElement(en,{key:"label-implicit",viewBox:n,value:t}):N.isValidElement(t)?t.type===en?N.cloneElement(t,{key:"label-implicit",viewBox:n}):H.createElement(en,{key:"label-implicit",content:t,viewBox:n}):Ce(t)?H.createElement(en,{key:"label-implicit",content:t,viewBox:n}):Kc(t)?H.createElement(en,uf({viewBox:n},t,{key:"label-implicit"})):null:null},bse=function(t,n){var r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0;if(!t||!t.children&&r&&!t.label)return null;var i=t.children,a=uM(t),s=Dr(i,en).map(function(u,d){return N.cloneElement(u,{viewBox:n||a,key:"label-".concat(d)})});if(!r)return s;var c=xse(t.label,n||a);return[c].concat(rse(s))};en.parseViewBox=uM;en.renderCallByParent=bse;function wse(e){var t=e==null?0:e.length;return t?e[t-1]:void 0}var _se=wse;const jse=Qe(_se);function df(e){"@babel/helpers - typeof";return df=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},df(e)}var Nse=["valueAccessor"],Sse=["data","dataKey","clockWise","id","textBreakAll"];function Pse(e){return Cse(e)||kse(e)||Ose(e)||Ese()}function Ese(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Ose(e,t){if(e){if(typeof e=="string")return y1(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);if(n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set")return Array.from(e);if(n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return y1(e,t)}}function kse(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function Cse(e){if(Array.isArray(e))return y1(e)}function y1(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function Lse(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}var $se=function(t){return Array.isArray(t.value)?jse(t.value):t.value};function No(e){var t=e.valueAccessor,n=t===void 0?$se:t,r=eE(e,Nse),i=r.data,a=r.dataKey,s=r.clockWise,c=r.id,u=r.textBreakAll,d=eE(r,Sse);return!i||!i.length?null:H.createElement(Ye,{className:"recharts-label-list"},i.map(function(h,m){var p=De(a)?n(h,m):_n(h&&h.payload,a),v=De(c)?{}:{id:"".concat(c,"-").concat(m)};return H.createElement(en,Bp({},Oe(h,!0),d,v,{parentViewBox:h.parentViewBox,value:p,textBreakAll:u,viewBox:en.parseViewBox(De(s)?h:J3(J3({},h),{},{clockWise:s})),key:"label-".concat(m),index:m}))}))}No.displayName="LabelList";function Ise(e,t){return e?e===!0?H.createElement(No,{key:"labelList-implicit",data:t}):H.isValidElement(e)||Ce(e)?H.createElement(No,{key:"labelList-implicit",data:t,content:e}):Kc(e)?H.createElement(No,Bp({data:t},e,{key:"labelList-implicit"})):null:null}function Rse(e,t){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0;if(!e||!e.children&&n&&!e.label)return null;var r=e.children,i=Dr(r,No).map(function(s,c){return N.cloneElement(s,{data:t,key:"labelList-".concat(c)})});if(!n)return i;var a=Ise(e.label,t);return[a].concat(Pse(i))}No.renderCallByParent=Rse;function ff(e){"@babel/helpers - typeof";return ff=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},ff(e)}function x1(){return x1=Object.assign?Object.assign.bind():function(e){for(var t=1;t180),",").concat(+(s>d),`, + `).concat(m.x,",").concat(m.y,` + `);if(i>0){var v=ct(n,r,i,s),_=ct(n,r,i,d);p+="L ".concat(_.x,",").concat(_.y,` + A `).concat(i,",").concat(i,`,0, + `).concat(+(Math.abs(u)>180),",").concat(+(s<=d),`, + `).concat(v.x,",").concat(v.y," Z")}else p+="L ".concat(n,",").concat(r," Z");return p},Use=function(t){var n=t.cx,r=t.cy,i=t.innerRadius,a=t.outerRadius,s=t.cornerRadius,c=t.forceCornerRadius,u=t.cornerIsExternal,d=t.startAngle,h=t.endAngle,m=Mn(h-d),p=rm({cx:n,cy:r,radius:a,angle:d,sign:m,cornerRadius:s,cornerIsExternal:u}),v=p.circleTangency,_=p.lineTangency,x=p.theta,y=rm({cx:n,cy:r,radius:a,angle:h,sign:-m,cornerRadius:s,cornerIsExternal:u}),w=y.circleTangency,b=y.lineTangency,j=y.theta,E=u?Math.abs(d-h):Math.abs(d-h)-x-j;if(E<0)return c?"M ".concat(_.x,",").concat(_.y,` + a`).concat(s,",").concat(s,",0,0,1,").concat(s*2,`,0 + a`).concat(s,",").concat(s,",0,0,1,").concat(-s*2,`,0 + `):dM({cx:n,cy:r,innerRadius:i,outerRadius:a,startAngle:d,endAngle:h});var P="M ".concat(_.x,",").concat(_.y,` + A`).concat(s,",").concat(s,",0,0,").concat(+(m<0),",").concat(v.x,",").concat(v.y,` + A`).concat(a,",").concat(a,",0,").concat(+(E>180),",").concat(+(m<0),",").concat(w.x,",").concat(w.y,` + A`).concat(s,",").concat(s,",0,0,").concat(+(m<0),",").concat(b.x,",").concat(b.y,` + `);if(i>0){var O=rm({cx:n,cy:r,radius:i,angle:d,sign:m,isExternal:!0,cornerRadius:s,cornerIsExternal:u}),C=O.circleTangency,A=O.lineTangency,T=O.theta,$=rm({cx:n,cy:r,radius:i,angle:h,sign:-m,isExternal:!0,cornerRadius:s,cornerIsExternal:u}),z=$.circleTangency,D=$.lineTangency,Z=$.theta,I=u?Math.abs(d-h):Math.abs(d-h)-T-Z;if(I<0&&s===0)return"".concat(P,"L").concat(n,",").concat(r,"Z");P+="L".concat(D.x,",").concat(D.y,` + A`).concat(s,",").concat(s,",0,0,").concat(+(m<0),",").concat(z.x,",").concat(z.y,` + A`).concat(i,",").concat(i,",0,").concat(+(I>180),",").concat(+(m>0),",").concat(C.x,",").concat(C.y,` + A`).concat(s,",").concat(s,",0,0,").concat(+(m<0),",").concat(A.x,",").concat(A.y,"Z")}else P+="L".concat(n,",").concat(r,"Z");return P},Wse={cx:0,cy:0,innerRadius:0,outerRadius:0,startAngle:0,endAngle:0,cornerRadius:0,forceCornerRadius:!1,cornerIsExternal:!1},fM=function(t){var n=nE(nE({},Wse),t),r=n.cx,i=n.cy,a=n.innerRadius,s=n.outerRadius,c=n.cornerRadius,u=n.forceCornerRadius,d=n.cornerIsExternal,h=n.startAngle,m=n.endAngle,p=n.className;if(s0&&Math.abs(h-m)<360?y=Use({cx:r,cy:i,innerRadius:a,outerRadius:s,cornerRadius:Math.min(x,_/2),forceCornerRadius:u,cornerIsExternal:d,startAngle:h,endAngle:m}):y=dM({cx:r,cy:i,innerRadius:a,outerRadius:s,startAngle:h,endAngle:m}),H.createElement("path",x1({},Oe(n,!0),{className:v,d:y,role:"img"}))};function hf(e){"@babel/helpers - typeof";return hf=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},hf(e)}function b1(){return b1=Object.assign?Object.assign.bind():function(e){for(var t=1;tnle.call(e,t));function Zs(e,t){return e===t||!e&&!t&&e!==e&&t!==t}const ale="__v",ole="__o",sle="_owner",{getOwnPropertyDescriptor:sE,keys:lE}=Object;function lle(e,t){return e.byteLength===t.byteLength&&zp(new Uint8Array(e),new Uint8Array(t))}function cle(e,t,n){let r=e.length;if(t.length!==r)return!1;for(;r-- >0;)if(!n.equals(e[r],t[r],r,r,e,t,n))return!1;return!0}function ule(e,t){return e.byteLength===t.byteLength&&zp(new Uint8Array(e.buffer,e.byteOffset,e.byteLength),new Uint8Array(t.buffer,t.byteOffset,t.byteLength))}function dle(e,t){return Zs(e.getTime(),t.getTime())}function fle(e,t){return e.name===t.name&&e.message===t.message&&e.cause===t.cause&&e.stack===t.stack}function hle(e,t){return e===t}function cE(e,t,n){const r=e.size;if(r!==t.size)return!1;if(!r)return!0;const i=new Array(r),a=e.entries();let s,c,u=0;for(;(s=a.next())&&!s.done;){const d=t.entries();let h=!1,m=0;for(;(c=d.next())&&!c.done;){if(i[m]){m++;continue}const p=s.value,v=c.value;if(n.equals(p[0],v[0],u,m,e,t,n)&&n.equals(p[1],v[1],p[0],v[0],e,t,n)){h=i[m]=!0;break}m++}if(!h)return!1;u++}return!0}const mle=Zs;function ple(e,t,n){const r=lE(e);let i=r.length;if(lE(t).length!==i)return!1;for(;i-- >0;)if(!gM(e,t,n,r[i]))return!1;return!0}function Ru(e,t,n){const r=oE(e);let i=r.length;if(oE(t).length!==i)return!1;let a,s,c;for(;i-- >0;)if(a=r[i],!gM(e,t,n,a)||(s=sE(e,a),c=sE(t,a),(s||c)&&(!s||!c||s.configurable!==c.configurable||s.enumerable!==c.enumerable||s.writable!==c.writable)))return!1;return!0}function gle(e,t){return Zs(e.valueOf(),t.valueOf())}function vle(e,t){return e.source===t.source&&e.flags===t.flags}function uE(e,t,n){const r=e.size;if(r!==t.size)return!1;if(!r)return!0;const i=new Array(r),a=e.values();let s,c;for(;(s=a.next())&&!s.done;){const u=t.values();let d=!1,h=0;for(;(c=u.next())&&!c.done;){if(!i[h]&&n.equals(s.value,c.value,s.value,c.value,e,t,n)){d=i[h]=!0;break}h++}if(!d)return!1}return!0}function zp(e,t){let n=e.byteLength;if(t.byteLength!==n||e.byteOffset!==t.byteOffset)return!1;for(;n-- >0;)if(e[n]!==t[n])return!1;return!0}function yle(e,t){return e.hostname===t.hostname&&e.pathname===t.pathname&&e.protocol===t.protocol&&e.port===t.port&&e.hash===t.hash&&e.username===t.username&&e.password===t.password}function gM(e,t,n,r){return(r===sle||r===ole||r===ale)&&(e.$$typeof||t.$$typeof)?!0:ile(t,r)&&n.equals(e[r],t[r],r,r,e,t,n)}const xle="[object ArrayBuffer]",ble="[object Arguments]",wle="[object Boolean]",_le="[object DataView]",jle="[object Date]",Nle="[object Error]",Sle="[object Map]",Ple="[object Number]",Ele="[object Object]",Ole="[object RegExp]",kle="[object Set]",Cle="[object String]",Ale={"[object Int8Array]":!0,"[object Uint8Array]":!0,"[object Uint8ClampedArray]":!0,"[object Int16Array]":!0,"[object Uint16Array]":!0,"[object Int32Array]":!0,"[object Uint32Array]":!0,"[object Float16Array]":!0,"[object Float32Array]":!0,"[object Float64Array]":!0,"[object BigInt64Array]":!0,"[object BigUint64Array]":!0},Tle="[object URL]",Mle=Object.prototype.toString;function Lle({areArrayBuffersEqual:e,areArraysEqual:t,areDataViewsEqual:n,areDatesEqual:r,areErrorsEqual:i,areFunctionsEqual:a,areMapsEqual:s,areNumbersEqual:c,areObjectsEqual:u,arePrimitiveWrappersEqual:d,areRegExpsEqual:h,areSetsEqual:m,areTypedArraysEqual:p,areUrlsEqual:v,unknownTagComparators:_}){return function(y,w,b){if(y===w)return!0;if(y==null||w==null)return!1;const j=typeof y;if(j!==typeof w)return!1;if(j!=="object")return j==="number"?c(y,w,b):j==="function"?a(y,w,b):!1;const E=y.constructor;if(E!==w.constructor)return!1;if(E===Object)return u(y,w,b);if(Array.isArray(y))return t(y,w,b);if(E===Date)return r(y,w,b);if(E===RegExp)return h(y,w,b);if(E===Map)return s(y,w,b);if(E===Set)return m(y,w,b);const P=Mle.call(y);if(P===jle)return r(y,w,b);if(P===Ole)return h(y,w,b);if(P===Sle)return s(y,w,b);if(P===kle)return m(y,w,b);if(P===Ele)return typeof y.then!="function"&&typeof w.then!="function"&&u(y,w,b);if(P===Tle)return v(y,w,b);if(P===Nle)return i(y,w,b);if(P===ble)return u(y,w,b);if(Ale[P])return p(y,w,b);if(P===xle)return e(y,w,b);if(P===_le)return n(y,w,b);if(P===wle||P===Ple||P===Cle)return d(y,w,b);if(_){let O=_[P];if(!O){const C=rle(y);C&&(O=_[C])}if(O)return O(y,w,b)}return!1}}function $le({circular:e,createCustomConfig:t,strict:n}){let r={areArrayBuffersEqual:lle,areArraysEqual:n?Ru:cle,areDataViewsEqual:ule,areDatesEqual:dle,areErrorsEqual:fle,areFunctionsEqual:hle,areMapsEqual:n?Dy(cE,Ru):cE,areNumbersEqual:mle,areObjectsEqual:n?Ru:ple,arePrimitiveWrappersEqual:gle,areRegExpsEqual:vle,areSetsEqual:n?Dy(uE,Ru):uE,areTypedArraysEqual:n?Dy(zp,Ru):zp,areUrlsEqual:yle,unknownTagComparators:void 0};if(t&&(r=Object.assign({},r,t(r))),e){const i=am(r.areArraysEqual),a=am(r.areMapsEqual),s=am(r.areObjectsEqual),c=am(r.areSetsEqual);r=Object.assign({},r,{areArraysEqual:i,areMapsEqual:a,areObjectsEqual:s,areSetsEqual:c})}return r}function Ile(e){return function(t,n,r,i,a,s,c){return e(t,n,c)}}function Rle({circular:e,comparator:t,createState:n,equals:r,strict:i}){if(n)return function(c,u){const{cache:d=e?new WeakMap:void 0,meta:h}=n();return t(c,u,{cache:d,equals:r,meta:h,strict:i})};if(e)return function(c,u){return t(c,u,{cache:new WeakMap,equals:r,meta:void 0,strict:i})};const a={cache:void 0,equals:r,meta:void 0,strict:i};return function(c,u){return t(c,u,a)}}const Fle=Lo();Lo({strict:!0});Lo({circular:!0});Lo({circular:!0,strict:!0});Lo({createInternalComparator:()=>Zs});Lo({strict:!0,createInternalComparator:()=>Zs});Lo({circular:!0,createInternalComparator:()=>Zs});Lo({circular:!0,createInternalComparator:()=>Zs,strict:!0});function Lo(e={}){const{circular:t=!1,createInternalComparator:n,createState:r,strict:i=!1}=e,a=$le(e),s=Lle(a),c=n?n(s):Ile(s);return Rle({circular:t,comparator:s,createState:r,equals:c,strict:i})}function Dle(e){typeof requestAnimationFrame<"u"&&requestAnimationFrame(e)}function dE(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,n=-1,r=function i(a){n<0&&(n=a),a-n>t?(e(a),n=-1):Dle(i)};requestAnimationFrame(r)}function _1(e){"@babel/helpers - typeof";return _1=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},_1(e)}function Ble(e){return Hle(e)||Wle(e)||Ule(e)||zle()}function zle(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Ule(e,t){if(e){if(typeof e=="string")return fE(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);if(n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set")return Array.from(e);if(n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return fE(e,t)}}function fE(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n1?1:w<0?0:w},x=function(w){for(var b=w>1?1:w,j=b,E=0;E<8;++E){var P=m(j)-b,O=v(j);if(Math.abs(P-b)0&&arguments[0]!==void 0?arguments[0]:{},n=t.stiff,r=n===void 0?100:n,i=t.damping,a=i===void 0?8:i,s=t.dt,c=s===void 0?17:s,u=function(h,m,p){var v=-(h-m)*r,_=p*a,x=p+(v-_)*c/1e3,y=p*c/1e3+h;return Math.abs(y-m)e.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function wce(e,t){if(e==null)return{};var n={},r=Object.keys(e),i,a;for(a=0;a=0)&&(n[i]=e[i]);return n}function By(e){return Sce(e)||Nce(e)||jce(e)||_ce()}function _ce(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function jce(e,t){if(e){if(typeof e=="string")return E1(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);if(n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set")return Array.from(e);if(n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return E1(e,t)}}function Nce(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function Sce(e){if(Array.isArray(e))return E1(e)}function E1(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n"u"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy=="function")return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){})),!0}catch{return!1}}function Hp(e){return Hp=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(n){return n.__proto__||Object.getPrototypeOf(n)},Hp(e)}var Ca=function(e){Cce(n,e);var t=Ace(n);function n(r,i){var a;Pce(this,n),a=t.call(this,r,i);var s=a.props,c=s.isActive,u=s.attributeName,d=s.from,h=s.to,m=s.steps,p=s.children,v=s.duration;if(a.handleStyleChange=a.handleStyleChange.bind(C1(a)),a.changeStyle=a.changeStyle.bind(C1(a)),!c||v<=0)return a.state={style:{}},typeof p=="function"&&(a.state={style:h}),k1(a);if(m&&m.length)a.state={style:m[0].style};else if(d){if(typeof p=="function")return a.state={style:d},k1(a);a.state={style:u?Gu({},u,d):d}}else a.state={style:{}};return a}return Oce(n,[{key:"componentDidMount",value:function(){var i=this.props,a=i.isActive,s=i.canBegin;this.mounted=!0,!(!a||!s)&&this.runAnimation(this.props)}},{key:"componentDidUpdate",value:function(i){var a=this.props,s=a.isActive,c=a.canBegin,u=a.attributeName,d=a.shouldReAnimate,h=a.to,m=a.from,p=this.state.style;if(c){if(!s){var v={style:u?Gu({},u,h):h};this.state&&p&&(u&&p[u]!==h||!u&&p!==h)&&this.setState(v);return}if(!(Fle(i.to,h)&&i.canBegin&&i.isActive)){var _=!i.canBegin||!i.isActive;this.manager&&this.manager.stop(),this.stopJSAnimation&&this.stopJSAnimation();var x=_||d?m:i.to;if(this.state&&p){var y={style:u?Gu({},u,x):x};(u&&p[u]!==x||!u&&p!==x)&&this.setState(y)}this.runAnimation(Qr(Qr({},this.props),{},{from:x,begin:0}))}}}},{key:"componentWillUnmount",value:function(){this.mounted=!1;var i=this.props.onAnimationEnd;this.unSubscribe&&this.unSubscribe(),this.manager&&(this.manager.stop(),this.manager=null),this.stopJSAnimation&&this.stopJSAnimation(),i&&i()}},{key:"handleStyleChange",value:function(i){this.changeStyle(i)}},{key:"changeStyle",value:function(i){this.mounted&&this.setState({style:i})}},{key:"runJSAnimation",value:function(i){var a=this,s=i.from,c=i.to,u=i.duration,d=i.easing,h=i.begin,m=i.onAnimationEnd,p=i.onAnimationStart,v=yce(s,c,sce(d),u,this.changeStyle),_=function(){a.stopJSAnimation=v()};this.manager.start([p,h,_,u,m])}},{key:"runStepAnimation",value:function(i){var a=this,s=i.steps,c=i.begin,u=i.onAnimationStart,d=s[0],h=d.style,m=d.duration,p=m===void 0?0:m,v=function(x,y,w){if(w===0)return x;var b=y.duration,j=y.easing,E=j===void 0?"ease":j,P=y.style,O=y.properties,C=y.onAnimationEnd,A=w>0?s[w-1]:y,T=O||Object.keys(P);if(typeof E=="function"||E==="spring")return[].concat(By(x),[a.runJSAnimation.bind(a,{from:A.style,to:P,duration:b,easing:E}),b]);var $=pE(T,b,E),z=Qr(Qr(Qr({},A.style),P),{},{transition:$});return[].concat(By(x),[z,b,C]).filter(Kle)};return this.manager.start([u].concat(By(s.reduce(v,[h,Math.max(p,c)])),[i.onAnimationEnd]))}},{key:"runAnimation",value:function(i){this.manager||(this.manager=Vle());var a=i.begin,s=i.duration,c=i.attributeName,u=i.to,d=i.easing,h=i.onAnimationStart,m=i.onAnimationEnd,p=i.steps,v=i.children,_=this.manager;if(this.unSubscribe=_.subscribe(this.handleStyleChange),typeof d=="function"||typeof v=="function"||d==="spring"){this.runJSAnimation(i);return}if(p.length>1){this.runStepAnimation(i);return}var x=c?Gu({},c,u):u,y=pE(Object.keys(x),s,d);_.start([h,a,Qr(Qr({},x),{},{transition:y}),s,m])}},{key:"render",value:function(){var i=this.props,a=i.children;i.begin;var s=i.duration;i.attributeName,i.easing;var c=i.isActive;i.steps,i.from,i.to,i.canBegin,i.onAnimationEnd,i.shouldReAnimate,i.onAnimationReStart;var u=bce(i,xce),d=N.Children.count(a),h=this.state.style;if(typeof a=="function")return a(h);if(!c||d===0||s<=0)return a;var m=function(v){var _=v.props,x=_.style,y=x===void 0?{}:x,w=_.className,b=N.cloneElement(v,Qr(Qr({},u),{},{style:Qr(Qr({},y),h),className:w}));return b};return d===1?m(N.Children.only(a)):H.createElement("div",null,N.Children.map(a,function(p){return m(p)}))}}]),n}(N.PureComponent);Ca.displayName="Animate";Ca.defaultProps={begin:0,duration:1e3,from:"",to:"",attributeName:"",easing:"ease",isActive:!0,canBegin:!0,steps:[],onAnimationEnd:function(){},onAnimationStart:function(){}};Ca.propTypes={from:re.oneOfType([re.object,re.string]),to:re.oneOfType([re.object,re.string]),attributeName:re.string,duration:re.number,begin:re.number,easing:re.oneOfType([re.string,re.func]),steps:re.arrayOf(re.shape({duration:re.number.isRequired,style:re.object.isRequired,easing:re.oneOfType([re.oneOf(["ease","ease-in","ease-out","ease-in-out","linear"]),re.func]),properties:re.arrayOf("string"),onAnimationEnd:re.func})),children:re.oneOfType([re.node,re.func]),isActive:re.bool,canBegin:re.bool,onAnimationEnd:re.func,shouldReAnimate:re.bool,onAnimationStart:re.func,onAnimationReStart:re.func};function gf(e){"@babel/helpers - typeof";return gf=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},gf(e)}function Vp(){return Vp=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0?1:-1,u=r>=0?1:-1,d=i>=0&&r>=0||i<0&&r<0?1:0,h;if(s>0&&a instanceof Array){for(var m=[0,0,0,0],p=0,v=4;ps?s:a[p];h="M".concat(t,",").concat(n+c*m[0]),m[0]>0&&(h+="A ".concat(m[0],",").concat(m[0],",0,0,").concat(d,",").concat(t+u*m[0],",").concat(n)),h+="L ".concat(t+r-u*m[1],",").concat(n),m[1]>0&&(h+="A ".concat(m[1],",").concat(m[1],",0,0,").concat(d,`, + `).concat(t+r,",").concat(n+c*m[1])),h+="L ".concat(t+r,",").concat(n+i-c*m[2]),m[2]>0&&(h+="A ".concat(m[2],",").concat(m[2],",0,0,").concat(d,`, + `).concat(t+r-u*m[2],",").concat(n+i)),h+="L ".concat(t+u*m[3],",").concat(n+i),m[3]>0&&(h+="A ".concat(m[3],",").concat(m[3],",0,0,").concat(d,`, + `).concat(t,",").concat(n+i-c*m[3])),h+="Z"}else if(s>0&&a===+a&&a>0){var _=Math.min(s,a);h="M ".concat(t,",").concat(n+c*_,` + A `).concat(_,",").concat(_,",0,0,").concat(d,",").concat(t+u*_,",").concat(n,` + L `).concat(t+r-u*_,",").concat(n,` + A `).concat(_,",").concat(_,",0,0,").concat(d,",").concat(t+r,",").concat(n+c*_,` + L `).concat(t+r,",").concat(n+i-c*_,` + A `).concat(_,",").concat(_,",0,0,").concat(d,",").concat(t+r-u*_,",").concat(n+i,` + L `).concat(t+u*_,",").concat(n+i,` + A `).concat(_,",").concat(_,",0,0,").concat(d,",").concat(t,",").concat(n+i-c*_," Z")}else h="M ".concat(t,",").concat(n," h ").concat(r," v ").concat(i," h ").concat(-r," Z");return h},zce=function(t,n){if(!t||!n)return!1;var r=t.x,i=t.y,a=n.x,s=n.y,c=n.width,u=n.height;if(Math.abs(c)>0&&Math.abs(u)>0){var d=Math.min(a,a+c),h=Math.max(a,a+c),m=Math.min(s,s+u),p=Math.max(s,s+u);return r>=d&&r<=h&&i>=m&&i<=p}return!1},Uce={x:0,y:0,width:0,height:0,radius:0,isAnimationActive:!1,isUpdateAnimationActive:!1,animationBegin:0,animationDuration:1500,animationEasing:"ease"},q2=function(t){var n=jE(jE({},Uce),t),r=N.useRef(),i=N.useState(-1),a=Mce(i,2),s=a[0],c=a[1];N.useEffect(function(){if(r.current&&r.current.getTotalLength)try{var E=r.current.getTotalLength();E&&c(E)}catch{}},[]);var u=n.x,d=n.y,h=n.width,m=n.height,p=n.radius,v=n.className,_=n.animationEasing,x=n.animationDuration,y=n.animationBegin,w=n.isAnimationActive,b=n.isUpdateAnimationActive;if(u!==+u||d!==+d||h!==+h||m!==+m||h===0||m===0)return null;var j=Ie("recharts-rectangle",v);return b?H.createElement(Ca,{canBegin:s>0,from:{width:h,height:m,x:u,y:d},to:{width:h,height:m,x:u,y:d},duration:x,animationEasing:_,isActive:b},function(E){var P=E.width,O=E.height,C=E.x,A=E.y;return H.createElement(Ca,{canBegin:s>0,from:"0px ".concat(s===-1?1:s,"px"),to:"".concat(s,"px 0px"),attributeName:"strokeDasharray",begin:y,duration:x,isActive:w,easing:_},H.createElement("path",Vp({},Oe(n,!0),{className:j,d:NE(C,A,P,O,p),ref:r})))}):H.createElement("path",Vp({},Oe(n,!0),{className:j,d:NE(u,d,h,m,p)}))},Wce=["points","className","baseLinePoints","connectNulls"];function Pl(){return Pl=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function Vce(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}function SE(e){return Kce(e)||Gce(e)||Zce(e)||qce()}function qce(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Zce(e,t){if(e){if(typeof e=="string")return A1(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);if(n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set")return Array.from(e);if(n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return A1(e,t)}}function Gce(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function Kce(e){if(Array.isArray(e))return A1(e)}function A1(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&arguments[0]!==void 0?arguments[0]:[],n=[[]];return t.forEach(function(r){PE(r)?n[n.length-1].push(r):n[n.length-1].length>0&&n.push([])}),PE(t[0])&&n[n.length-1].push(t[0]),n[n.length-1].length<=0&&(n=n.slice(0,-1)),n},gd=function(t,n){var r=Yce(t);n&&(r=[r.reduce(function(a,s){return[].concat(SE(a),SE(s))},[])]);var i=r.map(function(a){return a.reduce(function(s,c,u){return"".concat(s).concat(u===0?"M":"L").concat(c.x,",").concat(c.y)},"")}).join("");return r.length===1?"".concat(i,"Z"):i},Xce=function(t,n,r){var i=gd(t,r);return"".concat(i.slice(-1)==="Z"?i.slice(0,-1):i,"L").concat(gd(n.reverse(),r).slice(1))},Qce=function(t){var n=t.points,r=t.className,i=t.baseLinePoints,a=t.connectNulls,s=Hce(t,Wce);if(!n||!n.length)return null;var c=Ie("recharts-polygon",r);if(i&&i.length){var u=s.stroke&&s.stroke!=="none",d=Xce(n,i,a);return H.createElement("g",{className:c},H.createElement("path",Pl({},Oe(s,!0),{fill:d.slice(-1)==="Z"?s.fill:"none",stroke:"none",d})),u?H.createElement("path",Pl({},Oe(s,!0),{fill:"none",d:gd(n,a)})):null,u?H.createElement("path",Pl({},Oe(s,!0),{fill:"none",d:gd(i,a)})):null)}var h=gd(n,a);return H.createElement("path",Pl({},Oe(s,!0),{fill:h.slice(-1)==="Z"?s.fill:"none",className:c,d:h}))};function T1(){return T1=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function aue(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}var oue=function(t,n,r,i,a,s){return"M".concat(t,",").concat(a,"v").concat(i,"M").concat(s,",").concat(n,"h").concat(r)},sue=function(t){var n=t.x,r=n===void 0?0:n,i=t.y,a=i===void 0?0:i,s=t.top,c=s===void 0?0:s,u=t.left,d=u===void 0?0:u,h=t.width,m=h===void 0?0:h,p=t.height,v=p===void 0?0:p,_=t.className,x=iue(t,Jce),y=eue({x:r,y:a,top:c,left:d,width:m,height:v},x);return!ue(r)||!ue(a)||!ue(m)||!ue(v)||!ue(c)||!ue(d)?null:H.createElement("path",M1({},Oe(y,!0),{className:Ie("recharts-cross",_),d:oue(r,a,m,v,c,d)}))},lue=I0,cue=L6,uue=Wi;function due(e,t){return e&&e.length?lue(e,uue(t),cue):void 0}var fue=due;const hue=Qe(fue);var mue=I0,pue=Wi,gue=$6;function vue(e,t){return e&&e.length?mue(e,pue(t),gue):void 0}var yue=vue;const xue=Qe(yue);var bue=["cx","cy","angle","ticks","axisLine"],wue=["ticks","tick","angle","tickFormatter","stroke"];function jc(e){"@babel/helpers - typeof";return jc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},jc(e)}function vd(){return vd=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function _ue(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}function jue(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function CE(e,t){for(var n=0;nME?s=i==="outer"?"start":"end":a<-ME?s=i==="outer"?"end":"start":s="middle",s}},{key:"renderAxisLine",value:function(){var r=this.props,i=r.cx,a=r.cy,s=r.radius,c=r.axisLine,u=r.axisLineType,d=Wo(Wo({},Oe(this.props,!1)),{},{fill:"none"},Oe(c,!1));if(u==="circle")return H.createElement(Z2,Jo({className:"recharts-polar-angle-axis-line"},d,{cx:i,cy:a,r:s}));var h=this.props.ticks,m=h.map(function(p){return ct(i,a,s,p.coordinate)});return H.createElement(Qce,Jo({className:"recharts-polar-angle-axis-line"},d,{points:m}))}},{key:"renderTicks",value:function(){var r=this,i=this.props,a=i.ticks,s=i.tick,c=i.tickLine,u=i.tickFormatter,d=i.stroke,h=Oe(this.props,!1),m=Oe(s,!1),p=Wo(Wo({},h),{},{fill:"none"},Oe(c,!1)),v=a.map(function(_,x){var y=r.getTickLineCoord(_),w=r.getTickTextAnchor(_),b=Wo(Wo(Wo({textAnchor:w},h),{},{stroke:"none",fill:d},m),{},{index:x,payload:_,x:y.x2,y:y.y2});return H.createElement(Ye,Jo({className:Ie("recharts-polar-angle-axis-tick",cM(s)),key:"tick-".concat(_.coordinate)},ks(r.props,_,x)),c&&H.createElement("line",Jo({className:"recharts-polar-angle-axis-tick-line"},p,y)),s&&t.renderTickItem(s,b,u?u(_.value,x):_.value))});return H.createElement(Ye,{className:"recharts-polar-angle-axis-ticks"},v)}},{key:"render",value:function(){var r=this.props,i=r.ticks,a=r.radius,s=r.axisLine;return a<=0||!i||!i.length?null:H.createElement(Ye,{className:Ie("recharts-polar-angle-axis",this.props.className)},s&&this.renderAxisLine(),this.renderTicks())}}],[{key:"renderTickItem",value:function(r,i,a){var s;return H.isValidElement(r)?s=H.cloneElement(r,i):Ce(r)?s=r(i):s=H.createElement(Cs,Jo({},i,{className:"recharts-polar-angle-axis-tick-value"}),a),s}}])}(N.PureComponent);V0(q0,"displayName","PolarAngleAxis");V0(q0,"axisType","angleAxis");V0(q0,"defaultProps",{type:"category",angleAxisId:0,scale:"auto",cx:0,cy:0,orientation:"outer",axisLine:!0,tickLine:!0,tickSize:8,tick:!0,hide:!1,allowDuplicatedCategory:!0});var Fue=ET,Due=Fue(Object.getPrototypeOf,Object),Bue=Due,zue=$a,Uue=Bue,Wue=Ia,Hue="[object Object]",Vue=Function.prototype,que=Object.prototype,EM=Vue.toString,Zue=que.hasOwnProperty,Gue=EM.call(Object);function Kue(e){if(!Wue(e)||zue(e)!=Hue)return!1;var t=Uue(e);if(t===null)return!0;var n=Zue.call(t,"constructor")&&t.constructor;return typeof n=="function"&&n instanceof n&&EM.call(n)==Gue}var Yue=Kue;const Xue=Qe(Yue);var Que=$a,Jue=Ia,ede="[object Boolean]";function tde(e){return e===!0||e===!1||Jue(e)&&Que(e)==ede}var nde=tde;const rde=Qe(nde);function yf(e){"@babel/helpers - typeof";return yf=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},yf(e)}function Gp(){return Gp=Object.assign?Object.assign.bind():function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n0,from:{upperWidth:0,lowerWidth:0,height:p,x:u,y:d},to:{upperWidth:h,lowerWidth:m,height:p,x:u,y:d},duration:x,animationEasing:_,isActive:w},function(j){var E=j.upperWidth,P=j.lowerWidth,O=j.height,C=j.x,A=j.y;return H.createElement(Ca,{canBegin:s>0,from:"0px ".concat(s===-1?1:s,"px"),to:"".concat(s,"px 0px"),attributeName:"strokeDasharray",begin:y,duration:x,easing:_},H.createElement("path",Gp({},Oe(n,!0),{className:b,d:RE(C,A,E,P,O),ref:r})))}):H.createElement("g",null,H.createElement("path",Gp({},Oe(n,!0),{className:b,d:RE(u,d,h,m,p)})))},mde=["option","shapeType","propTransformer","activeClassName","isActive"];function xf(e){"@babel/helpers - typeof";return xf=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},xf(e)}function pde(e,t){if(e==null)return{};var n=gde(e,t),r,i;if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(i=0;i=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function gde(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}function FE(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function Kp(e){for(var t=1;t0?gr(j,"paddingAngle",0):0;if(P){var C=qa(P.endAngle-P.startAngle,j.endAngle-j.startAngle),A=ot(ot({},j),{},{startAngle:b+O,endAngle:b+C(x)+O});y.push(A),b=A.endAngle}else{var T=j.endAngle,$=j.startAngle,z=qa(0,T-$),D=z(x),Z=ot(ot({},j),{},{startAngle:b+O,endAngle:b+D+O});y.push(Z),b=Z.endAngle}}),H.createElement(Ye,null,r.renderSectorsStatically(y))})}},{key:"attachKeyboardHandlers",value:function(r){var i=this;r.onkeydown=function(a){if(!a.altKey)switch(a.key){case"ArrowLeft":{var s=++i.state.sectorToFocus%i.sectorRefs.length;i.sectorRefs[s].focus(),i.setState({sectorToFocus:s});break}case"ArrowRight":{var c=--i.state.sectorToFocus<0?i.sectorRefs.length-1:i.state.sectorToFocus%i.sectorRefs.length;i.sectorRefs[c].focus(),i.setState({sectorToFocus:c});break}case"Escape":{i.sectorRefs[i.state.sectorToFocus].blur(),i.setState({sectorToFocus:0});break}}}}},{key:"renderSectors",value:function(){var r=this.props,i=r.sectors,a=r.isAnimationActive,s=this.state.prevSectors;return a&&i&&i.length&&(!s||!D0(s,i))?this.renderSectorsWithAnimation():this.renderSectorsStatically(i)}},{key:"componentDidMount",value:function(){this.pieRef&&this.attachKeyboardHandlers(this.pieRef)}},{key:"render",value:function(){var r=this,i=this.props,a=i.hide,s=i.sectors,c=i.className,u=i.label,d=i.cx,h=i.cy,m=i.innerRadius,p=i.outerRadius,v=i.isAnimationActive,_=this.state.isAnimationFinished;if(a||!s||!s.length||!ue(d)||!ue(h)||!ue(m)||!ue(p))return null;var x=Ie("recharts-pie",c);return H.createElement(Ye,{tabIndex:this.props.rootTabIndex,className:x,ref:function(w){r.pieRef=w}},this.renderSectors(),u&&this.renderLabels(s),en.renderCallByParent(this.props,null,!1),(!v||_)&&No.renderCallByParent(this.props,s,!1))}}],[{key:"getDerivedStateFromProps",value:function(r,i){return i.prevIsAnimationActive!==r.isAnimationActive?{prevIsAnimationActive:r.isAnimationActive,prevAnimationId:r.animationId,curSectors:r.sectors,prevSectors:[],isAnimationFinished:!0}:r.isAnimationActive&&r.animationId!==i.prevAnimationId?{prevAnimationId:r.animationId,curSectors:r.sectors,prevSectors:i.curSectors,isAnimationFinished:!0}:r.sectors!==i.curSectors?{curSectors:r.sectors,isAnimationFinished:!0}:null}},{key:"getTextAnchor",value:function(r,i){return r>i?"start":r=360?b:b-1)*u,E=y-b*v-j,P=i.reduce(function(A,T){var $=_n(T,w,0);return A+(ue($)?$:0)},0),O;if(P>0){var C;O=i.map(function(A,T){var $=_n(A,w,0),z=_n(A,h,T),D=(ue($)?$:0)/P,Z;T?Z=C.endAngle+Mn(x)*u*($!==0?1:0):Z=s;var I=Z+Mn(x)*(($!==0?v:0)+D*E),F=(Z+I)/2,B=(_.innerRadius+_.outerRadius)/2,G=[{name:z,value:$,payload:A,dataKey:w,type:p}],R=ct(_.cx,_.cy,B,F);return C=ot(ot(ot({percent:D,cornerRadius:a,name:z,tooltipPayload:G,midAngle:F,middleRadius:B,tooltipPosition:R},A),_),{},{value:_n(A,w),startAngle:Z,endAngle:I,payload:A,paddingAngle:Mn(x)*u}),C})}return ot(ot({},_),{},{sectors:O,data:i})});var Rde=Math.ceil,Fde=Math.max;function Dde(e,t,n,r){for(var i=-1,a=Fde(Rde((t-e)/(n||1)),0),s=Array(a);a--;)s[r?a:++i]=e,e+=n;return s}var Bde=Dde,zde=qT,UE=1/0,Ude=17976931348623157e292;function Wde(e){if(!e)return e===0?e:0;if(e=zde(e),e===UE||e===-UE){var t=e<0?-1:1;return t*Ude}return e===e?e:0}var AM=Wde,Hde=Bde,Vde=O0,zy=AM;function qde(e){return function(t,n,r){return r&&typeof r!="number"&&Vde(t,n,r)&&(n=r=void 0),t=zy(t),n===void 0?(n=t,t=0):n=zy(n),r=r===void 0?t0&&r.handleDrag(i.changedTouches[0])}),ir(r,"handleDragEnd",function(){r.setState({isTravellerMoving:!1,isSlideMoving:!1},function(){var i=r.props,a=i.endIndex,s=i.onDragEnd,c=i.startIndex;s==null||s({endIndex:a,startIndex:c})}),r.detachDragEndListener()}),ir(r,"handleLeaveWrapper",function(){(r.state.isTravellerMoving||r.state.isSlideMoving)&&(r.leaveTimer=window.setTimeout(r.handleDragEnd,r.props.leaveTimeOut))}),ir(r,"handleEnterSlideOrTraveller",function(){r.setState({isTextActive:!0})}),ir(r,"handleLeaveSlideOrTraveller",function(){r.setState({isTextActive:!1})}),ir(r,"handleSlideDragStart",function(i){var a=ZE(i)?i.changedTouches[0]:i;r.setState({isTravellerMoving:!1,isSlideMoving:!0,slideMoveStartX:a.pageX}),r.attachDragEndListener()}),r.travellerDragStartHandlers={startX:r.handleTravellerDragStart.bind(r,"startX"),endX:r.handleTravellerDragStart.bind(r,"endX")},r.state={},r}return ofe(t,e),nfe(t,[{key:"componentWillUnmount",value:function(){this.leaveTimer&&(clearTimeout(this.leaveTimer),this.leaveTimer=null),this.detachDragEndListener()}},{key:"getIndex",value:function(r){var i=r.startX,a=r.endX,s=this.state.scaleValues,c=this.props,u=c.gap,d=c.data,h=d.length-1,m=Math.min(i,a),p=Math.max(i,a),v=t.getIndexInRange(s,m),_=t.getIndexInRange(s,p);return{startIndex:v-v%u,endIndex:_===h?h:_-_%u}}},{key:"getTextOfTick",value:function(r){var i=this.props,a=i.data,s=i.tickFormatter,c=i.dataKey,u=_n(a[r],c,r);return Ce(s)?s(u,r):u}},{key:"attachDragEndListener",value:function(){window.addEventListener("mouseup",this.handleDragEnd,!0),window.addEventListener("touchend",this.handleDragEnd,!0),window.addEventListener("mousemove",this.handleDrag,!0)}},{key:"detachDragEndListener",value:function(){window.removeEventListener("mouseup",this.handleDragEnd,!0),window.removeEventListener("touchend",this.handleDragEnd,!0),window.removeEventListener("mousemove",this.handleDrag,!0)}},{key:"handleSlideDrag",value:function(r){var i=this.state,a=i.slideMoveStartX,s=i.startX,c=i.endX,u=this.props,d=u.x,h=u.width,m=u.travellerWidth,p=u.startIndex,v=u.endIndex,_=u.onChange,x=r.pageX-a;x>0?x=Math.min(x,d+h-m-c,d+h-m-s):x<0&&(x=Math.max(x,d-s,d-c));var y=this.getIndex({startX:s+x,endX:c+x});(y.startIndex!==p||y.endIndex!==v)&&_&&_(y),this.setState({startX:s+x,endX:c+x,slideMoveStartX:r.pageX})}},{key:"handleTravellerDragStart",value:function(r,i){var a=ZE(i)?i.changedTouches[0]:i;this.setState({isSlideMoving:!1,isTravellerMoving:!0,movingTravellerId:r,brushMoveStartX:a.pageX}),this.attachDragEndListener()}},{key:"handleTravellerMove",value:function(r){var i=this.state,a=i.brushMoveStartX,s=i.movingTravellerId,c=i.endX,u=i.startX,d=this.state[s],h=this.props,m=h.x,p=h.width,v=h.travellerWidth,_=h.onChange,x=h.gap,y=h.data,w={startX:this.state.startX,endX:this.state.endX},b=r.pageX-a;b>0?b=Math.min(b,m+p-v-d):b<0&&(b=Math.max(b,m-d)),w[s]=d+b;var j=this.getIndex(w),E=j.startIndex,P=j.endIndex,O=function(){var A=y.length-1;return s==="startX"&&(c>u?E%x===0:P%x===0)||cu?P%x===0:E%x===0)||c>u&&P===A};this.setState(ir(ir({},s,d+b),"brushMoveStartX",r.pageX),function(){_&&O()&&_(j)})}},{key:"handleTravellerMoveKeyboard",value:function(r,i){var a=this,s=this.state,c=s.scaleValues,u=s.startX,d=s.endX,h=this.state[i],m=c.indexOf(h);if(m!==-1){var p=m+r;if(!(p===-1||p>=c.length)){var v=c[p];i==="startX"&&v>=d||i==="endX"&&v<=u||this.setState(ir({},i,v),function(){a.props.onChange(a.getIndex({startX:a.state.startX,endX:a.state.endX}))})}}}},{key:"renderBackground",value:function(){var r=this.props,i=r.x,a=r.y,s=r.width,c=r.height,u=r.fill,d=r.stroke;return H.createElement("rect",{stroke:d,fill:u,x:i,y:a,width:s,height:c})}},{key:"renderPanorama",value:function(){var r=this.props,i=r.x,a=r.y,s=r.width,c=r.height,u=r.data,d=r.children,h=r.padding,m=N.Children.only(d);return m?H.cloneElement(m,{x:i,y:a,width:s,height:c,margin:h,compact:!0,data:u}):null}},{key:"renderTravellerLayer",value:function(r,i){var a,s,c=this,u=this.props,d=u.y,h=u.travellerWidth,m=u.height,p=u.traveller,v=u.ariaLabel,_=u.data,x=u.startIndex,y=u.endIndex,w=Math.max(r,this.props.x),b=Uy(Uy({},Oe(this.props,!1)),{},{x:w,y:d,width:h,height:m}),j=v||"Min value: ".concat((a=_[x])===null||a===void 0?void 0:a.name,", Max value: ").concat((s=_[y])===null||s===void 0?void 0:s.name);return H.createElement(Ye,{tabIndex:0,role:"slider","aria-label":j,"aria-valuenow":r,className:"recharts-brush-traveller",onMouseEnter:this.handleEnterSlideOrTraveller,onMouseLeave:this.handleLeaveSlideOrTraveller,onMouseDown:this.travellerDragStartHandlers[i],onTouchStart:this.travellerDragStartHandlers[i],onKeyDown:function(P){["ArrowLeft","ArrowRight"].includes(P.key)&&(P.preventDefault(),P.stopPropagation(),c.handleTravellerMoveKeyboard(P.key==="ArrowRight"?1:-1,i))},onFocus:function(){c.setState({isTravellerFocused:!0})},onBlur:function(){c.setState({isTravellerFocused:!1})},style:{cursor:"col-resize"}},t.renderTraveller(p,b))}},{key:"renderSlide",value:function(r,i){var a=this.props,s=a.y,c=a.height,u=a.stroke,d=a.travellerWidth,h=Math.min(r,i)+d,m=Math.max(Math.abs(i-r)-d,0);return H.createElement("rect",{className:"recharts-brush-slide",onMouseEnter:this.handleEnterSlideOrTraveller,onMouseLeave:this.handleLeaveSlideOrTraveller,onMouseDown:this.handleSlideDragStart,onTouchStart:this.handleSlideDragStart,style:{cursor:"move"},stroke:"none",fill:u,fillOpacity:.2,x:h,y:s,width:m,height:c})}},{key:"renderText",value:function(){var r=this.props,i=r.startIndex,a=r.endIndex,s=r.y,c=r.height,u=r.travellerWidth,d=r.stroke,h=this.state,m=h.startX,p=h.endX,v=5,_={pointerEvents:"none",fill:d};return H.createElement(Ye,{className:"recharts-brush-texts"},H.createElement(Cs,Qp({textAnchor:"end",verticalAnchor:"middle",x:Math.min(m,p)-v,y:s+c/2},_),this.getTextOfTick(i)),H.createElement(Cs,Qp({textAnchor:"start",verticalAnchor:"middle",x:Math.max(m,p)+u+v,y:s+c/2},_),this.getTextOfTick(a)))}},{key:"render",value:function(){var r=this.props,i=r.data,a=r.className,s=r.children,c=r.x,u=r.y,d=r.width,h=r.height,m=r.alwaysShowText,p=this.state,v=p.startX,_=p.endX,x=p.isTextActive,y=p.isSlideMoving,w=p.isTravellerMoving,b=p.isTravellerFocused;if(!i||!i.length||!ue(c)||!ue(u)||!ue(d)||!ue(h)||d<=0||h<=0)return null;var j=Ie("recharts-brush",a),E=H.Children.count(s)===1,P=efe("userSelect","none");return H.createElement(Ye,{className:j,onMouseLeave:this.handleLeaveWrapper,onTouchMove:this.handleTouchMove,style:P},this.renderBackground(),E&&this.renderPanorama(),this.renderSlide(v,_),this.renderTravellerLayer(v,"startX"),this.renderTravellerLayer(_,"endX"),(x||y||w||b||m)&&this.renderText())}}],[{key:"renderDefaultTraveller",value:function(r){var i=r.x,a=r.y,s=r.width,c=r.height,u=r.stroke,d=Math.floor(a+c/2)-1;return H.createElement(H.Fragment,null,H.createElement("rect",{x:i,y:a,width:s,height:c,fill:u,stroke:"none"}),H.createElement("line",{x1:i+1,y1:d,x2:i+s-1,y2:d,fill:"none",stroke:"#fff"}),H.createElement("line",{x1:i+1,y1:d+2,x2:i+s-1,y2:d+2,fill:"none",stroke:"#fff"}))}},{key:"renderTraveller",value:function(r,i){var a;return H.isValidElement(r)?a=H.cloneElement(r,i):Ce(r)?a=r(i):a=t.renderDefaultTraveller(i),a}},{key:"getDerivedStateFromProps",value:function(r,i){var a=r.data,s=r.width,c=r.x,u=r.travellerWidth,d=r.updateId,h=r.startIndex,m=r.endIndex;if(a!==i.prevData||d!==i.prevUpdateId)return Uy({prevData:a,prevTravellerWidth:u,prevUpdateId:d,prevX:c,prevWidth:s},a&&a.length?lfe({data:a,width:s,x:c,travellerWidth:u,startIndex:h,endIndex:m}):{scale:null,scaleValues:null});if(i.scale&&(s!==i.prevWidth||c!==i.prevX||u!==i.prevTravellerWidth)){i.scale.range([c,c+s-u]);var p=i.scale.domain().map(function(v){return i.scale(v)});return{prevData:a,prevTravellerWidth:u,prevUpdateId:d,prevX:c,prevWidth:s,startX:i.scale(r.startIndex),endX:i.scale(r.endIndex),scaleValues:p}}return null}},{key:"getIndexInRange",value:function(r,i){for(var a=r.length,s=0,c=a-1;c-s>1;){var u=Math.floor((s+c)/2);r[u]>i?c=u:s=u}return i>=r[c]?c:s}}])}(N.PureComponent);ir(Ec,"displayName","Brush");ir(Ec,"defaultProps",{height:40,travellerWidth:5,gap:1,fill:"#fff",stroke:"#666",padding:{top:1,right:1,bottom:1,left:1},leaveTimeOut:1e3,alwaysShowText:!1});var cfe=y2;function ufe(e,t){var n;return cfe(e,function(r,i,a){return n=t(r,i,a),!n}),!!n}var dfe=ufe,ffe=xT,hfe=Wi,mfe=dfe,pfe=tr,gfe=O0;function vfe(e,t,n){var r=pfe(e)?ffe:mfe;return n&&gfe(e,t,n)&&(t=void 0),r(e,hfe(t))}var yfe=vfe;const xfe=Qe(yfe);var Di=function(t,n){var r=t.alwaysShow,i=t.ifOverflow;return r&&(i="extendDomain"),i===n},GE=zT;function bfe(e,t,n){t=="__proto__"&&GE?GE(e,t,{configurable:!0,enumerable:!0,value:n,writable:!0}):e[t]=n}var wfe=bfe,_fe=wfe,jfe=DT,Nfe=Wi;function Sfe(e,t){var n={};return t=Nfe(t),jfe(e,function(r,i,a){_fe(n,i,t(r,i,a))}),n}var Pfe=Sfe;const Efe=Qe(Pfe);function Ofe(e,t){for(var n=-1,r=e==null?0:e.length;++n=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function Vfe(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}function qfe(e,t){var n=e.x,r=e.y,i=Hfe(e,Bfe),a="".concat(n),s=parseInt(a,10),c="".concat(r),u=parseInt(c,10),d="".concat(t.height||i.height),h=parseInt(d,10),m="".concat(t.width||i.width),p=parseInt(m,10);return Fu(Fu(Fu(Fu(Fu({},t),i),s?{x:s}:{}),u?{y:u}:{}),{},{height:h,width:p,name:t.name,radius:t.radius})}function YE(e){return H.createElement(OM,F1({shapeType:"rectangle",propTransformer:qfe,activeClassName:"recharts-active-bar"},e))}var Zfe=function(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0;return function(r,i){if(typeof t=="number")return t;var a=ue(r)||pV(r);return a?t(r,i):(a||Ms(),n)}},Gfe=["value","background"],IM;function Oc(e){"@babel/helpers - typeof";return Oc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Oc(e)}function Kfe(e,t){if(e==null)return{};var n=Yfe(e,t),r,i;if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(i=0;i=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function Yfe(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}function eg(){return eg=Object.assign?Object.assign.bind():function(e){for(var t=1;t0&&Math.abs(F)0&&Math.abs(I)0&&(Z=Math.min((ae||0)-(I[ee-1]||0),Z))}),Number.isFinite(Z)){var F=Z/D,B=x.layout==="vertical"?r.height:r.width;if(x.padding==="gap"&&(C=F*B/2),x.padding==="no-gap"){var G=Ln(t.barCategoryGap,F*B),R=F*B/2;C=R-G-(R-G)/B*G}}}i==="xAxis"?A=[r.left+(j.left||0)+(C||0),r.left+r.width-(j.right||0)-(C||0)]:i==="yAxis"?A=u==="horizontal"?[r.top+r.height-(j.bottom||0),r.top+(j.top||0)]:[r.top+(j.top||0)+(C||0),r.top+r.height-(j.bottom||0)-(C||0)]:A=x.range,P&&(A=[A[1],A[0]]);var K=nM(x,a,p),W=K.scale,U=K.realScaleType;W.domain(w).range(A),rM(W);var Y=iM(W,ri(ri({},x),{},{realScaleType:U}));i==="xAxis"?(z=y==="top"&&!E||y==="bottom"&&E,T=r.left,$=m[O]-z*x.height):i==="yAxis"&&(z=y==="left"&&!E||y==="right"&&E,T=m[O]-z*x.width,$=r.top);var ne=ri(ri(ri({},x),Y),{},{realScaleType:U,x:T,y:$,scale:W,width:i==="xAxis"?r.width:x.width,height:i==="yAxis"?r.height:x.height});return ne.bandSize=Fp(ne,Y),!x.hide&&i==="xAxis"?m[O]+=(z?-1:1)*ne.height:x.hide||(m[O]+=(z?-1:1)*ne.width),ri(ri({},v),{},K0({},_,ne))},{})},BM=function(t,n){var r=t.x,i=t.y,a=n.x,s=n.y;return{x:Math.min(r,a),y:Math.min(i,s),width:Math.abs(a-r),height:Math.abs(s-i)}},lhe=function(t){var n=t.x1,r=t.y1,i=t.x2,a=t.y2;return BM({x:n,y:r},{x:i,y:a})},zM=function(){function e(t){ihe(this,e),this.scale=t}return ahe(e,[{key:"domain",get:function(){return this.scale.domain}},{key:"range",get:function(){return this.scale.range}},{key:"rangeMin",get:function(){return this.range()[0]}},{key:"rangeMax",get:function(){return this.range()[1]}},{key:"bandwidth",get:function(){return this.scale.bandwidth}},{key:"apply",value:function(n){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},i=r.bandAware,a=r.position;if(n!==void 0){if(a)switch(a){case"start":return this.scale(n);case"middle":{var s=this.bandwidth?this.bandwidth()/2:0;return this.scale(n)+s}case"end":{var c=this.bandwidth?this.bandwidth():0;return this.scale(n)+c}default:return this.scale(n)}if(i){var u=this.bandwidth?this.bandwidth()/2:0;return this.scale(n)+u}return this.scale(n)}}},{key:"isInRange",value:function(n){var r=this.range(),i=r[0],a=r[r.length-1];return i<=a?n>=i&&n<=a:n>=a&&n<=i}}],[{key:"create",value:function(n){return new e(n)}}])}();K0(zM,"EPS",1e-4);var G2=function(t){var n=Object.keys(t).reduce(function(r,i){return ri(ri({},r),{},K0({},i,zM.create(t[i])))},{});return ri(ri({},n),{},{apply:function(i){var a=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},s=a.bandAware,c=a.position;return Efe(i,function(u,d){return n[d].apply(u,{bandAware:s,position:c})})},isInRange:function(i){return $M(i,function(a,s){return n[s].isInRange(a)})}})};function che(e){return(e%180+180)%180}var uhe=function(t){var n=t.width,r=t.height,i=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0,a=che(i),s=a*Math.PI/180,c=Math.atan(r/n),u=s>c&&s-1?i[a?t[s]:s]:void 0}}var phe=mhe,ghe=AM;function vhe(e){var t=ghe(e),n=t%1;return t===t?n?t-n:t:0}var yhe=vhe,xhe=MT,bhe=Wi,whe=yhe,_he=Math.max;function jhe(e,t,n){var r=e==null?0:e.length;if(!r)return-1;var i=n==null?0:whe(n);return i<0&&(i=_he(r+i,0)),xhe(e,bhe(t),i)}var Nhe=jhe,She=phe,Phe=Nhe,Ehe=She(Phe),Ohe=Ehe;const khe=Qe(Ohe);var Che=wH(function(e){return{x:e.left,y:e.top,width:e.width,height:e.height}},function(e){return["l",e.left,"t",e.top,"w",e.width,"h",e.height].join("")}),K2=N.createContext(void 0),Y2=N.createContext(void 0),UM=N.createContext(void 0),WM=N.createContext({}),HM=N.createContext(void 0),VM=N.createContext(0),qM=N.createContext(0),tO=function(t){var n=t.state,r=n.xAxisMap,i=n.yAxisMap,a=n.offset,s=t.clipPathId,c=t.children,u=t.width,d=t.height,h=Che(a);return H.createElement(K2.Provider,{value:r},H.createElement(Y2.Provider,{value:i},H.createElement(WM.Provider,{value:a},H.createElement(UM.Provider,{value:h},H.createElement(HM.Provider,{value:s},H.createElement(VM.Provider,{value:d},H.createElement(qM.Provider,{value:u},c)))))))},Ahe=function(){return N.useContext(HM)},ZM=function(t){var n=N.useContext(K2);n==null&&Ms();var r=n[t];return r==null&&Ms(),r},The=function(){var t=N.useContext(K2);return Xa(t)},Mhe=function(){var t=N.useContext(Y2),n=khe(t,function(r){return $M(r.domain,Number.isFinite)});return n||Xa(t)},GM=function(t){var n=N.useContext(Y2);n==null&&Ms();var r=n[t];return r==null&&Ms(),r},Lhe=function(){var t=N.useContext(UM);return t},$he=function(){return N.useContext(WM)},X2=function(){return N.useContext(qM)},Q2=function(){return N.useContext(VM)};function kc(e){"@babel/helpers - typeof";return kc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},kc(e)}function Ihe(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function Rhe(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);ne*i)return!1;var a=n();return e*(t-e*a/2-r)>=0&&e*(t+e*a/2-i)<=0}function yme(e,t){return tL(e,t+1)}function xme(e,t,n,r,i){for(var a=(r||[]).slice(),s=t.start,c=t.end,u=0,d=1,h=s,m=function(){var _=r==null?void 0:r[u];if(_===void 0)return{v:tL(r,d)};var x=u,y,w=function(){return y===void 0&&(y=n(_,x)),y},b=_.coordinate,j=u===0||ag(e,b,w,h,c);j||(u=0,h=s,d+=1),j&&(h=b+e*(w()/2+i),u+=d)},p;d<=a.length;)if(p=m(),p)return p.v;return[]}function Nf(e){"@babel/helpers - typeof";return Nf=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Nf(e)}function cO(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function xn(e){for(var t=1;t0?v.coordinate-y*e:v.coordinate})}else a[p]=v=xn(xn({},v),{},{tickCoord:v.coordinate});var w=ag(e,v.tickCoord,x,c,u);w&&(u=v.tickCoord-e*(x()/2+i),a[p]=xn(xn({},v),{},{isShow:!0}))},h=s-1;h>=0;h--)d(h);return a}function Nme(e,t,n,r,i,a){var s=(r||[]).slice(),c=s.length,u=t.start,d=t.end;if(a){var h=r[c-1],m=n(h,c-1),p=e*(h.coordinate+e*m/2-d);s[c-1]=h=xn(xn({},h),{},{tickCoord:p>0?h.coordinate-p*e:h.coordinate});var v=ag(e,h.tickCoord,function(){return m},u,d);v&&(d=h.tickCoord-e*(m/2+i),s[c-1]=xn(xn({},h),{},{isShow:!0}))}for(var _=a?c-1:c,x=function(b){var j=s[b],E,P=function(){return E===void 0&&(E=n(j,b)),E};if(b===0){var O=e*(j.coordinate-e*P()/2-u);s[b]=j=xn(xn({},j),{},{tickCoord:O<0?j.coordinate-O*e:j.coordinate})}else s[b]=j=xn(xn({},j),{},{tickCoord:j.coordinate});var C=ag(e,j.tickCoord,P,u,d);C&&(u=j.tickCoord+e*(P()/2+i),s[b]=xn(xn({},j),{},{isShow:!0}))},y=0;y<_;y++)x(y);return s}function tj(e,t,n){var r=e.tick,i=e.ticks,a=e.viewBox,s=e.minTickGap,c=e.orientation,u=e.interval,d=e.tickFormatter,h=e.unit,m=e.angle;if(!i||!i.length||!r)return[];if(ue(u)||nu.isSsr)return yme(i,typeof u=="number"&&ue(u)?u:0);var p=[],v=c==="top"||c==="bottom"?"width":"height",_=h&&v==="width"?fd(h,{fontSize:t,letterSpacing:n}):{width:0,height:0},x=function(j,E){var P=Ce(d)?d(j.value,E):j.value;return v==="width"?gme(fd(P,{fontSize:t,letterSpacing:n}),_,m):fd(P,{fontSize:t,letterSpacing:n})[v]},y=i.length>=2?Mn(i[1].coordinate-i[0].coordinate):1,w=vme(a,y,v);return u==="equidistantPreserveStart"?xme(y,w,x,i,s):(u==="preserveStart"||u==="preserveStartEnd"?p=Nme(y,w,x,i,s,u==="preserveStartEnd"):p=jme(y,w,x,i,s),p.filter(function(b){return b.isShow}))}var Sme=["viewBox"],Pme=["viewBox"],Eme=["ticks"];function Tc(e){"@babel/helpers - typeof";return Tc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Tc(e)}function Ol(){return Ol=Object.assign?Object.assign.bind():function(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function Ome(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}function kme(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function dO(e,t){for(var n=0;n0?u(this.props):u(v)),s<=0||c<=0||!_||!_.length?null:H.createElement(Ye,{className:Ie("recharts-cartesian-axis",d),ref:function(y){r.layerReference=y}},a&&this.renderAxisLine(),this.renderTicks(_,this.state.fontSize,this.state.letterSpacing),en.renderCallByParent(this.props))}}],[{key:"renderTickItem",value:function(r,i,a){var s,c=Ie(i.className,"recharts-cartesian-axis-tick-value");return H.isValidElement(r)?s=H.cloneElement(r,Wt(Wt({},i),{},{className:c})):Ce(r)?s=r(Wt(Wt({},i),{},{className:c})):s=H.createElement(Cs,Ol({},i,{className:"recharts-cartesian-axis-tick-value"}),a),s}}])}(N.Component);nj(ou,"displayName","CartesianAxis");nj(ou,"defaultProps",{x:0,y:0,width:0,height:0,viewBox:{x:0,y:0,width:0,height:0},orientation:"bottom",ticks:[],stroke:"#666",tickLine:!0,axisLine:!0,tick:!0,mirror:!1,minTickGap:5,tickSize:6,tickMargin:2,interval:"preserveEnd"});var Ime=["x1","y1","x2","y2","key"],Rme=["offset"];function Ls(e){"@babel/helpers - typeof";return Ls=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Ls(e)}function fO(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),n.push.apply(n,r)}return n}function wn(e){for(var t=1;t=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function zme(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}var Ume=function(t){var n=t.fill;if(!n||n==="none")return null;var r=t.fillOpacity,i=t.x,a=t.y,s=t.width,c=t.height,u=t.ry;return H.createElement("rect",{x:i,y:a,ry:u,width:s,height:c,stroke:"none",fill:n,fillOpacity:r,className:"recharts-cartesian-grid-bg"})};function iL(e,t){var n;if(H.isValidElement(e))n=H.cloneElement(e,t);else if(Ce(e))n=e(t);else{var r=t.x1,i=t.y1,a=t.x2,s=t.y2,c=t.key,u=hO(t,Ime),d=Oe(u,!1);d.offset;var h=hO(d,Rme);n=H.createElement("line",ls({},h,{x1:r,y1:i,x2:a,y2:s,fill:"none",key:c}))}return n}function Wme(e){var t=e.x,n=e.width,r=e.horizontal,i=r===void 0?!0:r,a=e.horizontalPoints;if(!i||!a||!a.length)return null;var s=a.map(function(c,u){var d=wn(wn({},e),{},{x1:t,y1:c,x2:t+n,y2:c,key:"line-".concat(u),index:u});return iL(i,d)});return H.createElement("g",{className:"recharts-cartesian-grid-horizontal"},s)}function Hme(e){var t=e.y,n=e.height,r=e.vertical,i=r===void 0?!0:r,a=e.verticalPoints;if(!i||!a||!a.length)return null;var s=a.map(function(c,u){var d=wn(wn({},e),{},{x1:c,y1:t,x2:c,y2:t+n,key:"line-".concat(u),index:u});return iL(i,d)});return H.createElement("g",{className:"recharts-cartesian-grid-vertical"},s)}function Vme(e){var t=e.horizontalFill,n=e.fillOpacity,r=e.x,i=e.y,a=e.width,s=e.height,c=e.horizontalPoints,u=e.horizontal,d=u===void 0?!0:u;if(!d||!t||!t.length)return null;var h=c.map(function(p){return Math.round(p+i-i)}).sort(function(p,v){return p-v});i!==h[0]&&h.unshift(0);var m=h.map(function(p,v){var _=!h[v+1],x=_?i+s-p:h[v+1]-p;if(x<=0)return null;var y=v%t.length;return H.createElement("rect",{key:"react-".concat(v),y:p,x:r,height:x,width:a,stroke:"none",fill:t[y],fillOpacity:n,className:"recharts-cartesian-grid-bg"})});return H.createElement("g",{className:"recharts-cartesian-gridstripes-horizontal"},m)}function qme(e){var t=e.vertical,n=t===void 0?!0:t,r=e.verticalFill,i=e.fillOpacity,a=e.x,s=e.y,c=e.width,u=e.height,d=e.verticalPoints;if(!n||!r||!r.length)return null;var h=d.map(function(p){return Math.round(p+a-a)}).sort(function(p,v){return p-v});a!==h[0]&&h.unshift(0);var m=h.map(function(p,v){var _=!h[v+1],x=_?a+c-p:h[v+1]-p;if(x<=0)return null;var y=v%r.length;return H.createElement("rect",{key:"react-".concat(v),x:p,y:s,width:x,height:u,stroke:"none",fill:r[y],fillOpacity:i,className:"recharts-cartesian-grid-bg"})});return H.createElement("g",{className:"recharts-cartesian-gridstripes-vertical"},m)}var Zme=function(t,n){var r=t.xAxis,i=t.width,a=t.height,s=t.offset;return tM(tj(wn(wn(wn({},ou.defaultProps),r),{},{ticks:ga(r,!0),viewBox:{x:0,y:0,width:i,height:a}})),s.left,s.left+s.width,n)},Gme=function(t,n){var r=t.yAxis,i=t.width,a=t.height,s=t.offset;return tM(tj(wn(wn(wn({},ou.defaultProps),r),{},{ticks:ga(r,!0),viewBox:{x:0,y:0,width:i,height:a}})),s.top,s.top+s.height,n)},cl={horizontal:!0,vertical:!0,stroke:"#ccc",fill:"none",verticalFill:[],horizontalFill:[]};function aL(e){var t,n,r,i,a,s,c=X2(),u=Q2(),d=$he(),h=wn(wn({},e),{},{stroke:(t=e.stroke)!==null&&t!==void 0?t:cl.stroke,fill:(n=e.fill)!==null&&n!==void 0?n:cl.fill,horizontal:(r=e.horizontal)!==null&&r!==void 0?r:cl.horizontal,horizontalFill:(i=e.horizontalFill)!==null&&i!==void 0?i:cl.horizontalFill,vertical:(a=e.vertical)!==null&&a!==void 0?a:cl.vertical,verticalFill:(s=e.verticalFill)!==null&&s!==void 0?s:cl.verticalFill,x:ue(e.x)?e.x:d.left,y:ue(e.y)?e.y:d.top,width:ue(e.width)?e.width:d.width,height:ue(e.height)?e.height:d.height}),m=h.x,p=h.y,v=h.width,_=h.height,x=h.syncWithTicks,y=h.horizontalValues,w=h.verticalValues,b=The(),j=Mhe();if(!ue(v)||v<=0||!ue(_)||_<=0||!ue(m)||m!==+m||!ue(p)||p!==+p)return null;var E=h.verticalCoordinatesGenerator||Zme,P=h.horizontalCoordinatesGenerator||Gme,O=h.horizontalPoints,C=h.verticalPoints;if((!O||!O.length)&&Ce(P)){var A=y&&y.length,T=P({yAxis:j?wn(wn({},j),{},{ticks:A?y:j.ticks}):void 0,width:c,height:u,offset:d},A?!0:x);hi(Array.isArray(T),"horizontalCoordinatesGenerator should return Array but instead it returned [".concat(Ls(T),"]")),Array.isArray(T)&&(O=T)}if((!C||!C.length)&&Ce(E)){var $=w&&w.length,z=E({xAxis:b?wn(wn({},b),{},{ticks:$?w:b.ticks}):void 0,width:c,height:u,offset:d},$?!0:x);hi(Array.isArray(z),"verticalCoordinatesGenerator should return Array but instead it returned [".concat(Ls(z),"]")),Array.isArray(z)&&(C=z)}return H.createElement("g",{className:"recharts-cartesian-grid"},H.createElement(Ume,{fill:h.fill,fillOpacity:h.fillOpacity,x:h.x,y:h.y,width:h.width,height:h.height,ry:h.ry}),H.createElement(Wme,ls({},h,{offset:d,horizontalPoints:O,xAxis:b,yAxis:j})),H.createElement(Hme,ls({},h,{offset:d,verticalPoints:C,xAxis:b,yAxis:j})),H.createElement(Vme,ls({},h,{horizontalPoints:O})),H.createElement(qme,ls({},h,{verticalPoints:C})))}aL.displayName="CartesianGrid";function Mc(e){"@babel/helpers - typeof";return Mc=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Mc(e)}function Kme(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function Yme(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function Ipe(e,t){if(e==null)return{};var n={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){if(t.indexOf(r)>=0)continue;n[r]=e[r]}return n}function Rpe(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function Fpe(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n0?s:t&&t.length&&ue(i)&&ue(a)?t.slice(i,a+1):[]};function xL(e){return e==="number"?[0,"auto"]:void 0}var tw=function(t,n,r,i){var a=t.graphicalItems,s=t.tooltipAxis,c=nv(n,t);return r<0||!a||!a.length||r>=c.length?null:a.reduce(function(u,d){var h,m=(h=d.props.data)!==null&&h!==void 0?h:n;m&&t.dataStartIndex+t.dataEndIndex!==0&&t.dataEndIndex-t.dataStartIndex>=r&&(m=m.slice(t.dataStartIndex,t.dataEndIndex+1));var p;if(s.dataKey&&!s.allowDuplicatedCategory){var v=m===void 0?c:m;p=Pb(v,s.dataKey,i)}else p=m&&m[r]||c[r];return p?[].concat(Ic(u),[oM(d,p)]):u},[])},xO=function(t,n,r,i){var a=i||{x:t.chartX,y:t.chartY},s=Ype(a,r),c=t.orderedTooltipTicks,u=t.tooltipAxis,d=t.tooltipTicks,h=Eoe(s,c,d,u);if(h>=0&&d){var m=d[h]&&d[h].value,p=tw(t,n,h,m),v=Xpe(r,c,h,a);return{activeTooltipIndex:h,activeLabel:m,activePayload:p,activeCoordinate:v}}return null},Qpe=function(t,n){var r=n.axes,i=n.graphicalItems,a=n.axisType,s=n.axisIdKey,c=n.stackGroups,u=n.dataStartIndex,d=n.dataEndIndex,h=t.layout,m=t.children,p=t.stackOffset,v=eM(h,a);return r.reduce(function(_,x){var y,w=x.type.defaultProps!==void 0?te(te({},x.type.defaultProps),x.props):x.props,b=w.type,j=w.dataKey,E=w.allowDataOverflow,P=w.allowDuplicatedCategory,O=w.scale,C=w.ticks,A=w.includeHidden,T=w[s];if(_[T])return _;var $=nv(t.data,{graphicalItems:i.filter(function(Y){var ne,ae=s in Y.props?Y.props[s]:(ne=Y.type.defaultProps)===null||ne===void 0?void 0:ne[s];return ae===T}),dataStartIndex:u,dataEndIndex:d}),z=$.length,D,Z,I;Npe(w.domain,E,b)&&(D=g1(w.domain,null,E),v&&(b==="number"||O!=="auto")&&(I=md($,j,"category")));var F=xL(b);if(!D||D.length===0){var B,G=(B=w.domain)!==null&&B!==void 0?B:F;if(j){if(D=md($,j,b),b==="category"&&v){var R=vV(D);P&&R?(Z=D,D=Xp(0,z)):P||(D=Z3(G,D,x).reduce(function(Y,ne){return Y.indexOf(ne)>=0?Y:[].concat(Ic(Y),[ne])},[]))}else if(b==="category")P?D=D.filter(function(Y){return Y!==""&&!De(Y)}):D=Z3(G,D,x).reduce(function(Y,ne){return Y.indexOf(ne)>=0||ne===""||De(ne)?Y:[].concat(Ic(Y),[ne])},[]);else if(b==="number"){var K=Toe($,i.filter(function(Y){var ne,ae,ee=s in Y.props?Y.props[s]:(ne=Y.type.defaultProps)===null||ne===void 0?void 0:ne[s],ce="hide"in Y.props?Y.props.hide:(ae=Y.type.defaultProps)===null||ae===void 0?void 0:ae.hide;return ee===T&&(A||!ce)}),j,a,h);K&&(D=K)}v&&(b==="number"||O!=="auto")&&(I=md($,j,"category"))}else v?D=Xp(0,z):c&&c[T]&&c[T].hasStack&&b==="number"?D=p==="expand"?[0,1]:aM(c[T].stackGroups,u,d):D=J6($,i.filter(function(Y){var ne=s in Y.props?Y.props[s]:Y.type.defaultProps[s],ae="hide"in Y.props?Y.props.hide:Y.type.defaultProps.hide;return ne===T&&(A||!ae)}),b,h,!0);if(b==="number")D=Q1(m,D,T,a,C),G&&(D=g1(G,D,E));else if(b==="category"&&G){var W=G,U=D.every(function(Y){return W.indexOf(Y)>=0});U&&(D=W)}}return te(te({},_),{},Se({},T,te(te({},w),{},{axisType:a,domain:D,categoricalDomain:I,duplicateDomain:Z,originalDomain:(y=w.domain)!==null&&y!==void 0?y:F,isCategorical:v,layout:h})))},{})},Jpe=function(t,n){var r=n.graphicalItems,i=n.Axis,a=n.axisType,s=n.axisIdKey,c=n.stackGroups,u=n.dataStartIndex,d=n.dataEndIndex,h=t.layout,m=t.children,p=nv(t.data,{graphicalItems:r,dataStartIndex:u,dataEndIndex:d}),v=p.length,_=eM(h,a),x=-1;return r.reduce(function(y,w){var b=w.type.defaultProps!==void 0?te(te({},w.type.defaultProps),w.props):w.props,j=b[s],E=xL("number");if(!y[j]){x++;var P;return _?P=Xp(0,v):c&&c[j]&&c[j].hasStack?(P=aM(c[j].stackGroups,u,d),P=Q1(m,P,j,a)):(P=g1(E,J6(p,r.filter(function(O){var C,A,T=s in O.props?O.props[s]:(C=O.type.defaultProps)===null||C===void 0?void 0:C[s],$="hide"in O.props?O.props.hide:(A=O.type.defaultProps)===null||A===void 0?void 0:A.hide;return T===j&&!$}),"number",h),i.defaultProps.allowDataOverflow),P=Q1(m,P,j,a)),te(te({},y),{},Se({},j,te(te({axisType:a},i.defaultProps),{},{hide:!0,orientation:gr(Gpe,"".concat(a,".").concat(x%2),null),domain:P,originalDomain:E,isCategorical:_,layout:h})))}return y},{})},ege=function(t,n){var r=n.axisType,i=r===void 0?"xAxis":r,a=n.AxisComp,s=n.graphicalItems,c=n.stackGroups,u=n.dataStartIndex,d=n.dataEndIndex,h=t.children,m="".concat(i,"Id"),p=Dr(h,a),v={};return p&&p.length?v=Qpe(t,{axes:p,graphicalItems:s,axisType:i,axisIdKey:m,stackGroups:c,dataStartIndex:u,dataEndIndex:d}):s&&s.length&&(v=Jpe(t,{Axis:a,graphicalItems:s,axisType:i,axisIdKey:m,stackGroups:c,dataStartIndex:u,dataEndIndex:d})),v},tge=function(t){var n=Xa(t),r=ga(n,!1,!0);return{tooltipTicks:r,orderedTooltipTicks:x2(r,function(i){return i.coordinate}),tooltipAxis:n,tooltipAxisBandSize:Fp(n,r)}},bO=function(t){var n=t.children,r=t.defaultShowTooltip,i=or(n,Ec),a=0,s=0;return t.data&&t.data.length!==0&&(s=t.data.length-1),i&&i.props&&(i.props.startIndex>=0&&(a=i.props.startIndex),i.props.endIndex>=0&&(s=i.props.endIndex)),{chartX:0,chartY:0,dataStartIndex:a,dataEndIndex:s,activeTooltipIndex:-1,isTooltipActive:!!r}},nge=function(t){return!t||!t.length?!1:t.some(function(n){var r=xa(n&&n.type);return r&&r.indexOf("Bar")>=0})},wO=function(t){return t==="horizontal"?{numericAxisName:"yAxis",cateAxisName:"xAxis"}:t==="vertical"?{numericAxisName:"xAxis",cateAxisName:"yAxis"}:t==="centric"?{numericAxisName:"radiusAxis",cateAxisName:"angleAxis"}:{numericAxisName:"angleAxis",cateAxisName:"radiusAxis"}},rge=function(t,n){var r=t.props,i=t.graphicalItems,a=t.xAxisMap,s=a===void 0?{}:a,c=t.yAxisMap,u=c===void 0?{}:c,d=r.width,h=r.height,m=r.children,p=r.margin||{},v=or(m,Ec),_=or(m,Dl),x=Object.keys(u).reduce(function(P,O){var C=u[O],A=C.orientation;return!C.mirror&&!C.hide?te(te({},P),{},Se({},A,P[A]+C.width)):P},{left:p.left||0,right:p.right||0}),y=Object.keys(s).reduce(function(P,O){var C=s[O],A=C.orientation;return!C.mirror&&!C.hide?te(te({},P),{},Se({},A,gr(P,"".concat(A))+C.height)):P},{top:p.top||0,bottom:p.bottom||0}),w=te(te({},y),x),b=w.bottom;v&&(w.bottom+=v.props.height||Ec.defaultProps.height),_&&n&&(w=Coe(w,i,r,n));var j=d-w.left-w.right,E=h-w.top-w.bottom;return te(te({brushBottom:b},w),{},{width:Math.max(j,0),height:Math.max(E,0)})},ige=function(t,n){if(n==="xAxis")return t[n].width;if(n==="yAxis")return t[n].height},bL=function(t){var n=t.chartName,r=t.GraphicalChild,i=t.defaultTooltipEventType,a=i===void 0?"axis":i,s=t.validateTooltipEventTypes,c=s===void 0?["axis"]:s,u=t.axisComponents,d=t.legendContent,h=t.formatAxisMap,m=t.defaultProps,p=function(w,b){var j=b.graphicalItems,E=b.stackGroups,P=b.offset,O=b.updateId,C=b.dataStartIndex,A=b.dataEndIndex,T=w.barSize,$=w.layout,z=w.barGap,D=w.barCategoryGap,Z=w.maxBarSize,I=wO($),F=I.numericAxisName,B=I.cateAxisName,G=nge(j),R=[];return j.forEach(function(K,W){var U=nv(w.data,{graphicalItems:[K],dataStartIndex:C,dataEndIndex:A}),Y=K.type.defaultProps!==void 0?te(te({},K.type.defaultProps),K.props):K.props,ne=Y.dataKey,ae=Y.maxBarSize,ee=Y["".concat(F,"Id")],ce=Y["".concat(B,"Id")],Ne={},Pe=u.reduce(function(Wn,nr){var Zi=b["".concat(nr.axisType,"Map")],$o=Y["".concat(nr.axisType,"Id")];Zi&&Zi[$o]||nr.axisType==="zAxis"||Ms();var X=Zi[$o];return te(te({},Wn),{},Se(Se({},nr.axisType,X),"".concat(nr.axisType,"Ticks"),ga(X)))},Ne),se=Pe[B],ye=Pe["".concat(B,"Ticks")],je=E&&E[ee]&&E[ee].hasStack&&zoe(K,E[ee].stackGroups),ie=xa(K.type).indexOf("Bar")>=0,Ve=Fp(se,ye),Re=[],ut=G&&Ooe({barSize:T,stackGroups:E,totalSize:ige(Pe,B)});if(ie){var dt,Tt,pn=De(ae)?Z:ae,Gr=(dt=(Tt=Fp(se,ye,!0))!==null&&Tt!==void 0?Tt:pn)!==null&&dt!==void 0?dt:0;Re=koe({barGap:z,barCategoryGap:D,bandSize:Gr!==Ve?Gr:Ve,sizeList:ut[ce],maxBarSize:pn}),Gr!==Ve&&(Re=Re.map(function(Wn){return te(te({},Wn),{},{position:te(te({},Wn.position),{},{offset:Wn.position.offset-Gr/2})})}))}var yi=K&&K.type&&K.type.getComposedData;yi&&R.push({props:te(te({},yi(te(te({},Pe),{},{displayedData:U,props:w,dataKey:ne,item:K,bandSize:Ve,barPosition:Re,offset:P,stackedData:je,layout:$,dataStartIndex:C,dataEndIndex:A}))),{},Se(Se(Se({key:K.key||"item-".concat(W)},F,Pe[F]),B,Pe[B]),"animationId",O)),childIndex:kV(K,w.children),item:K})}),R},v=function(w,b){var j=w.props,E=w.dataStartIndex,P=w.dataEndIndex,O=w.updateId;if(!B5({props:j}))return null;var C=j.children,A=j.layout,T=j.stackOffset,$=j.data,z=j.reverseStackOrder,D=wO(A),Z=D.numericAxisName,I=D.cateAxisName,F=Dr(C,r),B=Doe($,F,"".concat(Z,"Id"),"".concat(I,"Id"),T,z),G=u.reduce(function(Y,ne){var ae="".concat(ne.axisType,"Map");return te(te({},Y),{},Se({},ae,ege(j,te(te({},ne),{},{graphicalItems:F,stackGroups:ne.axisType===Z&&B,dataStartIndex:E,dataEndIndex:P}))))},{}),R=rge(te(te({},G),{},{props:j,graphicalItems:F}),b==null?void 0:b.legendBBox);Object.keys(G).forEach(function(Y){G[Y]=h(j,G[Y],R,Y.replace("Map",""),n)});var K=G["".concat(I,"Map")],W=tge(K),U=p(j,te(te({},G),{},{dataStartIndex:E,dataEndIndex:P,updateId:O,graphicalItems:F,stackGroups:B,offset:R}));return te(te({formattedGraphicalItems:U,graphicalItems:F,offset:R,stackGroups:B},W),G)},_=function(y){function w(b){var j,E,P;return Rpe(this,w),P=Bpe(this,w,[b]),Se(P,"eventEmitterSymbol",Symbol("rechartsEventEmitter")),Se(P,"accessibilityManager",new jpe),Se(P,"handleLegendBBoxUpdate",function(O){if(O){var C=P.state,A=C.dataStartIndex,T=C.dataEndIndex,$=C.updateId;P.setState(te({legendBBox:O},v({props:P.props,dataStartIndex:A,dataEndIndex:T,updateId:$},te(te({},P.state),{},{legendBBox:O}))))}}),Se(P,"handleReceiveSyncEvent",function(O,C,A){if(P.props.syncId===O){if(A===P.eventEmitterSymbol&&typeof P.props.syncMethod!="function")return;P.applySyncEvent(C)}}),Se(P,"handleBrushChange",function(O){var C=O.startIndex,A=O.endIndex;if(C!==P.state.dataStartIndex||A!==P.state.dataEndIndex){var T=P.state.updateId;P.setState(function(){return te({dataStartIndex:C,dataEndIndex:A},v({props:P.props,dataStartIndex:C,dataEndIndex:A,updateId:T},P.state))}),P.triggerSyncEvent({dataStartIndex:C,dataEndIndex:A})}}),Se(P,"handleMouseEnter",function(O){var C=P.getMouseInfo(O);if(C){var A=te(te({},C),{},{isTooltipActive:!0});P.setState(A),P.triggerSyncEvent(A);var T=P.props.onMouseEnter;Ce(T)&&T(A,O)}}),Se(P,"triggeredAfterMouseMove",function(O){var C=P.getMouseInfo(O),A=C?te(te({},C),{},{isTooltipActive:!0}):{isTooltipActive:!1};P.setState(A),P.triggerSyncEvent(A);var T=P.props.onMouseMove;Ce(T)&&T(A,O)}),Se(P,"handleItemMouseEnter",function(O){P.setState(function(){return{isTooltipActive:!0,activeItem:O,activePayload:O.tooltipPayload,activeCoordinate:O.tooltipPosition||{x:O.cx,y:O.cy}}})}),Se(P,"handleItemMouseLeave",function(){P.setState(function(){return{isTooltipActive:!1}})}),Se(P,"handleMouseMove",function(O){O.persist(),P.throttleTriggeredAfterMouseMove(O)}),Se(P,"handleMouseLeave",function(O){P.throttleTriggeredAfterMouseMove.cancel();var C={isTooltipActive:!1};P.setState(C),P.triggerSyncEvent(C);var A=P.props.onMouseLeave;Ce(A)&&A(C,O)}),Se(P,"handleOuterEvent",function(O){var C=OV(O),A=gr(P.props,"".concat(C));if(C&&Ce(A)){var T,$;/.*touch.*/i.test(C)?$=P.getMouseInfo(O.changedTouches[0]):$=P.getMouseInfo(O),A((T=$)!==null&&T!==void 0?T:{},O)}}),Se(P,"handleClick",function(O){var C=P.getMouseInfo(O);if(C){var A=te(te({},C),{},{isTooltipActive:!0});P.setState(A),P.triggerSyncEvent(A);var T=P.props.onClick;Ce(T)&&T(A,O)}}),Se(P,"handleMouseDown",function(O){var C=P.props.onMouseDown;if(Ce(C)){var A=P.getMouseInfo(O);C(A,O)}}),Se(P,"handleMouseUp",function(O){var C=P.props.onMouseUp;if(Ce(C)){var A=P.getMouseInfo(O);C(A,O)}}),Se(P,"handleTouchMove",function(O){O.changedTouches!=null&&O.changedTouches.length>0&&P.throttleTriggeredAfterMouseMove(O.changedTouches[0])}),Se(P,"handleTouchStart",function(O){O.changedTouches!=null&&O.changedTouches.length>0&&P.handleMouseDown(O.changedTouches[0])}),Se(P,"handleTouchEnd",function(O){O.changedTouches!=null&&O.changedTouches.length>0&&P.handleMouseUp(O.changedTouches[0])}),Se(P,"handleDoubleClick",function(O){var C=P.props.onDoubleClick;if(Ce(C)){var A=P.getMouseInfo(O);C(A,O)}}),Se(P,"handleContextMenu",function(O){var C=P.props.onContextMenu;if(Ce(C)){var A=P.getMouseInfo(O);C(A,O)}}),Se(P,"triggerSyncEvent",function(O){P.props.syncId!==void 0&&Hy.emit(Vy,P.props.syncId,O,P.eventEmitterSymbol)}),Se(P,"applySyncEvent",function(O){var C=P.props,A=C.layout,T=C.syncMethod,$=P.state.updateId,z=O.dataStartIndex,D=O.dataEndIndex;if(O.dataStartIndex!==void 0||O.dataEndIndex!==void 0)P.setState(te({dataStartIndex:z,dataEndIndex:D},v({props:P.props,dataStartIndex:z,dataEndIndex:D,updateId:$},P.state)));else if(O.activeTooltipIndex!==void 0){var Z=O.chartX,I=O.chartY,F=O.activeTooltipIndex,B=P.state,G=B.offset,R=B.tooltipTicks;if(!G)return;if(typeof T=="function")F=T(R,O);else if(T==="value"){F=-1;for(var K=0;K=0){var je,ie;if(Z.dataKey&&!Z.allowDuplicatedCategory){var Ve=typeof Z.dataKey=="function"?ye:"payload.".concat(Z.dataKey.toString());je=Pb(K,Ve,F),ie=W&&U&&Pb(U,Ve,F)}else je=K==null?void 0:K[I],ie=W&&U&&U[I];if(ce||ee){var Re=O.props.activeIndex!==void 0?O.props.activeIndex:I;return[N.cloneElement(O,te(te(te({},T.props),Pe),{},{activeIndex:Re})),null,null]}if(!De(je))return[se].concat(Ic(P.renderActivePoints({item:T,activePoint:je,basePoint:ie,childIndex:I,isRange:W})))}else{var ut,dt=(ut=P.getItemByXY(P.state.activeCoordinate))!==null&&ut!==void 0?ut:{graphicalItem:se},Tt=dt.graphicalItem,pn=Tt.item,Gr=pn===void 0?O:pn,yi=Tt.childIndex,Wn=te(te(te({},T.props),Pe),{},{activeIndex:yi});return[N.cloneElement(Gr,Wn),null,null]}return W?[se,null,null]:[se,null]}),Se(P,"renderCustomized",function(O,C,A){return N.cloneElement(O,te(te({key:"recharts-customized-".concat(A)},P.props),P.state))}),Se(P,"renderMap",{CartesianGrid:{handler:sm,once:!0},ReferenceArea:{handler:P.renderReferenceElement},ReferenceLine:{handler:sm},ReferenceDot:{handler:P.renderReferenceElement},XAxis:{handler:sm},YAxis:{handler:sm},Brush:{handler:P.renderBrush,once:!0},Bar:{handler:P.renderGraphicChild},Line:{handler:P.renderGraphicChild},Area:{handler:P.renderGraphicChild},Radar:{handler:P.renderGraphicChild},RadialBar:{handler:P.renderGraphicChild},Scatter:{handler:P.renderGraphicChild},Pie:{handler:P.renderGraphicChild},Funnel:{handler:P.renderGraphicChild},Tooltip:{handler:P.renderCursor,once:!0},PolarGrid:{handler:P.renderPolarGrid,once:!0},PolarAngleAxis:{handler:P.renderPolarAxis},PolarRadiusAxis:{handler:P.renderPolarAxis},Customized:{handler:P.renderCustomized}}),P.clipPathId="".concat((j=b.id)!==null&&j!==void 0?j:Yf("recharts"),"-clip"),P.throttleTriggeredAfterMouseMove=ZT(P.triggeredAfterMouseMove,(E=b.throttleDelay)!==null&&E!==void 0?E:1e3/60),P.state={},P}return Wpe(w,y),Dpe(w,[{key:"componentDidMount",value:function(){var j,E;this.addListener(),this.accessibilityManager.setDetails({container:this.container,offset:{left:(j=this.props.margin.left)!==null&&j!==void 0?j:0,top:(E=this.props.margin.top)!==null&&E!==void 0?E:0},coordinateList:this.state.tooltipTicks,mouseHandlerCallback:this.triggeredAfterMouseMove,layout:this.props.layout}),this.displayDefaultTooltip()}},{key:"displayDefaultTooltip",value:function(){var j=this.props,E=j.children,P=j.data,O=j.height,C=j.layout,A=or(E,ni);if(A){var T=A.props.defaultIndex;if(!(typeof T!="number"||T<0||T>this.state.tooltipTicks.length-1)){var $=this.state.tooltipTicks[T]&&this.state.tooltipTicks[T].value,z=tw(this.state,P,T,$),D=this.state.tooltipTicks[T].coordinate,Z=(this.state.offset.top+O)/2,I=C==="horizontal",F=I?{x:D,y:Z}:{y:D,x:Z},B=this.state.formattedGraphicalItems.find(function(R){var K=R.item;return K.type.name==="Scatter"});B&&(F=te(te({},F),B.props.points[T].tooltipPosition),z=B.props.points[T].tooltipPayload);var G={activeTooltipIndex:T,isTooltipActive:!0,activeLabel:$,activePayload:z,activeCoordinate:F};this.setState(G),this.renderCursor(A),this.accessibilityManager.setIndex(T)}}}},{key:"getSnapshotBeforeUpdate",value:function(j,E){if(!this.props.accessibilityLayer)return null;if(this.state.tooltipTicks!==E.tooltipTicks&&this.accessibilityManager.setDetails({coordinateList:this.state.tooltipTicks}),this.props.layout!==j.layout&&this.accessibilityManager.setDetails({layout:this.props.layout}),this.props.margin!==j.margin){var P,O;this.accessibilityManager.setDetails({offset:{left:(P=this.props.margin.left)!==null&&P!==void 0?P:0,top:(O=this.props.margin.top)!==null&&O!==void 0?O:0}})}return null}},{key:"componentDidUpdate",value:function(j){Ob([or(j.children,ni)],[or(this.props.children,ni)])||this.displayDefaultTooltip()}},{key:"componentWillUnmount",value:function(){this.removeListener(),this.throttleTriggeredAfterMouseMove.cancel()}},{key:"getTooltipEventType",value:function(){var j=or(this.props.children,ni);if(j&&typeof j.props.shared=="boolean"){var E=j.props.shared?"axis":"item";return c.indexOf(E)>=0?E:a}return a}},{key:"getMouseInfo",value:function(j){if(!this.container)return null;var E=this.container,P=E.getBoundingClientRect(),O=tne(P),C={chartX:Math.round(j.pageX-O.left),chartY:Math.round(j.pageY-O.top)},A=P.width/E.offsetWidth||1,T=this.inRange(C.chartX,C.chartY,A);if(!T)return null;var $=this.state,z=$.xAxisMap,D=$.yAxisMap,Z=this.getTooltipEventType(),I=xO(this.state,this.props.data,this.props.layout,T);if(Z!=="axis"&&z&&D){var F=Xa(z).scale,B=Xa(D).scale,G=F&&F.invert?F.invert(C.chartX):null,R=B&&B.invert?B.invert(C.chartY):null;return te(te({},C),{},{xValue:G,yValue:R},I)}return I?te(te({},C),I):null}},{key:"inRange",value:function(j,E){var P=arguments.length>2&&arguments[2]!==void 0?arguments[2]:1,O=this.props.layout,C=j/P,A=E/P;if(O==="horizontal"||O==="vertical"){var T=this.state.offset,$=C>=T.left&&C<=T.left+T.width&&A>=T.top&&A<=T.top+T.height;return $?{x:C,y:A}:null}var z=this.state,D=z.angleAxisMap,Z=z.radiusAxisMap;if(D&&Z){var I=Xa(D);return Y3({x:C,y:A},I)}return null}},{key:"parseEventsOfWrapper",value:function(){var j=this.props.children,E=this.getTooltipEventType(),P=or(j,ni),O={};P&&E==="axis"&&(P.props.trigger==="click"?O={onClick:this.handleClick}:O={onMouseEnter:this.handleMouseEnter,onDoubleClick:this.handleDoubleClick,onMouseMove:this.handleMouseMove,onMouseLeave:this.handleMouseLeave,onTouchMove:this.handleTouchMove,onTouchStart:this.handleTouchStart,onTouchEnd:this.handleTouchEnd,onContextMenu:this.handleContextMenu});var C=fp(this.props,this.handleOuterEvent);return te(te({},C),O)}},{key:"addListener",value:function(){Hy.on(Vy,this.handleReceiveSyncEvent)}},{key:"removeListener",value:function(){Hy.removeListener(Vy,this.handleReceiveSyncEvent)}},{key:"filterFormatItem",value:function(j,E,P){for(var O=this.state.formattedGraphicalItems,C=0,A=O.length;C(await vt.get("/dashboard")).data});if(t)return o.jsx("div",{className:"flex justify-center items-center h-screen",children:o.jsx("div",{className:"text-xl text-gray-600",children:"Loading analytics..."})});const n=Object.entries((e==null?void 0:e.topics)||{}).map(([a,s])=>({name:a.replace(/_/g," ").replace(/\b\w/g,c=>c.toUpperCase()),value:s}));return o.jsx("div",{className:"min-h-screen p-8",style:{backgroundColor:"#F1F5F9"},children:o.jsxs("div",{className:"max-w-7xl mx-auto",children:[o.jsxs("div",{className:"mb-8",children:[o.jsx("h1",{className:"text-3xl font-bold mb-2",style:{color:"#354F52"},children:"Data & Trends"}),o.jsx("p",{className:"text-gray-600",children:"Statistics and insights across communities - see what's happening in local government"})]}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-6 mb-8",children:[o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 border-l-4 border-primary-500",children:[o.jsx("h3",{className:"text-sm font-medium text-gray-600 uppercase tracking-wide",children:"Total Documents"}),o.jsx("p",{className:"mt-2 text-4xl font-bold text-primary-600",children:((r=e==null?void 0:e.total_documents)==null?void 0:r.toLocaleString())||"0"}),o.jsx("p",{className:"mt-1 text-sm text-gray-500",children:"Meeting minutes & budgets"})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 border-l-4 border-amber-500",children:[o.jsx("h3",{className:"text-sm font-medium text-gray-600 uppercase tracking-wide",children:"Opportunities Found"}),o.jsx("p",{className:"mt-2 text-4xl font-bold text-amber-600",children:((i=e==null?void 0:e.total_opportunities)==null?void 0:i.toLocaleString())||"0"}),o.jsx("p",{className:"mt-1 text-sm text-gray-500",children:"Advocacy windows identified"})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 border-l-4 border-emerald-500",children:[o.jsx("h3",{className:"text-sm font-medium text-gray-600 uppercase tracking-wide",children:"States Monitored"}),o.jsx("p",{className:"mt-2 text-4xl font-bold text-emerald-600",children:(e==null?void 0:e.states_monitored)||"0"}),o.jsx("p",{className:"mt-1 text-sm text-gray-500",children:"Across the nation"})]})]}),o.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8",children:[o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6",children:[o.jsx("h3",{className:"text-xl font-semibold mb-4 text-gray-900",children:"Policy Topics"}),n.length>0?o.jsx(DP,{width:"100%",height:300,children:o.jsxs(age,{data:n,children:[o.jsx(aL,{strokeDasharray:"3 3"}),o.jsx(ev,{dataKey:"name",angle:-45,textAnchor:"end",height:100}),o.jsx(tv,{}),o.jsx(ni,{}),o.jsx(Gs,{dataKey:"value",fill:"#0ea5e9"})]})}):o.jsx("div",{className:"h-64 flex items-center justify-center text-gray-400",children:"No topic data available"})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6",children:[o.jsx("h3",{className:"text-xl font-semibold mb-4 text-gray-900",children:"Topic Distribution"}),n.length>0?o.jsx(DP,{width:"100%",height:300,children:o.jsxs(oge,{children:[o.jsx(Fa,{data:n,cx:"50%",cy:"50%",labelLine:!1,label:a=>a.name,outerRadius:80,fill:"#8884d8",dataKey:"value",children:n.map((a,s)=>o.jsx(k0,{fill:_O[s%_O.length]},`cell-${s}`))}),o.jsx(ni,{})]})}):o.jsx("div",{className:"h-64 flex items-center justify-center text-gray-400",children:"No topic data available"})]})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6",children:[o.jsx("h3",{className:"text-xl font-semibold mb-4 text-gray-900",children:"Recent Causes"}),e!=null&&e.recent_opportunities&&e.recent_opportunities.length>0?o.jsx("div",{className:"overflow-x-auto",children:o.jsxs("table",{className:"min-w-full divide-y divide-gray-200",children:[o.jsx("thead",{className:"bg-gray-50",children:o.jsxs("tr",{children:[o.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Location"}),o.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Topic"}),o.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Urgency"}),o.jsx("th",{className:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider",children:"Date"})]})}),o.jsx("tbody",{className:"bg-white divide-y divide-gray-200",children:e.recent_opportunities.map((a,s)=>o.jsxs("tr",{className:"hover:bg-gray-50",children:[o.jsxs("td",{className:"px-6 py-4 whitespace-nowrap",children:[o.jsx("div",{className:"text-sm font-medium text-gray-900",children:a.municipality}),o.jsx("div",{className:"text-sm text-gray-500",children:a.state})]}),o.jsx("td",{className:"px-6 py-4 whitespace-nowrap text-sm text-gray-900",children:a.topic.replace(/_/g," ")}),o.jsx("td",{className:"px-6 py-4 whitespace-nowrap",children:o.jsx("span",{className:`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${a.urgency==="critical"?"bg-red-100 text-red-800":a.urgency==="high"?"bg-orange-100 text-orange-800":a.urgency==="medium"?"bg-yellow-100 text-yellow-800":"bg-green-100 text-green-800"}`,children:a.urgency})}),o.jsx("td",{className:"px-6 py-4 whitespace-nowrap text-sm text-gray-500",children:new Date(a.date).toLocaleDateString()})]},s))})]})}):o.jsxs("div",{className:"text-center py-12",children:[o.jsx("p",{className:"text-gray-400 text-lg mb-4",children:"No opportunities found yet"}),o.jsx("p",{className:"text-gray-500 text-sm",children:"Run the data ingestion pipeline to analyze meetings and identify advocacy opportunities"})]})]})]})})}function lge(){const[e,t]=N.useState("all"),[n,r]=N.useState("2024");return o.jsx("div",{className:"min-h-screen p-8",style:{backgroundColor:"#F1F5F9"},children:o.jsxs("div",{className:"max-w-7xl mx-auto",children:[o.jsxs("div",{className:"mb-8",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-2",children:[o.jsx(fc,{className:"h-8 w-8",style:{color:"#52796F"}}),o.jsx("h1",{className:"text-3xl font-bold",style:{color:"#354F52"},children:"Budget Analysis"})]}),o.jsx("p",{className:"text-gray-600",children:"Explore city, county, and school budgets with budget-to-minutes delta analysis"})]}),o.jsx("div",{className:"bg-white rounded-lg shadow-sm p-6 mb-8",children:o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-4",children:[o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"State"}),o.jsxs("select",{value:e,onChange:i=>t(i.target.value),className:"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:outline-none",children:[o.jsx("option",{value:"all",children:"All States"}),o.jsx("option",{value:"AL",children:"Alabama"}),o.jsx("option",{value:"GA",children:"Georgia"}),o.jsx("option",{value:"MA",children:"Massachusetts"}),o.jsx("option",{value:"WA",children:"Washington"}),o.jsx("option",{value:"WI",children:"Wisconsin"})]})]}),o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Fiscal Year"}),o.jsxs("select",{value:n,onChange:i=>r(i.target.value),className:"w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:outline-none",children:[o.jsx("option",{value:"2024",children:"FY 2024"}),o.jsx("option",{value:"2023",children:"FY 2023"}),o.jsx("option",{value:"2022",children:"FY 2022"})]})]})]})}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-4 gap-6 mb-8",children:[o.jsx("div",{className:"bg-white rounded-lg shadow-sm p-6 border-l-4",style:{borderColor:"#354F52"},children:o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsxs("div",{children:[o.jsx("p",{className:"text-sm font-medium text-gray-600 uppercase tracking-wide",children:"Jurisdictions Tracked"}),o.jsx("p",{className:"mt-2 text-3xl font-bold",style:{color:"#354F52"},children:"90,000+"})]}),o.jsx(BB,{className:"h-12 w-12 text-gray-400"})]})}),o.jsx("div",{className:"bg-white rounded-lg shadow-sm p-6 border-l-4",style:{borderColor:"#52796F"},children:o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsxs("div",{children:[o.jsx("p",{className:"text-sm font-medium text-gray-600 uppercase tracking-wide",children:"Budget Records"}),o.jsx("p",{className:"mt-2 text-3xl font-bold",style:{color:"#52796F"},children:"15,000+"})]}),o.jsx(PA,{className:"h-12 w-12 text-gray-400"})]})}),o.jsx("div",{className:"bg-white rounded-lg shadow-sm p-6 border-l-4 border-emerald-500",children:o.jsx("div",{className:"flex items-center justify-between",children:o.jsxs("div",{children:[o.jsx("p",{className:"text-sm font-medium text-gray-600 uppercase tracking-wide",children:"Avg Budget Increase"}),o.jsxs("p",{className:"mt-2 text-3xl font-bold text-emerald-600 flex items-center gap-2",children:["+3.2%",o.jsx(hB,{className:"h-6 w-6"})]})]})})}),o.jsx("div",{className:"bg-white rounded-lg shadow-sm p-6 border-l-4 border-amber-500",children:o.jsx("div",{className:"flex items-center justify-between",children:o.jsxs("div",{children:[o.jsx("p",{className:"text-sm font-medium text-gray-600 uppercase tracking-wide",children:"States Covered"}),o.jsx("p",{className:"mt-2 text-3xl font-bold text-amber-600",children:"50"})]})})})]}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-6 mb-8",children:[o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6",children:[o.jsx("h3",{className:"text-lg font-semibold mb-4",style:{color:"#354F52"},children:"Budget-to-Minutes Delta Analysis"}),o.jsxs("p",{className:"text-gray-600 mb-4",children:["Compare what governments ",o.jsx("strong",{children:"say"})," in meeting minutes versus what they ",o.jsx("strong",{children:"actually allocate"})," in budgets."]}),o.jsxs("ul",{className:"space-y-2 text-sm text-gray-700",children:[o.jsxs("li",{className:"flex items-start gap-2",children:[o.jsx("span",{className:"text-green-600 font-bold",children:"✓"}),"Track rhetoric vs. reality in budget decisions"]}),o.jsxs("li",{className:"flex items-start gap-2",children:[o.jsx("span",{className:"text-green-600 font-bold",children:"✓"}),"Identify funding priorities and gaps"]}),o.jsxs("li",{className:"flex items-start gap-2",children:[o.jsx("span",{className:"text-green-600 font-bold",children:"✓"}),"Monitor year-over-year changes"]})]})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6",children:[o.jsx("h3",{className:"text-lg font-semibold mb-4",style:{color:"#52796F"},children:"Budget Categories Tracked"}),o.jsxs("div",{className:"space-y-3",children:[o.jsxs("div",{children:[o.jsxs("div",{className:"flex justify-between text-sm mb-1",children:[o.jsx("span",{className:"text-gray-700",children:"Education & Schools"}),o.jsx("span",{className:"font-semibold",children:"35%"})]}),o.jsx("div",{className:"w-full bg-gray-200 rounded-full h-2",children:o.jsx("div",{className:"bg-blue-600 h-2 rounded-full",style:{width:"35%"}})})]}),o.jsxs("div",{children:[o.jsxs("div",{className:"flex justify-between text-sm mb-1",children:[o.jsx("span",{className:"text-gray-700",children:"Public Safety"}),o.jsx("span",{className:"font-semibold",children:"25%"})]}),o.jsx("div",{className:"w-full bg-gray-200 rounded-full h-2",children:o.jsx("div",{className:"bg-red-600 h-2 rounded-full",style:{width:"25%"}})})]}),o.jsxs("div",{children:[o.jsxs("div",{className:"flex justify-between text-sm mb-1",children:[o.jsx("span",{className:"text-gray-700",children:"Infrastructure"}),o.jsx("span",{className:"font-semibold",children:"20%"})]}),o.jsx("div",{className:"w-full bg-gray-200 rounded-full h-2",children:o.jsx("div",{className:"bg-amber-600 h-2 rounded-full",style:{width:"20%"}})})]}),o.jsxs("div",{children:[o.jsxs("div",{className:"flex justify-between text-sm mb-1",children:[o.jsx("span",{className:"text-gray-700",children:"Health & Human Services"}),o.jsx("span",{className:"font-semibold",children:"15%"})]}),o.jsx("div",{className:"w-full bg-gray-200 rounded-full h-2",children:o.jsx("div",{className:"bg-green-600 h-2 rounded-full",style:{width:"15%"}})})]})]})]})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-8 text-center border-2 border-dashed border-gray-300",children:[o.jsx(fc,{className:"h-16 w-16 mx-auto text-gray-400 mb-4"}),o.jsx("h3",{className:"text-xl font-semibold mb-2",style:{color:"#354F52"},children:"Budget Data Integration In Progress"}),o.jsx("p",{className:"text-gray-600 max-w-2xl mx-auto",children:"We're currently integrating budget data from cities, counties, and school districts across all 50 states. Check back soon for interactive budget comparisons, trend analysis, and meeting-to-budget correlation insights."}),o.jsx("div",{className:"mt-6",children:o.jsx("a",{href:"/documents?search=budget",className:"inline-block px-6 py-3 rounded-lg text-white transition-colors",style:{backgroundColor:"#52796F"},onMouseOver:i=>i.currentTarget.style.backgroundColor="#354F52",onMouseOut:i=>i.currentTarget.style.backgroundColor="#52796F",children:"Search Budget Meeting Minutes"})})]})]})})}function wL(e,t){const n=N.useRef(t);N.useEffect(function(){t!==n.current&&e.attributionControl!=null&&(n.current!=null&&e.attributionControl.removeAttribution(n.current),t!=null&&e.attributionControl.addAttribution(t)),n.current=t},[e,t])}function cge(e,t,n){t.center!==n.center&&e.setLatLng(t.center),t.radius!=null&&t.radius!==n.radius&&e.setRadius(t.radius)}const uge=1;function dge(e){return Object.freeze({__version:uge,map:e})}function fge(e,t){return Object.freeze({...e,...t})}const _L=N.createContext(null),jL=_L.Provider;function rj(){const e=N.useContext(_L);if(e==null)throw new Error("No context provided: useLeafletContext() can only be used in a descendant of ");return e}function hge(e){function t(n,r){const{instance:i,context:a}=e(n).current;return N.useImperativeHandle(r,()=>i),n.children==null?null:H.createElement(jL,{value:a},n.children)}return N.forwardRef(t)}function mge(e){function t(n,r){const[i,a]=N.useState(!1),{instance:s}=e(n,a).current;N.useImperativeHandle(r,()=>s),N.useEffect(function(){i&&s.update()},[s,i,n.children]);const c=s._contentNode;return c?F4.createPortal(n.children,c):null}return N.forwardRef(t)}function pge(e){function t(n,r){const{instance:i}=e(n).current;return N.useImperativeHandle(r,()=>i),null}return N.forwardRef(t)}function ij(e,t){const n=N.useRef();N.useEffect(function(){return t!=null&&e.instance.on(t),n.current=t,function(){n.current!=null&&e.instance.off(n.current),n.current=null}},[e,t])}function rv(e,t){const n=e.pane??t.pane;return n?{...e,pane:n}:e}function gge(e,t){return function(r,i){const a=rj(),s=e(rv(r,a),a);return wL(a.map,r.attribution),ij(s.current,r.eventHandlers),t(s.current,a,r,i),s}}var nw={exports:{}};/* @preserve + * Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com + * (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade + */(function(e,t){(function(n,r){r(t)})(zu,function(n){var r="1.9.4";function i(l){var f,g,S,k;for(g=1,S=arguments.length;g"u"||!L||!L.Mixin)){l=j(l)?l:[l];for(var f=0;f0?Math.floor(l):Math.ceil(l)};R.prototype={clone:function(){return new R(this.x,this.y)},add:function(l){return this.clone()._add(W(l))},_add:function(l){return this.x+=l.x,this.y+=l.y,this},subtract:function(l){return this.clone()._subtract(W(l))},_subtract:function(l){return this.x-=l.x,this.y-=l.y,this},divideBy:function(l){return this.clone()._divideBy(l)},_divideBy:function(l){return this.x/=l,this.y/=l,this},multiplyBy:function(l){return this.clone()._multiplyBy(l)},_multiplyBy:function(l){return this.x*=l,this.y*=l,this},scaleBy:function(l){return new R(this.x*l.x,this.y*l.y)},unscaleBy:function(l){return new R(this.x/l.x,this.y/l.y)},round:function(){return this.clone()._round()},_round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this},floor:function(){return this.clone()._floor()},_floor:function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this},ceil:function(){return this.clone()._ceil()},_ceil:function(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this},trunc:function(){return this.clone()._trunc()},_trunc:function(){return this.x=K(this.x),this.y=K(this.y),this},distanceTo:function(l){l=W(l);var f=l.x-this.x,g=l.y-this.y;return Math.sqrt(f*f+g*g)},equals:function(l){return l=W(l),l.x===this.x&&l.y===this.y},contains:function(l){return l=W(l),Math.abs(l.x)<=Math.abs(this.x)&&Math.abs(l.y)<=Math.abs(this.y)},toString:function(){return"Point("+p(this.x)+", "+p(this.y)+")"}};function W(l,f,g){return l instanceof R?l:j(l)?new R(l[0],l[1]):l==null?l:typeof l=="object"&&"x"in l&&"y"in l?new R(l.x,l.y):new R(l,f,g)}function U(l,f){if(l)for(var g=f?[l,f]:l,S=0,k=g.length;S=this.min.x&&g.x<=this.max.x&&f.y>=this.min.y&&g.y<=this.max.y},intersects:function(l){l=Y(l);var f=this.min,g=this.max,S=l.min,k=l.max,M=k.x>=f.x&&S.x<=g.x,V=k.y>=f.y&&S.y<=g.y;return M&&V},overlaps:function(l){l=Y(l);var f=this.min,g=this.max,S=l.min,k=l.max,M=k.x>f.x&&S.xf.y&&S.y=f.lat&&k.lat<=g.lat&&S.lng>=f.lng&&k.lng<=g.lng},intersects:function(l){l=ae(l);var f=this._southWest,g=this._northEast,S=l.getSouthWest(),k=l.getNorthEast(),M=k.lat>=f.lat&&S.lat<=g.lat,V=k.lng>=f.lng&&S.lng<=g.lng;return M&&V},overlaps:function(l){l=ae(l);var f=this._southWest,g=this._northEast,S=l.getSouthWest(),k=l.getNorthEast(),M=k.lat>f.lat&&S.latf.lng&&S.lng1,q$=function(){var l=!1;try{var f=Object.defineProperty({},"passive",{get:function(){l=!0}});window.addEventListener("testPassiveEventSupport",m,f),window.removeEventListener("testPassiveEventSupport",m,f)}catch{}return l}(),Z$=function(){return!!document.createElement("canvas").getContext}(),dv=!!(document.createElementNS&&ut("svg").createSVGRect),G$=!!dv&&function(){var l=document.createElement("div");return l.innerHTML="",(l.firstChild&&l.firstChild.namespaceURI)==="http://www.w3.org/2000/svg"}(),K$=!dv&&function(){try{var l=document.createElement("div");l.innerHTML='';var f=l.firstChild;return f.style.behavior="url(#default#VML)",f&&typeof f.adj=="object"}catch{return!1}}(),Y$=navigator.platform.indexOf("Mac")===0,X$=navigator.platform.indexOf("Linux")===0;function bi(l){return navigator.userAgent.toLowerCase().indexOf(l)>=0}var be={ie:pn,ielt9:Gr,edge:yi,webkit:Wn,android:nr,android23:Zi,androidStock:X,opera:de,chrome:Ae,gecko:Pt,safari:on,phantom:sn,opera12:xi,win:Ks,ie3d:Rj,webkit3d:uv,gecko3d:Fj,any3d:D$,mobile:lu,mobileWebkit:B$,mobileWebkit3d:z$,msPointer:Dj,pointer:Bj,touch:U$,touchNative:zj,mobileOpera:W$,mobileGecko:H$,retina:V$,passiveEvents:q$,canvas:Z$,svg:dv,vml:K$,inlineSvg:G$,mac:Y$,linux:X$},Uj=be.msPointer?"MSPointerDown":"pointerdown",Wj=be.msPointer?"MSPointerMove":"pointermove",Hj=be.msPointer?"MSPointerUp":"pointerup",Vj=be.msPointer?"MSPointerCancel":"pointercancel",fv={touchstart:Uj,touchmove:Wj,touchend:Hj,touchcancel:Vj},qj={touchstart:rI,touchmove:lh,touchend:lh,touchcancel:lh},Ys={},Zj=!1;function Q$(l,f,g){return f==="touchstart"&&nI(),qj[f]?(g=qj[f].bind(this,g),l.addEventListener(fv[f],g,!1),g):(console.warn("wrong event specified:",f),m)}function J$(l,f,g){if(!fv[f]){console.warn("wrong event specified:",f);return}l.removeEventListener(fv[f],g,!1)}function eI(l){Ys[l.pointerId]=l}function tI(l){Ys[l.pointerId]&&(Ys[l.pointerId]=l)}function Gj(l){delete Ys[l.pointerId]}function nI(){Zj||(document.addEventListener(Uj,eI,!0),document.addEventListener(Wj,tI,!0),document.addEventListener(Hj,Gj,!0),document.addEventListener(Vj,Gj,!0),Zj=!0)}function lh(l,f){if(f.pointerType!==(f.MSPOINTER_TYPE_MOUSE||"mouse")){f.touches=[];for(var g in Ys)f.touches.push(Ys[g]);f.changedTouches=[f],l(f)}}function rI(l,f){f.MSPOINTER_TYPE_TOUCH&&f.pointerType===f.MSPOINTER_TYPE_TOUCH&&ln(f),lh(l,f)}function iI(l){var f={},g,S;for(S in l)g=l[S],f[S]=g&&g.bind?g.bind(l):g;return l=f,f.type="dblclick",f.detail=2,f.isTrusted=!1,f._simulated=!0,f}var aI=200;function oI(l,f){l.addEventListener("dblclick",f);var g=0,S;function k(M){if(M.detail!==1){S=M.detail;return}if(!(M.pointerType==="mouse"||M.sourceCapabilities&&!M.sourceCapabilities.firesTouchEvents)){var V=Jj(M);if(!(V.some(function(J){return J instanceof HTMLLabelElement&&J.attributes.for})&&!V.some(function(J){return J instanceof HTMLInputElement||J instanceof HTMLSelectElement}))){var Q=Date.now();Q-g<=aI?(S++,S===2&&f(iI(M))):S=1,g=Q}}}return l.addEventListener("click",k),{dblclick:f,simDblclick:k}}function sI(l,f){l.removeEventListener("dblclick",f.dblclick),l.removeEventListener("click",f.simDblclick)}var hv=dh(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),cu=dh(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),Kj=cu==="webkitTransition"||cu==="OTransition"?cu+"End":"transitionend";function Yj(l){return typeof l=="string"?document.getElementById(l):l}function uu(l,f){var g=l.style[f]||l.currentStyle&&l.currentStyle[f];if((!g||g==="auto")&&document.defaultView){var S=document.defaultView.getComputedStyle(l,null);g=S?S[f]:null}return g==="auto"?null:g}function qe(l,f,g){var S=document.createElement(l);return S.className=f||"",g&&g.appendChild(S),S}function xt(l){var f=l.parentNode;f&&f.removeChild(l)}function ch(l){for(;l.firstChild;)l.removeChild(l.firstChild)}function Xs(l){var f=l.parentNode;f&&f.lastChild!==l&&f.appendChild(l)}function Qs(l){var f=l.parentNode;f&&f.firstChild!==l&&f.insertBefore(l,f.firstChild)}function mv(l,f){if(l.classList!==void 0)return l.classList.contains(f);var g=uh(l);return g.length>0&&new RegExp("(^|\\s)"+f+"(\\s|$)").test(g)}function Le(l,f){if(l.classList!==void 0)for(var g=_(f),S=0,k=g.length;S0?2*window.devicePixelRatio:1;function tN(l){return be.edge?l.wheelDeltaY/2:l.deltaY&&l.deltaMode===0?-l.deltaY/uI:l.deltaY&&l.deltaMode===1?-l.deltaY*20:l.deltaY&&l.deltaMode===2?-l.deltaY*60:l.deltaX||l.deltaZ?0:l.wheelDelta?(l.wheelDeltaY||l.wheelDelta)/2:l.detail&&Math.abs(l.detail)<32765?-l.detail*20:l.detail?l.detail/-32765*60:0}function Pv(l,f){var g=f.relatedTarget;if(!g)return!0;try{for(;g&&g!==l;)g=g.parentNode}catch{return!1}return g!==l}var dI={__proto__:null,on:Te,off:at,stopPropagation:Fo,disableScrollPropagation:Sv,disableClickPropagation:mu,preventDefault:ln,stop:Do,getPropagationPath:Jj,getMousePosition:eN,getWheelDelta:tN,isExternalTarget:Pv,addListener:Te,removeListener:at},nN=G.extend({run:function(l,f,g,S){this.stop(),this._el=l,this._inProgress=!0,this._duration=g||.25,this._easeOutPower=1/Math.max(S||.5,.2),this._startPos=Ro(l),this._offset=f.subtract(this._startPos),this._startTime=+new Date,this.fire("start"),this._animate()},stop:function(){this._inProgress&&(this._step(!0),this._complete())},_animate:function(){this._animId=z(this._animate,this),this._step()},_step:function(l){var f=+new Date-this._startTime,g=this._duration*1e3;fthis.options.maxZoom)?this.setZoom(l):this},panInsideBounds:function(l,f){this._enforcingBounds=!0;var g=this.getCenter(),S=this._limitCenter(g,this._zoom,ae(l));return g.equals(S)||this.panTo(S,f),this._enforcingBounds=!1,this},panInside:function(l,f){f=f||{};var g=W(f.paddingTopLeft||f.padding||[0,0]),S=W(f.paddingBottomRight||f.padding||[0,0]),k=this.project(this.getCenter()),M=this.project(l),V=this.getPixelBounds(),Q=Y([V.min.add(g),V.max.subtract(S)]),J=Q.getSize();if(!Q.contains(M)){this._enforcingBounds=!0;var oe=M.subtract(Q.getCenter()),he=Q.extend(M).getSize().subtract(J);k.x+=oe.x<0?-he.x:he.x,k.y+=oe.y<0?-he.y:he.y,this.panTo(this.unproject(k),f),this._enforcingBounds=!1}return this},invalidateSize:function(l){if(!this._loaded)return this;l=i({animate:!1,pan:!0},l===!0?{animate:!0}:l);var f=this.getSize();this._sizeChanged=!0,this._lastCenter=null;var g=this.getSize(),S=f.divideBy(2).round(),k=g.divideBy(2).round(),M=S.subtract(k);return!M.x&&!M.y?this:(l.animate&&l.pan?this.panBy(M):(l.pan&&this._rawPanBy(M),this.fire("move"),l.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(s(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:f,newSize:g}))},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(l){if(l=this._locateOptions=i({timeout:1e4,watch:!1},l),!("geolocation"in navigator))return this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this;var f=s(this._handleGeolocationResponse,this),g=s(this._handleGeolocationError,this);return l.watch?this._locationWatchId=navigator.geolocation.watchPosition(f,g,l):navigator.geolocation.getCurrentPosition(f,g,l),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(l){if(this._container._leaflet_id){var f=l.code,g=l.message||(f===1?"permission denied":f===2?"position unavailable":"timeout");this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:f,message:"Geolocation error: "+g+"."})}},_handleGeolocationResponse:function(l){if(this._container._leaflet_id){var f=l.coords.latitude,g=l.coords.longitude,S=new ee(f,g),k=S.toBounds(l.coords.accuracy*2),M=this._locateOptions;if(M.setView){var V=this.getBoundsZoom(k);this.setView(S,M.maxZoom?Math.min(V,M.maxZoom):V)}var Q={latlng:S,bounds:k,timestamp:l.timestamp};for(var J in l.coords)typeof l.coords[J]=="number"&&(Q[J]=l.coords[J]);this.fire("locationfound",Q)}},addHandler:function(l,f){if(!f)return this;var g=this[l]=new f(this);return this._handlers.push(g),this.options[l]&&g.enable(),this},remove:function(){if(this._initEvents(!0),this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch{this._container._leaflet_id=void 0,this._containerId=void 0}this._locationWatchId!==void 0&&this.stopLocate(),this._stop(),xt(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(D(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload");var l;for(l in this._layers)this._layers[l].remove();for(l in this._panes)xt(this._panes[l]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(l,f){var g="leaflet-pane"+(l?" leaflet-"+l.replace("Pane","")+"-pane":""),S=qe("div",g,f||this._mapPane);return l&&(this._panes[l]=S),S},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter.clone():this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var l=this.getPixelBounds(),f=this.unproject(l.getBottomLeft()),g=this.unproject(l.getTopRight());return new ne(f,g)},getMinZoom:function(){return this.options.minZoom===void 0?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return this.options.maxZoom===void 0?this._layersMaxZoom===void 0?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(l,f,g){l=ae(l),g=W(g||[0,0]);var S=this.getZoom()||0,k=this.getMinZoom(),M=this.getMaxZoom(),V=l.getNorthWest(),Q=l.getSouthEast(),J=this.getSize().subtract(g),oe=Y(this.project(Q,S),this.project(V,S)).getSize(),he=be.any3d?this.options.zoomSnap:1,Ee=J.x/oe.x,Fe=J.y/oe.y,Pn=f?Math.max(Ee,Fe):Math.min(Ee,Fe);return S=this.getScaleZoom(Pn,S),he&&(S=Math.round(S/(he/100))*(he/100),S=f?Math.ceil(S/he)*he:Math.floor(S/he)*he),Math.max(k,Math.min(M,S))},getSize:function(){return(!this._size||this._sizeChanged)&&(this._size=new R(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(l,f){var g=this._getTopLeftPoint(l,f);return new U(g,g.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(l){return this.options.crs.getProjectedBounds(l===void 0?this.getZoom():l)},getPane:function(l){return typeof l=="string"?this._panes[l]:l},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(l,f){var g=this.options.crs;return f=f===void 0?this._zoom:f,g.scale(l)/g.scale(f)},getScaleZoom:function(l,f){var g=this.options.crs;f=f===void 0?this._zoom:f;var S=g.zoom(l*g.scale(f));return isNaN(S)?1/0:S},project:function(l,f){return f=f===void 0?this._zoom:f,this.options.crs.latLngToPoint(ce(l),f)},unproject:function(l,f){return f=f===void 0?this._zoom:f,this.options.crs.pointToLatLng(W(l),f)},layerPointToLatLng:function(l){var f=W(l).add(this.getPixelOrigin());return this.unproject(f)},latLngToLayerPoint:function(l){var f=this.project(ce(l))._round();return f._subtract(this.getPixelOrigin())},wrapLatLng:function(l){return this.options.crs.wrapLatLng(ce(l))},wrapLatLngBounds:function(l){return this.options.crs.wrapLatLngBounds(ae(l))},distance:function(l,f){return this.options.crs.distance(ce(l),ce(f))},containerPointToLayerPoint:function(l){return W(l).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(l){return W(l).add(this._getMapPanePos())},containerPointToLatLng:function(l){var f=this.containerPointToLayerPoint(W(l));return this.layerPointToLatLng(f)},latLngToContainerPoint:function(l){return this.layerPointToContainerPoint(this.latLngToLayerPoint(ce(l)))},mouseEventToContainerPoint:function(l){return eN(l,this._container)},mouseEventToLayerPoint:function(l){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(l))},mouseEventToLatLng:function(l){return this.layerPointToLatLng(this.mouseEventToLayerPoint(l))},_initContainer:function(l){var f=this._container=Yj(l);if(f){if(f._leaflet_id)throw new Error("Map container is already initialized.")}else throw new Error("Map container not found.");Te(f,"scroll",this._onScroll,this),this._containerId=u(f)},_initLayout:function(){var l=this._container;this._fadeAnimated=this.options.fadeAnimation&&be.any3d,Le(l,"leaflet-container"+(be.touch?" leaflet-touch":"")+(be.retina?" leaflet-retina":"")+(be.ielt9?" leaflet-oldie":"")+(be.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":""));var f=uu(l,"position");f!=="absolute"&&f!=="relative"&&f!=="fixed"&&f!=="sticky"&&(l.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var l=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),It(this._mapPane,new R(0,0)),this.createPane("tilePane"),this.createPane("overlayPane"),this.createPane("shadowPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(Le(l.markerPane,"leaflet-zoom-hide"),Le(l.shadowPane,"leaflet-zoom-hide"))},_resetView:function(l,f,g){It(this._mapPane,new R(0,0));var S=!this._loaded;this._loaded=!0,f=this._limitZoom(f),this.fire("viewprereset");var k=this._zoom!==f;this._moveStart(k,g)._move(l,f)._moveEnd(k),this.fire("viewreset"),S&&this.fire("load")},_moveStart:function(l,f){return l&&this.fire("zoomstart"),f||this.fire("movestart"),this},_move:function(l,f,g,S){f===void 0&&(f=this._zoom);var k=this._zoom!==f;return this._zoom=f,this._lastCenter=l,this._pixelOrigin=this._getNewPixelOrigin(l),S?g&&g.pinch&&this.fire("zoom",g):((k||g&&g.pinch)&&this.fire("zoom",g),this.fire("move",g)),this},_moveEnd:function(l){return l&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return D(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(l){It(this._mapPane,this._getMapPanePos().subtract(l))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(l){this._targets={},this._targets[u(this._container)]=this;var f=l?at:Te;f(this._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup",this._handleDOMEvent,this),this.options.trackResize&&f(window,"resize",this._onResize,this),be.any3d&&this.options.transform3DLimit&&(l?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){D(this._resizeRequest),this._resizeRequest=z(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var l=this._getMapPanePos();Math.max(Math.abs(l.x),Math.abs(l.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(l,f){for(var g=[],S,k=f==="mouseout"||f==="mouseover",M=l.target||l.srcElement,V=!1;M;){if(S=this._targets[u(M)],S&&(f==="click"||f==="preclick")&&this._draggableMoved(S)){V=!0;break}if(S&&S.listens(f,!0)&&(k&&!Pv(M,l)||(g.push(S),k))||M===this._container)break;M=M.parentNode}return!g.length&&!V&&!k&&this.listens(f,!0)&&(g=[this]),g},_isClickDisabled:function(l){for(;l&&l!==this._container;){if(l._leaflet_disable_click)return!0;l=l.parentNode}},_handleDOMEvent:function(l){var f=l.target||l.srcElement;if(!(!this._loaded||f._leaflet_disable_events||l.type==="click"&&this._isClickDisabled(f))){var g=l.type;g==="mousedown"&&bv(f),this._fireDOMEvent(l,g)}},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(l,f,g){if(l.type==="click"){var S=i({},l);S.type="preclick",this._fireDOMEvent(S,S.type,g)}var k=this._findEventTargets(l,f);if(g){for(var M=[],V=0;V0?Math.round(l-f)/2:Math.max(0,Math.ceil(l))-Math.max(0,Math.floor(f))},_limitZoom:function(l){var f=this.getMinZoom(),g=this.getMaxZoom(),S=be.any3d?this.options.zoomSnap:1;return S&&(l=Math.round(l/S)*S),Math.max(f,Math.min(g,l))},_onPanTransitionStep:function(){this.fire("move")},_onPanTransitionEnd:function(){Mt(this._mapPane,"leaflet-pan-anim"),this.fire("moveend")},_tryAnimatedPan:function(l,f){var g=this._getCenterOffset(l)._trunc();return(f&&f.animate)!==!0&&!this.getSize().contains(g)?!1:(this.panBy(g,f),!0)},_createAnimProxy:function(){var l=this._proxy=qe("div","leaflet-proxy leaflet-zoom-animated");this._panes.mapPane.appendChild(l),this.on("zoomanim",function(f){var g=hv,S=this._proxy.style[g];Io(this._proxy,this.project(f.center,f.zoom),this.getZoomScale(f.zoom,1)),S===this._proxy.style[g]&&this._animatingZoom&&this._onZoomTransitionEnd()},this),this.on("load moveend",this._animMoveEnd,this),this._on("unload",this._destroyAnimProxy,this)},_destroyAnimProxy:function(){xt(this._proxy),this.off("load moveend",this._animMoveEnd,this),delete this._proxy},_animMoveEnd:function(){var l=this.getCenter(),f=this.getZoom();Io(this._proxy,this.project(l,f),this.getZoomScale(f,1))},_catchTransitionEnd:function(l){this._animatingZoom&&l.propertyName.indexOf("transform")>=0&&this._onZoomTransitionEnd()},_nothingToAnimate:function(){return!this._container.getElementsByClassName("leaflet-zoom-animated").length},_tryAnimatedZoom:function(l,f,g){if(this._animatingZoom)return!0;if(g=g||{},!this._zoomAnimated||g.animate===!1||this._nothingToAnimate()||Math.abs(f-this._zoom)>this.options.zoomAnimationThreshold)return!1;var S=this.getZoomScale(f),k=this._getCenterOffset(l)._divideBy(1-1/S);return g.animate!==!0&&!this.getSize().contains(k)?!1:(z(function(){this._moveStart(!0,g.noMoveStart||!1)._animateZoom(l,f,!0)},this),!0)},_animateZoom:function(l,f,g,S){this._mapPane&&(g&&(this._animatingZoom=!0,this._animateToCenter=l,this._animateToZoom=f,Le(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:l,zoom:f,noUpdate:S}),this._tempFireZoomEvent||(this._tempFireZoomEvent=this._zoom!==this._animateToZoom),this._move(this._animateToCenter,this._animateToZoom,void 0,!0),setTimeout(s(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&Mt(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom,void 0,!0),this._tempFireZoomEvent&&this.fire("zoom"),delete this._tempFireZoomEvent,this.fire("move"),this._moveEnd(!0))}});function fI(l,f){return new We(l,f)}var Kr=I.extend({options:{position:"topright"},initialize:function(l){x(this,l)},getPosition:function(){return this.options.position},setPosition:function(l){var f=this._map;return f&&f.removeControl(this),this.options.position=l,f&&f.addControl(this),this},getContainer:function(){return this._container},addTo:function(l){this.remove(),this._map=l;var f=this._container=this.onAdd(l),g=this.getPosition(),S=l._controlCorners[g];return Le(f,"leaflet-control"),g.indexOf("bottom")!==-1?S.insertBefore(f,S.firstChild):S.appendChild(f),this._map.on("unload",this.remove,this),this},remove:function(){return this._map?(xt(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null,this):this},_refocusOnMap:function(l){this._map&&l&&l.screenX>0&&l.screenY>0&&this._map.getContainer().focus()}}),pu=function(l){return new Kr(l)};We.include({addControl:function(l){return l.addTo(this),this},removeControl:function(l){return l.remove(),this},_initControlPos:function(){var l=this._controlCorners={},f="leaflet-",g=this._controlContainer=qe("div",f+"control-container",this._container);function S(k,M){var V=f+k+" "+f+M;l[k+M]=qe("div",V,g)}S("top","left"),S("top","right"),S("bottom","left"),S("bottom","right")},_clearControlPos:function(){for(var l in this._controlCorners)xt(this._controlCorners[l]);xt(this._controlContainer),delete this._controlCorners,delete this._controlContainer}});var rN=Kr.extend({options:{collapsed:!0,position:"topright",autoZIndex:!0,hideSingleBase:!1,sortLayers:!1,sortFunction:function(l,f,g,S){return g1,this._baseLayersList.style.display=l?"":"none"),this._separator.style.display=f&&l?"":"none",this},_onLayerChange:function(l){this._handlingClick||this._update();var f=this._getLayer(u(l.target)),g=f.overlay?l.type==="add"?"overlayadd":"overlayremove":l.type==="add"?"baselayerchange":null;g&&this._map.fire(g,f)},_createRadioElement:function(l,f){var g='",S=document.createElement("div");return S.innerHTML=g,S.firstChild},_addItem:function(l){var f=document.createElement("label"),g=this._map.hasLayer(l.layer),S;l.overlay?(S=document.createElement("input"),S.type="checkbox",S.className="leaflet-control-layers-selector",S.defaultChecked=g):S=this._createRadioElement("leaflet-base-layers_"+u(this),g),this._layerControlInputs.push(S),S.layerId=u(l.layer),Te(S,"click",this._onInputClick,this);var k=document.createElement("span");k.innerHTML=" "+l.name;var M=document.createElement("span");f.appendChild(M),M.appendChild(S),M.appendChild(k);var V=l.overlay?this._overlaysList:this._baseLayersList;return V.appendChild(f),this._checkDisabledLayers(),f},_onInputClick:function(){if(!this._preventClick){var l=this._layerControlInputs,f,g,S=[],k=[];this._handlingClick=!0;for(var M=l.length-1;M>=0;M--)f=l[M],g=this._getLayer(f.layerId).layer,f.checked?S.push(g):f.checked||k.push(g);for(M=0;M=0;k--)f=l[k],g=this._getLayer(f.layerId).layer,f.disabled=g.options.minZoom!==void 0&&Sg.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expandSafely:function(){var l=this._section;this._preventClick=!0,Te(l,"click",ln),this.expand();var f=this;setTimeout(function(){at(l,"click",ln),f._preventClick=!1})}}),hI=function(l,f,g){return new rN(l,f,g)},Ev=Kr.extend({options:{position:"topleft",zoomInText:'',zoomInTitle:"Zoom in",zoomOutText:'',zoomOutTitle:"Zoom out"},onAdd:function(l){var f="leaflet-control-zoom",g=qe("div",f+" leaflet-bar"),S=this.options;return this._zoomInButton=this._createButton(S.zoomInText,S.zoomInTitle,f+"-in",g,this._zoomIn),this._zoomOutButton=this._createButton(S.zoomOutText,S.zoomOutTitle,f+"-out",g,this._zoomOut),this._updateDisabled(),l.on("zoomend zoomlevelschange",this._updateDisabled,this),g},onRemove:function(l){l.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(l){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(l.shiftKey?3:1))},_createButton:function(l,f,g,S,k){var M=qe("a",g,S);return M.innerHTML=l,M.href="#",M.title=f,M.setAttribute("role","button"),M.setAttribute("aria-label",f),mu(M),Te(M,"click",Do),Te(M,"click",k,this),Te(M,"click",this._refocusOnMap,this),M},_updateDisabled:function(){var l=this._map,f="leaflet-disabled";Mt(this._zoomInButton,f),Mt(this._zoomOutButton,f),this._zoomInButton.setAttribute("aria-disabled","false"),this._zoomOutButton.setAttribute("aria-disabled","false"),(this._disabled||l._zoom===l.getMinZoom())&&(Le(this._zoomOutButton,f),this._zoomOutButton.setAttribute("aria-disabled","true")),(this._disabled||l._zoom===l.getMaxZoom())&&(Le(this._zoomInButton,f),this._zoomInButton.setAttribute("aria-disabled","true"))}});We.mergeOptions({zoomControl:!0}),We.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new Ev,this.addControl(this.zoomControl))});var mI=function(l){return new Ev(l)},iN=Kr.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(l){var f="leaflet-control-scale",g=qe("div",f),S=this.options;return this._addScales(S,f+"-line",g),l.on(S.updateWhenIdle?"moveend":"move",this._update,this),l.whenReady(this._update,this),g},onRemove:function(l){l.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(l,f,g){l.metric&&(this._mScale=qe("div",f,g)),l.imperial&&(this._iScale=qe("div",f,g))},_update:function(){var l=this._map,f=l.getSize().y/2,g=l.distance(l.containerPointToLatLng([0,f]),l.containerPointToLatLng([this.options.maxWidth,f]));this._updateScales(g)},_updateScales:function(l){this.options.metric&&l&&this._updateMetric(l),this.options.imperial&&l&&this._updateImperial(l)},_updateMetric:function(l){var f=this._getRoundNum(l),g=f<1e3?f+" m":f/1e3+" km";this._updateScale(this._mScale,g,f/l)},_updateImperial:function(l){var f=l*3.2808399,g,S,k;f>5280?(g=f/5280,S=this._getRoundNum(g),this._updateScale(this._iScale,S+" mi",S/g)):(k=this._getRoundNum(f),this._updateScale(this._iScale,k+" ft",k/f))},_updateScale:function(l,f,g){l.style.width=Math.round(this.options.maxWidth*g)+"px",l.innerHTML=f},_getRoundNum:function(l){var f=Math.pow(10,(Math.floor(l)+"").length-1),g=l/f;return g=g>=10?10:g>=5?5:g>=3?3:g>=2?2:1,f*g}}),pI=function(l){return new iN(l)},gI='',Ov=Kr.extend({options:{position:"bottomright",prefix:''+(be.inlineSvg?gI+" ":"")+"Leaflet"},initialize:function(l){x(this,l),this._attributions={}},onAdd:function(l){l.attributionControl=this,this._container=qe("div","leaflet-control-attribution"),mu(this._container);for(var f in l._layers)l._layers[f].getAttribution&&this.addAttribution(l._layers[f].getAttribution());return this._update(),l.on("layeradd",this._addAttribution,this),this._container},onRemove:function(l){l.off("layeradd",this._addAttribution,this)},_addAttribution:function(l){l.layer.getAttribution&&(this.addAttribution(l.layer.getAttribution()),l.layer.once("remove",function(){this.removeAttribution(l.layer.getAttribution())},this))},setPrefix:function(l){return this.options.prefix=l,this._update(),this},addAttribution:function(l){return l?(this._attributions[l]||(this._attributions[l]=0),this._attributions[l]++,this._update(),this):this},removeAttribution:function(l){return l?(this._attributions[l]&&(this._attributions[l]--,this._update()),this):this},_update:function(){if(this._map){var l=[];for(var f in this._attributions)this._attributions[f]&&l.push(f);var g=[];this.options.prefix&&g.push(this.options.prefix),l.length&&g.push(l.join(", ")),this._container.innerHTML=g.join(' ')}}});We.mergeOptions({attributionControl:!0}),We.addInitHook(function(){this.options.attributionControl&&new Ov().addTo(this)});var vI=function(l){return new Ov(l)};Kr.Layers=rN,Kr.Zoom=Ev,Kr.Scale=iN,Kr.Attribution=Ov,pu.layers=hI,pu.zoom=mI,pu.scale=pI,pu.attribution=vI;var _i=I.extend({initialize:function(l){this._map=l},enable:function(){return this._enabled?this:(this._enabled=!0,this.addHooks(),this)},disable:function(){return this._enabled?(this._enabled=!1,this.removeHooks(),this):this},enabled:function(){return!!this._enabled}});_i.addTo=function(l,f){return l.addHandler(f,this),this};var yI={Events:B},aN=be.touch?"touchstart mousedown":"mousedown",Da=G.extend({options:{clickTolerance:3},initialize:function(l,f,g,S){x(this,S),this._element=l,this._dragStartTarget=f||l,this._preventOutline=g},enable:function(){this._enabled||(Te(this._dragStartTarget,aN,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Da._dragging===this&&this.finishDrag(!0),at(this._dragStartTarget,aN,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(l){if(this._enabled&&(this._moved=!1,!mv(this._element,"leaflet-zoom-anim"))){if(l.touches&&l.touches.length!==1){Da._dragging===this&&this.finishDrag();return}if(!(Da._dragging||l.shiftKey||l.which!==1&&l.button!==1&&!l.touches)&&(Da._dragging=this,this._preventOutline&&bv(this._element),vv(),du(),!this._moving)){this.fire("down");var f=l.touches?l.touches[0]:l,g=Xj(this._element);this._startPoint=new R(f.clientX,f.clientY),this._startPos=Ro(this._element),this._parentScale=wv(g);var S=l.type==="mousedown";Te(document,S?"mousemove":"touchmove",this._onMove,this),Te(document,S?"mouseup":"touchend touchcancel",this._onUp,this)}}},_onMove:function(l){if(this._enabled){if(l.touches&&l.touches.length>1){this._moved=!0;return}var f=l.touches&&l.touches.length===1?l.touches[0]:l,g=new R(f.clientX,f.clientY)._subtract(this._startPoint);!g.x&&!g.y||Math.abs(g.x)+Math.abs(g.y)M&&(V=Q,M=J);M>g&&(f[V]=1,Cv(l,f,g,S,V),Cv(l,f,g,V,k))}function _I(l,f){for(var g=[l[0]],S=1,k=0,M=l.length;Sf&&(g.push(l[S]),k=S);return kf.max.x&&(g|=2),l.yf.max.y&&(g|=8),g}function jI(l,f){var g=f.x-l.x,S=f.y-l.y;return g*g+S*S}function gu(l,f,g,S){var k=f.x,M=f.y,V=g.x-k,Q=g.y-M,J=V*V+Q*Q,oe;return J>0&&(oe=((l.x-k)*V+(l.y-M)*Q)/J,oe>1?(k=g.x,M=g.y):oe>0&&(k+=V*oe,M+=Q*oe)),V=l.x-k,Q=l.y-M,S?V*V+Q*Q:new R(k,M)}function _r(l){return!j(l[0])||typeof l[0][0]!="object"&&typeof l[0][0]<"u"}function fN(l){return console.warn("Deprecated use of _flat, please use L.LineUtil.isFlat instead."),_r(l)}function hN(l,f){var g,S,k,M,V,Q,J,oe;if(!l||l.length===0)throw new Error("latlngs not passed");_r(l)||(console.warn("latlngs are not flat! Only the first ring will be used"),l=l[0]);var he=ce([0,0]),Ee=ae(l),Fe=Ee.getNorthWest().distanceTo(Ee.getSouthWest())*Ee.getNorthEast().distanceTo(Ee.getNorthWest());Fe<1700&&(he=kv(l));var Pn=l.length,Yt=[];for(g=0;gS){J=(M-S)/k,oe=[Q.x-J*(Q.x-V.x),Q.y-J*(Q.y-V.y)];break}var Hn=f.unproject(W(oe));return ce([Hn.lat+he.lat,Hn.lng+he.lng])}var NI={__proto__:null,simplify:lN,pointToSegmentDistance:cN,closestPointOnSegment:bI,clipSegment:dN,_getEdgeIntersection:mh,_getBitCode:Bo,_sqClosestPointOnSegment:gu,isFlat:_r,_flat:fN,polylineCenter:hN},Av={project:function(l){return new R(l.lng,l.lat)},unproject:function(l){return new ee(l.y,l.x)},bounds:new U([-180,-90],[180,90])},Tv={R:6378137,R_MINOR:6356752314245179e-9,bounds:new U([-2003750834279e-5,-1549657073972e-5],[2003750834279e-5,1876465623138e-5]),project:function(l){var f=Math.PI/180,g=this.R,S=l.lat*f,k=this.R_MINOR/g,M=Math.sqrt(1-k*k),V=M*Math.sin(S),Q=Math.tan(Math.PI/4-S/2)/Math.pow((1-V)/(1+V),M/2);return S=-g*Math.log(Math.max(Q,1e-10)),new R(l.lng*f*g,S)},unproject:function(l){for(var f=180/Math.PI,g=this.R,S=this.R_MINOR/g,k=Math.sqrt(1-S*S),M=Math.exp(-l.y/g),V=Math.PI/2-2*Math.atan(M),Q=0,J=.1,oe;Q<15&&Math.abs(J)>1e-7;Q++)oe=k*Math.sin(V),oe=Math.pow((1-oe)/(1+oe),k/2),J=Math.PI/2-2*Math.atan(M*oe)-V,V+=J;return new ee(V*f,l.x*f/g)}},SI={__proto__:null,LonLat:Av,Mercator:Tv,SphericalMercator:ye},PI=i({},Pe,{code:"EPSG:3395",projection:Tv,transformation:function(){var l=.5/(Math.PI*Tv.R);return ie(l,.5,-l,.5)}()}),mN=i({},Pe,{code:"EPSG:4326",projection:Av,transformation:ie(1/180,1,-1/180,.5)}),EI=i({},Ne,{projection:Av,transformation:ie(1,0,-1,0),scale:function(l){return Math.pow(2,l)},zoom:function(l){return Math.log(l)/Math.LN2},distance:function(l,f){var g=f.lng-l.lng,S=f.lat-l.lat;return Math.sqrt(g*g+S*S)},infinite:!0});Ne.Earth=Pe,Ne.EPSG3395=PI,Ne.EPSG3857=Ve,Ne.EPSG900913=Re,Ne.EPSG4326=mN,Ne.Simple=EI;var Yr=G.extend({options:{pane:"overlayPane",attribution:null,bubblingMouseEvents:!0},addTo:function(l){return l.addLayer(this),this},remove:function(){return this.removeFrom(this._map||this._mapToAdd)},removeFrom:function(l){return l&&l.removeLayer(this),this},getPane:function(l){return this._map.getPane(l?this.options[l]||l:this.options.pane)},addInteractiveTarget:function(l){return this._map._targets[u(l)]=this,this},removeInteractiveTarget:function(l){return delete this._map._targets[u(l)],this},getAttribution:function(){return this.options.attribution},_layerAdd:function(l){var f=l.target;if(f.hasLayer(this)){if(this._map=f,this._zoomAnimated=f._zoomAnimated,this.getEvents){var g=this.getEvents();f.on(g,this),this.once("remove",function(){f.off(g,this)},this)}this.onAdd(f),this.fire("add"),f.fire("layeradd",{layer:this})}}});We.include({addLayer:function(l){if(!l._layerAdd)throw new Error("The provided object is not a Layer.");var f=u(l);return this._layers[f]?this:(this._layers[f]=l,l._mapToAdd=this,l.beforeAdd&&l.beforeAdd(this),this.whenReady(l._layerAdd,l),this)},removeLayer:function(l){var f=u(l);return this._layers[f]?(this._loaded&&l.onRemove(this),delete this._layers[f],this._loaded&&(this.fire("layerremove",{layer:l}),l.fire("remove")),l._map=l._mapToAdd=null,this):this},hasLayer:function(l){return u(l)in this._layers},eachLayer:function(l,f){for(var g in this._layers)l.call(f,this._layers[g]);return this},_addLayers:function(l){l=l?j(l)?l:[l]:[];for(var f=0,g=l.length;fthis._layersMaxZoom&&this.setZoom(this._layersMaxZoom),this.options.minZoom===void 0&&this._layersMinZoom&&this.getZoom()=2&&f[0]instanceof ee&&f[0].equals(f[g-1])&&f.pop(),f},_setLatLngs:function(l){Ki.prototype._setLatLngs.call(this,l),_r(this._latlngs)&&(this._latlngs=[this._latlngs])},_defaultShape:function(){return _r(this._latlngs[0])?this._latlngs[0]:this._latlngs[0][0]},_clipPoints:function(){var l=this._renderer._bounds,f=this.options.weight,g=new R(f,f);if(l=new U(l.min.subtract(g),l.max.add(g)),this._parts=[],!(!this._pxBounds||!this._pxBounds.intersects(l))){if(this.options.noClip){this._parts=this._rings;return}for(var S=0,k=this._rings.length,M;Sl.y!=k.y>l.y&&l.x<(k.x-S.x)*(l.y-S.y)/(k.y-S.y)+S.x&&(f=!f);return f||Ki.prototype._containsPoint.call(this,l,!0)}});function $I(l,f){return new tl(l,f)}var Yi=Gi.extend({initialize:function(l,f){x(this,f),this._layers={},l&&this.addData(l)},addData:function(l){var f=j(l)?l:l.features,g,S,k;if(f){for(g=0,S=f.length;g0&&k.push(k[0].slice()),k}function nl(l,f){return l.feature?i({},l.feature,{geometry:f}):bh(f)}function bh(l){return l.type==="Feature"||l.type==="FeatureCollection"?l:{type:"Feature",properties:{},geometry:l}}var Iv={toGeoJSON:function(l){return nl(this,{type:"Point",coordinates:$v(this.getLatLng(),l)})}};ph.include(Iv),Mv.include(Iv),gh.include(Iv),Ki.include({toGeoJSON:function(l){var f=!_r(this._latlngs),g=xh(this._latlngs,f?1:0,!1,l);return nl(this,{type:(f?"Multi":"")+"LineString",coordinates:g})}}),tl.include({toGeoJSON:function(l){var f=!_r(this._latlngs),g=f&&!_r(this._latlngs[0]),S=xh(this._latlngs,g?2:f?1:0,!0,l);return f||(S=[S]),nl(this,{type:(g?"Multi":"")+"Polygon",coordinates:S})}}),Js.include({toMultiPoint:function(l){var f=[];return this.eachLayer(function(g){f.push(g.toGeoJSON(l).geometry.coordinates)}),nl(this,{type:"MultiPoint",coordinates:f})},toGeoJSON:function(l){var f=this.feature&&this.feature.geometry&&this.feature.geometry.type;if(f==="MultiPoint")return this.toMultiPoint(l);var g=f==="GeometryCollection",S=[];return this.eachLayer(function(k){if(k.toGeoJSON){var M=k.toGeoJSON(l);if(g)S.push(M.geometry);else{var V=bh(M);V.type==="FeatureCollection"?S.push.apply(S,V.features):S.push(V)}}}),g?nl(this,{geometries:S,type:"GeometryCollection"}):{type:"FeatureCollection",features:S}}});function vN(l,f){return new Yi(l,f)}var II=vN,wh=Yr.extend({options:{opacity:1,alt:"",interactive:!1,crossOrigin:!1,errorOverlayUrl:"",zIndex:1,className:""},initialize:function(l,f,g){this._url=l,this._bounds=ae(f),x(this,g)},onAdd:function(){this._image||(this._initImage(),this.options.opacity<1&&this._updateOpacity()),this.options.interactive&&(Le(this._image,"leaflet-interactive"),this.addInteractiveTarget(this._image)),this.getPane().appendChild(this._image),this._reset()},onRemove:function(){xt(this._image),this.options.interactive&&this.removeInteractiveTarget(this._image)},setOpacity:function(l){return this.options.opacity=l,this._image&&this._updateOpacity(),this},setStyle:function(l){return l.opacity&&this.setOpacity(l.opacity),this},bringToFront:function(){return this._map&&Xs(this._image),this},bringToBack:function(){return this._map&&Qs(this._image),this},setUrl:function(l){return this._url=l,this._image&&(this._image.src=l),this},setBounds:function(l){return this._bounds=ae(l),this._map&&this._reset(),this},getEvents:function(){var l={zoom:this._reset,viewreset:this._reset};return this._zoomAnimated&&(l.zoomanim=this._animateZoom),l},setZIndex:function(l){return this.options.zIndex=l,this._updateZIndex(),this},getBounds:function(){return this._bounds},getElement:function(){return this._image},_initImage:function(){var l=this._url.tagName==="IMG",f=this._image=l?this._url:qe("img");if(Le(f,"leaflet-image-layer"),this._zoomAnimated&&Le(f,"leaflet-zoom-animated"),this.options.className&&Le(f,this.options.className),f.onselectstart=m,f.onmousemove=m,f.onload=s(this.fire,this,"load"),f.onerror=s(this._overlayOnError,this,"error"),(this.options.crossOrigin||this.options.crossOrigin==="")&&(f.crossOrigin=this.options.crossOrigin===!0?"":this.options.crossOrigin),this.options.zIndex&&this._updateZIndex(),l){this._url=f.src;return}f.src=this._url,f.alt=this.options.alt},_animateZoom:function(l){var f=this._map.getZoomScale(l.zoom),g=this._map._latLngBoundsToNewLayerBounds(this._bounds,l.zoom,l.center).min;Io(this._image,g,f)},_reset:function(){var l=this._image,f=new U(this._map.latLngToLayerPoint(this._bounds.getNorthWest()),this._map.latLngToLayerPoint(this._bounds.getSouthEast())),g=f.getSize();It(l,f.min),l.style.width=g.x+"px",l.style.height=g.y+"px"},_updateOpacity:function(){wr(this._image,this.options.opacity)},_updateZIndex:function(){this._image&&this.options.zIndex!==void 0&&this.options.zIndex!==null&&(this._image.style.zIndex=this.options.zIndex)},_overlayOnError:function(){this.fire("error");var l=this.options.errorOverlayUrl;l&&this._url!==l&&(this._url=l,this._image.src=l)},getCenter:function(){return this._bounds.getCenter()}}),RI=function(l,f,g){return new wh(l,f,g)},yN=wh.extend({options:{autoplay:!0,loop:!0,keepAspectRatio:!0,muted:!1,playsInline:!0},_initImage:function(){var l=this._url.tagName==="VIDEO",f=this._image=l?this._url:qe("video");if(Le(f,"leaflet-image-layer"),this._zoomAnimated&&Le(f,"leaflet-zoom-animated"),this.options.className&&Le(f,this.options.className),f.onselectstart=m,f.onmousemove=m,f.onloadeddata=s(this.fire,this,"load"),l){for(var g=f.getElementsByTagName("source"),S=[],k=0;k0?S:[f.src];return}j(this._url)||(this._url=[this._url]),!this.options.keepAspectRatio&&Object.prototype.hasOwnProperty.call(f.style,"objectFit")&&(f.style.objectFit="fill"),f.autoplay=!!this.options.autoplay,f.loop=!!this.options.loop,f.muted=!!this.options.muted,f.playsInline=!!this.options.playsInline;for(var M=0;Mk?(f.height=k+"px",Le(l,M)):Mt(l,M),this._containerWidth=this._container.offsetWidth},_animateZoom:function(l){var f=this._map._latLngToNewLayerPoint(this._latlng,l.zoom,l.center),g=this._getAnchor();It(this._container,f.add(g))},_adjustPan:function(){if(this.options.autoPan){if(this._map._panAnim&&this._map._panAnim.stop(),this._autopanning){this._autopanning=!1;return}var l=this._map,f=parseInt(uu(this._container,"marginBottom"),10)||0,g=this._container.offsetHeight+f,S=this._containerWidth,k=new R(this._containerLeft,-g-this._containerBottom);k._add(Ro(this._container));var M=l.layerPointToContainerPoint(k),V=W(this.options.autoPanPadding),Q=W(this.options.autoPanPaddingTopLeft||V),J=W(this.options.autoPanPaddingBottomRight||V),oe=l.getSize(),he=0,Ee=0;M.x+S+J.x>oe.x&&(he=M.x+S-oe.x+J.x),M.x-he-Q.x<0&&(he=M.x-Q.x),M.y+g+J.y>oe.y&&(Ee=M.y+g-oe.y+J.y),M.y-Ee-Q.y<0&&(Ee=M.y-Q.y),(he||Ee)&&(this.options.keepInView&&(this._autopanning=!0),l.fire("autopanstart").panBy([he,Ee]))}},_getAnchor:function(){return W(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}}),BI=function(l,f){return new _h(l,f)};We.mergeOptions({closePopupOnClick:!0}),We.include({openPopup:function(l,f,g){return this._initOverlay(_h,l,f,g).openOn(this),this},closePopup:function(l){return l=arguments.length?l:this._popup,l&&l.close(),this}}),Yr.include({bindPopup:function(l,f){return this._popup=this._initOverlay(_h,this._popup,l,f),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(l){return this._popup&&(this instanceof Gi||(this._popup._source=this),this._popup._prepareOpen(l||this._latlng)&&this._popup.openOn(this._map)),this},closePopup:function(){return this._popup&&this._popup.close(),this},togglePopup:function(){return this._popup&&this._popup.toggle(this),this},isPopupOpen:function(){return this._popup?this._popup.isOpen():!1},setPopupContent:function(l){return this._popup&&this._popup.setContent(l),this},getPopup:function(){return this._popup},_openPopup:function(l){if(!(!this._popup||!this._map)){Do(l);var f=l.layer||l.target;if(this._popup._source===f&&!(f instanceof Ba)){this._map.hasLayer(this._popup)?this.closePopup():this.openPopup(l.latlng);return}this._popup._source=f,this.openPopup(l.latlng)}},_movePopup:function(l){this._popup.setLatLng(l.latlng)},_onKeyPress:function(l){l.originalEvent.keyCode===13&&this._openPopup(l)}});var jh=ji.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,opacity:.9},onAdd:function(l){ji.prototype.onAdd.call(this,l),this.setOpacity(this.options.opacity),l.fire("tooltipopen",{tooltip:this}),this._source&&(this.addEventParent(this._source),this._source.fire("tooltipopen",{tooltip:this},!0))},onRemove:function(l){ji.prototype.onRemove.call(this,l),l.fire("tooltipclose",{tooltip:this}),this._source&&(this.removeEventParent(this._source),this._source.fire("tooltipclose",{tooltip:this},!0))},getEvents:function(){var l=ji.prototype.getEvents.call(this);return this.options.permanent||(l.preclick=this.close),l},_initLayout:function(){var l="leaflet-tooltip",f=l+" "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=qe("div",f),this._container.setAttribute("role","tooltip"),this._container.setAttribute("id","leaflet-tooltip-"+u(this))},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(l){var f,g,S=this._map,k=this._container,M=S.latLngToContainerPoint(S.getCenter()),V=S.layerPointToContainerPoint(l),Q=this.options.direction,J=k.offsetWidth,oe=k.offsetHeight,he=W(this.options.offset),Ee=this._getAnchor();Q==="top"?(f=J/2,g=oe):Q==="bottom"?(f=J/2,g=0):Q==="center"?(f=J/2,g=oe/2):Q==="right"?(f=0,g=oe/2):Q==="left"?(f=J,g=oe/2):V.xthis.options.maxZoom||gS?this._retainParent(k,M,V,S):!1)},_retainChildren:function(l,f,g,S){for(var k=2*l;k<2*l+2;k++)for(var M=2*f;M<2*f+2;M++){var V=new R(k,M);V.z=g+1;var Q=this._tileCoordsToKey(V),J=this._tiles[Q];if(J&&J.active){J.retain=!0;continue}else J&&J.loaded&&(J.retain=!0);g+1this.options.maxZoom||this.options.minZoom!==void 0&&k1){this._setView(l,g);return}for(var Ee=k.min.y;Ee<=k.max.y;Ee++)for(var Fe=k.min.x;Fe<=k.max.x;Fe++){var Pn=new R(Fe,Ee);if(Pn.z=this._tileZoom,!!this._isValidTile(Pn)){var Yt=this._tiles[this._tileCoordsToKey(Pn)];Yt?Yt.current=!0:V.push(Pn)}}if(V.sort(function(Hn,il){return Hn.distanceTo(M)-il.distanceTo(M)}),V.length!==0){this._loading||(this._loading=!0,this.fire("loading"));var jr=document.createDocumentFragment();for(Fe=0;Feg.max.x)||!f.wrapLat&&(l.yg.max.y))return!1}if(!this.options.bounds)return!0;var S=this._tileCoordsToBounds(l);return ae(this.options.bounds).overlaps(S)},_keyToBounds:function(l){return this._tileCoordsToBounds(this._keyToTileCoords(l))},_tileCoordsToNwSe:function(l){var f=this._map,g=this.getTileSize(),S=l.scaleBy(g),k=S.add(g),M=f.unproject(S,l.z),V=f.unproject(k,l.z);return[M,V]},_tileCoordsToBounds:function(l){var f=this._tileCoordsToNwSe(l),g=new ne(f[0],f[1]);return this.options.noWrap||(g=this._map.wrapLatLngBounds(g)),g},_tileCoordsToKey:function(l){return l.x+":"+l.y+":"+l.z},_keyToTileCoords:function(l){var f=l.split(":"),g=new R(+f[0],+f[1]);return g.z=+f[2],g},_removeTile:function(l){var f=this._tiles[l];f&&(xt(f.el),delete this._tiles[l],this.fire("tileunload",{tile:f.el,coords:this._keyToTileCoords(l)}))},_initTile:function(l){Le(l,"leaflet-tile");var f=this.getTileSize();l.style.width=f.x+"px",l.style.height=f.y+"px",l.onselectstart=m,l.onmousemove=m,be.ielt9&&this.options.opacity<1&&wr(l,this.options.opacity)},_addTile:function(l,f){var g=this._getTilePos(l),S=this._tileCoordsToKey(l),k=this.createTile(this._wrapCoords(l),s(this._tileReady,this,l));this._initTile(k),this.createTile.length<2&&z(s(this._tileReady,this,l,null,k)),It(k,g),this._tiles[S]={el:k,coords:l,current:!0},f.appendChild(k),this.fire("tileloadstart",{tile:k,coords:l})},_tileReady:function(l,f,g){f&&this.fire("tileerror",{error:f,tile:g,coords:l});var S=this._tileCoordsToKey(l);g=this._tiles[S],g&&(g.loaded=+new Date,this._map._fadeAnimated?(wr(g.el,0),D(this._fadeFrame),this._fadeFrame=z(this._updateOpacity,this)):(g.active=!0,this._pruneTiles()),f||(Le(g.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:g.el,coords:l})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),be.ielt9||!this._map._fadeAnimated?z(this._pruneTiles,this):setTimeout(s(this._pruneTiles,this),250)))},_getTilePos:function(l){return l.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(l){var f=new R(this._wrapX?h(l.x,this._wrapX):l.x,this._wrapY?h(l.y,this._wrapY):l.y);return f.z=l.z,f},_pxBoundsToTileRange:function(l){var f=this.getTileSize();return new U(l.min.unscaleBy(f).floor(),l.max.unscaleBy(f).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var l in this._tiles)if(!this._tiles[l].loaded)return!1;return!0}});function WI(l){return new yu(l)}var rl=yu.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1,referrerPolicy:!1},initialize:function(l,f){this._url=l,f=x(this,f),f.detectRetina&&be.retina&&f.maxZoom>0?(f.tileSize=Math.floor(f.tileSize/2),f.zoomReverse?(f.zoomOffset--,f.minZoom=Math.min(f.maxZoom,f.minZoom+1)):(f.zoomOffset++,f.maxZoom=Math.max(f.minZoom,f.maxZoom-1)),f.minZoom=Math.max(0,f.minZoom)):f.zoomReverse?f.minZoom=Math.min(f.maxZoom,f.minZoom):f.maxZoom=Math.max(f.minZoom,f.maxZoom),typeof f.subdomains=="string"&&(f.subdomains=f.subdomains.split("")),this.on("tileunload",this._onTileRemove)},setUrl:function(l,f){return this._url===l&&f===void 0&&(f=!0),this._url=l,f||this.redraw(),this},createTile:function(l,f){var g=document.createElement("img");return Te(g,"load",s(this._tileOnLoad,this,f,g)),Te(g,"error",s(this._tileOnError,this,f,g)),(this.options.crossOrigin||this.options.crossOrigin==="")&&(g.crossOrigin=this.options.crossOrigin===!0?"":this.options.crossOrigin),typeof this.options.referrerPolicy=="string"&&(g.referrerPolicy=this.options.referrerPolicy),g.alt="",g.src=this.getTileUrl(l),g},getTileUrl:function(l){var f={r:be.retina?"@2x":"",s:this._getSubdomain(l),x:l.x,y:l.y,z:this._getZoomForUrl()};if(this._map&&!this._map.options.crs.infinite){var g=this._globalTileRange.max.y-l.y;this.options.tms&&(f.y=g),f["-y"]=g}return b(this._url,i(f,this.options))},_tileOnLoad:function(l,f){be.ielt9?setTimeout(s(l,this,null,f),0):l(null,f)},_tileOnError:function(l,f,g){var S=this.options.errorTileUrl;S&&f.getAttribute("src")!==S&&(f.src=S),l(g,f)},_onTileRemove:function(l){l.tile.onload=null},_getZoomForUrl:function(){var l=this._tileZoom,f=this.options.maxZoom,g=this.options.zoomReverse,S=this.options.zoomOffset;return g&&(l=f-l),l+S},_getSubdomain:function(l){var f=Math.abs(l.x+l.y)%this.options.subdomains.length;return this.options.subdomains[f]},_abortLoading:function(){var l,f;for(l in this._tiles)if(this._tiles[l].coords.z!==this._tileZoom&&(f=this._tiles[l].el,f.onload=m,f.onerror=m,!f.complete)){f.src=P;var g=this._tiles[l].coords;xt(f),delete this._tiles[l],this.fire("tileabort",{tile:f,coords:g})}},_removeTile:function(l){var f=this._tiles[l];if(f)return f.el.setAttribute("src",P),yu.prototype._removeTile.call(this,l)},_tileReady:function(l,f,g){if(!(!this._map||g&&g.getAttribute("src")===P))return yu.prototype._tileReady.call(this,l,f,g)}});function wN(l,f){return new rl(l,f)}var _N=rl.extend({defaultWmsParams:{service:"WMS",request:"GetMap",layers:"",styles:"",format:"image/jpeg",transparent:!1,version:"1.1.1"},options:{crs:null,uppercase:!1},initialize:function(l,f){this._url=l;var g=i({},this.defaultWmsParams);for(var S in f)S in this.options||(g[S]=f[S]);f=x(this,f);var k=f.detectRetina&&be.retina?2:1,M=this.getTileSize();g.width=M.x*k,g.height=M.y*k,this.wmsParams=g},onAdd:function(l){this._crs=this.options.crs||l.options.crs,this._wmsVersion=parseFloat(this.wmsParams.version);var f=this._wmsVersion>=1.3?"crs":"srs";this.wmsParams[f]=this._crs.code,rl.prototype.onAdd.call(this,l)},getTileUrl:function(l){var f=this._tileCoordsToNwSe(l),g=this._crs,S=Y(g.project(f[0]),g.project(f[1])),k=S.min,M=S.max,V=(this._wmsVersion>=1.3&&this._crs===mN?[k.y,k.x,M.y,M.x]:[k.x,k.y,M.x,M.y]).join(","),Q=rl.prototype.getTileUrl.call(this,l);return Q+y(this.wmsParams,Q,this.options.uppercase)+(this.options.uppercase?"&BBOX=":"&bbox=")+V},setParams:function(l,f){return i(this.wmsParams,l),f||this.redraw(),this}});function HI(l,f){return new _N(l,f)}rl.WMS=_N,wN.wms=HI;var Xi=Yr.extend({options:{padding:.1},initialize:function(l){x(this,l),u(this),this._layers=this._layers||{}},onAdd:function(){this._container||(this._initContainer(),Le(this._container,"leaflet-zoom-animated")),this.getPane().appendChild(this._container),this._update(),this.on("update",this._updatePaths,this)},onRemove:function(){this.off("update",this._updatePaths,this),this._destroyContainer()},getEvents:function(){var l={viewreset:this._reset,zoom:this._onZoom,moveend:this._update,zoomend:this._onZoomEnd};return this._zoomAnimated&&(l.zoomanim=this._onAnimZoom),l},_onAnimZoom:function(l){this._updateTransform(l.center,l.zoom)},_onZoom:function(){this._updateTransform(this._map.getCenter(),this._map.getZoom())},_updateTransform:function(l,f){var g=this._map.getZoomScale(f,this._zoom),S=this._map.getSize().multiplyBy(.5+this.options.padding),k=this._map.project(this._center,f),M=S.multiplyBy(-g).add(k).subtract(this._map._getNewPixelOrigin(l,f));be.any3d?Io(this._container,M,g):It(this._container,M)},_reset:function(){this._update(),this._updateTransform(this._center,this._zoom);for(var l in this._layers)this._layers[l]._reset()},_onZoomEnd:function(){for(var l in this._layers)this._layers[l]._project()},_updatePaths:function(){for(var l in this._layers)this._layers[l]._update()},_update:function(){var l=this.options.padding,f=this._map.getSize(),g=this._map.containerPointToLayerPoint(f.multiplyBy(-l)).round();this._bounds=new U(g,g.add(f.multiplyBy(1+l*2)).round()),this._center=this._map.getCenter(),this._zoom=this._map.getZoom()}}),jN=Xi.extend({options:{tolerance:0},getEvents:function(){var l=Xi.prototype.getEvents.call(this);return l.viewprereset=this._onViewPreReset,l},_onViewPreReset:function(){this._postponeUpdatePaths=!0},onAdd:function(){Xi.prototype.onAdd.call(this),this._draw()},_initContainer:function(){var l=this._container=document.createElement("canvas");Te(l,"mousemove",this._onMouseMove,this),Te(l,"click dblclick mousedown mouseup contextmenu",this._onClick,this),Te(l,"mouseout",this._handleMouseOut,this),l._leaflet_disable_events=!0,this._ctx=l.getContext("2d")},_destroyContainer:function(){D(this._redrawRequest),delete this._ctx,xt(this._container),at(this._container),delete this._container},_updatePaths:function(){if(!this._postponeUpdatePaths){var l;this._redrawBounds=null;for(var f in this._layers)l=this._layers[f],l._update();this._redraw()}},_update:function(){if(!(this._map._animatingZoom&&this._bounds)){Xi.prototype._update.call(this);var l=this._bounds,f=this._container,g=l.getSize(),S=be.retina?2:1;It(f,l.min),f.width=S*g.x,f.height=S*g.y,f.style.width=g.x+"px",f.style.height=g.y+"px",be.retina&&this._ctx.scale(2,2),this._ctx.translate(-l.min.x,-l.min.y),this.fire("update")}},_reset:function(){Xi.prototype._reset.call(this),this._postponeUpdatePaths&&(this._postponeUpdatePaths=!1,this._updatePaths())},_initPath:function(l){this._updateDashArray(l),this._layers[u(l)]=l;var f=l._order={layer:l,prev:this._drawLast,next:null};this._drawLast&&(this._drawLast.next=f),this._drawLast=f,this._drawFirst=this._drawFirst||this._drawLast},_addPath:function(l){this._requestRedraw(l)},_removePath:function(l){var f=l._order,g=f.next,S=f.prev;g?g.prev=S:this._drawLast=S,S?S.next=g:this._drawFirst=g,delete l._order,delete this._layers[u(l)],this._requestRedraw(l)},_updatePath:function(l){this._extendRedrawBounds(l),l._project(),l._update(),this._requestRedraw(l)},_updateStyle:function(l){this._updateDashArray(l),this._requestRedraw(l)},_updateDashArray:function(l){if(typeof l.options.dashArray=="string"){var f=l.options.dashArray.split(/[, ]+/),g=[],S,k;for(k=0;k')}}catch{}return function(l){return document.createElement("<"+l+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}(),VI={_initContainer:function(){this._container=qe("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(Xi.prototype._update.call(this),this.fire("update"))},_initPath:function(l){var f=l._container=xu("shape");Le(f,"leaflet-vml-shape "+(this.options.className||"")),f.coordsize="1 1",l._path=xu("path"),f.appendChild(l._path),this._updateStyle(l),this._layers[u(l)]=l},_addPath:function(l){var f=l._container;this._container.appendChild(f),l.options.interactive&&l.addInteractiveTarget(f)},_removePath:function(l){var f=l._container;xt(f),l.removeInteractiveTarget(f),delete this._layers[u(l)]},_updateStyle:function(l){var f=l._stroke,g=l._fill,S=l.options,k=l._container;k.stroked=!!S.stroke,k.filled=!!S.fill,S.stroke?(f||(f=l._stroke=xu("stroke")),k.appendChild(f),f.weight=S.weight+"px",f.color=S.color,f.opacity=S.opacity,S.dashArray?f.dashStyle=j(S.dashArray)?S.dashArray.join(" "):S.dashArray.replace(/( *, *)/g," "):f.dashStyle="",f.endcap=S.lineCap.replace("butt","flat"),f.joinstyle=S.lineJoin):f&&(k.removeChild(f),l._stroke=null),S.fill?(g||(g=l._fill=xu("fill")),k.appendChild(g),g.color=S.fillColor||S.color,g.opacity=S.fillOpacity):g&&(k.removeChild(g),l._fill=null)},_updateCircle:function(l){var f=l._point.round(),g=Math.round(l._radius),S=Math.round(l._radiusY||g);this._setPath(l,l._empty()?"M0 0":"AL "+f.x+","+f.y+" "+g+","+S+" 0,"+65535*360)},_setPath:function(l,f){l._path.v=f},_bringToFront:function(l){Xs(l._container)},_bringToBack:function(l){Qs(l._container)}},Nh=be.vml?xu:ut,bu=Xi.extend({_initContainer:function(){this._container=Nh("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=Nh("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){xt(this._container),at(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_update:function(){if(!(this._map._animatingZoom&&this._bounds)){Xi.prototype._update.call(this);var l=this._bounds,f=l.getSize(),g=this._container;(!this._svgSize||!this._svgSize.equals(f))&&(this._svgSize=f,g.setAttribute("width",f.x),g.setAttribute("height",f.y)),It(g,l.min),g.setAttribute("viewBox",[l.min.x,l.min.y,f.x,f.y].join(" ")),this.fire("update")}},_initPath:function(l){var f=l._path=Nh("path");l.options.className&&Le(f,l.options.className),l.options.interactive&&Le(f,"leaflet-interactive"),this._updateStyle(l),this._layers[u(l)]=l},_addPath:function(l){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(l._path),l.addInteractiveTarget(l._path)},_removePath:function(l){xt(l._path),l.removeInteractiveTarget(l._path),delete this._layers[u(l)]},_updatePath:function(l){l._project(),l._update()},_updateStyle:function(l){var f=l._path,g=l.options;f&&(g.stroke?(f.setAttribute("stroke",g.color),f.setAttribute("stroke-opacity",g.opacity),f.setAttribute("stroke-width",g.weight),f.setAttribute("stroke-linecap",g.lineCap),f.setAttribute("stroke-linejoin",g.lineJoin),g.dashArray?f.setAttribute("stroke-dasharray",g.dashArray):f.removeAttribute("stroke-dasharray"),g.dashOffset?f.setAttribute("stroke-dashoffset",g.dashOffset):f.removeAttribute("stroke-dashoffset")):f.setAttribute("stroke","none"),g.fill?(f.setAttribute("fill",g.fillColor||g.color),f.setAttribute("fill-opacity",g.fillOpacity),f.setAttribute("fill-rule",g.fillRule||"evenodd")):f.setAttribute("fill","none"))},_updatePoly:function(l,f){this._setPath(l,dt(l._parts,f))},_updateCircle:function(l){var f=l._point,g=Math.max(Math.round(l._radius),1),S=Math.max(Math.round(l._radiusY),1)||g,k="a"+g+","+S+" 0 1,0 ",M=l._empty()?"M0 0":"M"+(f.x-g)+","+f.y+k+g*2+",0 "+k+-g*2+",0 ";this._setPath(l,M)},_setPath:function(l,f){l._path.setAttribute("d",f)},_bringToFront:function(l){Xs(l._path)},_bringToBack:function(l){Qs(l._path)}});be.vml&&bu.include(VI);function SN(l){return be.svg||be.vml?new bu(l):null}We.include({getRenderer:function(l){var f=l.options.renderer||this._getPaneRenderer(l.options.pane)||this.options.renderer||this._renderer;return f||(f=this._renderer=this._createRenderer()),this.hasLayer(f)||this.addLayer(f),f},_getPaneRenderer:function(l){if(l==="overlayPane"||l===void 0)return!1;var f=this._paneRenderers[l];return f===void 0&&(f=this._createRenderer({pane:l}),this._paneRenderers[l]=f),f},_createRenderer:function(l){return this.options.preferCanvas&&NN(l)||SN(l)}});var PN=tl.extend({initialize:function(l,f){tl.prototype.initialize.call(this,this._boundsToLatLngs(l),f)},setBounds:function(l){return this.setLatLngs(this._boundsToLatLngs(l))},_boundsToLatLngs:function(l){return l=ae(l),[l.getSouthWest(),l.getNorthWest(),l.getNorthEast(),l.getSouthEast()]}});function qI(l,f){return new PN(l,f)}bu.create=Nh,bu.pointsToPath=dt,Yi.geometryToLayer=vh,Yi.coordsToLatLng=Lv,Yi.coordsToLatLngs=yh,Yi.latLngToCoords=$v,Yi.latLngsToCoords=xh,Yi.getFeature=nl,Yi.asFeature=bh,We.mergeOptions({boxZoom:!0});var EN=_i.extend({initialize:function(l){this._map=l,this._container=l._container,this._pane=l._panes.overlayPane,this._resetStateTimeout=0,l.on("unload",this._destroy,this)},addHooks:function(){Te(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){at(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){xt(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){this._resetStateTimeout!==0&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(l){if(!l.shiftKey||l.which!==1&&l.button!==1)return!1;this._clearDeferredResetState(),this._resetState(),du(),vv(),this._startPoint=this._map.mouseEventToContainerPoint(l),Te(document,{contextmenu:Do,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(l){this._moved||(this._moved=!0,this._box=qe("div","leaflet-zoom-box",this._container),Le(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(l);var f=new U(this._point,this._startPoint),g=f.getSize();It(this._box,f.min),this._box.style.width=g.x+"px",this._box.style.height=g.y+"px"},_finish:function(){this._moved&&(xt(this._box),Mt(this._container,"leaflet-crosshair")),fu(),yv(),at(document,{contextmenu:Do,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(l){if(!(l.which!==1&&l.button!==1)&&(this._finish(),!!this._moved)){this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(s(this._resetState,this),0);var f=new ne(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point));this._map.fitBounds(f).fire("boxzoomend",{boxZoomBounds:f})}},_onKeyDown:function(l){l.keyCode===27&&(this._finish(),this._clearDeferredResetState(),this._resetState())}});We.addInitHook("addHandler","boxZoom",EN),We.mergeOptions({doubleClickZoom:!0});var ON=_i.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(l){var f=this._map,g=f.getZoom(),S=f.options.zoomDelta,k=l.originalEvent.shiftKey?g-S:g+S;f.options.doubleClickZoom==="center"?f.setZoom(k):f.setZoomAround(l.containerPoint,k)}});We.addInitHook("addHandler","doubleClickZoom",ON),We.mergeOptions({dragging:!0,inertia:!0,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0});var kN=_i.extend({addHooks:function(){if(!this._draggable){var l=this._map;this._draggable=new Da(l._mapPane,l._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),l.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),l.on("zoomend",this._onZoomEnd,this),l.whenReady(this._onZoomEnd,this))}Le(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){Mt(this._map._container,"leaflet-grab"),Mt(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var l=this._map;if(l._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity){var f=ae(this._map.options.maxBounds);this._offsetLimit=Y(this._map.latLngToContainerPoint(f.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(f.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))}else this._offsetLimit=null;l.fire("movestart").fire("dragstart"),l.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(l){if(this._map.options.inertia){var f=this._lastTime=+new Date,g=this._lastPos=this._draggable._absPos||this._draggable._newPos;this._positions.push(g),this._times.push(f),this._prunePositions(f)}this._map.fire("move",l).fire("drag",l)},_prunePositions:function(l){for(;this._positions.length>1&&l-this._times[0]>50;)this._positions.shift(),this._times.shift()},_onZoomEnd:function(){var l=this._map.getSize().divideBy(2),f=this._map.latLngToLayerPoint([0,0]);this._initialWorldOffset=f.subtract(l).x,this._worldWidth=this._map.getPixelWorldBounds().getSize().x},_viscousLimit:function(l,f){return l-(l-f)*this._viscosity},_onPreDragLimit:function(){if(!(!this._viscosity||!this._offsetLimit)){var l=this._draggable._newPos.subtract(this._draggable._startPos),f=this._offsetLimit;l.xf.max.x&&(l.x=this._viscousLimit(l.x,f.max.x)),l.y>f.max.y&&(l.y=this._viscousLimit(l.y,f.max.y)),this._draggable._newPos=this._draggable._startPos.add(l)}},_onPreDragWrap:function(){var l=this._worldWidth,f=Math.round(l/2),g=this._initialWorldOffset,S=this._draggable._newPos.x,k=(S-f+g)%l+f-g,M=(S+f+g)%l-f-g,V=Math.abs(k+g)0?M:-M))-f;this._delta=0,this._startTime=null,V&&(l.options.scrollWheelZoom==="center"?l.setZoom(f+V):l.setZoomAround(this._lastMousePos,f+V))}});We.addInitHook("addHandler","scrollWheelZoom",AN);var ZI=600;We.mergeOptions({tapHold:be.touchNative&&be.safari&&be.mobile,tapTolerance:15});var TN=_i.extend({addHooks:function(){Te(this._map._container,"touchstart",this._onDown,this)},removeHooks:function(){at(this._map._container,"touchstart",this._onDown,this)},_onDown:function(l){if(clearTimeout(this._holdTimeout),l.touches.length===1){var f=l.touches[0];this._startPos=this._newPos=new R(f.clientX,f.clientY),this._holdTimeout=setTimeout(s(function(){this._cancel(),this._isTapValid()&&(Te(document,"touchend",ln),Te(document,"touchend touchcancel",this._cancelClickPrevent),this._simulateEvent("contextmenu",f))},this),ZI),Te(document,"touchend touchcancel contextmenu",this._cancel,this),Te(document,"touchmove",this._onMove,this)}},_cancelClickPrevent:function l(){at(document,"touchend",ln),at(document,"touchend touchcancel",l)},_cancel:function(){clearTimeout(this._holdTimeout),at(document,"touchend touchcancel contextmenu",this._cancel,this),at(document,"touchmove",this._onMove,this)},_onMove:function(l){var f=l.touches[0];this._newPos=new R(f.clientX,f.clientY)},_isTapValid:function(){return this._newPos.distanceTo(this._startPos)<=this._map.options.tapTolerance},_simulateEvent:function(l,f){var g=new MouseEvent(l,{bubbles:!0,cancelable:!0,view:window,screenX:f.screenX,screenY:f.screenY,clientX:f.clientX,clientY:f.clientY});g._simulated=!0,f.target.dispatchEvent(g)}});We.addInitHook("addHandler","tapHold",TN),We.mergeOptions({touchZoom:be.touch,bounceAtZoomLimits:!0});var MN=_i.extend({addHooks:function(){Le(this._map._container,"leaflet-touch-zoom"),Te(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){Mt(this._map._container,"leaflet-touch-zoom"),at(this._map._container,"touchstart",this._onTouchStart,this)},_onTouchStart:function(l){var f=this._map;if(!(!l.touches||l.touches.length!==2||f._animatingZoom||this._zooming)){var g=f.mouseEventToContainerPoint(l.touches[0]),S=f.mouseEventToContainerPoint(l.touches[1]);this._centerPoint=f.getSize()._divideBy(2),this._startLatLng=f.containerPointToLatLng(this._centerPoint),f.options.touchZoom!=="center"&&(this._pinchStartLatLng=f.containerPointToLatLng(g.add(S)._divideBy(2))),this._startDist=g.distanceTo(S),this._startZoom=f.getZoom(),this._moved=!1,this._zooming=!0,f._stop(),Te(document,"touchmove",this._onTouchMove,this),Te(document,"touchend touchcancel",this._onTouchEnd,this),ln(l)}},_onTouchMove:function(l){if(!(!l.touches||l.touches.length!==2||!this._zooming)){var f=this._map,g=f.mouseEventToContainerPoint(l.touches[0]),S=f.mouseEventToContainerPoint(l.touches[1]),k=g.distanceTo(S)/this._startDist;if(this._zoom=f.getScaleZoom(k,this._startZoom),!f.options.bounceAtZoomLimits&&(this._zoomf.getMaxZoom()&&k>1)&&(this._zoom=f._limitZoom(this._zoom)),f.options.touchZoom==="center"){if(this._center=this._startLatLng,k===1)return}else{var M=g._add(S)._divideBy(2)._subtract(this._centerPoint);if(k===1&&M.x===0&&M.y===0)return;this._center=f.unproject(f.project(this._pinchStartLatLng,this._zoom).subtract(M),this._zoom)}this._moved||(f._moveStart(!0,!1),this._moved=!0),D(this._animRequest);var V=s(f._move,f,this._center,this._zoom,{pinch:!0,round:!1},void 0);this._animRequest=z(V,this,!0),ln(l)}},_onTouchEnd:function(){if(!this._moved||!this._zooming){this._zooming=!1;return}this._zooming=!1,D(this._animRequest),at(document,"touchmove",this._onTouchMove,this),at(document,"touchend touchcancel",this._onTouchEnd,this),this._map.options.zoomAnimation?this._map._animateZoom(this._center,this._map._limitZoom(this._zoom),!0,this._map.options.zoomSnap):this._map._resetView(this._center,this._map._limitZoom(this._zoom))}});We.addInitHook("addHandler","touchZoom",MN),We.BoxZoom=EN,We.DoubleClickZoom=ON,We.Drag=kN,We.Keyboard=CN,We.ScrollWheelZoom=AN,We.TapHold=TN,We.TouchZoom=MN,n.Bounds=U,n.Browser=be,n.CRS=Ne,n.Canvas=jN,n.Circle=Mv,n.CircleMarker=gh,n.Class=I,n.Control=Kr,n.DivIcon=bN,n.DivOverlay=ji,n.DomEvent=dI,n.DomUtil=cI,n.Draggable=Da,n.Evented=G,n.FeatureGroup=Gi,n.GeoJSON=Yi,n.GridLayer=yu,n.Handler=_i,n.Icon=el,n.ImageOverlay=wh,n.LatLng=ee,n.LatLngBounds=ne,n.Layer=Yr,n.LayerGroup=Js,n.LineUtil=NI,n.Map=We,n.Marker=ph,n.Mixin=yI,n.Path=Ba,n.Point=R,n.PolyUtil=xI,n.Polygon=tl,n.Polyline=Ki,n.Popup=_h,n.PosAnimation=nN,n.Projection=SI,n.Rectangle=PN,n.Renderer=Xi,n.SVG=bu,n.SVGOverlay=xN,n.TileLayer=rl,n.Tooltip=jh,n.Transformation=je,n.Util=Z,n.VideoOverlay=yN,n.bind=s,n.bounds=Y,n.canvas=NN,n.circle=MI,n.circleMarker=TI,n.control=pu,n.divIcon=UI,n.extend=i,n.featureGroup=kI,n.geoJSON=vN,n.geoJson=II,n.gridLayer=WI,n.icon=CI,n.imageOverlay=RI,n.latLng=ce,n.latLngBounds=ae,n.layerGroup=OI,n.map=fI,n.marker=AI,n.point=W,n.polygon=$I,n.polyline=LI,n.popup=BI,n.rectangle=qI,n.setOptions=x,n.stamp=u,n.svg=SN,n.svgOverlay=DI,n.tileLayer=wN,n.tooltip=zI,n.transformation=ie,n.version=r,n.videoOverlay=FI;var GI=window.L;n.noConflict=function(){return window.L=GI,this},window.L=n})})(nw,nw.exports);var iv=nw.exports;function aj(e,t,n){return Object.freeze({instance:e,context:t,container:n})}function oj(e,t){return t==null?function(r,i){const a=N.useRef();return a.current||(a.current=e(r,i)),a}:function(r,i){const a=N.useRef();a.current||(a.current=e(r,i));const s=N.useRef(r),{instance:c}=a.current;return N.useEffect(function(){s.current!==r&&(t(c,r,s.current),s.current=r)},[c,r,i]),a}}function NL(e,t){N.useEffect(function(){return(t.layerContainer??t.map).addLayer(e.instance),function(){var a;(a=t.layerContainer)==null||a.removeLayer(e.instance),t.map.removeLayer(e.instance)}},[t,e])}function vge(e){return function(n){const r=rj(),i=e(rv(n,r),r);return wL(r.map,n.attribution),ij(i.current,n.eventHandlers),NL(i.current,r),i}}function yge(e,t){const n=N.useRef();N.useEffect(function(){if(t.pathOptions!==n.current){const i=t.pathOptions??{};e.instance.setStyle(i),n.current=i}},[e,t])}function xge(e){return function(n){const r=rj(),i=e(rv(n,r),r);return ij(i.current,n.eventHandlers),NL(i.current,r),yge(i.current,n),i}}function bge(e,t){const n=oj(e),r=gge(n,t);return mge(r)}function wge(e,t){const n=oj(e,t),r=xge(n);return hge(r)}function _ge(e,t){const n=oj(e,t),r=vge(n);return pge(r)}function jge(e,t,n){const{opacity:r,zIndex:i}=t;r!=null&&r!==n.opacity&&e.setOpacity(r),i!=null&&i!==n.zIndex&&e.setZIndex(i)}const Nge=wge(function({center:t,children:n,...r},i){const a=new iv.CircleMarker(t,r);return aj(a,fge(i,{overlayContainer:a}))},cge);function rw(){return rw=Object.assign||function(e){for(var t=1;t(v==null?void 0:v.map)??null,[v]);const x=N.useCallback(w=>{if(w!==null&&v===null){const b=new iv.Map(w,h);n!=null&&d!=null?b.setView(n,d):e!=null&&b.fitBounds(e,t),u!=null&&b.whenReady(u),_(dge(b))}},[]);N.useEffect(()=>()=>{v==null||v.map.remove()},[v]);const y=v?H.createElement(jL,{value:v},r):s??null;return H.createElement("div",rw({},p,{ref:x}),y)}const Pge=N.forwardRef(Sge),Ege=bge(function(t,n){const r=new iv.Popup(t,n.overlayContainer);return aj(r,n)},function(t,n,{position:r},i){N.useEffect(function(){const{instance:s}=t;function c(d){d.popup===s&&(s.update(),i(!0))}function u(d){d.popup===s&&i(!1)}return n.map.on({popupopen:c,popupclose:u}),n.overlayContainer==null?(r!=null&&s.setLatLng(r),s.openOn(n.map)):n.overlayContainer.bindPopup(s),function(){var h;n.map.off({popupopen:c,popupclose:u}),(h=n.overlayContainer)==null||h.unbindPopup(),n.map.removeLayer(s)}},[t,n,i,r])}),Oge=_ge(function({url:t,...n},r){const i=new iv.TileLayer(t,rv(n,r));return aj(i,r)},function(t,n,r){jge(t,n,r);const{url:i}=n;i!=null&&i!==r.url&&t.setUrl(i)}),Zy={critical:"#dc2626",high:"#f97316",medium:"#fbbf24",low:"#22c55e"};function kge(){const[e,t]=N.useState(null),[n,r]=N.useState(null),{data:i,isLoading:a}=zt({queryKey:["opportunities",e,n],queryFn:async()=>{const s=new URLSearchParams;return e&&s.append("state",e),n&&s.append("topic",n),(await vt.get(`/opportunities?${s}`)).data.opportunities||[]}});return a?o.jsx("div",{className:"flex justify-center items-center h-96",children:"Loading map..."}):o.jsxs("div",{className:"space-y-6",children:[o.jsxs("div",{className:"card flex gap-4",children:[o.jsxs("div",{className:"flex-1",children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Filter by State"}),o.jsxs("select",{className:"block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 text-gray-900",value:e||"",onChange:s=>t(s.target.value||null),children:[o.jsx("option",{value:"",children:"All States"}),o.jsx("option",{value:"AL",children:"Alabama"}),o.jsx("option",{value:"GA",children:"Georgia"}),o.jsx("option",{value:"IN",children:"Indiana"}),o.jsx("option",{value:"MA",children:"Massachusetts"}),o.jsx("option",{value:"WA",children:"Washington"}),o.jsx("option",{value:"WI",children:"Wisconsin"})]})]}),o.jsxs("div",{className:"flex-1",children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Filter by Topic"}),o.jsxs("select",{className:"block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 text-gray-900",value:n||"",onChange:s=>r(s.target.value||null),children:[o.jsx("option",{value:"",children:"All Topics"}),o.jsx("option",{value:"water_fluoridation",children:"Water Fluoridation"}),o.jsx("option",{value:"school_dental_screening",children:"School Dental Screening"}),o.jsx("option",{value:"medicaid_dental_expansion",children:"Medicaid Dental"})]})]})]}),o.jsxs("div",{className:"card",children:[o.jsx("h3",{className:"text-sm font-medium text-gray-700 mb-3",children:"Urgency Level"}),o.jsx("div",{className:"flex gap-6",children:Object.entries(Zy).map(([s,c])=>o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("div",{className:"w-4 h-4 rounded-full",style:{backgroundColor:c}}),o.jsx("span",{className:"text-sm capitalize text-gray-700",children:s})]},s))})]}),o.jsx("div",{className:"card h-[600px]",children:o.jsxs(Pge,{center:[39.8283,-98.5795],zoom:4,style:{height:"100%",width:"100%"},children:[o.jsx(Oge,{attribution:'© OpenStreetMap',url:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"}),i==null?void 0:i.map((s,c)=>o.jsx(Nge,{center:[s.latitude,s.longitude],radius:8,pathOptions:{fillColor:Zy[s.urgency],fillOpacity:.7,color:Zy[s.urgency],weight:2},children:o.jsx(Ege,{children:o.jsxs("div",{className:"p-2 min-w-[250px]",children:[o.jsxs("h4",{className:"font-bold text-gray-900",children:[s.municipality,", ",s.state]}),s.title&&o.jsxs("p",{className:"text-sm mt-2 text-gray-800",children:[o.jsx("strong",{children:"Bill:"})," ",s.bill_id," - ",s.title.substring(0,100),s.title.length>100?"...":""]}),o.jsxs("p",{className:"text-sm mt-1 text-gray-700",children:[o.jsx("strong",{children:"Topic:"})," ",s.topic.replace(/_/g," ")]}),o.jsxs("p",{className:"text-sm text-gray-700",children:[o.jsx("strong",{children:"Urgency:"})," ",s.urgency]}),o.jsxs("p",{className:"text-sm text-gray-700",children:[o.jsx("strong",{children:"Confidence:"})," ",(s.confidence*100).toFixed(0),"%"]}),o.jsxs("p",{className:"text-sm text-gray-700",children:[o.jsx("strong",{children:"Last Updated:"})," ",new Date(s.meeting_date).toLocaleDateString()]}),s.latest_action&&o.jsxs("p",{className:"text-sm mt-1 text-gray-700",children:[o.jsx("strong",{children:"Status:"})," ",s.latest_action.substring(0,80),s.latest_action.length>80?"...":""]})]})})},c))]})}),o.jsxs("div",{className:"card",children:[o.jsx("h3",{className:"text-lg font-semibold mb-2",children:"Summary"}),o.jsxs("p",{className:"text-gray-600",children:["Showing ",o.jsx("strong",{children:(i==null?void 0:i.length)||0})," advocacy opportunities",e&&` in ${e}`,n&&` for ${n.replace(/_/g," ")}`]})]})]})}function Cge(){var m;const[e,t]=Us(),[n,r]=N.useState(e.get("search")||""),[i,a]=N.useState(1),[s,c]=N.useState("all");N.useEffect(()=>{const p=e.get("search");p&&r(p)},[e]);const{data:u,isLoading:d}=zt({queryKey:["documents",n,i],queryFn:async()=>(await vt.get("/documents",{params:{search:n,page:i,limit:20}})).data}),h=p=>{p.preventDefault(),a(1),t(n?{search:n}:{})};return o.jsx("div",{className:"min-h-screen p-8",style:{backgroundColor:"#F1F5F9"},children:o.jsxs("div",{className:"max-w-7xl mx-auto",children:[o.jsxs("div",{className:"mb-8",children:[o.jsx("h1",{className:"text-3xl font-bold text-gray-900 mb-2",children:"Meeting Minutes"}),o.jsx("p",{className:"text-gray-600",children:"Search what local governments are discussing - 90,000+ cities, counties, and school districts"})]}),o.jsx("div",{className:"bg-white rounded-lg shadow-sm p-6 mb-6",children:o.jsx("form",{onSubmit:h,children:o.jsxs("div",{className:"relative",children:[o.jsx("input",{type:"text",placeholder:"Search by location, topic, keyword...",className:"w-full px-4 py-3 pl-12 border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent",value:n,onChange:p=>r(p.target.value)}),o.jsx(fn,{className:"absolute left-4 top-3.5 h-6 w-6 text-gray-400"}),o.jsx("button",{type:"submit",className:"absolute right-2 top-2 bg-neutral-600 text-white px-6 py-2 rounded-md hover:bg-neutral-700 transition-colors",children:"Search"})]})})}),o.jsx("div",{className:"bg-white rounded-lg shadow-sm p-4 mb-6 border-b border-gray-200",children:o.jsxs("div",{className:"flex flex-wrap gap-2",children:[o.jsx("button",{onClick:()=>c("all"),className:`px-4 py-2 rounded-full font-medium transition-colors ${s==="all"?"bg-primary-600 text-white":"bg-gray-100 text-gray-700 hover:bg-gray-200"}`,children:"All"}),o.jsx("button",{onClick:()=>c("meetings"),className:`px-4 py-2 rounded-full font-medium transition-colors ${s==="meetings"?"bg-primary-600 text-white":"bg-gray-100 text-gray-700 hover:bg-gray-200"}`,children:"Meeting Minutes"}),o.jsx("button",{onClick:()=>c("budgets"),className:`px-4 py-2 rounded-full font-medium transition-colors ${s==="budgets"?"bg-primary-600 text-white":"bg-gray-100 text-gray-700 hover:bg-gray-200"}`,children:"Budgets"}),o.jsx("button",{onClick:()=>c("people"),className:`px-4 py-2 rounded-full font-medium transition-colors ${s==="people"?"bg-primary-600 text-white":"bg-gray-100 text-gray-700 hover:bg-gray-200"}`,children:"People"}),o.jsx("button",{onClick:()=>c("organizations"),className:`px-4 py-2 rounded-full font-medium transition-colors ${s==="organizations"?"bg-primary-600 text-white":"bg-gray-100 text-gray-700 hover:bg-gray-200"}`,children:"Organizations"}),o.jsx("button",{onClick:()=>c("charities"),className:`px-4 py-2 rounded-full font-medium transition-colors ${s==="charities"?"bg-primary-600 text-white":"bg-gray-100 text-gray-700 hover:bg-gray-200"}`,children:"Charities"}),o.jsx("button",{onClick:()=>c("events"),className:`px-4 py-2 rounded-full font-medium transition-colors ${s==="events"?"bg-primary-600 text-white":"bg-gray-100 text-gray-700 hover:bg-gray-200"}`,children:"Events"}),o.jsxs("button",{className:"px-4 py-2 rounded-full font-medium bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors flex items-center gap-1",children:[o.jsx("svg",{className:"h-4 w-4",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:o.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"})}),"All filters"]})]})}),o.jsx("div",{className:"space-y-4",children:d?o.jsx("div",{className:"card",children:"Loading documents..."}):o.jsxs(o.Fragment,{children:[(m=u==null?void 0:u.documents)==null?void 0:m.map(p=>o.jsx("div",{className:"card hover:shadow-lg transition-shadow",children:o.jsxs("div",{className:"flex justify-between items-start",children:[o.jsxs("div",{className:"flex-1",children:[o.jsx("h3",{className:"text-lg font-semibold text-gray-900",children:p.title}),o.jsxs("p",{className:"text-sm text-gray-600 mt-1",children:[p.municipality,", ",p.state," • ",new Date(p.meeting_date).toLocaleDateString()]}),o.jsx("div",{className:"mt-3 flex flex-wrap gap-2",children:p.topics.map(v=>o.jsx("span",{className:"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800",children:v.replace(/_/g," ")},v))})]}),o.jsx("a",{href:p.url,target:"_blank",rel:"noopener noreferrer",className:"btn-primary ml-4",children:"View Document"})]})},p.id)),o.jsxs("div",{className:"flex justify-center gap-2 mt-6",children:[o.jsx("button",{onClick:()=>a(i-1),disabled:i===1,className:"btn-secondary disabled:opacity-50",children:"Previous"}),o.jsxs("span",{className:"flex items-center px-4",children:["Page ",i," of ",(u==null?void 0:u.total_pages)||1]}),o.jsx("button",{onClick:()=>a(i+1),disabled:i>=((u==null?void 0:u.total_pages)||1),className:"btn-secondary disabled:opacity-50",children:"Next"})]})]})})]})})}function Age(){const[e,t]=N.useState(null),{data:n,isLoading:r}=zt({queryKey:["opportunities-list",e],queryFn:async()=>{const a=new URLSearchParams;return e&&a.append("urgency",e),(await vt.get(`/opportunities?${a}`)).data.opportunities||[]}}),i=async a=>{try{const s=await vt.post(`/advocacy/email/${a}`),c=new Blob([s.data.content],{type:"text/plain"}),u=window.URL.createObjectURL(c),d=document.createElement("a");d.href=u,d.download=`advocacy-email-${a}.txt`,d.click()}catch(s){console.error("Failed to generate email:",s)}};return o.jsxs("div",{className:"space-y-6",children:[o.jsxs("div",{className:"card",children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Filter by Urgency"}),o.jsx("div",{className:"flex gap-2",children:["critical","high","medium","low"].map(a=>o.jsx("button",{onClick:()=>t(e===a?null:a),className:`px-4 py-2 rounded-lg capitalize ${e===a?"bg-primary-600 text-white":"bg-gray-200 text-gray-700 hover:bg-gray-300"}`,children:a},a))})]}),o.jsx("div",{className:"space-y-4",children:r?o.jsx("div",{className:"card",children:"Loading causes..."}):n==null?void 0:n.map(a=>{var s;return o.jsxs("div",{className:"card",children:[o.jsxs("div",{className:"flex justify-between items-start mb-4",children:[o.jsxs("div",{children:[o.jsxs("h3",{className:"text-xl font-semibold text-gray-900",children:[a.municipality,", ",a.state]}),o.jsxs("p",{className:"text-sm text-gray-600 mt-1",children:[a.topic.replace(/_/g," ")," • Meeting: ",new Date(a.meeting_date).toLocaleDateString()]})]}),o.jsx("span",{className:`px-3 py-1 rounded-full text-sm font-medium ${a.urgency==="critical"?"bg-red-100 text-red-800":a.urgency==="high"?"bg-orange-100 text-orange-800":a.urgency==="medium"?"bg-yellow-100 text-yellow-800":"bg-green-100 text-green-800"}`,children:a.urgency})]}),a.talking_points&&a.talking_points.length>0&&o.jsxs("div",{className:"mb-4",children:[o.jsx("h4",{className:"font-medium text-gray-900 mb-2",children:"Key Talking Points:"}),o.jsx("ul",{className:"list-disc list-inside space-y-1",children:a.talking_points.map((c,u)=>o.jsx("li",{className:"text-sm text-gray-700",children:c},u))})]}),o.jsxs("div",{className:"flex gap-3",children:[o.jsx("button",{onClick:()=>i(a.id),className:"btn-primary",children:"Generate Email"}),((s=a.contact_info)==null?void 0:s.email)&&o.jsx("a",{href:`mailto:${a.contact_info.email}`,className:"btn-secondary",children:"Contact via Email"}),a.next_meeting&&o.jsx("button",{className:"btn-secondary",children:"📅 Add to Calendar"})]}),o.jsx("div",{className:"mt-4 pt-4 border-t border-gray-200",children:o.jsxs("div",{className:"flex items-center justify-between text-sm",children:[o.jsx("span",{className:"text-gray-600",children:"Confidence Score:"}),o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("div",{className:"w-32 bg-gray-200 rounded-full h-2",children:o.jsx("div",{className:"bg-primary-600 h-2 rounded-full",style:{width:`${a.confidence*100}%`}})}),o.jsxs("span",{className:"font-medium",children:[(a.confidence*100).toFixed(0),"%"]})]})]})})]},a.id)})})]})}const oi=e=>{if(!e||e===0)return"$0";const t=Math.abs(e);return t>=1e9?`$${(e/1e9).toFixed(1)}B`:t>=1e6?`$${(e/1e6).toFixed(1)}M`:t>=1e3?`$${(e/1e3).toFixed(1)}K`:`$${e.toFixed(0)}`};function Tge(){const[e,t]=N.useState([]),[n,r]=N.useState(!1),[i,a]=N.useState(null),[s,c]=N.useState({location:"Tuscaloosa, AL",keyword:"dental",state:"",nteeCode:"E"}),u=async()=>{r(!0),a(null);try{const d=new URLSearchParams;s.location&&d.append("location",s.location),s.keyword&&d.append("keyword",s.keyword),s.state&&d.append("state",s.state),s.nteeCode&&d.append("ntee_code",s.nteeCode);const h=await fetch(`/api/nonprofits?${d}`);if(!h.ok)throw new Error("Failed to fetch nonprofits");const m=await h.json();t(m.nonprofits||[])}catch(d){a(d instanceof Error?d.message:"An error occurred")}finally{r(!1)}};return N.useEffect(()=>{u()},[]),o.jsx("div",{className:"min-h-screen p-8",style:{backgroundColor:"#F1F5F9"},children:o.jsxs("div",{className:"max-w-7xl mx-auto space-y-6",children:[o.jsxs("div",{children:[o.jsx("h1",{className:"text-3xl font-bold mb-2",style:{color:"#354F52"},children:"Local Charities"}),o.jsx("p",{className:"text-gray-600",children:"Find charities and nonprofits providing services in your community"})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6",children:[o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-4 gap-4",children:[o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"Location"}),o.jsx("input",{type:"text",value:s.location,onChange:d=>c({...s,location:d.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",placeholder:"City, State"})]}),o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"Keyword"}),o.jsx("input",{type:"text",value:s.keyword,onChange:d=>c({...s,keyword:d.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",placeholder:"dental, health, etc."})]}),o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"NTEE Code"}),o.jsxs("select",{value:s.nteeCode,onChange:d=>c({...s,nteeCode:d.target.value}),className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",children:[o.jsx("option",{value:"",children:"All Categories"}),o.jsx("option",{value:"E",children:"Health (E)"}),o.jsx("option",{value:"E20",children:"Hospitals (E20)"}),o.jsx("option",{value:"E30",children:"Community Clinics (E30)"}),o.jsx("option",{value:"E32",children:"School-Based Health (E32)"}),o.jsx("option",{value:"B",children:"Education (B)"}),o.jsx("option",{value:"P",children:"Human Services (P)"})]})]}),o.jsx("div",{className:"flex items-end",children:o.jsx("button",{onClick:u,disabled:n,className:"w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed",children:n?"Searching...":"Search"})})]}),o.jsxs("div",{className:"mt-4 text-sm text-gray-500",children:[o.jsxs("p",{children:[o.jsx("strong",{children:"Data Sources:"})," ProPublica Nonprofit Explorer • Every.org • IRS TEOS"]}),o.jsxs("p",{className:"mt-1",children:[o.jsx("a",{href:"https://projects.propublica.org/nonprofits/api",target:"_blank",rel:"noopener noreferrer",className:"text-blue-600 hover:underline",children:"ProPublica API"})," • ",o.jsx("a",{href:"https://www.every.org/nonprofit-api",target:"_blank",rel:"noopener noreferrer",className:"text-blue-600 hover:underline",children:"Every.org API"})," • ",o.jsx("a",{href:"https://www.irs.gov/charities-non-profits/tax-exempt-organization-search-bulk-data-downloads",target:"_blank",rel:"noopener noreferrer",className:"text-blue-600 hover:underline",children:"IRS TEOS"})]})]})]}),i&&o.jsx("div",{className:"bg-red-50 border border-red-200 rounded-lg p-4 text-red-700",children:i}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm",children:[o.jsx("div",{className:"px-6 py-4 border-b border-gray-200",children:o.jsx("h2",{className:"text-lg font-semibold text-gray-900",children:n?"Loading...":`Found ${e.length} Organizations`})}),o.jsx("div",{className:"divide-y divide-gray-200",children:e.length===0&&!n?o.jsx("div",{className:"px-6 py-12 text-center text-gray-500",children:"No nonprofits found. Try adjusting your search criteria."}):e.map((d,h)=>o.jsx("div",{className:"px-6 py-4 hover:bg-gray-50",children:o.jsxs("div",{className:"flex items-start justify-between",children:[o.jsxs("div",{className:"flex-1",children:[o.jsxs("div",{className:"flex items-center gap-3",children:[d.logo_url?o.jsx("img",{src:d.logo_url,alt:d.name,className:"w-12 h-12 rounded object-contain bg-gray-100 border border-gray-200",onError:m=>{m.currentTarget.style.display="none";const p=m.currentTarget.nextElementSibling;p&&(p.style.display="flex")}}):null,o.jsx("div",{className:"w-12 h-12 rounded flex items-center justify-center text-white text-lg font-bold",style:{backgroundColor:"#52796F",display:d.logo_url?"none":"flex"},children:d.name.charAt(0)}),o.jsxs("div",{children:[o.jsx("h3",{className:"text-lg font-semibold text-gray-900",children:d.name}),o.jsxs("p",{className:"text-sm text-gray-500",children:["EIN: ",d.ein," • ",d.city,", ",d.state]})]})]}),(d.description||d.mission)&&o.jsx("p",{className:"mt-2 text-gray-600",children:d.description||d.mission}),o.jsxs("div",{className:"mt-2 flex flex-wrap gap-2",children:[d.ntee_description&&o.jsx("span",{className:"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800",children:d.ntee_description}),o.jsxs("span",{className:"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800",children:["Source: ",d.source]})]}),(d.revenue_amount||d.asset_amount)&&o.jsxs("div",{className:"mt-3 grid grid-cols-3 gap-4 text-sm",children:[d.revenue_amount&&o.jsxs("div",{children:[o.jsx("span",{className:"text-gray-500",children:"Revenue: "}),o.jsx("span",{className:"font-medium text-gray-900",children:oi(d.revenue_amount)})]}),d.asset_amount&&o.jsxs("div",{children:[o.jsx("span",{className:"text-gray-500",children:"Assets: "}),o.jsx("span",{className:"font-medium text-gray-900",children:oi(d.asset_amount)})]}),d.income_amount&&o.jsxs("div",{children:[o.jsx("span",{className:"text-gray-500",children:"Income: "}),o.jsx("span",{className:"font-medium text-gray-900",children:oi(d.income_amount)})]})]})]}),d.website_url&&o.jsx("a",{href:d.website_url,target:"_blank",rel:"noopener noreferrer",className:"ml-4 px-4 py-2 bg-blue-600 text-white text-sm rounded-md hover:bg-blue-700",children:"Visit Website"})]})},`${d.ein}-${h}`))})]}),o.jsxs("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-6",children:[o.jsx("h3",{className:"text-lg font-semibold text-blue-900 mb-2",children:"Why This Matters"}),o.jsxs("p",{className:"text-blue-800",children:[`When officials reject policy proposals with technical objections ("We can't do dental screenings - legal liability"), you can instantly show citizens the nonprofits`," ",o.jsx("strong",{children:"already doing it successfully"}),". This bypasses technocratic vetoes, creates accountability pressure, and mobilizes citizens with direct volunteer/donation pathways."]}),o.jsxs("div",{className:"mt-4 space-y-2 text-sm text-blue-700",children:[o.jsx("p",{children:"✓ Access financial data from 3+ million organizations"}),o.jsx("p",{children:"✓ Find local service providers already solving the problem"}),o.jsx("p",{children:"✓ Show working alternatives to government inaction"}),o.jsx("p",{children:"✓ All data is 100% free from public APIs"})]})]})]})})}const SL="https://datasets-server.huggingface.co";async function Mge(e,t=0,n=100){const r=new URLSearchParams({dataset:e.dataset,config:e.config||"default",split:e.split,offset:t.toString(),length:Math.min(n,100).toString()}),i=`${SL}/rows?${r}`,a=await fetch(i);if(!a.ok)throw new Error(`HuggingFace API error: ${a.statusText}`);return a.json()}async function Lge(e,t,n=0,r=100){const i=new URLSearchParams({dataset:e.dataset,config:e.config||"default",split:e.split,query:t,offset:n.toString(),length:Math.min(r,100).toString()}),a=`${SL}/search?${i}`,s=await fetch(a);if(!s.ok)throw new Error(`HuggingFace search error: ${s.statusText}`);return s.json()}async function $ge(e,t="organizations",n=1e3){const r=[];let i=0;const a=100;for(;iu.row);if(r.push(...c),c.lengthu.row):s=await $ge(t,"organizations",a),r&&(s=s.filter(c=>c.state===r)),i&&(s=s.filter(c=>{var u;return(u=c.ntee_code)==null?void 0:u.startsWith(i)})),s.slice(0,a)}const Gy="CommunityOne/one-nonprofits-organizations",jO=[{code:"",label:"All Categories"},{code:"E",label:"Health (E)"},{code:"E20",label:"Hospitals & Medical Centers (E20)"},{code:"E30",label:"Community Health Centers (E30)"},{code:"E40",label:"Reproductive Health (E40)"},{code:"E50",label:"Rehabilitative Care (E50)"},{code:"E60",label:"Health Support Services (E60)"},{code:"E70",label:"Public Health (E70)"},{code:"E80",label:"Health - General & Financing (E80)"},{code:"E90",label:"Nursing Services (E90)"},{code:"P",label:"Human Services (P)"},{code:"X",label:"Religion-Related (X)"},{code:"X20",label:"Christian (X20)"},{code:"X21",label:"Protestant (X21)"},{code:"X22",label:"Roman Catholic (X22)"},{code:"X30",label:"Jewish (X30)"},{code:"X40",label:"Islamic (X40)"},{code:"B",label:"Education (B)"},{code:"C",label:"Environment (C)"},{code:"D",label:"Animal-Related (D)"},{code:"F",label:"Mental Health (F)"},{code:"G",label:"Disease-Specific (G)"},{code:"H",label:"Medical Research (H)"}],NO=[{code:"",label:"All States"},{code:"AL",label:"Alabama"},{code:"AK",label:"Alaska"},{code:"AZ",label:"Arizona"},{code:"AR",label:"Arkansas"},{code:"CA",label:"California"},{code:"CO",label:"Colorado"},{code:"CT",label:"Connecticut"},{code:"DE",label:"Delaware"},{code:"FL",label:"Florida"},{code:"GA",label:"Georgia"},{code:"HI",label:"Hawaii"},{code:"ID",label:"Idaho"},{code:"IL",label:"Illinois"},{code:"IN",label:"Indiana"},{code:"IA",label:"Iowa"},{code:"KS",label:"Kansas"},{code:"KY",label:"Kentucky"},{code:"LA",label:"Louisiana"},{code:"ME",label:"Maine"},{code:"MD",label:"Maryland"},{code:"MA",label:"Massachusetts"},{code:"MI",label:"Michigan"},{code:"MN",label:"Minnesota"},{code:"MS",label:"Mississippi"},{code:"MO",label:"Missouri"},{code:"MT",label:"Montana"},{code:"NE",label:"Nebraska"},{code:"NV",label:"Nevada"},{code:"NH",label:"New Hampshire"},{code:"NJ",label:"New Jersey"},{code:"NM",label:"New Mexico"},{code:"NY",label:"New York"},{code:"NC",label:"North Carolina"},{code:"ND",label:"North Dakota"},{code:"OH",label:"Ohio"},{code:"OK",label:"Oklahoma"},{code:"OR",label:"Oregon"},{code:"PA",label:"Pennsylvania"},{code:"RI",label:"Rhode Island"},{code:"SC",label:"South Carolina"},{code:"SD",label:"South Dakota"},{code:"TN",label:"Tennessee"},{code:"TX",label:"Texas"},{code:"UT",label:"Utah"},{code:"VT",label:"Vermont"},{code:"VA",label:"Virginia"},{code:"WA",label:"Washington"},{code:"WV",label:"West Virginia"},{code:"WI",label:"Wisconsin"},{code:"WY",label:"Wyoming"}];function Rge(){var _,x;const[e]=Us(),[t,n]=N.useState(e.get("search")||"dental"),[r,i]=N.useState(e.get("state")||"AL"),[a,s]=N.useState(""),[c,u]=N.useState(0),d=100;N.useEffect(()=>{const y=e.get("search"),w=e.get("state");y&&n(y),w&&i(w)},[e]);const{data:h,isLoading:m,error:p}=zt({queryKey:["nonprofits-hf",t,r,a,c],queryFn:async()=>await Ige({dataset:Gy,query:t||void 0,state:r||void 0,nteeCode:a||void 0,limit:d}),staleTime:5*60*1e3}),v=y=>({3:"501(c)(3) - Charitable",4:"501(c)(4) - Social Welfare",5:"501(c)(5) - Labor/Agricultural",6:"501(c)(6) - Business League",7:"501(c)(7) - Social/Recreational",8:"501(c)(8) - Fraternal Beneficiary",9:"501(c)(9) - Employees Association",10:"501(c)(10) - Domestic Fraternal",13:"501(c)(13) - Cemetery Company",19:"501(c)(19) - Veterans Organization"})[y||""]||`501(c)(${y})`;return o.jsx("div",{className:"min-h-screen p-8",style:{backgroundColor:"#F1F5F9"},children:o.jsxs("div",{className:"max-w-7xl mx-auto space-y-6",children:[o.jsxs("div",{children:[o.jsx("h1",{className:"text-3xl font-bold mb-2",style:{color:"#354F52"},children:"Nonprofit Organizations"}),o.jsx("p",{className:"text-gray-600",children:"Explore 1.9M+ U.S. nonprofits from IRS EO-BMF via HuggingFace Datasets"}),o.jsxs("div",{className:"mt-2 flex items-center gap-2 text-sm text-gray-500",children:[o.jsx("span",{className:"px-2 py-1 bg-blue-100 text-blue-700 rounded",children:"📊 Source: HuggingFace Dataset"}),o.jsx("span",{className:"px-2 py-1 bg-green-100 text-green-700 rounded",children:"✅ 1,952,238 organizations"}),o.jsx("span",{className:"px-2 py-1 bg-purple-100 text-purple-700 rounded",children:"🔄 Updated Monthly (IRS)"})]})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6",children:[o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-4 gap-4",children:[o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"Search"}),o.jsx("input",{type:"text",value:t,onChange:y=>n(y.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",placeholder:"e.g., dental, clinic, food bank"})]}),o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"State"}),o.jsx("select",{value:r,onChange:y=>i(y.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",children:NO.map(y=>o.jsx("option",{value:y.code,children:y.label},y.code))})]}),o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"Category (NTEE)"}),o.jsx("select",{value:a,onChange:y=>s(y.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500",children:jO.map(y=>o.jsx("option",{value:y.code,children:y.label},y.code))})]}),o.jsx("div",{className:"flex items-end",children:o.jsx("button",{onClick:()=>u(0),className:"w-full px-4 py-2 text-white rounded-md hover:opacity-90 transition-opacity",style:{backgroundColor:"#52796F"},children:"🔍 Search"})})]}),o.jsxs("div",{className:"mt-4 flex flex-wrap gap-2",children:[t&&o.jsxs("span",{className:"px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm",children:['Query: "',t,'"']}),r&&o.jsxs("span",{className:"px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm",children:["State: ",(_=NO.find(y=>y.code===r))==null?void 0:_.label]}),a&&o.jsxs("span",{className:"px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-sm",children:["Category: ",(x=jO.find(y=>y.code===a))==null?void 0:x.label]})]})]}),m&&o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-12 text-center",children:[o.jsx("div",{className:"inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"}),o.jsx("p",{className:"mt-4 text-gray-600",children:"Loading nonprofits from HuggingFace..."})]}),p&&o.jsxs("div",{className:"bg-red-50 border border-red-200 rounded-lg p-6",children:[o.jsx("h3",{className:"text-red-800 font-semibold mb-2",children:"Error Loading Data"}),o.jsx("p",{className:"text-red-600",children:p.message}),o.jsxs("p",{className:"text-sm text-red-500 mt-2",children:["Make sure the dataset is uploaded to HuggingFace. Run: ",o.jsx("code",{children:"python scripts/upload_nonprofits_to_hf.py --all"})]})]}),!m&&!p&&h&&o.jsxs(o.Fragment,{children:[o.jsx("div",{className:"bg-white rounded-lg shadow-sm p-4",children:o.jsxs("p",{className:"text-gray-700",children:["Found ",o.jsx("strong",{children:h.length})," nonprofits",h.length===d&&" (showing first 100)"]})}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6",children:h.map(y=>o.jsx("div",{className:"bg-white rounded-lg shadow-sm p-6 hover:shadow-md transition-shadow",children:o.jsxs("div",{className:"space-y-3",children:[o.jsx("h3",{className:"text-lg font-semibold",style:{color:"#354F52"},children:y.name}),o.jsxs("div",{className:"text-sm text-gray-600",children:[o.jsx("span",{className:"font-medium",children:"EIN:"})," ",y.ein]}),o.jsxs("div",{className:"text-sm text-gray-600",children:[o.jsx("span",{className:"font-medium",children:"📍 Location:"})," ",y.city&&y.state?o.jsxs(o.Fragment,{children:[y.city,", ",y.state," ",y.zip_code]}):y.state||"N/A"]}),y.ntee_code&&o.jsx("div",{className:"text-sm",children:o.jsx("span",{className:"px-2 py-1 bg-purple-100 text-purple-700 rounded text-xs",children:y.ntee_code})}),y.subsection_code&&o.jsxs("div",{className:"text-sm text-gray-600",children:[o.jsx("span",{className:"font-medium",children:"Type:"})," ",v(y.subsection_code)]}),o.jsxs("div",{className:"pt-3 border-t space-y-1",children:[o.jsxs("div",{className:"text-sm text-gray-600",children:[o.jsx("span",{className:"font-medium",children:"Assets:"})," ",oi(y.asset_amount)]}),o.jsxs("div",{className:"text-sm text-gray-600",children:[o.jsx("span",{className:"font-medium",children:"Income:"})," ",oi(y.income_amount)]}),o.jsxs("div",{className:"text-sm text-gray-600",children:[o.jsx("span",{className:"font-medium",children:"Revenue:"})," ",oi(y.revenue_amount)]})]}),y.tax_exempt_status&&o.jsx("div",{className:"text-sm",children:o.jsx("span",{className:"px-2 py-1 bg-green-100 text-green-700 rounded text-xs",children:y.tax_exempt_status==="1"?"✅ Tax-Exempt":"Status: "+y.tax_exempt_status})}),y.ruling_date&&o.jsxs("div",{className:"text-xs text-gray-500",children:["Ruling Date: ",y.ruling_date]})]})},y.ein))}),h.length===0&&o.jsxs("div",{className:"bg-gray-50 rounded-lg p-12 text-center",children:[o.jsx("p",{className:"text-gray-600 text-lg",children:"No nonprofits found matching your criteria"}),o.jsx("p",{className:"text-gray-500 text-sm mt-2",children:"Try adjusting your filters or search query"})]})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6",children:[o.jsx("h3",{className:"text-lg font-semibold mb-3",style:{color:"#354F52"},children:"📚 Data Source"}),o.jsxs("div",{className:"space-y-2 text-sm text-gray-600",children:[o.jsxs("p",{children:[o.jsx("strong",{children:"Source:"})," IRS Exempt Organizations Business Master File (EO-BMF)"]}),o.jsxs("p",{children:[o.jsx("strong",{children:"Dataset:"})," ",o.jsx("a",{href:`https://huggingface.co/datasets/${Gy}`,target:"_blank",rel:"noopener noreferrer",className:"text-blue-600 hover:underline",children:Gy})]}),o.jsxs("p",{children:[o.jsx("strong",{children:"Records:"})," 1,952,238 organizations"]}),o.jsxs("p",{children:[o.jsx("strong",{children:"Updated:"})," Monthly by IRS"]}),o.jsxs("p",{children:[o.jsx("strong",{children:"License:"})," Public Domain (U.S. government data)"]}),o.jsx("p",{className:"pt-2 border-t",children:o.jsx("a",{href:"https://www.irs.gov/charities-non-profits/exempt-organizations-business-master-file-extract-eo-bmf",target:"_blank",rel:"noopener noreferrer",className:"text-blue-600 hover:underline",children:"View IRS EO-BMF Documentation →"})})]})]})]})})}function Fge(){const{user:e}=e0(),[t,n]=N.useState(!1),[r,i]=N.useState({state:(e==null?void 0:e.state)||"",county:(e==null?void 0:e.county)||"",city:(e==null?void 0:e.city)||"",school_board:(e==null?void 0:e.school_board)||""}),[a,s]=N.useState(!1),[c,u]=N.useState(null),[d,h]=N.useState({}),m="/api";N.useEffect(()=>{e&&i({state:e.state||"",county:e.county||"",city:e.city||"",school_board:e.school_board||""})},[e]);const p=x=>{i({state:x.state,county:x.county,city:x.city,school_board:r.school_board}),n(!1)},v=(x,y)=>{i(w=>({...w,[x]:y})),d[x]&&h(w=>{const b={...w};return delete b[x],b}),u(null)},_=async x=>{x.preventDefault();const y={};if(r.state.trim()||(y.state="State is required"),r.county.trim()||(y.county="County is required"),r.city.trim()||(y.city="City is required"),Object.keys(y).length>0){h(y);return}s(!0),u(null);try{const w=localStorage.getItem("auth_token");(await fetch(`${m}/auth/profile`,{method:"PATCH",headers:{"Content-Type":"application/json",Authorization:`Bearer ${w}`},body:JSON.stringify(r)})).ok?(u("success"),setTimeout(()=>window.location.reload(),1500)):u("error")}catch(w){console.error("Failed to update location:",w),u("error")}finally{s(!1)}};return e?o.jsxs("div",{className:"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8",children:[o.jsx("h1",{className:"text-3xl font-bold mb-8",style:{color:"#354F52"},children:"Settings"}),o.jsxs("div",{className:"bg-white rounded-lg shadow mb-6",children:[o.jsx("div",{className:"border-b border-gray-200 px-6 py-4",children:o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(jb,{className:"h-6 w-6 text-gray-600"}),o.jsx("h2",{className:"text-xl font-semibold text-gray-900",children:"Profile Information"})]})}),o.jsx("div",{className:"px-6 py-4",children:o.jsxs("div",{className:"space-y-4",children:[o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"Email"}),o.jsx("input",{type:"text",value:e.email,disabled:!0,className:"w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-600"})]}),e.full_name&&o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"Full Name"}),o.jsx("input",{type:"text",value:e.full_name,disabled:!0,className:"w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-600"})]}),e.oauth_provider&&o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"Sign-in Method"}),o.jsx("input",{type:"text",value:e.oauth_provider.charAt(0).toUpperCase()+e.oauth_provider.slice(1),disabled:!0,className:"w-full px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-600 capitalize"})]})]})})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow",children:[o.jsxs("div",{className:"border-b border-gray-200 px-6 py-4",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Rn,{className:"h-6 w-6 text-gray-600"}),o.jsx("h2",{className:"text-xl font-semibold text-gray-900",children:"Location Preferences"})]}),o.jsx("p",{className:"text-sm text-gray-500 mt-1",children:"Help us show you relevant local government and nonprofit information"})]}),o.jsxs("form",{onSubmit:_,className:"px-6 py-4",children:[o.jsx("div",{className:"mb-6 p-4 bg-gray-50 rounded-lg",children:o.jsxs("label",{className:"flex items-center gap-2 cursor-pointer",children:[o.jsx("input",{type:"checkbox",checked:t,onChange:x=>n(x.target.checked),className:"rounded border-gray-300 text-primary-600 focus:ring-primary-500"}),o.jsx("span",{className:"text-sm font-medium text-gray-700",children:"Use address lookup to auto-fill location"})]})}),t?o.jsxs("div",{className:"mb-6",children:[o.jsx(K_,{onLocationFound:p}),o.jsx("p",{className:"mt-2 text-sm text-gray-500",children:"After finding your address, you can review and edit the details below"})]}):null,o.jsxs("div",{className:"space-y-4",children:[o.jsxs("div",{children:[o.jsxs("label",{htmlFor:"state",className:"block text-sm font-medium text-gray-700 mb-1",children:["State ",o.jsx("span",{className:"text-red-500",children:"*"})]}),o.jsx("input",{type:"text",id:"state",value:r.state,onChange:x=>v("state",x.target.value),placeholder:"e.g., California, Texas, New York",className:`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${d.state?"border-red-500":"border-gray-300"}`}),d.state&&o.jsx("p",{className:"mt-1 text-sm text-red-600",children:d.state})]}),o.jsxs("div",{children:[o.jsxs("label",{htmlFor:"county",className:"block text-sm font-medium text-gray-700 mb-1",children:["County ",o.jsx("span",{className:"text-red-500",children:"*"})]}),o.jsx("input",{type:"text",id:"county",value:r.county,onChange:x=>v("county",x.target.value),placeholder:"e.g., Los Angeles County, Harris County",className:`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${d.county?"border-red-500":"border-gray-300"}`}),d.county&&o.jsx("p",{className:"mt-1 text-sm text-red-600",children:d.county})]}),o.jsxs("div",{children:[o.jsxs("label",{htmlFor:"city",className:"block text-sm font-medium text-gray-700 mb-1",children:["City ",o.jsx("span",{className:"text-red-500",children:"*"})]}),o.jsx("input",{type:"text",id:"city",value:r.city,onChange:x=>v("city",x.target.value),placeholder:"e.g., Los Angeles, Houston, New York",className:`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${d.city?"border-red-500":"border-gray-300"}`}),d.city&&o.jsx("p",{className:"mt-1 text-sm text-red-600",children:d.city})]}),o.jsxs("div",{children:[o.jsxs("label",{htmlFor:"school_board",className:"block text-sm font-medium text-gray-700 mb-1",children:["School Board / District ",o.jsx("span",{className:"text-gray-400 text-xs",children:"(Optional)"})]}),o.jsx("input",{type:"text",id:"school_board",value:r.school_board,onChange:x=>v("school_board",x.target.value),placeholder:"e.g., LAUSD, Houston ISD",className:"w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"})]})]}),c&&o.jsx("div",{className:`mt-4 p-4 rounded-lg flex items-center gap-2 ${c==="success"?"bg-green-50 text-green-800":"bg-red-50 text-red-800"}`,children:c==="success"?o.jsxs(o.Fragment,{children:[o.jsx(ci,{className:"h-5 w-5"}),o.jsx("span",{children:"Settings saved successfully!"})]}):o.jsxs(o.Fragment,{children:[o.jsx(qB,{className:"h-5 w-5"}),o.jsx("span",{children:"Failed to save settings. Please try again."})]})}),o.jsx("div",{className:"mt-6",children:o.jsx("button",{type:"submit",disabled:a,className:"w-full sm:w-auto px-6 py-2.5 text-white rounded-lg transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed",style:{backgroundColor:"#354F52"},onMouseEnter:x=>!a&&(x.currentTarget.style.backgroundColor="#2e4346"),onMouseLeave:x=>!a&&(x.currentTarget.style.backgroundColor="#354F52"),children:a?"Saving...":"Save Changes"})})]})]})]}):o.jsx("div",{className:"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8",children:o.jsx("div",{className:"bg-white rounded-lg shadow p-6 text-center",children:o.jsx("p",{className:"text-gray-600",children:"Please sign in to access settings."})})})}function Dge({title:e,titleId:t,...n},r){return N.createElement("svg",Object.assign({xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor","aria-hidden":"true","data-slot":"icon",ref:r,"aria-labelledby":t},n),e?N.createElement("title",{id:t},e):null,N.createElement("path",{fillRule:"evenodd",d:"M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z",clipRule:"evenodd"}))}const SO=N.forwardRef(Dge);function km({type:e,id:t,initialFollowing:n=!1,initialCount:r=0,showCount:i=!0,compact:a=!1,onFollowChange:s}){const[c,u]=N.useState(n),[d,h]=N.useState(r),[m,p]=N.useState(!1),v=R_(),_=h5({mutationFn:async()=>{const b=`/social/follow/${e}/${t}`;return(await vt.post(b)).data},onSuccess:b=>{u(!0),h(b.follower_count),s==null||s(!0,b.follower_count),v.invalidateQueries({queryKey:["social","stats"]}),v.invalidateQueries({queryKey:["following",e]})}}),x=h5({mutationFn:async()=>{const b=`/social/follow/${e}/${t}`;return(await vt.delete(b)).data},onSuccess:b=>{u(!1),h(b.follower_count),s==null||s(!1,b.follower_count),v.invalidateQueries({queryKey:["social","stats"]}),v.invalidateQueries({queryKey:["following",e]})}}),y=()=>{c?x.mutate():_.mutate()},w=_.isPending||x.isPending;return a?o.jsx("button",{onClick:y,disabled:w,onMouseEnter:()=>p(!0),onMouseLeave:()=>p(!1),className:` + inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full text-sm font-medium + transition-all duration-200 border + ${c?m?"bg-red-50 border-red-300 text-red-700 hover:bg-red-100":"bg-white border-gray-300 text-gray-700 hover:bg-gray-50":"bg-blue-600 border-blue-600 text-white hover:bg-blue-700"} + disabled:opacity-50 disabled:cursor-not-allowed + `,children:w?o.jsx("span",{className:"h-4 w-4 border-2 border-t-transparent border-current rounded-full animate-spin"}):c?o.jsxs(o.Fragment,{children:[m?o.jsx(S5,{className:"h-4 w-4"}):o.jsx(SO,{className:"h-4 w-4"}),o.jsx("span",{children:m?"Unfollow":"Following"})]}):o.jsxs(o.Fragment,{children:[o.jsx(Nb,{className:"h-4 w-4"}),o.jsx("span",{children:"Follow"})]})}):o.jsxs("div",{className:"inline-flex items-center gap-3",children:[o.jsx("button",{onClick:y,disabled:w,onMouseEnter:()=>p(!0),onMouseLeave:()=>p(!1),className:` + inline-flex items-center gap-2 px-5 py-2 rounded-lg text-sm font-semibold + transition-all duration-200 border-2 + ${c?m?"bg-red-50 border-red-400 text-red-700 hover:bg-red-100":"bg-white border-gray-300 text-gray-700 hover:border-gray-400":"bg-blue-600 border-blue-600 text-white hover:bg-blue-700"} + disabled:opacity-50 disabled:cursor-not-allowed shadow-sm hover:shadow-md + `,children:w?o.jsx("span",{className:"h-5 w-5 border-2 border-t-transparent border-current rounded-full animate-spin"}):c?o.jsxs(o.Fragment,{children:[m?o.jsx(S5,{className:"h-5 w-5"}):o.jsx(SO,{className:"h-5 w-5"}),o.jsx("span",{children:m?"Unfollow":"Following"})]}):o.jsxs(o.Fragment,{children:[o.jsx(Nb,{className:"h-5 w-5"}),o.jsx("span",{children:"Follow"})]})}),i&&o.jsxs("span",{className:"text-sm text-gray-600",children:[d.toLocaleString()," ",d===1?"follower":"followers"]})]})}const ul={"decision-makers":{title:"Decision Makers",subtitle:"People who vote on policy and budgets",icon:Ur,color:"#354F52",roles:["Community Representatives","The Decision Makers","Board Members","Policy Voters"]},support:{title:"Support Staff",subtitle:"People who help and advise",icon:Eo,color:"#64748B",roles:["Expert Advisors","Community Guides","Program Staff"]},public:{title:"Community Members",subtitle:"Residents and advocates",icon:Az,color:"#8a9d9e",roles:["Neighbors & Residents","Parents & Families","Advocates"]},"open-source":{title:"Open Source Contributors",subtitle:"Civic tech maintainers and developers",icon:$r,color:"#06B6D4",roles:["Project Maintainers","Core Contributors","Community Developers"]}};function Bge(){var j,E,P,O,C;const[e,t]=N.useState(""),[n,r]=N.useState("all"),[i,a]=N.useState("all"),[s,c]=N.useState(1),{location:u}=Hf(),h=(u==null?void 0:u.state)||"AL",m=(u==null?void 0:u.city)||null,{data:p,isLoading:v,error:_}=zt({queryKey:["people-finder",h,s],queryFn:async()=>{const A={types:"contacts",limit:100,state:h,page:s};return(await vt.get("/search",{params:A})).data},staleTime:6e4}),x=(((j=p==null?void 0:p.results)==null?void 0:j.contacts)||[]).map((A,T)=>({id:T+1,name:A.metadata.name,role:"decision-makers",specificRole:A.metadata.title||"Official",organization:A.metadata.jurisdiction||"Local Government",location:`${A.metadata.jurisdiction||""}, ${A.metadata.state||""}`.trim().replace(/^,\s*/,""),contact:void 0}));p&&!v&&console.log("PeopleFinder Debug:",{effectiveState:h,defaultCity:m,totalResults:p.total_results,contactsReturned:((P=(E=p.results)==null?void 0:E.contacts)==null?void 0:P.length)||0,peopleConverted:x.length,sampleContact:(C=(O=p.results)==null?void 0:O.contacts)==null?void 0:C[0]});const y=Array.from(new Set(x.map(A=>A.organization).filter(Boolean))).sort();N.useEffect(()=>{m&&y.includes(m)&&i==="all"&&a(m)},[m,y,i]);const w=x.reduce((A,T)=>{const $=T.organization||"Unknown";return A[$]=(A[$]||0)+1,A},{}),b=x.filter(A=>{const T=e===""||A.name.toLowerCase().includes(e.toLowerCase())||A.organization.toLowerCase().includes(e.toLowerCase())||A.location.toLowerCase().includes(e.toLowerCase())||A.specificRole.toLowerCase().includes(e.toLowerCase()),$=n==="all"||A.role===n,z=i==="all"||A.organization===i;return T&&$&&z});return o.jsxs("div",{className:"p-8",children:[o.jsxs("div",{className:"mb-8",children:[o.jsx("h1",{className:"text-3xl font-bold mb-2",style:{color:"#354F52"},children:"Find Leaders"}),o.jsxs("p",{className:"text-gray-600",children:["Discover elected officials, decision makers, and community leaders",o.jsxs("span",{className:"font-medium text-primary-600",children:[" in ",h]}),!(u!=null&&u.state)&&o.jsx("span",{className:"text-sm text-gray-500 ml-2",children:"(default state - set your location to customize)"})]})]}),v&&o.jsx("div",{className:"flex justify-center items-center py-12",children:o.jsx("div",{className:"animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"})}),_&&o.jsxs("div",{className:"bg-red-50 border border-red-200 rounded-lg p-6 mb-8",children:[o.jsx("h3",{className:"text-red-800 font-semibold mb-2",children:"Error loading contacts"}),o.jsx("p",{className:"text-red-600 text-sm",children:_ instanceof Error?_.message:"Failed to load contacts from the API"}),o.jsx("button",{onClick:()=>window.location.reload(),className:"mt-3 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700",children:"Reload Page"})]}),!v&&!_&&o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"mb-6 space-y-4",children:[o.jsxs("div",{className:"relative",children:[o.jsx("input",{type:"text",placeholder:"Search by name, title, organization, or location...",value:e,onChange:A=>t(A.target.value),className:"w-full px-4 py-3 pl-12 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent text-gray-900"}),o.jsx(fn,{className:"absolute left-4 top-3.5 h-6 w-6 text-gray-400"})]}),o.jsxs("div",{className:"flex gap-4 items-center",children:[o.jsx("label",{htmlFor:"city-filter",className:"text-sm font-medium text-gray-700",children:"Filter by City/Jurisdiction:"}),o.jsxs("select",{id:"city-filter",value:i,onChange:A=>{a(A.target.value),c(1)},className:"px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-900",children:[o.jsxs("option",{value:"all",children:["All Cities (",x.length," officials)"]}),y.map(A=>o.jsxs("option",{value:A,children:[A," (",w[A]," officials)"]},A))]}),(i!=="all"||e!==""||n!=="all")&&o.jsx("button",{onClick:()=>{a("all"),t(""),r("all"),c(1)},className:"px-4 py-2 text-sm text-gray-600 hover:text-gray-900 underline",children:"Clear all filters"})]}),o.jsxs("div",{className:"flex items-center gap-4 text-sm text-gray-600",children:[o.jsxs("span",{children:["Showing ",b.length," of ",x.length," officials"]}),i!=="all"&&o.jsxs("span",{className:"font-medium text-primary-600",children:["• Filtered to ",i]}),o.jsxs("span",{className:"text-gray-400",children:["• Page ",s," • ",h]})]})]}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 mb-8",children:[o.jsx("button",{onClick:()=>r("all"),className:`p-4 rounded-lg border-2 transition-all ${n==="all"?"border-[#354F52] bg-[#354F52] bg-opacity-10":"border-gray-200 hover:border-gray-300"}`,children:o.jsxs("div",{className:"text-center",children:[o.jsx("div",{className:"font-semibold",style:{color:"#354F52"},children:"All People"}),o.jsxs("div",{className:"text-sm text-gray-500 mt-1",children:[x.length," total"]})]})}),Object.keys(ul).map(A=>{const T=ul[A],$=T.icon,z=x.filter(D=>D.role===A).length;return o.jsx("button",{onClick:()=>r(A),className:`p-4 rounded-lg border-2 transition-all ${n===A?"border-[#354F52] bg-[#354F52] bg-opacity-10":"border-gray-200 hover:border-gray-300"}`,children:o.jsxs("div",{className:"flex flex-col items-center text-center",children:[o.jsx($,{className:"h-8 w-8 mb-2",style:{color:T.color}}),o.jsx("div",{className:"font-semibold",style:{color:T.color},children:T.title}),o.jsxs("div",{className:"text-sm text-gray-500 mt-1",children:[z," people"]})]})},A)})]}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8",children:Object.keys(ul).map(A=>{const T=ul[A],$=T.icon;return o.jsxs("div",{className:"bg-white rounded-lg shadow-md p-6",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-3",children:[o.jsx($,{className:"h-6 w-6",style:{color:T.color}}),o.jsx("h3",{className:"text-lg font-semibold",style:{color:T.color},children:T.title})]}),o.jsx("p",{className:"text-sm text-gray-600 mb-3",children:T.subtitle}),o.jsx("ul",{className:"space-y-1",children:T.roles.map(z=>o.jsxs("li",{className:"text-sm text-gray-700",children:["• ",z]},z))})]},A)})}),o.jsxs("div",{children:[o.jsxs("div",{className:"flex justify-between items-center mb-4",children:[o.jsxs("h2",{className:"text-xl font-semibold",style:{color:"#354F52"},children:[n==="all"?"All People":ul[n].title,o.jsxs("span",{className:"text-gray-500 font-normal ml-2",children:["(",b.length," results)"]})]}),p&&p.pagination&&o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("button",{onClick:()=>c(A=>Math.max(1,A-1)),disabled:!p.pagination.has_prev,className:"px-3 py-1 border rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50",children:"← Prev"}),o.jsxs("span",{className:"text-sm text-gray-600",children:["Page ",s," of ",p.pagination.total_pages]}),o.jsx("button",{onClick:()=>c(A=>A+1),disabled:!p.pagination.has_next,className:"px-3 py-1 border rounded disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50",children:"Next →"})]})]}),b.length===0?o.jsx("div",{className:"bg-white rounded-lg shadow-md p-8 text-center",children:o.jsx("p",{className:"text-gray-500",children:"No people found matching your criteria"})}):o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6",children:b.map(A=>{const T=ul[A.role],$=T.icon;return o.jsxs("div",{className:"bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow",children:[o.jsxs("div",{className:"flex items-start gap-3 mb-3",children:[o.jsx($,{className:"h-6 w-6 mt-1",style:{color:T.color}}),o.jsxs("div",{className:"flex-1",children:[o.jsx("h3",{className:"font-semibold text-lg",style:{color:"#354F52"},children:A.name}),o.jsx("p",{className:"text-sm font-medium",style:{color:T.color},children:A.specificRole})]})]}),o.jsxs("div",{className:"space-y-2 text-sm mb-4",children:[o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-gray-700",children:A.role==="open-source"?"Repository:":"Organization:"}),A.role==="open-source"?o.jsx("a",{href:`https://github.com/${A.organization}`,target:"_blank",rel:"noopener noreferrer",className:"text-cyan-600 hover:underline block",children:A.organization}):o.jsx("p",{className:"text-gray-600",children:A.organization})]}),o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-gray-700",children:"Location:"}),o.jsx("p",{className:"text-gray-600",children:A.location})]}),A.contact&&o.jsxs("div",{children:[o.jsx("span",{className:"font-medium text-gray-700",children:"Contact:"}),A.role==="open-source"&&A.contact.startsWith("@")?o.jsx("a",{href:`https://github.com/${A.contact.substring(1)}`,target:"_blank",rel:"noopener noreferrer",className:"text-cyan-600 hover:underline block",children:A.contact}):o.jsx("p",{className:"text-gray-600",children:A.contact})]})]}),o.jsx("div",{className:"pt-4 border-t border-gray-100",children:o.jsx(km,{type:"leader",id:A.id,initialFollowing:!1,initialCount:Math.floor(Math.random()*500),compact:!0})})]},A.id)})})]})]})]})}function zge(){const[e,t]=N.useState(""),[n,r]=N.useState(""),[i,a]=N.useState(null),[s,c]=N.useState(!1),[u,d]=N.useState(""),h=async()=>{if(!e){d("Please enter some text to grade");return}c(!0),d("");try{const v=new URLSearchParams;v.set("text",e),n&&v.set("title",n);const _=await fetch(`/api/debate-grade?${v.toString()}`,{method:"POST"});if(!_.ok)throw new Error("Failed to grade decision");const x=await _.json();a(x.debate_grade)}catch(v){d(v instanceof Error?v.message:"An error occurred")}finally{c(!1)}},m=v=>{switch(v){case"excellent":return"text-green-600";case"good":return"text-blue-600";case"fair":return"text-yellow-600";case"weak":return"text-orange-600";case"missing":return"text-red-600";default:return"text-gray-600"}},p=v=>{switch(v){case"excellent":case"good":return o.jsx(ci,{className:"h-6 w-6 text-green-600"});case"fair":return o.jsx(OA,{className:"h-6 w-6 text-yellow-600"});case"weak":case"missing":return o.jsx(Sb,{className:"h-6 w-6 text-red-600"});default:return null}};return o.jsx("div",{className:"min-h-screen p-8",style:{backgroundColor:"#F1F5F9"},children:o.jsxs("div",{className:"max-w-5xl mx-auto space-y-6",children:[o.jsxs("div",{children:[o.jsx("h1",{className:"text-3xl font-bold mb-2",style:{color:"#354F52"},children:"Debate Finder"}),o.jsx("p",{className:"text-gray-600",children:"Evaluate government decisions using debate framework: Harms, Solvency, and Topicality"})]}),o.jsx("div",{className:"bg-white rounded-lg shadow-sm p-6",children:o.jsxs("div",{className:"space-y-4",children:[o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Decision Title (optional)"}),o.jsx("input",{type:"text",value:n,onChange:v=>r(v.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500",placeholder:"e.g., City Council approves dental screening program"})]}),o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Decision Text"}),o.jsx("textarea",{value:e,onChange:v=>t(v.target.value),className:"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500",rows:8,placeholder:"Paste the government decision, meeting minutes, or policy text here..."})]}),u&&o.jsx("div",{className:"bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded",children:u}),o.jsx("button",{onClick:h,disabled:s,className:"px-6 py-2 text-white rounded-md transition-colors",style:{backgroundColor:"#354F52"},onMouseEnter:v=>v.currentTarget.style.backgroundColor="#2e4346",onMouseLeave:v=>v.currentTarget.style.backgroundColor="#354F52",children:s?"Grading...":"Grade This Decision"})]})}),i&&o.jsxs("div",{className:"space-y-6",children:[o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6",children:[o.jsxs("h2",{className:"text-2xl font-bold mb-4",style:{color:"#354F52"},children:["Overall Grade: ",o.jsx("span",{className:m(i.overall.grade),children:i.overall.grade.toUpperCase()})]}),o.jsxs("p",{className:"text-gray-700 mb-2",children:["Score: ",i.overall.score,"/5"]}),o.jsx("p",{className:"text-gray-600",children:i.overall.summary})]}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-6",children:[o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6",children:[o.jsxs("div",{className:"flex items-center justify-between mb-4",children:[o.jsx("h3",{className:"text-xl font-semibold",style:{color:"#354F52"},children:i.dimensions.harms.layperson_label}),p(i.dimensions.harms.grade)]}),o.jsxs("p",{className:"text-sm text-gray-600 mb-3 italic",children:['"',i.dimensions.harms.layperson_question,'"']}),o.jsxs("div",{className:"mb-3",children:[o.jsx("span",{className:`text-lg font-bold ${m(i.dimensions.harms.grade)}`,children:i.dimensions.harms.grade.toUpperCase()}),o.jsxs("span",{className:"text-gray-500 ml-2",children:["(",i.dimensions.harms.score,"/5)"]})]}),o.jsx("p",{className:"text-sm text-gray-700",children:i.dimensions.harms.explanation})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6",children:[o.jsxs("div",{className:"flex items-center justify-between mb-4",children:[o.jsx("h3",{className:"text-xl font-semibold",style:{color:"#354F52"},children:i.dimensions.solvency.layperson_label}),p(i.dimensions.solvency.grade)]}),o.jsxs("p",{className:"text-sm text-gray-600 mb-3 italic",children:['"',i.dimensions.solvency.layperson_question,'"']}),o.jsxs("div",{className:"mb-3",children:[o.jsx("span",{className:`text-lg font-bold ${m(i.dimensions.solvency.grade)}`,children:i.dimensions.solvency.grade.toUpperCase()}),o.jsxs("span",{className:"text-gray-500 ml-2",children:["(",i.dimensions.solvency.score,"/5)"]})]}),o.jsx("p",{className:"text-sm text-gray-700",children:i.dimensions.solvency.explanation})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6",children:[o.jsxs("div",{className:"flex items-center justify-between mb-4",children:[o.jsx("h3",{className:"text-xl font-semibold",style:{color:"#354F52"},children:i.dimensions.topicality.layperson_label}),p(i.dimensions.topicality.grade)]}),o.jsxs("p",{className:"text-sm text-gray-600 mb-3 italic",children:['"',i.dimensions.topicality.layperson_question,'"']}),o.jsxs("div",{className:"mb-3",children:[o.jsx("span",{className:`text-lg font-bold ${m(i.dimensions.topicality.grade)}`,children:i.dimensions.topicality.grade.toUpperCase()}),o.jsxs("span",{className:"text-gray-500 ml-2",children:["(",i.dimensions.topicality.score,"/5)"]})]}),o.jsx("p",{className:"text-sm text-gray-700",children:i.dimensions.topicality.explanation})]})]}),o.jsxs("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-6",children:[o.jsx("h3",{className:"text-lg font-semibold text-blue-900 mb-3",children:"Understanding the Debate Framework"}),o.jsxs("div",{className:"space-y-2 text-sm text-blue-800",children:[o.jsxs("p",{children:[o.jsx("strong",{children:"The Problem (Harms):"})," Does the decision clearly explain the crisis or problem? Is there data to back it up? Who is affected?"]}),o.jsxs("p",{children:[o.jsx("strong",{children:"The Fix (Solvency):"})," Does the solution actually work? Is there a clear plan? Has it worked elsewhere?"]}),o.jsxs("p",{children:[o.jsx("strong",{children:"The Scope (Topicality):"})," Does the government body have the legal authority to do this? Is it within their jurisdiction?"]})]})]})]})]})})}function Uge({userId:e,showBreakdown:t=!1,clickable:n=!0}){const{data:r,isLoading:i}=zt({queryKey:["social","stats",e],queryFn:async()=>{const s=e?`?user_id=${e}`:"";return(await vt.get(`/social/stats${s}`)).data}});if(i)return o.jsxs("div",{className:"animate-pulse flex gap-6",children:[o.jsx("div",{className:"h-5 w-20 bg-gray-200 rounded"}),o.jsx("div",{className:"h-5 w-20 bg-gray-200 rounded"})]});if(!r)return null;const a=()=>o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"flex items-center gap-6 text-sm",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Ur,{className:"h-5 w-5",style:{color:"#52796F"}}),o.jsx("span",{className:"font-semibold",style:{color:"#354F52"},children:r.followers.toLocaleString()}),o.jsx("span",{className:"text-gray-600",children:r.followers===1?"Follower":"Followers"})]}),o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Nb,{className:"h-5 w-5",style:{color:"#52796F"}}),o.jsx("span",{className:"font-semibold",style:{color:"#354F52"},children:r.following.toLocaleString()}),o.jsx("span",{className:"text-gray-600",children:"Following"})]})]}),t&&r.following>0&&o.jsxs("div",{className:"mt-4 grid grid-cols-2 md:grid-cols-4 gap-4 text-sm",children:[r.following_leaders>0&&o.jsxs("div",{className:"bg-blue-50 rounded-lg p-3",children:[o.jsx("div",{className:"font-semibold text-blue-900",children:r.following_leaders}),o.jsx("div",{className:"text-blue-700 text-xs",children:"Leaders"})]}),r.following_organizations>0&&o.jsxs("div",{className:"bg-green-50 rounded-lg p-3",children:[o.jsx("div",{className:"font-semibold text-green-900",children:r.following_organizations}),o.jsx("div",{className:"text-green-700 text-xs",children:"Charities"})]}),r.following_causes>0&&o.jsxs("div",{className:"bg-purple-50 rounded-lg p-3",children:[o.jsx("div",{className:"font-semibold text-purple-900",children:r.following_causes}),o.jsx("div",{className:"text-purple-700 text-xs",children:"Causes"})]}),r.following_users>0&&o.jsxs("div",{className:"bg-orange-50 rounded-lg p-3",children:[o.jsx("div",{className:"font-semibold text-orange-900",children:r.following_users}),o.jsx("div",{className:"text-orange-700 text-xs",children:"People"})]})]})]});return n?o.jsx(ke,{to:"/profile/following",className:"hover:opacity-75 transition-opacity",children:o.jsx(a,{})}):o.jsx(a,{})}function Ky(...e){return e.filter(Boolean).join(" ")}function Wge(){const{user:e}=e0(),[t,n]=N.useState(0),{data:r}=zt({queryKey:["following","leaders"],queryFn:async()=>(await vt.get("/social/following/leaders")).data,enabled:!!e}),{data:i}=zt({queryKey:["following","organizations"],queryFn:async()=>(await vt.get("/social/following/organizations")).data,enabled:!!e}),{data:a}=zt({queryKey:["following","causes"],queryFn:async()=>(await vt.get("/social/following/causes")).data,enabled:!!e});return e?o.jsx("div",{className:"min-h-screen p-8",style:{backgroundColor:"#F1F5F9"},children:o.jsxs("div",{className:"max-w-6xl mx-auto",children:[o.jsx("div",{className:"bg-white rounded-lg shadow-md p-8 mb-6",children:o.jsxs("div",{className:"flex items-start gap-6",children:[o.jsx("div",{className:"flex-shrink-0",children:e.avatar_url?o.jsx("img",{src:e.avatar_url,alt:e.full_name||e.email,className:"h-24 w-24 rounded-full object-cover border-4 border-gray-200"}):o.jsx("div",{className:"h-24 w-24 rounded-full flex items-center justify-center text-white text-3xl font-bold",style:{backgroundColor:"#354F52"},children:(e.full_name||e.email).charAt(0).toUpperCase()})}),o.jsxs("div",{className:"flex-1",children:[o.jsx("h1",{className:"text-3xl font-bold mb-2",style:{color:"#354F52"},children:e.full_name||"Community Member"}),o.jsx("p",{className:"text-gray-600 mb-4",children:e.email}),e.city&&e.state&&o.jsxs("p",{className:"text-sm text-gray-500 mb-4",children:["📍 ",e.city,", ",e.state]}),o.jsx(Uge,{showBreakdown:!0,clickable:!1})]}),o.jsxs("a",{href:"/settings",className:"flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors",children:[o.jsx(Z_,{className:"h-5 w-5"}),o.jsx("span",{children:"Edit Profile"})]})]})}),o.jsxs("div",{className:"bg-white rounded-lg shadow-md p-6",children:[o.jsx("h2",{className:"text-2xl font-bold mb-6",style:{color:"#354F52"},children:"Following"}),o.jsxs(wt.Group,{selectedIndex:t,onChange:n,children:[o.jsxs(wt.List,{className:"flex gap-2 border-b border-gray-200 mb-6",children:[o.jsx(wt,{className:({selected:s})=>Ky("px-4 py-2 font-medium text-sm border-b-2 transition-colors",s?"border-[#354F52] text-[#354F52]":"border-transparent text-gray-500 hover:text-gray-700"),children:o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Li,{className:"h-5 w-5"}),"Leaders (",(r==null?void 0:r.length)||0,")"]})}),o.jsx(wt,{className:({selected:s})=>Ky("px-4 py-2 font-medium text-sm border-b-2 transition-colors",s?"border-[#354F52] text-[#354F52]":"border-transparent text-gray-500 hover:text-gray-700"),children:o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(dc,{className:"h-5 w-5"}),"Charities (",(i==null?void 0:i.length)||0,")"]})}),o.jsx(wt,{className:({selected:s})=>Ky("px-4 py-2 font-medium text-sm border-b-2 transition-colors",s?"border-[#354F52] text-[#354F52]":"border-transparent text-gray-500 hover:text-gray-700"),children:o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx($t,{className:"h-5 w-5"}),"Causes (",(a==null?void 0:a.length)||0,")"]})})]}),o.jsxs(wt.Panels,{children:[o.jsx(wt.Panel,{children:!r||r.length===0?o.jsxs("div",{className:"text-center py-12",children:[o.jsx(Li,{className:"h-16 w-16 mx-auto text-gray-300 mb-4"}),o.jsx("p",{className:"text-gray-500",children:"You're not following any leaders yet"}),o.jsx("a",{href:"/people",className:"inline-block mt-4 text-blue-600 hover:text-blue-700 font-medium",children:"Find leaders to follow →"})]}):o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-4",children:r.map(s=>o.jsxs("div",{className:"flex items-start gap-4 p-4 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors",children:[s.photo_url?o.jsx("img",{src:s.photo_url,alt:s.name,className:"h-16 w-16 rounded-full object-cover"}):o.jsx("div",{className:"h-16 w-16 rounded-full flex items-center justify-center text-white text-xl font-bold",style:{backgroundColor:"#354F52"},children:s.name.charAt(0)}),o.jsxs("div",{className:"flex-1",children:[o.jsxs("h3",{className:"font-semibold",style:{color:"#354F52"},children:[s.name,s.is_verified&&o.jsx("span",{className:"ml-1 text-blue-500",children:"✓"})]}),s.title&&o.jsx("p",{className:"text-sm text-gray-600",children:s.title}),s.office&&o.jsx("p",{className:"text-sm text-gray-500",children:s.office}),s.city&&s.state&&o.jsxs("p",{className:"text-xs text-gray-400 mt-1",children:[s.city,", ",s.state]}),o.jsxs("p",{className:"text-xs text-gray-500 mt-1",children:[s.follower_count.toLocaleString()," followers"]})]}),o.jsx(km,{type:"leader",id:s.id,initialFollowing:!0,initialCount:s.follower_count,showCount:!1,compact:!0})]},s.id))})}),o.jsx(wt.Panel,{children:!i||i.length===0?o.jsxs("div",{className:"text-center py-12",children:[o.jsx(dc,{className:"h-16 w-16 mx-auto text-gray-300 mb-4"}),o.jsx("p",{className:"text-gray-500",children:"You're not following any charities yet"}),o.jsx("a",{href:"/nonprofits",className:"inline-block mt-4 text-blue-600 hover:text-blue-700 font-medium",children:"Find charities to follow →"})]}):o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-4",children:i.map(s=>o.jsxs("div",{className:"flex items-start gap-4 p-4 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors",children:[s.logo_url?o.jsx("img",{src:s.logo_url,alt:s.name,className:"h-16 w-16 rounded object-contain"}):o.jsx("div",{className:"h-16 w-16 rounded flex items-center justify-center text-white text-xl font-bold",style:{backgroundColor:"#52796F"},children:s.name.charAt(0)}),o.jsxs("div",{className:"flex-1",children:[o.jsxs("h3",{className:"font-semibold",style:{color:"#354F52"},children:[s.name,s.is_verified&&o.jsx("span",{className:"ml-1 text-blue-500",children:"✓"})]}),s.org_type&&o.jsx("span",{className:"inline-block px-2 py-0.5 text-xs rounded-full bg-green-100 text-green-800 capitalize mb-1",children:s.org_type}),s.description&&o.jsx("p",{className:"text-sm text-gray-600 line-clamp-2",children:s.description}),s.city&&s.state&&o.jsxs("p",{className:"text-xs text-gray-400 mt-1",children:[s.city,", ",s.state]}),o.jsxs("p",{className:"text-xs text-gray-500 mt-1",children:[s.follower_count.toLocaleString()," followers"]})]}),o.jsx(km,{type:"organization",id:s.id,initialFollowing:!0,initialCount:s.follower_count,showCount:!1,compact:!0})]},s.id))})}),o.jsx(wt.Panel,{children:!a||a.length===0?o.jsxs("div",{className:"text-center py-12",children:[o.jsx($t,{className:"h-16 w-16 mx-auto text-gray-300 mb-4"}),o.jsx("p",{className:"text-gray-500",children:"You're not following any causes yet"}),o.jsx("a",{href:"/",className:"inline-block mt-4 text-blue-600 hover:text-blue-700 font-medium",children:"Explore causes →"})]}):o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-4",children:a.map(s=>o.jsxs("div",{className:"p-4 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-2",children:[s.icon_url?o.jsx("img",{src:s.icon_url,alt:s.name,className:"h-8 w-8"}):o.jsx("div",{className:"h-8 w-8 rounded-full",style:{backgroundColor:s.color||"#354F52"}}),o.jsx("h3",{className:"font-semibold flex-1",style:{color:"#354F52"},children:s.name})]}),s.description&&o.jsx("p",{className:"text-sm text-gray-600 mb-3",children:s.description}),s.category&&o.jsx("span",{className:"inline-block px-2 py-0.5 text-xs rounded-full bg-purple-100 text-purple-800 capitalize mb-2",children:s.category}),o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsxs("p",{className:"text-xs text-gray-500",children:[s.follower_count.toLocaleString()," followers"]}),o.jsx(km,{type:"cause",id:s.id,initialFollowing:!0,initialCount:s.follower_count,showCount:!1,compact:!0})]})]},s.id))})})]})]})]})]})}):o.jsx("div",{className:"min-h-screen flex items-center justify-center",style:{backgroundColor:"#F1F5F9"},children:o.jsxs("div",{className:"text-center",children:[o.jsx("h2",{className:"text-2xl font-bold mb-4",style:{color:"#354F52"},children:"Please sign in"}),o.jsx("p",{className:"text-gray-600",children:"You need to be signed in to view your profile"})]})})}const Hge=[{title:"Policy Decisions",description:"Track decisions, budget deltas, stakeholder positions, and deferral patterns across government meetings.",icon:Ws,path:"/documents",color:"#354F52",stats:"500K+ meeting pages"},{title:"Budget Analysis",description:"Explore city, county, and school budgets with budget-to-minutes delta analysis showing rhetoric vs. reality.",icon:fc,path:"/analytics",color:"#52796F",stats:"Real-time tracking"},{title:"Elected Officials",description:"Find local, state, and school board officials with voting records and decision patterns.",icon:Ur,path:"/people",color:"#84A98C",stats:"100K+ officials"},{title:"Policy Map",description:"Track state legislation and bills across all sessions. Search and filter 13,000+ bills by topic, session, and status.",icon:la,path:"/policy-map",color:"#CAD2C5",stats:"13K+ bills"}],Vge=[{title:"Nonprofits & Churches",description:"Search 43,726 nonprofits including 4,372 churches with financial data from 5 states.",icon:Ci,path:"/nonprofits",color:"#354F52",stats:"43,726 organizations"},{title:"Advocacy Topics",description:"Track what your community is discussing. Find advocacy alerts and engagement opportunities.",icon:Hd,path:"/advocacy-topics",color:"#52796F",stats:"Get involved"},{title:"Grants & Funding",description:"Discover government grants, foundation funding, and program delivery outcomes for nonprofits.",icon:SA,path:"/analytics",color:"#84A98C",stats:"Find funding"},{title:"Fact-Checking",description:"Verify claims from meetings and legislation with PolitiFact, FactCheck.org, and Google Fact Check data.",icon:_b,path:"/fact-checking",color:"#CAD2C5",stats:"Verified claims"}],qge=[{title:"Open Source Projects",description:"Contribute to civic tech, data pipelines, AI models, and open government tools.",icon:$r,path:"/opensource",color:"#354F52",stats:"Join the community"},{title:"Hackathons for Good",description:"Build solutions for civic engagement, transparency, and community empowerment at our quarterly hackathons.",icon:jo,path:"/hackathons",color:"#52796F",stats:"Make an impact"}],Zge=[{title:"Community Events",description:"Discover local government meetings, public hearings, town halls, and community events you can attend.",icon:ha,path:"/events",color:"#354F52",stats:"Attend & engage"},{title:"Training & Services",description:"Find community programs, educational workshops, health services, and family support resources.",icon:Eo,path:"/services",color:"#52796F",stats:"Learn & grow"},{title:"Voter Registration",description:"Register to vote, find your polling place, check registration status, and learn about candidates.",icon:SB,path:"/analytics?topic=elections",color:"#84A98C",stats:"Make your voice heard"},{title:"Contact Your Representatives",description:"Find contact information for elected officials, city council members, and school board representatives.",icon:EA,path:"/people?view=contact",color:"#CAD2C5",stats:"100K+ officials"},{title:"Submit Feedback",description:"Provide public comments, share concerns, and participate in community decision-making processes.",icon:jB,path:"/opportunities?type=feedback",color:"#52796F",stats:"Be heard"},{title:"Community Resources",description:"Access food banks, housing assistance, healthcare, childcare, and other family support services.",icon:$t,path:"/nonprofits?category=family-services",color:"#84A98C",stats:"Get help"}];function Gge(){return N.useEffect(()=>{window.scrollTo({top:0,behavior:"smooth"})},[]),o.jsxs("div",{className:"min-h-screen bg-gradient-to-br from-gray-50 to-gray-100",children:[o.jsx("div",{className:"bg-white shadow-sm",children:o.jsx("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8",children:o.jsxs("div",{className:"text-center",children:[o.jsx("h1",{className:"text-4xl font-bold text-gray-900 mb-3",children:"Explore Open Navigator Data"}),o.jsx("p",{className:"text-xl text-gray-600 max-w-3xl mx-auto",children:"Access comprehensive data on government decisions, budgets, demographics, nonprofits, and community engagement."})]})})}),o.jsxs("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8",children:[o.jsxs("div",{className:"mb-8",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-2",children:"For Families & Individuals"}),o.jsx("p",{className:"text-gray-600",children:"Events, training, services, voter registration, and ways to engage with your community."})]}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12",children:Zge.map(e=>{const t=e.icon;return o.jsxs(ke,{to:e.path,className:"group bg-white rounded-xl shadow-md hover:shadow-xl transition-all duration-300 overflow-hidden border border-gray-100 hover:border-gray-200",children:[o.jsxs("div",{className:"p-6",children:[o.jsx("div",{className:"w-14 h-14 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300",style:{backgroundColor:`${e.color}15`},children:o.jsx("div",{style:{color:e.color},children:o.jsx(t,{className:"h-7 w-7"})})}),o.jsxs("div",{className:"mb-3",children:[o.jsx("h3",{className:"text-xl font-bold text-gray-900 mb-1 group-hover:text-[#354F52] transition-colors",children:e.title}),e.stats&&o.jsx("p",{className:"text-sm font-medium",style:{color:e.color},children:e.stats})]}),o.jsx("p",{className:"text-gray-600 text-sm leading-relaxed",children:e.description}),o.jsx("div",{className:"mt-4 flex items-center text-sm font-medium",style:{color:e.color},children:o.jsx("span",{className:"group-hover:translate-x-1 transition-transform duration-300",children:"Explore →"})})]}),o.jsx("div",{className:"h-1 w-full transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300 origin-left",style:{backgroundColor:e.color}})]},e.path)})}),o.jsxs("div",{className:"mb-8 mt-16",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-2",children:"For Policy Makers & Government"}),o.jsx("p",{className:"text-gray-600",children:"Track decisions, budgets, officials, and demographic data across 90,000+ jurisdictions."})]}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12",children:Hge.map(e=>{const t=e.icon;return o.jsxs(ke,{to:e.path,className:"group bg-white rounded-xl shadow-md hover:shadow-xl transition-all duration-300 overflow-hidden border border-gray-100 hover:border-gray-200",children:[o.jsxs("div",{className:"p-6",children:[o.jsx("div",{className:"w-14 h-14 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300",style:{backgroundColor:`${e.color}15`},children:o.jsx("div",{style:{color:e.color},children:o.jsx(t,{className:"h-7 w-7"})})}),o.jsxs("div",{className:"mb-3",children:[o.jsx("h3",{className:"text-xl font-bold text-gray-900 mb-1 group-hover:text-[#354F52] transition-colors",children:e.title}),e.stats&&o.jsx("p",{className:"text-sm font-medium",style:{color:e.color},children:e.stats})]}),o.jsx("p",{className:"text-gray-600 text-sm leading-relaxed",children:e.description}),o.jsx("div",{className:"mt-4 flex items-center text-sm font-medium",style:{color:e.color},children:o.jsx("span",{className:"group-hover:translate-x-1 transition-transform duration-300",children:"Explore →"})})]}),o.jsx("div",{className:"h-1 w-full transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300 origin-left",style:{backgroundColor:e.color}})]},e.path)})}),o.jsxs("div",{className:"mb-8 mt-16",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-2",children:"For Advocates & Community Members"}),o.jsx("p",{className:"text-gray-600",children:"Find nonprofits, advocacy topics, funding, and fact-checked information."})]}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6",children:Vge.map(e=>{const t=e.icon;return o.jsxs(ke,{to:e.path,className:"group bg-white rounded-xl shadow-md hover:shadow-xl transition-all duration-300 overflow-hidden border border-gray-100 hover:border-gray-200",children:[o.jsxs("div",{className:"p-6",children:[o.jsx("div",{className:"w-14 h-14 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300",style:{backgroundColor:`${e.color}15`},children:o.jsx("div",{style:{color:e.color},children:o.jsx(t,{className:"h-7 w-7"})})}),o.jsxs("div",{className:"mb-3",children:[o.jsx("h3",{className:"text-xl font-bold text-gray-900 mb-1 group-hover:text-[#354F52] transition-colors",children:e.title}),e.stats&&o.jsx("p",{className:"text-sm font-medium",style:{color:e.color},children:e.stats})]}),o.jsx("p",{className:"text-gray-600 text-sm leading-relaxed",children:e.description}),o.jsx("div",{className:"mt-4 flex items-center text-sm font-medium",style:{color:e.color},children:o.jsx("span",{className:"group-hover:translate-x-1 transition-transform duration-300",children:"Explore →"})})]}),o.jsx("div",{className:"h-1 w-full transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300 origin-left",style:{backgroundColor:e.color}})]},e.path)})}),o.jsxs("div",{className:"mb-8 mt-16",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-2",children:"For Developers & Civic Tech"}),o.jsx("p",{className:"text-gray-600",children:"Build with open data, contribute to open source, and join hackathons for social good."})]}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-6",children:qge.map(e=>{const t=e.icon,n=e.path.startsWith("http"),r=o.jsxs("div",{className:"group bg-white rounded-xl shadow-md hover:shadow-xl transition-all duration-300 overflow-hidden border border-gray-100 hover:border-gray-200",children:[o.jsxs("div",{className:"p-6",children:[o.jsx("div",{className:"w-14 h-14 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition-transform duration-300",style:{backgroundColor:`${e.color}15`},children:o.jsx("div",{style:{color:e.color},children:o.jsx(t,{className:"h-7 w-7"})})}),o.jsxs("div",{className:"mb-3",children:[o.jsx("h3",{className:"text-xl font-bold text-gray-900 mb-1 group-hover:text-[#354F52] transition-colors",children:e.title}),e.stats&&o.jsx("p",{className:"text-sm font-medium",style:{color:e.color},children:e.stats})]}),o.jsx("p",{className:"text-gray-600 text-sm leading-relaxed",children:e.description}),o.jsx("div",{className:"mt-4 flex items-center text-sm font-medium",style:{color:e.color},children:o.jsx("span",{className:"group-hover:translate-x-1 transition-transform duration-300",children:n?"View on GitHub →":"Explore →"})})]}),o.jsx("div",{className:"h-1 w-full transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300 origin-left",style:{backgroundColor:e.color}})]});return n?o.jsx("a",{href:e.path,target:"_blank",rel:"noopener noreferrer",children:r},e.path):o.jsx(ke,{to:e.path,children:r},e.path)})}),o.jsx("div",{className:"mt-16 text-center",children:o.jsxs("div",{className:"bg-white rounded-xl shadow-md p-8 max-w-2xl mx-auto border border-gray-100",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-3",children:"Access the Complete Dataset"}),o.jsx("p",{className:"text-gray-600 mb-6",children:"All data is available on HuggingFace with full documentation, APIs, and CSV/Parquet downloads."}),o.jsxs("div",{className:"flex flex-col sm:flex-row gap-4 justify-center",children:[o.jsx("a",{href:"https://huggingface.co/datasets/CommunityOne/open-navigator-data",target:"_blank",rel:"noopener noreferrer",className:"px-6 py-3 rounded-lg text-white font-semibold hover:shadow-lg transition-all",style:{backgroundColor:"#354F52"},children:"🤗 View on HuggingFace"}),o.jsx("a",{href:"https://www.communityone.com/docs/data-sources/data-model-erd",target:"_blank",rel:"noopener noreferrer",className:"px-6 py-3 rounded-lg font-semibold hover:shadow-lg transition-all border-2",style:{borderColor:"#354F52",color:"#354F52"},children:"📊 Explore Data Model"})]})]})})]}),o.jsx("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-12 text-center",children:o.jsx(ke,{to:"/",className:"inline-flex items-center text-gray-600 hover:text-gray-900 font-medium transition-colors",children:"← Back to Home"})})]})}function Kge(){return o.jsxs("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8",children:[o.jsx("div",{className:"mb-8",children:o.jsxs("div",{className:"flex items-center gap-3 mb-3",children:[o.jsx("div",{className:"p-3 bg-primary-50 rounded-lg",children:o.jsx(ha,{className:"h-8 w-8 text-primary-600"})}),o.jsxs("div",{children:[o.jsx("h1",{className:"text-3xl font-bold text-gray-900",children:"Community Events"}),o.jsx("p",{className:"text-gray-600 mt-1",children:"Discover upcoming public meetings, community gatherings, and civic events"})]})]})}),o.jsx("div",{className:"bg-blue-50 border border-blue-200 rounded-xl p-8 mb-8",children:o.jsxs("div",{className:"flex items-start gap-4",children:[o.jsx("div",{className:"flex-shrink-0",children:o.jsx(ha,{className:"h-12 w-12 text-blue-600"})}),o.jsxs("div",{children:[o.jsx("h2",{className:"text-2xl font-bold text-blue-900 mb-2",children:"Coming Soon!"}),o.jsx("p",{className:"text-blue-800 mb-4",children:"We're building a comprehensive events calendar to help you stay engaged with your community."}),o.jsxs("div",{className:"space-y-2 text-blue-700",children:[o.jsx("h3",{className:"font-semibold",children:"What you'll find here:"}),o.jsxs("ul",{className:"list-disc list-inside space-y-1 ml-2",children:[o.jsx("li",{children:"City council meetings, school board sessions, and public hearings"}),o.jsx("li",{children:"Community forums, town halls, and neighborhood gatherings"}),o.jsx("li",{children:"Voter registration drives and civic engagement events"}),o.jsx("li",{children:"Training workshops and educational programs"})]})]})]})]})}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-6",children:[o.jsxs("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200 opacity-60",children:[o.jsxs("div",{className:"flex items-start justify-between mb-4",children:[o.jsx("h3",{className:"text-xl font-bold text-gray-900",children:"City Council Meeting"}),o.jsx("span",{className:"px-3 py-1 bg-green-100 text-green-800 text-xs font-semibold rounded-full",children:"Upcoming"})]}),o.jsxs("div",{className:"space-y-3 text-gray-600",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(j5,{className:"h-5 w-5 text-gray-400"}),o.jsx("span",{children:"Tuesday, May 5, 2026 at 6:00 PM"})]}),o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Rn,{className:"h-5 w-5 text-gray-400"}),o.jsx("span",{children:"City Hall, Main Chamber"})]}),o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Ur,{className:"h-5 w-5 text-gray-400"}),o.jsx("span",{children:"Open to the public"})]})]}),o.jsx("div",{className:"mt-4 pt-4 border-t border-gray-200",children:o.jsx("p",{className:"text-sm text-gray-600",children:"Budget discussions, zoning changes, and community feedback session"})})]}),o.jsxs("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200 opacity-60",children:[o.jsxs("div",{className:"flex items-start justify-between mb-4",children:[o.jsx("h3",{className:"text-xl font-bold text-gray-900",children:"Community Health Fair"}),o.jsx("span",{className:"px-3 py-1 bg-purple-100 text-purple-800 text-xs font-semibold rounded-full",children:"Registration Open"})]}),o.jsxs("div",{className:"space-y-3 text-gray-600",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(j5,{className:"h-5 w-5 text-gray-400"}),o.jsx("span",{children:"Saturday, May 10, 2026 at 10:00 AM"})]}),o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Rn,{className:"h-5 w-5 text-gray-400"}),o.jsx("span",{children:"Community Center, 123 Main St"})]}),o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Ur,{className:"h-5 w-5 text-gray-400"}),o.jsx("span",{children:"Free for families"})]})]}),o.jsx("div",{className:"mt-4 pt-4 border-t border-gray-200",children:o.jsx("p",{className:"text-sm text-gray-600",children:"Free dental screenings, health resources, and family activities"})})]})]}),o.jsxs("div",{className:"mt-8 text-center",children:[o.jsx("p",{className:"text-gray-600 mb-4",children:"In the meantime, browse upcoming meetings:"}),o.jsx("a",{href:"/documents?filter=upcoming",className:"inline-flex items-center px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors font-semibold",children:"View Meeting Calendar →"})]})]})}function Yge(){return N.useEffect(()=>{window.scrollTo({top:0,behavior:"smooth"})},[]),o.jsxs("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8",children:[o.jsx("div",{className:"mb-8",children:o.jsxs("div",{className:"flex items-center gap-3 mb-3",children:[o.jsx("div",{className:"p-3 bg-primary-50 rounded-lg",children:o.jsx($t,{className:"h-8 w-8 text-primary-600"})}),o.jsxs("div",{children:[o.jsx("h1",{className:"text-3xl font-bold text-gray-900",children:"Services & Resources"}),o.jsx("p",{className:"text-gray-600 mt-1",children:"Find family services, social programs, and community support resources"})]})]})}),o.jsx("div",{className:"bg-purple-50 border border-purple-200 rounded-xl p-8 mb-8",children:o.jsxs("div",{className:"flex items-start gap-4",children:[o.jsx("div",{className:"flex-shrink-0",children:o.jsx($t,{className:"h-12 w-12 text-purple-600"})}),o.jsxs("div",{children:[o.jsx("h2",{className:"text-2xl font-bold text-purple-900 mb-2",children:"Coming Soon!"}),o.jsx("p",{className:"text-purple-800 mb-4",children:"We're creating a comprehensive directory of family services and community resources."}),o.jsxs("div",{className:"space-y-2 text-purple-700",children:[o.jsx("h3",{className:"font-semibold",children:"What you'll find here:"}),o.jsxs("ul",{className:"list-disc list-inside space-y-1 ml-2",children:[o.jsx("li",{children:"Healthcare services including dental clinics and mental health support"}),o.jsx("li",{children:"Educational programs, tutoring, and after-school activities"}),o.jsx("li",{children:"Food assistance, housing support, and financial aid programs"}),o.jsx("li",{children:"Legal aid, translation services, and crisis hotlines"}),o.jsx("li",{children:"Parks & recreation programs, dog parks, and community facilities"}),o.jsx("li",{children:"Senior services, youth activities, and family programs"})]})]})]})]})}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6",children:[o.jsxs("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200 opacity-60",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-4",children:[o.jsx("div",{className:"p-2 bg-red-100 rounded-lg",children:o.jsx($t,{className:"h-6 w-6 text-red-600"})}),o.jsx("h3",{className:"text-lg font-bold text-gray-900",children:"Health Services"})]}),o.jsxs("ul",{className:"space-y-2 text-sm text-gray-600",children:[o.jsx("li",{children:"• Free dental clinics"}),o.jsx("li",{children:"• Community health centers"}),o.jsx("li",{children:"• Mental health counseling"}),o.jsx("li",{children:"• Vision screening programs"})]})]}),o.jsxs("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200 opacity-60",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-4",children:[o.jsx("div",{className:"p-2 bg-blue-100 rounded-lg",children:o.jsx(Eo,{className:"h-6 w-6 text-blue-600"})}),o.jsx("h3",{className:"text-lg font-bold text-gray-900",children:"Education"})]}),o.jsxs("ul",{className:"space-y-2 text-sm text-gray-600",children:[o.jsx("li",{children:"• After-school programs"}),o.jsx("li",{children:"• Tutoring services"}),o.jsx("li",{children:"• Adult education"}),o.jsx("li",{children:"• STEM workshops"})]})]}),o.jsxs("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200 opacity-60",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-4",children:[o.jsx("div",{className:"p-2 bg-green-100 rounded-lg",children:o.jsx(G_,{className:"h-6 w-6 text-green-600"})}),o.jsx("h3",{className:"text-lg font-bold text-gray-900",children:"Basic Needs"})]}),o.jsxs("ul",{className:"space-y-2 text-sm text-gray-600",children:[o.jsx("li",{children:"• Food pantries"}),o.jsx("li",{children:"• Housing assistance"}),o.jsx("li",{children:"• Utility support"}),o.jsx("li",{children:"• Emergency shelters"})]})]}),o.jsxs("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200 opacity-60",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-4",children:[o.jsx("div",{className:"p-2 bg-emerald-100 rounded-lg",children:o.jsx(la,{className:"h-6 w-6 text-emerald-600"})}),o.jsx("h3",{className:"text-lg font-bold text-gray-900",children:"Parks & Recreation"})]}),o.jsxs("ul",{className:"space-y-2 text-sm text-gray-600",children:[o.jsx("li",{children:"• Community parks"}),o.jsx("li",{children:"• Dog parks & pet areas"}),o.jsx("li",{children:"• Sports facilities"}),o.jsx("li",{children:"• Recreation programs"})]})]})]}),o.jsxs("div",{className:"mt-8 bg-gradient-to-r from-red-500 to-red-600 rounded-xl p-6 text-white",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-3",children:[o.jsx(EA,{className:"h-8 w-8"}),o.jsx("h2",{className:"text-2xl font-bold",children:"Emergency Resources"})]}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-4 text-sm",children:[o.jsxs("div",{children:[o.jsx("div",{className:"font-semibold mb-1",children:"Crisis Hotline"}),o.jsx("div",{children:"988 - Suicide & Crisis Lifeline"})]}),o.jsxs("div",{children:[o.jsx("div",{className:"font-semibold mb-1",children:"Domestic Violence"}),o.jsx("div",{children:"1-800-799-7233 (SAFE)"})]}),o.jsxs("div",{children:[o.jsx("div",{className:"font-semibold mb-1",children:"Child Abuse"}),o.jsx("div",{children:"1-800-422-4453"})]})]})]}),o.jsxs("div",{className:"mt-8 text-center",children:[o.jsx("p",{className:"text-gray-600 mb-4",children:"Browse nonprofit organizations offering services:"}),o.jsx("a",{href:"/nonprofits?category=family-services",className:"inline-flex items-center px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors font-semibold",children:"Explore Nonprofits →"})]})]})}function Xge(){return o.jsxs("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8",children:[o.jsx("div",{className:"mb-8",children:o.jsxs("div",{className:"flex items-center gap-3 mb-3",children:[o.jsx("div",{className:"p-3 bg-primary-50 rounded-lg",children:o.jsx($r,{className:"h-8 w-8 text-primary-600"})}),o.jsxs("div",{children:[o.jsx("h1",{className:"text-3xl font-bold text-gray-900",children:"Developers & Civic Tech"}),o.jsx("p",{className:"text-gray-600 mt-1",children:"Build civic tech solutions with open data and open source tools"})]})]})}),o.jsxs("div",{className:"bg-gradient-to-r from-primary-600 to-primary-700 rounded-xl p-8 mb-8 text-white",children:[o.jsx("h2",{className:"text-3xl font-bold mb-4",children:"Build the Future of Civic Engagement"}),o.jsx("p",{className:"text-xl mb-6 text-primary-50",children:"Open Navigator is 100% open source. Join developers worldwide building tools for transparency, accountability, and community empowerment."}),o.jsxs("div",{className:"flex flex-wrap gap-4",children:[o.jsxs("a",{href:"https://github.com/getcommunityone/open-navigator-for-engagement",target:"_blank",rel:"noopener noreferrer",className:"px-6 py-3 bg-white text-primary-700 rounded-lg hover:bg-primary-50 transition-colors font-semibold flex items-center gap-2",children:[o.jsx($r,{className:"h-5 w-5"}),"View on GitHub"]}),o.jsxs("a",{href:"/hackathons",className:"px-6 py-3 bg-primary-500 text-white rounded-lg hover:bg-primary-400 transition-colors font-semibold border-2 border-white flex items-center gap-2",children:[o.jsx(jo,{className:"h-5 w-5"}),"Join Hackathons"]})]})]}),o.jsxs("div",{className:"mb-8",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-4",children:"Tech Stack"}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-6",children:[o.jsxs("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-4",children:[o.jsx("div",{className:"p-2 bg-blue-100 rounded-lg",children:o.jsx(RB,{className:"h-6 w-6 text-blue-600"})}),o.jsx("h3",{className:"text-lg font-bold text-gray-900",children:"Frontend"})]}),o.jsxs("ul",{className:"space-y-2 text-sm text-gray-600",children:[o.jsx("li",{children:"• React 18 + TypeScript"}),o.jsx("li",{children:"• Vite + Tailwind CSS"}),o.jsx("li",{children:"• TanStack Query"}),o.jsx("li",{children:"• Recharts for visualizations"})]})]}),o.jsxs("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-4",children:[o.jsx("div",{className:"p-2 bg-green-100 rounded-lg",children:o.jsx(mz,{className:"h-6 w-6 text-green-600"})}),o.jsx("h3",{className:"text-lg font-bold text-gray-900",children:"Backend"})]}),o.jsxs("ul",{className:"space-y-2 text-sm text-gray-600",children:[o.jsx("li",{children:"• Python FastAPI"}),o.jsx("li",{children:"• LangChain + LangGraph"}),o.jsx("li",{children:"• OpenAI, Anthropic APIs"}),o.jsx("li",{children:"• Supabase Auth"})]})]}),o.jsxs("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-4",children:[o.jsx("div",{className:"p-2 bg-purple-100 rounded-lg",children:o.jsx(_5,{className:"h-6 w-6 text-purple-600"})}),o.jsx("h3",{className:"text-lg font-bold text-gray-900",children:"Data"})]}),o.jsxs("ul",{className:"space-y-2 text-sm text-gray-600",children:[o.jsx("li",{children:"• 925 jurisdictions"}),o.jsx("li",{children:"• 43,726 nonprofits"}),o.jsx("li",{children:"• 6,913 meeting pages"}),o.jsx("li",{children:"• Medallion architecture"})]})]})]})]}),o.jsxs("div",{className:"mb-8",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-4",children:"Get Started"}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-4",children:[o.jsxs("a",{href:"https://github.com/getcommunityone/open-navigator-for-engagement#readme",target:"_blank",rel:"noopener noreferrer",className:"flex items-start gap-4 p-6 bg-white rounded-xl shadow-md border border-gray-200 hover:border-primary-300 transition-colors group",children:[o.jsx(Zf,{className:"h-8 w-8 text-primary-600 flex-shrink-0"}),o.jsxs("div",{children:[o.jsx("h3",{className:"text-lg font-bold text-gray-900 mb-1 group-hover:text-primary-600",children:"Documentation"}),o.jsx("p",{className:"text-sm text-gray-600",children:"Setup guides, API docs, and architecture overview"})]})]}),o.jsxs("a",{href:"https://huggingface.co/datasets/CommunityOne/open-navigator-data",target:"_blank",rel:"noopener noreferrer",className:"flex items-start gap-4 p-6 bg-white rounded-xl shadow-md border border-gray-200 hover:border-primary-300 transition-colors group",children:[o.jsx(_5,{className:"h-8 w-8 text-primary-600 flex-shrink-0"}),o.jsxs("div",{children:[o.jsx("h3",{className:"text-lg font-bold text-gray-900 mb-1 group-hover:text-primary-600",children:"Open Datasets"}),o.jsx("p",{className:"text-sm text-gray-600",children:"Download CSV/Parquet files from HuggingFace"})]})]}),o.jsxs("a",{href:"https://github.com/getcommunityone/open-navigator-for-engagement/issues",target:"_blank",rel:"noopener noreferrer",className:"flex items-start gap-4 p-6 bg-white rounded-xl shadow-md border border-gray-200 hover:border-primary-300 transition-colors group",children:[o.jsx($r,{className:"h-8 w-8 text-primary-600 flex-shrink-0"}),o.jsxs("div",{children:[o.jsx("h3",{className:"text-lg font-bold text-gray-900 mb-1 group-hover:text-primary-600",children:"Contribute Code"}),o.jsx("p",{className:"text-sm text-gray-600",children:"Pick an issue, submit a PR, or propose new features"})]})]}),o.jsxs("a",{href:"/hackathons",className:"flex items-start gap-4 p-6 bg-white rounded-xl shadow-md border border-gray-200 hover:border-primary-300 transition-colors group",children:[o.jsx(jo,{className:"h-8 w-8 text-primary-600 flex-shrink-0"}),o.jsxs("div",{children:[o.jsx("h3",{className:"text-lg font-bold text-gray-900 mb-1 group-hover:text-primary-600",children:"Join Hackathons"}),o.jsx("p",{className:"text-sm text-gray-600",children:"Build solutions at our quarterly civic tech hackathons"})]})]})]})]}),o.jsxs("div",{className:"bg-gray-900 rounded-xl p-6 text-white",children:[o.jsxs("h2",{className:"text-xl font-bold mb-4 flex items-center gap-2",children:[o.jsx($r,{className:"h-6 w-6"}),"API Example"]}),o.jsx("pre",{className:"bg-gray-800 rounded-lg p-4 overflow-x-auto text-sm",children:o.jsx("code",{children:`# Search meeting transcripts +import requests + +response = requests.get( + 'http://localhost:8000/api/search/', + params={ + 'q': 'dental health', + 'state': 'AL', + 'limit': 10 + } +) + +for result in response.json()['results']: + print(f"{result['title']} - {result['date']}") + print(f"Score: {result['score']}") + print(result['snippet']) + print()`})})]})]})}function Qge(){return o.jsxs("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8",children:[o.jsx("div",{className:"mb-8",children:o.jsxs("div",{className:"flex items-center gap-3 mb-3",children:[o.jsx("div",{className:"p-3 bg-primary-50 rounded-lg",children:o.jsx(jo,{className:"h-8 w-8 text-primary-600"})}),o.jsxs("div",{children:[o.jsx("h1",{className:"text-3xl font-bold text-gray-900",children:"Hackathons for Good"}),o.jsx("p",{className:"text-gray-600 mt-1",children:"Build civic tech solutions that empower communities and promote transparency"})]})]})}),o.jsx("div",{className:"bg-gradient-to-r from-purple-600 to-pink-600 rounded-xl p-8 mb-8 text-white",children:o.jsxs("div",{className:"flex items-start gap-4",children:[o.jsx("div",{className:"flex-shrink-0",children:o.jsx(jo,{className:"h-12 w-12"})}),o.jsxs("div",{children:[o.jsx("h2",{className:"text-3xl font-bold mb-2",children:"Quarterly Civic Tech Hackathons"}),o.jsx("p",{className:"text-lg mb-4 text-purple-50",children:"Join developers, designers, and civic advocates to build tools for social impact."}),o.jsxs("div",{className:"inline-flex items-center gap-2 px-4 py-2 bg-white text-purple-700 rounded-lg font-semibold",children:[o.jsx(ha,{className:"h-5 w-5"}),"Next Event: Coming Soon!"]})]})]})}),o.jsxs("div",{className:"mb-8",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-6",children:"Why Participate?"}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-6",children:[o.jsxs("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-4",children:[o.jsx("div",{className:"p-2 bg-blue-100 rounded-lg",children:o.jsx(JB,{className:"h-6 w-6 text-blue-600"})}),o.jsx("h3",{className:"text-lg font-bold text-gray-900",children:"Real Impact"})]}),o.jsx("p",{className:"text-gray-600 text-sm",children:"Build tools that help communities access services, hold government accountable, and participate in democracy."})]}),o.jsxs("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-4",children:[o.jsx("div",{className:"p-2 bg-green-100 rounded-lg",children:o.jsx(Ur,{className:"h-6 w-6 text-green-600"})}),o.jsx("h3",{className:"text-lg font-bold text-gray-900",children:"Collaboration"})]}),o.jsx("p",{className:"text-gray-600 text-sm",children:"Work alongside civic advocates, policy experts, and fellow developers to solve real problems."})]}),o.jsxs("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-4",children:[o.jsx("div",{className:"p-2 bg-purple-100 rounded-lg",children:o.jsx(_z,{className:"h-6 w-6 text-purple-600"})}),o.jsx("h3",{className:"text-lg font-bold text-gray-900",children:"Recognition"})]}),o.jsx("p",{className:"text-gray-600 text-sm",children:"Win prizes, get featured in our showcase, and add meaningful projects to your portfolio."})]})]})]}),o.jsxs("div",{className:"mb-8",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-4",children:"Challenge Tracks"}),o.jsxs("div",{className:"space-y-4",children:[o.jsx("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200",children:o.jsxs("div",{className:"flex items-start gap-4",children:[o.jsx("div",{className:"p-3 bg-blue-100 rounded-lg",children:o.jsx($r,{className:"h-8 w-8 text-blue-600"})}),o.jsxs("div",{className:"flex-1",children:[o.jsx("h3",{className:"text-xl font-bold text-gray-900 mb-2",children:"Data Visualization & Dashboards"}),o.jsx("p",{className:"text-gray-600 mb-3",children:"Create interactive visualizations that make government data accessible to everyday citizens."}),o.jsxs("div",{className:"flex flex-wrap gap-2",children:[o.jsx("span",{className:"px-3 py-1 bg-blue-50 text-blue-700 text-xs font-semibold rounded-full",children:"React"}),o.jsx("span",{className:"px-3 py-1 bg-blue-50 text-blue-700 text-xs font-semibold rounded-full",children:"D3.js"}),o.jsx("span",{className:"px-3 py-1 bg-blue-50 text-blue-700 text-xs font-semibold rounded-full",children:"Data Analysis"})]})]})]})}),o.jsx("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200",children:o.jsxs("div",{className:"flex items-start gap-4",children:[o.jsx("div",{className:"p-3 bg-green-100 rounded-lg",children:o.jsx(jo,{className:"h-8 w-8 text-green-600"})}),o.jsxs("div",{className:"flex-1",children:[o.jsx("h3",{className:"text-xl font-bold text-gray-900 mb-2",children:"AI for Civic Engagement"}),o.jsx("p",{className:"text-gray-600 mb-3",children:"Use LLMs and AI agents to summarize meetings, extract policy insights, or answer citizen questions."}),o.jsxs("div",{className:"flex flex-wrap gap-2",children:[o.jsx("span",{className:"px-3 py-1 bg-green-50 text-green-700 text-xs font-semibold rounded-full",children:"LangChain"}),o.jsx("span",{className:"px-3 py-1 bg-green-50 text-green-700 text-xs font-semibold rounded-full",children:"RAG"}),o.jsx("span",{className:"px-3 py-1 bg-green-50 text-green-700 text-xs font-semibold rounded-full",children:"NLP"})]})]})]})}),o.jsx("div",{className:"bg-white rounded-xl shadow-md p-6 border border-gray-200",children:o.jsxs("div",{className:"flex items-start gap-4",children:[o.jsx("div",{className:"p-3 bg-purple-100 rounded-lg",children:o.jsx(Ur,{className:"h-8 w-8 text-purple-600"})}),o.jsxs("div",{className:"flex-1",children:[o.jsx("h3",{className:"text-xl font-bold text-gray-900 mb-2",children:"Community Engagement Tools"}),o.jsx("p",{className:"text-gray-600 mb-3",children:"Build mobile apps, notification systems, or tools that help people participate in local government."}),o.jsxs("div",{className:"flex flex-wrap gap-2",children:[o.jsx("span",{className:"px-3 py-1 bg-purple-50 text-purple-700 text-xs font-semibold rounded-full",children:"Mobile"}),o.jsx("span",{className:"px-3 py-1 bg-purple-50 text-purple-700 text-xs font-semibold rounded-full",children:"Notifications"}),o.jsx("span",{className:"px-3 py-1 bg-purple-50 text-purple-700 text-xs font-semibold rounded-full",children:"UX Design"})]})]})]})})]})]}),o.jsxs("div",{className:"bg-gradient-to-r from-primary-50 to-primary-100 rounded-xl p-8 border border-primary-200",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-4",children:"Resources for Participants"}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-6 text-sm",children:[o.jsxs("div",{children:[o.jsx("h3",{className:"font-semibold text-gray-900 mb-2",children:"Data Access"}),o.jsxs("ul",{className:"space-y-1 text-gray-700",children:[o.jsx("li",{children:"• 925 jurisdiction records"}),o.jsx("li",{children:"• 43,726 nonprofit organizations"}),o.jsx("li",{children:"• 6,913 meeting pages with transcripts"}),o.jsx("li",{children:"• API access and bulk downloads"})]})]}),o.jsxs("div",{children:[o.jsx("h3",{className:"font-semibold text-gray-900 mb-2",children:"Support"}),o.jsxs("ul",{className:"space-y-1 text-gray-700",children:[o.jsx("li",{children:"• Mentors from civic tech community"}),o.jsx("li",{children:"• Technical workshops and tutorials"}),o.jsx("li",{children:"• GitHub repository with starter code"}),o.jsx("li",{children:"• Discord community for collaboration"})]})]})]})]}),o.jsx("div",{className:"mt-8 text-center",children:o.jsxs("div",{className:"bg-white rounded-xl shadow-lg p-8 max-w-2xl mx-auto border border-gray-200",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-3",children:"Join Our Community"}),o.jsx("p",{className:"text-gray-600 mb-6",children:"Get notified about upcoming hackathons, workshops, and civic tech events."}),o.jsxs("div",{className:"flex flex-col sm:flex-row gap-4 justify-center",children:[o.jsx("a",{href:"https://github.com/getcommunityone/open-navigator-for-engagement",target:"_blank",rel:"noopener noreferrer",className:"px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors font-semibold",children:"Star on GitHub"}),o.jsx("a",{href:"/opportunities",className:"px-6 py-3 bg-white text-primary-600 rounded-lg hover:bg-primary-50 transition-colors font-semibold border-2 border-primary-600",children:"Browse All Opportunities"})]})]})})]})}function Jge(){const e=[{icon:$r,title:"Open Source Codebase",description:"All code is publicly available on GitHub under an open source license. Fork, contribute, or learn from our implementation.",link:"https://github.com/getcommunityone/open-navigator-for-engagement"},{icon:Ws,title:"Comprehensive Documentation",description:"Detailed documentation for developers, including API docs, data models, deployment guides, and contribution guidelines.",link:"https://www.communityone.com/docs/intro"},{icon:Ur,title:"Community Contributions",description:"Join our community of civic tech developers. Submit issues, create pull requests, or participate in discussions.",link:"https://github.com/getcommunityone/open-navigator-for-engagement/issues"},{icon:jo,title:"Hackathons & Events",description:"Participate in quarterly hackathons focused on civic engagement, government transparency, and community empowerment.",link:"/hackathons"}],t=[{category:"Frontend",tech:"React, TypeScript, Vite, TailwindCSS"},{category:"Backend",tech:"FastAPI, Python, PostgreSQL"},{category:"Data",tech:"Pandas, Parquet, HuggingFace Datasets"},{category:"Deployment",tech:"Docker, HuggingFace Spaces, Databricks"},{category:"Documentation",tech:"Docusaurus, Markdown"}];return o.jsx("div",{className:"min-h-screen p-8",style:{backgroundColor:"#F1F5F9"},children:o.jsxs("div",{className:"max-w-7xl mx-auto",children:[o.jsxs("div",{className:"mb-8",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-2",children:[o.jsx($r,{className:"h-8 w-8",style:{color:"#354F52"}}),o.jsx("h1",{className:"text-3xl font-bold",style:{color:"#354F52"},children:"Open Source Projects"})]}),o.jsx("p",{className:"text-gray-600",children:"Build with us. Contribute to civic tech. Make government data accessible to everyone."})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-8 mb-8 border-l-4",style:{borderColor:"#354F52"},children:[o.jsxs("div",{className:"flex items-start justify-between mb-4",children:[o.jsxs("div",{children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-2",children:"Open Navigator"}),o.jsx("p",{className:"text-gray-600 mb-4",children:"A comprehensive platform for civic engagement, government transparency, and community empowerment."})]}),o.jsxs("a",{href:"https://github.com/getcommunityone/open-navigator-for-engagement",target:"_blank",rel:"noopener noreferrer",className:"flex items-center gap-2 px-4 py-2 rounded-lg text-white hover:shadow-lg transition-all",style:{backgroundColor:"#354F52"},children:[o.jsx(bz,{className:"h-5 w-5"}),o.jsx("span",{children:"Star on GitHub"}),o.jsx(yb,{className:"h-4 w-4"})]})]}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-6 mb-6",children:[o.jsxs("div",{children:[o.jsx("h3",{className:"text-sm font-semibold text-gray-700 uppercase tracking-wide mb-2",children:"Key Features"}),o.jsxs("ul",{className:"space-y-2 text-sm text-gray-600",children:[o.jsxs("li",{className:"flex items-start gap-2",children:[o.jsx("span",{className:"text-green-600 font-bold",children:"✓"}),"Track 925 cities, counties, and school districts"]}),o.jsxs("li",{className:"flex items-start gap-2",children:[o.jsx("span",{className:"text-green-600 font-bold",children:"✓"}),"Monitor 43,726 nonprofits and community organizations"]}),o.jsxs("li",{className:"flex items-start gap-2",children:[o.jsx("span",{className:"text-green-600 font-bold",children:"✓"}),"Analyze government meeting transcripts with AI"]}),o.jsxs("li",{className:"flex items-start gap-2",children:[o.jsx("span",{className:"text-green-600 font-bold",children:"✓"}),"Open datasets on HuggingFace"]})]})]}),o.jsxs("div",{children:[o.jsx("h3",{className:"text-sm font-semibold text-gray-700 uppercase tracking-wide mb-2",children:"Languages & Tools"}),o.jsx("div",{className:"flex flex-wrap gap-2",children:["TypeScript","Python","React","FastAPI","PostgreSQL","Docker"].map(n=>o.jsx("span",{className:"px-3 py-1 rounded-full text-xs font-medium",style:{backgroundColor:"#CAD2C5",color:"#354F52"},children:n},n))})]})]}),o.jsxs("div",{className:"flex flex-wrap gap-4",children:[o.jsx("a",{href:"https://github.com/getcommunityone/open-navigator-for-engagement",target:"_blank",rel:"noopener noreferrer",className:"px-6 py-3 rounded-lg font-semibold hover:shadow-lg transition-all border-2",style:{borderColor:"#354F52",color:"#354F52"},children:"View Repository →"}),o.jsx("a",{href:"https://github.com/getcommunityone/open-navigator-for-engagement/issues",target:"_blank",rel:"noopener noreferrer",className:"px-6 py-3 rounded-lg font-semibold hover:shadow-lg transition-all border-2 border-gray-300 text-gray-700",children:"Report Issue"}),o.jsx("a",{href:"https://github.com/getcommunityone/open-navigator-for-engagement/fork",target:"_blank",rel:"noopener noreferrer",className:"px-6 py-3 rounded-lg font-semibold hover:shadow-lg transition-all border-2 border-gray-300 text-gray-700",children:"Fork Project"})]})]}),o.jsxs("div",{className:"mb-8",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-6",children:"How to Get Involved"}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-6",children:e.map(n=>{const r=n.icon,i=n.link.startsWith("http");return o.jsx("a",{href:n.link,target:i?"_blank":void 0,rel:i?"noopener noreferrer":void 0,className:"bg-white rounded-lg shadow-sm p-6 hover:shadow-md transition-shadow",children:o.jsxs("div",{className:"flex items-start gap-4",children:[o.jsx("div",{className:"w-12 h-12 rounded-lg flex items-center justify-center flex-shrink-0",style:{backgroundColor:"#354F5215"},children:o.jsx(r,{className:"h-6 w-6",style:{color:"#354F52"}})}),o.jsxs("div",{children:[o.jsxs("h3",{className:"text-lg font-semibold text-gray-900 mb-2 flex items-center gap-2",children:[n.title,i&&o.jsx(yb,{className:"h-4 w-4"})]}),o.jsx("p",{className:"text-sm text-gray-600",children:n.description})]})]})},n.title)})})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-8 mb-8",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-6",children:"Technology Stack"}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6",children:t.map(n=>o.jsxs("div",{children:[o.jsx("h3",{className:"text-sm font-semibold uppercase tracking-wide mb-2",style:{color:"#52796F"},children:n.category}),o.jsx("p",{className:"text-gray-700",children:n.tech})]},n.category))})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-8",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-4",children:"Contribution Guidelines"}),o.jsxs("div",{className:"space-y-4 text-gray-600",children:[o.jsx("p",{children:"We welcome contributions from developers of all skill levels! Here's how to get started:"}),o.jsxs("ol",{className:"list-decimal list-inside space-y-2 ml-4",children:[o.jsxs("li",{children:[o.jsx("strong",{children:"Fork the repository"})," and clone it to your local machine"]}),o.jsxs("li",{children:[o.jsx("strong",{children:"Create a new branch"})," for your feature or bug fix"]}),o.jsxs("li",{children:[o.jsx("strong",{children:"Make your changes"})," following our coding standards"]}),o.jsxs("li",{children:[o.jsx("strong",{children:"Write tests"})," for new functionality"]}),o.jsxs("li",{children:[o.jsx("strong",{children:"Submit a pull request"})," with a clear description of your changes"]})]}),o.jsxs("p",{className:"mt-4",children:["See our"," ",o.jsx("a",{href:"https://github.com/getcommunityone/open-navigator-for-engagement/blob/main/CONTRIBUTING.md",target:"_blank",rel:"noopener noreferrer",className:"font-semibold hover:underline",style:{color:"#354F52"},children:"CONTRIBUTING.md"})," ","for detailed guidelines."]})]})]})]})})}function e0e(){const e=[{icon:$t,title:"Oral Health",description:"Track dental programs, fluoridation policies, and oral health initiatives in your community.",count:"5,200+ related meetings",color:"#DC143C",keywords:["dental","fluoride","oral health","teeth"]},{icon:Eo,title:"Education",description:"Monitor school budgets, curriculum changes, and educational programs across districts.",count:"15,000+ related meetings",color:"#354F52",keywords:["schools","education","curriculum","budget"]},{icon:YB,title:"Housing & Development",description:"Follow zoning decisions, affordable housing initiatives, and development projects.",count:"8,500+ related meetings",color:"#52796F",keywords:["housing","zoning","development","affordable"]},{icon:kA,title:"Public Safety",description:"Stay informed on police budgets, fire department resources, and emergency services.",count:"12,000+ related meetings",color:"#84A98C",keywords:["police","fire","safety","emergency"]},{icon:Ur,title:"Social Services",description:"Track programs for seniors, families, mental health, and community support.",count:"7,800+ related meetings",color:"#CAD2C5",keywords:["social services","mental health","seniors","families"]},{icon:CA,title:"Environment & Sustainability",description:"Monitor climate initiatives, recycling programs, and environmental policies.",count:"6,400+ related meetings",color:"#52796F",keywords:["environment","climate","sustainability","recycling"]}],t=[{step:"1",title:"Find Your Topic",description:"Browse advocacy topics or search for specific issues affecting your community."},{step:"2",title:"Track Meetings",description:"Monitor upcoming government meetings where your topic will be discussed."},{step:"3",title:"Prepare Your Message",description:"Use our talking points and data to craft compelling testimony."},{step:"4",title:"Make Your Voice Heard",description:"Attend meetings, submit comments, or contact officials directly."}];return o.jsx("div",{className:"min-h-screen p-8",style:{backgroundColor:"#F1F5F9"},children:o.jsxs("div",{className:"max-w-7xl mx-auto",children:[o.jsxs("div",{className:"mb-8",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-2",children:[o.jsx(Hd,{className:"h-8 w-8",style:{color:"#52796F"}}),o.jsx("h1",{className:"text-3xl font-bold",style:{color:"#354F52"},children:"Advocacy Topics"})]}),o.jsx("p",{className:"text-gray-600",children:"Track what your community is discussing. Find advocacy opportunities and get involved in local decision-making."})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-8 mb-8 text-center border-l-4",style:{borderColor:"#52796F"},children:[o.jsx(oz,{className:"h-12 w-12 mx-auto mb-4",style:{color:"#52796F"}}),o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-3",children:"Find Advocacy Opportunities in Your Area"}),o.jsx("p",{className:"text-gray-600 mb-6 max-w-2xl mx-auto",children:"Search meeting minutes for topics that matter to you. Get alerts when your issues are being discussed."}),o.jsxs("div",{className:"flex flex-col sm:flex-row gap-4 justify-center",children:[o.jsx(ke,{to:"/documents",className:"px-6 py-3 rounded-lg text-white font-semibold hover:shadow-lg transition-all",style:{backgroundColor:"#52796F"},children:"Search Meeting Minutes"}),o.jsx(ke,{to:"/opportunities",className:"px-6 py-3 rounded-lg font-semibold hover:shadow-lg transition-all border-2",style:{borderColor:"#52796F",color:"#52796F"},children:"View All Opportunities"})]})]}),o.jsxs("div",{className:"mb-12",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-6",children:"Popular Advocacy Topics"}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6",children:e.map(n=>{const r=n.icon;return o.jsxs(ke,{to:`/documents?search=${encodeURIComponent(n.keywords[0])}`,className:"bg-white rounded-lg shadow-sm p-6 hover:shadow-md transition-all group",children:[o.jsxs("div",{className:"flex items-start justify-between mb-4",children:[o.jsx("div",{className:"w-12 h-12 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform",style:{backgroundColor:`${n.color}15`},children:o.jsx(r,{className:"h-6 w-6",style:{color:n.color}})}),o.jsx("span",{className:"text-xs font-medium px-2 py-1 rounded-full",style:{backgroundColor:`${n.color}15`,color:n.color},children:n.count})]}),o.jsx("h3",{className:"text-lg font-semibold text-gray-900 mb-2 group-hover:text-primary-600 transition-colors",children:n.title}),o.jsx("p",{className:"text-sm text-gray-600 mb-3",children:n.description}),o.jsx("div",{className:"flex flex-wrap gap-1",children:n.keywords.slice(0,3).map(i=>o.jsx("span",{className:"text-xs px-2 py-1 rounded-full bg-gray-100 text-gray-700",children:i},i))}),o.jsx("div",{className:"mt-4 text-sm font-medium group-hover:translate-x-1 transition-transform",style:{color:n.color},children:"Search meetings →"})]},n.title)})})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-8 mb-8",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-6",children:"How to Advocate Effectively"}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6",children:t.map(n=>o.jsxs("div",{className:"text-center",children:[o.jsx("div",{className:"w-12 h-12 rounded-full mx-auto mb-4 flex items-center justify-center text-white text-xl font-bold",style:{backgroundColor:"#52796F"},children:n.step}),o.jsx("h3",{className:"font-semibold text-gray-900 mb-2",children:n.title}),o.jsx("p",{className:"text-sm text-gray-600",children:n.description})]},n.step))})]}),o.jsxs("div",{className:"bg-gradient-to-r from-primary-50 to-primary-100 rounded-lg p-8 text-center",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-3",children:"Ready to Make a Difference?"}),o.jsx("p",{className:"text-gray-700 mb-6 max-w-2xl mx-auto",children:"Start by searching for your community and exploring what local government is discussing. Your voice matters in local decision-making."}),o.jsxs("div",{className:"flex flex-col sm:flex-row gap-4 justify-center",children:[o.jsx(ke,{to:"/heatmap",className:"px-6 py-3 rounded-lg text-white font-semibold hover:shadow-lg transition-all",style:{backgroundColor:"#354F52"},children:"Find Your Community"}),o.jsx(ke,{to:"/events",className:"px-6 py-3 rounded-lg font-semibold hover:shadow-lg transition-all bg-white border-2 border-gray-300 text-gray-700",children:"View Upcoming Meetings"})]})]})]})})}function t0e(){const e=[{name:"PolitiFact",description:"Fact-checking political claims with the Truth-O-Meter rating system.",url:"https://www.politifact.com",logo:"🔍",focus:"National & State Politics"},{name:"FactCheck.org",description:"Nonpartisan fact-checking by the Annenberg Public Policy Center.",url:"https://www.factcheck.org",logo:"✓",focus:"Political Claims"},{name:"Google Fact Check Explorer",description:"Search fact checks from publishers worldwide using Google's Fact Check Tools.",url:"https://toolbox.google.com/factcheck/explorer",logo:"🔎",focus:"Global Claims"},{name:"Snopes",description:"Fact-checking urban legends, internet rumors, and misinformation.",url:"https://www.snopes.com",logo:"📰",focus:"Internet & Media"}],t=[{icon:UB,title:"Find the Claim",description:"Identify specific statements made in government meetings or public discourse."},{icon:kA,title:"Check Credible Sources",description:"Use fact-checking organizations and official data sources to verify claims."},{icon:ci,title:"Evaluate Evidence",description:"Look for primary sources, statistics, and expert analysis to support or refute claims."},{icon:_b,title:"Share Findings",description:"Report misinformation and share accurate information with your community."}],n=[{type:"True",icon:ci,color:"text-green-600",bgColor:"bg-green-50",description:"Claim is accurate and supported by evidence"},{type:"Mostly True",icon:ci,color:"text-blue-600",bgColor:"bg-blue-50",description:"Claim is mostly accurate with minor omissions"},{type:"Half True",icon:OA,color:"text-yellow-600",bgColor:"bg-yellow-50",description:"Claim contains elements of truth but lacks context"},{type:"Mostly False",icon:Sb,color:"text-orange-600",bgColor:"bg-orange-50",description:"Claim contains significant inaccuracies"},{type:"False",icon:Sb,color:"text-red-600",bgColor:"bg-red-50",description:"Claim is not supported by evidence"}];return o.jsx("div",{className:"min-h-screen p-8",style:{backgroundColor:"#F1F5F9"},children:o.jsxs("div",{className:"max-w-7xl mx-auto",children:[o.jsxs("div",{className:"mb-8",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-2",children:[o.jsx(_b,{className:"h-8 w-8",style:{color:"#CAD2C5"}}),o.jsx("h1",{className:"text-3xl font-bold",style:{color:"#354F52"},children:"Fact-Checking Tools"})]}),o.jsx("p",{className:"text-gray-600",children:"Verify claims from meetings and legislation with trusted fact-checking sources and tools."})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-8 mb-8 border-l-4",style:{borderColor:"#CAD2C5"},children:[o.jsxs("div",{className:"flex items-start justify-between mb-4",children:[o.jsxs("div",{children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-2",children:"Debate Framework Analyzer"}),o.jsx("p",{className:"text-gray-600 mb-4",children:"Evaluate government decisions using a debate framework that analyzes Harms, Solvency, and Topicality."})]}),o.jsx(ke,{to:"/debate-grader",className:"px-6 py-3 rounded-lg text-white font-semibold hover:shadow-lg transition-all whitespace-nowrap",style:{backgroundColor:"#52796F"},children:"Try Debate Grader →"})]}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-4",children:[o.jsxs("div",{className:"p-4 rounded-lg bg-gray-50",children:[o.jsx("h3",{className:"font-semibold text-gray-900 mb-1",children:"Harms Analysis"}),o.jsx("p",{className:"text-sm text-gray-600",children:"Evaluates whether the decision addresses a real problem and its severity."})]}),o.jsxs("div",{className:"p-4 rounded-lg bg-gray-50",children:[o.jsx("h3",{className:"font-semibold text-gray-900 mb-1",children:"Solvency Check"}),o.jsx("p",{className:"text-sm text-gray-600",children:"Assesses if the proposed solution will actually solve the identified problem."})]}),o.jsxs("div",{className:"p-4 rounded-lg bg-gray-50",children:[o.jsx("h3",{className:"font-semibold text-gray-900 mb-1",children:"Topicality Review"}),o.jsx("p",{className:"text-sm text-gray-600",children:"Determines if the decision is within the authority and scope of the body making it."})]})]})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-8 mb-8",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-6",children:"Understanding Truth Ratings"}),o.jsx("div",{className:"space-y-3",children:n.map(r=>{const i=r.icon;return o.jsxs("div",{className:`flex items-start gap-4 p-4 rounded-lg ${r.bgColor}`,children:[o.jsx(i,{className:`h-6 w-6 flex-shrink-0 ${r.color}`}),o.jsxs("div",{children:[o.jsx("h3",{className:`font-semibold ${r.color}`,children:r.type}),o.jsx("p",{className:"text-sm text-gray-700",children:r.description})]})]},r.type)})})]}),o.jsxs("div",{className:"mb-8",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-6",children:"Trusted Fact-Checking Resources"}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-6",children:e.map(r=>o.jsxs("a",{href:r.url,target:"_blank",rel:"noopener noreferrer",className:"bg-white rounded-lg shadow-sm p-6 hover:shadow-md transition-all group",children:[o.jsxs("div",{className:"flex items-start justify-between mb-4",children:[o.jsx("div",{className:"text-4xl",children:r.logo}),o.jsx(yb,{className:"h-5 w-5 text-gray-400 group-hover:text-gray-600"})]}),o.jsx("h3",{className:"text-xl font-semibold text-gray-900 mb-2 group-hover:text-primary-600 transition-colors",children:r.name}),o.jsx("p",{className:"text-sm text-gray-600 mb-3",children:r.description}),o.jsx("div",{className:"flex items-center gap-2",children:o.jsx("span",{className:"text-xs px-3 py-1 rounded-full bg-gray-100 text-gray-700 font-medium",children:r.focus})})]},r.name))})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-8 mb-8",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-6",children:"How to Fact-Check Claims"}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6",children:t.map(r=>{const i=r.icon;return o.jsxs("div",{className:"text-center",children:[o.jsx("div",{className:"w-16 h-16 rounded-full mx-auto mb-4 flex items-center justify-center",style:{backgroundColor:"#CAD2C5"},children:o.jsx(i,{className:"h-8 w-8",style:{color:"#354F52"}})}),o.jsx("h3",{className:"font-semibold text-gray-900 mb-2",children:r.title}),o.jsx("p",{className:"text-sm text-gray-600",children:r.description})]},r.title)})})]}),o.jsxs("div",{className:"bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-8",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-4",children:"Tips for Critical Thinking"}),o.jsxs("div",{className:"space-y-3 text-gray-700",children:[o.jsxs("div",{className:"flex items-start gap-3",children:[o.jsx("span",{className:"text-xl flex-shrink-0",children:"💡"}),o.jsxs("p",{children:[o.jsx("strong",{children:"Question the Source:"})," Who is making the claim? Do they have expertise or potential bias?"]})]}),o.jsxs("div",{className:"flex items-start gap-3",children:[o.jsx("span",{className:"text-xl flex-shrink-0",children:"📊"}),o.jsxs("p",{children:[o.jsx("strong",{children:"Look for Data:"})," Are there statistics or studies backing up the claim? Are they from credible sources?"]})]}),o.jsxs("div",{className:"flex items-start gap-3",children:[o.jsx("span",{className:"text-xl flex-shrink-0",children:"🔍"}),o.jsxs("p",{children:[o.jsx("strong",{children:"Check Multiple Sources:"})," Does the claim appear in multiple independent, reliable sources?"]})]}),o.jsxs("div",{className:"flex items-start gap-3",children:[o.jsx("span",{className:"text-xl flex-shrink-0",children:"⏰"}),o.jsxs("p",{children:[o.jsx("strong",{children:"Consider Context:"})," Is the claim taken out of context? When was it made, and is it still relevant?"]})]}),o.jsxs("div",{className:"flex items-start gap-3",children:[o.jsx("span",{className:"text-xl flex-shrink-0",children:"🤔"}),o.jsxs("p",{children:[o.jsx("strong",{children:"Beware of Bias:"})," Does the source have a political or financial incentive to make this claim?"]})]})]})]})]})})}function n0e(){var ye,je,ie,Ve,Re,ut,dt,Tt,pn,Gr,yi,Wn,nr,Zi,$o;const e=qc(),[t,n]=Us(),[r,i]=N.useState(()=>t.get("q")||""),[a,s]=N.useState(()=>t.get("q")||""),[c,u]=N.useState(()=>{const X=t.get("types");if(X){const de=X.split(",").filter(Ae=>["contacts","organizations","causes","meetings"].includes(Ae.trim()));return de.length>0?de:["contacts","organizations","causes"]}return["contacts","organizations","causes"]}),[d,h]=N.useState(()=>t.get("state")||""),[m,p]=N.useState(()=>parseInt(t.get("page")||"1")),[v,_]=N.useState(!1),[x,y]=N.useState(!1),[w,b]=N.useState(()=>t.get("sort")||"relevance"),[j,E]=N.useState(()=>t.get("ntee")||""),[P,O]=N.useState(()=>{const X=t.get("jurisdiction_details");if(X)try{return JSON.parse(decodeURIComponent(X))}catch{return[]}return[]}),[C,A]=N.useState(()=>{const X=t.get("state");if(X)return X;const de=t.get("jurisdiction_details");if(de)try{const Ae=JSON.parse(decodeURIComponent(de));for(const Pt of Ae){if(Pt.state)return Pt.state;if(Pt.type==="State"||Pt.type==="state")return{Massachusetts:"MA",Alabama:"AL",Georgia:"GA",Washington:"WA",Wisconsin:"WI",California:"CA",Texas:"TX","New York":"NY",Florida:"FL"}[Pt.name]||Pt.name}}catch{}return""}),[T,$]=N.useState(new Set),[z,D]=N.useState(new Set),[Z,I]=N.useState(r),F=N.useRef(null);N.useEffect(()=>{const X=setTimeout(()=>{I(r)},300);return()=>clearTimeout(X)},[r]),N.useEffect(()=>{const X=t.get("q"),de=t.get("state"),Ae=t.get("types");t.get("page"),t.get("sort"),t.get("ntee");const Pt=t.get("jurisdiction_details");if(X&&(i(X),s(X)),de)h(de),A(de);else if(Pt)try{const on=JSON.parse(decodeURIComponent(Pt));O(on);for(const sn of on){if(sn.state){A(sn.state);break}if(sn.type==="State"||sn.type==="state"){const Ks={Massachusetts:"MA",Alabama:"AL",Georgia:"GA",Washington:"WA",Wisconsin:"WI",California:"CA",Texas:"TX","New York":"NY",Florida:"FL"}[sn.name]||sn.name;A(Ks);break}}}catch{O([])}if(Ae){const on=Ae.split(",").filter(sn=>["contacts","meetings","organizations","causes"].includes(sn.trim()));on.length>0&&u(on)}},[t,C]);const{data:B,isFetching:G}=zt({queryKey:["search-preview",Z,C],queryFn:async()=>{if(!Z||Z.length<2)return null;const X={q:Z,types:"causes,contacts,organizations",limit:3};return C&&(X.state=C),(await vt.get("/search/",{params:X})).data},enabled:Z.length>=2&&x,staleTime:5e3}),{data:R,isLoading:K,error:W}=zt({queryKey:["unified-search",a,c,d,m,w,j],queryFn:async()=>{if(!a&&!d&&!c.length)return null;const X={types:c.join(","),limit:20,page:m};return a&&(X.q=a),d&&(X.state=d),w&&w!=="relevance"&&(X.sort=w),j&&(X.ntee_code=j),(await vt.get("/search/",{params:X})).data},enabled:a&&a.length>=2||d!==""||c.length>0}),U=X=>{if(X==null||X.preventDefault(),r.trim().length>=2||d||c.length>0){s(r),y(!1),p(1);const de={};r.trim()&&(de.q=r),d&&(de.state=d),c.length>0&&c.length<5&&(de.types=c.join(",")),w&&w!=="relevance"&&(de.sort=w),j&&(de.ntee=j),n(de)}},Y=X=>{p(X);const de={};a&&(de.q=a),d&&(de.state=d),c.length>0&&c.length<5&&(de.types=c.join(",")),w&&w!=="relevance"&&(de.sort=w),j&&(de.ntee=j),X>1&&(de.page=X.toString()),n(de),window.scrollTo({top:0,behavior:"smooth"})},ne=X=>{s(r),y(!1),u([X]);const de={q:r};d&&(de.state=d),de.types=X,w&&w!=="relevance"&&(de.sort=w),j&&(de.ntee=j),n(de)},ae=X=>{const de=c.includes(X)?c.filter(Pt=>Pt!==X):[...c,X];u(de),p(1);const Ae={};a&&(Ae.q=a),d&&(Ae.state=d),de.length>0&&de.length<5&&(Ae.types=de.join(",")),w&&w!=="relevance"&&(Ae.sort=w),j&&(Ae.ntee=j),n(Ae)},ee=X=>{$(de=>{const Ae=new Set(de);return Ae.has(X)?Ae.delete(X):Ae.add(X),Ae})},ce=X=>{D(de=>{const Ae=new Set(de);return Ae.has(X)?Ae.delete(X):Ae.add(X),Ae})},Ne=X=>{switch(X){case"contact":return o.jsx(Li,{className:"h-5 w-5"});case"meeting":return o.jsx(ha,{className:"h-5 w-5"});case"organization":return o.jsx(Ci,{className:"h-5 w-5"});case"cause":return o.jsx($t,{className:"h-5 w-5"});default:return null}},Pe=X=>{switch(X){case"contact":return"bg-blue-100 text-blue-700 border-blue-200";case"meeting":return"bg-green-100 text-green-700 border-green-200";case"organization":return"bg-purple-100 text-purple-700 border-purple-200";case"cause":return"bg-pink-100 text-pink-700 border-pink-200";default:return"bg-gray-100 text-gray-700 border-gray-200"}},se=({result:X})=>{var de,Ae,Pt,on,sn;return o.jsx("div",{className:"bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow",children:o.jsxs("div",{className:"flex items-start gap-3",children:[o.jsx("div",{className:`p-2 rounded-lg border ${Pe(X.type)}`,children:Ne(X.type)}),o.jsxs("div",{className:"flex-1",children:[o.jsxs("div",{className:"flex items-start justify-between gap-2",children:[o.jsxs("div",{className:"flex-1",children:[o.jsx("h3",{onClick:()=>e(X.url),className:"font-semibold text-gray-900 cursor-pointer hover:text-blue-600 mb-1",children:X.title}),o.jsx("p",{className:"text-sm text-gray-600 mb-2",children:X.subtitle})]}),X.type==="organization"&&((de=X.metadata)!=null&&de.logo_url?o.jsx("img",{src:X.metadata.logo_url,alt:X.title,className:"w-12 h-12 rounded object-contain flex-shrink-0 bg-gray-100 border border-gray-200",onError:xi=>{xi.currentTarget.style.display="none";const Ks=xi.currentTarget.nextElementSibling;Ks&&(Ks.style.display="flex")}}):null),X.type==="organization"&&o.jsx("div",{className:"w-12 h-12 rounded flex items-center justify-center text-white text-lg font-bold flex-shrink-0",style:{backgroundColor:"#52796F",display:(Ae=X.metadata)!=null&&Ae.logo_url?"none":"flex"},children:X.title.charAt(0)})]}),o.jsx("p",{className:"text-sm text-gray-500 line-clamp-2 mb-2",children:X.description}),X.type==="organization"&&((Pt=X.metadata)==null?void 0:Pt.mission)&&o.jsx("div",{className:"mt-2 mb-2 p-3 bg-blue-50 border-l-4 border-blue-400 rounded",children:o.jsxs("p",{className:"text-sm text-gray-700 italic",children:[o.jsx("span",{className:"font-semibold text-blue-900",children:"Mission: "}),X.metadata.mission]})}),X.type==="organization"&&((on=X.metadata)==null?void 0:on.website)&&o.jsxs("a",{href:X.metadata.website,target:"_blank",rel:"noopener noreferrer",onClick:xi=>xi.stopPropagation(),className:"text-sm text-blue-600 hover:text-blue-800 hover:underline inline-flex items-center gap-1 mb-2",children:["🔗 ",X.metadata.website]}),X.type==="organization"&&X.metadata&&o.jsxs("div",{className:"mt-2 flex flex-wrap gap-2 text-xs",children:[X.metadata.ein&&o.jsxs("span",{className:"px-2 py-1 bg-gray-100 text-gray-700 rounded",children:["EIN: ",X.metadata.ein]}),X.metadata.revenue&&X.metadata.revenue>0&&o.jsxs("span",{className:"px-2 py-1 bg-green-100 text-green-700 rounded",children:["💰 Revenue: ",oi(X.metadata.revenue),X.metadata.tax_year&&` (${X.metadata.tax_year})`]}),X.metadata.assets&&X.metadata.assets>0&&o.jsxs("span",{className:"px-2 py-1 bg-blue-100 text-blue-700 rounded",children:["📊 Assets: ",oi(X.metadata.assets),X.metadata.tax_year&&` (${X.metadata.tax_year})`]}),X.metadata.causes&&X.metadata.causes.length>0&&o.jsxs("span",{className:"px-2 py-1 bg-purple-100 text-purple-700 rounded",children:["🏷️ ",X.metadata.causes.slice(0,3).join(", ")]})]}),X.type==="organization"&&((sn=X.metadata)==null?void 0:sn.ein)&&o.jsxs("div",{className:"mt-3",children:[o.jsxs("button",{onClick:xi=>{xi.stopPropagation(),ce(X.metadata.ein)},className:"flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 font-medium",children:[z.has(X.metadata.ein)?o.jsx(bb,{className:"h-4 w-4"}):o.jsx(Vd,{className:"h-4 w-4"}),z.has(X.metadata.ein)?"Hide":"Show"," Details"]}),z.has(X.metadata.ein)&&o.jsxs("div",{className:"mt-3 space-y-3 border-t pt-3",children:[o.jsxs("div",{children:[o.jsxs("h4",{className:"text-sm font-semibold text-gray-700 mb-2 flex items-center gap-1",children:["💰 Financial Information",X.metadata.tax_year&&o.jsxs("span",{className:"text-xs text-gray-500 font-normal",children:["(Tax Year ",X.metadata.tax_year,")"]})]}),!X.metadata.revenue&&!X.metadata.assets&&!X.metadata.income?o.jsxs("div",{className:"bg-amber-50 p-4 rounded border border-amber-200 text-sm",children:[o.jsx("p",{className:"text-amber-800 mb-2",children:o.jsx("span",{className:"font-semibold",children:"📊 Form 990 data not yet available"})}),o.jsxs("p",{className:"text-amber-700 text-xs",children:["Financial information from IRS Form 990 filings is being enriched. Check back later or visit"," ",o.jsx("a",{href:`https://projects.propublica.org/nonprofits/organizations/${X.metadata.ein}`,target:"_blank",rel:"noopener noreferrer",className:"underline hover:text-amber-900",children:"ProPublica Nonprofit Explorer"})," ","for current data."]})]}):o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-3 text-sm",children:[o.jsxs("div",{className:"bg-green-50 p-3 rounded border border-green-200",children:[o.jsx("div",{className:"text-xs text-green-700 font-medium mb-1",children:"Total Revenue"}),o.jsx("div",{className:"text-lg font-bold text-green-900",children:X.metadata.revenue!==null&&X.metadata.revenue!==void 0?oi(X.metadata.revenue):o.jsx("span",{className:"text-sm text-gray-500",children:"Pending"})})]}),o.jsxs("div",{className:"bg-blue-50 p-3 rounded border border-blue-200",children:[o.jsx("div",{className:"text-xs text-blue-700 font-medium mb-1",children:"Total Assets"}),o.jsx("div",{className:"text-lg font-bold text-blue-900",children:X.metadata.assets!==null&&X.metadata.assets!==void 0?oi(X.metadata.assets):o.jsx("span",{className:"text-sm text-gray-500",children:"Pending"})})]}),o.jsxs("div",{className:"bg-purple-50 p-3 rounded border border-purple-200",children:[o.jsx("div",{className:"text-xs text-purple-700 font-medium mb-1",children:"Net Income"}),o.jsx("div",{className:"text-lg font-bold text-purple-900",children:X.metadata.income!==null&&X.metadata.income!==void 0?oi(X.metadata.income):o.jsx("span",{className:"text-sm text-gray-500",children:"Pending"})})]})]})]}),o.jsxs("div",{children:[o.jsx("h4",{className:"text-sm font-semibold text-gray-700 mb-2 flex items-center gap-1",children:"👥 Board Members"}),o.jsx("div",{className:"bg-gray-50 p-3 rounded border border-gray-200 text-sm text-gray-600",children:"Board member information coming soon"})]}),o.jsxs("div",{children:[o.jsx("h4",{className:"text-sm font-semibold text-gray-700 mb-2 flex items-center gap-1",children:"📜 Recent Grants"}),o.jsx("div",{className:"bg-gray-50 p-3 rounded border border-gray-200 text-sm text-gray-600",children:"Grant information coming soon"})]})]})]}),o.jsx("div",{className:"mt-2",children:o.jsxs("span",{className:`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${Pe(X.type)}`,children:[Ne(X.type),X.type.charAt(0).toUpperCase()+X.type.slice(1)]})})]})]})})};return o.jsx("div",{className:"min-h-screen bg-gray-50",children:o.jsxs("div",{className:"max-w-6xl mx-auto px-6 pb-6",children:[o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 mb-6",children:[o.jsx("h1",{className:"text-3xl font-bold text-gray-900 mb-4",children:"Search"}),o.jsxs("form",{onSubmit:U,className:"relative",children:[o.jsxs("div",{className:"relative",children:[o.jsx("input",{ref:F,type:"text",value:r,onChange:X=>{i(X.target.value),y(!0)},onFocus:()=>y(!0),onBlur:()=>setTimeout(()=>y(!1),200),placeholder:"Search for people, meetings, organizations, causes...",className:"w-full px-12 py-3 border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent text-lg text-gray-900"}),o.jsx(fn,{className:"absolute left-4 top-3.5 h-6 w-6 text-gray-400"}),r&&o.jsx("button",{type:"button",onClick:()=>{var X;i(""),s(""),(X=F.current)==null||X.focus()},className:"absolute right-4 top-3.5 text-gray-400 hover:text-gray-600",children:o.jsx(Cr,{className:"h-6 w-6"})})]}),x&&r.length>=2&&o.jsxs("div",{className:"absolute z-10 w-full mt-2 bg-white border border-gray-200 rounded-lg shadow-xl max-h-96 overflow-y-auto",children:[(G||r!==Z)&&o.jsxs("div",{className:"px-4 py-8 text-center",children:[o.jsx("div",{className:"inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mb-2"}),o.jsx("p",{className:"text-sm text-gray-600",children:"Searching..."})]}),!G&&r===Z&&B&&B.total_results>0&&o.jsxs(o.Fragment,{children:[B.results.causes.length>0&&o.jsxs("div",{className:"border-b border-gray-200",children:[o.jsxs("div",{className:"px-4 py-2 bg-gray-50 flex items-center justify-between",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx($t,{className:"h-4 w-4 text-gray-500"}),o.jsx("span",{className:"text-xs font-semibold text-gray-700 uppercase",children:"Causes"})]}),B.results.causes.length>0&&o.jsx("button",{onClick:()=>ne("causes"),className:"text-xs text-primary-600 hover:text-primary-700 font-medium",children:"View All"})]}),B.results.causes.slice(0,3).map((X,de)=>o.jsxs("button",{onClick:()=>e(X.url),className:"w-full text-left px-4 py-2 hover:bg-gray-50 flex items-start gap-3 transition-colors",children:[o.jsx($t,{className:"h-5 w-5 text-gray-600 mt-0.5 flex-shrink-0"}),o.jsxs("div",{className:"flex-1 min-w-0",children:[o.jsx("div",{className:"font-medium text-gray-900 truncate",children:X.title}),o.jsx("div",{className:"text-sm text-gray-600 truncate",children:X.subtitle})]})]},de))]}),B.results.contacts.length>0&&o.jsxs("div",{className:"border-b border-gray-200",children:[o.jsxs("div",{className:"px-4 py-2 bg-gray-50 flex items-center justify-between",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Li,{className:"h-4 w-4 text-gray-500"}),o.jsx("span",{className:"text-xs font-semibold text-gray-700 uppercase",children:"People"})]}),B.results.contacts.length>0&&o.jsx("button",{onClick:()=>ne("contacts"),className:"text-xs text-primary-600 hover:text-primary-700 font-medium",children:"View All"})]}),B.results.contacts.slice(0,3).map((X,de)=>o.jsxs("button",{onClick:()=>e(X.url),className:"w-full text-left px-4 py-2 hover:bg-gray-50 flex items-start gap-3 transition-colors",children:[o.jsx(Li,{className:"h-5 w-5 text-gray-600 mt-0.5 flex-shrink-0"}),o.jsxs("div",{className:"flex-1 min-w-0",children:[o.jsx("div",{className:"font-medium text-gray-900 truncate",children:X.title}),o.jsx("div",{className:"text-sm text-gray-600 truncate",children:X.subtitle})]})]},de))]}),B.results.organizations.length>0&&o.jsxs("div",{children:[o.jsxs("div",{className:"px-4 py-2 bg-gray-50 flex items-center justify-between",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(Ci,{className:"h-4 w-4 text-gray-500"}),o.jsx("span",{className:"text-xs font-semibold text-gray-700 uppercase",children:"Organizations"})]}),B.results.organizations.length>0&&o.jsx("button",{onClick:()=>ne("organizations"),className:"text-xs text-primary-600 hover:text-primary-700 font-medium",children:"View All"})]}),B.results.organizations.slice(0,3).map((X,de)=>{var Ae,Pt;return o.jsxs("button",{onClick:()=>e(X.url),className:"w-full text-left px-4 py-2 hover:bg-gray-50 flex items-start gap-3 transition-colors last:rounded-b-lg",children:[(Ae=X.metadata)!=null&&Ae.logo_url?o.jsx("img",{src:X.metadata.logo_url,alt:X.title,className:"h-10 w-10 rounded object-contain flex-shrink-0 bg-gray-100 border border-gray-200",onError:on=>{on.currentTarget.style.display="none";const sn=on.currentTarget.nextElementSibling;sn&&(sn.style.display="flex")}}):null,o.jsx("div",{className:"h-10 w-10 rounded flex items-center justify-center text-white text-sm font-bold flex-shrink-0",style:{backgroundColor:"#52796F",display:(Pt=X.metadata)!=null&&Pt.logo_url?"none":"flex"},children:X.title.charAt(0)}),o.jsxs("div",{className:"flex-1 min-w-0",children:[o.jsx("div",{className:"font-medium text-gray-900 truncate",children:X.title}),o.jsx("div",{className:"text-sm text-gray-600 truncate",children:X.subtitle})]})]},de)})]}),o.jsx("div",{className:"px-4 py-2 bg-gray-50 text-center border-t border-gray-200",children:o.jsxs("button",{onClick:()=>U(),className:"text-sm text-primary-600 hover:text-primary-700 font-medium",children:["See all ",B.total_results," results →"]})})]}),!G&&r===Z&&B&&B.total_results===0&&o.jsxs("div",{className:"px-4 py-8 text-center",children:[o.jsxs("p",{className:"text-gray-600",children:['No results found for "',r,'"']}),o.jsx("p",{className:"text-sm text-gray-500 mt-1",children:"Try a different search term"})]})]})]}),o.jsxs("div",{className:"mt-4 flex items-center gap-3 flex-wrap",children:[o.jsxs("button",{onClick:()=>_(!v),className:`flex items-center gap-2 px-4 py-2 rounded-lg border-2 transition-colors ${v?"border-primary-500 bg-primary-50 text-primary-700":"border-gray-300 text-gray-700 hover:border-gray-400 hover:bg-gray-50"}`,children:[o.jsx(jA,{className:"h-5 w-5"}),"Filters",(d||w!=="relevance"||j)&&o.jsx("span",{className:"ml-1 px-2 py-0.5 bg-primary-600 text-white text-xs rounded-full",children:[d,w!=="relevance"?"sorted":null,j].filter(Boolean).length})]}),["contacts","organizations","causes","meetings"].map(X=>o.jsxs("button",{onClick:()=>ae(X),className:`flex items-center gap-2 px-4 py-2 rounded-full border-2 transition-all ${c.includes(X)?`${Pe(X)} border-current font-medium shadow-sm`:"border-gray-300 bg-white text-gray-600 hover:border-gray-400 hover:bg-gray-50"}`,children:[c.includes(X)&&o.jsx(xb,{className:"h-4 w-4 flex-shrink-0"}),Ne(X),X.charAt(0).toUpperCase()+X.slice(1)]},X))]}),(d||w!=="relevance"||j||P.length>0)&&o.jsxs("div",{className:"mt-3 flex items-center gap-2 flex-wrap",children:[o.jsx("span",{className:"text-sm text-gray-600",children:"Active filters:"}),d&&o.jsxs("span",{className:"inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm",children:["State: ",d,o.jsx("button",{onClick:()=>{h(""),setTimeout(()=>U(),0)},className:"hover:bg-blue-200 rounded-full p-0.5",children:o.jsx(Cr,{className:"h-3 w-3"})})]}),P.length>0&&o.jsxs("span",{className:"inline-flex items-center gap-1 px-3 py-1 bg-teal-100 text-teal-800 rounded-full text-sm",children:[o.jsx(Rn,{className:"h-3 w-3"}),P.length," Jurisdictions",o.jsx("button",{onClick:()=>{O([]);const X=new URLSearchParams(window.location.search);X.delete("jurisdiction_details"),n(X)},className:"hover:bg-teal-200 rounded-full p-0.5",children:o.jsx(Cr,{className:"h-3 w-3"})})]}),w!=="relevance"&&o.jsxs("span",{className:"inline-flex items-center gap-1 px-3 py-1 bg-purple-100 text-purple-800 rounded-full text-sm",children:["Sort: ",w==="name-asc"?"Name A-Z":w==="name-desc"?"Name Z-A":w==="revenue-desc"?"Revenue ↓":w==="revenue-asc"?"Revenue ↑":w==="assets-desc"?"Assets ↓":w==="assets-asc"?"Assets ↑":w,o.jsx("button",{onClick:()=>{b("relevance"),setTimeout(()=>U(),0)},className:"hover:bg-purple-200 rounded-full p-0.5",children:o.jsx(Cr,{className:"h-3 w-3"})})]}),j&&o.jsxs("span",{className:"inline-flex items-center gap-1 px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm",children:["Category: ",j,o.jsx("button",{onClick:()=>{E(""),setTimeout(()=>U(),0)},className:"hover:bg-green-200 rounded-full p-0.5",children:o.jsx(Cr,{className:"h-3 w-3"})})]})]}),v&&o.jsxs("div",{className:"mt-4 p-4 bg-gray-50 rounded-lg border border-gray-200",children:[o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-4",children:[o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"State"}),o.jsxs("select",{value:d,onChange:X=>{h(X.target.value),p(1),setTimeout(()=>U(),0)},className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-900 bg-white",children:[o.jsx("option",{value:"",className:"text-gray-900",children:"All States"}),o.jsx("option",{value:"AL",className:"text-gray-900",children:"Alabama"}),o.jsx("option",{value:"GA",className:"text-gray-900",children:"Georgia"}),o.jsx("option",{value:"MA",className:"text-gray-900",children:"Massachusetts"}),o.jsx("option",{value:"WA",className:"text-gray-900",children:"Washington"}),o.jsx("option",{value:"WI",className:"text-gray-900",children:"Wisconsin"})]})]}),o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Sort By"}),o.jsxs("select",{value:w,onChange:X=>{b(X.target.value),p(1),setTimeout(()=>U(),0)},className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-900 bg-white",children:[o.jsx("option",{value:"relevance",className:"text-gray-900",children:"Relevance"}),o.jsx("option",{value:"name-asc",className:"text-gray-900",children:"Name (A-Z)"}),o.jsx("option",{value:"name-desc",className:"text-gray-900",children:"Name (Z-A)"}),o.jsx("option",{value:"revenue-desc",className:"text-gray-900",children:"Revenue (High to Low)"}),o.jsx("option",{value:"revenue-asc",className:"text-gray-900",children:"Revenue (Low to High)"}),o.jsx("option",{value:"assets-desc",className:"text-gray-900",children:"Assets (High to Low)"}),o.jsx("option",{value:"assets-asc",className:"text-gray-900",children:"Assets (Low to High)"})]})]}),o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"Category (NTEE)"}),o.jsxs("select",{value:j,onChange:X=>{E(X.target.value),p(1),setTimeout(()=>U(),0)},className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-900 bg-white",disabled:!c.includes("organizations"),children:[o.jsx("option",{value:"",className:"text-gray-900",children:"All Categories"}),o.jsx("option",{value:"A",className:"text-gray-900",children:"Arts & Culture"}),o.jsx("option",{value:"B",className:"text-gray-900",children:"Education"}),o.jsx("option",{value:"C",className:"text-gray-900",children:"Environment"}),o.jsx("option",{value:"D",className:"text-gray-900",children:"Animal-Related"}),o.jsx("option",{value:"E",className:"text-gray-900",children:"Health"}),o.jsx("option",{value:"F",className:"text-gray-900",children:"Mental Health"}),o.jsx("option",{value:"G",className:"text-gray-900",children:"Diseases"}),o.jsx("option",{value:"H",className:"text-gray-900",children:"Medical Research"}),o.jsx("option",{value:"I",className:"text-gray-900",children:"Crime & Legal"}),o.jsx("option",{value:"J",className:"text-gray-900",children:"Employment"}),o.jsx("option",{value:"K",className:"text-gray-900",children:"Food & Agriculture"}),o.jsx("option",{value:"L",className:"text-gray-900",children:"Housing"}),o.jsx("option",{value:"M",className:"text-gray-900",children:"Public Safety"}),o.jsx("option",{value:"N",className:"text-gray-900",children:"Recreation & Sports"}),o.jsx("option",{value:"O",className:"text-gray-900",children:"Youth Development"}),o.jsx("option",{value:"P",className:"text-gray-900",children:"Human Services"}),o.jsx("option",{value:"Q",className:"text-gray-900",children:"International"}),o.jsx("option",{value:"R",className:"text-gray-900",children:"Civil Rights"}),o.jsx("option",{value:"S",className:"text-gray-900",children:"Community"}),o.jsx("option",{value:"T",className:"text-gray-900",children:"Philanthropy"}),o.jsx("option",{value:"U",className:"text-gray-900",children:"Science"}),o.jsx("option",{value:"V",className:"text-gray-900",children:"Social Science"}),o.jsx("option",{value:"W",className:"text-gray-900",children:"Public Affairs"}),o.jsx("option",{value:"X",className:"text-gray-900",children:"Religion"}),o.jsx("option",{value:"Y",className:"text-gray-900",children:"Mutual Benefit"})]})]})]}),o.jsx("div",{className:"mt-4",children:o.jsx("button",{onClick:()=>{h(""),b("relevance"),E(""),setTimeout(()=>U(),0)},className:"px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors",children:"Clear All Filters"})})]})]}),(a||d||R)&&o.jsxs("div",{children:[K&&o.jsxs("div",{className:"text-center py-12",children:[o.jsx("div",{className:"inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"}),o.jsx("p",{className:"mt-4 text-gray-600",children:"Searching..."})]}),W&&o.jsx("div",{className:"bg-red-50 border border-red-200 rounded-lg p-6 text-center",children:o.jsx("p",{className:"text-red-600",children:"Error loading search results. Please try again."})}),R&&R.total_results!==void 0&&R.pagination&&o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"mb-6",children:[o.jsx("h2",{className:"text-xl font-semibold text-gray-900",children:R.query?o.jsxs(o.Fragment,{children:[R.total_results.toLocaleString(),' results for "',R.query,'"',R.total_results>0&&o.jsxs("span",{className:"text-base font-normal text-gray-600 ml-2",children:["(showing ",R.pagination.offset+1,"-",Math.min(R.pagination.offset+R.pagination.limit,R.total_results),")"]})]}):o.jsxs(o.Fragment,{children:[R.total_results.toLocaleString()," results",R.total_results>0&&o.jsxs("span",{className:"text-base font-normal text-gray-600 ml-2",children:["(showing ",R.pagination.offset+1,"-",Math.min(R.pagination.offset+R.pagination.limit,R.total_results),")"]})]})}),d&&o.jsxs("p",{className:"text-sm text-gray-600 mt-1",children:["Filtered by state: ",d]})]}),P.length>0&&o.jsxs("div",{className:"mb-6 p-6 bg-gradient-to-br from-teal-50 to-blue-50 rounded-xl border border-teal-200",children:[o.jsxs("h3",{className:"text-lg font-semibold text-gray-900 mb-3 flex items-center gap-2",children:[o.jsx(Rn,{className:"h-6 w-6 text-teal-600"}),"Your Jurisdictions"]}),o.jsxs("p",{className:"text-sm text-gray-600 mb-4",children:["When you select a city, you're connected to ",P.length," levels of government:"]}),o.jsx("div",{className:"space-y-3",children:P.map((X,de)=>{const Ae=T.has(de);return o.jsxs("div",{className:"bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden transition-all duration-200 hover:shadow-md",children:[o.jsxs("button",{onClick:()=>ee(de),className:"w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 transition-colors",children:[o.jsxs("div",{className:"flex-1",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("span",{className:"font-semibold text-gray-900",children:X.type}),o.jsx("span",{className:"text-gray-400",children:"•"}),o.jsx("span",{className:"text-gray-700",children:X.name})]}),!Ae&&o.jsx("div",{className:"text-sm text-gray-500 mt-1",children:"Click to view details and discover data sources"})]}),o.jsxs("div",{className:"flex items-center gap-3",children:[o.jsx(xb,{className:"h-5 w-5 text-green-600 flex-shrink-0"}),Ae?o.jsx(bb,{className:"h-5 w-5 text-gray-400 flex-shrink-0"}):o.jsx(Vd,{className:"h-5 w-5 text-gray-400 flex-shrink-0"})]})]}),Ae&&o.jsx("div",{className:"px-4 pb-4 border-t border-gray-100 bg-gray-50",children:o.jsxs("div",{className:"mt-4 space-y-3",children:[o.jsxs("div",{className:"grid grid-cols-2 gap-4",children:[o.jsxs("div",{children:[o.jsx("div",{className:"text-xs font-medium text-gray-500 uppercase tracking-wide mb-1",children:"Type"}),o.jsx("div",{className:"text-sm text-gray-900",children:X.type})]}),o.jsxs("div",{children:[o.jsx("div",{className:"text-xs font-medium text-gray-500 uppercase tracking-wide mb-1",children:"Name"}),o.jsx("div",{className:"text-sm text-gray-900",children:X.name})]}),X.count!==void 0&&o.jsxs("div",{children:[o.jsx("div",{className:"text-xs font-medium text-gray-500 uppercase tracking-wide mb-1",children:"Count"}),o.jsx("div",{className:"text-sm text-gray-900",children:X.count.toLocaleString()})]})]}),o.jsx("div",{className:"pt-3 border-t border-gray-200",children:o.jsx("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-4",children:o.jsxs("div",{className:"flex items-start gap-3",children:[o.jsx("div",{className:"flex-shrink-0",children:o.jsx(wb,{className:"h-5 w-5 text-blue-600"})}),o.jsxs("div",{className:"flex-1",children:[o.jsx("h4",{className:"text-sm font-semibold text-blue-900 mb-1",children:"Automated Data Discovery"}),o.jsx("p",{className:"text-xs text-blue-700 mb-3",children:"Automatically find official websites, meeting agendas, YouTube channels, and social media for this jurisdiction."}),o.jsxs("button",{onClick:Pt=>{Pt.stopPropagation();const on=`${X.name} ${X.type}`;e(`/discovery?q=${encodeURIComponent(on)}&jurisdiction=${encodeURIComponent(X.name)}`)},className:"inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors",children:[o.jsx(fn,{className:"h-4 w-4"}),"Discover Data Sources"]})]})]})})}),o.jsxs("div",{className:"pt-3 border-t border-gray-200",children:[o.jsx("div",{className:"text-xs font-medium text-gray-500 uppercase tracking-wide mb-2",children:"What We'll Discover"}),o.jsxs("div",{className:"grid grid-cols-2 gap-2 text-sm",children:[o.jsxs("div",{className:"flex items-center gap-2 text-gray-700",children:[o.jsx(wb,{className:"h-4 w-4 text-gray-400"}),"Official Website"]}),o.jsxs("div",{className:"flex items-center gap-2 text-gray-700",children:[o.jsx(ha,{className:"h-4 w-4 text-gray-400"}),"Meeting Agendas"]}),o.jsxs("div",{className:"flex items-center gap-2 text-gray-700",children:[o.jsx(AA,{className:"h-4 w-4 text-gray-400"}),"YouTube Channels"]}),o.jsxs("div",{className:"flex items-center gap-2 text-gray-700",children:[o.jsx(Ci,{className:"h-4 w-4 text-gray-400"}),"Social Media"]})]})]})]})})]},de)})}),o.jsx("div",{className:"mt-4 p-4 bg-blue-100 rounded-lg",children:o.jsxs("p",{className:"text-sm text-blue-900",children:[o.jsx("strong",{children:"💡 Why this matters:"})," Each jurisdiction has its own meetings, budgets, and leaders that affect your daily life. Track all of them in one place."]})})]}),c.includes("contacts")&&((ye=R.results)==null?void 0:ye.contacts)&&R.results.contacts.length>0&&o.jsxs("div",{className:"mb-8",children:[o.jsxs("h3",{className:"text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2",children:[o.jsx(Li,{className:"h-6 w-6 text-blue-600"}),"People (",((ie=(je=R.type_totals)==null?void 0:je.contacts)==null?void 0:ie.toLocaleString())||R.results.contacts.length,")"]}),o.jsx("div",{className:"grid grid-cols-1 gap-4",children:R.results.contacts.map((X,de)=>o.jsx(se,{result:X},de))})]}),c.includes("meetings")&&((Ve=R.results)==null?void 0:Ve.meetings)&&R.results.meetings.length>0&&o.jsxs("div",{className:"mb-8",children:[o.jsxs("h3",{className:"text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2",children:[o.jsx(ha,{className:"h-6 w-6 text-green-600"}),"Meetings (",((ut=(Re=R.type_totals)==null?void 0:Re.meetings)==null?void 0:ut.toLocaleString())||R.results.meetings.length,")"]}),o.jsx("div",{className:"grid grid-cols-1 gap-4",children:R.results.meetings.map((X,de)=>o.jsx(se,{result:X},de))})]}),c.includes("organizations")&&((dt=R.results)==null?void 0:dt.organizations)&&R.results.organizations.length>0&&o.jsxs("div",{className:"mb-8",children:[o.jsxs("h3",{className:"text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2",children:[o.jsx(Ci,{className:"h-6 w-6 text-purple-600"}),"Organizations (",((pn=(Tt=R.type_totals)==null?void 0:Tt.organizations)==null?void 0:pn.toLocaleString())||R.results.organizations.length,")"]}),o.jsx("div",{className:"grid grid-cols-1 gap-4",children:R.results.organizations.map((X,de)=>o.jsx(se,{result:X},de))})]}),c.includes("causes")&&((Gr=R.results)==null?void 0:Gr.causes)&&R.results.causes.length>0&&o.jsxs("div",{className:"mb-8",children:[o.jsxs("h3",{className:"text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2",children:[o.jsx($t,{className:"h-6 w-6 text-pink-600"}),"Causes (",((Wn=(yi=R.type_totals)==null?void 0:yi.causes)==null?void 0:Wn.toLocaleString())||R.results.causes.length,")"]}),o.jsx("div",{className:"grid grid-cols-1 gap-4",children:R.results.causes.map((X,de)=>o.jsx(se,{result:X},de))})]}),c.includes("jurisdictions")&&((nr=R.results)==null?void 0:nr.jurisdictions)&&R.results.jurisdictions.length>0&&o.jsxs("div",{className:"mb-8",children:[o.jsxs("h3",{className:"text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2",children:[o.jsx(Rn,{className:"h-6 w-6 text-orange-600"}),"Jurisdictions (",(($o=(Zi=R.type_totals)==null?void 0:Zi.jurisdictions)==null?void 0:$o.toLocaleString())||R.results.jurisdictions.length,")"]}),o.jsx("div",{className:"grid grid-cols-1 gap-4",children:R.results.jurisdictions.map((X,de)=>o.jsx(se,{result:X},de))})]}),R.total_results===0&&o.jsxs("div",{className:"text-center py-12 bg-white rounded-lg border border-gray-200",children:[o.jsx(fn,{className:"h-16 w-16 text-gray-300 mx-auto mb-4"}),o.jsx("h3",{className:"text-lg font-semibold text-gray-900 mb-2",children:"No results found"}),o.jsx("p",{className:"text-gray-600",children:"Try different keywords or adjust your filters"})]}),R.total_results>0&&R.pagination.total_pages>1&&o.jsxs("div",{className:"mt-8 flex items-center justify-between bg-white rounded-lg border border-gray-200 p-4",children:[o.jsxs("div",{className:"text-sm text-gray-600",children:["Page ",R.pagination.page," of ",R.pagination.total_pages,o.jsx("span",{className:"ml-2",children:"•"}),o.jsxs("span",{className:"ml-2",children:[R.total_results," total results"]})]}),o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("button",{onClick:()=>Y(R.pagination.page-1),disabled:!R.pagination.has_prev,className:`px-4 py-2 rounded-lg font-medium transition-colors ${R.pagination.has_prev?"bg-primary-600 text-white hover:bg-primary-700":"bg-gray-100 text-gray-400 cursor-not-allowed"}`,children:"← Previous"}),o.jsx("div",{className:"flex items-center gap-1",children:Array.from({length:Math.min(5,R.pagination.total_pages)},(X,de)=>{const Ae=Math.max(1,Math.min(R.pagination.page-2+de,R.pagination.total_pages-4))+Math.min(de,4);return Ae>R.pagination.total_pages?null:o.jsx("button",{onClick:()=>Y(Ae),className:`px-3 py-1 rounded ${Ae===R.pagination.page?"bg-primary-600 text-white font-semibold":"bg-gray-100 text-gray-700 hover:bg-gray-200"}`,children:Ae},Ae)})}),o.jsx("button",{onClick:()=>Y(R.pagination.page+1),disabled:!R.pagination.has_next,className:`px-4 py-2 rounded-lg font-medium transition-colors ${R.pagination.has_next?"bg-primary-600 text-white hover:bg-primary-700":"bg-gray-100 text-gray-400 cursor-not-allowed"}`,children:"Next →"})]})]})]})]}),!a&&o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-8",children:[(d||c.length<5)&&o.jsxs("div",{className:"mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg",children:[o.jsxs("p",{className:"text-blue-800 font-medium",children:[d&&`State filter: ${d}`,d&&c.length<5&&" • ",c.length<5&&`Type filter: ${c.join(", ")}`]}),o.jsx("p",{className:"text-blue-700 text-sm mt-1",children:"Enter a search query above to see results with these filters applied."})]}),o.jsx("h2",{className:"text-xl font-semibold text-gray-900 mb-6",children:"Try searching for:"}),o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-4",children:[{query:"dental health",icon:$t,description:"Find organizations and meetings about dental health"},{query:"affordable housing",icon:Ci,description:"Discover housing-related initiatives"},{query:"school board",icon:ha,description:"View school board meetings and decisions"},{query:"mental health",icon:$t,description:"Explore mental health programs and services"}].map((X,de)=>o.jsxs("button",{onClick:()=>{i(X.query),s(X.query)},className:"text-left p-4 border-2 border-gray-200 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-colors",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-2",children:[o.jsx(X.icon,{className:"h-6 w-6 text-primary-600"}),o.jsx("span",{className:"font-semibold text-gray-900",children:X.query})]}),o.jsx("p",{className:"text-sm text-gray-600",children:X.description})]},de))})]})]})})}function r0e({jurisdiction:e}){var i,a,s,c,u;const[t,n]=N.useState(!1),r=e.website||((i=e.youtube_channels)==null?void 0:i.length)||e.facebook;return o.jsxs("div",{className:"border border-gray-200 rounded-lg bg-white hover:shadow-md transition-shadow",children:[o.jsx("div",{className:"p-4",children:o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsxs("div",{className:"flex-1",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx(ci,{className:"h-5 w-5 text-green-600"}),o.jsxs("h3",{className:"font-bold text-gray-900 uppercase",children:[e.name,", ",e.state," - DISCOVERY COMPLETE!"]})]}),o.jsxs("div",{className:"mt-2 flex flex-wrap gap-3 text-sm text-gray-600",children:[e.website&&o.jsxs("span",{className:"flex items-center gap-1",children:[o.jsx(wb,{className:"h-4 w-4"}),"Website"]}),e.youtube_channels&&e.youtube_channels.length>0&&o.jsxs("span",{className:"flex items-center gap-1",children:[o.jsx(AA,{className:"h-4 w-4"}),e.youtube_channels.length," YouTube Channel",e.youtube_channels.length>1?"s":""]}),e.agenda_portal&&o.jsxs("span",{className:"flex items-center gap-1",children:[o.jsx(Ws,{className:"h-4 w-4"}),"Agenda Portal"]}),(e.facebook||e.twitter)&&o.jsxs("span",{className:"flex items-center gap-1",children:[o.jsx(gz,{className:"h-4 w-4"}),"Social Media"]})]}),r&&o.jsxs("div",{className:"mt-3",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("div",{className:"flex-1 bg-gray-200 rounded-full h-2",children:o.jsx("div",{className:"bg-green-600 h-2 rounded-full transition-all",style:{width:`${e.completeness}%`}})}),o.jsxs("span",{className:"text-sm font-medium text-gray-700",children:[e.completeness,"%"]})]}),o.jsxs("p",{className:"text-xs text-gray-500 mt-1",children:["Completeness: ~",Math.round(e.completeness),"% - ",e.completeness>=75?"Good":e.completeness>=50?"Fair":"Limited"," digital infrastructure!"]})]})]}),r&&o.jsx("button",{onClick:()=>n(!t),className:"ml-4 p-2 hover:bg-gray-100 rounded-lg transition-colors",children:t?o.jsx(bb,{className:"h-5 w-5 text-gray-600"}):o.jsx(Vd,{className:"h-5 w-5 text-gray-600"})})]})}),t&&r&&o.jsxs("div",{className:"border-t border-gray-200 p-4 bg-gray-50",children:[o.jsxs("h4",{className:"text-lg font-bold text-gray-900 mb-4",children:["🎯 ",e.name.toUpperCase(),", ",e.state," FINDINGS"]}),o.jsxs("div",{className:"space-y-4",children:[e.website&&o.jsxs("div",{children:[o.jsx("h5",{className:"font-semibold text-gray-700 mb-2",children:"🌐 Official Website:"}),o.jsxs("a",{href:e.website,target:"_blank",rel:"noopener noreferrer",className:"text-blue-600 hover:underline flex items-center gap-2",children:["✅ ",e.website]})]}),e.agenda_portal&&o.jsxs("div",{children:[o.jsx("h5",{className:"font-semibold text-gray-700 mb-2",children:"📄 Meeting/Agenda Portal:"}),o.jsxs("a",{href:e.agenda_portal,target:"_blank",rel:"noopener noreferrer",className:"text-blue-600 hover:underline flex items-center gap-2",children:["✅ ",e.agenda_portal]})]}),e.youtube_channels&&e.youtube_channels.length>0&&o.jsxs("div",{children:[o.jsx("h5",{className:"font-semibold text-gray-700 mb-2",children:"📺 YouTube Channels:"}),e.youtube_channels.map((d,h)=>o.jsxs("div",{className:"ml-4 text-blue-600 hover:underline",children:["✅ @",d]},h))]}),(e.facebook||e.twitter)&&o.jsxs("div",{children:[o.jsx("h5",{className:"font-semibold text-gray-700 mb-2",children:"📱 Social Media:"}),o.jsxs("div",{className:"ml-4 space-y-1",children:[e.facebook&&o.jsxs("div",{className:"text-blue-600",children:["✅ Facebook: ",e.facebook]}),e.twitter&&o.jsxs("div",{className:"text-blue-600",children:["✅ Twitter: ",e.twitter]})]})]}),e.meeting_platform&&o.jsxs("div",{children:[o.jsx("h5",{className:"font-semibold text-gray-700 mb-2",children:"🏛️ Meeting Platform:"}),o.jsx("div",{className:"ml-4",children:e.meeting_platform})]}),o.jsxs("div",{className:"mt-6 border-t border-gray-300 pt-4",children:[o.jsxs("h5",{className:"font-semibold text-gray-700 mb-3",children:["📊 ",e.name.toUpperCase()," SUMMARY"]}),o.jsxs("table",{className:"min-w-full divide-y divide-gray-200 text-sm",children:[o.jsx("thead",{children:o.jsxs("tr",{className:"bg-gray-100",children:[o.jsx("th",{className:"px-3 py-2 text-left font-semibold",children:"Category"}),o.jsx("th",{className:"px-3 py-2 text-left font-semibold",children:"Found"}),o.jsx("th",{className:"px-3 py-2 text-left font-semibold",children:"Details"})]})}),o.jsxs("tbody",{className:"divide-y divide-gray-200",children:[o.jsxs("tr",{children:[o.jsx("td",{className:"px-3 py-2",children:"Website"}),o.jsx("td",{className:"px-3 py-2",children:e.website?"✅":"❌"}),o.jsx("td",{className:"px-3 py-2 text-gray-600",children:e.website?new URL(e.website).hostname:"Not found"})]}),o.jsxs("tr",{children:[o.jsx("td",{className:"px-3 py-2",children:"YouTube"}),o.jsx("td",{className:"px-3 py-2",children:(a=e.youtube_channels)!=null&&a.length?"✅":"❌"}),o.jsxs("td",{className:"px-3 py-2 text-gray-600",children:[((s=e.youtube_channels)==null?void 0:s.length)||0," channel",((c=e.youtube_channels)==null?void 0:c.length)!==1?"s":""]})]}),o.jsxs("tr",{children:[o.jsx("td",{className:"px-3 py-2",children:"Agendas"}),o.jsx("td",{className:"px-3 py-2",children:e.agenda_portal?"✅":"❌"}),o.jsx("td",{className:"px-3 py-2 text-gray-600",children:e.agenda_portal?"Portal found":"Not available"})]}),o.jsxs("tr",{children:[o.jsx("td",{className:"px-3 py-2",children:"Social"}),o.jsx("td",{className:"px-3 py-2",children:e.facebook||e.twitter?"✅":"❌"}),o.jsx("td",{className:"px-3 py-2 text-gray-600",children:[e.facebook&&"Facebook",e.twitter&&"Twitter"].filter(Boolean).join(", ")||"None"})]}),o.jsxs("tr",{children:[o.jsx("td",{className:"px-3 py-2",children:"Platform"}),o.jsx("td",{className:"px-3 py-2",children:e.meeting_platform?"✅":"❌"}),o.jsx("td",{className:"px-3 py-2 text-gray-600",children:e.meeting_platform||"Unknown"})]})]})]})]}),o.jsxs("div",{className:"mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg",children:[o.jsx("h5",{className:"font-semibold text-blue-900 mb-2",children:"💡 KEY TAKEAWAY"}),o.jsx("p",{className:"text-sm text-blue-800",children:"The automation successfully discovered:"}),o.jsxs("ul",{className:"mt-2 space-y-1 text-sm text-blue-800",children:[e.website&&o.jsx("li",{children:"✅ Official website (automatic)"}),(((u=e.youtube_channels)==null?void 0:u.length)??0)>0&&o.jsx("li",{children:"✅ YouTube channels (automatic)"}),e.agenda_portal&&o.jsx("li",{children:"✅ Agenda portal (found via link scanning)"}),(e.facebook||e.twitter)&&o.jsx("li",{children:"✅ Social media (automatic)"})]})]})]})]}),!r&&o.jsx("div",{className:"border-t border-gray-200 p-4 bg-gray-50 text-center text-gray-500",children:o.jsx("p",{children:"No discovery data available yet. Run discovery pipeline to populate."})})]})}const Yy=[{id:"city",label:"Cities",icon:"🏙️"},{id:"county",label:"Counties",icon:"🏛️"},{id:"state",label:"States",icon:"🗺️"},{id:"school_district",label:"School Districts",icon:"🎓"},{id:"special_district",label:"Special Districts",icon:"⚙️"},{id:"town",label:"Towns",icon:"🏘️"},{id:"village",label:"Villages",icon:"🏡"}];function i0e(){const[e,t]=Us(),[n,r]=N.useState(()=>e.get("q")||""),[i,a]=N.useState(()=>e.get("q")||""),[s,c]=N.useState(()=>{const $=e.get("levels");return $?$.split(",").filter(z=>Yy.some(D=>D.id===z)):[]}),[u,d]=N.useState(()=>e.get("state")||""),[h,m]=N.useState(()=>e.get("city")||""),[p,v]=N.useState(()=>e.get("county")||""),[_,x]=N.useState(()=>parseInt(e.get("page")||"1")),[y,w]=N.useState(!1),b=N.useRef(null);N.useEffect(()=>{const $=e.get("q"),z=e.get("state"),D=e.get("city"),Z=e.get("county"),I=e.get("levels"),F=e.get("page");if($&&(r($),a($)),z&&d(z),D&&m(D),Z&&v(Z),I){const B=I.split(",").filter(G=>Yy.some(R=>R.id===G));B.length>0&&c(B)}F&&x(parseInt(F))},[e]);const{data:j,isLoading:E,error:P}=zt({queryKey:["jurisdictions-search",i,s,u,h,p,_],queryFn:async()=>{if(!i&&!u&&!h&&!p&&!s.length)return null;const $={types:"jurisdictions",limit:20,page:_};return i&&($.q=i),u&&($.state=u),h&&($.city=h),p&&($.county=p),s.length>0&&($.jurisdiction_levels=s.join(",")),(await vt.get("/search/",{params:$})).data},enabled:i&&i.length>=2||u!==""||h!==""||p!==""||s.length>0}),O=$=>{if($==null||$.preventDefault(),n.trim().length>=2||u||h||p||s.length>0){a(n),x(1);const z={};n.trim()&&(z.q=n),u&&(z.state=u),h&&(z.city=h),p&&(z.county=p),s.length>0&&(z.levels=s.join(",")),t(z)}},C=$=>{x($);const z={};i&&(z.q=i),u&&(z.state=u),s.length>0&&(z.levels=s.join(",")),$>1&&(z.page=$.toString()),t(z),window.scrollTo({top:0,behavior:"smooth"})},A=$=>{const z=s.includes($)?s.filter(Z=>Z!==$):[...s,$];c(z),x(1);const D={};i&&(D.q=i),u&&(D.state=u),z.length>0&&(D.levels=z.join(",")),t(D)},T=$=>({city:"bg-blue-100 text-blue-700 border-blue-200",county:"bg-purple-100 text-purple-700 border-purple-200",state:"bg-green-100 text-green-700 border-green-200",school_district:"bg-yellow-100 text-yellow-700 border-yellow-200",special_district:"bg-orange-100 text-orange-700 border-orange-200",town:"bg-teal-100 text-teal-700 border-teal-200",village:"bg-pink-100 text-pink-700 border-pink-200"})[$]||"bg-gray-100 text-gray-700 border-gray-200";return o.jsx("div",{className:"min-h-screen bg-gray-50",children:o.jsxs("div",{className:"max-w-6xl mx-auto px-6 pb-6",children:[o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 mb-6",children:[o.jsxs("div",{className:"flex items-center gap-2 mb-4",children:[o.jsx(Rn,{className:"h-8 w-8 text-primary-600"}),o.jsx("h1",{className:"text-3xl font-bold text-gray-900",children:"Jurisdiction Search"})]}),o.jsx("p",{className:"text-gray-600 mb-4",children:"Search across 90,000+ cities, counties, states, and school districts"}),o.jsx("form",{onSubmit:O,className:"relative",children:o.jsxs("div",{className:"relative",children:[o.jsx("input",{ref:b,type:"text",value:n,onChange:$=>r($.target.value),placeholder:"Search for cities, counties, states, school districts...",className:"w-full px-12 py-3 border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent text-lg text-gray-900"}),o.jsx(fn,{className:"absolute left-4 top-3.5 h-6 w-6 text-gray-400"}),n&&o.jsx("button",{type:"button",onClick:()=>{var $;r(""),a(""),($=b.current)==null||$.focus()},className:"absolute right-4 top-3.5 text-gray-400 hover:text-gray-600",children:o.jsx(Cr,{className:"h-6 w-6"})})]})}),o.jsxs("div",{className:"mt-4 flex items-center gap-3 flex-wrap",children:[o.jsxs("button",{onClick:()=>w(!y),className:`flex items-center gap-2 px-4 py-2 rounded-lg border-2 transition-colors ${y?"border-primary-500 bg-primary-50 text-primary-700":"border-gray-300 text-gray-700 hover:border-gray-400 hover:bg-gray-50"}`,children:[o.jsx(jA,{className:"h-5 w-5"}),"Filters",u&&o.jsx("span",{className:"ml-1 px-2 py-0.5 bg-primary-600 text-white text-xs rounded-full",children:"1"})]}),o.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[o.jsx("span",{className:"text-sm text-gray-600 font-medium",children:"Levels:"}),Yy.map($=>o.jsxs("button",{onClick:()=>A($.id),className:`flex items-center gap-2 px-4 py-2 rounded-full border-2 transition-all ${s.includes($.id)?`${T($.id)} border-current font-medium shadow-sm`:"border-gray-300 bg-white text-gray-600 hover:border-gray-400 hover:bg-gray-50"}`,children:[s.includes($.id)&&o.jsx(xb,{className:"h-4 w-4 flex-shrink-0"}),o.jsx("span",{children:$.icon}),o.jsx("span",{children:$.label})]},$.id))]})]}),(u||h||p||s.length>0)&&o.jsxs("div",{className:"mt-3 flex items-center gap-2 flex-wrap",children:[o.jsx("span",{className:"text-sm text-gray-600",children:"Active filters:"}),u&&o.jsxs("span",{className:"inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm",children:["State: ",u,o.jsx("button",{onClick:()=>{d(""),setTimeout(()=>O(),0)},className:"hover:bg-blue-200 rounded-full p-0.5",children:o.jsx(Cr,{className:"h-3 w-3"})})]}),h&&o.jsxs("span",{className:"inline-flex items-center gap-1 px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm",children:["City: ",h,o.jsx("button",{onClick:()=>{m(""),setTimeout(()=>O(),0)},className:"hover:bg-green-200 rounded-full p-0.5",children:o.jsx(Cr,{className:"h-3 w-3"})})]}),p&&o.jsxs("span",{className:"inline-flex items-center gap-1 px-3 py-1 bg-amber-100 text-amber-800 rounded-full text-sm",children:["County: ",p,o.jsx("button",{onClick:()=>{v(""),setTimeout(()=>O(),0)},className:"hover:bg-amber-200 rounded-full p-0.5",children:o.jsx(Cr,{className:"h-3 w-3"})})]}),s.length>0&&o.jsxs("span",{className:"inline-flex items-center gap-1 px-3 py-1 bg-purple-100 text-purple-800 rounded-full text-sm",children:[s.length," Level",s.length>1?"s":"",o.jsx("button",{onClick:()=>{c([]),setTimeout(()=>O(),0)},className:"hover:bg-purple-200 rounded-full p-0.5",children:o.jsx(Cr,{className:"h-3 w-3"})})]})]}),y&&o.jsxs("div",{className:"mt-4 p-4 bg-gray-50 rounded-lg border border-gray-200",children:[o.jsx("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-4",children:o.jsxs("div",{children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-2",children:"State"}),o.jsxs("select",{value:u,onChange:$=>{d($.target.value),x(1),setTimeout(()=>O(),0)},className:"w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-900 bg-white",children:[o.jsx("option",{value:"",className:"text-gray-900",children:"All States"}),o.jsx("option",{value:"AL",className:"text-gray-900",children:"Alabama"}),o.jsx("option",{value:"AK",className:"text-gray-900",children:"Alaska"}),o.jsx("option",{value:"AZ",className:"text-gray-900",children:"Arizona"}),o.jsx("option",{value:"AR",className:"text-gray-900",children:"Arkansas"}),o.jsx("option",{value:"CA",className:"text-gray-900",children:"California"}),o.jsx("option",{value:"CO",className:"text-gray-900",children:"Colorado"}),o.jsx("option",{value:"CT",className:"text-gray-900",children:"Connecticut"}),o.jsx("option",{value:"DE",className:"text-gray-900",children:"Delaware"}),o.jsx("option",{value:"FL",className:"text-gray-900",children:"Florida"}),o.jsx("option",{value:"GA",className:"text-gray-900",children:"Georgia"}),o.jsx("option",{value:"HI",className:"text-gray-900",children:"Hawaii"}),o.jsx("option",{value:"ID",className:"text-gray-900",children:"Idaho"}),o.jsx("option",{value:"IL",className:"text-gray-900",children:"Illinois"}),o.jsx("option",{value:"IN",className:"text-gray-900",children:"Indiana"}),o.jsx("option",{value:"IA",className:"text-gray-900",children:"Iowa"}),o.jsx("option",{value:"KS",className:"text-gray-900",children:"Kansas"}),o.jsx("option",{value:"KY",className:"text-gray-900",children:"Kentucky"}),o.jsx("option",{value:"LA",className:"text-gray-900",children:"Louisiana"}),o.jsx("option",{value:"ME",className:"text-gray-900",children:"Maine"}),o.jsx("option",{value:"MD",className:"text-gray-900",children:"Maryland"}),o.jsx("option",{value:"MA",className:"text-gray-900",children:"Massachusetts"}),o.jsx("option",{value:"MI",className:"text-gray-900",children:"Michigan"}),o.jsx("option",{value:"MN",className:"text-gray-900",children:"Minnesota"}),o.jsx("option",{value:"MS",className:"text-gray-900",children:"Mississippi"}),o.jsx("option",{value:"MO",className:"text-gray-900",children:"Missouri"}),o.jsx("option",{value:"MT",className:"text-gray-900",children:"Montana"}),o.jsx("option",{value:"NE",className:"text-gray-900",children:"Nebraska"}),o.jsx("option",{value:"NV",className:"text-gray-900",children:"Nevada"}),o.jsx("option",{value:"NH",className:"text-gray-900",children:"New Hampshire"}),o.jsx("option",{value:"NJ",className:"text-gray-900",children:"New Jersey"}),o.jsx("option",{value:"NM",className:"text-gray-900",children:"New Mexico"}),o.jsx("option",{value:"NY",className:"text-gray-900",children:"New York"}),o.jsx("option",{value:"NC",className:"text-gray-900",children:"North Carolina"}),o.jsx("option",{value:"ND",className:"text-gray-900",children:"North Dakota"}),o.jsx("option",{value:"OH",className:"text-gray-900",children:"Ohio"}),o.jsx("option",{value:"OK",className:"text-gray-900",children:"Oklahoma"}),o.jsx("option",{value:"OR",className:"text-gray-900",children:"Oregon"}),o.jsx("option",{value:"PA",className:"text-gray-900",children:"Pennsylvania"}),o.jsx("option",{value:"RI",className:"text-gray-900",children:"Rhode Island"}),o.jsx("option",{value:"SC",className:"text-gray-900",children:"South Carolina"}),o.jsx("option",{value:"SD",className:"text-gray-900",children:"South Dakota"}),o.jsx("option",{value:"TN",className:"text-gray-900",children:"Tennessee"}),o.jsx("option",{value:"TX",className:"text-gray-900",children:"Texas"}),o.jsx("option",{value:"UT",className:"text-gray-900",children:"Utah"}),o.jsx("option",{value:"VT",className:"text-gray-900",children:"Vermont"}),o.jsx("option",{value:"VA",className:"text-gray-900",children:"Virginia"}),o.jsx("option",{value:"WA",className:"text-gray-900",children:"Washington"}),o.jsx("option",{value:"WV",className:"text-gray-900",children:"West Virginia"}),o.jsx("option",{value:"WI",className:"text-gray-900",children:"Wisconsin"}),o.jsx("option",{value:"WY",className:"text-gray-900",children:"Wyoming"})]})]})}),o.jsx("div",{className:"mt-4",children:o.jsx("button",{onClick:()=>{d(""),c([]),setTimeout(()=>O(),0)},className:"px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors",children:"Clear All Filters"})})]})]}),(i||u||s.length>0||j)&&o.jsxs("div",{children:[E&&o.jsxs("div",{className:"text-center py-12",children:[o.jsx("div",{className:"inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"}),o.jsx("p",{className:"mt-4 text-gray-600",children:"Searching..."})]}),P&&o.jsx("div",{className:"bg-red-50 border border-red-200 rounded-lg p-6 text-center",children:o.jsx("p",{className:"text-red-600",children:"Error loading search results. Please try again."})}),j&&j.total_results!==void 0&&j.pagination&&o.jsxs(o.Fragment,{children:[o.jsxs("div",{className:"mb-6",children:[o.jsx("h2",{className:"text-xl font-semibold text-gray-900",children:j.query?o.jsxs(o.Fragment,{children:[j.total_results.toLocaleString(),' jurisdictions for "',j.query,'"',j.total_results>0&&o.jsxs("span",{className:"text-base font-normal text-gray-600 ml-2",children:["(showing ",j.pagination.offset+1,"-",Math.min(j.pagination.offset+j.pagination.limit,j.total_results),")"]})]}):o.jsxs(o.Fragment,{children:[j.total_results.toLocaleString()," jurisdictions",j.total_results>0&&o.jsxs("span",{className:"text-base font-normal text-gray-600 ml-2",children:["(showing ",j.pagination.offset+1,"-",Math.min(j.pagination.offset+j.pagination.limit,j.total_results),")"]})]})}),u&&o.jsxs("p",{className:"text-sm text-gray-600 mt-1",children:["Filtered by state: ",u]})]}),o.jsx("div",{className:"space-y-4",children:j.results.jurisdictions.map(($,z)=>o.jsx(r0e,{jurisdiction:{name:$.title,state:$.metadata.state||"",website:$.metadata.website,youtube_channels:$.metadata.youtube_channels,facebook:$.metadata.facebook,twitter:$.metadata.twitter,agenda_portal:$.metadata.agenda_portal,meeting_platform:$.metadata.meeting_platform,completeness:$.metadata.completeness||0}},z))}),j.total_results===0&&o.jsxs("div",{className:"bg-gray-50 border border-gray-200 rounded-lg p-12 text-center",children:[o.jsx(Rn,{className:"h-16 w-16 text-gray-400 mx-auto mb-4"}),o.jsx("h3",{className:"text-xl font-semibold text-gray-900 mb-2",children:"No jurisdictions found"}),o.jsx("p",{className:"text-gray-600",children:"Try adjusting your search terms or filters"})]}),j.total_results>0&&j.pagination.total_pages>1&&o.jsxs("div",{className:"mt-8 flex items-center justify-center gap-2",children:[o.jsx("button",{onClick:()=>C(_-1),disabled:!j.pagination.has_prev,className:`px-4 py-2 rounded-lg ${j.pagination.has_prev?"bg-white border border-gray-300 text-gray-700 hover:bg-gray-50":"bg-gray-100 text-gray-400 cursor-not-allowed"}`,children:"Previous"}),o.jsx("div",{className:"flex items-center gap-2",children:Array.from({length:Math.min(5,j.pagination.total_pages)},($,z)=>{const D=z+1;return o.jsx("button",{onClick:()=>C(D),className:`px-4 py-2 rounded-lg ${_===D?"bg-primary-600 text-white":"bg-white border border-gray-300 text-gray-700 hover:bg-gray-50"}`,children:D},D)})}),o.jsx("button",{onClick:()=>C(_+1),disabled:!j.pagination.has_next,className:`px-4 py-2 rounded-lg ${j.pagination.has_next?"bg-white border border-gray-300 text-gray-700 hover:bg-gray-50":"bg-gray-100 text-gray-400 cursor-not-allowed"}`,children:"Next"})]})]})]})]})})}var we=1e-6,Ef=1e-12,$e=Math.PI,At=$e/2,ug=$e/4,Fn=$e*2,rt=180/$e,_e=$e/180,Ue=Math.abs,su=Math.atan,Dn=Math.atan2,ve=Math.cos,lm=Math.ceil,PL=Math.exp,iw=Math.hypot,dg=Math.log,Xy=Math.pow,me=Math.sin,Rr=Math.sign||function(e){return e>0?1:e<0?-1:0},rn=Math.sqrt,sj=Math.tan;function EL(e){return e>1?0:e<-1?$e:Math.acos(e)}function Bn(e){return e>1?At:e<-1?-At:Math.asin(e)}function PO(e){return(e=me(e/2))*e}function jt(){}function fg(e,t){e&&OO.hasOwnProperty(e.type)&&OO[e.type](e,t)}var EO={Feature:function(e,t){fg(e.geometry,t)},FeatureCollection:function(e,t){for(var n=e.features,r=-1,i=n.length;++r=0?1:-1,i=r*n,a=ve(t),s=me(t),c=lw*s,u=sw*a+c*ve(i),d=c*r*me(i);hg.add(Dn(d,u)),ow=e,sw=a,lw=s}function l0e(e){return mg=new Nn,si(e,zi),mg*2}function pg(e){return[Dn(e[1],e[0]),Bn(e[2])]}function $s(e){var t=e[0],n=e[1],r=ve(n);return[r*ve(t),r*me(t),me(n)]}function cm(e,t){return e[0]*t[0]+e[1]*t[1]+e[2]*t[2]}function Rc(e,t){return[e[1]*t[2]-e[2]*t[1],e[2]*t[0]-e[0]*t[2],e[0]*t[1]-e[1]*t[0]]}function Qy(e,t){e[0]+=t[0],e[1]+=t[1],e[2]+=t[2]}function um(e,t){return[e[0]*t,e[1]*t,e[2]*t]}function gg(e){var t=rn(e[0]*e[0]+e[1]*e[1]+e[2]*e[2]);e[0]/=t,e[1]/=t,e[2]/=t}var bt,Gn,Ot,dr,Qo,AL,TL,Ul,yd,Ga,Aa,ra={point:cw,lineStart:CO,lineEnd:AO,polygonStart:function(){ra.point=LL,ra.lineStart=c0e,ra.lineEnd=u0e,yd=new Nn,zi.polygonStart()},polygonEnd:function(){zi.polygonEnd(),ra.point=cw,ra.lineStart=CO,ra.lineEnd=AO,hg<0?(bt=-(Ot=180),Gn=-(dr=90)):yd>we?dr=90:yd<-we&&(Gn=-90),Aa[0]=bt,Aa[1]=Ot},sphere:function(){bt=-(Ot=180),Gn=-(dr=90)}};function cw(e,t){Ga.push(Aa=[bt=e,Ot=e]),tdr&&(dr=t)}function ML(e,t){var n=$s([e*_e,t*_e]);if(Ul){var r=Rc(Ul,n),i=[r[1],-r[0],0],a=Rc(i,r);gg(a),a=pg(a);var s=e-Qo,c=s>0?1:-1,u=a[0]*rt*c,d,h=Ue(s)>180;h^(c*Qodr&&(dr=d)):(u=(u+360)%360-180,h^(c*Qodr&&(dr=t))),h?esr(bt,Ot)&&(Ot=e):sr(e,Ot)>sr(bt,Ot)&&(bt=e):Ot>=bt?(eOt&&(Ot=e)):e>Qo?sr(bt,e)>sr(bt,Ot)&&(Ot=e):sr(e,Ot)>sr(bt,Ot)&&(bt=e)}else Ga.push(Aa=[bt=e,Ot=e]);tdr&&(dr=t),Ul=n,Qo=e}function CO(){ra.point=ML}function AO(){Aa[0]=bt,Aa[1]=Ot,ra.point=cw,Ul=null}function LL(e,t){if(Ul){var n=e-Qo;yd.add(Ue(n)>180?n+(n>0?360:-360):n)}else AL=e,TL=t;zi.point(e,t),ML(e,t)}function c0e(){zi.lineStart()}function u0e(){LL(AL,TL),zi.lineEnd(),Ue(yd)>we&&(bt=-(Ot=180)),Aa[0]=bt,Aa[1]=Ot,Ul=null}function sr(e,t){return(t-=e)<0?t+360:t}function d0e(e,t){return e[0]-t[0]}function TO(e,t){return e[0]<=e[1]?e[0]<=t&&t<=e[1]:tsr(r[0],r[1])&&(r[1]=i[1]),sr(i[0],r[1])>sr(r[0],r[1])&&(r[0]=i[0])):a.push(r=i);for(s=-1/0,n=a.length-1,t=0,r=a[n];t<=n;r=i,++t)i=a[t],(c=sr(r[1],i[0]))>s&&(s=c,bt=i[0],Ot=r[1])}return Ga=Aa=null,bt===1/0||Gn===1/0?[[NaN,NaN],[NaN,NaN]]:[[bt,Gn],[Ot,dr]]}var Ku,vg,yg,xg,bg,wg,_g,jg,uw,dw,fw,$L,IL,Cn,An,Tn,ui={sphere:jt,point:lj,lineStart:MO,lineEnd:LO,polygonStart:function(){ui.lineStart=p0e,ui.lineEnd=g0e},polygonEnd:function(){ui.lineStart=MO,ui.lineEnd=LO}};function lj(e,t){e*=_e,t*=_e;var n=ve(t);rh(n*ve(e),n*me(e),me(t))}function rh(e,t,n){++Ku,yg+=(e-yg)/Ku,xg+=(t-xg)/Ku,bg+=(n-bg)/Ku}function MO(){ui.point=h0e}function h0e(e,t){e*=_e,t*=_e;var n=ve(t);Cn=n*ve(e),An=n*me(e),Tn=me(t),ui.point=m0e,rh(Cn,An,Tn)}function m0e(e,t){e*=_e,t*=_e;var n=ve(t),r=n*ve(e),i=n*me(e),a=me(t),s=Dn(rn((s=An*a-Tn*i)*s+(s=Tn*r-Cn*a)*s+(s=Cn*i-An*r)*s),Cn*r+An*i+Tn*a);vg+=s,wg+=s*(Cn+(Cn=r)),_g+=s*(An+(An=i)),jg+=s*(Tn+(Tn=a)),rh(Cn,An,Tn)}function LO(){ui.point=lj}function p0e(){ui.point=v0e}function g0e(){RL($L,IL),ui.point=lj}function v0e(e,t){$L=e,IL=t,e*=_e,t*=_e,ui.point=RL;var n=ve(t);Cn=n*ve(e),An=n*me(e),Tn=me(t),rh(Cn,An,Tn)}function RL(e,t){e*=_e,t*=_e;var n=ve(t),r=n*ve(e),i=n*me(e),a=me(t),s=An*a-Tn*i,c=Tn*r-Cn*a,u=Cn*i-An*r,d=iw(s,c,u),h=Bn(d),m=d&&-h/d;uw.add(m*s),dw.add(m*c),fw.add(m*u),vg+=h,wg+=h*(Cn+(Cn=r)),_g+=h*(An+(An=i)),jg+=h*(Tn+(Tn=a)),rh(Cn,An,Tn)}function y0e(e){Ku=vg=yg=xg=bg=wg=_g=jg=0,uw=new Nn,dw=new Nn,fw=new Nn,si(e,ui);var t=+uw,n=+dw,r=+fw,i=iw(t,n,r);return i$e?e+Math.round(-e/Fn)*Fn:e,t]}mw.invert=mw;function cj(e,t,n){return(e%=Fn)?t||n?hw(IO(e),RO(t,n)):IO(e):t||n?RO(t,n):mw}function $O(e){return function(t,n){return t+=e,[t>$e?t-Fn:t<-$e?t+Fn:t,n]}}function IO(e){var t=$O(e);return t.invert=$O(-e),t}function RO(e,t){var n=ve(e),r=me(e),i=ve(t),a=me(t);function s(c,u){var d=ve(u),h=ve(c)*d,m=me(c)*d,p=me(u),v=p*n+h*r;return[Dn(m*i-v*a,h*n-p*r),Bn(v*i+m*a)]}return s.invert=function(c,u){var d=ve(u),h=ve(c)*d,m=me(c)*d,p=me(u),v=p*i-m*a;return[Dn(m*i+p*a,h*n+v*r),Bn(v*n-h*r)]},s}function FL(e){e=cj(e[0]*_e,e[1]*_e,e.length>2?e[2]*_e:0);function t(n){return n=e(n[0]*_e,n[1]*_e),n[0]*=rt,n[1]*=rt,n}return t.invert=function(n){return n=e.invert(n[0]*_e,n[1]*_e),n[0]*=rt,n[1]*=rt,n},t}function DL(e,t,n,r,i,a){if(n){var s=ve(t),c=me(t),u=r*n;i==null?(i=t+r*Fn,a=t-u/2):(i=FO(s,i),a=FO(s,a),(r>0?ia)&&(i+=r*Fn));for(var d,h=i;r>0?h>a:h1&&e.push(e.pop().concat(e.shift()))},result:function(){var n=e;return e=[],t=null,n}}}function Cm(e,t){return Ue(e[0]-t[0])=0;--c)i.point((m=h[c])[0],m[1]);else r(p.x,p.p.x,-1,i);p=p.p}p=p.o,h=p.z,v=!v}while(!p.v);i.lineEnd()}}}function DO(e){if(t=e.length){for(var t,n=0,r=e[0],i;++n=0?1:-1,$=T*A,z=$>$e,D=y*O;if(u.add(Dn(D*T*me($),w*C+D*ve($))),s+=z?A+T*Fn:A,z^_>=n^E>=n){var Z=Rc($s(v),$s(j));gg(Z);var I=Rc(a,Z);gg(I);var F=(z^A>=0?-1:1)*Bn(I[2]);(r>F||r===F&&(Z[0]||Z[1]))&&(c+=z^A>=0?1:-1)}}return(s<-we||s0){for(u||(i.polygonStart(),u=!0),i.lineStart(),O=0;O1&&E&2&&P.push(P.pop().concat(P.shift())),h.push(P.filter(b0e))}}return p}}function b0e(e){return e.length>1}function w0e(e,t){return((e=e.x)[0]<0?e[1]-At-we:At-e[1])-((t=t.x)[0]<0?t[1]-At-we:At-t[1])}const pw=WL(function(){return!0},_0e,N0e,[-$e,-At]);function _0e(e){var t=NaN,n=NaN,r=NaN,i;return{lineStart:function(){e.lineStart(),i=1},point:function(a,s){var c=a>0?$e:-$e,u=Ue(a-t);Ue(u-$e)0?At:-At),e.point(r,n),e.lineEnd(),e.lineStart(),e.point(c,n),e.point(a,n),i=0):r!==c&&u>=$e&&(Ue(t-r)we?su((me(t)*(a=ve(r))*me(n)-me(r)*(i=ve(t))*me(e))/(i*a*s)):(t+r)/2}function N0e(e,t,n,r){var i;if(e==null)i=n*At,r.point(-$e,i),r.point(0,i),r.point($e,i),r.point($e,0),r.point($e,-i),r.point(0,-i),r.point(-$e,-i),r.point(-$e,0),r.point(-$e,i);else if(Ue(e[0]-t[0])>we){var a=e[0]0,i=Ue(t)>we;function a(h,m,p,v){DL(v,e,n,p,h,m)}function s(h,m){return ve(h)*ve(m)>t}function c(h){var m,p,v,_,x;return{lineStart:function(){_=v=!1,x=1},point:function(y,w){var b=[y,w],j,E=s(y,w),P=r?E?0:d(y,w):E?d(y+(y<0?$e:-$e),w):0;if(!m&&(_=v=E)&&h.lineStart(),E!==v&&(j=u(m,b),(!j||Cm(m,j)||Cm(b,j))&&(b[2]=1)),E!==v)x=0,E?(h.lineStart(),j=u(b,m),h.point(j[0],j[1])):(j=u(m,b),h.point(j[0],j[1],2),h.lineEnd()),m=j;else if(i&&m&&r^E){var O;!(P&p)&&(O=u(b,m,!0))&&(x=0,r?(h.lineStart(),h.point(O[0][0],O[0][1]),h.point(O[1][0],O[1][1]),h.lineEnd()):(h.point(O[1][0],O[1][1]),h.lineEnd(),h.lineStart(),h.point(O[0][0],O[0][1],3)))}E&&(!m||!Cm(m,b))&&h.point(b[0],b[1]),m=b,v=E,p=P},lineEnd:function(){v&&h.lineEnd(),m=null},clean:function(){return x|(_&&v)<<1}}}function u(h,m,p){var v=$s(h),_=$s(m),x=[1,0,0],y=Rc(v,_),w=cm(y,y),b=y[0],j=w-b*b;if(!j)return!p&&h;var E=t*w/j,P=-t*b/j,O=Rc(x,y),C=um(x,E),A=um(y,P);Qy(C,A);var T=O,$=cm(C,T),z=cm(T,T),D=$*$-z*(cm(C,C)-1);if(!(D<0)){var Z=rn(D),I=um(T,(-$-Z)/z);if(Qy(I,C),I=pg(I),!p)return I;var F=h[0],B=m[0],G=h[1],R=m[1],K;B0^I[1]<(Ue(I[0]-F)$e^(F<=I[0]&&I[0]<=B)){var ne=um(T,(-$+Z)/z);return Qy(ne,C),[I,pg(ne)]}}}function d(h,m){var p=r?e:$e-e,v=0;return h<-p?v|=1:h>p&&(v|=2),m<-p?v|=4:m>p&&(v|=8),v}return WL(s,c,a,r?[0,-e]:[-$e,e-$e])}function S0e(e,t,n,r,i,a){var s=e[0],c=e[1],u=t[0],d=t[1],h=0,m=1,p=u-s,v=d-c,_;if(_=n-s,!(!p&&_>0)){if(_/=p,p<0){if(_0){if(_>m)return;_>h&&(h=_)}if(_=i-s,!(!p&&_<0)){if(_/=p,p<0){if(_>m)return;_>h&&(h=_)}else if(p>0){if(_0)){if(_/=v,v<0){if(_0){if(_>m)return;_>h&&(h=_)}if(_=a-c,!(!v&&_<0)){if(_/=v,v<0){if(_>m)return;_>h&&(h=_)}else if(v>0){if(_0&&(e[0]=s+h*p,e[1]=c+h*v),m<1&&(t[0]=s+m*p,t[1]=c+m*v),!0}}}}}var Yu=1e9,fm=-Yu;function av(e,t,n,r){function i(d,h){return e<=d&&d<=n&&t<=h&&h<=r}function a(d,h,m,p){var v=0,_=0;if(d==null||(v=s(d,m))!==(_=s(h,m))||u(d,h)<0^m>0)do p.point(v===0||v===3?e:n,v>1?r:t);while((v=(v+m+4)%4)!==_);else p.point(h[0],h[1])}function s(d,h){return Ue(d[0]-e)0?0:3:Ue(d[0]-n)0?2:1:Ue(d[1]-t)0?1:0:h>0?3:2}function c(d,h){return u(d.x,h.x)}function u(d,h){var m=s(d,1),p=s(h,1);return m!==p?m-p:m===0?h[1]-d[1]:m===1?d[0]-h[0]:m===2?d[1]-h[1]:h[0]-d[0]}return function(d){var h=d,m=BL(),p,v,_,x,y,w,b,j,E,P,O,C={point:A,lineStart:D,lineEnd:Z,polygonStart:$,polygonEnd:z};function A(F,B){i(F,B)&&h.point(F,B)}function T(){for(var F=0,B=0,G=v.length;Br&&(ae-Y)*(r-ne)>(ee-ne)*(e-Y)&&++F:ee<=r&&(ae-Y)*(r-ne)<(ee-ne)*(e-Y)&&--F;return F}function $(){h=m,p=[],v=[],O=!0}function z(){var F=T(),B=O&&F,G=(p=t6(p)).length;(B||G)&&(d.polygonStart(),B&&(d.lineStart(),a(null,null,1,d),d.lineEnd()),G&&zL(p,c,F,a,d),d.polygonEnd()),h=d,p=v=_=null}function D(){C.point=I,v&&v.push(_=[]),P=!0,E=!1,b=j=NaN}function Z(){p&&(I(x,y),w&&E&&m.rejoin(),p.push(m.result())),C.point=A,E&&h.lineEnd()}function I(F,B){var G=i(F,B);if(v&&_.push([F,B]),P)x=F,y=B,w=G,P=!1,G&&(h.lineStart(),h.point(F,B));else if(G&&E)h.point(F,B);else{var R=[b=Math.max(fm,Math.min(Yu,b)),j=Math.max(fm,Math.min(Yu,j))],K=[F=Math.max(fm,Math.min(Yu,F)),B=Math.max(fm,Math.min(Yu,B))];S0e(R,K,e,t,n,r)?(E||(h.lineStart(),h.point(R[0],R[1])),h.point(K[0],K[1]),G||h.lineEnd(),O=!1):G&&(h.lineStart(),h.point(F,B),O=!1)}b=F,j=B,E=G}return C}}function P0e(){var e=0,t=0,n=960,r=500,i,a,s;return s={stream:function(c){return i&&a===c?i:i=av(e,t,n,r)(a=c)},extent:function(c){return arguments.length?(e=+c[0][0],t=+c[0][1],n=+c[1][0],r=+c[1][1],i=a=null,s):[[e,t],[n,r]]}}}var gw,vw,Am,Tm,Fc={sphere:jt,point:jt,lineStart:E0e,lineEnd:jt,polygonStart:jt,polygonEnd:jt};function E0e(){Fc.point=k0e,Fc.lineEnd=O0e}function O0e(){Fc.point=Fc.lineEnd=jt}function k0e(e,t){e*=_e,t*=_e,vw=e,Am=me(t),Tm=ve(t),Fc.point=C0e}function C0e(e,t){e*=_e,t*=_e;var n=me(t),r=ve(t),i=Ue(e-vw),a=ve(i),s=me(i),c=r*s,u=Tm*n-Am*r*a,d=Am*n+Tm*r*a;gw.add(Dn(rn(c*c+u*u),d)),vw=e,Am=n,Tm=r}function VL(e){return gw=new Nn,si(e,Fc),+gw}var yw=[null,null],A0e={type:"LineString",coordinates:yw};function Ng(e,t){return yw[0]=e,yw[1]=t,VL(A0e)}var BO={Feature:function(e,t){return Sg(e.geometry,t)},FeatureCollection:function(e,t){for(var n=e.features,r=-1,i=n.length;++r0&&(i=Ng(e[a],e[a-1]),i>0&&n<=i&&r<=i&&(n+r-i)*(1-Math.pow((n-r)/i,2))we}).map(p)).concat(os(lm(a/d)*d,i,d).filter(function(j){return Ue(j%m)>we}).map(v))}return w.lines=function(){return b().map(function(j){return{type:"LineString",coordinates:j}})},w.outline=function(){return{type:"Polygon",coordinates:[_(r).concat(x(s).slice(1),_(n).reverse().slice(1),x(c).reverse().slice(1))]}},w.extent=function(j){return arguments.length?w.extentMajor(j).extentMinor(j):w.extentMinor()},w.extentMajor=function(j){return arguments.length?(r=+j[0][0],n=+j[1][0],c=+j[0][1],s=+j[1][1],r>n&&(j=r,r=n,n=j),c>s&&(j=c,c=s,s=j),w.precision(y)):[[r,c],[n,s]]},w.extentMinor=function(j){return arguments.length?(t=+j[0][0],e=+j[1][0],a=+j[0][1],i=+j[1][1],t>e&&(j=t,t=e,e=j),a>i&&(j=a,a=i,i=j),w.precision(y)):[[t,a],[e,i]]},w.step=function(j){return arguments.length?w.stepMajor(j).stepMinor(j):w.stepMinor()},w.stepMajor=function(j){return arguments.length?(h=+j[0],m=+j[1],w):[h,m]},w.stepMinor=function(j){return arguments.length?(u=+j[0],d=+j[1],w):[u,d]},w.precision=function(j){return arguments.length?(y=+j,p=VO(a,i,90),v=qO(t,e,y),_=VO(c,s,90),x=qO(r,n,y),w):y},w.extentMajor([[-180,-90+we],[180,90-we]]).extentMinor([[-180,-80-we],[180,80+we]])}function L0e(){return uj()()}function $0e(e,t){var n=e[0]*_e,r=e[1]*_e,i=t[0]*_e,a=t[1]*_e,s=ve(r),c=me(r),u=ve(a),d=me(a),h=s*ve(n),m=s*me(n),p=u*ve(i),v=u*me(i),_=2*Bn(rn(PO(a-r)+s*u*PO(i-n))),x=me(_),y=_?function(w){var b=me(w*=_)/x,j=me(_-w)/x,E=j*h+b*p,P=j*m+b*v,O=j*c+b*d;return[Dn(P,E)*rt,Dn(O,rn(E*E+P*P))*rt]}:function(){return[n*rt,r*rt]};return y.distance=_,y}const Of=e=>e;var ex=new Nn,xw=new Nn,ZL,GL,bw,ww,ca={point:jt,lineStart:jt,lineEnd:jt,polygonStart:function(){ca.lineStart=I0e,ca.lineEnd=F0e},polygonEnd:function(){ca.lineStart=ca.lineEnd=ca.point=jt,ex.add(Ue(xw)),xw=new Nn},result:function(){var e=ex/2;return ex=new Nn,e}};function I0e(){ca.point=R0e}function R0e(e,t){ca.point=KL,ZL=bw=e,GL=ww=t}function KL(e,t){xw.add(ww*e-bw*t),bw=e,ww=t}function F0e(){KL(ZL,GL)}var Dc=1/0,Pg=Dc,kf=-Dc,Eg=kf,Og={point:D0e,lineStart:jt,lineEnd:jt,polygonStart:jt,polygonEnd:jt,result:function(){var e=[[Dc,Pg],[kf,Eg]];return kf=Eg=-(Pg=Dc=1/0),e}};function D0e(e,t){ekf&&(kf=e),tEg&&(Eg=t)}var _w=0,jw=0,Xu=0,kg=0,Cg=0,Cl=0,Nw=0,Sw=0,Qu=0,YL,XL,Ti,Mi,Tr={point:Is,lineStart:ZO,lineEnd:GO,polygonStart:function(){Tr.lineStart=U0e,Tr.lineEnd=W0e},polygonEnd:function(){Tr.point=Is,Tr.lineStart=ZO,Tr.lineEnd=GO},result:function(){var e=Qu?[Nw/Qu,Sw/Qu]:Cl?[kg/Cl,Cg/Cl]:Xu?[_w/Xu,jw/Xu]:[NaN,NaN];return _w=jw=Xu=kg=Cg=Cl=Nw=Sw=Qu=0,e}};function Is(e,t){_w+=e,jw+=t,++Xu}function ZO(){Tr.point=B0e}function B0e(e,t){Tr.point=z0e,Is(Ti=e,Mi=t)}function z0e(e,t){var n=e-Ti,r=t-Mi,i=rn(n*n+r*r);kg+=i*(Ti+e)/2,Cg+=i*(Mi+t)/2,Cl+=i,Is(Ti=e,Mi=t)}function GO(){Tr.point=Is}function U0e(){Tr.point=H0e}function W0e(){QL(YL,XL)}function H0e(e,t){Tr.point=QL,Is(YL=Ti=e,XL=Mi=t)}function QL(e,t){var n=e-Ti,r=t-Mi,i=rn(n*n+r*r);kg+=i*(Ti+e)/2,Cg+=i*(Mi+t)/2,Cl+=i,i=Mi*e-Ti*t,Nw+=i*(Ti+e),Sw+=i*(Mi+t),Qu+=i*3,Is(Ti=e,Mi=t)}function JL(e){this._context=e}JL.prototype={_radius:4.5,pointRadius:function(e){return this._radius=e,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){this._line===0&&this._context.closePath(),this._point=NaN},point:function(e,t){switch(this._point){case 0:{this._context.moveTo(e,t),this._point=1;break}case 1:{this._context.lineTo(e,t);break}default:{this._context.moveTo(e+this._radius,t),this._context.arc(e,t,this._radius,0,Fn);break}}},result:jt};var Pw=new Nn,tx,e$,t$,Ju,ed,Cf={point:jt,lineStart:function(){Cf.point=V0e},lineEnd:function(){tx&&n$(e$,t$),Cf.point=jt},polygonStart:function(){tx=!0},polygonEnd:function(){tx=null},result:function(){var e=+Pw;return Pw=new Nn,e}};function V0e(e,t){Cf.point=n$,e$=Ju=e,t$=ed=t}function n$(e,t){Ju-=e,ed-=t,Pw.add(rn(Ju*Ju+ed*ed)),Ju=e,ed=t}function r$(){this._string=[]}r$.prototype={_radius:4.5,_circle:KO(4.5),pointRadius:function(e){return(e=+e)!==this._radius&&(this._radius=e,this._circle=null),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){this._line===0&&this._string.push("Z"),this._point=NaN},point:function(e,t){switch(this._point){case 0:{this._string.push("M",e,",",t),this._point=1;break}case 1:{this._string.push("L",e,",",t);break}default:{this._circle==null&&(this._circle=KO(this._radius)),this._string.push("M",e,",",t,this._circle);break}}},result:function(){if(this._string.length){var e=this._string.join("");return this._string=[],e}else return null}};function KO(e){return"m0,"+e+"a"+e+","+e+" 0 1,1 0,"+-2*e+"a"+e+","+e+" 0 1,1 0,"+2*e+"z"}function i$(e,t){var n=4.5,r,i;function a(s){return s&&(typeof n=="function"&&i.pointRadius(+n.apply(this,arguments)),si(s,r(i))),i.result()}return a.area=function(s){return si(s,r(ca)),ca.result()},a.measure=function(s){return si(s,r(Cf)),Cf.result()},a.bounds=function(s){return si(s,r(Og)),Og.result()},a.centroid=function(s){return si(s,r(Tr)),Tr.result()},a.projection=function(s){return arguments.length?(r=s==null?(e=null,Of):(e=s).stream,a):e},a.context=function(s){return arguments.length?(i=s==null?(t=null,new r$):new JL(t=s),typeof n!="function"&&i.pointRadius(n),a):t},a.pointRadius=function(s){return arguments.length?(n=typeof s=="function"?s:(i.pointRadius(+s),+s),a):n},a.projection(e).context(t)}function q0e(e){return{stream:ih(e)}}function ih(e){return function(t){var n=new Ew;for(var r in e)n[r]=e[r];return n.stream=t,n}}function Ew(){}Ew.prototype={constructor:Ew,point:function(e,t){this.stream.point(e,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};function dj(e,t,n){var r=e.clipExtent&&e.clipExtent();return e.scale(150).translate([0,0]),r!=null&&e.clipExtent(null),si(n,e.stream(Og)),t(Og.result()),r!=null&&e.clipExtent(r),e}function ov(e,t,n){return dj(e,function(r){var i=t[1][0]-t[0][0],a=t[1][1]-t[0][1],s=Math.min(i/(r[1][0]-r[0][0]),a/(r[1][1]-r[0][1])),c=+t[0][0]+(i-s*(r[1][0]+r[0][0]))/2,u=+t[0][1]+(a-s*(r[1][1]+r[0][1]))/2;e.scale(150*s).translate([c,u])},n)}function fj(e,t,n){return ov(e,[[0,0],t],n)}function hj(e,t,n){return dj(e,function(r){var i=+t,a=i/(r[1][0]-r[0][0]),s=(i-a*(r[1][0]+r[0][0]))/2,c=-a*r[0][1];e.scale(150*a).translate([s,c])},n)}function mj(e,t,n){return dj(e,function(r){var i=+t,a=i/(r[1][1]-r[0][1]),s=-a*r[0][0],c=(i-a*(r[1][1]+r[0][1]))/2;e.scale(150*a).translate([s,c])},n)}var YO=16,Z0e=ve(30*_e);function XO(e,t){return+t?K0e(e,t):G0e(e)}function G0e(e){return ih({point:function(t,n){t=e(t,n),this.stream.point(t[0],t[1])}})}function K0e(e,t){function n(r,i,a,s,c,u,d,h,m,p,v,_,x,y){var w=d-r,b=h-i,j=w*w+b*b;if(j>4*t&&x--){var E=s+p,P=c+v,O=u+_,C=rn(E*E+P*P+O*O),A=Bn(O/=C),T=Ue(Ue(O)-1)t||Ue((w*Z+b*I)/j-.5)>.3||s*p+c*v+u*_2?F[2]%360*_e:0,Z()):[c*rt,u*rt,d*rt]},z.angle=function(F){return arguments.length?(m=F%360*_e,Z()):m*rt},z.reflectX=function(F){return arguments.length?(p=F?-1:1,Z()):p<0},z.reflectY=function(F){return arguments.length?(v=F?-1:1,Z()):v<0},z.precision=function(F){return arguments.length?(O=XO(C,P=F*F),I()):rn(P)},z.fitExtent=function(F,B){return ov(z,F,B)},z.fitSize=function(F,B){return fj(z,F,B)},z.fitWidth=function(F,B){return hj(z,F,B)},z.fitHeight=function(F,B){return mj(z,F,B)};function Z(){var F=QO(n,0,0,p,v,m).apply(null,t(a,s)),B=QO(n,r-F[0],i-F[1],p,v,m);return h=cj(c,u,d),C=hw(t,B),A=hw(h,C),O=XO(C,P),I()}function I(){return T=$=null,z}return function(){return t=e.apply(this,arguments),z.invert=t.invert&&D,Z()}}function gj(e){var t=0,n=$e/3,r=pj(e),i=r(t,n);return i.parallels=function(a){return arguments.length?r(t=a[0]*_e,n=a[1]*_e):[t*rt,n*rt]},i}function J0e(e){var t=ve(e);function n(r,i){return[r*t,me(i)/t]}return n.invert=function(r,i){return[r/t,Bn(i*t)]},n}function a$(e,t){var n=me(e),r=(n+me(t))/2;if(Ue(r)=.12&&y<.234&&x>=-.425&&x<-.214?i:y>=.166&&y<.234&&x>=-.214&&x<-.115?s:n).invert(p)},h.stream=function(p){return e&&t===p?e:e=eve([n.stream(t=p),i.stream(p),s.stream(p)])},h.precision=function(p){return arguments.length?(n.precision(p),i.precision(p),s.precision(p),m()):n.precision()},h.scale=function(p){return arguments.length?(n.scale(p),i.scale(p*.35),s.scale(p),h.translate(n.translate())):n.scale()},h.translate=function(p){if(!arguments.length)return n.translate();var v=n.scale(),_=+p[0],x=+p[1];return r=n.translate(p).clipExtent([[_-.455*v,x-.238*v],[_+.455*v,x+.238*v]]).stream(d),a=i.translate([_-.307*v,x+.201*v]).clipExtent([[_-.425*v+we,x+.12*v+we],[_-.214*v-we,x+.234*v-we]]).stream(d),c=s.translate([_-.205*v,x+.212*v]).clipExtent([[_-.214*v+we,x+.166*v+we],[_-.115*v-we,x+.234*v-we]]).stream(d),m()},h.fitExtent=function(p,v){return ov(h,p,v)},h.fitSize=function(p,v){return fj(h,p,v)},h.fitWidth=function(p,v){return hj(h,p,v)},h.fitHeight=function(p,v){return mj(h,p,v)};function m(){return e=t=null,h}return h.scale(1070)}function s$(e){return function(t,n){var r=ve(t),i=ve(n),a=e(r*i);return a===1/0?[2,0]:[a*i*me(t),a*me(n)]}}function ah(e){return function(t,n){var r=rn(t*t+n*n),i=e(r),a=me(i),s=ve(i);return[Dn(t*a,r*s),Bn(r&&n*a/r)]}}var vj=s$(function(e){return rn(2/(1+e))});vj.invert=ah(function(e){return 2*Bn(e/2)});function nve(){return Hi(vj).scale(124.75).clipAngle(180-.001)}var yj=s$(function(e){return(e=EL(e))&&e/me(e)});yj.invert=ah(function(e){return e});function rve(){return Hi(yj).scale(79.4188).clipAngle(180-.001)}function oh(e,t){return[e,dg(sj((At+t)/2))]}oh.invert=function(e,t){return[e,2*su(PL(t))-At]};function ive(){return l$(oh).scale(961/Fn)}function l$(e){var t=Hi(e),n=t.center,r=t.scale,i=t.translate,a=t.clipExtent,s=null,c,u,d;t.scale=function(m){return arguments.length?(r(m),h()):r()},t.translate=function(m){return arguments.length?(i(m),h()):i()},t.center=function(m){return arguments.length?(n(m),h()):n()},t.clipExtent=function(m){return arguments.length?(m==null?s=c=u=d=null:(s=+m[0][0],c=+m[0][1],u=+m[1][0],d=+m[1][1]),h()):s==null?null:[[s,c],[u,d]]};function h(){var m=$e*r(),p=t(FL(t.rotate()).invert([0,0]));return a(s==null?[[p[0]-m,p[1]-m],[p[0]+m,p[1]+m]]:e===oh?[[Math.max(p[0]-m,s),c],[Math.min(p[0]+m,u),d]]:[[s,Math.max(p[1]-m,c)],[u,Math.min(p[1]+m,d)]])}return h()}function hm(e){return sj((At+e)/2)}function c$(e,t){var n=ve(e),r=e===t?me(e):dg(n/ve(t))/dg(hm(t)/hm(e)),i=n*Xy(hm(e),r)/r;if(!r)return oh;function a(s,c){i>0?c<-At+we&&(c=-At+we):c>At-we&&(c=At-we);var u=i/Xy(hm(c),r);return[u*me(r*s),i-u*ve(r*s)]}return a.invert=function(s,c){var u=i-c,d=Rr(r)*rn(s*s+u*u),h=Dn(s,Ue(u))*Rr(u);return u*r<0&&(h-=$e*Rr(s)*Rr(u)),[h/r,2*su(Xy(i/d,1/r))-At]},a}function ave(){return gj(c$).scale(109.5).parallels([30,30])}function Af(e,t){return[e,t]}Af.invert=Af;function ove(){return Hi(Af).scale(152.63)}function u$(e,t){var n=ve(e),r=e===t?me(e):(n-ve(t))/(t-e),i=n/r+e;if(Ue(r)we&&--r>0);return[e/(.8707+(a=n*n)*(-.131979+a*(-.013791+a*a*a*(.003971-.001529*a)))),n]};function fve(){return Hi(wj).scale(175.295)}function _j(e,t){return[ve(t)*me(e),me(t)]}_j.invert=ah(Bn);function hve(){return Hi(_j).scale(249.5).clipAngle(90+we)}function jj(e,t){var n=ve(t),r=1+ve(e)*n;return[n*me(e)/r,me(t)/r]}jj.invert=ah(function(e){return 2*su(e)});function mve(){return Hi(jj).scale(250).clipAngle(142)}function Nj(e,t){return[dg(sj((At+t)/2)),-e]}Nj.invert=function(e,t){return[-t,2*su(PL(e))-At]};function pve(){var e=l$(Nj),t=e.center,n=e.rotate;return e.center=function(r){return arguments.length?t([-r[1],r[0]]):(r=t(),[r[1],-r[0]])},e.rotate=function(r){return arguments.length?n([r[0],r[1],r.length>2?r[2]+90:90]):(r=n(),[r[0],r[1],r[2]-90])},n([0,0,90]).scale(159.155)}const gve=Object.freeze(Object.defineProperty({__proto__:null,geoAlbers:o$,geoAlbersUsa:tve,geoArea:l0e,geoAzimuthalEqualArea:nve,geoAzimuthalEqualAreaRaw:vj,geoAzimuthalEquidistant:rve,geoAzimuthalEquidistantRaw:yj,geoBounds:f0e,geoCentroid:y0e,geoCircle:x0e,geoClipAntimeridian:pw,geoClipCircle:HL,geoClipExtent:P0e,geoClipRectangle:av,geoConicConformal:ave,geoConicConformalRaw:c$,geoConicEqualArea:Ag,geoConicEqualAreaRaw:a$,geoConicEquidistant:sve,geoConicEquidistantRaw:u$,geoContains:M0e,geoDistance:Ng,geoEqualEarth:cve,geoEqualEarthRaw:xj,geoEquirectangular:ove,geoEquirectangularRaw:Af,geoGnomonic:uve,geoGnomonicRaw:bj,geoGraticule:uj,geoGraticule10:L0e,geoIdentity:dve,geoInterpolate:$0e,geoLength:VL,geoMercator:ive,geoMercatorRaw:oh,geoNaturalEarth1:fve,geoNaturalEarth1Raw:wj,geoOrthographic:hve,geoOrthographicRaw:_j,geoPath:i$,geoProjection:Hi,geoProjectionMutator:pj,geoRotation:FL,geoStereographic:mve,geoStereographicRaw:jj,geoStream:si,geoTransform:q0e,geoTransverseMercator:pve,geoTransverseMercatorRaw:Nj},Symbol.toStringTag,{value:"Module"}));function vve(e){return e}function yve(e){if(e==null)return vve;var t,n,r=e.scale[0],i=e.scale[1],a=e.translate[0],s=e.translate[1];return function(c,u){u||(t=n=0);var d=2,h=c.length,m=new Array(h);for(m[0]=(t+=c[0])*r+a,m[1]=(n+=c[1])*i+s;d1)r=jve(e,t,n);else for(i=0,r=new Array(a=e.arcs.length);i{}};function Sj(){for(var e=0,t=arguments.length,n={},r;e=0&&(r=n.slice(i+1),n=n.slice(0,i)),n&&!t.hasOwnProperty(n))throw new Error("unknown type: "+n);return{type:n,name:r}})}Mm.prototype=Sj.prototype={constructor:Mm,on:function(e,t){var n=this._,r=Sve(e+"",n),i,a=-1,s=r.length;if(arguments.length<2){for(;++a0)for(var n=new Array(i),r=0,i,a;r=0&&(t=e.slice(0,n))!=="xmlns"&&(e=e.slice(n+1)),nk.hasOwnProperty(t)?{space:nk[t],local:e}:e}function Eve(e){return function(){var t=this.ownerDocument,n=this.namespaceURI;return n===Ow&&t.documentElement.namespaceURI===Ow?t.createElement(e):t.createElementNS(n,e)}}function Ove(e){return function(){return this.ownerDocument.createElementNS(e.space,e.local)}}function f$(e){var t=sv(e);return(t.local?Ove:Eve)(t)}function kve(){}function Pj(e){return e==null?kve:function(){return this.querySelector(e)}}function Cve(e){typeof e!="function"&&(e=Pj(e));for(var t=this._groups,n=t.length,r=new Array(n),i=0;i=j&&(j=b+1);!(P=y[j])&&++j<_;);E._next=P||null}}return s=new Jn(s,r),s._enter=c,s._exit=u,s}function Kve(){return new Jn(this._exit||this._groups.map(v$),this._parents)}function Yve(e,t,n){var r=this.enter(),i=this,a=this.exit();return r=typeof e=="function"?e(r):r.append(e+""),t!=null&&(i=t(i)),n==null?a.remove():n(a),r&&i?r.merge(i).order():i}function Xve(e){if(!(e instanceof Jn))throw new Error("invalid merge");for(var t=this._groups,n=e._groups,r=t.length,i=n.length,a=Math.min(r,i),s=new Array(r),c=0;c=0;)(s=r[i])&&(a&&s.compareDocumentPosition(a)^4&&a.parentNode.insertBefore(s,a),a=s);return this}function Jve(e){e||(e=eye);function t(m,p){return m&&p?e(m.__data__,p.__data__):!m-!p}for(var n=this._groups,r=n.length,i=new Array(r),a=0;at?1:e>=t?0:NaN}function tye(){var e=arguments[0];return arguments[0]=this,e.apply(null,arguments),this}function nye(){return Array.from(this)}function rye(){for(var e=this._groups,t=0,n=e.length;t1?this.each((t==null?mye:typeof t=="function"?gye:pye)(e,t,n??"")):Bc(this.node(),e)}function Bc(e,t){return e.style.getPropertyValue(t)||y$(e).getComputedStyle(e,null).getPropertyValue(t)}function yye(e){return function(){delete this[e]}}function xye(e,t){return function(){this[e]=t}}function bye(e,t){return function(){var n=t.apply(this,arguments);n==null?delete this[e]:this[e]=n}}function wye(e,t){return arguments.length>1?this.each((t==null?yye:typeof t=="function"?bye:xye)(e,t)):this.node()[e]}function x$(e){return e.trim().split(/^|\s+/)}function Ej(e){return e.classList||new b$(e)}function b$(e){this._node=e,this._names=x$(e.getAttribute("class")||"")}b$.prototype={add:function(e){var t=this._names.indexOf(e);t<0&&(this._names.push(e),this._node.setAttribute("class",this._names.join(" ")))},remove:function(e){var t=this._names.indexOf(e);t>=0&&(this._names.splice(t,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(e){return this._names.indexOf(e)>=0}};function w$(e,t){for(var n=Ej(e),r=-1,i=t.length;++r=0&&(n=t.slice(r+1),t=t.slice(0,r)),{type:t,name:n}})}function Kye(e){return function(){var t=this.__on;if(t){for(var n=0,r=-1,i=t.length,a;n=0&&e._call.call(null,t),e=e._next;--zc}function rk(){Rs=($g=Tf.now())+lv,zc=td=0;try{sxe()}finally{zc=0,cxe(),Rs=0}}function lxe(){var e=Tf.now(),t=e-$g;t>S$&&(lv-=t,$g=e)}function cxe(){for(var e,t=Lg,n,r=1/0;t;)t._call?(r>t._time&&(r=t._time),e=t,t=t._next):(n=t._next,t._next=null,t=e?e._next=n:Lg=n);nd=e,Cw(r)}function Cw(e){if(!zc){td&&(td=clearTimeout(td));var t=e-Rs;t>24?(e<1/0&&(td=setTimeout(rk,e-Tf.now()-lv)),Du&&(Du=clearInterval(Du))):(Du||($g=Tf.now(),Du=setInterval(lxe,S$)),zc=1,P$(rk))}}function ik(e,t,n){var r=new Ig;return t=t==null?0:+t,r.restart(i=>{r.stop(),e(i+t)},t,n),r}var uxe=Sj("start","end","cancel","interrupt"),dxe=[],O$=0,ak=1,Aw=2,Lm=3,ok=4,Tw=5,$m=6;function cv(e,t,n,r,i,a){var s=e.__transition;if(!s)e.__transition={};else if(n in s)return;fxe(e,n,{name:t,index:r,group:i,on:uxe,tween:dxe,time:a.time,delay:a.delay,duration:a.duration,ease:a.ease,timer:null,state:O$})}function kj(e,t){var n=vi(e,t);if(n.state>O$)throw new Error("too late; already scheduled");return n}function Vi(e,t){var n=vi(e,t);if(n.state>Lm)throw new Error("too late; already running");return n}function vi(e,t){var n=e.__transition;if(!n||!(n=n[t]))throw new Error("transition not found");return n}function fxe(e,t,n){var r=e.__transition,i;r[t]=n,n.timer=E$(a,0,n.time);function a(d){n.state=ak,n.timer.restart(s,n.delay,n.time),n.delay<=d&&s(d-n.delay)}function s(d){var h,m,p,v;if(n.state!==ak)return u();for(h in r)if(v=r[h],v.name===n.name){if(v.state===Lm)return ik(s);v.state===ok?(v.state=$m,v.timer.stop(),v.on.call("interrupt",e,e.__data__,v.index,v.group),delete r[h]):+hAw&&r.state=0&&(t=t.slice(0,n)),!t||t==="start"})}function Uxe(e,t,n){var r,i,a=zxe(t)?kj:Vi;return function(){var s=a(this,e),c=s.on;c!==r&&(i=(r=c).copy()).on(t,n),s.on=i}}function Wxe(e,t){var n=this._id;return arguments.length<2?vi(this.node(),n).on.on(e):this.each(Uxe(n,e,t))}function Hxe(e){return function(){var t=this.parentNode;for(var n in this.__transition)if(+n!==e)return;t&&t.removeChild(this)}}function Vxe(){return this.on("end.remove",Hxe(this._id))}function qxe(e){var t=this._name,n=this._id;typeof e!="function"&&(e=Pj(e));for(var r=this._groups,i=r.length,a=new Array(i),s=0;s()=>e;function vbe(e,{sourceEvent:t,target:n,transform:r,dispatch:i}){Object.defineProperties(this,{type:{value:e,enumerable:!0,configurable:!0},sourceEvent:{value:t,enumerable:!0,configurable:!0},target:{value:n,enumerable:!0,configurable:!0},transform:{value:r,enumerable:!0,configurable:!0},_:{value:i}})}function va(e,t,n){this.k=e,this.x=t,this.y=n}va.prototype={constructor:va,scale:function(e){return e===1?this:new va(this.k*e,this.x,this.y)},translate:function(e,t){return e===0&t===0?this:new va(this.k,this.x+this.k*e,this.y+this.k*t)},apply:function(e){return[e[0]*this.k+this.x,e[1]*this.k+this.y]},applyX:function(e){return e*this.k+this.x},applyY:function(e){return e*this.k+this.y},invert:function(e){return[(e[0]-this.x)/this.k,(e[1]-this.y)/this.k]},invertX:function(e){return(e-this.x)/this.k},invertY:function(e){return(e-this.y)/this.k},rescaleX:function(e){return e.copy().domain(e.range().map(this.invertX,this).map(e.invert,e))},rescaleY:function(e){return e.copy().domain(e.range().map(this.invertY,this).map(e.invert,e))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var Aj=new va(1,0,0);va.prototype;function nx(e){e.stopImmediatePropagation()}function Bu(e){e.preventDefault(),e.stopImmediatePropagation()}function ybe(e){return(!e.ctrlKey||e.type==="wheel")&&!e.button}function xbe(){var e=this;return e instanceof SVGElement?(e=e.ownerSVGElement||e,e.hasAttribute("viewBox")?(e=e.viewBox.baseVal,[[e.x,e.y],[e.x+e.width,e.y+e.height]]):[[0,0],[e.width.baseVal.value,e.height.baseVal.value]]):[[0,0],[e.clientWidth,e.clientHeight]]}function sk(){return this.__zoom||Aj}function bbe(e){return-e.deltaY*(e.deltaMode===1?.05:e.deltaMode?1:.002)*(e.ctrlKey?10:1)}function wbe(){return navigator.maxTouchPoints||"ontouchstart"in this}function _be(e,t,n){var r=e.invertX(t[0][0])-n[0][0],i=e.invertX(t[1][0])-n[1][0],a=e.invertY(t[0][1])-n[0][1],s=e.invertY(t[1][1])-n[1][1];return e.translate(i>r?(r+i)/2:Math.min(0,r)||Math.max(0,i),s>a?(a+s)/2:Math.min(0,a)||Math.max(0,s))}function jbe(){var e=ybe,t=xbe,n=_be,r=bbe,i=wbe,a=[0,1/0],s=[[-1/0,-1/0],[1/0,1/0]],c=250,u=lre,d=Sj("start","zoom","end"),h,m,p,v=500,_=150,x=0,y=10;function w(I){I.property("__zoom",sk).on("wheel.zoom",A).on("mousedown.zoom",T).on("dblclick.zoom",$).filter(i).on("touchstart.zoom",z).on("touchmove.zoom",D).on("touchend.zoom touchcancel.zoom",Z).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}w.transform=function(I,F,B,G){var R=I.selection?I.selection():I;R.property("__zoom",sk),I!==R?P(I,F,B,G):R.interrupt().each(function(){O(this,arguments).event(G).start().zoom(null,typeof F=="function"?F.apply(this,arguments):F).end()})},w.scaleBy=function(I,F,B,G){w.scaleTo(I,function(){var R=this.__zoom.k,K=typeof F=="function"?F.apply(this,arguments):F;return R*K},B,G)},w.scaleTo=function(I,F,B,G){w.transform(I,function(){var R=t.apply(this,arguments),K=this.__zoom,W=B==null?E(R):typeof B=="function"?B.apply(this,arguments):B,U=K.invert(W),Y=typeof F=="function"?F.apply(this,arguments):F;return n(j(b(K,Y),W,U),R,s)},B,G)},w.translateBy=function(I,F,B,G){w.transform(I,function(){return n(this.__zoom.translate(typeof F=="function"?F.apply(this,arguments):F,typeof B=="function"?B.apply(this,arguments):B),t.apply(this,arguments),s)},null,G)},w.translateTo=function(I,F,B,G,R){w.transform(I,function(){var K=t.apply(this,arguments),W=this.__zoom,U=G==null?E(K):typeof G=="function"?G.apply(this,arguments):G;return n(Aj.translate(U[0],U[1]).scale(W.k).translate(typeof F=="function"?-F.apply(this,arguments):-F,typeof B=="function"?-B.apply(this,arguments):-B),K,s)},G,R)};function b(I,F){return F=Math.max(a[0],Math.min(a[1],F)),F===I.k?I:new va(F,I.x,I.y)}function j(I,F,B){var G=F[0]-B[0]*I.k,R=F[1]-B[1]*I.k;return G===I.x&&R===I.y?I:new va(I.k,G,R)}function E(I){return[(+I[0][0]+ +I[1][0])/2,(+I[0][1]+ +I[1][1])/2]}function P(I,F,B,G){I.on("start.zoom",function(){O(this,arguments).event(G).start()}).on("interrupt.zoom end.zoom",function(){O(this,arguments).event(G).end()}).tween("zoom",function(){var R=this,K=arguments,W=O(R,K).event(G),U=t.apply(R,K),Y=B==null?E(U):typeof B=="function"?B.apply(R,K):B,ne=Math.max(U[1][0]-U[0][0],U[1][1]-U[0][1]),ae=R.__zoom,ee=typeof F=="function"?F.apply(R,K):F,ce=u(ae.invert(Y).concat(ne/ae.k),ee.invert(Y).concat(ne/ee.k));return function(Ne){if(Ne===1)Ne=ee;else{var Pe=ce(Ne),se=ne/Pe[2];Ne=new va(se,Y[0]-Pe[0]*se,Y[1]-Pe[1]*se)}W.zoom(null,Ne)}})}function O(I,F,B){return!B&&I.__zooming||new C(I,F)}function C(I,F){this.that=I,this.args=F,this.active=0,this.sourceEvent=null,this.extent=t.apply(I,F),this.taps=0}C.prototype={event:function(I){return I&&(this.sourceEvent=I),this},start:function(){return++this.active===1&&(this.that.__zooming=this,this.emit("start")),this},zoom:function(I,F){return this.mouse&&I!=="mouse"&&(this.mouse[1]=F.invert(this.mouse[0])),this.touch0&&I!=="touch"&&(this.touch0[1]=F.invert(this.touch0[0])),this.touch1&&I!=="touch"&&(this.touch1[1]=F.invert(this.touch1[0])),this.that.__zoom=F,this.emit("zoom"),this},end:function(){return--this.active===0&&(delete this.that.__zooming,this.emit("end")),this},emit:function(I){var F=ua(this.that).datum();d.call(I,this.that,new vbe(I,{sourceEvent:this.sourceEvent,target:w,transform:this.that.__zoom,dispatch:d}),F)}};function A(I,...F){if(!e.apply(this,arguments))return;var B=O(this,F).event(I),G=this.__zoom,R=Math.max(a[0],Math.min(a[1],G.k*Math.pow(2,r.apply(this,arguments)))),K=Ho(I);if(B.wheel)(B.mouse[0][0]!==K[0]||B.mouse[0][1]!==K[1])&&(B.mouse[1]=G.invert(B.mouse[0]=K)),clearTimeout(B.wheel);else{if(G.k===R)return;B.mouse=[K,G.invert(K)],Im(this),B.start()}Bu(I),B.wheel=setTimeout(W,_),B.zoom("mouse",n(j(b(G,R),B.mouse[0],B.mouse[1]),B.extent,s));function W(){B.wheel=null,B.end()}}function T(I,...F){if(p||!e.apply(this,arguments))return;var B=O(this,F,!0).event(I),G=ua(I.view).on("mousemove.zoom",Y,!0).on("mouseup.zoom",ne,!0),R=Ho(I,K),K=I.currentTarget,W=I.clientX,U=I.clientY;ixe(I.view),nx(I),B.mouse=[R,this.__zoom.invert(R)],Im(this),B.start();function Y(ae){if(Bu(ae),!B.moved){var ee=ae.clientX-W,ce=ae.clientY-U;B.moved=ee*ee+ce*ce>x}B.event(ae).zoom("mouse",n(j(B.that.__zoom,B.mouse[0]=Ho(ae,K),B.mouse[1]),B.extent,s))}function ne(ae){G.on("mousemove.zoom mouseup.zoom",null),axe(ae.view,B.moved),Bu(ae),B.event(ae).end()}}function $(I,...F){if(e.apply(this,arguments)){var B=this.__zoom,G=Ho(I.changedTouches?I.changedTouches[0]:I,this),R=B.invert(G),K=B.k*(I.shiftKey?.5:2),W=n(j(b(B,K),G,R),t.apply(this,F),s);Bu(I),c>0?ua(this).transition().duration(c).call(P,W,G,I):ua(this).call(w.transform,W,G,I)}}function z(I,...F){if(e.apply(this,arguments)){var B=I.touches,G=B.length,R=O(this,F,I.changedTouches.length===G).event(I),K,W,U,Y;for(nx(I),W=0;W=0)&&(n[i]=e[i]);return n}function Zr(e,t){if(e==null)return{};var n=Sbe(e,t),r,i;if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(i=0;i=0)&&Object.prototype.propertyIsEnumerable.call(e,r)&&(n[r]=e[r])}return n}function dn(e,t){return Pbe(e)||Ebe(e,t)||Obe(e,t)||kbe()}function Pbe(e){if(Array.isArray(e))return e}function Ebe(e,t){var n=e==null?null:typeof Symbol<"u"&&e[Symbol.iterator]||e["@@iterator"];if(n!=null){var r=[],i=!0,a=!1,s,c;try{for(n=n.call(e);!(i=(s=n.next()).done)&&(r.push(s.value),!(t&&r.length===t));i=!0);}catch(u){a=!0,c=u}finally{try{!i&&n.return!=null&&n.return()}finally{if(a)throw c}}return r}}function Obe(e,t){if(e){if(typeof e=="string")return ck(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);if(n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set")return Array.from(e);if(n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return ck(e,t)}}function ck(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&arguments[0]!==void 0?arguments[0]:30,t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:30,n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:.5,r=Array.isArray(n)?n:[n,n],i=e/2*r[0],a=t/2*r[1];return"M".concat(0,",",0," Q",-e/2-i,",").concat(-t/2+a," ").concat(-e,",").concat(-t)}function Dbe(e){return typeof e=="string"}function Bbe(e){var t=e.geography,n=e.parseGeographies,r=N.useContext(qi),i=r.path,a=N.useState({}),s=dn(a,2),c=s[0],u=s[1];N.useEffect(function(){(typeof window>"u"?"undefined":Mw(window))!=="undefined"&&t&&(Dbe(t)?$be(t).then(function(v){v&&u({geographies:dk(v,n),mesh:fk(v)})}):u({geographies:dk(t,n),mesh:fk(t)}))},[t,n]);var d=N.useMemo(function(){var v=c.mesh||{},_=Ibe(v.outline,v.borders,i);return{geographies:Rbe(c.geographies,i),outline:_.outline,borders:_.borders}},[c,i]),h=d.geographies,m=d.outline,p=d.borders;return{geographies:h,outline:m,borders:p}}var zbe=["geography","children","parseGeographies","className"],Mj=N.forwardRef(function(e,t){var n=e.geography,r=e.children,i=e.parseGeographies,a=e.className,s=a===void 0?"":a,c=Zr(e,zbe),u=N.useContext(qi),d=u.path,h=u.projection,m=Bbe({geography:n,parseGeographies:i}),p=m.geographies,v=m.outline,_=m.borders;return H.createElement("g",er({ref:t,className:"rsm-geographies ".concat(s)},c),p&&p.length>0&&r({geographies:p,outline:v,borders:_,path:d,projection:h}))});Mj.displayName="Geographies";Mj.propTypes={geography:re.oneOfType([re.string,re.object,re.array]),children:re.func,parseGeographies:re.func,className:re.string};var Ube=["geography","onMouseEnter","onMouseLeave","onMouseDown","onMouseUp","onFocus","onBlur","style","className"],Lj=N.forwardRef(function(e,t){var n=e.geography,r=e.onMouseEnter,i=e.onMouseLeave,a=e.onMouseDown,s=e.onMouseUp,c=e.onFocus,u=e.onBlur,d=e.style,h=d===void 0?{}:d,m=e.className,p=m===void 0?"":m,v=Zr(e,Ube),_=N.useState(!1),x=dn(_,2),y=x[0],w=x[1],b=N.useState(!1),j=dn(b,2),E=j[0],P=j[1];function O(D){P(!0),r&&r(D)}function C(D){P(!1),y&&w(!1),i&&i(D)}function A(D){P(!0),c&&c(D)}function T(D){P(!1),y&&w(!1),u&&u(D)}function $(D){w(!0),a&&a(D)}function z(D){w(!1),s&&s(D)}return H.createElement("path",er({ref:t,tabIndex:"0",className:"rsm-geography ".concat(p),d:n.svgPath,onMouseEnter:O,onMouseLeave:C,onFocus:A,onBlur:T,onMouseDown:$,onMouseUp:z,style:h[y||E?y?"pressed":"hover":"default"]},v))});Lj.displayName="Geography";Lj.propTypes={geography:re.object,onMouseEnter:re.func,onMouseLeave:re.func,onMouseDown:re.func,onMouseUp:re.func,onFocus:re.func,onBlur:re.func,style:re.object,className:re.string};var Wbe=N.memo(Lj),Hbe=["fill","stroke","step","className"],$j=N.forwardRef(function(e,t){var n=e.fill,r=n===void 0?"transparent":n,i=e.stroke,a=i===void 0?"currentcolor":i,s=e.step,c=s===void 0?[10,10]:s,u=e.className,d=u===void 0?"":u,h=Zr(e,Hbe),m=N.useContext(qi),p=m.path;return H.createElement("path",er({ref:t,d:p(uj().step(c)()),fill:r,stroke:a,className:"rsm-graticule ".concat(d)},h))});$j.displayName="Graticule";$j.propTypes={fill:re.string,stroke:re.string,step:re.array,className:re.string};N.memo($j);var Vbe=["value"],qbe=N.createContext(),Zbe={x:0,y:0,k:1,transformString:"translate(0 0) scale(1)"},M$=function(t){var n=t.value,r=n===void 0?Zbe:n,i=Zr(t,Vbe);return H.createElement(qbe.Provider,er({value:r},i))};M$.propTypes={x:re.number,y:re.number,k:re.number,transformString:re.string};function Gbe(e){var t=e.center,n=e.filterZoomEvent,r=e.onMoveStart,i=e.onMoveEnd,a=e.onMove,s=e.translateExtent,c=s===void 0?[[-1/0,-1/0],[1/0,1/0]]:s,u=e.scaleExtent,d=u===void 0?[1,8]:u,h=e.zoom,m=h===void 0?1:h,p=N.useContext(qi),v=p.width,_=p.height,x=p.projection,y=dn(t,2),w=y[0],b=y[1],j=N.useState({x:0,y:0,k:1}),E=dn(j,2),P=E[0],O=E[1],C=N.useRef({x:0,y:0,k:1}),A=N.useRef(),T=N.useRef(),$=N.useRef(!1),z=dn(c,2),D=z[0],Z=z[1],I=dn(D,2),F=I[0],B=I[1],G=dn(Z,2),R=G[0],K=G[1],W=dn(d,2),U=W[0],Y=W[1];return N.useEffect(function(){var ne=ua(A.current);function ae(se){!r||$.current||r({coordinates:x.invert(uk(v,_,se.transform)),zoom:se.transform.k},se)}function ee(se){if(!$.current){var ye=se.transform,je=se.sourceEvent;O({x:ye.x,y:ye.y,k:ye.k,dragging:je}),a&&a({x:ye.x,y:ye.y,zoom:ye.k,dragging:je},se)}}function ce(se){if($.current){$.current=!1;return}var ye=x.invert(uk(v,_,se.transform)),je=dn(ye,2),ie=je[0],Ve=je[1];C.current={x:ie,y:Ve,k:se.transform.k},i&&i({coordinates:[ie,Ve],zoom:se.transform.k},se)}function Ne(se){return n?n(se):se?!se.ctrlKey&&!se.button:!1}var Pe=jbe().filter(Ne).scaleExtent([U,Y]).translateExtent([[F,B],[R,K]]).on("start",ae).on("zoom",ee).on("end",ce);T.current=Pe,ne.call(Pe)},[v,_,F,B,R,K,U,Y,x,r,a,i,n]),N.useEffect(function(){if(!(w===C.current.x&&b===C.current.y&&m===C.current.k)){var ne=x([w,b]),ae=ne[0]*m,ee=ne[1]*m,ce=ua(A.current);$.current=!0,ce.call(T.current.transform,Aj.translate(v/2-ae,_/2-ee).scale(m)),O({x:v/2-ae,y:_/2-ee,k:m}),C.current={x:w,y:b,k:m}}},[w,b,m,v,_,x]),{mapRef:A,position:P,transformString:"translate(".concat(P.x," ").concat(P.y,") scale(").concat(P.k,")")}}var Kbe=["center","zoom","minZoom","maxZoom","translateExtent","filterZoomEvent","onMoveStart","onMove","onMoveEnd","className"],L$=N.forwardRef(function(e,t){var n=e.center,r=n===void 0?[0,0]:n,i=e.zoom,a=i===void 0?1:i,s=e.minZoom,c=s===void 0?1:s,u=e.maxZoom,d=u===void 0?8:u,h=e.translateExtent,m=e.filterZoomEvent,p=e.onMoveStart,v=e.onMove,_=e.onMoveEnd,x=e.className,y=Zr(e,Kbe),w=N.useContext(qi),b=w.width,j=w.height,E=Gbe({center:r,filterZoomEvent:m,onMoveStart:p,onMove:v,onMoveEnd:_,scaleExtent:[c,d],translateExtent:h,zoom:a}),P=E.mapRef,O=E.transformString,C=E.position;return H.createElement(M$,{value:{x:C.x,y:C.y,k:C.k,transformString:O}},H.createElement("g",{ref:P},H.createElement("rect",{width:b,height:j,fill:"transparent"}),H.createElement("g",er({ref:t,transform:O,className:"rsm-zoomable-group ".concat(x)},y))))});L$.displayName="ZoomableGroup";L$.propTypes={center:re.array,zoom:re.number,minZoom:re.number,maxZoom:re.number,translateExtent:re.arrayOf(re.array),onMoveStart:re.func,onMove:re.func,onMoveEnd:re.func,className:re.string};var Ybe=["id","fill","stroke","strokeWidth","className"],Ij=N.forwardRef(function(e,t){var n=e.id,r=n===void 0?"rsm-sphere":n,i=e.fill,a=i===void 0?"transparent":i,s=e.stroke,c=s===void 0?"currentcolor":s,u=e.strokeWidth,d=u===void 0?.5:u,h=e.className,m=h===void 0?"":h,p=Zr(e,Ybe),v=N.useContext(qi),_=v.path,x=N.useMemo(function(){return _({type:"Sphere"})},[_]);return H.createElement(N.Fragment,null,H.createElement("defs",null,H.createElement("clipPath",{id:r},H.createElement("path",{d:x}))),H.createElement("path",er({ref:t,d:x,fill:a,stroke:c,strokeWidth:d,style:{pointerEvents:"none"},className:"rsm-sphere ".concat(m)},p)))});Ij.displayName="Sphere";Ij.propTypes={id:re.string,fill:re.string,stroke:re.string,strokeWidth:re.number,className:re.string};N.memo(Ij);var Xbe=["coordinates","children","onMouseEnter","onMouseLeave","onMouseDown","onMouseUp","onFocus","onBlur","style","className"],$$=N.forwardRef(function(e,t){var n=e.coordinates,r=e.children,i=e.onMouseEnter,a=e.onMouseLeave,s=e.onMouseDown,c=e.onMouseUp,u=e.onFocus,d=e.onBlur,h=e.style,m=h===void 0?{}:h,p=e.className,v=p===void 0?"":p,_=Zr(e,Xbe),x=N.useContext(qi),y=x.projection,w=N.useState(!1),b=dn(w,2),j=b[0],E=b[1],P=N.useState(!1),O=dn(P,2),C=O[0],A=O[1],T=y(n),$=dn(T,2),z=$[0],D=$[1];function Z(K){A(!0),i&&i(K)}function I(K){A(!1),j&&E(!1),a&&a(K)}function F(K){A(!0),u&&u(K)}function B(K){A(!1),j&&E(!1),d&&d(K)}function G(K){E(!0),s&&s(K)}function R(K){E(!1),c&&c(K)}return H.createElement("g",er({ref:t,transform:"translate(".concat(z,", ").concat(D,")"),className:"rsm-marker ".concat(v),onMouseEnter:Z,onMouseLeave:I,onFocus:F,onBlur:B,onMouseDown:G,onMouseUp:R,style:m[j||C?j?"pressed":"hover":"default"]},_),r)});$$.displayName="Marker";$$.propTypes={coordinates:re.array,children:re.oneOfType([re.node,re.arrayOf(re.node)]),onMouseEnter:re.func,onMouseLeave:re.func,onMouseDown:re.func,onMouseUp:re.func,onFocus:re.func,onBlur:re.func,style:re.object,className:re.string};var Qbe=["from","to","coordinates","stroke","strokeWidth","fill","className"],I$=N.forwardRef(function(e,t){var n=e.from,r=n===void 0?[0,0]:n,i=e.to,a=i===void 0?[0,0]:i,s=e.coordinates,c=e.stroke,u=c===void 0?"currentcolor":c,d=e.strokeWidth,h=d===void 0?3:d,m=e.fill,p=m===void 0?"transparent":m,v=e.className,_=v===void 0?"":v,x=Zr(e,Qbe),y=N.useContext(qi),w=y.path,b={type:"LineString",coordinates:s||[r,a]};return H.createElement("path",er({ref:t,d:w(b),className:"rsm-line ".concat(_),stroke:u,strokeWidth:h,fill:p},x))});I$.displayName="Line";I$.propTypes={from:re.array,to:re.array,coordinates:re.array,stroke:re.string,strokeWidth:re.number,fill:re.string,className:re.string};var Jbe=["subject","children","connectorProps","dx","dy","curve","className"],R$=N.forwardRef(function(e,t){var n=e.subject,r=e.children,i=e.connectorProps,a=e.dx,s=a===void 0?30:a,c=e.dy,u=c===void 0?30:c,d=e.curve,h=d===void 0?0:d,m=e.className,p=m===void 0?"":m,v=Zr(e,Jbe),_=N.useContext(qi),x=_.projection,y=x(n),w=dn(y,2),b=w[0],j=w[1],E=Fbe(s,u,h);return H.createElement("g",er({ref:t,transform:"translate(".concat(b+s,", ").concat(j+u,")"),className:"rsm-annotation ".concat(p)},v),H.createElement("path",er({d:E,fill:"transparent",stroke:"#000"},i)),r)});R$.displayName="Annotation";R$.propTypes={subject:re.array,children:re.oneOfType([re.node,re.arrayOf(re.node)]),dx:re.number,dy:re.number,curve:re.number,connectorProps:re.object,className:re.string};const e1e="https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json",t1e={AL:"01",AK:"02",AZ:"04",AR:"05",CA:"06",CO:"08",CT:"09",DE:"10",FL:"12",GA:"13",HI:"15",ID:"16",IL:"17",IN:"18",IA:"19",KS:"20",KY:"21",LA:"22",ME:"23",MD:"24",MA:"25",MI:"26",MN:"27",MS:"28",MO:"29",MT:"30",NE:"31",NV:"32",NH:"33",NJ:"34",NM:"35",NY:"36",NC:"37",ND:"38",OH:"39",OK:"40",OR:"41",PA:"42",RI:"44",SC:"45",SD:"46",TN:"47",TX:"48",UT:"49",VT:"50",VA:"51",WA:"53",WV:"54",WI:"55",WY:"56",DC:"11",PR:"72"},n1e=Object.fromEntries(Object.entries(t1e).map(([e,t])=>[t,e])),r1e={mandate:"#4CAF50",removal:"#F44336",study:"#9C27B0",coverage_expansion:"#4CAF50",screening:"#FF9800",provider_access:"#2196F3",expansion:"#4CAF50",coverage:"#2196F3",reimbursement:"#FF9800",eligibility:"#9C27B0",requirement:"#FF9800",curriculum:"#2196F3",reform:"#9C27B0",protection:"#4CAF50",restriction:"#F44336",support:"#4CAF50",oppose:"#F44336",regulate:"#FF9800",funding:"#2196F3",other:"#9E9E9E"},F$=e=>r1e[e]||"#9E9E9E",hk=(e,t)=>{const n=t[e];if(!n||n.total_bills===0)return"#E3F2FD";const{primary_type:r,primary_status:i}=n;let a=F$(r);return i==="enacted"?mk(a,-20):i==="failed"?mk(a,40):a},mk=(e,t)=>{const n=parseInt(e.replace("#",""),16),r=Math.round(2.55*t),i=(n>>16)+r,a=(n>>8&255)+r,s=(n&255)+r;return"#"+(16777216+(i<255?i<1?0:i:255)*65536+(a<255?a<1?0:a:255)*256+(s<255?s<1?0:s:255)).toString(16).slice(1).toUpperCase()},i1e=(e,t)=>{const n=t[e];if(!n||n.total_bills===0)return null;const{primary_status:r}=n;return r==="failed"?"crosshatch":r==="enacted"?"diagonal":null};function a1e({stateData:e,onStateClick:t,legend:n}){const[r,i]=N.useState(null),[a,s]=N.useState({x:0,y:0}),c=N.useRef(null),u=N.useRef(null),d=N.useRef(null),h=(n==null?void 0:n.types)||{};n!=null&&n.statuses;const m=y=>{if(!y||!d.current)return{x:0,y:0};const w=y.getBoundingClientRect(),b=d.current.getBoundingClientRect();return{x:w.left-b.left+w.width/2,y:w.top-b.top}};N.useEffect(()=>{const y=()=>{r&&u.current&&s(m(u.current))};if(r)return window.addEventListener("scroll",y,!0),()=>window.removeEventListener("scroll",y,!0)},[r]);const p=(y,w)=>{c.current&&(clearTimeout(c.current),c.current=null),u.current=y.target,r?r!==w&&(c.current=setTimeout(()=>{i(w),s(m(y.target))},200)):(i(w),s(m(y.target)))},v=()=>{c.current&&(clearTimeout(c.current),c.current=null)},_=()=>{i(null),c.current&&(clearTimeout(c.current),c.current=null)},x=r?e[r]:null;return o.jsxs("div",{ref:d,className:"relative",children:[o.jsx("svg",{width:"0",height:"0",children:o.jsxs("defs",{children:[o.jsxs("pattern",{id:"crosshatch",width:"10",height:"10",patternUnits:"userSpaceOnUse",children:[o.jsx("line",{x1:"0",y1:"0",x2:"10",y2:"10",stroke:"#000",strokeWidth:"1",opacity:"0.3"}),o.jsx("line",{x1:"10",y1:"0",x2:"0",y2:"10",stroke:"#000",strokeWidth:"1",opacity:"0.3"})]}),o.jsx("pattern",{id:"diagonal",width:"10",height:"10",patternUnits:"userSpaceOnUse",children:o.jsx("line",{x1:"0",y1:"0",x2:"10",y2:"10",stroke:"#000",strokeWidth:"2",opacity:"0.4"})})]})}),o.jsx(Tj,{projection:"geoAlbersUsa",projectionConfig:{scale:1e3},className:"w-full h-auto",children:o.jsx(Mj,{geography:e1e,children:({geographies:y})=>y.map(w=>{const b=w.id,j=n1e[b]||b;e[j];const E=i1e(j,e);return o.jsx(Wbe,{geography:w,fill:E?`url(#${E})`:hk(j,e),stroke:"#FFFFFF",strokeWidth:.5,style:{default:{fill:hk(j,e),outline:"none"},hover:{fill:"#607D8B",outline:"none",cursor:"pointer"},pressed:{fill:"#455A64",outline:"none"}},onClick:()=>t==null?void 0:t(j),onMouseEnter:P=>p(P,j),onMouseLeave:v},w.rsmKey)})})}),r&&x&&o.jsx("div",{className:"absolute z-50 pointer-events-auto",style:{left:`${a.x}px`,top:`${a.y-10}px`,transform:"translate(-50%, -100%)"},children:o.jsxs("div",{className:"bg-gray-900 text-white px-4 py-3 rounded-lg shadow-xl max-w-sm border border-gray-700",children:[o.jsx("button",{onClick:_,className:"absolute top-2 right-2 text-gray-400 hover:text-white transition-colors","aria-label":"Close tooltip",children:o.jsx("svg",{className:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:o.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})}),o.jsxs("div",{className:"flex items-center justify-between mb-3",children:[o.jsx("div",{className:"font-bold text-lg",children:r}),o.jsxs("div",{className:"text-xs text-gray-400",children:[x.total_bills.toLocaleString()," bill",x.total_bills!==1?"s":""]})]}),x.total_bills>0&&o.jsxs(o.Fragment,{children:[o.jsx("div",{className:"mb-3 p-2 bg-gray-800 rounded-md",children:o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("div",{className:` + w-3 h-3 rounded-full + ${x.primary_type==="removal"?"bg-red-500":x.primary_type==="mandate"?"bg-green-500":x.primary_type==="study"?"bg-blue-500":x.primary_type==="funding"?"bg-yellow-500":"bg-gray-500"} + `}),o.jsxs("div",{children:[o.jsx("div",{className:"text-sm font-semibold text-white",children:h[x.primary_type]||x.primary_type}),o.jsx("div",{className:"text-xs text-gray-400",children:"Primary Type"})]})]}),o.jsx("div",{className:` + px-2 py-1 rounded text-xs font-medium + ${x.primary_status==="enacted"?"bg-green-500/30 text-green-300 border border-green-500/50":x.primary_status==="failed"?"bg-red-500/30 text-red-300 border border-red-500/50":"bg-yellow-500/30 text-yellow-300 border border-yellow-500/50"} + `,children:x.primary_status==="enacted"?"✓ Enacted":x.primary_status==="failed"?"✗ Failed":"⏳ Pending"})]})}),x.sample_bills&&x.sample_bills.length>0&&o.jsxs("div",{className:"mb-3 max-h-48 overflow-y-auto",children:[o.jsx("div",{className:"text-xs font-medium text-gray-300 mb-2",children:"Recent Bills by Type:"}),o.jsx("div",{className:"space-y-2",children:(()=>{const y=x.sample_bills.reduce((w,b)=>(w[b.type]||(w[b.type]=[]),w[b.type].push(b),w),{});return Object.entries(y).map(([w,b])=>o.jsxs("div",{className:"bg-gray-800/50 rounded p-2",children:[o.jsxs("div",{className:"flex items-center gap-2 mb-1.5",children:[o.jsx("div",{className:` + w-2 h-2 rounded-full + ${w==="removal"?"bg-red-500":w==="mandate"?"bg-green-500":w==="study"?"bg-blue-500":w==="funding"?"bg-yellow-500":"bg-gray-500"} + `}),o.jsxs("div",{className:"text-xs font-medium text-gray-200",children:[h[w]||w," (",b.length,")"]})]}),o.jsx("div",{className:"space-y-1 ml-4",children:b.map((j,E)=>o.jsxs(ke,{to:`/bill/${j.state}-${j.bill_number}`,className:"block text-xs hover:bg-gray-700/50 rounded px-2 py-1 transition-colors group",children:[o.jsxs("div",{className:"flex items-center justify-between gap-2",children:[o.jsx("span",{className:"font-mono text-blue-300 group-hover:text-blue-200 font-semibold",children:j.bill_number}),o.jsx("span",{className:` + px-1.5 py-0.5 rounded text-[10px] font-medium + ${j.status==="enacted"?"bg-green-500/20 text-green-300":j.status==="failed"?"bg-red-500/20 text-red-300":"bg-yellow-500/20 text-yellow-300"} + `,children:j.status==="enacted"?"✓ Enacted":j.status==="failed"?"✗ Failed":"⏳ Pending"})]}),o.jsxs("div",{className:"text-gray-400 text-[10px] mt-0.5 flex items-center gap-1",children:[j.date&&o.jsxs("span",{className:"text-gray-500",children:["📅 ",j.date]}),j.date&&j.action&&o.jsx("span",{className:"text-gray-600",children:"•"}),o.jsx("span",{className:"flex-1 truncate",children:j.action||"Click for details"})]})]},E))})]},w))})()})]}),o.jsxs("div",{className:"grid grid-cols-3 gap-2 mb-3 text-xs",children:[o.jsxs("div",{className:"bg-green-500/10 border border-green-500/30 rounded px-2 py-1.5",children:[o.jsx("div",{className:"text-green-400 font-bold text-base",children:x.status_counts.enacted}),o.jsx("div",{className:"text-green-300/70",children:"Enacted"})]}),o.jsxs("div",{className:"bg-yellow-500/10 border border-yellow-500/30 rounded px-2 py-1.5",children:[o.jsx("div",{className:"text-yellow-400 font-bold text-base",children:x.status_counts.pending}),o.jsx("div",{className:"text-yellow-300/70",children:"Pending"})]}),o.jsxs("div",{className:"bg-red-500/10 border border-red-500/30 rounded px-2 py-1.5",children:[o.jsx("div",{className:"text-red-400 font-bold text-base",children:x.status_counts.failed}),o.jsx("div",{className:"text-red-300/70",children:"Failed"})]})]}),o.jsxs("button",{onClick:()=>t&&t(r),className:"w-full bg-blue-600 hover:bg-blue-700 text-white font-medium text-sm py-2 px-3 rounded transition-colors flex items-center justify-center gap-2",children:[o.jsx("span",{children:"View All Bills"}),o.jsx("span",{className:"text-lg",children:"→"})]})]}),x.total_bills===0&&o.jsx("div",{className:"text-gray-400 text-sm italic text-center py-2",children:"No legislation found"}),o.jsx("div",{className:"absolute left-1/2 bottom-0 transform -translate-x-1/2 translate-y-full",style:{width:0,height:0,borderLeft:"8px solid transparent",borderRight:"8px solid transparent",borderTop:"8px solid rgb(17, 24, 39)"}})]})}),o.jsxs("div",{className:"absolute bottom-4 right-4 bg-white/95 rounded-lg shadow-lg p-4 border border-gray-200 max-w-xs",children:[o.jsx("div",{className:"text-sm font-semibold text-gray-800 mb-3",children:"Legend"}),(()=>{const y=new Set;return Object.values(e).forEach(w=>{w.primary_type&&y.add(w.primary_type)}),y.size>0&&o.jsxs("div",{className:"mb-3",children:[o.jsx("div",{className:"text-xs font-medium text-gray-600 mb-2",children:"Type of Legislation"}),o.jsxs("div",{className:"space-y-1",children:[Array.from(y).sort().map(w=>o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("div",{className:"w-4 h-4 rounded flex-shrink-0",style:{backgroundColor:F$(w)}}),o.jsx("span",{className:"text-xs text-gray-700 capitalize",children:h[w]||w.replace(/_/g," ")})]},w)),o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("div",{className:"w-4 h-4 rounded flex-shrink-0",style:{backgroundColor:"#E3F2FD"}}),o.jsx("span",{className:"text-xs text-gray-700",children:"No Legislation"})]})]})]})})(),o.jsxs("div",{children:[o.jsx("div",{className:"text-xs font-medium text-gray-600 mb-2",children:"Status"}),o.jsxs("div",{className:"space-y-1",children:[o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("div",{className:"w-4 h-4 rounded border border-gray-300 flex-shrink-0",style:{backgroundColor:"#666"}}),o.jsx("span",{className:"text-xs text-gray-700",children:"Enacted (darker)"})]}),o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("div",{className:"w-4 h-4 rounded border border-gray-300 flex-shrink-0 bg-white"}),o.jsx("span",{className:"text-xs text-gray-700",children:"Pending (normal)"})]}),o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("div",{className:"w-4 h-4 rounded border border-gray-300 flex-shrink-0",style:{backgroundColor:"#ddd"}}),o.jsx("span",{className:"text-xs text-gray-700",children:"Failed (lighter)"})]})]})]})]})]})}function o1e(){var F,B,G,R,K;const[e,t]=Us(),[n,r]=N.useState("map"),[i,a]=N.useState("AL"),[s,c]=N.useState(""),[u,d]=N.useState(""),h=e.get("topic")||"",[m,p]=N.useState(h),[v,_]=N.useState(!h);N.useEffect(()=>{t(m?{topic:m}:{})},[m,t]);const[x,y]=N.useState(1),w=20,{data:b,isLoading:j,error:E}=zt({queryKey:["billsMap",m,s],queryFn:async()=>{const W=new URLSearchParams;return m&&W.append("topic",m),s&&W.append("session",s),(await vt.get(`/bills/map?${W}`)).data},enabled:n==="map"&&!v&&m!=="",retry:2,retryDelay:1e3}),{data:P}=zt({queryKey:["sessions",i],queryFn:async()=>(await vt.get(`/bills/sessions?state=${i}`)).data,enabled:n==="list",retry:2,retryDelay:1e3}),{data:O,isLoading:C,error:A}=zt({queryKey:["bills",i,s,u,x],queryFn:async()=>{const W=new URLSearchParams({state:i,limit:w.toString(),offset:((x-1)*w).toString()});return s&&W.append("session",s),u&&W.append("q",u),(await vt.get(`/bills?${W}`)).data},enabled:n==="list",retry:2,retryDelay:1e3}),T=Math.ceil(((O==null?void 0:O.total)||0)/w),$=W=>{a(W),r("list")},z=W=>{p(W),_(!1),r("map")},D=()=>{_(!0),p("")},Z=b?Object.values(b.states).filter(W=>W.total_bills>0).length:0,I=b?Object.values(b.states).reduce((W,U)=>W+U.total_bills,0):0;return o.jsx("div",{className:"min-h-screen bg-gray-50 py-8",children:o.jsxs("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:[o.jsx("div",{className:"mb-8",children:o.jsxs("div",{className:"flex items-center justify-between",children:[o.jsxs("div",{children:[o.jsx("h1",{className:"text-3xl font-bold text-gray-900 mb-1",children:"📜 Legislative Policy Map"}),o.jsx("p",{className:"text-gray-600",children:v?"Choose a topic to explore state-by-state legislation":"Track state legislation initiatives compared across the country"})]}),!v&&o.jsx("button",{onClick:D,className:"flex items-center gap-2 px-4 py-2 rounded-md font-medium bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors",children:"← Back to Topics"}),!v&&o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsxs("button",{onClick:()=>r("map"),className:`flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-colors ${n==="map"?"bg-blue-600 text-white":"bg-gray-200 text-gray-700 hover:bg-gray-300"}`,children:[o.jsx(la,{className:"h-5 w-5"}),"Map View"]}),o.jsxs("button",{onClick:()=>r("list"),className:`flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-colors ${n==="list"?"bg-blue-600 text-white":"bg-gray-200 text-gray-700 hover:bg-gray-300"}`,children:[o.jsx(tz,{className:"h-5 w-5"}),"List View"]})]})]})}),v&&o.jsxs("div",{className:"space-y-8",children:[o.jsxs("div",{className:"text-center max-w-3xl mx-auto",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-3",children:"Select a Policy Topic"}),o.jsx("p",{className:"text-gray-600",children:"Choose a topic below to see how states across the country are addressing it through legislation"})]}),o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6",children:[o.jsxs("button",{onClick:()=>z("fluoride"),className:"bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-all hover:scale-105 text-left border-2 border-transparent hover:border-blue-500",children:[o.jsx("div",{className:"text-5xl mb-4",children:"💧"}),o.jsx("h3",{className:"text-xl font-bold text-gray-900 mb-2",children:"Water Fluoridation"}),o.jsx("p",{className:"text-gray-600 text-sm mb-4",children:"Track mandates, removals, funding initiatives, and studies on community water fluoridation programs"}),o.jsx("div",{className:"text-blue-600 font-medium text-sm",children:"View Legislation →"})]}),o.jsxs("button",{onClick:()=>z("dental"),className:"bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-all hover:scale-105 text-left border-2 border-transparent hover:border-blue-500",children:[o.jsx("div",{className:"text-5xl mb-4",children:"🦷"}),o.jsx("h3",{className:"text-xl font-bold text-gray-900 mb-2",children:"Dental Health"}),o.jsx("p",{className:"text-gray-600 text-sm mb-4",children:"Monitor coverage expansions, screening programs, provider access, and funding for dental health services"}),o.jsx("div",{className:"text-blue-600 font-medium text-sm",children:"View Legislation →"})]}),o.jsxs("button",{onClick:()=>z("oral health"),className:"bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-all hover:scale-105 text-left border-2 border-transparent hover:border-blue-500",children:[o.jsx("div",{className:"text-5xl mb-4",children:"😁"}),o.jsx("h3",{className:"text-xl font-bold text-gray-900 mb-2",children:"Oral Health (General)"}),o.jsx("p",{className:"text-gray-600 text-sm mb-4",children:"Explore comprehensive oral health policies including prevention, treatment, and public health initiatives"}),o.jsx("div",{className:"text-blue-600 font-medium text-sm",children:"View Legislation →"})]}),o.jsxs("button",{onClick:()=>z("medicaid"),className:"bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-all hover:scale-105 text-left border-2 border-transparent hover:border-blue-500",children:[o.jsx("div",{className:"text-5xl mb-4",children:"🏥"}),o.jsx("h3",{className:"text-xl font-bold text-gray-900 mb-2",children:"Medicaid"}),o.jsx("p",{className:"text-gray-600 text-sm mb-4",children:"Follow Medicaid expansions, coverage changes, reimbursement rates, and eligibility requirements"}),o.jsx("div",{className:"text-blue-600 font-medium text-sm",children:"View Legislation →"})]}),o.jsxs("button",{onClick:()=>z("education"),className:"bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-all hover:scale-105 text-left border-2 border-transparent hover:border-blue-500",children:[o.jsx("div",{className:"text-5xl mb-4",children:"🎓"}),o.jsx("h3",{className:"text-xl font-bold text-gray-900 mb-2",children:"Education"}),o.jsx("p",{className:"text-gray-600 text-sm mb-4",children:"View educational requirements, curriculum changes, funding initiatives, and school health programs"}),o.jsx("div",{className:"text-blue-600 font-medium text-sm",children:"View Legislation →"})]}),o.jsxs("button",{onClick:()=>z("health"),className:"bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-all hover:scale-105 text-left border-2 border-transparent hover:border-blue-500",children:[o.jsx("div",{className:"text-5xl mb-4",children:"🏨"}),o.jsx("h3",{className:"text-xl font-bold text-gray-900 mb-2",children:"Health (General)"}),o.jsx("p",{className:"text-gray-600 text-sm mb-4",children:"Examine broader health policies including protections, restrictions, funding, and healthcare reforms"}),o.jsx("div",{className:"text-blue-600 font-medium text-sm",children:"View Legislation →"})]})]})]}),!v&&o.jsxs(o.Fragment,{children:[o.jsx("div",{className:"bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4",children:o.jsx("div",{className:"flex items-center justify-between",children:o.jsxs("div",{className:"flex items-center gap-3",children:[o.jsxs("span",{className:"text-2xl",children:[m==="fluoride"&&"💧",m==="dental"&&"🦷",m==="oral health"&&"😁",m==="medicaid"&&"🏥",m==="education"&&"🎓",m==="health"&&"🏨"]}),o.jsxs("div",{children:[o.jsx("div",{className:"text-sm text-gray-600",children:"Viewing legislation for:"}),o.jsx("div",{className:"text-lg font-bold text-gray-900 capitalize",children:m==="fluoride"?"Water Fluoridation":m==="dental"?"Dental Health":m==="oral health"?"Oral Health":m==="medicaid"?"Medicaid":m==="education"?"Education":"Health"})]})]})})}),o.jsx("div",{className:"bg-white rounded-lg shadow-sm p-4 mb-4",children:o.jsxs("div",{className:"flex flex-wrap items-end gap-4",children:[n==="list"&&o.jsxs("div",{className:"flex-1 min-w-[150px]",children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"State"}),o.jsxs("select",{className:"block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm text-gray-900 py-2",value:i,onChange:W=>{a(W.target.value),y(1)},children:[o.jsx("option",{value:"AL",children:"Alabama"}),o.jsx("option",{value:"GA",children:"Georgia"}),o.jsx("option",{value:"IN",children:"Indiana"}),o.jsx("option",{value:"MA",children:"Massachusetts"}),o.jsx("option",{value:"WA",children:"Washington"}),o.jsx("option",{value:"WI",children:"Wisconsin"})]})]}),n==="list"&&o.jsxs("div",{className:"flex-1 min-w-[200px]",children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:"Legislative Session"}),o.jsxs("select",{className:"block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm text-gray-900 py-2",value:s,onChange:W=>{c(W.target.value),y(1)},children:[o.jsx("option",{value:"",children:"All Sessions"}),(F=P==null?void 0:P.sessions)==null?void 0:F.map(W=>o.jsxs("option",{value:W.session,children:[W.session_name," (",W.bill_count.toLocaleString()," bills)"]},W.session))]})]}),o.jsxs("div",{className:"flex-1 min-w-[250px]",children:[o.jsx("label",{className:"block text-sm font-medium text-gray-700 mb-1",children:n==="map"?"Search Keywords":"Search Bills"}),o.jsxs("div",{className:"relative",children:[o.jsx("input",{type:"text",className:"block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 pl-10 text-sm text-gray-900 py-2",placeholder:"Search within results...",value:u,onChange:W=>d(W.target.value),onKeyPress:W=>W.key==="Enter"&&y(1)}),o.jsx(fn,{className:"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400"})]})]}),(u||s)&&o.jsx("button",{type:"button",onClick:()=>{d(""),c(""),y(1)},className:"px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors text-sm font-medium",children:"Clear"})]})}),n==="map"&&o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 mb-6",children:[o.jsxs("div",{className:"mb-6 border-b border-gray-200 pb-4",children:[o.jsx("h2",{className:"text-2xl font-bold text-gray-900 mb-2",children:m?o.jsxs(o.Fragment,{children:[m.charAt(0).toUpperCase()+m.slice(1)," Legislation Across the US"]}):o.jsx(o.Fragment,{children:"State-by-State Legislative Policy Overview"})}),o.jsxs("p",{className:"text-base text-gray-600",children:[m==="fluoride"&&o.jsx(o.Fragment,{children:"See which states mandate water fluoridation, which have removed it, and where funding or studies are underway. Each state's color shows the primary type of legislation, while darker/lighter shades indicate whether bills have been enacted, are pending, or have failed."}),m==="dental"&&o.jsx(o.Fragment,{children:"Track dental health policies including coverage expansion, screening programs, provider access initiatives, and funding. Colors show the main focus of legislation in each state, with shading indicating current status."}),m==="medicaid"&&o.jsx(o.Fragment,{children:"Monitor Medicaid program changes across states, including expansions, coverage modifications, reimbursement adjustments, and eligibility requirements. The map shows what type of Medicaid legislation is most active in each state."}),m==="health"&&o.jsx(o.Fragment,{children:"View health-related legislation including protections, restrictions, funding initiatives, and healthcare reforms. Each state's color indicates the dominant type of health policy being considered or enacted."}),m==="education"&&o.jsx(o.Fragment,{children:"Explore educational policy across states, from new requirements and curriculum changes to funding initiatives and system reforms. Colors represent the primary focus of education legislation in each state."}),!m&&o.jsx(o.Fragment,{children:"This interactive map shows legislative activity across all 50 states. Click any state to drill down into specific bills, or use the topic filter above to focus on a particular policy area. Colors indicate the primary type of legislation, while shading shows whether bills have been enacted (darker), are pending (normal), or failed (lighter)."})]})]}),E?o.jsxs("div",{className:"bg-red-50 border border-red-200 rounded-lg p-8 text-center",children:[o.jsx("div",{className:"text-red-600 text-5xl mb-4",children:"⚠️"}),o.jsx("h3",{className:"text-lg font-semibold text-red-900 mb-2",children:"Failed to load map data"}),o.jsx("p",{className:"text-red-700",children:String(E)})]}):j?o.jsx("div",{className:"flex justify-center items-center h-96",children:o.jsxs("div",{className:"text-center",children:[o.jsx("div",{className:"animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"}),o.jsx("p",{className:"text-gray-600",children:"Loading map..."})]})}):o.jsx(a1e,{stateData:(b==null?void 0:b.states)||{},onStateClick:$,legend:b==null?void 0:b.legend})]}),n==="map"&&b&&o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-6 mb-6",children:[o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 border-l-4 border-blue-500",children:[o.jsx("div",{className:"text-sm font-medium text-gray-600 uppercase tracking-wide",children:"States with Legislation"}),o.jsx("div",{className:"mt-2 text-3xl font-bold text-gray-900",children:Z}),o.jsx("div",{className:"text-sm text-gray-500 mt-1",children:m?`matching "${m}"`:"all topics"})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 border-l-4 border-green-500",children:[o.jsx("div",{className:"text-sm font-medium text-gray-600 uppercase tracking-wide",children:"Total Bills"}),o.jsx("div",{className:"mt-2 text-3xl font-bold text-gray-900",children:I.toLocaleString()}),o.jsx("div",{className:"text-sm text-gray-500 mt-1",children:"across all states"})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 border-l-4 border-purple-500",children:[o.jsx("div",{className:"text-sm font-medium text-gray-600 uppercase tracking-wide",children:"Filter Topic"}),o.jsx("div",{className:"mt-2 text-xl font-bold text-gray-900",children:m||"All Topics"}),o.jsx("div",{className:"text-sm text-gray-500 mt-1",children:"Click map to drill down"})]})]}),n==="list"&&O&&o.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-6 mb-6",children:[o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 border-l-4 border-blue-500",children:[o.jsx("div",{className:"text-sm font-medium text-gray-600 uppercase tracking-wide",children:"Total Bills"}),o.jsx("div",{className:"mt-2 text-3xl font-bold text-gray-900",children:O.total.toLocaleString()}),o.jsx("div",{className:"text-sm text-gray-500 mt-1",children:s?`in ${s}`:"all sessions"})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 border-l-4 border-green-500",children:[o.jsx("div",{className:"text-sm font-medium text-gray-600 uppercase tracking-wide",children:"Sessions Available"}),o.jsx("div",{className:"mt-2 text-3xl font-bold text-gray-900",children:(P==null?void 0:P.total_sessions)||0}),o.jsxs("div",{className:"text-sm text-gray-500 mt-1",children:[(G=(B=P==null?void 0:P.sessions)==null?void 0:B[0])==null?void 0:G.session," - ",(K=(R=P==null?void 0:P.sessions)==null?void 0:R[P.sessions.length-1])==null?void 0:K.session]})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 border-l-4 border-purple-500",children:[o.jsx("div",{className:"text-sm font-medium text-gray-600 uppercase tracking-wide",children:"Showing"}),o.jsx("div",{className:"mt-2 text-3xl font-bold text-gray-900",children:O.bills.length}),o.jsxs("div",{className:"text-sm text-gray-500 mt-1",children:["Page ",x," of ",T]})]})]}),n==="list"&&o.jsx(o.Fragment,{children:A?o.jsxs("div",{className:"bg-red-50 border border-red-200 rounded-lg p-8 text-center",children:[o.jsx("div",{className:"text-red-600 text-5xl mb-4",children:"⚠️"}),o.jsx("h3",{className:"text-xl font-semibold text-red-900 mb-2",children:"Unable to Load Bills"}),o.jsx("p",{className:"text-red-700 mb-4",children:A instanceof Error?A.message:"There was an error fetching bills data. The API server may be unavailable."}),o.jsxs("div",{className:"flex gap-3 justify-center",children:[o.jsx("button",{onClick:()=>window.location.reload(),className:"px-6 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors font-medium",children:"Retry"}),o.jsx("button",{onClick:()=>r("map"),className:"px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors font-medium",children:"Switch to Map View"})]})]}):C?o.jsxs("div",{className:"text-center py-12",children:[o.jsx("div",{className:"animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"}),o.jsx("p",{className:"mt-4 text-gray-600",children:"Loading bills..."})]}):o.jsxs(o.Fragment,{children:[o.jsx("div",{className:"space-y-4 mb-6",children:O==null?void 0:O.bills.map(W=>o.jsx("div",{className:"bg-white rounded-lg shadow-sm p-6 hover:shadow-md transition-shadow border-l-4 border-blue-500",children:o.jsx("div",{className:"flex items-start justify-between",children:o.jsxs("div",{className:"flex-1",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-2",children:[o.jsx("span",{className:"inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800",children:W.bill_number}),o.jsx("span",{className:"inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800",children:W.classification.join(", ")}),o.jsx("span",{className:"text-sm text-gray-500",children:W.session_name})]}),o.jsx("h3",{className:"text-lg font-semibold text-gray-900 mb-2",children:W.title}),o.jsxs("div",{className:"flex items-center gap-4 text-sm text-gray-600",children:[o.jsxs("span",{children:[o.jsx("strong",{children:"Latest Action:"})," ",W.latest_action]}),W.latest_action_date&&o.jsxs("span",{children:[o.jsx("strong",{children:"Date:"})," ",new Date(W.latest_action_date).toLocaleDateString()]})]})]})})},W.bill_id))}),T>1&&o.jsxs("div",{className:"flex items-center justify-between bg-white rounded-lg shadow-sm p-4",children:[o.jsxs("div",{className:"text-sm text-gray-600",children:["Showing ",(x-1)*w+1," to"," ",Math.min(x*w,(O==null?void 0:O.total)||0)," of"," ",O==null?void 0:O.total.toLocaleString()," bills"]}),o.jsxs("div",{className:"flex gap-2",children:[o.jsx("button",{onClick:()=>y(W=>Math.max(1,W-1)),disabled:x===1,className:"px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed transition-colors",children:"Previous"}),o.jsxs("span",{className:"px-4 py-2 text-gray-700",children:["Page ",x," of ",T]}),o.jsx("button",{onClick:()=>y(W=>Math.min(T,W+1)),disabled:x===T,className:"px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 disabled:opacity-50 disabled:cursor-not-allowed transition-colors",children:"Next"})]})]}),!C&&!A&&O&&O.bills.length===0&&o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-12 text-center",children:[o.jsx("p",{className:"text-gray-600 text-lg",children:"No bills found matching your filters."}),o.jsx("button",{onClick:()=>{d(""),c(""),y(1)},className:"mt-4 px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors",children:"Clear Filters"})]})]})})]})]})})}function s1e(){var a,s,c,u,d;const{billId:e}=c9(),{data:t,isLoading:n,error:r}=zt({queryKey:["bill",e],queryFn:async()=>(await vt.get(`/bills/${e}`)).data,enabled:!!e});if(n)return o.jsx("div",{className:"min-h-screen bg-gray-50 py-8",children:o.jsx("div",{className:"max-w-4xl mx-auto px-4",children:o.jsx("div",{className:"flex justify-center items-center h-96",children:o.jsxs("div",{className:"text-center",children:[o.jsx("div",{className:"animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"}),o.jsx("p",{className:"text-gray-600",children:"Loading bill details..."})]})})})});if(r||!t){const h=r instanceof Error?r.message:(s=(a=r==null?void 0:r.response)==null?void 0:a.data)!=null&&s.detail?r.response.data.detail:r!=null&&r.message?r.message:"Unable to load bill details";return o.jsx("div",{className:"min-h-screen bg-gray-50 py-8",children:o.jsx("div",{className:"max-w-4xl mx-auto px-4",children:o.jsxs("div",{className:"bg-red-50 border border-red-200 rounded-lg p-8 text-center",children:[o.jsx("div",{className:"text-red-600 text-5xl mb-4",children:"⚠️"}),o.jsx("h3",{className:"text-lg font-semibold text-red-900 mb-2",children:"Bill not found"}),o.jsx("p",{className:"text-red-700 mb-4",children:h}),o.jsxs(ke,{to:"/policy-map",className:"inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors",children:[o.jsx(w5,{className:"h-5 w-5"}),"Back to Policy Map"]})]})})})}const i=(c=t.latest_action)!=null&&c.toLowerCase().includes("enact")?"green":(u=t.latest_action)!=null&&u.toLowerCase().includes("fail")||(d=t.latest_action)!=null&&d.toLowerCase().includes("veto")?"red":"yellow";return o.jsx("div",{className:"min-h-screen bg-gray-50 py-8",children:o.jsxs("div",{className:"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8",children:[o.jsx("div",{className:"mb-6",children:o.jsxs(ke,{to:"/policy-map",className:"inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 font-medium",children:[o.jsx(w5,{className:"h-5 w-5"}),"Back to Policy Map"]})}),o.jsx("div",{className:"bg-white rounded-lg shadow-sm p-6 mb-6",children:o.jsx("div",{className:"flex items-start justify-between mb-4",children:o.jsxs("div",{className:"flex-1",children:[o.jsxs("div",{className:"flex items-center gap-3 mb-2",children:[o.jsx("span",{className:"inline-flex items-center px-4 py-2 rounded-lg text-lg font-bold bg-blue-100 text-blue-800",children:t.bill_number}),o.jsx("span",{className:"inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800",children:t.state}),o.jsx("span",{className:`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${i==="green"?"bg-green-100 text-green-800":i==="red"?"bg-red-100 text-red-800":"bg-yellow-100 text-yellow-800"}`,children:t.latest_action||"Status Unknown"})]}),o.jsx("h1",{className:"text-2xl font-bold text-gray-900 mb-2",children:t.title}),o.jsxs("div",{className:"flex items-center gap-4 text-sm text-gray-600",children:[o.jsxs("div",{className:"flex items-center gap-1",children:[o.jsx(dc,{className:"h-4 w-4"}),o.jsx("span",{children:t.session_name})]}),t.classification&&t.classification.length>0&&o.jsx("span",{className:"text-gray-400",children:"•"}),t.classification&&t.classification.length>0&&o.jsx("span",{children:t.classification.join(", ")})]})]})})}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 mb-6",children:[o.jsxs("h2",{className:"text-lg font-bold text-gray-900 mb-4 flex items-center gap-2",children:[o.jsx(ha,{className:"h-5 w-5"}),"Timeline"]}),o.jsxs("div",{className:"space-y-3",children:[t.first_action_date&&o.jsxs("div",{className:"flex gap-4",children:[o.jsx("div",{className:"text-sm text-gray-600 w-32",children:new Date(t.first_action_date).toLocaleDateString()}),o.jsx("div",{className:"flex-1",children:o.jsx("div",{className:"text-sm font-medium text-gray-900",children:"First Action"})})]}),t.latest_action_date&&o.jsxs("div",{className:"flex gap-4",children:[o.jsx("div",{className:"text-sm text-gray-600 w-32",children:new Date(t.latest_action_date).toLocaleDateString()}),o.jsxs("div",{className:"flex-1",children:[o.jsx("div",{className:"text-sm font-medium text-gray-900",children:"Latest Action"}),o.jsx("div",{className:"text-sm text-gray-600",children:t.latest_action})]})]})]})]}),t.sponsors&&t.sponsors.length>0&&o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 mb-6",children:[o.jsx("h2",{className:"text-lg font-bold text-gray-900 mb-4",children:"Sponsors"}),o.jsx("div",{className:"space-y-2",children:t.sponsors.map((h,m)=>o.jsxs("div",{className:"flex items-center gap-2",children:[o.jsx("span",{className:`px-2 py-1 text-xs font-medium rounded ${h.primary?"bg-blue-100 text-blue-800":"bg-gray-100 text-gray-800"}`,children:h.primary?"Primary":"Co-sponsor"}),o.jsx("span",{className:"text-sm text-gray-900",children:h.name})]},m))})]}),t.actions&&t.actions.length>0&&o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6 mb-6",children:[o.jsxs("h2",{className:"text-lg font-bold text-gray-900 mb-4 flex items-center gap-2",children:[o.jsx(Ws,{className:"h-5 w-5"}),"Legislative Actions"]}),o.jsx("div",{className:"space-y-3",children:t.actions.map((h,m)=>o.jsxs("div",{className:"flex gap-4 border-l-2 border-gray-200 pl-4",children:[o.jsx("div",{className:"text-sm text-gray-600 w-32",children:new Date(h.date).toLocaleDateString()}),o.jsxs("div",{className:"flex-1",children:[o.jsx("div",{className:"text-sm text-gray-900",children:h.description}),h.classification&&h.classification.length>0&&o.jsx("div",{className:"text-xs text-gray-500 mt-1",children:h.classification.join(", ")})]})]},m))})]}),o.jsxs("div",{className:"bg-white rounded-lg shadow-sm p-6",children:[o.jsx("h2",{className:"text-lg font-bold text-gray-900 mb-4",children:"Official Sources"}),o.jsxs("div",{className:"space-y-2",children:[t.openstates_url&&o.jsx("a",{href:t.openstates_url,target:"_blank",rel:"noopener noreferrer",className:"block text-blue-600 hover:text-blue-700 text-sm hover:underline",children:"View on OpenStates →"}),t.sources&&t.sources.map((h,m)=>o.jsxs("a",{href:h.url,target:"_blank",rel:"noopener noreferrer",className:"block text-blue-600 hover:text-blue-700 text-sm hover:underline",children:[h.note||"View on Legislature Website"," →"]},m))]})]})]})})}function l1e(){return o.jsxs(P9,{children:[o.jsx(et,{path:"/",element:o.jsx(Fz,{})}),o.jsx(et,{path:"/classic",element:o.jsx(P5,{}),children:o.jsx(et,{index:!0,element:o.jsx(Rz,{})})}),o.jsxs(et,{path:"/",element:o.jsx(P5,{}),children:[o.jsx(et,{path:"explore",element:o.jsx(Gge,{})}),o.jsx(et,{path:"search",element:o.jsx(n0e,{})}),o.jsx(et,{path:"jurisdictions",element:o.jsx(i0e,{})}),o.jsx(et,{path:"dashboard",element:o.jsx(sge,{})}),o.jsx(et,{path:"analytics",element:o.jsx(lge,{})}),o.jsx(et,{path:"people",element:o.jsx(Bge,{})}),o.jsx(et,{path:"heatmap",element:o.jsx(kge,{})}),o.jsx(et,{path:"policy-map",element:o.jsx(o1e,{})}),o.jsx(et,{path:"bill/:billId",element:o.jsx(s1e,{})}),o.jsx(et,{path:"documents",element:o.jsx(Cge,{})}),o.jsx(et,{path:"opportunities",element:o.jsx(Age,{})}),o.jsx(et,{path:"nonprofits",element:o.jsx(Tge,{})}),o.jsx(et,{path:"nonprofits-hf",element:o.jsx(Rge,{})}),o.jsx(et,{path:"debate-grader",element:o.jsx(zge,{})}),o.jsx(et,{path:"profile",element:o.jsx(Wge,{})}),o.jsx(et,{path:"settings",element:o.jsx(Fge,{})}),o.jsx(et,{path:"events",element:o.jsx(Kge,{})}),o.jsx(et,{path:"services",element:o.jsx(Yge,{})}),o.jsx(et,{path:"developers",element:o.jsx(Xge,{})}),o.jsx(et,{path:"hackathons",element:o.jsx(Qge,{})}),o.jsx(et,{path:"opensource",element:o.jsx(Jge,{})}),o.jsx(et,{path:"advocacy-topics",element:o.jsx(e0e,{})}),o.jsx(et,{path:"fact-checking",element:o.jsx(t0e,{})})]})]})}const c1e=new cF({defaultOptions:{queries:{refetchOnWindowFocus:!1,retry:1,staleTime:5*60*1e3}}});ix.createRoot(document.getElementById("root")).render(o.jsx(H.StrictMode,{children:o.jsx(uF,{client:c1e,children:o.jsxs(L9,{future:{v7_startTransition:!0,v7_relativeSplatPath:!0},children:[o.jsx(NF,{}),o.jsx(_F,{children:o.jsx(jF,{children:o.jsx(l1e,{})})})]})})})); diff --git a/api/static/communityone_logo.jpg b/api/static/communityone_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..67106acec272efff0b126b734c4d83681c2e18cf Binary files /dev/null and b/api/static/communityone_logo.jpg differ diff --git a/api/static/communityone_logo.svg b/api/static/communityone_logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..ce4f291e644305123f831ff0b98c503d5342e3cf --- /dev/null +++ b/api/static/communityone_logo.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + C1 + + + + diff --git a/api/static/communityone_logo_64.png b/api/static/communityone_logo_64.png new file mode 100644 index 0000000000000000000000000000000000000000..696b25f394add52200236e0c000c52be2ebdf0dd Binary files /dev/null and b/api/static/communityone_logo_64.png differ diff --git a/api/static/favicon.ico b/api/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0aad19ae960e9e7bbfa315c0ba4968b7400d1e18 Binary files /dev/null and b/api/static/favicon.ico differ diff --git a/api/static/google6934fc6e3618949f.html b/api/static/google6934fc6e3618949f.html new file mode 100644 index 0000000000000000000000000000000000000000..8455eded11cce9f3642beebc17f1a9897f62d039 --- /dev/null +++ b/api/static/google6934fc6e3618949f.html @@ -0,0 +1 @@ +google-site-verification: google6934fc6e3618949f.html \ No newline at end of file diff --git a/api/static/index.html b/api/static/index.html new file mode 100644 index 0000000000000000000000000000000000000000..367aa1aa15beacfd9fcd0f933f7a668490aae0cf --- /dev/null +++ b/api/static/index.html @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + Open Navigator - AI-Powered Civic Engagement Platform + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + diff --git a/api/static/privacyfacebook.html b/api/static/privacyfacebook.html new file mode 100644 index 0000000000000000000000000000000000000000..4e3b53afc5ee5f56f504657ebdc07293c78051fc --- /dev/null +++ b/api/static/privacyfacebook.html @@ -0,0 +1,276 @@ + + + + + + Privacy Policy - Open Navigator + + + +
+
+

🏛️ Privacy Policy

+

Last Updated: April 26, 2026

+
+ +
+

Open Navigator ("we," "our," or "us") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our platform.

+
+ +
+

1. Information We Collect

+ +

1.1 Information You Provide

+

When you create an account or use our services, we may collect:

+
    +
  • Account Information: Email address, name, and profile information
  • +
  • OAuth Provider Data: When you log in via Google, Facebook, GitHub, or HuggingFace, we receive your public profile information and email address
  • +
  • User Preferences: Settings and preferences you configure within the application
  • +
+ +

1.2 Automatically Collected Information

+
    +
  • Usage Data: Pages visited, features used, and interactions with the platform
  • +
  • Device Information: Browser type, operating system, IP address
  • +
  • Cookies: We use essential cookies for authentication and session management
  • +
+
+ +
+

2. How We Use Your Information

+

We use the collected information to:

+
    +
  • Provide, maintain, and improve our services
  • +
  • Authenticate your account and maintain security
  • +
  • Personalize your experience on the platform
  • +
  • Send important updates about the service
  • +
  • Analyze usage patterns to improve functionality
  • +
  • Comply with legal obligations
  • +
+
+ +
+

3. Third-Party Authentication

+
+

OAuth Providers: We support login via Google, Facebook, GitHub, and HuggingFace. When you use these services:

+
    +
  • We only request access to your email address and basic profile information
  • +
  • We do not store your social media passwords
  • +
  • We do not post to your social media accounts
  • +
  • You can revoke our access at any time through your provider's settings
  • +
+
+
+ +
+

4. Data Storage and Security

+

We implement appropriate technical and organizational measures to protect your information:

+
    +
  • Encryption: Data is encrypted in transit using HTTPS/TLS
  • +
  • Access Controls: Strict access controls limit who can access your data
  • +
  • Secure Authentication: JWT tokens with secure secret keys
  • +
  • Regular Updates: We keep our systems updated with security patches
  • +
+

However, no method of transmission over the internet is 100% secure, and we cannot guarantee absolute security.

+
+ +
+

5. Data Sharing and Disclosure

+

We do not sell your personal information. We may share your information only in the following circumstances:

+
    +
  • With Your Consent: When you explicitly authorize us to share information
  • +
  • Service Providers: With trusted third-party services that help us operate (e.g., hosting providers)
  • +
  • Legal Requirements: When required by law, court order, or governmental authority
  • +
  • Business Transfers: In connection with a merger, acquisition, or sale of assets
  • +
+
+ +
+

6. Public Data Sources

+

Our platform aggregates publicly available information from:

+
    +
  • City council meeting minutes and transcripts
  • +
  • Government public records and budgets
  • +
  • Nonprofit organization databases (IRS Form 990 data)
  • +
  • Legislative information from state and local governments
  • +
+

This public information is not considered personal data and is used to provide civic engagement insights.

+
+ +
+

7. Your Rights and Choices

+

You have the following rights regarding your personal information:

+
    +
  • Access: Request a copy of the personal information we hold about you
  • +
  • Correction: Request correction of inaccurate information
  • +
  • Deletion: Request deletion of your account and personal data
  • +
  • Data Portability: Request your data in a portable format
  • +
  • Opt-Out: Unsubscribe from non-essential communications
  • +
+

To exercise these rights, contact us at the email address provided below.

+
+ +
+

8. Children's Privacy

+

Our service is not intended for children under 13 years of age. We do not knowingly collect personal information from children under 13. If you become aware that a child has provided us with personal information, please contact us, and we will delete such information.

+
+ +
+

9. Data Retention

+

We retain your personal information for as long as necessary to:

+
    +
  • Provide our services to you
  • +
  • Comply with legal obligations
  • +
  • Resolve disputes and enforce our agreements
  • +
+

When you delete your account, we will delete or anonymize your personal information within 30 days, except where required to retain it by law.

+
+ +
+

10. International Data Transfers

+

Your information may be transferred to and processed in countries other than your country of residence. We ensure appropriate safeguards are in place to protect your information in accordance with this Privacy Policy.

+
+ +
+

11. Changes to This Privacy Policy

+

We may update this Privacy Policy from time to time. We will notify you of any changes by:

+
    +
  • Posting the new Privacy Policy on this page
  • +
  • Updating the "Last Updated" date
  • +
  • Sending you an email notification (for material changes)
  • +
+

Your continued use of our services after changes constitutes acceptance of the updated policy.

+
+ +
+

12. Contact Us

+

If you have questions about this Privacy Policy or our privacy practices, please contact us:

+ +
+ +
+

13. Additional Information for EU/UK Users (GDPR)

+

If you are located in the European Union or United Kingdom, you have additional rights under GDPR:

+
    +
  • Legal Basis: We process your data based on consent, contract performance, and legitimate interests
  • +
  • Data Protection Officer: You may contact our DPO at privacy@communityone.com
  • +
  • Supervisory Authority: You have the right to lodge a complaint with your local data protection authority
  • +
  • Automated Decision-Making: We do not use automated decision-making or profiling that produces legal effects
  • +
+
+ +
+

14. California Privacy Rights (CCPA)

+

If you are a California resident, you have specific rights under the California Consumer Privacy Act (CCPA):

+
    +
  • Right to Know: What personal information we collect, use, and share
  • +
  • Right to Delete: Request deletion of your personal information
  • +
  • Right to Opt-Out: Opt-out of the sale of personal information (we do not sell your data)
  • +
  • Non-Discrimination: We will not discriminate against you for exercising your rights
  • +
+
+ +
+

© 2026 Community One. All rights reserved.

+

Open Navigator is an open-source project licensed under the MIT License.

+

Return to Home | View on GitHub

+
+
+ + diff --git a/app.yaml b/app.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b61780c8314c571c9cd0f4789a95dcd6f8e2c1f3 --- /dev/null +++ b/app.yaml @@ -0,0 +1,37 @@ +# Databricks App Configuration +# See: https://docs.databricks.com/en/dev-tools/databricks-apps/index.html + +command: + - "uvicorn" + - "api.app:app" + - "--host" + - "0.0.0.0" + - "--port" + - "8000" + +env: + - name: DATABRICKS_HOST + valueFrom: + databricksSecret: + key: host + scope: oral-health-app + - name: DATABRICKS_TOKEN + valueFrom: + databricksSecret: + key: token + scope: oral-health-app + - name: OPENAI_API_KEY + valueFrom: + databricksSecret: + key: openai_key + scope: oral-health-app + +resources: + - name: policy-classifier-endpoint + modelServing: + endpoint: policy-classifier-prod + - name: sentiment-analyzer-endpoint + modelServing: + endpoint: sentiment-analyzer-prod + +port: 8000 diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7ed48da742ea4348eccc323ddb5593c12190061b --- /dev/null +++ b/config/__init__.py @@ -0,0 +1,4 @@ +"""Configuration module for Oral Health Policy Pulse.""" +from config.settings import settings + +__all__ = ["settings"] diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000000000000000000000000000000000000..59982f937126cf0820203217592c837edd08374b --- /dev/null +++ b/config/settings.py @@ -0,0 +1,120 @@ +""" +Configuration settings for the Oral Health Policy Pulse system. +""" +from typing import List, Optional +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """Application settings with environment variable support.""" + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=False, + extra="ignore" + ) + + # API Keys + openai_api_key: Optional[str] = Field(None, description="OpenAI API key (optional for local mode)") + anthropic_api_key: Optional[str] = Field(None, description="Anthropic API key") + huggingface_token: Optional[str] = Field(None, description="HuggingFace API token for dataset uploads") + census_api_key: Optional[str] = Field(None, description="U.S. Census Bureau API key (free, increases rate limit from 500 to 5000/day)") + dataverse_api_key: Optional[str] = Field(None, description="Harvard Dataverse API key (optional, improves rate limits)") + openstates_api_key: Optional[str] = Field(None, description="Open States API key (free tier: 50k requests/month)") + google_civic_api_key: Optional[str] = Field(None, description="Google Civic Information API key (free tier: 25k requests/day)") + + # Paid services (for reference only - not recommended for free/OSS projects) + ballotpedia_api_key: Optional[str] = Field(None, description="Ballotpedia API v3.0 key (PAID SERVICE - contact Ballotpedia)") + cicero_api_key: Optional[str] = Field(None, description="Cicero API key (PAID SERVICE - enterprise pricing)") + + # HuggingFace Configuration + hf_organization: Optional[str] = Field(None, description="HuggingFace organization name (e.g., 'CommunityOne')") + hf_dataset_prefix: str = Field("open-navigator", description="Prefix for dataset names") + + # Databricks Configuration + databricks_host: Optional[str] = Field(None, description="Databricks workspace URL") + databricks_token: Optional[str] = Field(None, description="Databricks access token") + databricks_warehouse_id: Optional[str] = Field(None, description="SQL warehouse ID") + + # Delta Lake Configuration + # For local mode: use "data/delta" + # For Databricks: use "dbfs:/open-navigator" + delta_lake_path: str = Field("data/delta", description="Delta Lake base path") + catalog_name: str = Field("oral_health", description="Unity Catalog name") + schema_name: str = Field("policy_analysis", description="Schema name") + + # MLflow Configuration (for Databricks Agent Bricks) + mlflow_tracking_uri: str = Field("databricks", description="MLflow tracking URI") + mlflow_experiment_name: str = Field("/Users/shared/oral-health-agents", description="MLflow experiment") + mlflow_model_name_prefix: str = Field("oral_health", description="Model name prefix in Unity Catalog") + + # Agent LLM Configuration + classifier_model: str = Field("gpt-4-turbo-preview", description="LLM model for classification") + sentiment_model_llm: str = Field("gpt-3.5-turbo", description="LLM model for sentiment analysis") + advocacy_model: str = Field("gpt-4-turbo-preview", description="LLM model for advocacy generation") + + # Agent Configuration + max_concurrent_agents: int = Field(5, description="Maximum concurrent agent operations") + scraper_timeout: int = Field(30, description="Scraper timeout in seconds") + classifier_batch_size: int = Field(50, description="Batch size for classification") + sentiment_model: str = Field( + "distilbert-base-uncased-finetuned-sst-2-english", + description="HuggingFace sentiment model" + ) + + # Data Sources (these are FREE public data - no API keys needed) + municode_api_key: Optional[str] = Field(None, description="Municode API key (not required - public data)") + legistar_api_key: Optional[str] = Field(None, description="Legistar API key (not required - public data)") + + # Logging + log_level: str = Field("INFO", description="Logging level") + log_file: str = Field("logs/open-navigator.log", description="Log file path") + + # API Configuration + api_host: str = Field("0.0.0.0", description="API host") + api_port: int = Field(8000, description="API port") + api_workers: int = Field(4, description="Number of API workers") + + # Vector Database + qdrant_host: str = Field("localhost", description="Qdrant host") + qdrant_port: int = Field(6333, description="Qdrant port") + qdrant_collection: str = Field("policy_minutes", description="Qdrant collection name") + + # Email Configuration + smtp_host: str = Field("smtp.gmail.com", description="SMTP host") + smtp_port: int = Field(587, description="SMTP port") + smtp_user: Optional[str] = Field(None, description="SMTP username") + smtp_password: Optional[str] = Field(None, description="SMTP password") + + # Policy Topics of Interest + policy_topics: List[str] = Field( + default=[ + "water fluoridation", + "fluoride", + "school dental screening", + "dental care funding", + "medicaid dental", + "children's dental health", + "oral health", + "dental clinic", + "community dental" + ], + description="Topics to monitor" + ) + + # Geographic Configuration + target_states: Optional[List[str]] = Field( + None, + description="Specific states to monitor (None = all states)" + ) + + min_population_threshold: int = Field( + 10000, + description="Minimum city population to include" + ) + + +# Global settings instance +settings = Settings() diff --git a/databricks/README.md b/databricks/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1dfd70686dfc8c5f162fb465df4e87db160e744a --- /dev/null +++ b/databricks/README.md @@ -0,0 +1,349 @@ +# Databricks Agent Bricks Implementation + +This directory contains the Databricks Agent Bricks (Mosaic AI Agent Framework) implementation for CommunityOne - a generic civic engagement and community data platform. + +## Schema Files + +- **`communityone_schema.sql`** - Current comprehensive schema for all community data (jurisdictions, nonprofits, grants, meetings, observations) +- **`oral_health_schema.sql`** - DEPRECATED - Legacy oral health-specific schema (use communityone_schema.sql instead) + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Databricks Workspace │ +│ │ +│ ┌────────────────┐ ┌──────────────────┐ │ +│ │ Unity Catalog │◄─────┤ MLflow Tracking │ │ +│ │ - Models │ │ - Experiments │ │ +│ │ - Governance │ │ - Runs │ │ +│ │ - Lineage │ │ - Artifacts │ │ +│ └────────┬───────┘ └──────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ Model Serving Endpoints │ │ +│ │ ┌──────────────┐ ┌─────────────────┐ │ │ +│ │ │ Classifier │ │ Sentiment │ │ │ +│ │ │ Agent │ │ Analyzer │ │ │ +│ │ └──────────────┘ └─────────────────┘ │ │ +│ │ Auto-scaling • Observability • A/B │ │ +│ └────────────┬────────────────────────────┘ │ +│ │ │ +└───────────────┼──────────────────────────────────────────────┘ + │ + ▼ + ┌────────────────┐ + │ REST API │ + │ Clients │ + └────────────────┘ +``` + +## Database Schema + +### CommunityOne Schema (`communityone_schema.sql`) + +Comprehensive data warehouse schema supporting: + +**Dimension Tables:** +- `dim_jurisdiction` - Cities, counties, states, school districts +- `dim_organization` - Nonprofits, foundations, churches (IRS EO-BMF) +- `dim_geography` - Geographic hierarchies and FIPS codes +- `dim_date` - Time dimension for temporal analysis +- `dim_measure` - Community outcome indicators (health, education, economic, social) + +**Fact Tables:** +- `fact_communityone_observation` - Community outcome measurements (replaces fact_oral_health_observation) +- `fact_grant` - **NEW** Grant transactions (990 Schedule I, 990-PF, USASpending.gov) +- `fact_nonprofit_finance` - **NEW** Annual 990 filings with revenue breakdowns +- `fact_jurisdiction_budget` - **NEW** Government budgets and spending +- `fact_meeting` - **NEW** Government meetings and public hearings + +**Bridge Tables:** +- `bridge_grant_program_area` - **NEW** Multi-purpose grant program areas + +### Key Changes from oral_health_schema.sql + +✅ **Generic community platform** (not oral health-specific) +✅ **Grant tracking system** aligned with ERD documentation +✅ **Nonprofit-government relationships** via fact_grant +✅ **Foundation giving patterns** (990-PF Schedule I data) +✅ **Complete financial transparency** for grants and budgets + +## Components + +### 1. MLflow Agent Base (`agents/mlflow_base.py`) +- `MLflowAgentBase`: Base class for all agents with MLflow Pyfunc interface +- `MLflowChainAgent`: Base for LangChain-powered agents +- Automatic tracing and observability +- Model Serving compatibility + +### 2. Classifier Agent (`agents/mlflow_classifier.py`) +- Policy topic classification +- Hybrid keyword + LLM approach +- Unity Catalog registered +- Deployable to Model Serving + +### 3. Deployment (`databricks/deployment.py`) +- `AgentDeploymentManager`: Handles registration and deployment +- Unity Catalog integration +- Endpoint management +- A/B testing support + +### 4. Evaluation (`databricks/evaluation.py`) +- `AgentEvaluator`: Quality metrics tracking +- Automated evaluation pipelines +- Regression detection +- Version comparison + +### 5. Notebooks (`databricks/notebooks/`) +- Interactive development environment +- Step-by-step deployment guide +- Evaluation examples +- Delta Lake integration + +## Getting Started + +### Option 1: Databricks Notebook (Recommended) + +1. Import notebook to your workspace: + ``` + databricks workspace import \ + databricks/notebooks/01_agent_bricks_quickstart.py \ + /Users/your-email@company.com/oral-health-agents + ``` + +2. Attach to a cluster with: + - DBR 14.3 LTS ML or higher + - Unity Catalog enabled + +3. Run all cells to: + - Register agents + - Deploy to Model Serving + - Evaluate performance + +### Option 2: Python Script + +1. Set environment variables: + ```bash + export DATABRICKS_HOST="https://your-workspace.cloud.databricks.com" + export DATABRICKS_TOKEN="your-token" + export OPENAI_API_KEY="your-openai-key" + ``` + +2. Register agents: + ```bash + source venv/bin/activate + python -m databricks.deployment + ``` + +3. Run evaluation: + ```bash + python -m databricks.evaluation + ``` + +## Unity Catalog Structure + +``` +main/ # Catalog +├── agents/ # Schema for agent models +│ ├── policy_classifier # Classifier agent +│ ├── sentiment_analyzer # Sentiment agent +│ └── advocacy_writer # Advocacy agent +└── policy_data/ # Schema for data + ├── raw_documents # Scraped documents + ├── classified_documents # Classified results + └── advocacy_opportunities # Identified opportunities +``` + +## Model Serving Endpoints + +### Development Endpoints +- `policy-classifier-dev`: Classifier for testing +- `sentiment-analyzer-dev`: Sentiment analysis +- `advocacy-writer-dev`: Content generation + +### Production Endpoints +- `policy-classifier-prod`: Production classifier +- `multi-agent-pipeline`: Full pipeline with traffic splitting + +## Deployment Workflow + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Development │────►│ Staging │────►│ Production │ +│ │ │ │ │ │ +│ • Local test │ │ • A/B test │ │ • Monitor │ +│ • Register │ │ • Evaluate │ │ • Scale │ +│ • Deploy dev │ │ • Approve │ │ • Feedback │ +└──────────────┘ └──────────────┘ └──────────────┘ +``` + +## API Usage + +### Invoke via REST + +```bash +curl -X POST https://your-workspace.cloud.databricks.com/serving-endpoints/policy-classifier-prod/invocations \ + -H "Authorization: Bearer $DATABRICKS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "dataframe_records": [{ + "document_id": "doc_001", + "title": "City Council Meeting", + "content": "Discussion on water fluoridation..." + }] + }' +``` + +### Invoke via Python SDK + +```python +from databricks.sdk import WorkspaceClient + +w = WorkspaceClient() + +response = w.serving_endpoints.query( + name="policy-classifier-prod", + dataframe_records=[{ + "document_id": "doc_001", + "title": "City Council Meeting", + "content": "Discussion on water fluoridation..." + }] +) + +print(response.predictions[0]) +``` + +## Monitoring & Observability + +### MLflow Tracing +- Automatic trace capture for all agent calls +- LLM request/response logging +- Latency tracking +- Cost estimation + +### View Traces +```python +import mlflow + +# Get traces for a run +traces = mlflow.get_traces( + experiment_id="your-experiment-id", + filter_string="attributes.agent_role = 'classifier'" +) + +for trace in traces: + print(f"Trace ID: {trace.request_id}") + print(f"Latency: {trace.execution_time_ms}ms") + print(f"Status: {trace.status}") +``` + +### Endpoint Metrics +- Request rate +- Latency (P50, P95, P99) +- Error rate +- Token usage +- Cost per request + +## Evaluation + +### Automated Evaluation +```python +from databricks.evaluation import AgentEvaluator + +evaluator = AgentEvaluator("policy_classifier") + +metrics = evaluator.evaluate_classifier( + model_uri="models:/main.agents.policy_classifier/1", + test_documents=test_docs, + ground_truth=labels +) + +print(f"Accuracy: {metrics.accuracy:.2%}") +print(f"F1 Score: {metrics.f1_score:.2%}") +``` + +### A/B Testing +```python +comparison = evaluator.compare_versions( + version_a="1", + version_b="2", + eval_data=eval_df +) + +# Automatically promote if v2 is better +if comparison["improvements"]["accuracy"]["improvement_pct"] > 5: + # Promote to production + manager.deploy_agent( + agent_name="policy_classifier", + endpoint_name="policy-classifier-prod", + version="2" + ) +``` + +## Best Practices + +1. **Version Control**: Always register new versions to Unity Catalog +2. **Evaluate First**: Run evaluation before deploying to production +3. **Monitor Continuously**: Set up alerts for drift and errors +4. **Use Feedback**: Collect corrections and retrain regularly +5. **Scale Gradually**: Start with small workloads, scale up +6. **Cost Optimization**: Use scale-to-zero for dev/staging endpoints + +## Cost Considerations + +| Component | Estimated Cost | +|-----------|---------------| +| Model Serving (Small, scale-to-zero) | ~$0.10-0.50/hour active | +| MLflow Tracking | Included | +| Unity Catalog | Included | +| LLM API calls | $0.002-0.03 per request | + +**Cost Optimization Tips:** +- Use keyword classification before LLM +- Enable scale-to-zero for dev endpoints +- Batch requests when possible +- Cache frequent queries +- Monitor token usage + +## Troubleshooting + +### Issue: Agent fails to load +```python +# Check model status +from mlflow.tracking import MlflowClient + +client = MlflowClient() +versions = client.search_model_versions( + filter_string="name='main.agents.policy_classifier'" +) +print(versions[0].status) +``` + +### Issue: Endpoint is slow +- Check workload size (upgrade from Small to Medium) +- Enable auto-scaling +- Review LLM prompt length +- Add caching layer + +### Issue: High error rate +- Check MLflow traces for specific errors +- Verify input schema matches signature +- Review LLM API rate limits +- Check Unity Catalog permissions + +## Next Steps + +1. **Deploy More Agents**: Add sentiment analyzer and advocacy writer +2. **Create Workflows**: Use Databricks Workflows for scheduled processing +3. **Add Feedback Loop**: Store corrections in Delta Lake +4. **Set Up Alerts**: Monitor for drift and errors +5. **Scale Production**: Process thousands of documents + +## Resources + +- [Databricks Agent Framework Docs](https://docs.databricks.com/en/generative-ai/agent-framework/index.html) +- [MLflow Agent Deployment](https://mlflow.org/docs/latest/llms/deployments/index.html) +- [Unity Catalog AI](https://docs.databricks.com/en/generative-ai/unity-catalog-ai.html) +- [Model Serving Guide](https://docs.databricks.com/en/machine-learning/model-serving/index.html) diff --git a/databricks/communityone_schema.sql b/databricks/communityone_schema.sql new file mode 100644 index 0000000000000000000000000000000000000000..afe5d474aa898f4cb854d5b3a265993390b3ce21 --- /dev/null +++ b/databricks/communityone_schema.sql @@ -0,0 +1,641 @@ +/* + * ER/Studio Data Architect SQL Code Generation + * Project : CommunityOne Schema - Generic Community Engagement Data Platform + * + * Date Created : Monday, April 28, 2026 + * Target DBMS : Databricks + * + * Description: Comprehensive schema for tracking civic engagement including: + * - Government jurisdictions and officials + * - Nonprofit organizations and grants + * - Meetings, legislation, and policy decisions + * - Community health and social outcome observations + */ + +/* ======================================== + * DIMENSION TABLES + * ======================================== */ + +/* + * TABLE: dim_data_source + */ +CREATE TABLE dim_data_source +( + source_key string NOT NULL, + data_steward_desc string, + data_steward_code string, + dataset_desc string, + dataset_code string, + collection_mode_type string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_data_source PRIMARY KEY (source_key) NOT ENFORCED +) +COMMENT 'Data source metadata and lineage tracking' +; + +/* + * TABLE: dim_date + */ +CREATE TABLE dim_date +( + date_key int NOT NULL, + full_date date NOT NULL, + day_of_month int, + day_of_week int, + day_of_week_name string, + is_weekend boolean, + week_of_year int, + iso_week string, + month_number int, + month_name string, + month_abbr string, + year_month int, + year_month_name string, + quarter_number int, + quarter_name string, + year int, + fiscal_year int, + fiscal_quarter int, + fiscal_month int, + is_holiday boolean DEFAULT FALSE, + holiday_name string, + is_pilot_period boolean DEFAULT FALSE, + is_baseline_period boolean DEFAULT FALSE, + CONSTRAINT dim_date_pk PRIMARY KEY (date_key) +) +TBLPROPERTIES('delta.feature.allowColumnDefaults' = 'supported') +COMMENT 'Time dimension for temporal analysis and trend tracking' +; + +/* + * TABLE: dim_geography + */ +CREATE TABLE dim_geography +( + geography_key string NOT NULL, + geo_type string, + fips_code string, + geo_name_desc string, + county_name_desc string, + state_code string, + record_start_dttm timestamp, + record_end_dttm timestamp, + current_record_ind smallint, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_geography PRIMARY KEY (geography_key) NOT ENFORCED +) +COMMENT 'Geographic dimension - cities, counties, states, districts' +; + +/* + * TABLE: dim_jurisdiction + */ +CREATE TABLE dim_jurisdiction +( + jurisdiction_key string NOT NULL, + jurisdiction_id string, + jurisdiction_name string, + jurisdiction_type string, + geography_key string, + ocd_id string, + website_url string, + population int, + record_start_dttm timestamp, + record_end_dttm timestamp, + current_record_ind smallint, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_jurisdiction PRIMARY KEY (jurisdiction_key) NOT ENFORCED +) +COMMENT 'Government jurisdictions - cities, counties, states, school districts' +; + +/* + * TABLE: dim_organization + */ +CREATE TABLE dim_organization +( + organization_key string NOT NULL, + ein string, + organization_name string, + organization_type string, + ntee_code string, + ntee_description string, + subsection_code string, + foundation_code string, + deductibility_status string, + exempt_status_code string, + geography_key string, + state_code string, + city string, + zip_code string, + asset_amount decimal(18, 2), + income_amount decimal(18, 2), + revenue_amount decimal(18, 2), + ruling_date string, + tax_period string, + mission_statement string, + is_private_foundation boolean, + record_start_dttm timestamp, + record_end_dttm timestamp, + current_record_ind smallint, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_organization PRIMARY KEY (organization_key) NOT ENFORCED +) +COMMENT 'Nonprofit organizations, churches, private foundations (IRS EO-BMF)' +; + +/* + * TABLE: dim_measure + */ +CREATE TABLE dim_measure +( + measure_key string NOT NULL, + source_key string, + measure_code string, + measure_desc string, + measure_long_desc string, + measure_category_type string, + measure_level_type string, + measure_tooltip_desc string, + base_unit_desc string, + unit_prefix_code string, + unit_suffix_code string, + indicator_nbr string, + indicator_group_type string, + indicator_desc string, + dashboard_trend_ind boolean, + dashboard_cross_ind boolean, + record_start_dttm timestamp, + record_end_dttm timestamp, + current_record_ind smallint, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_measure PRIMARY KEY (measure_key) NOT ENFORCED +) +COMMENT 'Community outcome measures - health, economic, education, social indicators' +; + +/* + * TABLE: dim_postal + */ +CREATE TABLE dim_postal +( + postal_key string NOT NULL, + postal_code string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_postal PRIMARY KEY (postal_key) NOT ENFORCED +) +; + +/* + * TABLE: dim_state + */ +CREATE TABLE dim_state +( + state_key string NOT NULL, + state_fips_nbr int, + state_name_desc string, + state_abbr string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_state PRIMARY KEY (state_key) NOT ENFORCED +) +; + +/* + * TABLE: dim_statistic_type + */ +CREATE TABLE dim_statistic_type +( + statistic_key string NOT NULL, + statistic_type string, + calculation_method_desc string, + adjustment_desc string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_statistic_type PRIMARY KEY (statistic_key) NOT ENFORCED +) +; + +/* + * TABLE: dim_stratification + */ +CREATE TABLE dim_stratification +( + stratification_key string NOT NULL, + stratification_category_type string, + stratification_level_desc string, + stratification_group_type string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_stratification PRIMARY KEY (stratification_key) NOT ENFORCED +) +COMMENT 'Demographic stratification - age, race, income, education levels' +; + +/* + * TABLE: dim_survey_period + */ +CREATE TABLE dim_survey_period +( + survey_period_key string NOT NULL, + date_type string, + year_nbr int, + year_start_nbr int, + year_end_nbr int, + approx_date date, + duration_desc string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_survey_period PRIMARY KEY (survey_period_key) NOT ENFORCED +) +; + +/* ======================================== + * FACT TABLES + * ======================================== */ + +/* + * TABLE: fact_communityone_observation + * Generic community outcome observations - health, economic, social, education + */ +CREATE TABLE fact_communityone_observation +( + observation_key string NOT NULL, + measure_key string, + geography_key string, + jurisdiction_key string, + stratification_key string, + statistic_key string, + postal_key string NOT NULL, + state_key string NOT NULL, + survey_period_key string NOT NULL, + date_key int NOT NULL, + population_desc string, + value_nbr decimal(18, 6), + ci_present_ind boolean, + ci_lower_nbr decimal(18, 6), + ci_upper_nbr decimal(18, 6), + proportion_nbr decimal(18, 6), + prop_lower_ci_nbr decimal(18, 6), + prop_upper_ci_nbr decimal(18, 6), + cell_size_unweighted_nbr int, + direction_desc string, + source_row_id_nbr bigint, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_fact_communityone PRIMARY KEY (observation_key) NOT ENFORCED +) +COMMENT 'Community outcome observations - health, education, economic, social indicators' +; + +/* + * TABLE: fact_grant + * Individual grant transactions - foundation grants, government grants, federal funding + */ +CREATE TABLE fact_grant +( + grant_key string NOT NULL, + grant_id string, + recipient_org_key string, + recipient_jurisdiction_key string, + funder_org_key string, + funder_jurisdiction_key string, + recipient_ein string, + recipient_name string, + recipient_type string, + funder_ein string, + funder_name string, + funder_type string, + grant_amount decimal(18, 2), + grant_purpose string, + program_area string, + award_date_key int, + start_date_key int, + end_date_key int, + grant_duration_months int, + grant_status string, + funding_source string, + is_multi_year boolean, + restrictions string, + reporting_requirements string, + source_key string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_fact_grant PRIMARY KEY (grant_key) NOT ENFORCED +) +COMMENT 'Grant transactions from 990 Schedule I, 990-PF, USASpending.gov, state grant databases' +; + +/* + * TABLE: fact_nonprofit_finance + * Annual nonprofit financial filings from Form 990 + */ +CREATE TABLE fact_nonprofit_finance +( + filing_key string NOT NULL, + organization_key string, + ein string, + tax_year int, + fiscal_year_end_date_key int, + filing_date_key int, + total_revenue decimal(18, 2), + total_expenses decimal(18, 2), + total_assets decimal(18, 2), + total_liabilities decimal(18, 2), + net_assets decimal(18, 2), + program_expenses decimal(18, 2), + admin_expenses decimal(18, 2), + fundraising_expenses decimal(18, 2), + grants_paid decimal(18, 2), + contributions_received decimal(18, 2), + government_grants decimal(18, 2), + foundation_grants decimal(18, 2), + corporate_donations decimal(18, 2), + individual_donations decimal(18, 2), + membership_dues decimal(18, 2), + special_events_revenue decimal(18, 2), + program_service_revenue decimal(18, 2), + investment_income decimal(18, 2), + rental_income decimal(18, 2), + other_revenue decimal(18, 2), + employee_count int, + volunteer_count int, + overhead_ratio decimal(8, 4), + fundraising_efficiency decimal(8, 4), + form_990_url string, + source_key string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_fact_nonprofit_finance PRIMARY KEY (filing_key) NOT ENFORCED +) +COMMENT 'Annual nonprofit 990 filings with revenue breakdown and efficiency metrics' +; + +/* + * TABLE: fact_jurisdiction_budget + * Government budgets and spending by jurisdiction + */ +CREATE TABLE fact_jurisdiction_budget +( + budget_key string NOT NULL, + jurisdiction_key string, + fiscal_year int, + fiscal_year_start_date_key int, + fiscal_year_end_date_key int, + budget_type string, + total_revenue decimal(18, 2), + total_expenditures decimal(18, 2), + total_debt decimal(18, 2), + property_tax_revenue decimal(18, 2), + sales_tax_revenue decimal(18, 2), + federal_grants decimal(18, 2), + state_grants decimal(18, 2), + general_fund_balance decimal(18, 2), + budget_document_url string, + published_date_key int, + source_key string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_fact_jurisdiction_budget PRIMARY KEY (budget_key) NOT ENFORCED +) +COMMENT 'Government budgets and financial data by jurisdiction' +; + +/* + * TABLE: fact_meeting + * Government meetings, hearings, trainings, community events + */ +CREATE TABLE fact_meeting +( + meeting_key string NOT NULL, + meeting_id string, + jurisdiction_key string, + meeting_date_key int, + meeting_type string, + meeting_title string, + body_name string, + status string, + platform string, + source_url string, + has_agenda boolean, + has_minutes boolean, + has_video boolean, + topic_tags array, + location_type string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_fact_meeting PRIMARY KEY (meeting_key) NOT ENFORCED +) +COMMENT 'Government meetings, public hearings, trainings, community events' +; + +/* + * TABLE: bridge_grant_program_area + * Many-to-many relationship between grants and program areas (grants can support multiple areas) + */ +CREATE TABLE bridge_grant_program_area +( + grant_key string NOT NULL, + program_area_code string NOT NULL, + program_area_desc string, + allocation_pct decimal(5, 2), + record_created_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_bridge_grant_program PRIMARY KEY (grant_key, program_area_code) NOT ENFORCED +) +COMMENT 'Bridge table for grant program areas (multi-purpose grants)' +; + +/* ======================================== + * FOREIGN KEY CONSTRAINTS + * ======================================== */ + +/* dim_measure */ +ALTER TABLE dim_measure ADD CONSTRAINT fk_measure_source + FOREIGN KEY (source_key) + REFERENCES dim_data_source NOT ENFORCED +; + +/* dim_jurisdiction */ +ALTER TABLE dim_jurisdiction ADD CONSTRAINT fk_jurisdiction_geography + FOREIGN KEY (geography_key) + REFERENCES dim_geography NOT ENFORCED +; + +/* dim_organization */ +ALTER TABLE dim_organization ADD CONSTRAINT fk_organization_geography + FOREIGN KEY (geography_key) + REFERENCES dim_geography NOT ENFORCED +; + +/* fact_communityone_observation */ +ALTER TABLE fact_communityone_observation ADD CONSTRAINT fk_observation_postal + FOREIGN KEY (postal_key) + REFERENCES dim_postal NOT ENFORCED +; + +ALTER TABLE fact_communityone_observation ADD CONSTRAINT fk_observation_state + FOREIGN KEY (state_key) + REFERENCES dim_state NOT ENFORCED +; + +ALTER TABLE fact_communityone_observation ADD CONSTRAINT fk_observation_survey_period + FOREIGN KEY (survey_period_key) + REFERENCES dim_survey_period NOT ENFORCED +; + +ALTER TABLE fact_communityone_observation ADD CONSTRAINT fk_observation_date + FOREIGN KEY (date_key) + REFERENCES dim_date NOT ENFORCED +; + +ALTER TABLE fact_communityone_observation ADD CONSTRAINT fk_observation_geography + FOREIGN KEY (geography_key) + REFERENCES dim_geography NOT ENFORCED +; + +ALTER TABLE fact_communityone_observation ADD CONSTRAINT fk_observation_jurisdiction + FOREIGN KEY (jurisdiction_key) + REFERENCES dim_jurisdiction NOT ENFORCED +; + +ALTER TABLE fact_communityone_observation ADD CONSTRAINT fk_observation_measure + FOREIGN KEY (measure_key) + REFERENCES dim_measure NOT ENFORCED +; + +ALTER TABLE fact_communityone_observation ADD CONSTRAINT fk_observation_statistic + FOREIGN KEY (statistic_key) + REFERENCES dim_statistic_type NOT ENFORCED +; + +ALTER TABLE fact_communityone_observation ADD CONSTRAINT fk_observation_stratification + FOREIGN KEY (stratification_key) + REFERENCES dim_stratification NOT ENFORCED +; + +/* fact_grant */ +ALTER TABLE fact_grant ADD CONSTRAINT fk_grant_recipient_org + FOREIGN KEY (recipient_org_key) + REFERENCES dim_organization NOT ENFORCED +; + +ALTER TABLE fact_grant ADD CONSTRAINT fk_grant_recipient_jurisdiction + FOREIGN KEY (recipient_jurisdiction_key) + REFERENCES dim_jurisdiction NOT ENFORCED +; + +ALTER TABLE fact_grant ADD CONSTRAINT fk_grant_funder_org + FOREIGN KEY (funder_org_key) + REFERENCES dim_organization NOT ENFORCED +; + +ALTER TABLE fact_grant ADD CONSTRAINT fk_grant_funder_jurisdiction + FOREIGN KEY (funder_jurisdiction_key) + REFERENCES dim_jurisdiction NOT ENFORCED +; + +ALTER TABLE fact_grant ADD CONSTRAINT fk_grant_award_date + FOREIGN KEY (award_date_key) + REFERENCES dim_date NOT ENFORCED +; + +ALTER TABLE fact_grant ADD CONSTRAINT fk_grant_start_date + FOREIGN KEY (start_date_key) + REFERENCES dim_date NOT ENFORCED +; + +ALTER TABLE fact_grant ADD CONSTRAINT fk_grant_end_date + FOREIGN KEY (end_date_key) + REFERENCES dim_date NOT ENFORCED +; + +ALTER TABLE fact_grant ADD CONSTRAINT fk_grant_source + FOREIGN KEY (source_key) + REFERENCES dim_data_source NOT ENFORCED +; + +/* fact_nonprofit_finance */ +ALTER TABLE fact_nonprofit_finance ADD CONSTRAINT fk_finance_organization + FOREIGN KEY (organization_key) + REFERENCES dim_organization NOT ENFORCED +; + +ALTER TABLE fact_nonprofit_finance ADD CONSTRAINT fk_finance_fiscal_year_end + FOREIGN KEY (fiscal_year_end_date_key) + REFERENCES dim_date NOT ENFORCED +; + +ALTER TABLE fact_nonprofit_finance ADD CONSTRAINT fk_finance_filing_date + FOREIGN KEY (filing_date_key) + REFERENCES dim_date NOT ENFORCED +; + +ALTER TABLE fact_nonprofit_finance ADD CONSTRAINT fk_finance_source + FOREIGN KEY (source_key) + REFERENCES dim_data_source NOT ENFORCED +; + +/* fact_jurisdiction_budget */ +ALTER TABLE fact_jurisdiction_budget ADD CONSTRAINT fk_budget_jurisdiction + FOREIGN KEY (jurisdiction_key) + REFERENCES dim_jurisdiction NOT ENFORCED +; + +ALTER TABLE fact_jurisdiction_budget ADD CONSTRAINT fk_budget_fiscal_year_start + FOREIGN KEY (fiscal_year_start_date_key) + REFERENCES dim_date NOT ENFORCED +; + +ALTER TABLE fact_jurisdiction_budget ADD CONSTRAINT fk_budget_fiscal_year_end + FOREIGN KEY (fiscal_year_end_date_key) + REFERENCES dim_date NOT ENFORCED +; + +ALTER TABLE fact_jurisdiction_budget ADD CONSTRAINT fk_budget_published_date + FOREIGN KEY (published_date_key) + REFERENCES dim_date NOT ENFORCED +; + +ALTER TABLE fact_jurisdiction_budget ADD CONSTRAINT fk_budget_source + FOREIGN KEY (source_key) + REFERENCES dim_data_source NOT ENFORCED +; + +/* fact_meeting */ +ALTER TABLE fact_meeting ADD CONSTRAINT fk_meeting_jurisdiction + FOREIGN KEY (jurisdiction_key) + REFERENCES dim_jurisdiction NOT ENFORCED +; + +ALTER TABLE fact_meeting ADD CONSTRAINT fk_meeting_date + FOREIGN KEY (meeting_date_key) + REFERENCES dim_date NOT ENFORCED +; + +/* bridge_grant_program_area */ +ALTER TABLE bridge_grant_program_area ADD CONSTRAINT fk_bridge_grant + FOREIGN KEY (grant_key) + REFERENCES fact_grant NOT ENFORCED +; diff --git a/databricks/deployment.py b/databricks/deployment.py new file mode 100644 index 0000000000000000000000000000000000000000..e5d8dd1d471c670d75790ba1cb5074b1e3020ef5 --- /dev/null +++ b/databricks/deployment.py @@ -0,0 +1,344 @@ +""" +Deployment scripts for Databricks Agent Bricks. + +Handles: +- Unity Catalog registration +- Model Serving deployment +- Endpoint management +- Version promotion +""" +import os +from typing import Optional, List +import mlflow +from mlflow.tracking import MlflowClient +from databricks.sdk import WorkspaceClient +from databricks.sdk.service.serving import ( + ServedEntityInput, + EndpointCoreConfigInput, + TrafficConfig, + Route +) +from databricks.sdk.service.catalog import ( + ModelVersionInfoStatus +) +from loguru import logger + +from config import settings + + +class AgentDeploymentManager: + """ + Manages deployment of agents to Databricks Model Serving. + + Workflow: + 1. Register agent to Unity Catalog + 2. Create/update serving endpoint + 3. Monitor deployment status + 4. Promote versions (dev -> staging -> prod) + """ + + def __init__(self): + """Initialize deployment manager.""" + self.client = MlflowClient() + self.w = WorkspaceClient( + host=settings.databricks_host, + token=settings.databricks_token + ) + self.catalog = settings.catalog_name + self.schema = settings.schema_name + + def register_agent( + self, + agent_class, + agent_name: str, + description: str, + tags: Optional[dict] = None + ) -> str: + """ + Register an agent to Unity Catalog. + + Args: + agent_class: Agent class to instantiate + agent_name: Name for the registered model (e.g., "policy_classifier") + description: Model description + tags: Optional tags for the model + + Returns: + Model version string + """ + full_model_name = f"{self.catalog}.{self.schema}.{agent_name}" + + logger.info(f"Registering agent: {full_model_name}") + + # Instantiate agent + agent = agent_class() + + # Log to MLflow and register + with mlflow.start_run(run_name=f"register_{agent_name}") as run: + # Log agent metadata + mlflow.log_param("agent_name", agent_name) + mlflow.log_param("catalog", self.catalog) + mlflow.log_param("schema", self.schema) + + if tags: + mlflow.set_tags(tags) + + # Get example for signature + example_input = agent._get_example_input() + example_output = agent.predict(None, example_input) + signature = mlflow.models.infer_signature(example_input, example_output) + + # Log model + model_info = mlflow.pyfunc.log_model( + artifact_path="agent", + python_model=agent, + signature=signature, + registered_model_name=full_model_name, + pip_requirements=self._get_requirements(agent_class) + ) + + run_id = run.info.run_id + + # Get version number + latest_version = self.client.get_latest_versions(full_model_name)[0] + version = latest_version.version + + # Update model description + if description: + self.client.update_registered_model( + name=full_model_name, + description=description + ) + + logger.info(f"✅ Registered {full_model_name} version {version}") + + return version + + def deploy_agent( + self, + agent_name: str, + endpoint_name: str, + version: Optional[str] = None, + workload_size: str = "Small", + scale_to_zero: bool = True, + min_replicas: int = 1, + max_replicas: int = 10 + ) -> str: + """ + Deploy agent to Model Serving endpoint. + + Args: + agent_name: Registered model name + endpoint_name: Serving endpoint name + version: Model version (defaults to latest) + workload_size: Endpoint size (Small, Medium, Large) + scale_to_zero: Enable scale-to-zero + min_replicas: Minimum replicas + max_replicas: Maximum replicas + + Returns: + Endpoint URL + """ + full_model_name = f"{self.catalog}.{self.schema}.{agent_name}" + + # Get version if not specified + if version is None: + latest_version = self.client.get_latest_versions(full_model_name)[0] + version = latest_version.version + + logger.info(f"Deploying {full_model_name} v{version} to {endpoint_name}") + + # Configure served entity + served_entity = ServedEntityInput( + entity_name=full_model_name, + entity_version=version, + workload_size=workload_size, + scale_to_zero_enabled=scale_to_zero, + min_replicas=min_replicas if not scale_to_zero else 0, + max_replicas=max_replicas + ) + + # Create or update endpoint + try: + endpoint = self.w.serving_endpoints.create_and_wait( + name=endpoint_name, + config=EndpointCoreConfigInput( + served_entities=[served_entity] + ) + ) + logger.info(f"✅ Created endpoint: {endpoint_name}") + + except Exception as e: + if "already exists" in str(e).lower(): + # Update existing endpoint + endpoint = self.w.serving_endpoints.update_config_and_wait( + name=endpoint_name, + served_entities=[served_entity] + ) + logger.info(f"✅ Updated endpoint: {endpoint_name}") + else: + raise + + # Return invocation URL + endpoint_url = f"{settings.databricks_host}/serving-endpoints/{endpoint_name}/invocations" + logger.info(f" Endpoint URL: {endpoint_url}") + + return endpoint_url + + def create_multi_agent_endpoint( + self, + endpoint_name: str, + agents: List[tuple], # [(agent_name, version, traffic_percentage)] + workload_size: str = "Medium" + ) -> str: + """ + Create endpoint serving multiple agents with traffic splitting. + + Args: + endpoint_name: Endpoint name + agents: List of (agent_name, version, traffic_percentage) tuples + workload_size: Endpoint size + + Returns: + Endpoint URL + """ + logger.info(f"Creating multi-agent endpoint: {endpoint_name}") + + # Build served entities + served_entities = [] + for agent_name, version, traffic_pct in agents: + full_model_name = f"{self.catalog}.{self.schema}.{agent_name}" + + served_entities.append( + ServedEntityInput( + entity_name=full_model_name, + entity_version=version, + workload_size=workload_size, + scale_to_zero_enabled=True + ) + ) + + # Create endpoint + endpoint = self.w.serving_endpoints.create_and_wait( + name=endpoint_name, + config=EndpointCoreConfigInput( + served_entities=served_entities + ) + ) + + logger.info(f"✅ Created multi-agent endpoint with {len(agents)} agents") + + return f"{settings.databricks_host}/serving-endpoints/{endpoint_name}/invocations" + + def test_endpoint(self, endpoint_name: str, test_input: dict) -> dict: + """ + Test a deployed endpoint. + + Args: + endpoint_name: Endpoint name + test_input: Test input data + + Returns: + Prediction result + """ + import requests + + url = f"{settings.databricks_host}/serving-endpoints/{endpoint_name}/invocations" + + headers = { + "Authorization": f"Bearer {settings.databricks_token}", + "Content-Type": "application/json" + } + + response = requests.post( + url, + headers=headers, + json={"dataframe_records": [test_input]} + ) + + response.raise_for_status() + return response.json() + + def get_endpoint_status(self, endpoint_name: str) -> dict: + """Get endpoint status and metrics.""" + endpoint = self.w.serving_endpoints.get(name=endpoint_name) + + return { + "name": endpoint.name, + "state": endpoint.state.config_update if endpoint.state else "Unknown", + "served_entities": [ + { + "name": entity.name, + "version": entity.entity_version, + "state": entity.state + } + for entity in (endpoint.config.served_entities or []) + ] + } + + def _get_requirements(self, agent_class) -> List[str]: + """Get pip requirements for an agent.""" + return [ + "mlflow>=2.10.0", + "databricks-agents>=0.1.0", + "langchain>=0.1.0", + "openai>=1.6.0", + "anthropic>=0.8.0", + "pydantic>=2.5.0", + "loguru>=0.7.0" + ] + + +def deploy_all_agents(): + """ + Deploy all agents to Databricks Model Serving. + + Usage: + python -m databricks.deployment + """ + from agents.mlflow_classifier import PolicyClassifierAgent + + manager = AgentDeploymentManager() + + # Register and deploy classifier + print("\n📦 Deploying Policy Classifier Agent...") + version = manager.register_agent( + agent_class=PolicyClassifierAgent, + agent_name="policy_classifier", + description="Classifies government meeting documents for oral health policy topics", + tags={"team": "advocacy", "domain": "oral_health"} + ) + + endpoint_url = manager.deploy_agent( + agent_name="policy_classifier", + endpoint_name="policy-classifier-prod", + version=version, + workload_size="Small", + scale_to_zero=True + ) + + print(f"\n✅ Deployment Complete!") + print(f" Endpoint: {endpoint_url}") + print(f"\n🧪 Test with:") + print(f""" + curl -X POST {endpoint_url} \\ + -H "Authorization: Bearer $DATABRICKS_TOKEN" \\ + -H "Content-Type: application/json" \\ + -d '{{"dataframe_records": [{{"document_id": "test", "title": "Meeting", "content": "Fluoride discussion..."}}]}}' + """) + + # Test endpoint + print("\n🧪 Testing endpoint...") + result = manager.test_endpoint( + endpoint_name="policy-classifier-prod", + test_input={ + "document_id": "test_001", + "title": "City Council Meeting", + "content": "Discussion on water fluoridation program" + } + ) + print(f" Result: {result}") + + +if __name__ == "__main__": + deploy_all_agents() diff --git a/databricks/evaluation.py b/databricks/evaluation.py new file mode 100644 index 0000000000000000000000000000000000000000..aa02fdb94ca730d535b35e96993910fa1e9b233a --- /dev/null +++ b/databricks/evaluation.py @@ -0,0 +1,343 @@ +""" +Agent Evaluation Framework for Databricks Agent Bricks. + +Provides: +- Automated evaluation of agent quality +- Ground truth comparison +- Metrics tracking (accuracy, latency, cost) +- A/B testing support +- Regression detection +""" +from typing import List, Dict, Any, Optional, Callable +import pandas as pd +import mlflow +from datetime import datetime +from dataclasses import dataclass +from loguru import logger + +from config import settings + + +@dataclass +class EvaluationMetrics: + """Container for evaluation metrics.""" + accuracy: float + precision: float + recall: float + f1_score: float + avg_latency_ms: float + avg_tokens: float + total_cost: float + error_rate: float + + +class AgentEvaluator: + """ + Evaluates agent performance using MLflow. + + Integrates with: + - MLflow Evaluate API + - Mosaic AI Agent Framework evaluation + - Unity Catalog for versioning + """ + + def __init__(self, agent_name: str): + """ + Initialize evaluator. + + Args: + agent_name: Name of agent to evaluate + """ + self.agent_name = agent_name + self.catalog = settings.catalog_name + self.schema = settings.schema_name + + def evaluate_agent( + self, + model_uri: str, + eval_data: pd.DataFrame, + evaluators: Optional[List[str]] = None, + custom_metrics: Optional[List[Callable]] = None + ) -> Dict[str, Any]: + """ + Evaluate an agent using MLflow. + + Args: + model_uri: URI of model to evaluate (e.g., "models:/main.agents.classifier/1") + eval_data: DataFrame with input data and ground truth + Must have columns: 'inputs', 'ground_truth' + evaluators: List of built-in evaluators ("default", "exact_match", etc.) + custom_metrics: Custom metric functions + + Returns: + Evaluation results + """ + logger.info(f"Evaluating {model_uri}") + + # Load model + model = mlflow.pyfunc.load_model(model_uri) + + # Default evaluators if none specified + if evaluators is None: + evaluators = ["default"] + + # Run evaluation + with mlflow.start_run(run_name=f"eval_{self.agent_name}") as run: + results = mlflow.evaluate( + model=model_uri, + data=eval_data, + targets="ground_truth", + model_type="text", + evaluators=evaluators, + extra_metrics=custom_metrics + ) + + # Log additional metrics + mlflow.log_param("eval_dataset_size", len(eval_data)) + mlflow.log_param("evaluators", ",".join(evaluators)) + + logger.info(f"Evaluation complete. Run ID: {run.info.run_id}") + + return { + "run_id": run.info.run_id, + "metrics": results.metrics, + "tables": results.tables + } + + def evaluate_classifier( + self, + model_uri: str, + test_documents: List[Dict[str, Any]], + ground_truth: List[str] + ) -> EvaluationMetrics: + """ + Evaluate a classifier agent. + + Args: + model_uri: Model URI + test_documents: List of test documents with 'document_id', 'title', 'content' + ground_truth: List of true labels + + Returns: + Evaluation metrics + """ + model = mlflow.pyfunc.load_model(model_uri) + + # Get predictions + predictions = [] + latencies = [] + + for doc in test_documents: + start = datetime.now() + result = model.predict(doc) + end = datetime.now() + + predictions.append(result.get("primary_topic")) + latencies.append((end - start).total_seconds() * 1000) + + # Calculate metrics + from sklearn.metrics import accuracy_score, precision_recall_fscore_support + + accuracy = accuracy_score(ground_truth, predictions) + precision, recall, f1, _ = precision_recall_fscore_support( + ground_truth, predictions, average='weighted', zero_division=0 + ) + + metrics = EvaluationMetrics( + accuracy=accuracy, + precision=precision, + recall=recall, + f1_score=f1, + avg_latency_ms=sum(latencies) / len(latencies), + avg_tokens=0, # Would track from LLM calls + total_cost=0, # Would calculate from token usage + error_rate=0 + ) + + # Log to MLflow + with mlflow.start_run(run_name=f"eval_classifier_{datetime.now().strftime('%Y%m%d_%H%M')}"): + mlflow.log_metrics({ + "accuracy": metrics.accuracy, + "precision": metrics.precision, + "recall": metrics.recall, + "f1_score": metrics.f1_score, + "avg_latency_ms": metrics.avg_latency_ms + }) + + # Log confusion matrix + from sklearn.metrics import confusion_matrix + import matplotlib.pyplot as plt + import seaborn as sns + + cm = confusion_matrix(ground_truth, predictions) + plt.figure(figsize=(10, 8)) + sns.heatmap(cm, annot=True, fmt='d', cmap='Blues') + plt.title('Classification Confusion Matrix') + plt.ylabel('True Label') + plt.xlabel('Predicted Label') + mlflow.log_figure(plt.gcf(), "confusion_matrix.png") + plt.close() + + return metrics + + def compare_versions( + self, + version_a: str, + version_b: str, + eval_data: pd.DataFrame + ) -> Dict[str, Any]: + """ + Compare two model versions (A/B testing). + + Args: + version_a: First version number + version_b: Second version number + eval_data: Evaluation dataset + + Returns: + Comparison results + """ + model_name = f"{self.catalog}.{self.schema}.{self.agent_name}" + + logger.info(f"Comparing {model_name} v{version_a} vs v{version_b}") + + # Evaluate version A + uri_a = f"models:/{model_name}/{version_a}" + results_a = self.evaluate_agent(uri_a, eval_data) + + # Evaluate version B + uri_b = f"models:/{model_name}/{version_b}" + results_b = self.evaluate_agent(uri_b, eval_data) + + # Compare metrics + comparison = { + "version_a": { + "version": version_a, + "metrics": results_a["metrics"] + }, + "version_b": { + "version": version_b, + "metrics": results_b["metrics"] + }, + "improvements": {} + } + + # Calculate improvements + for metric_name in results_a["metrics"]: + if metric_name in results_b["metrics"]: + a_val = results_a["metrics"][metric_name] + b_val = results_b["metrics"][metric_name] + + if isinstance(a_val, (int, float)) and isinstance(b_val, (int, float)): + improvement = ((b_val - a_val) / a_val * 100) if a_val != 0 else 0 + comparison["improvements"][metric_name] = { + "v{version_a}": a_val, + "v{version_b}": b_val, + "improvement_pct": improvement + } + + logger.info("Comparison complete") + return comparison + + def create_eval_dataset_from_feedback( + self, + feedback_table: str, + min_confidence: float = 0.8, + max_samples: int = 1000 + ) -> pd.DataFrame: + """ + Create evaluation dataset from user feedback in Delta Lake. + + Args: + feedback_table: Feedback table name + min_confidence: Minimum confidence for inclusion + max_samples: Maximum samples to include + + Returns: + Evaluation DataFrame + """ + from databricks.sdk import WorkspaceClient + + w = WorkspaceClient( + host=settings.databricks_host, + token=settings.databricks_token + ) + + # Query feedback table + query = f""" + SELECT + document_id, + input_data, + predicted_label, + user_corrected_label, + feedback_timestamp + FROM {self.catalog}.{self.schema}.{feedback_table} + WHERE user_corrected_label IS NOT NULL + AND feedback_confidence >= {min_confidence} + ORDER BY feedback_timestamp DESC + LIMIT {max_samples} + """ + + # This would execute via Databricks SQL + # For now, return placeholder + logger.info(f"Would query: {query}") + + return pd.DataFrame({ + "inputs": [], + "ground_truth": [] + }) + + +def run_evaluation_suite(): + """ + Run full evaluation suite for all agents. + + Usage: + python -m databricks.evaluation + """ + from agents.mlflow_classifier import PolicyClassifierAgent + + print("\n🧪 Running Agent Evaluation Suite\n") + + # Prepare test data + test_documents = [ + { + "document_id": "eval_001", + "title": "Water Quality Board Meeting", + "content": "Discussion of fluoride levels in municipal water supply. Motion to increase fluoridation to optimal levels." + }, + { + "document_id": "eval_002", + "title": "School Board Session", + "content": "Proposal for free dental screenings for all elementary students as part of school health program." + }, + { + "document_id": "eval_003", + "title": "Budget Committee", + "content": "Review of general fund allocations for the upcoming fiscal year. No health-related items." + } + ] + + ground_truth = [ + "water_fluoridation", + "school_dental_screening", + "not_oral_health_related" + ] + + # Evaluate classifier + print("📊 Evaluating Policy Classifier...") + evaluator = AgentEvaluator("policy_classifier") + + # Note: Would use actual model URI after registration + # metrics = evaluator.evaluate_classifier( + # model_uri="models:/main.agents.policy_classifier/1", + # test_documents=test_documents, + # ground_truth=ground_truth + # ) + + print("\n✅ Evaluation Suite Complete!") + print(" View results in MLflow UI: {}/ml/experiments".format(settings.databricks_host)) + + +if __name__ == "__main__": + run_evaluation_suite() diff --git a/databricks/notebooks/01_agent_bricks_quickstart.py b/databricks/notebooks/01_agent_bricks_quickstart.py new file mode 100644 index 0000000000000000000000000000000000000000..68d2f3b6965339cc0d8a3bc722beb65e795346a1 --- /dev/null +++ b/databricks/notebooks/01_agent_bricks_quickstart.py @@ -0,0 +1,329 @@ +# Databricks notebook source +# MAGIC %md +# MAGIC # Oral Health Policy Finder - Agent Bricks Quickstart +# MAGIC +# MAGIC This notebook demonstrates the Databricks Agent Bricks implementation of the Oral Health Policy Finder system. +# MAGIC +# MAGIC **Features:** +# MAGIC - MLflow-based agents with automatic tracing +# MAGIC - Unity Catalog governance +# MAGIC - Model Serving deployment +# MAGIC - Agent evaluation framework +# MAGIC - Delta Lake integration +# MAGIC +# MAGIC **Prerequisites:** +# MAGIC - Databricks Runtime 14.3 LTS ML or higher +# MAGIC - Unity Catalog enabled +# MAGIC - Model Serving permissions + +# COMMAND ---------- + +# MAGIC %md +# MAGIC ## 1. Setup and Configuration + +# COMMAND ---------- + +# Install dependencies +%pip install -q mlflow>=2.10.0 databricks-agents>=0.1.0 langchain>=0.1.0 openai>=1.6.0 + +# COMMAND ---------- + +# Configure MLflow +import mlflow +mlflow.set_registry_uri("databricks-uc") + +# Set Unity Catalog +CATALOG = "main" +SCHEMA = "agents" + +# Ensure catalog and schema exist +spark.sql(f"CREATE CATALOG IF NOT EXISTS {CATALOG}") +spark.sql(f"CREATE SCHEMA IF NOT EXISTS {CATALOG}.{SCHEMA}") + +print(f"✅ Using Unity Catalog: {CATALOG}.{SCHEMA}") + +# COMMAND ---------- + +# MAGIC %md +# MAGIC ## 2. Test Policy Classifier Agent Locally + +# COMMAND ---------- + +# Import agent +import sys +sys.path.append("/Workspace/Repos/your-repo/open-navigator") + +from agents.mlflow_classifier import PolicyClassifierAgent + +# Initialize agent +agent = PolicyClassifierAgent() + +# Test with sample document +test_input = { + "document_id": "test_001", + "title": "City Council Meeting - Water Infrastructure", + "content": """ + The city council voted 5-2 to approve fluoridation of the municipal water supply. + The program will begin next quarter with monitoring by the health department. + Expected to benefit approximately 50,000 residents. + """ +} + +# Get prediction +result = agent.predict(None, test_input) + +print("Classification Result:") +print(f" Topic: {result['primary_topic']}") +print(f" Confidence: {result['confidence']:.2%}") +print(f" Method: {result['method']}") +print(f" Reasoning: {result['reasoning']}") + +# COMMAND ---------- + +# MAGIC %md +# MAGIC ## 3. Register Agent to Unity Catalog + +# COMMAND ---------- + +from databricks.deployment import AgentDeploymentManager + +# Initialize deployment manager +manager = AgentDeploymentManager() + +# Register agent +version = manager.register_agent( + agent_class=PolicyClassifierAgent, + agent_name="policy_classifier", + description="Classifies government meeting documents for oral health policy topics", + tags={ + "team": "advocacy", + "domain": "oral_health", + "framework": "databricks-agent-bricks" + } +) + +print(f"✅ Registered policy_classifier version {version}") +print(f" Model: {CATALOG}.{SCHEMA}.policy_classifier") + +# COMMAND ---------- + +# MAGIC %md +# MAGIC ## 4. Deploy to Model Serving + +# COMMAND ---------- + +# Deploy agent to serving endpoint +endpoint_url = manager.deploy_agent( + agent_name="policy_classifier", + endpoint_name="policy-classifier-dev", + version=version, + workload_size="Small", + scale_to_zero=True +) + +print(f"✅ Deployed to Model Serving") +print(f" Endpoint: policy-classifier-dev") +print(f" URL: {endpoint_url}") + +# COMMAND ---------- + +# MAGIC %md +# MAGIC ## 5. Test Deployed Endpoint + +# COMMAND ---------- + +import requests +import os + +# Test endpoint +endpoint_name = "policy-classifier-dev" +databricks_host = dbutils.notebook.entry_point.getDbutils().notebook().getContext().apiUrl().get() +databricks_token = dbutils.notebook.entry_point.getDbutils().notebook().getContext().apiToken().get() + +url = f"{databricks_host}/serving-endpoints/{endpoint_name}/invocations" + +headers = { + "Authorization": f"Bearer {databricks_token}", + "Content-Type": "application/json" +} + +test_payload = { + "dataframe_records": [ + { + "document_id": "endpoint_test_001", + "title": "School Board Meeting", + "content": "Discussion of new dental screening program for elementary students" + } + ] +} + +response = requests.post(url, headers=headers, json=test_payload) +result = response.json() + +print("Endpoint Response:") +print(result) + +# COMMAND ---------- + +# MAGIC %md +# MAGIC ## 6. Evaluate Agent Performance + +# COMMAND ---------- + +from databricks.evaluation import AgentEvaluator +import pandas as pd + +# Create evaluation dataset +eval_data = pd.DataFrame([ + { + "document_id": "eval_001", + "title": "Water Board Meeting", + "content": "Approved fluoride addition to water supply", + "ground_truth": "water_fluoridation" + }, + { + "document_id": "eval_002", + "title": "School Board Session", + "content": "New dental screening program for students", + "ground_truth": "school_dental_screening" + }, + { + "document_id": "eval_003", + "title": "Budget Review", + "content": "General fund allocation discussion", + "ground_truth": "not_oral_health_related" + } +]) + +# Evaluate +evaluator = AgentEvaluator("policy_classifier") +metrics = evaluator.evaluate_classifier( + model_uri=f"models:/{CATALOG}.{SCHEMA}.policy_classifier/{version}", + test_documents=eval_data[["document_id", "title", "content"]].to_dict('records'), + ground_truth=eval_data["ground_truth"].tolist() +) + +print(f"\n📊 Evaluation Metrics:") +print(f" Accuracy: {metrics.accuracy:.2%}") +print(f" Precision: {metrics.precision:.2%}") +print(f" Recall: {metrics.recall:.2%}") +print(f" F1 Score: {metrics.f1_score:.2%}") +print(f" Avg Latency: {metrics.avg_latency_ms:.0f}ms") + +# COMMAND ---------- + +# MAGIC %md +# MAGIC ## 7. Query Results from Delta Lake + +# COMMAND ---------- + +# Create sample data in Delta Lake +spark.sql(f""" +CREATE TABLE IF NOT EXISTS {CATALOG}.{SCHEMA}.classified_documents ( + document_id STRING, + municipality STRING, + state STRING, + meeting_date TIMESTAMP, + primary_topic STRING, + confidence DOUBLE, + relevant_excerpts ARRAY, + classification_timestamp TIMESTAMP +) +USING DELTA +PARTITIONED BY (state) +""") + +# Query documents by topic +df = spark.sql(f""" +SELECT + state, + primary_topic, + COUNT(*) as document_count, + AVG(confidence) as avg_confidence +FROM {CATALOG}.{SCHEMA}.classified_documents +WHERE primary_topic != 'not_oral_health_related' +GROUP BY state, primary_topic +ORDER BY document_count DESC +""") + +display(df) + +# COMMAND ---------- + +# MAGIC %md +# MAGIC ## 8. Create Advocacy Heatmap + +# COMMAND ---------- + +# Query advocacy opportunities +opportunities = spark.sql(f""" +SELECT + state, + municipality, + primary_topic, + confidence, + relevant_excerpts +FROM {CATALOG}.{SCHEMA}.classified_documents +WHERE + primary_topic IN ('water_fluoridation', 'school_dental_screening', 'low_income_dental_funding') + AND confidence > 0.7 +ORDER BY confidence DESC +LIMIT 100 +""") + +# Convert to pandas for visualization +pdf = opportunities.toPandas() + +print(f"Found {len(pdf)} advocacy opportunities") +display(pdf.head(10)) + +# COMMAND ---------- + +# MAGIC %md +# MAGIC ## 9. Monitor Agent Performance + +# COMMAND ---------- + +# Get endpoint metrics +status = manager.get_endpoint_status("policy-classifier-dev") + +print(f"Endpoint Status:") +print(f" Name: {status['name']}") +print(f" State: {status['state']}") +print(f"\nServed Entities:") +for entity in status['served_entities']: + print(f" - {entity['name']} v{entity['version']}: {entity['state']}") + +# COMMAND ---------- + +# MAGIC %md +# MAGIC ## 10. A/B Test Model Versions + +# COMMAND ---------- + +# Compare two versions (if you have multiple) +# comparison = evaluator.compare_versions( +# version_a="1", +# version_b="2", +# eval_data=eval_data +# ) +# +# print("Version Comparison:") +# for metric, data in comparison["improvements"].items(): +# print(f" {metric}: {data['improvement_pct']:.1f}% improvement") + +# COMMAND ---------- + +# MAGIC %md +# MAGIC ## Next Steps +# MAGIC +# MAGIC 1. **Scale Up**: Process thousands of documents using Spark +# MAGIC 2. **Add Monitoring**: Set up alerts for model drift +# MAGIC 3. **Feedback Loop**: Collect user corrections in Delta Lake +# MAGIC 4. **Multi-Agent**: Deploy sentiment and advocacy writer agents +# MAGIC 5. **Production**: Promote to production endpoint with traffic splitting +# MAGIC +# MAGIC **Resources:** +# MAGIC - [Databricks Agent Framework Docs](https://docs.databricks.com/en/generative-ai/agent-framework/index.html) +# MAGIC - [MLflow Guide](https://mlflow.org/docs/latest/index.html) +# MAGIC - [Unity Catalog](https://docs.databricks.com/en/data-governance/unity-catalog/index.html) diff --git a/databricks/oral_health_schema.sql.deprecated b/databricks/oral_health_schema.sql.deprecated new file mode 100644 index 0000000000000000000000000000000000000000..3246da3fafdd0076d5d1b992c380502886b2d148 --- /dev/null +++ b/databricks/oral_health_schema.sql.deprecated @@ -0,0 +1,285 @@ +/* + * ER/Studio Data Architect SQL Code Generation + * Project : NOHDP_Oral_Health_Schema_Databricks_Std_Naming_Key_TypeApplied.DM1 + * + * Date Created : Wednesday, April 22, 2026 07:19:07 + * Target DBMS : Databricks + */ + +/* + * TABLE: dim_data_source + */ + +CREATE TABLE dim_data_source +( + source_key string NOT NULL, + data_steward_desc string, + data_steward_code string, + dataset_desc string, + dataset_code string, + collection_mode_type string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_data_source PRIMARY KEY (source_key) NOT ENFORCED +) +; + +/* + * TABLE: dim_date + */ + +CREATE TABLE dim_date +( + date_key int NOT NULL, + full_date date NOT NULL, + day_of_month int, + day_of_week int, + day_of_week_name string, + is_weekend boolean, + week_of_year int, + iso_week string, + month_number int, + month_name string, + month_abbr string, + year_month int, + year_month_name string, + quarter_number int, + quarter_name string, + year int, + fiscal_year int, + fiscal_quarter int, + fiscal_month int, + is_holiday boolean DEFAULT FALSE, + holiday_name string, + is_pilot_period boolean DEFAULT FALSE, + is_baseline_period boolean DEFAULT FALSE, + CONSTRAINT dim_date_pk PRIMARY KEY (date_key) +) +TBLPROPERTIES('delta.feature.allowColumnDefaults' = 'supported') +; + +/* + * TABLE: dim_geography + */ + +CREATE TABLE dim_geography +( + geography_key string NOT NULL, + geo_type string, + fips_code string, + geo_name_desc string, + county_name_desc string, + record_start_dttm timestamp, + record_end_dttm timestamp, + current_record_ind smallint, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_geography PRIMARY KEY (geography_key) NOT ENFORCED +) +; + +/* + * TABLE: dim_measure + */ + +CREATE TABLE dim_measure +( + measure_key string NOT NULL, + source_key string, + measure_code string, + measure_desc string, + measure_long_desc string, + measure_category_type string, + measure_level_type string, + measure_tooltip_desc string, + base_unit_desc string, + unit_prefix_code string, + unit_suffix_code string, + nohss_indicator_nbr string, + nohss_indicator_group_type string, + nohss_indicator_desc string, + dashboard_trend_ind boolean, + dashboard_cross_ind boolean, + record_start_dttm timestamp, + record_end_dttm timestamp, + current_record_ind smallint, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_measure PRIMARY KEY (measure_key) NOT ENFORCED +) +; + +/* + * TABLE: dim_postal + */ + +CREATE TABLE dim_postal +( + postal_key string NOT NULL, + postal_code string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_postal PRIMARY KEY (postal_key) NOT ENFORCED +) +; + +/* + * TABLE: dim_state + */ + +CREATE TABLE dim_state +( + state_key string NOT NULL, + state_fips_nbr int, + state_name_desc string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_state PRIMARY KEY (state_key) NOT ENFORCED +) +; + +/* + * TABLE: dim_statistic_type + */ + +CREATE TABLE dim_statistic_type +( + statistic_key string NOT NULL, + statistic_type string, + calculation_method_desc string, + adjustment_desc string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_statistic_type PRIMARY KEY (statistic_key) NOT ENFORCED +) +; + +/* + * TABLE: dim_stratification + */ + +CREATE TABLE dim_stratification +( + stratification_key string NOT NULL, + stratification_category_type string, + stratification_level_desc string, + stratification_group_type string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_stratification PRIMARY KEY (stratification_key) NOT ENFORCED +) +; + +/* + * TABLE: dim_survey_period + */ + +CREATE TABLE dim_survey_period +( + survey_period_key string NOT NULL, + date_type string, + year_nbr int, + year_start_nbr int, + year_end_nbr int, + approx_date date, + duration_desc string, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_dim_survey_period PRIMARY KEY (survey_period_key) NOT ENFORCED +) +; + +/* + * TABLE: fact_oral_health_observation + */ + +CREATE TABLE fact_oral_health_observation +( + observation_key string NOT NULL, + measure_key string, + geography_key string, + stratification_key string, + statistic_key string, + postal_key string NOT NULL, + state_key string NOT NULL, + survey_period_key string NOT NULL, + date_key int NOT NULL, + population_desc string, + value_nbr decimal(18, 6), + ci_present_ind boolean, + ci_lower_nbr decimal(18, 6), + ci_upper_nbr decimal(18, 6), + proportion_nbr decimal(18, 6), + prop_lower_ci_nbr decimal(18, 6), + prop_upper_ci_nbr decimal(18, 6), + cell_size_unweighted_nbr int, + direction_desc string, + source_row_id_nbr bigint, + record_created_dttm timestamp, + record_last_modified_dttm timestamp, + load_run_id bigint, + CONSTRAINT pk_fact_oral_health PRIMARY KEY (observation_key) NOT ENFORCED +) +; + +/* + * TABLE: dim_measure + */ + +ALTER TABLE dim_measure ADD CONSTRAINT fk_measure_source + FOREIGN KEY (source_key) + REFERENCES dim_data_source NOT ENFORCED +; + + +/* + * TABLE: fact_oral_health_observation + */ + +ALTER TABLE fact_oral_health_observation ADD CONSTRAINT Refdim_postal11 + FOREIGN KEY (postal_key) + REFERENCES dim_postal +; + +ALTER TABLE fact_oral_health_observation ADD CONSTRAINT Refdim_state12 + FOREIGN KEY (state_key) + REFERENCES dim_state +; + +ALTER TABLE fact_oral_health_observation ADD CONSTRAINT Refdim_survey_period14 + FOREIGN KEY (survey_period_key) + REFERENCES dim_survey_period +; + +ALTER TABLE fact_oral_health_observation ADD CONSTRAINT Refdim_date15 + FOREIGN KEY (date_key) + REFERENCES dim_date +; + +ALTER TABLE fact_oral_health_observation ADD CONSTRAINT fk_fact_geography + FOREIGN KEY (geography_key) + REFERENCES dim_geography NOT ENFORCED +; + +ALTER TABLE fact_oral_health_observation ADD CONSTRAINT fk_fact_measure + FOREIGN KEY (measure_key) + REFERENCES dim_measure NOT ENFORCED +; + +ALTER TABLE fact_oral_health_observation ADD CONSTRAINT fk_fact_statistic + FOREIGN KEY (statistic_key) + REFERENCES dim_statistic_type NOT ENFORCED +; + +ALTER TABLE fact_oral_health_observation ADD CONSTRAINT fk_fact_stratification + FOREIGN KEY (stratification_key) + REFERENCES dim_stratification NOT ENFORCED +; diff --git a/discovery/README_IRS_BMF.md b/discovery/README_IRS_BMF.md new file mode 100644 index 0000000000000000000000000000000000000000..33a09520e5e99c0d9120dbee07d28b831a608073 --- /dev/null +++ b/discovery/README_IRS_BMF.md @@ -0,0 +1,83 @@ +# IRS EO-BMF Ingestion Module + +Download and process **all 1.9M+ U.S. nonprofits** from the IRS Exempt Organizations Business Master File. + +## Quick Start + +```python +from discovery.irs_bmf_ingestion import IRSBMFIngestion + +# Initialize +irs = IRSBMFIngestion() + +# Download ALL U.S. nonprofits (1.9M+) +df = irs.download_all_regions() +# Result: 1,952,238 organizations in ~30 seconds + +# Or download specific state +df_alabama = irs.download_state_file("AL") +# Result: 26,148 Alabama nonprofits + +# Filter by NTEE code +health_orgs = irs.filter_by_ntee(df, ["E"]) +# Result: ~80,000 health organizations + +# Convert to ProPublica-compatible format +standardized = irs.standardize_to_propublica_format(df) +``` + +## Command Line Usage + +```bash +# Download ALL nonprofits +python scripts/create_all_gold_tables.py \ + --nonprofits-only \ + --use-irs \ + --download-all-irs + +# Download specific states +python scripts/create_all_gold_tables.py \ + --nonprofits-only \ + --states AL GA FL \ + --use-irs + +# Filter by NTEE codes +python scripts/create_all_gold_tables.py \ + --nonprofits-only \ + --states AL \ + --ntee-codes E P \ + --use-irs +``` + +## Data Source + +- **URL**: https://www.irs.gov/charities-non-profits/exempt-organizations-business-master-file-extract-eo-bmf +- **Format**: CSV (converted to Parquet for caching) +- **Records**: 1,952,238 organizations (April 2026) +- **Update Frequency**: Monthly +- **License**: Public domain + +## Features + +✅ **Complete coverage** - All 1.9M+ U.S. tax-exempt organizations +✅ **Fast download** - 4 regional files in ~30 seconds +✅ **Automatic caching** - Parquet format for instant reloading +✅ **NTEE filtering** - Filter by nonprofit type +✅ **State filtering** - Download specific states +✅ **ProPublica compatibility** - Seamless integration with existing pipeline + +## Comparison: IRS vs ProPublica API + +| Metric | ProPublica API | IRS EO-BMF | +|--------|----------------|------------| +| **Alabama nonprofits** | 25 | 26,148 | +| **Total available** | 3M+ (paginated) | 1,952,238 | +| **Results per request** | 25 max | All | +| **Download speed** | Slow (API) | Fast (bulk) | +| **Pagination** | ❌ Not available | ✅ Complete dataset | + +**IRS provides 1,000x more data per request!** + +## Full Documentation + +See [website/docs/data-sources/irs-bulk-data.md](../../website/docs/data-sources/irs-bulk-data.md) for complete documentation. diff --git a/discovery/README_NONPROFIT_DISCOVERY.md b/discovery/README_NONPROFIT_DISCOVERY.md new file mode 100644 index 0000000000000000000000000000000000000000..9a0120c69d39a5b85b05bbf35b94ddf06378bc6f --- /dev/null +++ b/discovery/README_NONPROFIT_DISCOVERY.md @@ -0,0 +1,439 @@ +# Nonprofit Discovery Module + +Automated discovery and enrichment of nonprofits and churches using **100% FREE** open data APIs. + +## Why This Matters + +When government says "no" to a policy (e.g., "We can't do dental screenings - legal risk"), you can instantly show citizens the nonprofits **already doing it**. This: + +1. **Bypasses the technocratic veto** - Shows direct alternatives +2. **Creates social pressure** - Exposes inefficiency ("$5K legal review vs $25 screening") +3. **Mobilizes citizens** - Provides volunteer/donation pathways + +## Data Sources (All Free) + +### 1. ProPublica Nonprofit Explorer API ⭐ PRIMARY SOURCE + +**What it provides:** +- Financial data (revenue, expenses, assets) from IRS Form 990 +- NTEE codes (standardized classification) +- EIN (tax ID) for verification +- 3+ million organizations, 10+ years of data + +**Coverage:** All nonprofits with >$50K revenue or >$250K assets + +**API Docs:** https://projects.propublica.org/nonprofits/api + +**Example Usage:** +```python +from discovery.nonprofit_discovery import NonprofitDiscovery + +discovery = NonprofitDiscovery() + +# Search all health organizations in Tuscaloosa +health_orgs = discovery.search_propublica( + state="AL", + city="Tuscaloosa", + ntee_code="E" # E = Health +) + +# Get detailed financials for specific org +details = discovery.get_propublica_org_details("63-0123456") +print(f"Revenue: ${details['filings'][0]['total_revenue']:,}") +``` + +**Rate Limits:** Free, unlimited. Be respectful: ~1 request/second suggested. + +--- + +### 2. IRS Tax-Exempt Organization Search (TEOS) + +**What it provides:** +- Official tax-exempt status +- Pub 78 verification (deductibility) +- Bulk download of all U.S. nonprofits + +**Source:** https://www.irs.gov/charities-non-profits/tax-exempt-organization-search-bulk-data-downloads + +**Note:** ProPublica API already includes this data, so direct IRS access only needed for bulk downloads. + +--- + +### 3. Every.org Charity API + +**What it provides:** +- Human-readable mission statements +- Organization logos and images +- Cause categories +- Cleaner data than raw IRS filings + +**API Docs:** https://www.every.org/nonprofit-api + +**Note:** May require API key for full access. Free tier available. + +**Example Usage:** +```python +# Search by location and cause +orgs = discovery.search_everyorg( + location="Tuscaloosa, AL", + causes=["health", "education", "youth"] +) +``` + +--- + +### 4. Local Service Directories (Manual Enrichment) + +**Findhelp.org (Aunt Bertha):** +- Most comprehensive directory of local social services +- Includes specific services, hours, eligibility +- Search: https://www.findhelp.org/search?query=dental&location=Tuscaloosa,%20AL +- API access varies (request from Findhelp.org) + +**211 Alabama:** +- Regional social services directory +- More detailed than IRS data (days/hours, languages, insurance) +- Search: https://www.211connects.org + +**Strategy:** Use ProPublica for financial backbone, then manually enrich with Findhelp/211 for specific service details. + +--- + +## NTEE Code Classification + +NTEE = **National Taxonomy of Exempt Entities** (IRS classification system) + +### Key Codes for Oral Health Policy + +| Code | Category | Description | Example Orgs | +|------|----------|-------------|--------------| +| **E** | Health | General and rehabilitative health | Community health centers | +| **E20** | Hospitals | Primary medical care facilities | County hospitals | +| **E32** | School Health | School-based health care | Mobile dental clinics in schools | +| **E40** | Health General | Community clinics | Free clinics | +| **E80** | Health Other | Health N.E.C. | Health advocacy groups | +| **F** | Mental Health | Crisis intervention | Counseling centers | +| **K** | Food/Nutrition | Food, agriculture, nutrition | Food banks | +| **K30** | Food Service | Free food distribution | School meal programs | +| **K34** | Congregate Meals | Community dining programs | Senior nutrition sites | +| **N** | Recreation | Sports, leisure, athletics | Community rec centers | +| **O** | Youth Dev | Youth development programs | After-school programs | +| **O50** | Youth Other | Youth development N.E.C. | Mentoring programs | +| **P** | Human Services | Multipurpose human services | Family support centers | +| **X** | Religion | Religious organizations | Churches, synagogues | +| **X20** | Christian | Christian orgs | Church health ministries | +| **W** | Public Benefit | Society benefit programs | Water advocacy groups | + +### NTEE Hierarchy + +``` +E (Health) +├── E20 (Hospitals) +├── E30 (Ambulatory Health) +│ └── E32 (School-Based Health) ⭐ Mobile dental units +├── E40 (Reproductive Health) +└── E80 (Health N.E.C.) + +X (Religion) +├── X20 (Christian) ⭐ Church health ministries +├── X30 (Jewish) +└── X40 (Islamic) +``` + +## Quick Start + +### 1. Discover All Tuscaloosa Nonprofits + +```bash +source .venv/bin/activate +python scripts/discover_tuscaloosa_nonprofits.py +``` + +**Output:** `frontend/policy-dashboards/src/data/tuscaloosa_nonprofits.json` + +### 2. Search Specific NTEE Codes + +```python +from discovery.nonprofit_discovery import NonprofitDiscovery + +discovery = NonprofitDiscovery() + +# Just dental/school health +dental = discovery.search_propublica( + state="AL", + city="Tuscaloosa", + ntee_code="E32" +) + +# Churches with health ministries +churches = discovery.search_propublica( + state="AL", + city="Tuscaloosa", + ntee_code="X20" +) + +# Food/nutrition programs +food = discovery.search_propublica( + state="AL", + city="Tuscaloosa", + ntee_code="K" +) + +# Merge and export +all_orgs = discovery.merge_nonprofit_data(dental, churches) +all_orgs.extend(food) +discovery.export_to_frontend(all_orgs) +``` + +### 3. Get Detailed Financials + +```python +# Get 5 years of 990 data for a specific org +details = discovery.get_propublica_org_details("63-0123456") + +print(f"Organization: {details['name']}") +print(f"NTEE: {details['ntee_code']} - {details['ntee_description']}") + +print("\nRecent Filings:") +for filing in details['filings']: + revenue = filing['total_revenue'] + expenses = filing['total_expenses'] + year = filing['tax_period'] + print(f" {year}: ${revenue:,} revenue, ${expenses:,} expenses") +``` + +## Data Model + +### Nonprofit Record (Frontend Format) + +```json +{ + "name": "Tuscaloosa County Interfaith Dental Initiative", + "ein": "63-0345678", + "ntee_code": "E32", + "ntee_description": "School-Based Health Care", + "mission": "Multi-faith collaboration providing free dental care", + "services": [ + "Mobile dental unit serving Title I schools", + "Free toothbrush and fluoride programs", + "Parent education workshops" + ], + "annual_budget": 125000, + "students_served": 2400, + "families_served": 0, + "youth_served": 0, + "contact": { + "website": "https://tuscaloosainterfaithdental.org", + "email": "contact@tuscaloosainterfaithdental.org", + "phone": "(205) 555-0300" + }, + "logo_url": "https://...", + "volunteer_opportunities": true, + "accepting_board_members": true +} +``` + +### ProPublica API Response + +```json +{ + "organizations": [ + { + "ein": "630345678", + "name": "TUSCALOOSA COUNTY INTERFAITH DENTAL INITIATIVE", + "city": "TUSCALOOSA", + "state": "AL", + "ntee_code": "E32", + "revenue_amount": 125000, + "asset_amount": 45000, + "income_amount": 125000 + } + ] +} +``` + +## Architecture + +### Discovery Pipeline + +``` +1. Search ProPublica API + ↓ (by state, city, NTEE code) +2. Get Financial Data + ↓ (revenue, expenses, assets) +3. Enrich with Every.org + ↓ (mission, logo, causes) +4. Match to Government Decisions + ↓ (by NTEE code) +5. Export to Frontend + ↓ +frontend/policy-dashboards/src/data/tuscaloosa_nonprofits.json +``` + +### Caching Strategy + +All API responses are cached in `data/cache/nonprofits/`: + +``` +data/cache/nonprofits/ +├── propublica_AL_E_Tuscaloosa.json +├── propublica_AL_E32_Tuscaloosa.json +├── propublica_org_63-0345678.json +└── everyorg_Tuscaloosa_AL_health-education.json +``` + +**Benefits:** +- Faster subsequent runs (no API calls) +- Respectful to free APIs (no repeated requests) +- Offline development possible +- Manual review/editing of cached data + +**Cache Invalidation:** +- Delete cache files to force fresh download +- Recommended refresh: Monthly (990 data updates annually) + +## Cost Comparison + +### Paid Services + +| Service | Cost | Coverage | +|---------|------|----------| +| **Candid/GuideStar Premium** | $500-2,000/month | Deep services data | +| **Charity Navigator API** | $500+/month | Ratings + financials | +| **GiveWell Data** | Free (limited) | Top charities only | + +### Our Free Stack + +| Service | Cost | Coverage | +|---------|------|----------| +| **ProPublica API** | $0 | 1.8M orgs, 10+ years | +| **IRS TEOS** | $0 | All U.S. nonprofits | +| **Every.org API** | $0 (basic) | Mission + logos | +| **Total** | **$0/month** | 95% of paid features | + +**What You Give Up:** +- Real-time "services provided" updates (need manual enrichment) +- Phone numbers/emails (need scraping or manual entry) +- Volunteer opportunities feed (need manual verification) + +**What You Keep:** +- All financial data (revenue, expenses, assets) +- NTEE classification (interoperable with paid services) +- Mission statements and descriptions +- Scalability to all 50 states + +## Advanced Usage + +### Bulk Download for All Alabama + +```python +# Get ALL health nonprofits in Alabama +alabama_health = [] + +for city in ["Birmingham", "Montgomery", "Mobile", "Tuscaloosa", "Huntsville"]: + orgs = discovery.search_propublica( + state="AL", + city=city, + ntee_code="E" + ) + alabama_health.extend(orgs) + time.sleep(1) # Rate limiting + +print(f"Found {len(alabama_health)} health nonprofits in Alabama") +``` + +### Find Nonprofits by Revenue + +```python +# Find large health orgs (>$1M revenue) +large_orgs = [ + org for org in nonprofits + if (org.get('revenue_amount') or 0) > 1000000 +] + +print(f"Large organizations: {len(large_orgs)}") +for org in sorted(large_orgs, key=lambda x: x['revenue_amount'], reverse=True)[:10]: + print(f" {org['name']}: ${org['revenue_amount']:,}") +``` + +### Match to Government Decisions + +```python +# Load government decisions with NTEE codes +with open('frontend/policy-dashboards/src/data/tuscaloosa_policies.json') as f: + decisions = json.load(f) + +# Find nonprofits for each deferred decision +for decision in decisions: + if decision.get('outcome') in ['Tabled', 'Deferred']: + ntee = decision.get('ntee_code') + + # Find matching nonprofits + matches = [ + org for org in nonprofits + if org['ntee_code'] == ntee or + org['ntee_code'].startswith(ntee[0]) + ] + + if matches: + print(f"\nDecision: {decision['decision_summary']}") + print(f"Government said NO, but {len(matches)} nonprofits are doing it:") + for org in matches[:3]: + revenue = org.get('revenue_amount', 0) + print(f" • {org['name']}: ${revenue:,}/year") +``` + +## Troubleshooting + +### ProPublica API Returns Empty Results + +**Possible causes:** +- City name spelling (try "Tuscaloosa" vs "TUSCALOOSA") +- NTEE code doesn't exist in that location +- No nonprofits in that category + +**Solutions:** +```python +# Try broader search (remove city filter) +orgs = discovery.search_propublica(state="AL", ntee_code="E32") + +# Try major category only (E vs E32) +orgs = discovery.search_propublica(state="AL", city="Tuscaloosa", ntee_code="E") +``` + +### Every.org API Requires Authentication + +**Solution:** Every.org is optional. ProPublica provides 90% of needed data. + +```python +# Skip Every.org if auth fails +try: + everyorg_orgs = discovery.search_everyorg(...) +except: + everyorg_orgs = [] # Continue with ProPublica data only +``` + +### Rate Limiting + +**Built-in protection:** Module automatically spaces requests 1 second apart. + +If you hit rate limits: +```python +discovery.min_request_interval = 2.0 # Increase to 2 seconds +``` + +## Next Steps + +1. **Run discovery:** `python scripts/discover_tuscaloosa_nonprofits.py` +2. **Review output:** Check `frontend/policy-dashboards/src/data/tuscaloosa_nonprofits.json` +3. **Manual enrichment:** Add phone/email from Findhelp.org or 211 +4. **Verify services:** Cross-check "services provided" with org websites +5. **Launch frontend:** `cd frontend/policy-dashboards && npm start` + +## Resources + +- **ProPublica Nonprofit Explorer:** https://projects.propublica.org/nonprofits/ +- **IRS Tax-Exempt Org Search:** https://www.irs.gov/charities-non-profits/tax-exempt-organization-search +- **NTEE Code Lookup:** https://nccs.urban.org/publication/irs-activity-codes +- **Findhelp.org:** https://www.findhelp.org +- **211 Directory:** https://www.211.org diff --git a/discovery/__init__.py b/discovery/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6f4ef3250b463a957d6c3521629a81f27bcea15f --- /dev/null +++ b/discovery/__init__.py @@ -0,0 +1,11 @@ +""" +Jurisdiction Discovery Module + +Identifies and tracks local government jurisdictions across the United States +for oral health policy monitoring. + +Data Sources: +- Census Bureau Government Integrated Directory (GID) +- CISA .gov Domain Master List (cisagov/dotgov-data) +- NCES Common Core of Data (school districts) +""" diff --git a/discovery/batch_processor.py b/discovery/batch_processor.py new file mode 100644 index 0000000000000000000000000000000000000000..a656cb36b038b18122aa00d55448c667f3704bd6 --- /dev/null +++ b/discovery/batch_processor.py @@ -0,0 +1,544 @@ +""" +Batch processing and quality metrics for large-scale jurisdiction scraping. + +Based on LocalView patterns for handling thousands of jurisdictions +with quality tracking and failure management. +""" +from typing import Dict, List, Optional, Iterator +from dataclasses import dataclass, asdict +from datetime import datetime, timedelta +from enum import Enum +import json + +from pyspark.sql import SparkSession, DataFrame +from pyspark.sql.functions import col, count, sum as spark_sum, avg, max as spark_max +from loguru import logger + +from config.settings import settings + + +class ScrapeStatus(Enum): + """Status of scraping operation.""" + PENDING = "pending" + IN_PROGRESS = "in_progress" + SUCCESS = "success" + PARTIAL = "partial" # Some data retrieved + FAILED = "failed" + SKIPPED = "skipped" + + +class HealthStatus(Enum): + """Health status of a jurisdiction scraper.""" + HEALTHY = "healthy" # No recent failures + DEGRADED = "degraded" # Some failures + FAILED = "failed" # Multiple consecutive failures + UNKNOWN = "unknown" # Never scraped + + +@dataclass +class JurisdictionQuality: + """ + LocalView pattern: Track data quality and completeness per jurisdiction. + """ + # Identification + jurisdiction_name: str + state_code: str + fips_code: Optional[str] + url: str + platform: Optional[str] + + # Completeness metrics + total_meetings_expected: int # Based on typical schedule + total_meetings_found: int + meetings_with_agendas: int + meetings_with_minutes: int + meetings_with_videos: int + meetings_with_transcripts: int + + # Freshness + last_scraped: Optional[datetime] + last_meeting_found: Optional[datetime] + scraping_frequency: str # 'daily', 'weekly', 'monthly' + + # Reliability + consecutive_successes: int + consecutive_failures: int + total_scrapes: int + successful_scrapes: int + last_success: Optional[datetime] + last_error: Optional[str] + + # Quality scores + completeness_score: float # 0-100 + reliability_score: float # 0-100 + freshness_score: float # 0-100 + overall_quality: float # 0-100 (weighted average) + health_status: str # healthy, degraded, failed + + # Timestamps + created_at: datetime + updated_at: datetime + + @classmethod + def from_dict(cls, data: dict) -> 'JurisdictionQuality': + """Create from dictionary with datetime parsing.""" + # Parse datetime fields + for field in ['last_scraped', 'last_meeting_found', 'last_success', 'created_at', 'updated_at']: + if data.get(field) and isinstance(data[field], str): + data[field] = datetime.fromisoformat(data[field]) + return cls(**data) + + def to_dict(self) -> dict: + """Convert to dictionary with datetime serialization.""" + data = asdict(self) + # Serialize datetime fields + for field in ['last_scraped', 'last_meeting_found', 'last_success', 'created_at', 'updated_at']: + if data.get(field): + data[field] = data[field].isoformat() + return data + + +@dataclass +class BatchResult: + """Result of processing a batch of jurisdictions.""" + batch_number: int + batch_size: int + jurisdictions_processed: int + jurisdictions_succeeded: int + jurisdictions_failed: int + meetings_found: int + agendas_found: int + minutes_found: int + errors: List[dict] + start_time: datetime + end_time: Optional[datetime] = None + duration_seconds: float = 0.0 + + @property + def success_rate(self) -> float: + """Percentage of jurisdictions successfully scraped.""" + if self.jurisdictions_processed == 0: + return 0.0 + return (self.jurisdictions_succeeded / self.jurisdictions_processed) * 100 + + +class BatchProcessor: + """ + LocalView pattern: Process large numbers of jurisdictions in batches. + + Features: + - Batch processing with configurable size + - Quality metrics per jurisdiction + - Failure tracking and retry logic + - Progress monitoring + - Resume from interruption + + Example: + >>> processor = BatchProcessor(batch_size=100) + >>> for batch_result in processor.process_all_jurisdictions(): + ... print(f"Batch {batch_result.batch_number}: " + ... f"{batch_result.success_rate:.1f}% success") + """ + + def __init__( + self, + spark: Optional[SparkSession] = None, + batch_size: int = 100, + max_failures: int = 3, + retry_delay_hours: int = 24 + ): + """ + Initialize batch processor. + + Args: + spark: SparkSession (creates new if None) + batch_size: Number of jurisdictions per batch + max_failures: Max consecutive failures before marking as failed + retry_delay_hours: Hours to wait before retrying failed jurisdictions + """ + self.spark = spark or self._create_spark_session() + self.batch_size = batch_size + self.max_failures = max_failures + self.retry_delay_hours = retry_delay_hours + + self.quality_metrics_path = f"{settings.delta_lake_path}/quality/jurisdiction_metrics" + self.batch_results_path = f"{settings.delta_lake_path}/quality/batch_results" + + def _create_spark_session(self) -> SparkSession: + """Create SparkSession if not provided.""" + from delta import configure_spark_with_delta_pip + + builder = SparkSession.builder \ + .appName("BatchProcessor") \ + .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \ + .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog") + + return configure_spark_with_delta_pip(builder).getOrCreate() + + def process_all_jurisdictions( + self, + priority_filter: str = "high", + resume_from_batch: Optional[int] = None + ) -> Iterator[BatchResult]: + """ + Process all jurisdictions in batches. + + Args: + priority_filter: Priority tier to process ('high', 'medium', 'low', 'all') + resume_from_batch: Resume from specific batch number (for interruptions) + + Yields: + BatchResult for each processed batch + """ + logger.info(f"Starting batch processing (batch_size={self.batch_size})") + + # Load targets from Gold layer + targets_df = self.spark.read.format("delta").load( + f"{settings.delta_lake_path}/gold/scraping_targets" + ) + + # Filter by priority + if priority_filter != "all": + targets_df = targets_df.filter(col("priority_tier") == priority_filter) + + # Filter out recently failed jurisdictions + quality_df = self._load_quality_metrics() + if quality_df is not None: + # Skip jurisdictions that failed recently and are within retry delay + retry_cutoff = datetime.utcnow() - timedelta(hours=self.retry_delay_hours) + retry_cutoff_str = retry_cutoff.isoformat() + + # Join with quality metrics and filter + targets_df = targets_df.join( + quality_df.select("url", "consecutive_failures", "last_scraped", "health_status"), + on="url", + how="left" + ).filter( + (col("consecutive_failures").isNull()) | # Never scraped + (col("consecutive_failures") < self.max_failures) | # Not max failures + (col("last_scraped") < retry_cutoff_str) # Retry delay passed + ) + + # Order by priority score + targets_df = targets_df.orderBy(col("priority_score").desc()) + + total_targets = targets_df.count() + logger.info(f"Processing {total_targets} jurisdictions") + + # Calculate starting batch + start_batch = resume_from_batch or 0 + + # Process in batches + for batch_num in range(start_batch, (total_targets // self.batch_size) + 1): + offset = batch_num * self.batch_size + + # Get batch + batch_df = targets_df.offset(offset).limit(self.batch_size) + batch_data = batch_df.collect() + + if not batch_data: + break + + logger.info(f"Processing batch {batch_num + 1} ({len(batch_data)} jurisdictions)") + + # Process batch + batch_result = self._process_batch(batch_num + 1, batch_data) + + # Save batch result + self._save_batch_result(batch_result) + + # Update quality metrics + # (In real implementation, this would be called after actual scraping) + + yield batch_result + + def _process_batch(self, batch_num: int, batch_data: List) -> BatchResult: + """ + Process a single batch of jurisdictions. + + Note: This is a skeleton. Actual scraping logic would go here. + """ + result = BatchResult( + batch_number=batch_num, + batch_size=len(batch_data), + jurisdictions_processed=0, + jurisdictions_succeeded=0, + jurisdictions_failed=0, + meetings_found=0, + agendas_found=0, + minutes_found=0, + errors=[], + start_time=datetime.utcnow() + ) + + for row in batch_data: + jurisdiction = row['jurisdiction_name'] + url = row['url'] + platform = row.get('platform') + + try: + # TODO: Replace with actual scraping logic + # For now, simulate scraping + logger.info(f"Processing {jurisdiction}: {url}") + + # Placeholder: Would call appropriate scraper here + # meetings = scrape_jurisdiction(url, platform) + + # Simulate success + result.jurisdictions_processed += 1 + result.jurisdictions_succeeded += 1 + result.meetings_found += 5 # Placeholder + result.agendas_found += 5 + result.minutes_found += 3 + + except Exception as e: + logger.error(f"Error processing {jurisdiction}: {e}") + result.jurisdictions_processed += 1 + result.jurisdictions_failed += 1 + result.errors.append({ + 'jurisdiction': jurisdiction, + 'url': url, + 'error': str(e) + }) + + result.end_time = datetime.utcnow() + result.duration_seconds = (result.end_time - result.start_time).total_seconds() + + return result + + def calculate_quality_metrics(self, jurisdiction_url: str) -> JurisdictionQuality: + """ + Calculate quality metrics for a jurisdiction. + + Args: + jurisdiction_url: URL of the jurisdiction + + Returns: + JurisdictionQuality object with all scores + """ + # Load existing metrics + existing = self._get_existing_metrics(jurisdiction_url) + + # Load scraped data for this jurisdiction + # (In production, query from silver/gold layers) + + # For now, create placeholder metrics + now = datetime.utcnow() + + # Calculate completeness score + if existing: + total_expected = existing.total_meetings_expected or 12 # Assume monthly meetings + total_found = existing.total_meetings_found or 0 + with_agendas = existing.meetings_with_agendas or 0 + with_minutes = existing.meetings_with_minutes or 0 + + found_rate = min(total_found / total_expected, 1.0) if total_expected > 0 else 0 + agenda_rate = with_agendas / total_found if total_found > 0 else 0 + minutes_rate = with_minutes / total_found if total_found > 0 else 0 + + completeness_score = ( + found_rate * 40 + # 40%: Finding meetings + agenda_rate * 30 + # 30%: Having agendas + minutes_rate * 30 # 30%: Having minutes + ) + else: + completeness_score = 0.0 + + # Calculate reliability score + if existing: + total_scrapes = existing.total_scrapes or 0 + successful = existing.successful_scrapes or 0 + reliability_score = (successful / total_scrapes * 100) if total_scrapes > 0 else 0 + else: + reliability_score = 0.0 + + # Calculate freshness score + if existing and existing.last_scraped: + days_since = (now - existing.last_scraped).days + if days_since <= 1: + freshness_score = 100 + elif days_since <= 7: + freshness_score = 80 + elif days_since <= 30: + freshness_score = 60 + else: + freshness_score = 40 + else: + freshness_score = 0.0 + + # Overall quality (weighted average) + overall_quality = ( + completeness_score * 0.5 + + reliability_score * 0.3 + + freshness_score * 0.2 + ) + + # Determine health status + consecutive_failures = existing.consecutive_failures if existing else 0 + if consecutive_failures >= self.max_failures: + health_status = HealthStatus.FAILED + elif consecutive_failures >= 2: + health_status = HealthStatus.DEGRADED + elif reliability_score >= 70: + health_status = HealthStatus.HEALTHY + else: + health_status = HealthStatus.UNKNOWN + + # Create metrics object + metrics = JurisdictionQuality( + jurisdiction_name=existing.jurisdiction_name if existing else "Unknown", + state_code=existing.state_code if existing else "XX", + fips_code=existing.fips_code if existing else None, + url=jurisdiction_url, + platform=existing.platform if existing else None, + total_meetings_expected=existing.total_meetings_expected if existing else 12, + total_meetings_found=existing.total_meetings_found if existing else 0, + meetings_with_agendas=existing.meetings_with_agendas if existing else 0, + meetings_with_minutes=existing.meetings_with_minutes if existing else 0, + meetings_with_videos=existing.meetings_with_videos if existing else 0, + meetings_with_transcripts=existing.meetings_with_transcripts if existing else 0, + last_scraped=now, + last_meeting_found=existing.last_meeting_found if existing else None, + scraping_frequency=existing.scraping_frequency if existing else "monthly", + consecutive_successes=existing.consecutive_successes if existing else 0, + consecutive_failures=consecutive_failures, + total_scrapes=existing.total_scrapes + 1 if existing else 1, + successful_scrapes=existing.successful_scrapes if existing else 0, + last_success=existing.last_success if existing else None, + last_error=existing.last_error if existing else None, + completeness_score=round(completeness_score, 2), + reliability_score=round(reliability_score, 2), + freshness_score=round(freshness_score, 2), + overall_quality=round(overall_quality, 2), + health_status=health_status.value, + created_at=existing.created_at if existing else now, + updated_at=now + ) + + return metrics + + def _get_existing_metrics(self, url: str) -> Optional[JurisdictionQuality]: + """Load existing metrics for a jurisdiction.""" + try: + df = self.spark.read.format("delta").load(self.quality_metrics_path) + result = df.filter(col("url") == url).first() + if result: + return JurisdictionQuality.from_dict(result.asDict()) + except Exception: + pass + return None + + def _load_quality_metrics(self) -> Optional[DataFrame]: + """Load all quality metrics.""" + try: + return self.spark.read.format("delta").load(self.quality_metrics_path) + except Exception: + return None + + def _save_batch_result(self, result: BatchResult): + """Save batch result to Delta Lake.""" + # Convert to DataFrame + data = [{ + 'batch_number': result.batch_number, + 'batch_size': result.batch_size, + 'jurisdictions_processed': result.jurisdictions_processed, + 'jurisdictions_succeeded': result.jurisdictions_succeeded, + 'jurisdictions_failed': result.jurisdictions_failed, + 'meetings_found': result.meetings_found, + 'agendas_found': result.agendas_found, + 'minutes_found': result.minutes_found, + 'success_rate': result.success_rate, + 'duration_seconds': result.duration_seconds, + 'start_time': result.start_time.isoformat(), + 'end_time': result.end_time.isoformat() if result.end_time else None, + 'errors': json.dumps(result.errors) + }] + + df = self.spark.createDataFrame(data) + + # Write to Delta Lake + df.write \ + .format("delta") \ + .mode("append") \ + .save(self.batch_results_path) + + logger.info(f"Saved batch result {result.batch_number} to Delta Lake") + + def get_system_health_report(self) -> dict: + """ + Generate overall system health report. + + Returns: + Dictionary with aggregate statistics + """ + quality_df = self._load_quality_metrics() + + if quality_df is None: + return { + 'status': 'no_data', + 'message': 'No quality metrics available yet' + } + + # Aggregate statistics + stats = quality_df.agg( + count("*").alias("total_jurisdictions"), + avg("overall_quality").alias("avg_quality"), + avg("completeness_score").alias("avg_completeness"), + avg("reliability_score").alias("avg_reliability"), + spark_sum((col("health_status") == "healthy").cast("int")).alias("healthy_count"), + spark_sum((col("health_status") == "degraded").cast("int")).alias("degraded_count"), + spark_sum((col("health_status") == "failed").cast("int")).alias("failed_count") + ).first() + + return { + 'total_jurisdictions': stats['total_jurisdictions'], + 'average_quality': round(stats['avg_quality'], 2), + 'average_completeness': round(stats['avg_completeness'], 2), + 'average_reliability': round(stats['avg_reliability'], 2), + 'healthy_count': stats['healthy_count'], + 'degraded_count': stats['degraded_count'], + 'failed_count': stats['failed_count'], + 'health_percentage': round( + (stats['healthy_count'] / stats['total_jurisdictions']) * 100, 1 + ) if stats['total_jurisdictions'] > 0 else 0 + } + + +if __name__ == "__main__": + # Demo + processor = BatchProcessor(batch_size=10) + + print("🔄 Batch Processing Demo") + print("=" * 70) + print("\nThis would process jurisdictions in batches with quality tracking.") + print("\nExample batch results:\n") + + # Simulate processing (would normally call process_all_jurisdictions) + for i in range(3): + result = BatchResult( + batch_number=i + 1, + batch_size=10, + jurisdictions_processed=10, + jurisdictions_succeeded=8, + jurisdictions_failed=2, + meetings_found=45, + agendas_found=40, + minutes_found=30, + errors=[], + start_time=datetime.utcnow(), + end_time=datetime.utcnow() + timedelta(minutes=5), + duration_seconds=300 + ) + + print(f"Batch {result.batch_number}:") + print(f" Processed: {result.jurisdictions_processed}") + print(f" Success rate: {result.success_rate:.1f}%") + print(f" Meetings found: {result.meetings_found}") + print(f" Duration: {result.duration_seconds:.0f}s") + print() + + print("📊 System health tracking:") + print(" • Quality scores per jurisdiction") + print(" • Completeness, reliability, freshness metrics") + print(" • Health status: healthy, degraded, failed") + print(" • Automatic retry with exponential backoff") diff --git a/discovery/city_scrapers_urls.py b/discovery/city_scrapers_urls.py new file mode 100644 index 0000000000000000000000000000000000000000..ccd67c5e8801837efa0fe158e86cd15fd39512b1 --- /dev/null +++ b/discovery/city_scrapers_urls.py @@ -0,0 +1,313 @@ +""" +City Scrapers URL Extraction + +City Scrapers is the most comprehensive civic tech project for scraping local +government meeting data across the United States. + +They maintain validated scrapers for: +- Chicago: ~100 agencies +- Pittsburgh: ~30 agencies +- Detroit: ~40 agencies +- Cleveland: ~30 agencies +- Los Angeles: ~50 agencies + +Each spider file contains: +- start_urls: Validated meeting pages +- Scraping logic for Granicus, Legistar, custom platforms +- Video link extraction (often YouTube embeds from Granicus) + +Website: https://cityscrapers.org +GitHub: https://github.com/city-scrapers +""" +import sys +import re +import subprocess +import tempfile +from pathlib import Path +from typing import List, Dict +from datetime import datetime +from loguru import logger +from pyspark.sql import SparkSession +from pyspark.sql.functions import lit + +# Add project root to Python path for standalone execution +project_root = Path(__file__).parent.parent +if str(project_root) not in sys.path: + sys.path.insert(0, str(project_root)) + +from config.settings import settings + + +CITY_SCRAPERS_REPOS = [ + { + "city": "Chicago", + "state": "IL", + "repo": "https://github.com/city-scrapers/city-scrapers", + "spiders_path": "city_scrapers/spiders", + "expected_agencies": 100 + }, + { + "city": "Pittsburgh", + "state": "PA", + "repo": "https://github.com/city-scrapers/city-scrapers-pitt", + "spiders_path": "city_scrapers_pitt/spiders", + "expected_agencies": 30 + }, + { + "city": "Detroit", + "state": "MI", + "repo": "https://github.com/city-scrapers/city-scrapers-detroit", + "spiders_path": "city_scrapers_det/spiders", + "expected_agencies": 40 + }, + { + "city": "Cleveland", + "state": "OH", + "repo": "https://github.com/city-scrapers/city-scrapers-cle", + "spiders_path": "city_scrapers_cle/spiders", + "expected_agencies": 30 + }, + { + "city": "Los Angeles", + "state": "CA", + "repo": "https://github.com/city-scrapers/city-scrapers-la", + "spiders_path": "city_scrapers_la/spiders", + "expected_agencies": 50 + } +] + + +def extract_start_urls_from_spider_file(spider_file_content: str) -> List[str]: + """ + Extract start_urls from a City Scrapers spider file. + + Pattern matches: + - start_urls = ["https://..."] + - start_urls = ['https://...'] + - start_urls = [ + "https://...", + "https://..." + ] + """ + urls = [] + + # Match start_urls = [...] + pattern = r'start_urls\s*=\s*\[(.*?)\]' + matches = re.findall(pattern, spider_file_content, re.DOTALL) + + for match in matches: + # Extract quoted strings + url_pattern = r'["\']([^"\']+)["\']' + found_urls = re.findall(url_pattern, match) + urls.extend(found_urls) + + return urls + + +def extract_agency_name_from_spider(spider_file_content: str, spider_filename: str) -> str: + """ + Extract agency name from spider class. + + Priority: + 1. agency = "..." attribute + 2. name = "..." attribute + 3. Spider filename (fallback) + """ + # Try agency attribute + agency_pattern = r'agency\s*=\s*["\']([^"\']+)["\']' + agency_match = re.search(agency_pattern, spider_file_content) + if agency_match: + return agency_match.group(1) + + # Try name attribute + name_pattern = r'name\s*=\s*["\']([^"\']+)["\']' + name_match = re.search(name_pattern, spider_file_content) + if name_match: + return name_match.group(1).replace('_', ' ').title() + + # Fallback to filename + return spider_filename.replace('_', ' ').replace('.py', '').title() + + +def clone_and_extract_city_scrapers_urls() -> List[Dict]: + """ + Clone all City Scrapers repos and extract URLs from spider files. + + Returns list of dicts with: + - url: Meeting page URL + - city: City name + - state: State code + - agency: Agency name (from spider file) + - source: "city_scrapers" + - repo: GitHub repo URL + """ + logger.info("Cloning City Scrapers repositories and extracting URLs") + + all_urls = [] + + with tempfile.TemporaryDirectory() as tmpdir: + for repo_info in CITY_SCRAPERS_REPOS: + logger.info(f"\n📦 Processing {repo_info['city']}, {repo_info['state']}...") + + # Clone repo + repo_path = Path(tmpdir) / repo_info['city'].replace(' ', '_') + + try: + subprocess.run([ + "git", "clone", "--depth", "1", "--quiet", + repo_info['repo'], str(repo_path) + ], check=True, capture_output=True) + + logger.info(f"✅ Cloned {repo_info['city']} repo") + + except subprocess.CalledProcessError as e: + logger.error(f"❌ Failed to clone {repo_info['city']} repo: {e}") + continue + + # Find spider files + spiders_path = repo_path / repo_info['spiders_path'] + + if not spiders_path.exists(): + logger.warning(f"⚠️ Spider path not found: {spiders_path}") + continue + + spider_files = list(spiders_path.glob("*.py")) + logger.info(f"Found {len(spider_files)} spider files") + + city_urls = [] + + for spider_file in spider_files: + # Skip internal files + if spider_file.name.startswith("_") or spider_file.name == "__init__.py": + continue + + try: + # Read spider file + content = spider_file.read_text(encoding='utf-8') + + # Extract start_urls + urls = extract_start_urls_from_spider_file(content) + + if not urls: + continue + + # Extract agency name + agency = extract_agency_name_from_spider(content, spider_file.stem) + + for url in urls: + city_urls.append({ + "url": url, + "city": repo_info['city'], + "state": repo_info['state'], + "agency": agency, + "source": "city_scrapers", + "repo": repo_info['repo'] + }) + + except Exception as e: + logger.warning(f"Failed to parse {spider_file.name}: {e}") + continue + + logger.info(f"✅ Extracted {len(city_urls)} URLs from {repo_info['city']}") + all_urls.extend(city_urls) + + logger.info(f"\n🎯 TOTAL: Extracted {len(all_urls)} URLs from {len(CITY_SCRAPERS_REPOS)} cities") + + return all_urls + + +def write_to_bronze_layer( + urls: List[Dict], + spark: SparkSession +) -> Dict[str, int]: + """ + Write City Scrapers URLs to Bronze layer. + + Creates table: bronze/city_scrapers_urls + """ + if not urls: + logger.warning("No URLs to write") + return {"total_urls": 0} + + # Add ingestion timestamp + for url in urls: + url['ingested_at'] = datetime.utcnow().isoformat() + + # Convert to DataFrame + df = spark.createDataFrame(urls) + + # Write to Delta Lake + output_path = f"{settings.delta_lake_path}/bronze/city_scrapers_urls" + + logger.info(f"Writing City Scrapers URLs to Bronze layer: {output_path}") + + df.write \ + .format("delta") \ + .mode("overwrite") \ + .save(output_path) + + # Get stats by city + city_counts = df.groupBy("city").count().collect() + + logger.info("\n✅ City Scrapers URLs written to Bronze layer") + logger.info("\nURLs by city:") + for row in sorted(city_counts, key=lambda r: r['count'], reverse=True): + logger.info(f" • {row['city']}: {row['count']} URLs") + + return { + "total_urls": df.count(), + "cities": df.select("city").distinct().count(), + "table": "bronze/city_scrapers_urls" + } + + +def ingest_city_scrapers_urls(spark: SparkSession) -> Dict[str, int]: + """ + Main function: Clone City Scrapers repos and extract URLs. + + Returns: + - total_urls: Number of URLs extracted + - cities: Number of cities covered + - table: Bronze layer table name + """ + logger.info("=" * 60) + logger.info("CITY SCRAPERS URL EXTRACTION") + logger.info("=" * 60) + + # Extract URLs from GitHub repos + urls = clone_and_extract_city_scrapers_urls() + + if not urls: + logger.error("❌ No URLs extracted from City Scrapers") + return {"total_urls": 0, "cities": 0} + + # Write to Bronze layer + stats = write_to_bronze_layer(urls, spark) + + logger.info("\n" + "=" * 60) + logger.info(f"✅ CITY SCRAPERS INGESTION COMPLETE") + logger.info(f" • URLs extracted: {stats['total_urls']}") + logger.info(f" • Cities covered: {stats['cities']}") + logger.info(f" • Table: {stats['table']}") + logger.info("=" * 60) + + return stats + + +if __name__ == "__main__": + # Test extraction + from delta import configure_spark_with_delta_pip + from pyspark.sql import SparkSession + + # Configure Spark with Delta Lake + builder = SparkSession.builder \ + .appName("CityScrapersURLExtraction") \ + .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \ + .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog") + + spark = configure_spark_with_delta_pip(builder).getOrCreate() + + # Run ingestion + stats = ingest_city_scrapers_urls(spark) + + print(f"\n✅ Extracted {stats['total_urls']} URLs from City Scrapers") diff --git a/discovery/comprehensive_discovery_pipeline.py b/discovery/comprehensive_discovery_pipeline.py new file mode 100644 index 0000000000000000000000000000000000000000..786d612cc18a4f74149fd9c90d74620836e40cf4 --- /dev/null +++ b/discovery/comprehensive_discovery_pipeline.py @@ -0,0 +1,635 @@ +""" +Comprehensive Discovery Pipeline for ALL U.S. Cities and Counties + +Automates discovery of: +- Government websites +- YouTube channels (with statistics) +- Vimeo channels +- Meeting platforms (Legistar, SuiteOne, Granicus, etc.) +- Agenda portals and document systems +- Social media accounts +- Meeting schedules and archives + +Scale: 3,143 counties + 19,000+ cities = ~22,000 jurisdictions + +Usage: + # Run for all jurisdictions + python discovery/comprehensive_discovery_pipeline.py --all + + # Run for specific state + python discovery/comprehensive_discovery_pipeline.py --state AL + + # Run for top 100 cities + python discovery/comprehensive_discovery_pipeline.py --top 100 +""" +import asyncio +import argparse +from typing import List, Dict, Optional +from datetime import datetime +import json +from pathlib import Path + +from loguru import logger +from tqdm.asyncio import tqdm +import pandas as pd + +from discovery.url_discovery_agent import URLDiscoveryAgent +from discovery.youtube_channel_discovery import YouTubeChannelDiscovery +from discovery.social_media_discovery import SocialMediaDiscovery +from discovery.platform_detector import detect_platform +import httpx + + +class ComprehensiveDiscoveryPipeline: + """ + Master pipeline for discovering all data sources for U.S. jurisdictions. + + Designed to scale to 22,000+ cities and counties nationwide. + """ + + def __init__( + self, + youtube_api_key: Optional[str] = None, + max_concurrent: int = 10, + output_dir: str = "data/bronze/discovered_sources" + ): + """ + Initialize discovery pipeline. + + Args: + youtube_api_key: YouTube Data API v3 key (optional but recommended) + max_concurrent: Max concurrent requests (rate limiting) + output_dir: Where to save discovered data + """ + self.youtube_api_key = youtube_api_key + self.max_concurrent = max_concurrent + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + + # Initialize discovery agents + # Note: URLDiscoveryAgent is optional - we use direct pattern matching + self.semaphore = asyncio.Semaphore(max_concurrent) + + async def discover_jurisdiction( + self, + jurisdiction: Dict + ) -> Dict: + """ + Comprehensive discovery for a single jurisdiction. + + Args: + jurisdiction: Dict with keys: name, state_code, type (city/county), population + + Returns: + Complete discovery results + """ + async with self.semaphore: + name = jurisdiction['name'] + state = jurisdiction['state_code'] + jtype = jurisdiction.get('type', 'city') + + logger.info(f"Discovering: {name}, {state} ({jtype})") + + results = { + 'jurisdiction': jurisdiction, + 'discovery_timestamp': datetime.now().isoformat(), + 'websites': [], + 'youtube_channels': [], + 'other_video': [], + 'meeting_platforms': [], + 'social_media': {}, + 'agenda_portals': [], + 'status': 'success' + } + + try: + # Step 1: Discover official website + logger.debug(f" Step 1/6: Finding website for {name}") + website = await self._discover_website(name, state, jtype) + + if website: + results['websites'].append(website) + homepage_url = website.get('url') + else: + logger.warning(f" No website found for {name}, {state}") + results['status'] = 'partial' + homepage_url = None + + # Step 2: Discover YouTube channels + logger.debug(f" Step 2/6: Finding YouTube channels") + youtube_channels = await self._discover_youtube( + name, state, jtype, homepage_url + ) + results['youtube_channels'] = youtube_channels + + # Step 3: Discover other video platforms (Vimeo, etc.) + if homepage_url: + logger.debug(f" Step 3/6: Finding other video platforms") + other_video = await self._discover_other_video(homepage_url) + results['other_video'] = other_video + + # Step 4: Detect meeting platforms + if homepage_url: + logger.debug(f" Step 4/6: Detecting meeting platforms") + platforms = await self._detect_meeting_platforms( + name, state, homepage_url + ) + results['meeting_platforms'] = platforms + + # Step 5: Discover social media + if homepage_url: + logger.debug(f" Step 5/6: Finding social media accounts") + social = await self._discover_social_media(homepage_url, name, state) + results['social_media'] = social + + # Step 6: Find agenda portals + if homepage_url: + logger.debug(f" Step 6/6: Finding agenda portals") + agendas = await self._find_agenda_portals(homepage_url, name) + results['agenda_portals'] = agendas + + # Calculate completeness score + results['completeness_score'] = self._calculate_completeness(results) + + logger.success(f"✓ {name}: {results['completeness_score']:.0%} complete") + + except Exception as e: + logger.error(f" Error discovering {name}: {e}") + results['status'] = 'error' + results['error'] = str(e) + + return results + + async def _discover_website( + self, + name: str, + state: str, + jtype: str + ) -> Optional[Dict]: + """Discover official government website.""" + # Try common URL patterns + name_clean = name.lower().replace(' ', '').replace("'", '') + + if jtype == 'county': + name_clean = name_clean.replace('county', '') + + patterns = [ + f'https://www.{name_clean}{state.lower()}.gov', + f'https://{name_clean}{state.lower()}.gov', + f'https://www.{name_clean}.gov', + f'https://{name_clean}.gov', + f'https://www.{name_clean}.{state.lower()}.gov', + f'https://www.cityof{name_clean}.com', + f'https://www.{name_clean}.com', + ] + + if jtype == 'county': + patterns.extend([ + f'https://www.{name_clean}co.com', + f'https://{name_clean}county.com', + f'https://www.{name_clean}county.gov', + ]) + + client = httpx.AsyncClient(timeout=10, follow_redirects=True) + + for url in patterns: + try: + response = await client.get(url) + if response.status_code == 200: + await client.aclose() + return { + 'url': url, + 'final_url': str(response.url), + 'status': 'active', + 'discovery_method': 'pattern_match' + } + except: + continue + + await client.aclose() + return None + + async def _discover_youtube( + self, + name: str, + state: str, + jtype: str, + homepage_url: Optional[str] + ) -> List[Dict]: + """Discover YouTube channels.""" + city_name = name if jtype == 'city' else name.replace(' County', '').strip() + county_name = name if jtype == 'county' else None + + async with YouTubeChannelDiscovery(self.youtube_api_key) as discovery: + channels = await discovery.discover_channels( + city_name=city_name, + county_name=county_name, + state_code=state, + homepage_url=homepage_url + ) + return channels + + async def _discover_other_video(self, homepage_url: str) -> List[Dict]: + """Discover Vimeo and other video platforms.""" + video_platforms = [] + + async with SocialMediaDiscovery() as discovery: + social = await discovery._scrape_page_for_social(homepage_url) + + if social.get('vimeo'): + for vimeo_url in social['vimeo']: + video_platforms.append({ + 'platform': 'vimeo', + 'url': vimeo_url, + 'discovery_method': 'website_scrape' + }) + + if social.get('archive_org'): + for archive_url in social['archive_org']: + video_platforms.append({ + 'platform': 'archive.org', + 'url': archive_url, + 'discovery_method': 'website_scrape' + }) + + return video_platforms + + async def _detect_meeting_platforms( + self, + name: str, + state: str, + homepage_url: str + ) -> List[Dict]: + """Detect meeting platforms (Legistar, SuiteOne, Granicus, etc.).""" + platforms = [] + + client = httpx.AsyncClient(timeout=15, follow_redirects=True) + + # Check website for platform + try: + response = await client.get(homepage_url) + if response.status_code == 200: + platform_type = detect_platform(homepage_url, response.text) + + if platform_type: + platforms.append({ + 'type': platform_type, + 'detected_on': homepage_url, + 'method': 'html_analysis' + }) + except: + pass + + # Check for Legistar API + name_clean = name.lower().replace(' ', '').replace("'", '') + legistar_slugs = [ + name_clean, + f'{name_clean}{state.lower()}', + f'{name_clean}county' if 'county' not in name_clean else name_clean + ] + + for slug in legistar_slugs: + try: + url = f'https://webapi.legistar.com/v1/{slug}/events' + response = await client.get(url, params={'$top': 1}, timeout=5) + + if response.status_code == 200: + platforms.append({ + 'type': 'legistar', + 'api_url': url, + 'slug': slug, + 'method': 'api_test' + }) + break + except: + continue + + # Check for SuiteOne (like Tuscaloosa) + suiteone_patterns = [ + f'https://{name_clean}{state.lower()}.suiteonemedia.com', + f'https://{name_clean}.suiteonemedia.com', + ] + + for url in suiteone_patterns: + try: + response = await client.get(url, timeout=5) + if response.status_code == 200 and 'suiteonemedia' in response.text.lower(): + platforms.append({ + 'type': 'suiteone', + 'url': url, + 'method': 'url_test' + }) + break + except: + continue + + # Check for Granicus + granicus_patterns = [ + f'https://{name_clean}.granicus.com', + f'https://{name_clean}{state.lower()}.granicus.com', + ] + + for url in granicus_patterns: + try: + response = await client.get(url, timeout=5) + if response.status_code == 200: + platforms.append({ + 'type': 'granicus', + 'url': url, + 'method': 'url_test' + }) + break + except: + continue + + await client.aclose() + return platforms + + async def _discover_social_media( + self, + homepage_url: str, + name: str, + state: str + ) -> Dict[str, List[str]]: + """Discover all social media accounts.""" + async with SocialMediaDiscovery() as discovery: + social = await discovery.discover_from_website( + homepage_url=homepage_url, + jurisdiction_name=name, + state=state + ) + return social + + async def _find_agenda_portals( + self, + homepage_url: str, + name: str + ) -> List[Dict]: + """Find agenda/document portals.""" + portals = [] + + client = httpx.AsyncClient(timeout=15, follow_redirects=True) + + # Check main page for agenda links + try: + response = await client.get(homepage_url) + if response.status_code == 200: + from bs4 import BeautifulSoup + soup = BeautifulSoup(response.content, 'html.parser') + + # Look for agenda-related links + for link in soup.find_all('a', href=True): + text = link.get_text().lower() + href = link.get('href', '') + + if any(word in text for word in ['agenda', 'minutes', 'meeting']): + # Check if it's an external portal + if any(domain in href for domain in ['suiteonemedia', 'granicus', 'civicclerk', 'municode']): + from urllib.parse import urljoin + full_url = urljoin(homepage_url, href) + + portals.append({ + 'url': full_url, + 'link_text': text, + 'discovery_method': 'homepage_scrape' + }) + except: + pass + + await client.aclose() + return portals + + def _calculate_completeness(self, results: Dict) -> float: + """Calculate how complete the discovery is (0.0 to 1.0).""" + score = 0.0 + total = 6.0 # 6 data categories + + if results['websites']: + score += 1.0 + if results['youtube_channels']: + score += 1.0 + if results['meeting_platforms']: + score += 1.0 + if results['social_media'] and any(results['social_media'].values()): + score += 1.0 + if results['other_video']: + score += 0.5 + if results['agenda_portals']: + score += 1.5 + + return min(score / total, 1.0) + + async def discover_batch( + self, + jurisdictions: List[Dict], + save_interval: int = 100 + ) -> List[Dict]: + """ + Discover data for a batch of jurisdictions with progress tracking. + + Args: + jurisdictions: List of jurisdiction dicts + save_interval: Save results every N jurisdictions + + Returns: + List of discovery results + """ + results = [] + + logger.info(f"Starting batch discovery for {len(jurisdictions)} jurisdictions") + + # Process with progress bar + tasks = [ + self.discover_jurisdiction(j) + for j in jurisdictions + ] + + for i, task in enumerate(tqdm.as_completed(tasks, total=len(tasks))): + result = await task + results.append(result) + + # Save intermediate results + if (i + 1) % save_interval == 0: + self._save_results(results, f'batch_{i+1}') + logger.info(f"Saved {i+1} results") + + # Final save + self._save_results(results, 'final') + + return results + + def _save_results(self, results: List[Dict], suffix: str = ''): + """Save results to JSON and CSV.""" + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + + # Save detailed JSON + json_file = self.output_dir / f'discovery_results_{suffix}_{timestamp}.json' + with open(json_file, 'w') as f: + json.dump(results, f, indent=2) + + logger.info(f"Saved JSON: {json_file}") + + # Save summary CSV + summary = [] + for r in results: + j = r['jurisdiction'] + summary.append({ + 'name': j['name'], + 'state': j['state_code'], + 'type': j.get('type', 'city'), + 'population': j.get('population', 0), + 'website': r['websites'][0]['url'] if r['websites'] else '', + 'youtube_channels': len(r['youtube_channels']), + 'meeting_platforms': len(r['meeting_platforms']), + 'agenda_portals': len(r['agenda_portals']), + 'completeness': r.get('completeness_score', 0.0), + 'status': r['status'] + }) + + csv_file = self.output_dir / f'discovery_summary_{suffix}_{timestamp}.csv' + pd.DataFrame(summary).to_csv(csv_file, index=False) + + logger.info(f"Saved CSV: {csv_file}") + + def load_jurisdictions( + self, + state_filter: Optional[str] = None, + top_n: Optional[int] = None + ) -> List[Dict]: + """ + Load jurisdiction list from Census/NACo data. + + Args: + state_filter: Filter to specific state (e.g., 'AL') + top_n: Limit to top N by population + + Returns: + List of jurisdiction dicts + """ + logger.info("Loading jurisdiction list...") + + # Check if we have Census data + census_file = Path('data/cache/census_jurisdictions.parquet') + + if census_file.exists(): + # Load from cached Census data + df = pd.read_parquet(census_file) + + if state_filter: + df = df[df['state_code'] == state_filter] + + if top_n: + df = df.nlargest(top_n, 'population') + + jurisdictions = df.to_dict('records') + logger.info(f"Loaded {len(jurisdictions)} jurisdictions from Census data") + + else: + # Generate sample list from NACo/known data + logger.warning("Census data not found, using sample list") + jurisdictions = self._generate_sample_list(state_filter, top_n) + + return jurisdictions + + def _generate_sample_list( + self, + state_filter: Optional[str], + top_n: Optional[int] + ) -> List[Dict]: + """Generate sample jurisdiction list.""" + # Top 100 U.S. cities by population (sample) + sample_cities = [ + {'name': 'New York', 'state_code': 'NY', 'type': 'city', 'population': 8336817}, + {'name': 'Los Angeles', 'state_code': 'CA', 'type': 'city', 'population': 3979576}, + {'name': 'Chicago', 'state_code': 'IL', 'type': 'city', 'population': 2693976}, + {'name': 'Houston', 'state_code': 'TX', 'type': 'city', 'population': 2320268}, + {'name': 'Phoenix', 'state_code': 'AZ', 'type': 'city', 'population': 1680992}, + {'name': 'Philadelphia', 'state_code': 'PA', 'type': 'city', 'population': 1584064}, + {'name': 'San Antonio', 'state_code': 'TX', 'type': 'city', 'population': 1547253}, + {'name': 'San Diego', 'state_code': 'CA', 'type': 'city', 'population': 1423851}, + {'name': 'Dallas', 'state_code': 'TX', 'type': 'city', 'population': 1343573}, + {'name': 'San Jose', 'state_code': 'CA', 'type': 'city', 'population': 1021795}, + {'name': 'Tuscaloosa', 'state_code': 'AL', 'type': 'city', 'population': 99600}, + # Add more... + ] + + if state_filter: + sample_cities = [c for c in sample_cities if c['state_code'] == state_filter] + + if top_n: + sample_cities = sample_cities[:top_n] + + return sample_cities + + +async def main(): + """Command-line interface for discovery pipeline.""" + parser = argparse.ArgumentParser( + description='Discover data sources for all U.S. cities and counties' + ) + parser.add_argument( + '--state', + type=str, + help='Filter to specific state (e.g., AL, CA, TX)' + ) + parser.add_argument( + '--top', + type=int, + help='Limit to top N jurisdictions by population' + ) + parser.add_argument( + '--all', + action='store_true', + help='Process all jurisdictions (warning: 20,000+)' + ) + parser.add_argument( + '--youtube-api-key', + type=str, + help='YouTube Data API v3 key for accurate statistics' + ) + parser.add_argument( + '--max-concurrent', + type=int, + default=10, + help='Maximum concurrent requests (default: 10)' + ) + + args = parser.parse_args() + + # Initialize pipeline + pipeline = ComprehensiveDiscoveryPipeline( + youtube_api_key=args.youtube_api_key, + max_concurrent=args.max_concurrent + ) + + # Load jurisdictions + jurisdictions = pipeline.load_jurisdictions( + state_filter=args.state, + top_n=args.top if not args.all else None + ) + + logger.info(f"Will process {len(jurisdictions)} jurisdictions") + + if len(jurisdictions) > 100 and not args.all: + logger.warning(f"Large batch ({len(jurisdictions)}). Use --all to confirm.") + return + + # Run discovery + results = await pipeline.discover_batch(jurisdictions) + + # Summary statistics + successful = sum(1 for r in results if r['status'] == 'success') + avg_completeness = sum(r.get('completeness_score', 0) for r in results) / len(results) + + print("\n" + "="*80) + print("DISCOVERY COMPLETE!") + print("="*80) + print(f"Total jurisdictions: {len(results)}") + print(f"Successful: {successful} ({successful/len(results):.1%})") + print(f"Average completeness: {avg_completeness:.1%}") + print(f"\nResults saved to: {pipeline.output_dir}") + print("="*80) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/discovery/curated_sources.py b/discovery/curated_sources.py new file mode 100644 index 0000000000000000000000000000000000000000..669005e6bdc95a1954cd21106459b4f30f034615 --- /dev/null +++ b/discovery/curated_sources.py @@ -0,0 +1,403 @@ +""" +ELGL & NACo Integration for Video Channel Discovery + +Two highly curated sources for finding the most active local government +YouTube channels and county digital innovation hubs. + +Data Sources: +1. ELGL (Engaging Local Government Leaders) + - "Top Local Government YouTube Channels" lists + - Curated, high-quality channels + - Most active local governments nationwide + +2. NACo (National Association of Counties) + - Database of 3,143 county websites + - Digital innovation showcase + - County media hubs and video portals +""" +import asyncio +import re +from typing import List, Dict, Optional +from datetime import datetime +import httpx +from bs4 import BeautifulSoup +from loguru import logger + + +class ELGLYouTubeDiscovery: + """ + Discover YouTube channels from ELGL's curated lists. + + ELGL (Engaging Local Government Leaders) regularly publishes lists of + top local government YouTube channels. These are the most active and + innovative channels across the country. + + Sources: + - ELGL Blog: https://elgl.org/ + - Annual "Top Local Gov YouTube Channels" articles + - Conference presentations and webinars + """ + + ELGL_SOURCES = [ + { + "name": "ELGL Top Channels 2024", + "url": "https://elgl.org/top-local-government-youtube-channels-2024/", + "type": "article" + }, + { + "name": "ELGL Top Channels 2023", + "url": "https://elgl.org/top-local-government-youtube-channels-2023/", + "type": "article" + }, + { + "name": "ELGL Digital Innovation", + "url": "https://elgl.org/category/communication/", + "type": "category" + } + ] + + def __init__(self): + """Initialize ELGL discovery.""" + self.client = httpx.AsyncClient( + timeout=15.0, + follow_redirects=True, + headers={ + "User-Agent": "Mozilla/5.0 (compatible; OralHealthPolicyBot/2.0)" + } + ) + + async def scrape_elgl_top_channels(self) -> List[Dict[str, str]]: + """ + Scrape ELGL's "Top Local Government YouTube Channels" articles. + + These articles typically list: + - YouTube channel URLs + - Municipality/county name + - State + - Brief description + - Subscriber count and activity metrics + + Returns: + List of dicts with channel info: + { + 'jurisdiction_name': 'Seattle', + 'state': 'WA', + 'youtube_url': 'https://youtube.com/@cityofseattle', + 'source': 'ELGL Top Channels 2024', + 'description': 'City council meetings and city updates', + 'subscribers': '15000', + 'is_top_ranked': True + } + """ + logger.info("Scraping ELGL Top YouTube Channels lists") + + all_channels = [] + + for source in self.ELGL_SOURCES: + try: + logger.info(f"Fetching {source['name']}...") + response = await self.client.get(source['url']) + + if response.status_code == 200: + channels = self._parse_elgl_article( + response.content, + source['name'] + ) + all_channels.extend(channels) + logger.success(f"✓ Found {len(channels)} channels from {source['name']}") + + await asyncio.sleep(1) # Rate limiting + + except Exception as e: + logger.warning(f"Error fetching {source['name']}: {e}") + + # Deduplicate by YouTube URL + unique_channels = {} + for channel in all_channels: + url = channel['youtube_url'] + if url not in unique_channels: + unique_channels[url] = channel + + logger.success(f"✓ Total unique channels from ELGL: {len(unique_channels)}") + + return list(unique_channels.values()) + + def _parse_elgl_article(self, content: bytes, source_name: str) -> List[Dict]: + """ + Parse ELGL article HTML to extract YouTube channels. + + ELGL articles typically have patterns like: + - Links to YouTube channels + - Municipality names in headers or lists + - Descriptions of channel content + """ + soup = BeautifulSoup(content, 'html.parser') + channels = [] + + # Find all YouTube links in the article + youtube_pattern = r'youtube\.com/(?:c/|channel/|user/|@)([\w-]+)' + + # Strategy 1: Find links in article body + article_body = soup.find('article') or soup.find('div', class_='entry-content') + + if article_body: + # Extract all links + links = article_body.find_all('a', href=True) + + for link in links: + href = link['href'] + match = re.search(youtube_pattern, href) + + if match: + # Try to extract context (city name, state) + context = self._extract_channel_context(link, soup) + + channel = { + 'youtube_url': href, + 'source': source_name, + 'jurisdiction_name': context.get('name', 'Unknown'), + 'state': context.get('state', ''), + 'description': context.get('description', ''), + 'is_top_ranked': True, + 'discovered_at': datetime.utcnow().isoformat() + } + + channels.append(channel) + + return channels + + def _extract_channel_context(self, link_element, soup) -> Dict[str, str]: + """ + Extract context around a YouTube link (city name, state, description). + + Looks at: + - Parent heading (h2, h3) + - Preceding text + - List item text + """ + context = {} + + # Try to find parent heading + parent = link_element.find_parent(['h2', 'h3', 'h4', 'li', 'p']) + + if parent: + text = parent.get_text().strip() + + # Extract city and state pattern: "City Name, ST" + city_state_match = re.search(r'([^,]+),\s*([A-Z]{2})', text) + if city_state_match: + context['name'] = city_state_match.group(1).strip() + context['state'] = city_state_match.group(2) + + # Use full text as description if it's a paragraph + if parent.name == 'p': + context['description'] = text[:200] # Truncate + + return context + + async def close(self): + """Close HTTP client.""" + await self.client.aclose() + + +class NACoCountyDiscovery: + """ + Discover county websites and video channels from NACo database. + + NACo (National Association of Counties) maintains: + - Database of all 3,143 U.S. counties + - County website URLs + - Digital innovation showcase + - County media and communication hubs + + Sources: + - NACo County Explorer: https://ce.naco.org/ + - NACo Digital Counties Survey + - NACo Communications & Media Awards + """ + + NACO_SOURCES = { + "county_explorer": "https://ce.naco.org/", + "digital_innovation": "https://www.naco.org/resources/featured/digital-counties-survey", + "achievement_awards": "https://www.naco.org/resources/programs-and-services/naco-achievement-awards" + } + + def __init__(self): + """Initialize NACo discovery.""" + self.client = httpx.AsyncClient( + timeout=15.0, + follow_redirects=True, + headers={ + "User-Agent": "Mozilla/5.0 (compatible; OralHealthPolicyBot/2.0)" + } + ) + + async def get_naco_county_websites(self) -> List[Dict[str, str]]: + """ + Get county website URLs from NACo County Explorer. + + The County Explorer provides: + - Official county website URLs for all 3,143 counties + - County demographics and facts + - Contact information + + Returns: + List of counties with official websites: + { + 'county_name': 'King County', + 'state': 'WA', + 'homepage_url': 'https://kingcounty.gov', + 'population': 2269675, + 'source': 'NACo County Explorer', + 'fips_code': '53033' + } + """ + logger.info("Fetching NACo County Explorer data") + + # Note: NACo County Explorer may require API access or scraping + # This is a placeholder for the actual implementation + + counties = [] + + try: + # Strategy 1: Check if NACo provides a data export or API + # Strategy 2: Scrape County Explorer if no API available + # Strategy 3: Use Census data + NACo verification + + logger.info("NACo County Explorer integration requires API/data access") + logger.info("Recommendation: Contact NACo for data partnership or bulk export") + + # Placeholder: Return empty for now + # In production, implement actual data retrieval + + except Exception as e: + logger.error(f"Error accessing NACo data: {e}") + + return counties + + async def scrape_naco_digital_innovation(self) -> List[Dict[str, str]]: + """ + Scrape NACo's digital innovation showcase for media hubs. + + NACo highlights counties with innovative digital services: + - Video streaming platforms + - Social media engagement + - Digital communication tools + + Returns: + List of counties with digital innovation: + { + 'county_name': 'Fairfax County', + 'state': 'VA', + 'innovation_type': 'Video Streaming', + 'description': 'Live streaming of board meetings', + 'platform_url': 'https://fairfaxcounty.gov/cableconsumer/channel-16', + 'source': 'NACo Digital Counties Survey' + } + """ + logger.info("Scraping NACo Digital Innovation showcase") + + innovations = [] + + try: + response = await self.client.get(self.NACO_SOURCES['digital_innovation']) + + if response.status_code == 200: + # Parse digital innovation examples + soup = BeautifulSoup(response.content, 'html.parser') + + # Look for case studies, awards, or highlighted counties + # This will vary based on NACo's website structure + + logger.debug("Parsing NACo digital innovation content") + + except Exception as e: + logger.warning(f"Error scraping NACo digital innovation: {e}") + + return innovations + + async def close(self): + """Close HTTP client.""" + await self.client.aclose() + + +async def integrate_curated_sources() -> Dict[str, List[Dict]]: + """ + Integration function to get channels from ELGL and NACo. + + This combines: + 1. ELGL's curated top YouTube channels (most active) + 2. NACo's county website database (comprehensive) + 3. NACo's digital innovation showcase (innovative counties) + + Returns: + Dictionary with results from each source: + { + 'elgl_channels': [...], + 'naco_counties': [...], + 'naco_innovations': [...] + } + """ + logger.info("=== Integrating ELGL & NACo Curated Sources ===") + + results = { + 'elgl_channels': [], + 'naco_counties': [], + 'naco_innovations': [] + } + + # ELGL YouTube Channels + async with ELGLYouTubeDiscovery() as elgl: + results['elgl_channels'] = await elgl.scrape_elgl_top_channels() + + # NACo County Data + async with NACoCountyDiscovery() as naco: + results['naco_counties'] = await naco.get_naco_county_websites() + results['naco_innovations'] = await naco.scrape_naco_digital_innovation() + + # Summary + total = ( + len(results['elgl_channels']) + + len(results['naco_counties']) + + len(results['naco_innovations']) + ) + + logger.success(f"✓ Total curated sources: {total}") + logger.info(f" • ELGL YouTube channels: {len(results['elgl_channels'])}") + logger.info(f" • NACo counties: {len(results['naco_counties'])}") + logger.info(f" • NACo innovations: {len(results['naco_innovations'])}") + + return results + + +async def main(): + """Example usage.""" + + # Get curated sources + results = await integrate_curated_sources() + + # Print results + import json + + if results['elgl_channels']: + print("\n=== ELGL Top YouTube Channels ===") + for channel in results['elgl_channels'][:5]: # First 5 + print(f" • {channel['jurisdiction_name']}, {channel['state']}") + print(f" {channel['youtube_url']}") + print(f" Source: {channel['source']}") + + if results['naco_counties']: + print("\n=== NACo County Websites ===") + for county in results['naco_counties'][:5]: # First 5 + print(f" • {county['county_name']}, {county['state']}") + print(f" {county['homepage_url']}") + + if results['naco_innovations']: + print("\n=== NACo Digital Innovation ===") + for innovation in results['naco_innovations'][:5]: # First 5 + print(f" • {innovation['county_name']}, {innovation['state']}") + print(f" Type: {innovation['innovation_type']}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/discovery/dataverse_client.py b/discovery/dataverse_client.py new file mode 100644 index 0000000000000000000000000000000000000000..e6e085a029d3f191b25184ddcf1a9ffebc6bd0e4 --- /dev/null +++ b/discovery/dataverse_client.py @@ -0,0 +1,624 @@ +""" +Dataverse API Client + +Production-ready client for Harvard Dataverse following IQSS best practices. +Based on official API documentation: https://guides.dataverse.org/en/latest/api/index.html + +Features: +- API token authentication +- Rate limiting with exponential backoff +- Checksum verification +- Version-aware caching +- Comprehensive error handling +- Pagination support +- Retry logic + +Source: https://github.com/IQSS/dataverse +""" +import sys +from pathlib import Path +import hashlib +import asyncio +from typing import Optional, Dict, Any, List +from datetime import datetime, timedelta +from loguru import logger +import json + +# Add project root to path +project_root = Path(__file__).parent.parent +if str(project_root) not in sys.path: + sys.path.insert(0, str(project_root)) + +try: + import httpx +except ImportError: + logger.error("httpx required. Install with: pip install httpx") + httpx = None + +from config import settings + + +class DataverseAPIError(Exception): + """Custom exception for Dataverse API errors.""" + pass + + +class DataverseClient: + """ + Official Dataverse API client following IQSS best practices. + + Usage: + client = DataverseClient(api_key="your-key") + metadata = await client.get_dataset_metadata("doi:10.7910/DVN/NJTBEM") + result = await client.download_dataset("doi:10.7910/DVN/NJTBEM") + """ + + # API endpoints + DATASET_ENDPOINT = "/api/datasets/:persistentId/" + FILE_DOWNLOAD_ENDPOINT = "/api/access/datafile/{file_id}" + SEARCH_ENDPOINT = "/api/search" + + # Rate limiting (requests per minute) + DEFAULT_RATE_LIMIT = 100 + RATE_LIMIT_PERIOD = 60 # seconds + + def __init__( + self, + base_url: str = "https://dataverse.harvard.edu", + api_key: Optional[str] = None, + timeout: int = 120, + max_retries: int = 3, + cache_enabled: bool = True + ): + """ + Initialize Dataverse client. + + Args: + base_url: Dataverse instance URL (default: Harvard Dataverse) + api_key: API token for authentication (optional but recommended) + timeout: Request timeout in seconds + max_retries: Maximum retry attempts for failed requests + cache_enabled: Enable version-aware file caching + """ + if not httpx: + raise ImportError("httpx required. Install with: pip install httpx") + + self.base_url = base_url.rstrip("/") + self.api_key = api_key or settings.dataverse_api_key + self.timeout = timeout + self.max_retries = max_retries + self.cache_enabled = cache_enabled + + # Cache directory + self.cache_dir = Path("data/cache/dataverse") + self.cache_dir.mkdir(parents=True, exist_ok=True) + + # Metadata cache + self.metadata_cache_dir = self.cache_dir / "metadata" + self.metadata_cache_dir.mkdir(parents=True, exist_ok=True) + + # Rate limiting state + self._request_times: List[datetime] = [] + + if self.api_key: + logger.info("Dataverse client initialized with API key") + else: + logger.warning("Dataverse client initialized without API key (rate limits may apply)") + + def _get_headers(self) -> Dict[str, str]: + """ + Get HTTP headers for API requests. + + Returns: + Headers dictionary with API key if available + """ + headers = { + "Content-Type": "application/json", + "User-Agent": "OralHealthPolicyPulse/1.0 (Civic Tech Research)" + } + + if self.api_key: + headers["X-Dataverse-key"] = self.api_key + + return headers + + async def _rate_limit_wait(self): + """ + Implement client-side rate limiting. + + Enforces maximum requests per minute to avoid 429 errors. + """ + now = datetime.now() + + # Remove requests older than the rate limit period + self._request_times = [ + t for t in self._request_times + if (now - t).total_seconds() < self.RATE_LIMIT_PERIOD + ] + + # Check if we've hit the limit + if len(self._request_times) >= self.DEFAULT_RATE_LIMIT: + oldest = min(self._request_times) + wait_time = self.RATE_LIMIT_PERIOD - (now - oldest).total_seconds() + + if wait_time > 0: + logger.warning(f"Rate limit reached. Waiting {wait_time:.1f}s...") + await asyncio.sleep(wait_time) + + # Record this request + self._request_times.append(now) + + async def _request_with_retry( + self, + method: str, + url: str, + **kwargs + ) -> httpx.Response: + """ + Make HTTP request with retry logic and exponential backoff. + + Args: + method: HTTP method (GET, POST, etc.) + url: Full URL to request + **kwargs: Additional arguments for httpx.request() + + Returns: + HTTP response + + Raises: + DataverseAPIError: If all retry attempts fail + """ + await self._rate_limit_wait() + + async with httpx.AsyncClient(timeout=self.timeout, follow_redirects=True) as client: + for attempt in range(self.max_retries): + try: + response = await client.request(method, url, **kwargs) + + # Handle specific status codes + if response.status_code == 200: + return response + + elif response.status_code == 401: + raise DataverseAPIError( + "Unauthorized: API key required or invalid. " + "Sign up at https://dataverse.harvard.edu/loginpage.xhtml" + ) + + elif response.status_code == 404: + raise DataverseAPIError(f"Not found: {url}") + + elif response.status_code == 429: + # Rate limited by server + retry_after = int(response.headers.get("Retry-After", 60)) + logger.warning(f"Server rate limit hit. Retrying after {retry_after}s") + await asyncio.sleep(retry_after) + continue + + elif response.status_code >= 500: + # Server error - retry with backoff + if attempt < self.max_retries - 1: + wait_time = 2 ** attempt + logger.warning(f"Server error {response.status_code}. Retrying in {wait_time}s...") + await asyncio.sleep(wait_time) + continue + else: + raise DataverseAPIError(f"Server error: HTTP {response.status_code}") + + else: + raise DataverseAPIError( + f"API error: HTTP {response.status_code} - {response.text}" + ) + + except httpx.TimeoutException: + if attempt < self.max_retries - 1: + wait_time = 2 ** attempt + logger.warning(f"Request timeout. Retrying in {wait_time}s...") + await asyncio.sleep(wait_time) + continue + else: + raise DataverseAPIError("Request timed out after all retry attempts") + + except Exception as e: + if attempt < self.max_retries - 1: + wait_time = 2 ** attempt + logger.warning(f"Request failed: {e}. Retrying in {wait_time}s...") + await asyncio.sleep(wait_time) + continue + else: + raise DataverseAPIError(f"Request failed: {e}") + + raise DataverseAPIError("All retry attempts exhausted") + + def _get_cached_metadata_path(self, persistent_id: str, version: str) -> Path: + """Get path to cached metadata file.""" + safe_id = persistent_id.replace(":", "_").replace("/", "_") + return self.metadata_cache_dir / f"{safe_id}_{version}.json" + + async def get_dataset_metadata( + self, + persistent_id: str, + version: str = ":latest", + use_cache: bool = True + ) -> Optional[Dict[str, Any]]: + """ + Get dataset metadata from Dataverse. + + Args: + persistent_id: DOI or handle (e.g., "doi:10.7910/DVN/NJTBEM") + version: Dataset version (":latest", ":draft", or specific version number) + use_cache: Use cached metadata if available (for :latest version only) + + Returns: + Dataset metadata dictionary or None if not found + + Example: + metadata = await client.get_dataset_metadata("doi:10.7910/DVN/NJTBEM") + files = metadata["data"]["latestVersion"]["files"] + """ + # Check cache + if use_cache and self.cache_enabled and version == ":latest": + cache_file = self._get_cached_metadata_path(persistent_id, version) + if cache_file.exists(): + # Check if cache is recent (less than 1 day old) + cache_age = datetime.now() - datetime.fromtimestamp(cache_file.stat().st_mtime) + if cache_age < timedelta(days=1): + logger.info(f"Using cached metadata (age: {cache_age.total_seconds() / 3600:.1f}h)") + with open(cache_file, 'r') as f: + return json.load(f) + + # Fetch from API + url = f"{self.base_url}{self.DATASET_ENDPOINT}" + params = { + "persistentId": persistent_id, + } + + # Add version if not :latest + if version != ":latest": + params["version"] = version + + logger.info(f"Fetching metadata for {persistent_id} (version: {version})") + + try: + response = await self._request_with_retry( + "GET", + url, + params=params, + headers=self._get_headers() + ) + + metadata = response.json() + + # Cache the metadata + if self.cache_enabled and version == ":latest": + cache_file = self._get_cached_metadata_path(persistent_id, version) + with open(cache_file, 'w') as f: + json.dump(metadata, f, indent=2) + logger.debug(f"Cached metadata to {cache_file}") + + return metadata + + except DataverseAPIError as e: + logger.error(f"Failed to fetch metadata: {e}") + return None + + def _verify_checksum(self, content: bytes, expected_md5: Optional[str]) -> bool: + """ + Verify file checksum. + + Args: + content: File content bytes + expected_md5: Expected MD5 checksum + + Returns: + True if checksum matches or no checksum provided + """ + if not expected_md5: + logger.warning("No checksum provided - skipping verification") + return True + + actual_md5 = hashlib.md5(content).hexdigest() + + if actual_md5.lower() == expected_md5.lower(): + logger.debug(f"✓ Checksum verified: {actual_md5}") + return True + else: + logger.error(f"✗ Checksum mismatch! Expected: {expected_md5}, Got: {actual_md5}") + return False + + async def download_file( + self, + file_id: int, + output_path: Path, + expected_checksum: Optional[str] = None, + verify_checksum: bool = True + ) -> bool: + """ + Download a file from Dataverse with checksum verification. + + Args: + file_id: Dataverse file ID + output_path: Where to save the file + expected_checksum: Expected MD5 checksum (if known) + verify_checksum: Whether to verify checksum + + Returns: + True if download successful and checksum valid + + Example: + success = await client.download_file( + file_id=123456, + output_path=Path("data/municipalities.csv"), + expected_checksum="abc123..." + ) + """ + url = f"{self.base_url}{self.FILE_DOWNLOAD_ENDPOINT.format(file_id=file_id)}" + + logger.info(f"Downloading file {file_id} to {output_path.name}") + + try: + response = await self._request_with_retry( + "GET", + url, + headers=self._get_headers() + ) + + # Verify checksum if requested + if verify_checksum and expected_checksum: + if not self._verify_checksum(response.content, expected_checksum): + logger.error("Checksum verification failed - file may be corrupted") + return False + + # Save file + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_bytes(response.content) + + file_size_mb = len(response.content) / (1024 * 1024) + logger.success(f"✓ Downloaded {output_path.name} ({file_size_mb:.2f} MB)") + + return True + + except DataverseAPIError as e: + logger.error(f"Download failed: {e}") + return False + + async def download_dataset( + self, + persistent_id: str, + output_dir: Optional[Path] = None, + file_types: Optional[List[str]] = None, + verify_checksums: bool = True + ) -> Dict[str, Any]: + """ + Download all files (or filtered subset) from a dataset. + + Args: + persistent_id: Dataset DOI (e.g., "doi:10.7910/DVN/NJTBEM") + output_dir: Where to save files (defaults to cache_dir/dataset_name) + file_types: List of file extensions to download (e.g., [".csv", ".tab"]) + If None, downloads all files + verify_checksums: Whether to verify MD5 checksums + + Returns: + Summary dictionary with download statistics + + Example: + result = await client.download_dataset( + "doi:10.7910/DVN/NJTBEM", + file_types=[".csv", ".tab"] + ) + print(f"Downloaded {result['downloaded']} files to {result['output_dir']}") + """ + # Set output directory + if output_dir is None: + safe_id = persistent_id.replace(":", "_").replace("/", "_") + output_dir = self.cache_dir / safe_id + + output_dir.mkdir(parents=True, exist_ok=True) + + # Get metadata + logger.info(f"Fetching dataset metadata for {persistent_id}") + metadata = await self.get_dataset_metadata(persistent_id) + + if not metadata: + return { + "status": "error", + "message": "Failed to fetch dataset metadata", + "downloaded": 0, + "failed": 0, + "files": [] + } + + # Extract file list + try: + files = metadata["data"]["latestVersion"]["files"] + logger.info(f"Found {len(files)} files in dataset") + except KeyError: + logger.error("Invalid metadata structure - cannot find files list") + return { + "status": "error", + "message": "Invalid metadata structure", + "downloaded": 0, + "failed": 0, + "files": [] + } + + # Filter by file type if specified + if file_types: + original_count = len(files) + files = [ + f for f in files + if any(f["dataFile"]["filename"].lower().endswith(ext.lower()) for ext in file_types) + ] + logger.info(f"Filtered to {len(files)} files matching {file_types} (from {original_count} total)") + + # Download each file + downloaded = [] + failed = [] + + for i, file_info in enumerate(files, 1): + try: + file_id = file_info["dataFile"]["id"] + filename = file_info["dataFile"]["filename"] + checksum = file_info["dataFile"].get("md5") + + output_path = output_dir / filename + + logger.info(f"[{i}/{len(files)}] Downloading {filename}...") + + success = await self.download_file( + file_id, + output_path, + expected_checksum=checksum, + verify_checksum=verify_checksums + ) + + if success: + downloaded.append(str(output_path)) + else: + failed.append(filename) + + except Exception as e: + logger.error(f"Error downloading {filename}: {e}") + failed.append(filename) + + # Summary + status = "success" if not failed else ("partial" if downloaded else "error") + + logger.info("") + logger.info("=" * 60) + if status == "success": + logger.success(f"✓ Successfully downloaded all {len(downloaded)} files") + elif status == "partial": + logger.warning(f"⚠ Downloaded {len(downloaded)} files, {len(failed)} failed") + else: + logger.error(f"✗ All downloads failed") + logger.info("=" * 60) + + return { + "status": status, + "downloaded": len(downloaded), + "failed": len(failed), + "failed_files": failed, + "files": downloaded, + "output_dir": str(output_dir) + } + + async def search_datasets( + self, + query: str, + type: str = "dataset", + per_page: int = 10, + start: int = 0 + ) -> Dict[str, Any]: + """ + Search for datasets in Dataverse. + + Args: + query: Search query string + type: Type of results ("dataset", "datafile", "all") + per_page: Number of results per page + start: Starting offset for pagination + + Returns: + Search results dictionary + + Example: + results = await client.search_datasets("municipal meetings") + for item in results["data"]["items"]: + print(item["name"], item["global_id"]) + """ + url = f"{self.base_url}{self.SEARCH_ENDPOINT}" + params = { + "q": query, + "type": type, + "per_page": per_page, + "start": start + } + + try: + response = await self._request_with_retry( + "GET", + url, + params=params, + headers=self._get_headers() + ) + + return response.json() + + except DataverseAPIError as e: + logger.error(f"Search failed: {e}") + return {"status": "error", "message": str(e)} + + +# Convenience functions for common operations + +async def download_localview_dataset( + api_key: Optional[str] = None, + output_dir: Optional[Path] = None +) -> Dict[str, Any]: + """ + Download the LocalView dataset from Harvard Dataverse. + + This is the largest known database of municipal meeting videos. + + Args: + api_key: Optional Dataverse API key (recommended) + output_dir: Where to save files (defaults to data/cache/dataverse/localview) + + Returns: + Download summary dictionary + + Example: + result = await download_localview_dataset() + print(f"Downloaded {result['downloaded']} files") + """ + client = DataverseClient(api_key=api_key) + + logger.info("=" * 60) + logger.info("LocalView Dataset Download") + logger.info("=" * 60) + + result = await client.download_dataset( + persistent_id="doi:10.7910/DVN/NJTBEM", + output_dir=output_dir or Path("data/cache/localview"), + file_types=[".csv", ".tab", ".tsv"] # Only download data files + ) + + return result + + +# CLI for testing +async def main(): + """Test the Dataverse client.""" + import argparse + + parser = argparse.ArgumentParser(description="Dataverse API Client") + parser.add_argument("--api-key", help="Dataverse API key") + parser.add_argument("--dataset", default="doi:10.7910/DVN/NJTBEM", help="Dataset DOI") + parser.add_argument("--output", help="Output directory") + parser.add_argument("--metadata-only", action="store_true", help="Only fetch metadata") + + args = parser.parse_args() + + client = DataverseClient(api_key=args.api_key) + + if args.metadata_only: + # Just fetch metadata + metadata = await client.get_dataset_metadata(args.dataset) + if metadata: + print(json.dumps(metadata, indent=2)) + else: + # Download full dataset + output_dir = Path(args.output) if args.output else None + result = await client.download_dataset(args.dataset, output_dir) + + print("\nDownload Summary:") + print(f"Status: {result['status']}") + print(f"Downloaded: {result['downloaded']} files") + print(f"Failed: {result['failed']} files") + print(f"Output: {result['output_dir']}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/discovery/discovery_pipeline.py b/discovery/discovery_pipeline.py new file mode 100644 index 0000000000000000000000000000000000000000..05d263006ac5497503e8a01fe59e50a86d8f6039 --- /dev/null +++ b/discovery/discovery_pipeline.py @@ -0,0 +1,342 @@ +""" +Complete Jurisdiction Discovery Pipeline + +Orchestrates the full discovery workflow: +1. Ingest Census Bureau data (Bronze layer) +2. Download GSA .gov domain list +3. Run URL discovery for all jurisdictions +4. Validate and score results (Silver layer) +5. Create actionable scraping targets (Gold layer) + +This implements the Medallion Architecture for jurisdiction discovery. +""" +import asyncio +from typing import List, Dict, Any, Optional +from datetime import datetime +from loguru import logger + +try: + from pyspark.sql import SparkSession + from pyspark.sql.functions import col, lit, when + PYSPARK_AVAILABLE = True +except ImportError: + PYSPARK_AVAILABLE = False + SparkSession = None + +from config import settings + +from discovery.census_ingestion import CensusGovernmentIngestion +from discovery.gsa_domains import GSADomainList +from discovery.url_discovery_agent import URLDiscoveryAgent, JurisdictionURL + + +class DiscoveryPipeline: + """Orchestrate full jurisdiction discovery pipeline.""" + + def __init__(self): + """Initialize pipeline components.""" + # Configure Spark with Delta Lake support + # For local mode, we need to explicitly add delta-spark JARs + import delta + + builder = SparkSession.builder \ + .appName("JurisdictionDiscovery") \ + .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \ + .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog") + + # Use delta-spark's configure_spark_with_delta_pip to add JARs + self.spark = delta.configure_spark_with_delta_pip(builder).getOrCreate() + + self.census = CensusGovernmentIngestion(self.spark) + self.gsa = GSADomainList(self.spark) + + async def run_bronze_ingestion(self): + """ + BRONZE LAYER: Ingest raw data from Census and GSA. + + Tables created: + - bronze/jurisdictions/counties + - bronze/jurisdictions/municipalities + - bronze/jurisdictions/school_districts + - bronze/jurisdictions/special_districts + - bronze/jurisdictions/unified + - bronze/gov_domains + """ + logger.info("=" * 60) + logger.info("BRONZE LAYER: Ingesting raw jurisdiction data") + logger.info("=" * 60) + + # Step 1: Census data + logger.info("\n[1/2] Downloading Census Bureau data...") + census_dfs = await self.census.ingest_all_jurisdictions() + census_result = self.census.write_to_bronze_layer(census_dfs) + + # Step 2: GSA domains + logger.info("\n[2/2] Downloading GSA .gov domain list...") + gsa_csv = await self.gsa.download_domain_list() + gsa_df = self.gsa.parse_domains(gsa_csv) + gsa_result = self.gsa.write_to_bronze_layer(gsa_df) + + logger.success("✓ Bronze layer ingestion complete") + + return { + "status": "complete", + "total_records": census_result.get("total_records", 0) + gsa_result.get("total_records", 0), + "census_records": census_result.get("total_records", 0), + "gsa_domains": gsa_result.get("total_records", 0) + } + + async def run_url_discovery(self, limit: Optional[int] = None): + """ + SILVER LAYER: Discover URLs for jurisdictions. + + Args: + limit: Optional limit for testing (e.g., 100 jurisdictions) + + Table created: + - silver/discovered_urls + """ + logger.info("=" * 60) + logger.info("SILVER LAYER: URL Discovery") + logger.info("=" * 60) + + # Load municipalities table (has most complete data) + # Note: Census 2022 data is aggregated counts, not individual listings + # For production, we'd need a different data source for individual jurisdictions + bronze_path = f"{settings.delta_lake_path}/bronze/jurisdictions/municipalities" + + try: + jurisdictions_df = self.spark.read.format("delta").load(bronze_path) + logger.info(f"Loaded {jurisdictions_df.count()} jurisdiction records from Census data") + except Exception as e: + logger.error(f"Failed to load jurisdiction data: {e}") + logger.warning("Skipping URL discovery - no jurisdiction data available") + return {"urls_discovered": 0, "validated_domains": 0} + + # Apply limit if specified + if limit: + jurisdictions_df = jurisdictions_df.limit(limit) + + total_count = jurisdictions_df.count() + logger.info(f"Processing {total_count:,} jurisdiction records...") + + # Load GSA domains for validation + gsa_path = f"{settings.delta_lake_path}/bronze/gov_domains" + gsa_df = self.spark.read.format("delta").load(gsa_path) + gsa_rows = gsa_df.collect() + + # Extract domain set and full data (column names have underscores after cleaning) + gsa_domains = set(row["Domain_name"] if "Domain_name" in row.asDict() else row.asDict().get(list(row.asDict().keys())[0], "") for row in gsa_rows) + gsa_domain_data = [row.asDict() for row in gsa_rows] + + logger.info(f"Loaded {len(gsa_domains):,} .gov domains for validation") + + # Construct search patterns from jurisdiction names + # GSA domains typically follow patterns like: + # - "citystatecode.gov" (e.g., "aberdeenmd.gov" for Aberdeen, MD) + # - "city-state.gov" (e.g., "abingdon-va.gov") + # - "countystate.gov" (e.g., "alamedacountyca.gov") + # - "cityname.gov" (e.g., "abilenetx.gov") + + discovered_urls = [] + + for row in jurisdictions_df.take(limit if limit else total_count): + row_dict = row.asDict() + name = row_dict.get("name", "") + state_code = row_dict.get("state_code", "") + fips = row_dict.get("fips_code", "") + + if not name: + continue + + # Strip common jurisdiction type suffixes + clean_name = name + for suffix in [" city", " town", " village", " borough", " CDP", " County", " municipality", + " City", " Town", " Village", " Borough", " COUNTY"]: + clean_name = clean_name.replace(suffix, "") + + # Normalize to lowercase and remove special characters + base_name = clean_name.lower().strip() + # Remove all spaces, periods, commas, apostrophes for compact form + compact_name = base_name.replace(" ", "").replace(".", "").replace(",", "").replace("'", "") + # Version with hyphens instead of spaces + hyphenated_name = base_name.replace(" ", "-").replace(".", "").replace(",", "").replace("'", "") + + state_lower = state_code.lower() + + # Try multiple domain patterns (most common first) + candidate_domains = [ + f"{compact_name}{state_lower}.gov", # aberdeenmd.gov + f"{compact_name}-{state_lower}.gov", # abingdon-va.gov + f"{state_lower}{compact_name}.gov", # mdaberdeen.gov + f"{hyphenated_name}{state_lower}.gov", # multi-word-cityal.gov + f"{hyphenated_name}-{state_lower}.gov", # multi-word-city-al.gov + f"{compact_name}.gov", # abilene.gov + f"{hyphenated_name}.gov", # multi-word.gov + f"cityof{compact_name}.gov", # cityofabilene.gov + f"{compact_name}city.gov", # aberdeencity.gov + f"{compact_name}county.gov", # alamedacounty.gov + f"{compact_name}county{state_lower}.gov", # alamedacountyca.gov + ] + + # Check if any candidate matches GSA domains + for domain in candidate_domains: + if domain in gsa_domains: + discovered_urls.append({ + "jurisdiction_name": name, + "state_code": state_code, + "fips_code": fips, + "url": f"https://{domain}", + "domain": domain, + "source": "gsa_match", + "confidence": "high" + }) + break + + logger.info(f"Discovered {len(discovered_urls):,} URLs from GSA domain matching") + + # Write discovered URLs to Silver layer + if discovered_urls: + from pyspark.sql import Row + urls_df = self.spark.createDataFrame([Row(**url) for url in discovered_urls]) + silver_path = f"{settings.delta_lake_path}/silver/discovered_urls" + urls_df.write.format("delta").mode("overwrite").save(silver_path) + logger.success(f"Wrote {len(discovered_urls):,} discovered URLs to Silver layer") + + return { + "census_records": total_count, + "gov_domains": len(gsa_domains), + "discovered_urls": len(discovered_urls) + } + + def create_scraping_targets(self): + """ + GOLD LAYER: Create actionable scraping targets. + + Filters for high-quality targets: + - Has minutes URL + - High confidence score (>0.6) + - Preferably .gov domain + + Table created: + - gold/scraping_targets + """ + logger.info("=" * 60) + logger.info("GOLD LAYER: Creating scraping targets") + logger.info("=" * 60) + + # Check if Silver layer exists + from pathlib import Path + silver_path = f"{settings.delta_lake_path}/silver/discovered_urls" + if not Path(silver_path).exists(): + logger.warning("Silver layer (discovered URLs) does not exist") + logger.info("Skipping Gold layer - requires Silver layer URL data") + return {"status": "skipped", "reason": "no_silver_layer"} + + # Load discovered URLs - these already have jurisdiction details + urls_df = self.spark.read.format("delta").load(silver_path) + + # Create scraping targets from discovered URLs + # Silver layer already has: jurisdiction_name, state_code, fips_code, url, domain, source, confidence + targets_df = urls_df \ + .withColumn("scraping_status", lit("pending")) \ + .withColumn("last_scraped", lit(None).cast("timestamp")) \ + .withColumn("documents_found", lit(0)) \ + .withColumn("created_at", lit(datetime.now().isoformat())) + + # Prioritization score based on confidence and source + targets_df = targets_df.withColumn( + "priority_score", + when(col("source") == "gsa_match", 100).otherwise(50) + + when(col("confidence") == "high", 50).otherwise(25) + ) + + # Write to Gold layer + gold_path = f"{settings.delta_lake_path}/gold/scraping_targets" + targets_df.write \ + .format("delta") \ + .mode("overwrite") \ + .partitionBy("state_code") \ + .save(gold_path) + + # Statistics + logger.success("✓ Scraping targets created:") + + total = targets_df.count() + high_priority = targets_df.filter(col("priority_score") >= 150).count() + medium_priority = targets_df.filter((col("priority_score") >= 100) & (col("priority_score") < 150)).count() + low_priority = targets_df.filter(col("priority_score") < 100).count() + + logger.info(f"\n TOTAL: {total:,} ready for scraping") + logger.info(f" High priority (≥150): {high_priority:,}") + logger.info(f" Medium priority (100-149): {medium_priority:,}") + logger.info(f" Low priority (<100): {low_priority:,}") + + return { + "targets_created": total, + "high_priority": high_priority, + "medium_priority": medium_priority, + "low_priority": low_priority + } + + async def run_full_pipeline(self, discovery_limit: Optional[int] = None, + state_filter: Optional[str] = None, + type_filter: Optional[str] = None): + """ + Execute complete discovery pipeline. + + Args: + discovery_limit: Limit URL discovery for testing + state_filter: Filter to single state (e.g., "CA") + type_filter: Filter to jurisdiction type (e.g., "county") + + Returns: + Dictionary with complete pipeline statistics + """ + start_time = datetime.now() + + logger.info("\n" + "=" * 60) + logger.info("JURISDICTION DISCOVERY PIPELINE") + logger.info("=" * 60 + "\n") + + try: + # Bronze Layer + bronze_stats = await self.run_bronze_ingestion() + + # Silver Layer (with optional filters) + # Note: Filters would need to be added to run_url_discovery method + discovery_stats = await self.run_url_discovery(limit=discovery_limit) + + # Gold Layer + gold_stats = self.create_scraping_targets() + + elapsed = (datetime.now() - start_time).total_seconds() + logger.success(f"\n{'=' * 60}") + logger.success(f"PIPELINE COMPLETE in {elapsed:.1f}s") + logger.success(f"{'=' * 60}\n") + + return { + "bronze_records": bronze_stats.get("total_records", 0), + "urls_discovered": discovery_stats.get("successful", 0) if discovery_stats else 0, + "scraping_targets": gold_stats.get("targets_created", 0) if gold_stats else 0, + "elapsed_seconds": elapsed, + "bronze_status": bronze_stats.get("status", "complete"), + "silver_status": discovery_stats.get("status", "skipped") if discovery_stats else "skipped", + "gold_status": gold_stats.get("status", "skipped") if gold_stats else "skipped" + } + + except Exception as e: + logger.error(f"Pipeline failed: {e}") + raise + + +async def main(): + """Run discovery pipeline.""" + pipeline = DiscoveryPipeline() + + # Run with limit for testing (remove limit for production) + await pipeline.run_full_pipeline(discovery_limit=100) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/discovery/external_url_datasets.py b/discovery/external_url_datasets.py new file mode 100644 index 0000000000000000000000000000000000000000..1e5b786c981000502897fada27dc876bc7e9730d --- /dev/null +++ b/discovery/external_url_datasets.py @@ -0,0 +1,499 @@ +""" +Integrate existing URL datasets from civic tech projects. + +Instead of trying to match Census names to domains (15% success rate), +we download pre-existing URL lists from: +1. LocalView (1,000-10,000 jurisdictions) +2. Council Data Project (20+ cities) +3. City Scrapers (100-500 agencies) +4. Legistar subdomain enumeration (1,000-3,000) + +This gives us 7,000-20,000 URLs vs. our current 76. +""" +import json +import httpx +from pathlib import Path +from typing import List, Dict +from datetime import datetime + +from pyspark.sql import SparkSession +from loguru import logger + +from config.settings import settings + + +# ============================================================================ +# Council Data Project Deployments (Confirmed 20+ locations) +# ============================================================================ + +CDP_DEPLOYMENTS = [ + { + "jurisdiction_name": "Seattle", + "state_code": "WA", + "cdp_url": "https://councildataproject.org/seattle", + "source_url": "https://seattle.gov/city-council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "King County", + "state_code": "WA", + "cdp_url": "https://councildataproject.org/king-county", + "source_url": "https://kingcounty.gov/council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Portland", + "state_code": "OR", + "cdp_url": "https://councildataproject.org/portland", + "source_url": "https://www.portland.gov/council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Denver", + "state_code": "CO", + "cdp_url": "https://councildataproject.org/denver", + "source_url": "https://www.denvergov.org/Government/Agencies-Departments-Offices/City-Council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Boston", + "state_code": "MA", + "cdp_url": "https://councildataproject.org/boston", + "source_url": "https://www.boston.gov/departments/city-council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Oakland", + "state_code": "CA", + "cdp_url": "https://councildataproject.org/oakland", + "source_url": "https://www.oaklandca.gov/departments/city-council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Charlotte", + "state_code": "NC", + "cdp_url": "https://councildataproject.org/charlotte", + "source_url": "https://www.charlottenc.gov/city-government/city-council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "San José", + "state_code": "CA", + "cdp_url": "https://councildataproject.org/san-jose", + "source_url": "https://www.sanjoseca.gov/your-government/city-council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Milwaukee", + "state_code": "WI", + "cdp_url": "https://councildataproject.org/milwaukee", + "source_url": "https://milwaukee.gov/CommonCouncil", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Louisville", + "state_code": "KY", + "cdp_url": "https://councildataproject.org/louisville", + "source_url": "https://louisvilleky.gov/government/metro-council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Atlanta", + "state_code": "GA", + "cdp_url": "https://councildataproject.org/atlanta", + "source_url": "https://www.atlantaga.gov/government/city-council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Pittsburgh", + "state_code": "PA", + "cdp_url": "https://councildataproject.org/pittsburgh-pa", + "source_url": "https://pittsburghpa.gov/council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Alameda", + "state_code": "CA", + "cdp_url": "https://councildataproject.org/alameda", + "source_url": "https://www.alamedaca.gov/Departments/City-Council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Mountain View", + "state_code": "CA", + "cdp_url": "https://councildataproject.org/mountain-view", + "source_url": "https://www.mountainview.gov/city-hall/departments/city-council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Long Beach", + "state_code": "CA", + "cdp_url": "https://councildataproject.org/long-beach", + "source_url": "https://www.longbeach.gov/city-council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Albuquerque", + "state_code": "NM", + "cdp_url": "https://councildataproject.org/albuquerque", + "source_url": "https://www.cabq.gov/council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Richmond", + "state_code": "VA", + "cdp_url": "https://councildataproject.org/richmond", + "source_url": "https://www.rva.gov/city-council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "excellent" + }, + { + "jurisdiction_name": "Asheville", + "state_code": "NC", + "cdp_url": "https://sunshine-request.github.io/cdp-asheville/", + "source_url": "https://www.ashevillenc.gov/department/city-council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "good" + }, + { + "jurisdiction_name": "Missoula", + "state_code": "MT", + "cdp_url": "https://www.openmontana.org/missoula-council-data-project", + "source_url": "https://www.ci.missoula.mt.us/government/mayor-city-council/city-council", + "has_transcripts": True, + "has_videos": True, + "data_quality": "good" + }, +] + + +# ============================================================================ +# City Scrapers Known Agencies +# ============================================================================ + +CITY_SCRAPERS_AGENCIES = { + "Chicago, IL": [ + "https://www.chicago.gov/city/en/depts/cdph.html", # Board of Health + "https://www.chicago.gov/city/en/depts/dol.html", # Board of Education + "https://www.chicago.gov/city/en/depts/dcd.html", # Planning Commission + # ... Chicago has ~100 agencies + ], + "Pittsburgh, PA": [ + "https://pittsburghpa.gov/council", + # ... more agencies + ], + # TODO: Clone city-scrapers repos and extract all URLs +} + + +# ============================================================================ +# Legistar Known Cities +# ============================================================================ + +KNOWN_LEGISTAR_CITIES = [ + {"name": "Chicago", "state": "IL", "url": "https://chicago.legistar.com"}, + {"name": "Seattle", "state": "WA", "url": "https://seattle.legistar.com"}, + {"name": "Los Angeles", "state": "CA", "url": "https://losangeles.legistar.com"}, + {"name": "Boston", "state": "MA", "url": "https://boston.legistar.com"}, + {"name": "Phoenix", "state": "AZ", "url": "https://phoenix.legistar.com"}, + {"name": "San Diego", "state": "CA", "url": "https://sandiego.legistar.com"}, + {"name": "Austin", "state": "TX", "url": "https://austin.legistar.com"}, + # TODO: Enumerate more by testing all Census jurisdictions +] + + +# ============================================================================ +# Integration Functions +# ============================================================================ + +def load_cdp_deployments_to_bronze(spark: SparkSession) -> dict: + """ + Load CDP deployments to Bronze layer. + + These are premium jurisdictions with full transcript/video pipelines. + """ + logger.info(f"Loading {len(CDP_DEPLOYMENTS)} CDP deployments to Bronze layer") + + # Convert to DataFrame + df = spark.createDataFrame(CDP_DEPLOYMENTS) + + # Add metadata + df = df.withColumn("source", "council_data_project") + df = df.withColumn("ingested_at", df.lit(datetime.utcnow().isoformat())) + df = df.withColumn("priority_score", df.lit(200)) # Very high priority + + # Write to Bronze layer + output_path = f"{settings.delta_lake_path}/bronze/cdp_deployments" + df.write \ + .format("delta") \ + .mode("overwrite") \ + .save(output_path) + + logger.info(f"✅ Wrote {len(CDP_DEPLOYMENTS)} CDP deployments to {output_path}") + + return { + "total_records": len(CDP_DEPLOYMENTS), + "source": "council_data_project", + "quality": "excellent" + } + + +async def download_localview_dataset() -> dict: + """ + Download LocalView dataset from Harvard Dataverse. + + This is the largest known database of local government meetings. + """ + logger.info("Downloading LocalView dataset from Harvard Dataverse") + + # Harvard Dataverse API + dataverse_api = "https://dataverse.harvard.edu/api/datasets/:persistentId/" + dataset_doi = "doi:10.7910/DVN/NJTBEM" + + # Get dataset metadata + async with httpx.AsyncClient(timeout=120.0) as client: + try: + response = await client.get( + dataverse_api, + params={"persistentId": dataset_doi} + ) + + if response.status_code == 200: + metadata = response.json() + + # Extract file download URLs + files = metadata.get("data", {}).get("latestVersion", {}).get("files", []) + + logger.info(f"Found {len(files)} files in LocalView dataset") + + # Download each file + cache_dir = Path("data/cache/localview") + cache_dir.mkdir(parents=True, exist_ok=True) + + downloaded_files = [] + for file_info in files: + file_id = file_info["dataFile"]["id"] + filename = file_info["dataFile"]["filename"] + + download_url = f"https://dataverse.harvard.edu/api/access/datafile/{file_id}" + + logger.info(f"Downloading {filename}...") + file_response = await client.get(download_url) + + if file_response.status_code == 200: + output_file = cache_dir / filename + output_file.write_bytes(file_response.content) + downloaded_files.append(str(output_file)) + logger.info(f"✅ Downloaded {filename}") + + return { + "status": "success", + "files_downloaded": len(downloaded_files), + "files": downloaded_files, + "cache_dir": str(cache_dir) + } + + else: + logger.error(f"Failed to fetch dataset metadata: {response.status_code}") + return {"status": "error", "message": f"HTTP {response.status_code}"} + + except Exception as e: + logger.error(f"Error downloading LocalView dataset: {e}") + return {"status": "error", "message": str(e)} + + +def enumerate_legistar_subdomains( + spark: SparkSession, + jurisdictions_df = None +) -> List[str]: + """ + Enumerate Legistar subdomains by testing jurisdiction names. + + Pattern: {city}.legistar.com, {city}-{state}.legistar.com + """ + logger.info("Enumerating Legistar subdomains") + + if jurisdictions_df is None: + # Load from Bronze layer + jurisdictions_df = spark.read.format("delta").load( + f"{settings.delta_lake_path}/bronze/census_jurisdictions" + ) + + # Get municipalities only (most likely to use Legistar) + municipalities = jurisdictions_df.filter( + jurisdictions_df["jurisdiction_type"] == "municipality" + ).collect() + + found_urls = [] + + async def test_legistar_url(url: str) -> bool: + """Test if a Legistar URL exists.""" + async with httpx.AsyncClient(timeout=10.0) as client: + try: + response = await client.head(url) + return response.status_code == 200 + except: + return False + + # Test patterns for each jurisdiction + import asyncio + + async def test_all(): + for row in municipalities[:100]: # Test first 100 for demo + name = row["name"].lower().replace(" ", "").replace("city", "") + state = row["state_code"].lower() + + # Generate test URLs + test_urls = [ + f"https://{name}.legistar.com", + f"https://{name}-{state}.legistar.com", + f"https://{name}{state}.legistar.com", + ] + + # Test each URL + for url in test_urls: + if await test_legistar_url(url): + found_urls.append({ + "jurisdiction_name": row["name"], + "state_code": row["state_code"], + "url": url, + "platform": "legistar" + }) + logger.info(f"✅ Found: {url}") + break + + # Run async tests + asyncio.run(test_all()) + + logger.info(f"Found {len(found_urls)} Legistar URLs") + + return found_urls + + +# ============================================================================ +# Main Integration Function +# ============================================================================ + +def integrate_external_url_datasets(spark: SparkSession = None) -> dict: + """ + Integrate all external URL datasets into Bronze layer. + + Priority order: + 1. CDP deployments (20+ premium jurisdictions) + 2. LocalView dataset (1,000-10,000 jurisdictions) + 3. City Scrapers agencies (100-500 URLs) + 4. Legistar enumeration (1,000-3,000 URLs) + """ + from delta import configure_spark_with_delta_pip + + if spark is None: + builder = SparkSession.builder \ + .appName("IntegrateExternalURLs") \ + .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \ + .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog") + spark = configure_spark_with_delta_pip(builder).getOrCreate() + + results = { + "cdp_deployments": 0, + "localview_dataset": 0, + "legistar_urls": 0, + "total_new_urls": 0 + } + + # 1. Load CDP deployments + logger.info("=" * 80) + logger.info("STEP 1: Loading CDP Deployments") + logger.info("=" * 80) + cdp_result = load_cdp_deployments_to_bronze(spark) + results["cdp_deployments"] = cdp_result["total_records"] + + # 2. Download LocalView dataset + logger.info("\n" + "=" * 80) + logger.info("STEP 2: Downloading LocalView Dataset") + logger.info("=" * 80) + logger.info("⚠️ Note: This requires manual download from Harvard Dataverse") + logger.info("Visit: https://dataverse.harvard.edu/dataset.xhtml?persistentId=doi:10.7910/DVN/NJTBEM") + logger.info("Download files and place in: data/cache/localview/") + + # 3. Enumerate Legistar subdomains + logger.info("\n" + "=" * 80) + logger.info("STEP 3: Enumerating Legistar Subdomains") + logger.info("=" * 80) + legistar_urls = enumerate_legistar_subdomains(spark) + results["legistar_urls"] = len(legistar_urls) + + # Save Legistar URLs to Bronze + if legistar_urls: + legistar_df = spark.createDataFrame(legistar_urls) + legistar_df = legistar_df.withColumn("source", legistar_df.lit("legistar_enumeration")) + legistar_df.write \ + .format("delta") \ + .mode("overwrite") \ + .save(f"{settings.delta_lake_path}/bronze/legistar_urls") + + # Calculate totals + results["total_new_urls"] = sum([ + results["cdp_deployments"], + results["legistar_urls"] + ]) + + logger.info("\n" + "=" * 80) + logger.info("INTEGRATION COMPLETE") + logger.info("=" * 80) + logger.info(f"CDP deployments: {results['cdp_deployments']}") + logger.info(f"Legistar URLs: {results['legistar_urls']}") + logger.info(f"Total new URLs: {results['total_new_urls']}") + logger.info("\n⚠️ Don't forget to download LocalView dataset manually!") + + return results + + +if __name__ == "__main__": + print("🔗 Integrating External URL Datasets") + print("=" * 80) + print("\nThis script integrates pre-existing URL lists from:") + print(" 1. Council Data Project (20+ cities)") + print(" 2. LocalView (1,000-10,000 jurisdictions)") + print(" 3. Legistar enumeration (1,000-3,000 cities)") + print("\nInstead of trying to discover URLs ourselves (15% success),") + print("we leverage work already done by the civic tech community.\n") + + results = integrate_external_url_datasets() + + print("\n✅ Integration complete!") + print(f"\nTotal URLs added: {results['total_new_urls']}") + print("\nNext: Download LocalView dataset from Harvard Dataverse") diff --git a/discovery/google_data_commons.py b/discovery/google_data_commons.py new file mode 100644 index 0000000000000000000000000000000000000000..9856786502d4c10f93e865e46039f69de74073ec --- /dev/null +++ b/discovery/google_data_commons.py @@ -0,0 +1,320 @@ +""" +Google Data Commons Integration for Jurisdiction Enrichment + +Uses Google Data Commons Knowledge Graph API to enrich jurisdiction data with: +- Demographics (population, age, gender, race/ethnicity) +- Economic indicators (income, employment, poverty) +- Education levels +- Health insurance coverage +- Housing characteristics + +Installation: + pip install datacommons datacommons-pandas + +Documentation: + https://docs.datacommons.org/api/ + https://datacommons.org/tools/statvar + +Citation: + Google LLC. Data Commons. https://datacommons.org/ +""" +from typing import List, Dict, Any, Optional +import pandas as pd +from loguru import logger + +try: + import datacommons as dc + import datacommons_pandas as dcpd + DATACOMMONS_AVAILABLE = True +except ImportError: + logger.warning("datacommons not installed. Run: pip install datacommons datacommons-pandas") + DATACOMMONS_AVAILABLE = False + + +class DataCommonsClient: + """ + Client for enriching jurisdiction data with Google Data Commons variables. + + Replaces manual U.S. Census API calls with simplified Data Commons API. + """ + + # Standard statistical variables for jurisdictions + DEMOGRAPHIC_VARS = [ + "Count_Person", # Total population + "Count_Person_Male", # Male population + "Count_Person_Female", # Female population + "Median_Age_Person", # Median age + "Count_Person_WhiteAlone", # White population + "Count_Person_BlackOrAfricanAmericanAlone", # Black population + "Count_Person_HispanicOrLatino", # Hispanic/Latino + "Count_Person_AsianAlone", # Asian population + ] + + ECONOMIC_VARS = [ + "Median_Income_Household", # Median household income + "UnemploymentRate_Person", # Unemployment rate + "Count_Person_BelowPovertyLevelInThePast12Months", # Poverty count + "Median_Earnings_Person", # Median earnings + ] + + EDUCATION_VARS = [ + "Count_Person_EducationalAttainmentBachelorsDegreeOrHigher", # College graduates + "Count_Person_EducationalAttainmentHighSchoolGraduateOrHigher", # HS graduates + ] + + HEALTH_VARS = [ + "Count_Person_WithHealthInsurance", # Insured population + "Count_Person_NoHealthInsurance", # Uninsured population + ] + + HOUSING_VARS = [ + "Median_Price_SoldHome", # Median home price + "Count_HousingUnit", # Total housing units + "Count_Household", # Total households + ] + + ALL_VARS = ( + DEMOGRAPHIC_VARS + + ECONOMIC_VARS + + EDUCATION_VARS + + HEALTH_VARS + + HOUSING_VARS + ) + + def __init__(self): + """Initialize the Data Commons client.""" + if not DATACOMMONS_AVAILABLE: + raise ImportError( + "datacommons package not installed. " + "Install with: pip install datacommons datacommons-pandas" + ) + + def get_place_dcid(self, fips_code: str, place_type: str = "County") -> str: + """ + Convert FIPS code to Data Commons ID (DCID). + + Args: + fips_code: 5-digit FIPS code (state+county) or 7-digit (state+place) + place_type: "County" or "City" + + Returns: + DCID like "geoId/01073" for Jefferson County, AL + + Examples: + >>> client = DataCommonsClient() + >>> client.get_place_dcid("01073", "County") + 'geoId/01073' + >>> client.get_place_dcid("0107000", "City") # Birmingham, AL + 'geoId/0107000' + """ + return f"geoId/{fips_code}" + + def enrich_jurisdiction( + self, + fips_code: str, + variables: Optional[List[str]] = None, + year: Optional[int] = None + ) -> Dict[str, Any]: + """ + Enrich a jurisdiction with Data Commons variables. + + Args: + fips_code: 5-digit (county) or 7-digit (city) FIPS code + variables: List of statistical variables (default: ALL_VARS) + year: Optional year filter (default: most recent) + + Returns: + Dictionary of {variable: value} + + Example: + >>> client = DataCommonsClient() + >>> data = client.enrich_jurisdiction("01073") # Jefferson County, AL + >>> print(data["Median_Income_Household"]) + 65000 + """ + if variables is None: + variables = self.ALL_VARS + + dcid = self.get_place_dcid(fips_code) + + try: + # Get latest observation for each variable + observations = dc.get_stat_value(dcid, variables) + + result = { + "fips_code": fips_code, + "dcid": dcid, + "data_source": "Google Data Commons", + "retrieval_date": pd.Timestamp.now().isoformat(), + } + + # Add statistical variables + for var in variables: + result[var] = observations.get(var) + + return result + + except Exception as e: + logger.error(f"Error enriching {fips_code}: {e}") + return {"fips_code": fips_code, "error": str(e)} + + def enrich_jurisdictions_bulk( + self, + fips_codes: List[str], + variables: Optional[List[str]] = None + ) -> pd.DataFrame: + """ + Enrich multiple jurisdictions in bulk. + + Args: + fips_codes: List of FIPS codes + variables: List of statistical variables + + Returns: + DataFrame with one row per jurisdiction + + Example: + >>> client = DataCommonsClient() + >>> fips_codes = ["01073", "01089", "01097"] # 3 AL counties + >>> df = client.enrich_jurisdictions_bulk(fips_codes) + >>> print(df[["fips_code", "Count_Person", "Median_Income_Household"]]) + """ + if variables is None: + variables = self.ALL_VARS + + dcids = [self.get_place_dcid(fips) for fips in fips_codes] + + try: + # Use datacommons_pandas for efficient bulk retrieval + df = dcpd.build_multivariate( + dcids=dcids, + stat_vars=variables + ) + + # Add FIPS codes + df["fips_code"] = fips_codes + df["data_source"] = "Google Data Commons" + df["retrieval_date"] = pd.Timestamp.now().isoformat() + + return df + + except Exception as e: + logger.error(f"Error enriching bulk jurisdictions: {e}") + return pd.DataFrame({"error": [str(e)]}) + + def get_time_series( + self, + fips_code: str, + variables: Optional[List[str]] = None, + start_year: int = 2010, + end_year: int = 2023 + ) -> pd.DataFrame: + """ + Get time series data for a jurisdiction. + + Args: + fips_code: FIPS code + variables: Statistical variables (default: economic indicators) + start_year: Start year + end_year: End year + + Returns: + DataFrame with time series (date index) + + Example: + >>> client = DataCommonsClient() + >>> df = client.get_time_series("01073", start_year=2015) + >>> df.plot(y="Median_Income_Household") + """ + if variables is None: + variables = self.ECONOMIC_VARS + + dcid = self.get_place_dcid(fips_code) + + try: + df = dcpd.build_time_series( + place=dcid, + stat_vars=variables + ) + + # Filter by year range + df = df.loc[f"{start_year}":f"{end_year}"] + + return df + + except Exception as e: + logger.error(f"Error getting time series for {fips_code}: {e}") + return pd.DataFrame({"error": [str(e)]}) + + def search_variables(self, query: str) -> List[Dict[str, str]]: + """ + Search for available statistical variables. + + Args: + query: Search query (e.g., "income", "education", "health") + + Returns: + List of {dcid, name, description} + + Example: + >>> client = DataCommonsClient() + >>> vars = client.search_variables("dental health") + >>> for v in vars: + ... print(v["dcid"], v["name"]) + """ + try: + results = dc.search_statvar(query, max_results=50) + return [ + { + "dcid": r.dcid, + "name": getattr(r, 'name', r.dcid), + "description": getattr(r, 'description', '') + } + for r in results + ] + except Exception as e: + logger.error(f"Error searching variables: {e}") + return [] + + +def example_usage(): + """Example usage of Data Commons integration.""" + client = DataCommonsClient() + + # Example 1: Enrich a single county + print("Example 1: Jefferson County, AL (FIPS 01073)") + data = client.enrich_jurisdiction("01073") + print(f"Population: {data.get('Count_Person')}") + print(f"Median Income: ${data.get('Median_Income_Household')}") + print(f"Unemployment Rate: {data.get('UnemploymentRate_Person')}%") + print() + + # Example 2: Bulk enrich multiple counties + print("Example 2: Top 3 AL counties by population") + fips_codes = ["01073", "01089", "01097"] # Jefferson, Madison, Mobile + df = client.enrich_jurisdictions_bulk(fips_codes) + print(df[["fips_code", "Count_Person", "Median_Income_Household"]]) + print() + + # Example 3: Time series + print("Example 3: Income trends for Birmingham, AL") + df_ts = client.get_time_series( + "0107000", # Birmingham city + variables=["Median_Income_Household"], + start_year=2015 + ) + print(df_ts) + print() + + # Example 4: Search for dental health variables + print("Example 4: Search for dental health variables") + vars = client.search_variables("dental health") + for v in vars[:5]: + print(f" - {v['dcid']}: {v['name']}") + + +if __name__ == "__main__": + if DATACOMMONS_AVAILABLE: + example_usage() + else: + print("Install datacommons: pip install datacommons datacommons-pandas") diff --git a/discovery/gsa_domains.py b/discovery/gsa_domains.py new file mode 100644 index 0000000000000000000000000000000000000000..3add616a19faa392de6b11d0e09fdb363e724bbf --- /dev/null +++ b/discovery/gsa_domains.py @@ -0,0 +1,199 @@ +""" +GSA .gov Domain List Integration + +Downloads and processes the GSA's public list of all registered .gov domains +to identify official government websites. + +Data Source: https://github.com/cisagov/dotgov-data +""" +import asyncio +from typing import List, Dict, Any, Set, Optional +from datetime import datetime +from pathlib import Path +import httpx +import csv +from loguru import logger + +try: + from pyspark.sql import SparkSession, DataFrame + from pyspark.sql.types import StructType, StructField, StringType, BooleanType + PYSPARK_AVAILABLE = True +except ImportError: + PYSPARK_AVAILABLE = False + SparkSession = None + DataFrame = None + +from config import settings + + +class GSADomainList: + """Process GSA .gov domain registry.""" + + # GSA maintains this on GitHub + DOMAIN_LIST_URL = "https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv" + + def __init__(self, spark: Optional[SparkSession] = None): + """Initialize with Spark session.""" + self.spark = spark or SparkSession.builder.appName("GSADomains").getOrCreate() + self.cache_dir = Path("data/cache/gsa") + self.cache_dir.mkdir(parents=True, exist_ok=True) + + async def download_domain_list(self) -> Path: + """ + Download latest .gov domain list from GSA. + + Returns: + Path to downloaded CSV file + """ + cache_file = self.cache_dir / f"dotgov_domains_{datetime.now().strftime('%Y%m%d')}.csv" + + # Use cached if recent (< 1 day old) + if cache_file.exists() and (datetime.now().timestamp() - cache_file.stat().st_mtime) < 86400: + logger.info(f"Using cached GSA domain list from {cache_file}") + return cache_file + + logger.info("Downloading .gov domain list from GSA...") + + async with httpx.AsyncClient(timeout=30.0) as client: + response = await client.get(self.DOMAIN_LIST_URL) + response.raise_for_status() + + cache_file.write_bytes(response.content) + logger.success(f"Downloaded {len(response.content)} bytes") + + return cache_file + + def parse_domains(self, csv_path: Path) -> DataFrame: + """ + Parse GSA domain CSV into Spark DataFrame. + + CSV columns: + - Domain name + - Domain type (Federal, State, County, City, etc.) + - Agency + - Organization + - City + - State + - Security contact email + + Returns: + Spark DataFrame with .gov domains + """ + logger.info(f"Parsing GSA domain list from {csv_path}...") + + df = self.spark.read.csv( + str(csv_path), + header=True, + inferSchema=True + ) + + # Clean column names (Delta Lake doesn't allow spaces or special chars) + for col_name in df.columns: + clean_name = col_name.replace(" ", "_").replace("(", "").replace(")", "").replace(",", "_") + if clean_name != col_name: + from pyspark.sql.functions import col + df = df.withColumnRenamed(col_name, clean_name) + + # Filter for local government domains (using cleaned column name) + local_gov_types = ["City", "County", "Township", "Special District", "School District"] + + if "Domain_Type" in df.columns: + df_local = df.filter(df["Domain_Type"].isin(local_gov_types)) + else: + # If Domain_Type column doesn't exist, return all domains + logger.warning("Domain_Type column not found, returning all domains") + df_local = df + + logger.success(f"Found {df_local.count():,} local government domains") + return df_local + + def create_domain_index(self, df: DataFrame) -> Dict[str, List[str]]: + """ + Create fast lookup index of domains by jurisdiction. + + Returns: + Dictionary mapping (state, city/county) to list of domains + """ + logger.info("Creating domain lookup index...") + + index = {} + + for row in df.collect(): + state = row["State"] + org = row["Organization"] or row["City"] + domain = row["Domain Name"] + + if state and org: + key = f"{state}_{org}".lower().replace(" ", "_") + if key not in index: + index[key] = [] + index[key].append(domain) + + logger.success(f"Indexed {len(index):,} jurisdiction-domain mappings") + return index + + def write_to_bronze_layer(self, df: DataFrame): + """ + Write .gov domain list to Delta Lake. + + Args: + df: DataFrame with domain data + """ + logger.info("Writing .gov domains to Bronze layer...") + + bronze_path = f"{settings.delta_lake_path}/bronze/gov_domains" + + # Check if partition columns exist (after column name cleaning, they'll have underscores) + partition_cols = [] + if "Domain_Type" in df.columns: + partition_cols.append("Domain_Type") + if "State" in df.columns: + partition_cols.append("State") + + if partition_cols: + df.write \ + .format("delta") \ + .mode("overwrite") \ + .partitionBy(*partition_cols) \ + .save(bronze_path) + else: + # Write without partitioning if columns don't exist + df.write \ + .format("delta") \ + .mode("overwrite") \ + .save(bronze_path) + + total_records = df.count() + logger.success(f"Wrote domains to {bronze_path}") + + return { + "total_records": total_records, + "tables": 1 + } + + +async def main(): + """Run GSA domain ingestion.""" + gsa = GSADomainList() + + # Download + csv_path = await gsa.download_domain_list() + + # Parse + df = gsa.parse_domains(csv_path) + + # Create index + index = gsa.create_domain_index(df) + + # Write to Delta Lake + gsa.write_to_bronze_layer(df) + + # Print statistics + print(f"\nGSA .gov Domain Statistics:") + print(f" Total local gov domains: {df.count():,}") + print(f" Unique states: {df.select('State').distinct().count()}") + print(f" Indexed jurisdictions: {len(index):,}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/discovery/nonprofit_discovery.py b/discovery/nonprofit_discovery.py new file mode 100644 index 0000000000000000000000000000000000000000..a0d30b9212f6f49fc6455844fe3a8634e218e657 --- /dev/null +++ b/discovery/nonprofit_discovery.py @@ -0,0 +1,692 @@ +""" +Nonprofit Discovery Module +Automated discovery and enrichment of nonprofits and churches using free APIs + +Data Sources: +1. ProPublica Nonprofit Explorer API - Financial data, EIN, NTEE codes +2. IRS Tax Exempt Organization Search - Official tax-exempt status +3. Every.org Charity API - Mission statements, logos +4. Findhelp.org (Aunt Bertha) - Local services directory +5. 211 Directory - Regional social services +""" + +import requests +import pandas as pd +from pathlib import Path +from typing import Dict, List, Optional +import json +import time +from datetime import datetime +from loguru import logger + + +class NonprofitDiscovery: + """Discover and enrich nonprofit data from multiple free sources""" + + def __init__(self, cache_dir: str = "data/cache/nonprofits"): + self.cache_dir = Path(cache_dir) + self.cache_dir.mkdir(parents=True, exist_ok=True) + + # Rate limiting + self.last_request_time = {} + self.min_request_interval = 1.0 # seconds between requests + + def _rate_limit(self, source: str): + """Rate limiting to be respectful to free APIs""" + if source in self.last_request_time: + elapsed = time.time() - self.last_request_time[source] + if elapsed < self.min_request_interval: + time.sleep(self.min_request_interval - elapsed) + self.last_request_time[source] = time.time() + + # ========================================================================= + # 1. ProPublica Nonprofit Explorer API + # ========================================================================= + + def search_propublica( + self, + state: str = "AL", + ntee_code: Optional[str] = None, + city: Optional[str] = None + ) -> List[Dict]: + """ + Search ProPublica Nonprofit Explorer API + + API Docs: https://projects.propublica.org/nonprofits/api + + NOTE: NTEE filtering is done CLIENT-SIDE due to ProPublica API bug + that causes 500 errors when using ntee[id] parameter. + + Args: + state: 2-letter state code (e.g., "AL") + ntee_code: NTEE major group (e.g., "E" for health) - filtered client-side + city: City name (e.g., "Tuscaloosa") + + Returns: + List of nonprofit records (filtered by NTEE if specified) + """ + cache_file = self.cache_dir / f"propublica_{state}_{ntee_code or 'all'}_{city or 'all'}.json" + + # Check cache + if cache_file.exists(): + logger.info(f"Using cached ProPublica data from {cache_file}") + with open(cache_file) as f: + return json.load(f) + + base_url = "https://projects.propublica.org/nonprofits/api/v2/search.json" + + # Build params - NOTE: Excluding ntee[id] due to API 500 error bug + params = { + "state[id]": state, + } + + if city: + params["q"] = city + + self._rate_limit("propublica") + + # Retry logic for API failures + max_retries = 3 + retry_delay = 2.0 + + for attempt in range(max_retries): + try: + logger.info(f"Searching ProPublica API: state={state}, city={city} (attempt {attempt + 1}/{max_retries})") + if ntee_code: + logger.info(f" → Will filter for NTEE code '{ntee_code}' client-side after retrieval") + + response = requests.get(base_url, params=params, timeout=30) + response.raise_for_status() + + data = response.json() + organizations = data.get("organizations", []) + + logger.success(f"Retrieved {len(organizations)} organizations from ProPublica") + + # Parse results + nonprofits = [] + for org in organizations: + org_ntee = org.get("ntee_code", "") + + # CLIENT-SIDE NTEE FILTERING (workaround for API bug) + if ntee_code and org_ntee: + # Check if org's NTEE code starts with requested code + if not org_ntee.startswith(ntee_code): + continue # Skip this org, doesn't match filter + + nonprofit = { + "source": "propublica", + "ein": org.get("ein"), + "name": org.get("name"), + "city": org.get("city"), + "state": org.get("state"), + "ntee_code": org_ntee, + "subsection_code": org.get("subsection_code"), + "classification_codes": org.get("classification_codes", ""), + "ruling_date": org.get("ruling_date"), + "deductibility_code": org.get("deductibility_code"), + "foundation_code": org.get("foundation_code"), + "organization_code": org.get("organization_code"), + "exempt_organization_status_code": org.get("exempt_organization_status_code"), + "tax_period": org.get("tax_period"), + "asset_cd": org.get("asset_cd"), + "income_cd": org.get("income_cd"), + "filing_requirement_code": org.get("filing_requirement_code"), + "pf_filing_requirement_code": org.get("pf_filing_requirement_code"), + "accounting_period": org.get("accounting_period"), + "asset_amount": org.get("asset_amount"), + "income_amount": org.get("income_amount"), + "revenue_amount": org.get("revenue_amount"), + "ntee_description": self._get_ntee_description(org_ntee) + } + nonprofits.append(nonprofit) + + if ntee_code: + logger.success(f"Filtered to {len(nonprofits)} organizations matching NTEE code '{ntee_code}'") + else: + logger.success(f"Returning all {len(nonprofits)} organizations") + + # Cache results + with open(cache_file, 'w') as f: + json.dump(nonprofits, f, indent=2) + + return nonprofits + + except requests.exceptions.HTTPError as e: + if e.response.status_code == 500: + # Server error - retry with backoff + if attempt < max_retries - 1: + logger.warning(f"ProPublica API returned 500 error, retrying in {retry_delay}s... (attempt {attempt + 1}/{max_retries})") + time.sleep(retry_delay) + retry_delay *= 2 # Exponential backoff + continue + else: + logger.error(f"ProPublica API failed after {max_retries} attempts: {e}") + logger.warning(f"Skipping state={state}, city={city} - API unavailable") + return [] + else: + # Other HTTP errors + logger.error(f"ProPublica API HTTP error: {e}") + return [] + + except requests.exceptions.Timeout: + if attempt < max_retries - 1: + logger.warning(f"Request timeout, retrying in {retry_delay}s...") + time.sleep(retry_delay) + retry_delay *= 2 + continue + else: + logger.error(f"ProPublica API timeout after {max_retries} attempts") + return [] + + except requests.exceptions.RequestException as e: + logger.error(f"ProPublica API request failed: {e}") + return [] + + # Should not reach here, but just in case + return [] + + def get_propublica_org_details(self, ein: str) -> Optional[Dict]: + """ + Get detailed information about a specific organization + + Args: + ein: Employer Identification Number (9 digits) + + Returns: + Detailed organization data including filings + """ + cache_file = self.cache_dir / f"propublica_org_{ein}.json" + + if cache_file.exists(): + logger.info(f"Using cached org details for EIN {ein}") + with open(cache_file) as f: + return json.load(f) + + base_url = f"https://projects.propublica.org/nonprofits/api/v2/organizations/{ein}.json" + + self._rate_limit("propublica") + + try: + logger.info(f"Fetching org details for EIN {ein}") + response = requests.get(base_url, timeout=30) + response.raise_for_status() + + data = response.json() + org = data.get("organization", {}) + + details = { + "source": "propublica", + "ein": ein, + "name": org.get("name"), + "city": org.get("city"), + "state": org.get("state"), + "zipcode": org.get("zipcode"), + "ntee_code": org.get("ntee_code"), + "ntee_description": self._get_ntee_description(org.get("ntee_code")), + "subsection_code": org.get("subsection_code"), + "filings": [] + } + + # Get recent filings + for filing in data.get("filings_with_data", [])[:5]: # Last 5 filings + filing_data = { + "tax_period": filing.get("tax_prd_yr"), + "total_revenue": filing.get("totrevenue"), + "total_expenses": filing.get("totfuncexpns"), + "total_assets": filing.get("totassetsend"), + "total_liabilities": filing.get("totliabend"), + "net_assets": filing.get("totnetassetend"), + "contributions": filing.get("totcntrbgfts"), + "program_service_revenue": filing.get("totprgmrevnue"), + "pdf_url": filing.get("pdf_url") + } + details["filings"].append(filing_data) + + # Cache results + with open(cache_file, 'w') as f: + json.dump(details, f, indent=2) + + return details + + except requests.exceptions.RequestException as e: + logger.error(f"Failed to fetch org details for EIN {ein}: {e}") + return None + + # ========================================================================= + # 2. Every.org Charity API + # ========================================================================= + + def search_everyorg( + self, + location: str = "Tuscaloosa, AL", + causes: Optional[List[str]] = None + ) -> List[Dict]: + """ + Search Every.org for nonprofits by location and cause + + API Docs: https://www.every.org/nonprofit-api + + Args: + location: City, State format + causes: List of causes (e.g., ["health", "education"]) + + Returns: + List of nonprofits with mission statements and logos + """ + cache_file = self.cache_dir / f"everyorg_{location.replace(' ', '_').replace(',', '')}_{'-'.join(causes or ['all'])}.json" + + if cache_file.exists(): + logger.info(f"Using cached Every.org data from {cache_file}") + with open(cache_file) as f: + return json.load(f) + + # Note: Every.org API requires authentication + # For now, we'll use their public browse endpoint + base_url = "https://www.every.org/api/nonprofits/search" + + params = { + "location": location, + "take": 100 + } + + if causes: + params["causes"] = ",".join(causes) + + self._rate_limit("everyorg") + + try: + logger.info(f"Searching Every.org: location={location}, causes={causes}") + response = requests.get(base_url, params=params, timeout=30) + + # Every.org may require API key - handle gracefully + if response.status_code == 401: + logger.warning("Every.org API requires authentication - skipping") + return [] + + response.raise_for_status() + data = response.json() + + nonprofits = [] + for org in data.get("nonprofits", []): + nonprofit = { + "source": "everyorg", + "ein": org.get("ein"), + "name": org.get("name"), + "slug": org.get("slug"), + "description": org.get("description"), + "mission": org.get("mission"), + "logo_url": org.get("logoUrl"), + "cover_image_url": org.get("coverImageUrl"), + "location": org.get("location"), + "website_url": org.get("websiteUrl"), + "ntee_code": org.get("nteeCode"), + "ntee_description": self._get_ntee_description(org.get("nteeCode")), + "causes": org.get("causes", []) + } + nonprofits.append(nonprofit) + + logger.success(f"Found {len(nonprofits)} organizations from Every.org") + + # Cache results + with open(cache_file, 'w') as f: + json.dump(nonprofits, f, indent=2) + + return nonprofits + + except requests.exceptions.RequestException as e: + logger.warning(f"Every.org API request failed (may require auth): {e}") + return [] + + # ========================================================================= + # 3. Findhelp.org (Aunt Bertha) Scraper + # ========================================================================= + + def search_findhelp( + self, + location: str = "Tuscaloosa, AL", + keyword: str = "dental" + ) -> List[Dict]: + """ + Scrape Findhelp.org for local service providers + + Note: This uses their public search page. For production, consider + requesting API access from Findhelp.org + + Args: + location: City, State + keyword: Service keyword (e.g., "dental", "health", "food") + + Returns: + List of service providers + """ + cache_file = self.cache_dir / f"findhelp_{location.replace(' ', '_').replace(',', '')}_{keyword}.json" + + if cache_file.exists(): + logger.info(f"Using cached Findhelp data from {cache_file}") + with open(cache_file) as f: + return json.load(f) + + # Findhelp.org search URL + base_url = "https://www.findhelp.org/search" + + params = { + "query": keyword, + "location": location + } + + self._rate_limit("findhelp") + + try: + logger.info(f"Searching Findhelp.org: location={location}, keyword={keyword}") + + # This would require HTML parsing - simplified version + # In production, use BeautifulSoup or Playwright for full scraping + + # Placeholder for now + logger.warning("Findhelp.org scraping requires HTML parsing - implement with BeautifulSoup") + + return [] + + except Exception as e: + logger.error(f"Findhelp.org scraping failed: {e}") + return [] + + # ========================================================================= + # 4. 211 Directory Search + # ========================================================================= + + def search_211( + self, + state: str = "AL", + county: str = "Tuscaloosa", + keyword: str = "dental" + ) -> List[Dict]: + """ + Search 211 directory for local social services + + Note: Each state/region has different 211 systems. This targets + Alabama's 211 system. + + Args: + state: State code + county: County name + keyword: Service keyword + + Returns: + List of service providers + """ + cache_file = self.cache_dir / f"211_{state}_{county}_{keyword}.json" + + if cache_file.exists(): + logger.info(f"Using cached 211 data from {cache_file}") + with open(cache_file) as f: + return json.load(f) + + # Alabama 211 URL + # Note: Different states use different 211 systems + base_url = "https://www.211connects.org" + + logger.info(f"Searching 211: state={state}, county={county}, keyword={keyword}") + + # This would require state-specific scraping + logger.warning("211 directory scraping requires state-specific implementation") + + return [] + + # ========================================================================= + # Helper Functions + # ========================================================================= + + def _get_ntee_description(self, ntee_code: Optional[str]) -> str: + """Get human-readable description for NTEE code""" + if not ntee_code: + return "Unknown" + + ntee_map = { + "E": "Health - General and Rehabilitative", + "E20": "Hospitals and Related Primary Medical Care Facilities", + "E30": "Ambulatory Health Center, Community Clinic", + "E32": "School-Based Health Care", + "E40": "Reproductive Health Care", + "E50": "Rehabilitative Medical Services", + "E80": "Health - General and Rehabilitative N.E.C.", + "F": "Mental Health, Crisis Intervention", + "K": "Food, Agriculture, and Nutrition", + "K30": "Food Service, Free Food Distribution Programs", + "K34": "Congregate Meals", + "N": "Recreation, Sports, Leisure, Athletics", + "O": "Youth Development", + "O50": "Youth Development Programs, Other", + "P": "Human Services - Multipurpose and Other", + "X": "Religion Related, Spiritual Development", + "X20": "Christian", + "W": "Public, Society Benefit - Multipurpose and Other" + } + + # Try exact match first + if ntee_code in ntee_map: + return ntee_map[ntee_code] + + # Try major group (first letter) + if ntee_code[0] in ntee_map: + return ntee_map[ntee_code[0]] + + return f"NTEE {ntee_code}" + + def merge_nonprofit_data( + self, + propublica_orgs: List[Dict], + everyorg_orgs: List[Dict] + ) -> List[Dict]: + """ + Merge nonprofit data from multiple sources by EIN + + Args: + propublica_orgs: Orgs from ProPublica API + everyorg_orgs: Orgs from Every.org API + + Returns: + Merged list with enriched data + """ + merged = {} + + # Add ProPublica data (financial backbone) + for org in propublica_orgs: + ein = org.get("ein") + if ein: + merged[ein] = org + + # Enrich with Every.org data (mission, logo) + for org in everyorg_orgs: + ein = org.get("ein") + if ein and ein in merged: + # Add Every.org fields to existing record + merged[ein].update({ + "mission": org.get("mission") or merged[ein].get("mission"), + "description": org.get("description"), + "logo_url": org.get("logo_url"), + "website_url": org.get("website_url"), + "causes": org.get("causes", []) + }) + elif ein: + # New org not in ProPublica data + merged[ein] = org + + logger.success(f"Merged {len(merged)} unique nonprofits") + + return list(merged.values()) + + def export_to_frontend( + self, + nonprofits: List[Dict], + output_file: str = "frontend/policy-dashboards/src/data/tuscaloosa_nonprofits.json" + ): + """ + Export nonprofit data in frontend-compatible format + + Args: + nonprofits: List of nonprofit records + output_file: Path to output JSON file + """ + frontend_data = [] + + for org in nonprofits: + # Get most recent financial data + revenue = org.get("revenue_amount") or org.get("income_amount", 0) + + # Estimate impact (this would come from services data in production) + estimated_impact = self._estimate_impact(org) + + frontend_org = { + "name": org.get("name"), + "ein": org.get("ein"), + "ntee_code": org.get("ntee_code"), + "ntee_description": org.get("ntee_description", ""), + "mission": org.get("mission") or org.get("description", ""), + "services": self._extract_services(org), + "annual_budget": revenue, + **estimated_impact, + "contact": { + "website": org.get("website_url", ""), + "email": "", # Not typically in public APIs + "phone": "" # Would need scraping + }, + "logo_url": org.get("logo_url"), + "volunteer_opportunities": True, # Default assumption + "accepting_board_members": False # Would need manual verification + } + + frontend_data.append(frontend_org) + + # Save to file + output_path = Path(output_file) + output_path.parent.mkdir(parents=True, exist_ok=True) + + with open(output_path, 'w') as f: + json.dump(frontend_data, f, indent=2) + + logger.success(f"Exported {len(frontend_data)} nonprofits to {output_file}") + + def _extract_services(self, org: Dict) -> List[str]: + """Extract services from org description/mission""" + # This is a placeholder - real version would use NLP + services = [] + + description = (org.get("description") or org.get("mission") or "").lower() + + service_keywords = { + "dental": "Dental care and screenings", + "food": "Food assistance and nutrition programs", + "health": "Healthcare services", + "mental": "Mental health services", + "youth": "Youth development programs", + "education": "Educational programs" + } + + for keyword, service in service_keywords.items(): + if keyword in description: + services.append(service) + + return services or ["Community services"] + + def _estimate_impact(self, org: Dict) -> Dict: + """Estimate impact metrics based on revenue""" + revenue = org.get("revenue_amount") or org.get("income_amount", 0) + + # Rough estimates based on nonprofit sector averages + # This would be replaced with actual reported data + if revenue: + return { + "students_served": int(revenue / 50) if "school" in org.get("ntee_description", "").lower() else 0, + "families_served": int(revenue / 200), + "youth_served": int(revenue / 100) if "youth" in org.get("ntee_description", "").lower() else 0 + } + + return { + "students_served": 0, + "families_served": 0, + "youth_served": 0 + } + + +def discover_tuscaloosa_nonprofits( + ntee_codes: List[str] = ["E", "E32", "E40", "K", "O", "X"] +) -> List[Dict]: + """ + Run complete nonprofit discovery for Tuscaloosa, AL + + Args: + ntee_codes: List of NTEE codes to search for + + Returns: + Merged list of nonprofits + """ + discovery = NonprofitDiscovery() + + all_nonprofits = [] + + # Search ProPublica for each NTEE code + for ntee in ntee_codes: + logger.info(f"Searching for NTEE code {ntee}...") + + orgs = discovery.search_propublica( + state="AL", + city="Tuscaloosa", + ntee_code=ntee + ) + + all_nonprofits.extend(orgs) + + # Be respectful to API + time.sleep(1) + + # Try Every.org (may require auth) + everyorg_orgs = discovery.search_everyorg( + location="Tuscaloosa, AL", + causes=["health", "education", "youth"] + ) + + # Merge data sources + merged = discovery.merge_nonprofit_data(all_nonprofits, everyorg_orgs) + + # Enrich top organizations with detailed data + for org in merged[:20]: # Top 20 by revenue + ein = org.get("ein") + if ein: + details = discovery.get_propublica_org_details(ein) + if details: + org.update(details) + time.sleep(1) # Rate limiting + + # Export to frontend + discovery.export_to_frontend(merged) + + return merged + + +if __name__ == "__main__": + logger.info("Starting Tuscaloosa nonprofit discovery...") + + nonprofits = discover_tuscaloosa_nonprofits() + + logger.success(f"✓ Discovered {len(nonprofits)} nonprofits in Tuscaloosa") + + # Print summary + print("\n" + "="*60) + print("TUSCALOOSA NONPROFIT SUMMARY") + print("="*60) + + by_ntee = {} + for org in nonprofits: + ntee = org.get("ntee_code", "Unknown") + if ntee not in by_ntee: + by_ntee[ntee] = [] + by_ntee[ntee].append(org) + + for ntee, orgs in sorted(by_ntee.items(), key=lambda x: len(x[1]), reverse=True): + desc = NonprofitDiscovery()._get_ntee_description(ntee) + print(f"\n{ntee} - {desc}: {len(orgs)} organizations") + for org in orgs[:3]: # Top 3 + revenue = org.get("revenue_amount", 0) + print(f" • {org['name']}: ${revenue:,}/year") diff --git a/discovery/openstates_sources.py b/discovery/openstates_sources.py new file mode 100644 index 0000000000000000000000000000000000000000..a61a501aad3ac869b4331944f67cf82997e23169 --- /dev/null +++ b/discovery/openstates_sources.py @@ -0,0 +1,296 @@ +""" +Open States Video Sources Integration + +Open States (now part of Plural) is the most comprehensive state legislative +data aggregator in the United States. + +Coverage: +- All 50 states + DC + Puerto Rico +- State legislatures with video archives +- Expanding to city councils and county boards +- Many jurisdictions host videos on YouTube/Vimeo + +API: https://openstates.org/api/ +Data: https://data.openstates.org/ +Free tier: 50,000 requests/month (plenty for our needs) + +The 'sources' field frequently contains: +- YouTube channel URLs (e.g., @CALegislature) +- Vimeo profile URLs +- Granicus video portals +- Archive.org collections +""" +import sys +from pathlib import Path + +# Add project root to Python path for standalone execution +project_root = Path(__file__).parent.parent +if str(project_root) not in sys.path: + sys.path.insert(0, str(project_root)) + +import requests +from typing import List, Dict, Optional +from datetime import datetime +from loguru import logger +from pyspark.sql import SparkSession +from pyspark.sql.functions import lit + +from config.settings import settings + + +OPENSTATES_API_BASE = "https://v3.openstates.org" + + +def get_api_key() -> Optional[str]: + """ + Get Open States API key from settings. + + Sign up for free at: https://openstates.org/accounts/signup/ + """ + api_key = getattr(settings, 'openstates_api_key', None) + + if not api_key: + logger.warning("⚠️ OPENSTATES_API_KEY not found in settings") + logger.warning(" Sign up at: https://openstates.org/accounts/signup/") + logger.warning(" Add to .env: OPENSTATES_API_KEY=your-key") + + return api_key + + +def extract_platform_from_url(url: str) -> str: + """ + Extract platform from URL. + """ + url_lower = url.lower() + + if 'youtube.com' in url_lower or 'youtu.be' in url_lower: + return 'youtube' + elif 'vimeo.com' in url_lower: + return 'vimeo' + elif 'granicus.com' in url_lower: + return 'granicus' + elif 'archive.org' in url_lower: + return 'archive_org' + elif 'legistar.com' in url_lower: + return 'legistar' + else: + return 'other' + + +def get_jurisdictions_with_video_sources(api_key: str) -> List[Dict]: + """ + Fetch all jurisdictions from Open States and extract video sources. + + Returns list of jurisdictions with YouTube/Vimeo/Granicus URLs. + """ + logger.info("Fetching jurisdictions from Open States API") + + try: + response = requests.get( + f"{OPENSTATES_API_BASE}/jurisdictions", + headers={"X-API-KEY": api_key}, + timeout=30 + ) + + response.raise_for_status() + + data = response.json() + jurisdictions = data.get('results', []) + + logger.info(f"✅ Retrieved {len(jurisdictions)} jurisdictions from Open States") + + video_sources = [] + + for jurisdiction in jurisdictions: + # Extract sources field + sources = jurisdiction.get('sources', []) + + if not sources: + continue + + for source in sources: + url = source.get('url', '') + + if not url: + continue + + # Check if it's a video platform + platform = extract_platform_from_url(url) + + if platform in ['youtube', 'vimeo', 'granicus', 'archive_org']: + video_sources.append({ + "jurisdiction_id": jurisdiction.get('id', ''), + "jurisdiction_name": jurisdiction.get('name', ''), + "classification": jurisdiction.get('classification', ''), + "division_id": jurisdiction.get('division_id', ''), + "video_url": url, + "platform": platform, + "source": "openstates" + }) + + logger.info(f"✅ Found {len(video_sources)} video sources from {len(jurisdictions)} jurisdictions") + + # Count by platform + platform_counts = {} + for source in video_sources: + platform = source['platform'] + platform_counts[platform] = platform_counts.get(platform, 0) + 1 + + logger.info("\nVideo sources by platform:") + for platform, count in sorted(platform_counts.items(), key=lambda x: x[1], reverse=True): + logger.info(f" • {platform}: {count} sources") + + return video_sources + + except requests.exceptions.RequestException as e: + logger.error(f"❌ Failed to fetch jurisdictions from Open States: {e}") + logger.error(" Check your API key and internet connection") + return [] + + +def get_legislative_sessions_with_videos(api_key: str, jurisdiction_id: str) -> List[Dict]: + """ + Fetch legislative sessions for a jurisdiction. + + Many states include video_url in session metadata. + """ + logger.info(f"Fetching legislative sessions for {jurisdiction_id}") + + try: + response = requests.get( + f"{OPENSTATES_API_BASE}/jurisdictions/{jurisdiction_id}", + headers={"X-API-KEY": api_key}, + timeout=30 + ) + + response.raise_for_status() + + data = response.json() + sessions = data.get('legislative_sessions', []) + + video_sessions = [] + + for session in sessions: + # Check for video URLs in session metadata + if 'video_url' in session or 'stream_url' in session: + video_sessions.append({ + "jurisdiction_id": jurisdiction_id, + "session_id": session.get('identifier', ''), + "session_name": session.get('name', ''), + "video_url": session.get('video_url') or session.get('stream_url'), + "source": "openstates_sessions" + }) + + return video_sessions + + except requests.exceptions.RequestException as e: + logger.warning(f"Failed to fetch sessions for {jurisdiction_id}: {e}") + return [] + + +def write_to_bronze_layer( + video_sources: List[Dict], + spark: SparkSession +) -> Dict[str, int]: + """ + Write Open States video sources to Bronze layer. + + Creates table: bronze/openstates_sources + """ + if not video_sources: + logger.warning("No video sources to write") + return {"total_sources": 0} + + # Add ingestion timestamp + for source in video_sources: + source['ingested_at'] = datetime.utcnow().isoformat() + + # Convert to DataFrame + df = spark.createDataFrame(video_sources) + + # Write to Delta Lake + output_path = f"{settings.delta_lake_path}/bronze/openstates_sources" + + logger.info(f"Writing Open States sources to Bronze layer: {output_path}") + + df.write \ + .format("delta") \ + .mode("overwrite") \ + .save(output_path) + + # Get stats by platform + platform_counts = df.groupBy("platform").count().collect() + + logger.info("\n✅ Open States sources written to Bronze layer") + logger.info("\nSources by platform:") + for row in sorted(platform_counts, key=lambda r: r['count'], reverse=True): + logger.info(f" • {row['platform']}: {row['count']} sources") + + return { + "total_sources": df.count(), + "jurisdictions": df.select("jurisdiction_name").distinct().count(), + "platforms": df.select("platform").distinct().count(), + "table": "bronze/openstates_sources" + } + + +def ingest_openstates_sources(spark: SparkSession) -> Dict[str, int]: + """ + Main function: Fetch Open States video sources via API. + + Returns: + - total_sources: Number of video sources extracted + - jurisdictions: Number of jurisdictions with video sources + - platforms: Number of video platforms found + - table: Bronze layer table name + """ + logger.info("=" * 60) + logger.info("OPEN STATES VIDEO SOURCES EXTRACTION") + logger.info("=" * 60) + + # Get API key + api_key = get_api_key() + + if not api_key: + logger.error("❌ Cannot proceed without Open States API key") + logger.error(" Get one free at: https://openstates.org/accounts/signup/") + return {"total_sources": 0, "jurisdictions": 0, "platforms": 0} + + # Fetch jurisdictions with video sources + video_sources = get_jurisdictions_with_video_sources(api_key) + + if not video_sources: + logger.warning("⚠️ No video sources found in Open States") + return {"total_sources": 0, "jurisdictions": 0, "platforms": 0} + + # Write to Bronze layer + stats = write_to_bronze_layer(video_sources, spark) + + logger.info("\n" + "=" * 60) + logger.info(f"✅ OPEN STATES INGESTION COMPLETE") + logger.info(f" • Video sources: {stats['total_sources']}") + logger.info(f" • Jurisdictions: {stats['jurisdictions']}") + logger.info(f" • Platforms: {stats['platforms']}") + logger.info(f" • Table: {stats['table']}") + logger.info("=" * 60) + + return stats + + +if __name__ == "__main__": + # Test extraction + from delta import configure_spark_with_delta_pip + from pyspark.sql import SparkSession + + # Configure Spark with Delta Lake + builder = SparkSession.builder \ + .appName("OpenStatesSourcesExtraction") \ + .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension") \ + .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog") + + spark = configure_spark_with_delta_pip(builder).getOrCreate() + + # Run ingestion + stats = ingest_openstates_sources(spark) + + print(f"\n✅ Extracted {stats['total_sources']} video sources from Open States") diff --git a/discovery/platform_detector.py b/discovery/platform_detector.py new file mode 100644 index 0000000000000000000000000000000000000000..5e60c544aa3cf2e388a5fce25fb4faf5be65d265 --- /dev/null +++ b/discovery/platform_detector.py @@ -0,0 +1,336 @@ +""" +Platform detection for municipal websites. + +Based on patterns from: +- biglocalnews/civic-scraper (Apache 2.0) +- city-scrapers/city-scrapers (MIT) + +Detects which content management system or meeting platform a municipality uses, +enabling optimized scraping strategies. +""" +from typing import Optional, Dict, List +from urllib.parse import urlparse +import httpx +from bs4 import BeautifulSoup +from loguru import logger + + +# Platform URL patterns (most specific first) +PLATFORM_PATTERNS = { + 'legistar': [ + 'legistar.com', + '/Legistar/', + '/LegislationDetail.aspx', + '/Calendar.aspx', + '/MeetingDetail.aspx', + 'WebApi/odata' + ], + 'granicus': [ + 'granicus.com', + '/Mediasite/', + '/ViewPublisher.php', + '/MetaViewer.php', + 'granicus-cdn.com' + ], + 'municode': [ + 'municode.com', + '/meeting_minutes', + '/MuniCode/' + ], + 'civicplus': [ + 'civicplus.com', + '/AgendaCenter/', + '/DocumentCenter/', + '/CivicSend/' + ], + 'primegov': [ + 'primegov.com', + '/Portal/', + '/Public/0/' + ], + 'calagenda': [ + 'ca-ilg.civicplus.com', + '/AgendaCenter/ViewFile/' + ], + 'swagit': [ + 'swagit.com', + '/play/', + '/videos/' + ], + 'zoomgov': [ + 'zoom.us/rec/', + 'zoomgov.com' + ] +} + +# HTML meta tag patterns that indicate platforms +META_PATTERNS = { + 'legistar': [ + 'Legistar', + 'InSite', + 'Granicus' # Granicus owns Legistar + ], + 'civicplus': [ + 'CivicPlus', + 'CivicEngage' + ] +} + +# Common CMS patterns (WordPress, Drupal, etc.) +CMS_PATTERNS = { + 'wordpress': [ + 'wp-content', + 'wp-includes', + 'wordpress' + ], + 'drupal': [ + '/sites/default/', + 'drupal.js', + 'Drupal.settings' + ], + 'joomla': [ + '/components/com_', + '/modules/mod_' + ] +} + + +def detect_platform(url: str, html_content: Optional[str] = None) -> Optional[str]: + """ + Detect which platform a municipality website uses. + + Performs two-stage detection: + 1. URL pattern matching (fast, works without fetching) + 2. HTML content analysis (slower, more accurate) + + Args: + url: Municipality website URL + html_content: Optional HTML content for deeper analysis + + Returns: + Platform name or None if unknown + + Examples: + >>> detect_platform("https://chicago.legistar.com/Calendar.aspx") + 'legistar' + >>> detect_platform("https://example.gov/meetings") + None + """ + url_lower = url.lower() + + # Stage 1: URL pattern matching + for platform, patterns in PLATFORM_PATTERNS.items(): + if any(pattern.lower() in url_lower for pattern in patterns): + logger.debug(f"Detected {platform} from URL pattern: {url}") + return platform + + # Stage 2: HTML content analysis (if provided) + if html_content: + platform = detect_from_html(html_content) + if platform: + logger.debug(f"Detected {platform} from HTML content: {url}") + return platform + + # Stage 3: Check for generic CMS + for cms, patterns in CMS_PATTERNS.items(): + if any(pattern.lower() in url_lower for pattern in patterns): + logger.debug(f"Detected generic CMS {cms}: {url}") + return 'generic' + + logger.debug(f"No platform detected for: {url}") + return None + + +def detect_from_html(html_content: str) -> Optional[str]: + """ + Detect platform from HTML content analysis. + + Checks: + - Meta tags (generator, description) + - Script sources + - Link hrefs + - Specific HTML structures + + Args: + html_content: Raw HTML content + + Returns: + Platform name or None + """ + try: + soup = BeautifulSoup(html_content, 'html.parser') + + # Check meta tags + for platform, keywords in META_PATTERNS.items(): + meta_generator = soup.find('meta', attrs={'name': 'generator'}) + if meta_generator: + content = meta_generator.get('content', '').lower() + if any(keyword.lower() in content for keyword in keywords): + return platform + + # Check scripts and links + all_text = html_content.lower() + for platform, patterns in PLATFORM_PATTERNS.items(): + if any(pattern.lower() in all_text for pattern in patterns): + return platform + + except Exception as e: + logger.warning(f"Error parsing HTML for platform detection: {e}") + + return None + + +async def detect_platform_async(url: str, fetch_html: bool = True) -> Dict[str, any]: + """ + Async version that can fetch HTML content for deeper detection. + + Args: + url: Municipality website URL + fetch_html: Whether to fetch HTML for content analysis + + Returns: + Dictionary with detection results: + { + 'platform': str, + 'confidence': float, + 'features': List[str], + 'scraper_available': bool + } + """ + result = { + 'url': url, + 'platform': None, + 'confidence': 0.0, + 'features': [], + 'scraper_available': False + } + + # Quick URL check + platform = detect_platform(url) + if platform: + result['platform'] = platform + result['confidence'] = 0.7 + result['features'].append('url_pattern') + + # Fetch and analyze HTML if requested + if fetch_html: + try: + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.get(url, follow_redirects=True) + response.raise_for_status() + + platform_from_html = detect_from_html(response.text) + if platform_from_html: + result['platform'] = platform_from_html + result['confidence'] = 0.9 + result['features'].append('html_content') + + except Exception as e: + logger.warning(f"Could not fetch {url} for platform detection: {e}") + + # Check if we have a scraper for this platform + if result['platform'] in ['legistar', 'granicus', 'civicplus']: + result['scraper_available'] = True + + return result + + +def get_platform_capabilities(platform: str) -> Dict[str, any]: + """ + Get capabilities and scraping strategies for a platform. + + Args: + platform: Platform name + + Returns: + Dictionary describing platform capabilities + """ + capabilities = { + 'legistar': { + 'has_api': True, + 'api_docs': 'https://webapi.legistar.com/Help', + 'supports_bulk_download': True, + 'common_endpoints': [ + '/events', + '/matters', + '/bodies' + ], + 'rate_limit': 'Unknown', + 'scraper_class': 'scrapers.legistar.LegistarScraper' + }, + 'granicus': { + 'has_api': True, + 'supports_bulk_download': True, + 'common_endpoints': [ + '/ViewPublisher.php', + '/MetaViewer.php' + ], + 'rate_limit': 'Unknown', + 'scraper_class': 'scrapers.granicus.GranicusScraper' + }, + 'civicplus': { + 'has_api': False, + 'supports_bulk_download': False, + 'requires_html_parsing': True, + 'scraper_class': 'scrapers.civicplus.CivicPlusScraper' + }, + 'generic': { + 'has_api': False, + 'supports_bulk_download': False, + 'requires_html_parsing': True, + 'scraper_class': 'scrapers.generic.GenericScraper' + } + } + + return capabilities.get(platform, { + 'has_api': False, + 'supports_bulk_download': False, + 'requires_html_parsing': True, + 'scraper_class': 'scrapers.generic.GenericScraper' + }) + + +def get_scraper_class(platform: str): + """ + Get appropriate scraper class for a platform. + + Args: + platform: Platform name + + Returns: + Scraper class (dynamically imported) + """ + # Note: This assumes you'll create these scraper classes + # For now, returns None to avoid import errors + + scraper_map = { + 'legistar': 'scrapers.legistar.LegistarScraper', + 'granicus': 'scrapers.granicus.GranicusScraper', + 'civicplus': 'scrapers.civicplus.CivicPlusScraper', + 'generic': 'scrapers.generic.GenericScraper' + } + + scraper_path = scraper_map.get(platform, 'scrapers.generic.GenericScraper') + + # TODO: Dynamic import when scrapers are implemented + # module_path, class_name = scraper_path.rsplit('.', 1) + # module = importlib.import_module(module_path) + # return getattr(module, class_name) + + logger.warning(f"Scraper class not yet implemented: {scraper_path}") + return None + + +# Example usage +if __name__ == "__main__": + # Test URL detection + test_urls = [ + "https://chicago.legistar.com/Calendar.aspx", + "https://birminghamal.gov/meetings", + "https://example.civicplus.com/AgendaCenter", + "https://unknown-city.gov/council" + ] + + for url in test_urls: + platform = detect_platform(url) + print(f"{url}\n → Platform: {platform}\n") diff --git a/discovery/social_media_discovery.py b/discovery/social_media_discovery.py new file mode 100644 index 0000000000000000000000000000000000000000..3f636de6d50760543099bdbb6786562c4e073737 --- /dev/null +++ b/discovery/social_media_discovery.py @@ -0,0 +1,433 @@ +""" +Social Media & Video Channel Discovery from Government Websites + +Discovers YouTube, Facebook, Vimeo, and other video channels by crawling +official government websites - specifically footer sections and contact pages. + +This complements dataset-based discovery (MeetingBank, Open States) with +real-time web scraping to find channels that aren't in existing datasets. + +Data Sources: +1. Government homepages (from url_discovery_agent) +2. USA.gov Local Directory (optional federal verification) +3. Common footer/contact page patterns + +Pattern Examples: +- Footer: YouTube +- Contact: "Follow us on YouTube: youtube.com/c/CityName" +- Social icons:
+ + Website + + )} + {contact?.email && ( + + + Email + + )} + {contact?.phone && ( + + + Call + + )} +
+ + {/* Opportunities */} +
+ {volunteer_opportunities && ( + + ✓ Accepting Volunteers + + )} + {accepting_board_members && ( + + ⭐ Board Seats Available + + )} +
+
+ ); +} diff --git a/frontend/policy-dashboards/src/components/SplitScreenView.jsx b/frontend/policy-dashboards/src/components/SplitScreenView.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8c5488f2385e2ae40e99e08a3dffbba995e26299 --- /dev/null +++ b/frontend/policy-dashboards/src/components/SplitScreenView.jsx @@ -0,0 +1,375 @@ +import React from 'react'; +import { ExternalLink, Users, DollarSign, Heart, Mail, Phone, Globe } from 'lucide-react'; + +/** + * SplitScreenView Component + * Shows government decisions on the left, community nonprofits on the right + * Demonstrates the accountability gap and community response + */ +export default function SplitScreenView({ decision, nonprofits = [] }) { + // Find nonprofits matching this decision's NTEE code + const matchingNonprofits = nonprofits.filter(np => + np.ntee_code === decision.ntee_code || + (decision.ntee_code && np.ntee_code?.startsWith(decision.ntee_code[0])) // Match category + ); + + const hasGap = decision.community_gap?.nonprofit_filling_gap; + + return ( +
+ {/* LEFT RAIL: The Public Sector (Government Decision) */} +
+
+ 🏛️ Public Sector Decision +
+ +

+ {decision.decision_summary} +

+ +
+
+ Official Rationale: +
+
+ "{decision.primary_rationale}" +
+
+ + {hasGap && ( +
+
+ ⚠️ The Accountability Gap +
+
+ {decision.community_gap.description} +
+
+ )} + +
+
Outcome: {decision.outcome}
+
Vote: {decision.vote_result}
+
Date: {new Date(decision.meeting_date).toLocaleDateString()}
+
+
+ + {/* RIGHT RAIL: The Community Sector (Nonprofits) */} +
+
+ 🤝 Community Sector Response +
+ + {matchingNonprofits.length === 0 ? ( +
+

+ No nonprofits found filling this gap yet. +

+

+ NTEE Code: {decision.ntee_code || 'Not classified'} +

+
+ ) : ( + <> +
+ {matchingNonprofits.length} organization{matchingNonprofits.length !== 1 ? 's' : ''} filling this gap: +
+ +
+ {matchingNonprofits.map((nonprofit, i) => ( + + ))} +
+ +
+
+ 💡 Bridge the Gap +
+
    +
  • Support these organizations with donations or volunteering
  • +
  • Join their boards to influence systemic change
  • +
  • Cite their work in public meetings to show solutions exist
  • +
+
+ + )} +
+
+ ); +} + +function NonprofitCard({ nonprofit }) { + return ( +
+
+

+ {nonprofit.name} +

+
+ NTEE {nonprofit.ntee_code}: {nonprofit.ntee_description} +
+
+ {nonprofit.mission} +
+
+ + {/* Services */} +
+
+ Services Provided: +
+
    + {nonprofit.services.slice(0, 3).map((service, i) => ( +
  • {service}
  • + ))} +
+
+ + {/* Impact */} +
+ {nonprofit.students_served && ( +
+
Impact
+
+ + {nonprofit.students_served.toLocaleString()} students +
+
+ )} + {nonprofit.families_served && ( +
+
Impact
+
+ + {nonprofit.families_served.toLocaleString()} families +
+
+ )} + {nonprofit.youth_served && ( +
+
Impact
+
+ + {nonprofit.youth_served.toLocaleString()} youth +
+
+ )} +
+
Annual Budget
+
+ + {(nonprofit.annual_budget / 1000).toFixed(0)}K +
+
+
+ + {/* Contact & Actions */} +
+ {nonprofit.contact.website && ( + + + Website + + )} + {nonprofit.contact.email && ( + + + Email + + )} +
+ + {/* Opportunities */} +
+ {nonprofit.volunteer_opportunities && ( + + ✓ Accepting Volunteers + + )} + {nonprofit.accepting_board_members && ( + + ⭐ Board Seats Available + + )} +
+
+ ); +} diff --git a/frontend/policy-dashboards/src/components/Summary.jsx b/frontend/policy-dashboards/src/components/Summary.jsx new file mode 100644 index 0000000000000000000000000000000000000000..89cea3b70ac34d8c8c8d67072de210c2339f551e --- /dev/null +++ b/frontend/policy-dashboards/src/components/Summary.jsx @@ -0,0 +1,183 @@ +import React from 'react'; +import { summaryData as d } from '../data/dashboardData'; +import { DashboardGrid } from './shared/DashboardTile'; + +/** + * Summary Dashboard + * Overview of all four findings with tile-based navigation + */ +export default function Summary({ onNavigate }) { + return ( +
+ {/* Headline */} +
+

+ {d.headline} +

+

+ {d.subheadline} +

+
+ + {/* Dashboard Tiles */} + + + {/* Legacy Finding Cards - Keep for compact view */} +
+ + Show compact list view + +
+ {d.findings.map((finding, i) => ( +
onNavigate && onNavigate(finding.id)} + style={{ + background: '#fff', + border: '1px solid #eee', + borderLeft: `4px solid ${finding.discomfort >= 9 ? '#D85A30' : '#BA7517'}`, + borderRadius: 8, + padding: 16, + cursor: onNavigate ? 'pointer' : 'default', + transition: 'all 0.2s ease' + }} + onMouseEnter={(e) => { + if (onNavigate) { + e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)'; + e.currentTarget.style.borderLeftWidth = '6px'; + } + }} + onMouseLeave={(e) => { + if (onNavigate) { + e.currentTarget.style.boxShadow = 'none'; + e.currentTarget.style.borderLeftWidth = '4px'; + } + }} + > +
+
+
+ Dashboard {finding.id} +
+
+ {finding.title} +
+
+
+
= 9 ? '#D85A30' : '#BA7517' + }}> + {finding.metric} +
+
+ {finding.context} +
+
+
+

+ {finding.summary} +

+ {finding.discomfort >= 9 && ( +
+ ⚠️ High accountability impact +
+ )} +
+ ))} +
+
+ + {/* How to Use Section */} +
+

+ {d.howToUse.title} +

+ +
+ {d.howToUse.strategies.map((strategy, i) => ( +
+
+
+ ❌ DON'T: {strategy.dont} +
+
+
+
+ ✅ DO: {strategy.do} +
+
+
+ ))} +
+
+ + {/* Bottom Navigation Hint */} + {onNavigate && ( +
+ 💡 Click any finding above to see the detailed dashboard +
+ )} +
+ ); +} diff --git a/frontend/policy-dashboards/src/components/TopicNavigation.jsx b/frontend/policy-dashboards/src/components/TopicNavigation.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b0ae6d3b098041ddeed2c9474983e7e50533676a --- /dev/null +++ b/frontend/policy-dashboards/src/components/TopicNavigation.jsx @@ -0,0 +1,511 @@ +import React, { useState } from 'react'; +import { Filter, Video, FileText, DollarSign, BarChart3 } from 'lucide-react'; + +/** + * TopicNavigation Component + * Primary and secondary filters for browsing decisions + */ +export default function TopicNavigation({ + selectedTopics = [], + selectedPatterns = [], + selectedResources = [], + startDate, + endDate, + onTopicToggle, + onPatternToggle, + onResourceToggle, + onStartDateChange, + onEndDateChange, + onClearAll +}) { + const [showFilters, setShowFilters] = useState(true); + + const topics = [ + { id: 'public-health', label: 'Public Health', sublabel: 'Dental, Water, Mental Health', color: '#1D9E75' }, + { id: 'education', label: 'Education & Youth', sublabel: 'School Board, Pre-K', color: '#185FA5' }, + { id: 'infrastructure', label: 'Infrastructure', sublabel: 'Roads, Utilities, Construction', color: '#BA7517' }, + { id: 'public-safety', label: 'Public Safety', sublabel: 'Police, Fire, EMS', color: '#E24B4A' } + ]; + + const patterns = [ + { id: 'technocratic-veto', label: 'Technocratic Veto', description: 'Legal/risk managers blocking decisions' }, + { id: 'sequential-deferral', label: 'Sequential Deferral', description: 'Repeated "tabling for study"' }, + { id: 'performance-rationale', label: 'Performance Rationale', description: 'Rhetoric not matching funding' } + ]; + + const resources = [ + { id: 'video', label: 'Video Recap', icon: Video }, + { id: 'budget', label: 'Budget PDF', icon: DollarSign }, + { id: 'dashboard', label: 'Impact Dashboard', icon: BarChart3 }, + { id: 'summary', label: 'Summary Notes', icon: FileText } + ]; + + const hasActiveFilters = selectedTopics.length > 0 || + selectedPatterns.length > 0 || + selectedResources.length > 0 || + startDate !== null || + endDate !== null; + + return ( +
+ {/* Header */} +
+ + + {hasActiveFilters && ( + + )} +
+ + {/* Filter Panel */} + {showFilters && ( +
+ {/* Primary Navigation: Topics */} +
+
+ Primary Topic / Domain +
+
+ {topics.map(topic => { + const isSelected = selectedTopics.includes(topic.id); + return ( + + ); + })} +
+
+ + {/* Secondary Filter: Patterns */} +
+
+ Filter by Pattern +
+
+ {patterns.map(pattern => { + const isSelected = selectedPatterns.includes(pattern.id); + return ( + + ); + })} +
+
+ + {/* Tertiary Filter: Resources */} +
+
+ Filter by Resource +
+
+ {resources.map(resource => { + const isSelected = selectedResources.includes(resource.id); + const Icon = resource.icon; + return ( + + ); + })} +
+
+ + {/* Time Window Filter */} +
+
+ Time Window +
+
+
+ + onStartDateChange(e.target.value || null)} + style={{ + width: '100%', + padding: '6px 8px', + borderRadius: 6, + border: '1px solid', + borderColor: startDate ? '#D85A30' : '#ddd', + fontSize: 12, + background: 'white' + }} + /> +
+
+ + onEndDateChange(e.target.value || null)} + style={{ + width: '100%', + padding: '6px 8px', + borderRadius: 6, + border: '1px solid', + borderColor: endDate ? '#D85A30' : '#ddd', + fontSize: 12, + background: 'white' + }} + /> +
+
+
+
+ )} + + {/* Active Filters Display */} + {hasActiveFilters && ( +
+ Active filters: + {selectedTopics.map(topicId => { + const topic = topics.find(t => t.id === topicId); + return ( + + {topic.label} + + + ); + })} + {selectedPatterns.map(patternId => { + const pattern = patterns.find(p => p.id === patternId); + return ( + + {pattern.label} + + + ); + })} + {selectedResources.map(resourceId => { + const resource = resources.find(r => r.id === resourceId); + return ( + + {resource.label} + + + ); + })} + {startDate && ( + + From: {new Date(startDate).toLocaleDateString()} + + + )} + {endDate && ( + + To: {new Date(endDate).toLocaleDateString()} + + + )} +
+ )} +
+ ); +} diff --git a/frontend/policy-dashboards/src/components/WhereMoneyWent.jsx b/frontend/policy-dashboards/src/components/WhereMoneyWent.jsx new file mode 100644 index 0000000000000000000000000000000000000000..52f250bca6a793ae4f6206c0165f90b72bee554c --- /dev/null +++ b/frontend/policy-dashboards/src/components/WhereMoneyWent.jsx @@ -0,0 +1,162 @@ +import React from 'react'; +import Compare from './shared/Compare'; +import InsightBox from './shared/InsightBox'; +import { displacementData as d } from '../data/dashboardData'; + +/** + * Dashboard 3: What got funded instead + * (Displacement Matrix) + */ +export default function WhereMoneyWent() { + return ( +
+

+ {d.conclusion} +

+ + {/* Topic */} +
+
+ Budget Cycle +
+
+ {d.topic} +
+
+ + {/* The Matrix Table */} + + + + {['Funded (winner)', 'Stagnant (loser)', 'Trade-off factor'].map(h => ( + + ))} + + + + {d.displacements.map((row, i) => ( + + + + + + ))} + +
+ {h} +
+ {row.winner} + {row.winnerAmount && ` — $${(row.winnerAmount / 1000).toFixed(0)}k`} + + {row.loser} + {row.loserAmount > 0 && ` — $${(row.loserAmount / 1000).toFixed(0)}k`} + + + {row.tradeoffFactor} + +
+ + {/* Per-Student Spending Breakdown */} +
+

+ Health Capital Spending (Per Student) +

+ +
+ +
+

+ Athletic Capital Spending (Per Student) +

+ +

+ Source: NCES F-33 Survey, Capital Outlay by Function, FY2025 +

+
+ + {/* The Logic */} + + {d.priorityPattern}: {d.inference} + + + {/* Question for the Room */} +
+ Ask them: +

+ "This budget year, you spent $ + {(d.displacements[0].winnerAmount / 1000).toFixed(0)}k on {d.displacements[0].winner.toLowerCase()} + and $0 on {d.displacements[0].loser.toLowerCase()}. Can you explain why turf is worth more than + the dental health of 5,000 students?" +

+
+
+ ); +} diff --git a/frontend/policy-dashboards/src/components/WhoIsInCharge.jsx b/frontend/policy-dashboards/src/components/WhoIsInCharge.jsx new file mode 100644 index 0000000000000000000000000000000000000000..24ebcd6514e60e3972d6d6bc2c876bdf2bf07cb1 --- /dev/null +++ b/frontend/policy-dashboards/src/components/WhoIsInCharge.jsx @@ -0,0 +1,162 @@ +import React from 'react'; +import BarMeter from './shared/BarMeter'; +import MetricCard from './shared/MetricCard'; +import Compare from './shared/Compare'; +import InsightBox from './shared/InsightBox'; +import { influenceData as d } from '../data/dashboardData'; + +const colors = { blocker: '#E24B4A', public: '#185FA5' }; + +/** + * Dashboard 4: One memo beat 240 residents + * (Influence Radar) + */ +export default function WhoIsInCharge() { + return ( +
+

+ {d.conclusion} +

+ + {/* Topic */} +
+
+ Policy Decision +
+
+ {d.topic} +
+
+ + {/* Influence Bars */} +
+

+ Influence on Final Decision +

+ + {d.actors.map((item, i) => ( +
+ +
+ {item.contactName && `Contact: ${item.contactName}`} +
+
+ ))} +
+ + {/* Key Metrics */} +
+ + +
+ + {/* Veto Holder Callout */} +
+
+ Effective Veto Holder +
+
+ {d.vetoHolder} +
+
+ One liability memo had {d.actors.find(a => a.type === 'blocker').influence}% influence + despite {d.publicComments}+ citizen testimonies +
+
+ + {/* Liability Benchmark */} +
+

+ Successful Liability Suits in States with Screening Programs +

+ +

+ Source: National Association of School Nurses, ADA Health Policy Institute +

+
+ + {/* The Logic */} + + {d.powerStructure}: {d.inference} + + + {/* Question for the Room */} +
+ Ask them: +

+ "{d.vetoHolder}, can you please stand and explain to these {d.publicComments} citizens + why your one memo expressing 'liability concerns' outweighed their collective voice? + And can you cite a single successful lawsuit in any of the 35 states with active + school dental screening programs?" +

+
+
+ ); +} diff --git a/frontend/policy-dashboards/src/components/WordsVsDollars.jsx b/frontend/policy-dashboards/src/components/WordsVsDollars.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7e9c09a03764a88f3fbea0cfc7a508f88fc99f76 --- /dev/null +++ b/frontend/policy-dashboards/src/components/WordsVsDollars.jsx @@ -0,0 +1,151 @@ +import React from 'react'; +import BarMeter from './shared/BarMeter'; +import MetricCard from './shared/MetricCard'; +import Compare from './shared/Compare'; +import InsightBox from './shared/InsightBox'; +import { rhetoricGapData as d } from '../data/dashboardData'; + +/** + * Dashboard 1: They cut health spending while praising wellness + * (Rhetoric Gap Monitor) + */ +export default function WordsVsDollars() { + return ( +
+

+ {d.conclusion} +

+ + {/* Key Metrics */} +
+ + +
+ + {/* What They SAY */} +
+

+ What They SAY +

+ +
+

Sample quotes ({d.totalMentions} total mentions):

+ {d.sampleQuotes.slice(0, 2).map((quote, i) => ( +

"{quote}"

+ ))} +
+
+ + {/* What They FUND */} +
+

+ What They FUND +

+ + +
+ + {/* Benchmark Comparison */} +
+

+ Per-Student Health Spending Comparison +

+ +

+ Source: NCES Common Core of Data (CCD), FY2025 +

+
+ + {/* The Logic */} + + {d.inference} + + + {/* Question for the Room */} +
+ Ask them: +

+ "You've praised student wellness {d.totalMentions} times this year with {d.sentimentScore}% + positive sentiment. But you cut the health budget by ${Math.abs(d.budgetDelta).toLocaleString()}. + Which statement is true: your words or your wallet?" +

+
+
+ ); +} diff --git a/frontend/policy-dashboards/src/components/shared/BarMeter.jsx b/frontend/policy-dashboards/src/components/shared/BarMeter.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9600590390b206639bb8eb508f168dd441fc95ca --- /dev/null +++ b/frontend/policy-dashboards/src/components/shared/BarMeter.jsx @@ -0,0 +1,34 @@ +import React from 'react'; + +/** + * BarMeter Component + * Reusable horizontal bar chart for showing metrics + */ +export default function BarMeter({ label, value, max = 100, color = "#D85A30", suffix = "%" }) { + const pct = Math.min((value / max) * 100, 100); + + return ( +
+
+ {label} + + {value}{suffix} + +
+
+
+
+
+ ); +} diff --git a/frontend/policy-dashboards/src/components/shared/Compare.jsx b/frontend/policy-dashboards/src/components/shared/Compare.jsx new file mode 100644 index 0000000000000000000000000000000000000000..02244e17b5e7e02b8f33874e882359414a9d99f0 --- /dev/null +++ b/frontend/policy-dashboards/src/components/shared/Compare.jsx @@ -0,0 +1,58 @@ +import React from 'react'; + +/** + * Compare Component + * Four-column comparison: This District → Republican → Democratic → National + */ +export default function Compare({ benchmarks, metric = "value", prefix = "$", suffix = "" }) { + const buckets = [ + { key: 'thisDistrict', color: '#D85A30' }, + { key: 'republicanAvg', color: '#BA7517' }, + { key: 'democraticAvg', color: '#185FA5' }, + { key: 'nationalAvg', color: '#1D9E75' } + ]; + + return ( +
+ {buckets.map(bucket => { + const data = benchmarks[bucket.key]; + const value = typeof data === 'object' ? data[metric] : data; + const label = typeof data === 'object' ? data.label : bucket.key; + + return ( +
+
+ {prefix}{value}{suffix} +
+
+ {label} +
+
+ ); + })} +
+ ); +} diff --git a/frontend/policy-dashboards/src/components/shared/DashboardTile.jsx b/frontend/policy-dashboards/src/components/shared/DashboardTile.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a29eaf10f72506fbf4aa148848b668dcd9f41c09 --- /dev/null +++ b/frontend/policy-dashboards/src/components/shared/DashboardTile.jsx @@ -0,0 +1,171 @@ +import React from 'react'; +import { ArrowRight, TrendingUp, Clock, DollarSign, Users } from 'lucide-react'; + +/** + * DashboardTile Component + * Tile-based navigation for dashboards + */ +export default function DashboardTile({ + id, + title, + metric, + context, + summary, + discomfort, + icon: Icon, + onClick +}) { + const getDiscomfortColor = (score) => { + if (score >= 9) return '#D85A30'; + if (score >= 7) return '#BA7517'; + return '#888'; + }; + + return ( +
onClick(id)} + style={{ + background: 'white', + border: '1px solid #eee', + borderRadius: 12, + padding: 18, + cursor: 'pointer', + transition: 'all 0.2s ease', + position: 'relative', + overflow: 'hidden' + }} + onMouseEnter={(e) => { + e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)'; + e.currentTarget.style.transform = 'translateY(-2px)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.boxShadow = 'none'; + e.currentTarget.style.transform = 'translateY(0)'; + }} + > + {/* Discomfort indicator */} +
+ + {/* Icon */} + {Icon && ( +
+ +
+ )} + + {/* Title */} +

+ {title} +

+ + {/* Metrics */} +
+
+
+ {metric} +
+
+ {context} +
+
+
+ + {/* Summary */} +

+ {summary} +

+ + {/* Footer */} +
+
+ {discomfort >= 9 ? '⚠️ High Impact' : discomfort >= 7 ? '⚡ Medium Impact' : '📊 Analysis'} +
+
+ View Details + +
+
+
+ ); +} + +/** + * DashboardGrid Component + * Grid layout for dashboard tiles + */ +export function DashboardGrid({ dashboards, onNavigate }) { + const icons = [TrendingUp, Clock, DollarSign, Users]; + + return ( +
+ {dashboards.map((dashboard, i) => ( + + ))} +
+ ); +} diff --git a/frontend/policy-dashboards/src/components/shared/DecisionCard.jsx b/frontend/policy-dashboards/src/components/shared/DecisionCard.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e5eed0c69582e0ace3b61a9f162aa4f968a27d9f --- /dev/null +++ b/frontend/policy-dashboards/src/components/shared/DecisionCard.jsx @@ -0,0 +1,253 @@ +import React from 'react'; +import { Users, MessageSquare, FileText, Calendar, CheckCircle, XCircle } from 'lucide-react'; + +/** + * DecisionCard Component + * Shows individual decision with speakers, rationale, and details + */ +export default function DecisionCard({ decision, onClick }) { + const { + decision_summary, + outcome, + primary_rationale, + supporters = [], + opponents = [], + vote_result, + meeting_date, + tradeoffs_discussed = [], + evidence_cited = [], + policy_domain = 'general', + community_gap, + ntee_code + } = decision; + + const domainColors = { + health: '#1D9E75', + education: '#185FA5', + facilities: '#BA7517', + budget: '#D85A30', + personnel: '#6B4C9A', + safety: '#E24B4A', + community: '#2C7A7B', + policy: '#744210', + general: '#888' + }; + + const isApproved = outcome?.toLowerCase().includes('approved') || + outcome?.toLowerCase().includes('passed'); + + return ( +
{ + e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)'; + e.currentTarget.style.borderLeftWidth = '6px'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.boxShadow = 'none'; + e.currentTarget.style.borderLeftWidth = '4px'; + }} + > + {/* Header */} +
+
+
+ {decision_summary} +
+
+ + + {meeting_date ? new Date(meeting_date).toLocaleDateString() : 'Date unknown'} + + {vote_result && ( + + Vote: {vote_result} + + )} +
+
+ +
+ {isApproved ? : } + {outcome || 'Unknown'} +
+
+ + {/* Rationale */} + {primary_rationale && ( +
+
+ + Primary Rationale +
+
+ "{primary_rationale}" +
+
+ )} + + {/* Speakers */} + {(supporters.length > 0 || opponents.length > 0) && ( +
+ {supporters.length > 0 && ( +
+
+ + Supporters ({supporters.length}) +
+ {supporters.slice(0, 2).map((supporter, i) => ( +
+ • {typeof supporter === 'string' ? supporter : supporter.name || 'Unknown'} + {supporter.role && ({supporter.role})} +
+ ))} + {supporters.length > 2 && ( +
+ +{supporters.length - 2} more +
+ )} +
+ )} + + {opponents.length > 0 && ( +
+
+ + Opponents ({opponents.length}) +
+ {opponents.slice(0, 2).map((opponent, i) => ( +
+ • {typeof opponent === 'string' ? opponent : opponent.name || 'Unknown'} + {opponent.role && ({opponent.role})} +
+ ))} + {opponents.length > 2 && ( +
+ +{opponents.length - 2} more +
+ )} +
+ )} +
+ )} + + {/* Tradeoffs & Evidence */} +
+ {tradeoffs_discussed.length > 0 && ( + + {tradeoffs_discussed.length} tradeoff{tradeoffs_discussed.length > 1 ? 's' : ''} + + )} + {evidence_cited.length > 0 && ( + + + {evidence_cited.length} source{evidence_cited.length > 1 ? 's' : ''} + + )} + {community_gap?.nonprofit_filling_gap && ( + + 🤝 Community filling gap + + )} + {ntee_code && ( + + NTEE: {ntee_code} + + )} +
+
+ ); +} diff --git a/frontend/policy-dashboards/src/components/shared/FilterPanel.jsx b/frontend/policy-dashboards/src/components/shared/FilterPanel.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9ff1ebfbc6a8dc6018de9ea7b31d9cf03d9bbd42 --- /dev/null +++ b/frontend/policy-dashboards/src/components/shared/FilterPanel.jsx @@ -0,0 +1,240 @@ +import React, { useState } from 'react'; +import { Search, Filter, X } from 'lucide-react'; + +/** + * FilterPanel Component + * Allows filtering by policy domains/keywords and search + */ +export default function FilterPanel({ + selectedDomains = [], + onDomainToggle, + searchQuery = "", + onSearchChange, + onClear +}) { + const [showFilters, setShowFilters] = useState(false); + + const policyDomains = [ + { id: 'health', label: 'Health & Wellness', color: '#1D9E75' }, + { id: 'education', label: 'Education & Curriculum', color: '#185FA5' }, + { id: 'facilities', label: 'Facilities & Infrastructure', color: '#BA7517' }, + { id: 'budget', label: 'Budget & Finance', color: '#D85A30' }, + { id: 'personnel', label: 'Personnel & Staffing', color: '#6B4C9A' }, + { id: 'safety', label: 'Safety & Security', color: '#E24B4A' }, + { id: 'community', label: 'Community & Partnerships', color: '#2C7A7B' }, + { id: 'policy', label: 'Policy & Governance', color: '#744210' } + ]; + + const keywords = [ + 'dental', 'health', 'screening', 'wellness', 'nurse', + 'budget', 'funding', 'capital', 'expenditure', + 'facility', 'building', 'construction', 'renovation', + 'teacher', 'salary', 'contract', 'hiring', + 'curriculum', 'textbook', 'program', 'academic', + 'safety', 'security', 'police', 'emergency', + 'community', 'partnership', 'grant', 'donation' + ]; + + const hasActiveFilters = selectedDomains.length > 0 || searchQuery.length > 0; + + return ( +
+ {/* Search Bar */} +
+
+ + onSearchChange(e.target.value)} + style={{ + width: '100%', + padding: '8px 12px 8px 36px', + border: '1px solid #ddd', + borderRadius: 8, + fontSize: 13, + fontFamily: 'inherit' + }} + /> + {searchQuery && ( + + )} +
+ + + + {hasActiveFilters && ( + + )} +
+ + {/* Filter Panel */} + {showFilters && ( +
+
+
+ Policy Domains +
+
+ {policyDomains.map(domain => { + const isSelected = selectedDomains.includes(domain.id); + return ( + + ); + })} +
+
+ +
+
+ Common Keywords +
+
+ {keywords.map(keyword => ( + + ))} +
+
+
+ )} +
+ ); +} diff --git a/frontend/policy-dashboards/src/components/shared/InsightBox.jsx b/frontend/policy-dashboards/src/components/shared/InsightBox.jsx new file mode 100644 index 0000000000000000000000000000000000000000..336b1a97b6768218bf09c22012783856f5021321 --- /dev/null +++ b/frontend/policy-dashboards/src/components/shared/InsightBox.jsx @@ -0,0 +1,37 @@ +import React from 'react'; + +/** + * InsightBox Component + * Bottom summary box with "The logic" explanation + */ +export default function InsightBox({ title = "The logic", children, type = "default" }) { + const styles = { + default: { + background: '#f5f5f2', + borderLeft: 'none' + }, + warning: { + background: '#fff4e6', + borderLeft: '3px solid #BA7517' + }, + critical: { + background: '#ffe6e6', + borderLeft: '3px solid #D85A30' + } + }; + + const style = styles[type] || styles.default; + + return ( +
+ {title}: {children} +
+ ); +} diff --git a/frontend/policy-dashboards/src/components/shared/MetricCard.jsx b/frontend/policy-dashboards/src/components/shared/MetricCard.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ec1304a791725eb9a1abbe438fac2a22123dbeeb --- /dev/null +++ b/frontend/policy-dashboards/src/components/shared/MetricCard.jsx @@ -0,0 +1,36 @@ +import React from 'react'; + +/** + * MetricCard Component + * Display key metrics with optional positive/negative/neutral tone + */ +export default function MetricCard({ value, label, tone = "neutral" }) { + const colors = { + positive: "#1D9E75", + negative: "#D85A30", + neutral: "#222" + }; + + return ( +
+
+ {value} +
+
+ {label} +
+
+ ); +} diff --git a/frontend/policy-dashboards/src/index.css b/frontend/policy-dashboards/src/index.css new file mode 100644 index 0000000000000000000000000000000000000000..1e557101e2f71e32c19e3f1e0fb8047843ac4c2b --- /dev/null +++ b/frontend/policy-dashboards/src/index.css @@ -0,0 +1,31 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-size: 16px; + line-height: 1.6; +} + +html { + overflow-x: hidden; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +* { + box-sizing: border-box; +} + +button { + font-family: inherit; +} + +input { + font-family: inherit; +} diff --git a/frontend/policy-dashboards/src/index.js b/frontend/policy-dashboards/src/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2cb1087e76eb7b33e49ace3622c9e108cd1ed64c --- /dev/null +++ b/frontend/policy-dashboards/src/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000000000000000000000000000000000000..33ad091d26d8a9dc95ebdf616e217d985ec215b8 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/communityone_logo.jpg b/frontend/public/communityone_logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..67106acec272efff0b126b734c4d83681c2e18cf Binary files /dev/null and b/frontend/public/communityone_logo.jpg differ diff --git a/frontend/public/communityone_logo.svg b/frontend/public/communityone_logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..ce4f291e644305123f831ff0b98c503d5342e3cf --- /dev/null +++ b/frontend/public/communityone_logo.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + C1 + + + + diff --git a/frontend/public/communityone_logo_64.png b/frontend/public/communityone_logo_64.png new file mode 100644 index 0000000000000000000000000000000000000000..696b25f394add52200236e0c000c52be2ebdf0dd Binary files /dev/null and b/frontend/public/communityone_logo_64.png differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0aad19ae960e9e7bbfa315c0ba4968b7400d1e18 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/google6934fc6e3618949f.html b/frontend/public/google6934fc6e3618949f.html new file mode 100644 index 0000000000000000000000000000000000000000..8455eded11cce9f3642beebc17f1a9897f62d039 --- /dev/null +++ b/frontend/public/google6934fc6e3618949f.html @@ -0,0 +1 @@ +google-site-verification: google6934fc6e3618949f.html \ No newline at end of file diff --git a/frontend/public/privacyfacebook.html b/frontend/public/privacyfacebook.html new file mode 100644 index 0000000000000000000000000000000000000000..4e3b53afc5ee5f56f504657ebdc07293c78051fc --- /dev/null +++ b/frontend/public/privacyfacebook.html @@ -0,0 +1,276 @@ + + + + + + Privacy Policy - Open Navigator + + + +
+
+

🏛️ Privacy Policy

+

Last Updated: April 26, 2026

+
+ +
+

Open Navigator ("we," "our," or "us") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, disclose, and safeguard your information when you use our platform.

+
+ +
+

1. Information We Collect

+ +

1.1 Information You Provide

+

When you create an account or use our services, we may collect:

+
    +
  • Account Information: Email address, name, and profile information
  • +
  • OAuth Provider Data: When you log in via Google, Facebook, GitHub, or HuggingFace, we receive your public profile information and email address
  • +
  • User Preferences: Settings and preferences you configure within the application
  • +
+ +

1.2 Automatically Collected Information

+
    +
  • Usage Data: Pages visited, features used, and interactions with the platform
  • +
  • Device Information: Browser type, operating system, IP address
  • +
  • Cookies: We use essential cookies for authentication and session management
  • +
+
+ +
+

2. How We Use Your Information

+

We use the collected information to:

+
    +
  • Provide, maintain, and improve our services
  • +
  • Authenticate your account and maintain security
  • +
  • Personalize your experience on the platform
  • +
  • Send important updates about the service
  • +
  • Analyze usage patterns to improve functionality
  • +
  • Comply with legal obligations
  • +
+
+ +
+

3. Third-Party Authentication

+
+

OAuth Providers: We support login via Google, Facebook, GitHub, and HuggingFace. When you use these services:

+
    +
  • We only request access to your email address and basic profile information
  • +
  • We do not store your social media passwords
  • +
  • We do not post to your social media accounts
  • +
  • You can revoke our access at any time through your provider's settings
  • +
+
+
+ +
+

4. Data Storage and Security

+

We implement appropriate technical and organizational measures to protect your information:

+
    +
  • Encryption: Data is encrypted in transit using HTTPS/TLS
  • +
  • Access Controls: Strict access controls limit who can access your data
  • +
  • Secure Authentication: JWT tokens with secure secret keys
  • +
  • Regular Updates: We keep our systems updated with security patches
  • +
+

However, no method of transmission over the internet is 100% secure, and we cannot guarantee absolute security.

+
+ +
+

5. Data Sharing and Disclosure

+

We do not sell your personal information. We may share your information only in the following circumstances:

+
    +
  • With Your Consent: When you explicitly authorize us to share information
  • +
  • Service Providers: With trusted third-party services that help us operate (e.g., hosting providers)
  • +
  • Legal Requirements: When required by law, court order, or governmental authority
  • +
  • Business Transfers: In connection with a merger, acquisition, or sale of assets
  • +
+
+ +
+

6. Public Data Sources

+

Our platform aggregates publicly available information from:

+
    +
  • City council meeting minutes and transcripts
  • +
  • Government public records and budgets
  • +
  • Nonprofit organization databases (IRS Form 990 data)
  • +
  • Legislative information from state and local governments
  • +
+

This public information is not considered personal data and is used to provide civic engagement insights.

+
+ +
+

7. Your Rights and Choices

+

You have the following rights regarding your personal information:

+
    +
  • Access: Request a copy of the personal information we hold about you
  • +
  • Correction: Request correction of inaccurate information
  • +
  • Deletion: Request deletion of your account and personal data
  • +
  • Data Portability: Request your data in a portable format
  • +
  • Opt-Out: Unsubscribe from non-essential communications
  • +
+

To exercise these rights, contact us at the email address provided below.

+
+ +
+

8. Children's Privacy

+

Our service is not intended for children under 13 years of age. We do not knowingly collect personal information from children under 13. If you become aware that a child has provided us with personal information, please contact us, and we will delete such information.

+
+ +
+

9. Data Retention

+

We retain your personal information for as long as necessary to:

+
    +
  • Provide our services to you
  • +
  • Comply with legal obligations
  • +
  • Resolve disputes and enforce our agreements
  • +
+

When you delete your account, we will delete or anonymize your personal information within 30 days, except where required to retain it by law.

+
+ +
+

10. International Data Transfers

+

Your information may be transferred to and processed in countries other than your country of residence. We ensure appropriate safeguards are in place to protect your information in accordance with this Privacy Policy.

+
+ +
+

11. Changes to This Privacy Policy

+

We may update this Privacy Policy from time to time. We will notify you of any changes by:

+
    +
  • Posting the new Privacy Policy on this page
  • +
  • Updating the "Last Updated" date
  • +
  • Sending you an email notification (for material changes)
  • +
+

Your continued use of our services after changes constitutes acceptance of the updated policy.

+
+ +
+

12. Contact Us

+

If you have questions about this Privacy Policy or our privacy practices, please contact us:

+ +
+ +
+

13. Additional Information for EU/UK Users (GDPR)

+

If you are located in the European Union or United Kingdom, you have additional rights under GDPR:

+
    +
  • Legal Basis: We process your data based on consent, contract performance, and legitimate interests
  • +
  • Data Protection Officer: You may contact our DPO at privacy@communityone.com
  • +
  • Supervisory Authority: You have the right to lodge a complaint with your local data protection authority
  • +
  • Automated Decision-Making: We do not use automated decision-making or profiling that produces legal effects
  • +
+
+ +
+

14. California Privacy Rights (CCPA)

+

If you are a California resident, you have specific rights under the California Consumer Privacy Act (CCPA):

+
    +
  • Right to Know: What personal information we collect, use, and share
  • +
  • Right to Delete: Request deletion of your personal information
  • +
  • Right to Opt-Out: Opt-out of the sale of personal information (we do not sell your data)
  • +
  • Non-Discrimination: We will not discriminate against you for exercising your rights
  • +
+
+ +
+

© 2026 Community One. All rights reserved.

+

Open Navigator is an open-source project licensed under the MIT License.

+

Return to Home | View on GitHub

+
+
+ + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6b0a521b76e35cc8ab5320799efe979a3ef65587 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,70 @@ +import { Routes, Route } from 'react-router-dom' +import Layout from './components/Layout' +import Home from './pages/Home' +import HomeModern from './pages/HomeModern' +import Dashboard from './pages/Dashboard' +import Analytics from './pages/Analytics' +import Heatmap from './pages/Heatmap' +import Documents from './pages/Documents' +import Opportunities from './pages/Opportunities' +import Nonprofits from './pages/Nonprofits' +import NonprofitsHF from './pages/NonprofitsHF' +import Settings from './pages/Settings' +import PeopleFinder from './pages/PeopleFinder' +import DebateFinder from './pages/DebateGrader' +import Profile from './pages/Profile' +import Explore from './pages/Explore' +import Events from './pages/Events' +import Services from './pages/Services' +import Developers from './pages/Developers' +import Hackathons from './pages/Hackathons' +import OpenSource from './pages/OpenSource' +import AdvocacyTopics from './pages/AdvocacyTopics' +import FactChecking from './pages/FactChecking' +import UnifiedSearch from './pages/UnifiedSearch' +import JurisdictionsSearch from './pages/JurisdictionsSearch' +import PolicyMap from './pages/PolicyMap' +import BillDetail from './pages/BillDetail' + +function App() { + return ( + + {/* Modern home page without Layout (has its own header) */} + } /> + + {/* Classic home page (if needed) */} + }> + } /> + + + {/* All other pages with sidebar layout */} + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ) +} + +export default App diff --git a/frontend/src/components/AddressLookup.tsx b/frontend/src/components/AddressLookup.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f143b7af23dfbf8ff0a5501923a2160c83838409 --- /dev/null +++ b/frontend/src/components/AddressLookup.tsx @@ -0,0 +1,671 @@ +import { useState, useEffect, useRef } from 'react' +import { MapPinIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline' +import { stateNameToCode } from '../utils/stateMapping' +import { useLocation as useLocationContext } from '../contexts/LocationContext' + +interface LocationData { + address: string + state: string + county: string + city: string + latitude?: number + longitude?: number +} + +interface AddressLookupProps { + onLocationFound: (location: LocationData) => void + initialAddress?: string + compact?: boolean +} + +export default function AddressLookup({ onLocationFound, initialAddress = '', compact = false }: AddressLookupProps) { + const { clearLocation } = useLocationContext() + const [address, setAddress] = useState(initialAddress) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [suggestions, setSuggestions] = useState([]) + const [foundLocation, setFoundLocation] = useState(null) + const [showSuggestions, setShowSuggestions] = useState(false) + const [selectedIndex, setSelectedIndex] = useState(-1) + const debounceTimer = useRef(null) + const inputRef = useRef(null) + + // Fetch suggestions as user types + const fetchSuggestions = async (query: string) => { + if (query.trim().length < 3) { + setSuggestions([]) + setShowSuggestions(false) + return + } + + try { + const response = await fetch( + `https://nominatim.openstreetmap.org/search?` + + `q=${encodeURIComponent(query)}&` + + `format=json&` + + `addressdetails=1&` + + `countrycodes=us&` + + `limit=5`, + { + headers: { + 'User-Agent': 'CommunityOne-Navigator/1.0' + } + } + ) + + if (!response.ok) { + return + } + + const data = await response.json() + + // Deduplicate results using OSM unique IDs + const uniqueResults = data.reduce((acc: any[], current: any) => { + const osmKey = `${current.osm_type}_${current.osm_id}` + const exists = acc.some((item) => { + const itemKey = `${item.osm_type}_${item.osm_id}` + return itemKey === osmKey + }) + if (!exists) { + acc.push(current) + } + return acc + }, []) + + setSuggestions(uniqueResults) + setShowSuggestions(uniqueResults.length > 0) + setSelectedIndex(-1) + } catch (err) { + console.error('Autocomplete error:', err) + } + } + + // Handle address input change with debouncing + const handleAddressChange = (value: string) => { + setAddress(value) + setError(null) + + // Clear previous timer + if (debounceTimer.current) { + clearTimeout(debounceTimer.current) + } + + // Set new timer + debounceTimer.current = setTimeout(() => { + fetchSuggestions(value) + }, 300) + } + + // Cleanup timer on unmount + useEffect(() => { + return () => { + if (debounceTimer.current) { + clearTimeout(debounceTimer.current) + } + } + }, []) + + const lookupAddress = async (addressToLookup: string) => { + if (!addressToLookup.trim()) { + setError('Please enter an address') + return + } + + setIsLoading(true) + setError(null) + setSuggestions([]) + setShowSuggestions(false) + + try { + // Use Nominatim (OpenStreetMap) geocoding service + const response = await fetch( + `https://nominatim.openstreetmap.org/search?` + + `q=${encodeURIComponent(addressToLookup)}&` + + `format=json&` + + `addressdetails=1&` + + `countrycodes=us&` + + `limit=5`, + { + headers: { + 'User-Agent': 'CommunityOne-Navigator/1.0' + } + } + ) + + if (!response.ok) { + throw new Error('Failed to lookup address') + } + + const data = await response.json() + + if (data.length === 0) { + setError('Address not found. Please try a different address or be more specific.') + return + } + + // Deduplicate results using OSM unique IDs + const uniqueResults = data.reduce((acc: any[], current: any) => { + // Use OSM type + ID as unique key (most reliable) + const osmKey = `${current.osm_type}_${current.osm_id}` + + const exists = acc.some((item) => { + const itemKey = `${item.osm_type}_${item.osm_id}` + return itemKey === osmKey + }) + + if (!exists) { + acc.push(current) + } + return acc + }, []) + + // If we have multiple unique results, show suggestions + if (uniqueResults.length > 1) { + setSuggestions(uniqueResults) + setShowSuggestions(true) + return + } + + // Single result - process it + processResult(uniqueResults[0]) + } catch (err) { + console.error('Address lookup error:', err) + setError('Failed to lookup address. Please try again.') + } finally { + setIsLoading(false) + } + } + + const processResult = (result: any) => { + const addr = result.address + + // Convert state name to 2-letter code + const stateName = addr.state || '' + const stateCode = stateNameToCode(stateName) + console.log(`🗺️ [AddressLookup] State conversion: "${stateName}" → "${stateCode}"`) + + const locationData: LocationData = { + address: result.display_name, + state: stateCode, + county: addr.county || '', + city: addr.city || addr.town || addr.village || addr.municipality || '', + latitude: parseFloat(result.lat), + longitude: parseFloat(result.lon), + } + + // Validate we got the essential data + if (!locationData.state || !locationData.city) { + setError('Could not determine city and state from this address. Please be more specific.') + setSuggestions([]) + setShowSuggestions(false) + return + } + + console.log('📍 [AddressLookup] Location found:', locationData) + setSuggestions([]) + setShowSuggestions(false) + setFoundLocation(locationData) + onLocationFound(locationData) + } + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + + // If a suggestion is selected, use that + if (selectedIndex >= 0 && suggestions[selectedIndex]) { + processResult(suggestions[selectedIndex]) + } else { + lookupAddress(address) + } + } + + const handleSuggestionClick = (suggestion: any) => { + setAddress(suggestion.display_name) + processResult(suggestion) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!showSuggestions || suggestions.length === 0) return + + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + setSelectedIndex(prev => + prev < suggestions.length - 1 ? prev + 1 : prev + ) + break + case 'ArrowUp': + e.preventDefault() + setSelectedIndex(prev => prev > 0 ? prev - 1 : -1) + break + case 'Enter': + if (selectedIndex >= 0) { + e.preventDefault() + processResult(suggestions[selectedIndex]) + } + break + case 'Escape': + setShowSuggestions(false) + setSelectedIndex(-1) + break + } + } + + const useMyLocation = () => { + if (!navigator.geolocation) { + setError('Geolocation is not supported by your browser') + return + } + + setIsLoading(true) + setError(null) + setSuggestions([]) + + navigator.geolocation.getCurrentPosition( + async (position) => { + const { latitude, longitude } = position.coords + + try { + // Reverse geocode using Nominatim + const response = await fetch( + `https://nominatim.openstreetmap.org/reverse?` + + `lat=${latitude}&` + + `lon=${longitude}&` + + `format=json&` + + `addressdetails=1`, + { + headers: { + 'User-Agent': 'CommunityOne-Navigator/1.0' + } + } + ) + + if (!response.ok) { + throw new Error('Failed to reverse geocode location') + } + + const data = await response.json() + + // Update the address input field + setAddress(data.display_name) + + // Process the result + processResult(data) + } catch (err) { + console.error('Reverse geocoding error:', err) + setError('Failed to determine your location. Please enter your address manually.') + } finally { + setIsLoading(false) + } + }, + (error) => { + console.error('Geolocation error:', error) + setIsLoading(false) + + switch (error.code) { + case error.PERMISSION_DENIED: + setError('Location access denied. Please enter your address manually or enable location permissions.') + break + case error.POSITION_UNAVAILABLE: + setError('Location information unavailable. Please enter your address manually.') + break + case error.TIMEOUT: + setError('Location request timed out. Please try again or enter your address manually.') + break + default: + setError('An error occurred while getting your location. Please enter your address manually.') + } + }, + { + enableHighAccuracy: false, // Use fast network-based location instead of GPS + timeout: 5000, // Reduced timeout since network location is faster + maximumAge: 30000 // Allow 30s cached location for faster response + } + ) + } + + if (compact) { + return ( +
+
+ handleAddressChange(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Enter your address..." + className="w-full px-4 py-2 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-gray-900" + disabled={isLoading} + autoComplete="off" + /> + + + + {/* Autocomplete suggestions dropdown */} + {showSuggestions && suggestions.length > 0 && ( +
+ {suggestions.map((suggestion, index) => { + const addr = suggestion.address + const locationName = addr.city || addr.town || addr.village || addr.county || 'Unknown' + return ( + + ) + })} +
+ )} +
+ {error && ( +

{error}

+ )} +
+ ) + } + + return ( +
+
+
+ +
+ handleAddressChange(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="123 Main St, Los Angeles, CA 90001" + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 text-base text-gray-900" + disabled={isLoading} + autoComplete="off" + /> + + {/* Autocomplete suggestions dropdown */} + {showSuggestions && suggestions.length > 0 && ( +
+ {suggestions.map((suggestion, index) => { + const addr = suggestion.address + const locationName = addr.city || addr.town || addr.village || addr.county || 'Unknown' + return ( + + ) + })} +
+ )} +
+

+ We'll find your local organizations based on your address +

+ + {/* Use My Location Button */} +
+ +
+ + {/* Divider */} +
+
+
+
+
+ or enter manually +
+
+
+ + +
+ + {/* Error Message */} + {error && ( +
+

{error}

+
+ )} + + {/* Note: Suggestions now appear as autocomplete dropdown above */} + + {/* Location Results */} + {foundLocation && !compact && ( +
+
+

+ + Your Local Community +

+ +
+
+

+ Select a jurisdiction level below to explore organizations, meeting minutes, and contacts: +

+
+ {/* City */} + {foundLocation.city && ( + + )} + + {/* County */} + {foundLocation.county && ( + + )} + + {/* State */} + {foundLocation.state && ( + + )} + + {/* School District */} + {foundLocation.city && ( + + )} +
+ + {/* Action Buttons */} +
+

Quick access to all local resources:

+
+ + +
+
+ + {/* Start Over */} +
+ +
+
+
+ )} +
+ ) +} diff --git a/frontend/src/components/FollowButton.tsx b/frontend/src/components/FollowButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e7972041d2aaed7dd15082ce19836b2fa98dfe2b --- /dev/null +++ b/frontend/src/components/FollowButton.tsx @@ -0,0 +1,160 @@ +import { useState } from 'react' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import api from '../lib/api' +import { UserPlusIcon, UserMinusIcon } from '@heroicons/react/24/outline' +import { CheckIcon } from '@heroicons/react/24/solid' + +interface FollowButtonProps { + type: 'user' | 'leader' | 'organization' | 'cause' + id: number + initialFollowing?: boolean + initialCount?: number + showCount?: boolean + compact?: boolean + onFollowChange?: (following: boolean, count: number) => void +} + +export default function FollowButton({ + type, + id, + initialFollowing = false, + initialCount = 0, + showCount = true, + compact = false, + onFollowChange +}: FollowButtonProps) { + const [isFollowing, setIsFollowing] = useState(initialFollowing) + const [followerCount, setFollowerCount] = useState(initialCount) + const [isHovered, setIsHovered] = useState(false) + const queryClient = useQueryClient() + + const followMutation = useMutation({ + mutationFn: async () => { + const endpoint = `/social/follow/${type}/${id}` + const response = await api.post(endpoint) + return response.data + }, + onSuccess: (data) => { + setIsFollowing(true) + setFollowerCount(data.follower_count) + onFollowChange?.(true, data.follower_count) + // Invalidate relevant queries + queryClient.invalidateQueries({ queryKey: ['social', 'stats'] }) + queryClient.invalidateQueries({ queryKey: ['following', type] }) + } + }) + + const unfollowMutation = useMutation({ + mutationFn: async () => { + const endpoint = `/social/follow/${type}/${id}` + const response = await api.delete(endpoint) + return response.data + }, + onSuccess: (data) => { + setIsFollowing(false) + setFollowerCount(data.follower_count) + onFollowChange?.(false, data.follower_count) + // Invalidate relevant queries + queryClient.invalidateQueries({ queryKey: ['social', 'stats'] }) + queryClient.invalidateQueries({ queryKey: ['following', type] }) + } + }) + + const handleClick = () => { + if (isFollowing) { + unfollowMutation.mutate() + } else { + followMutation.mutate() + } + } + + const isLoading = followMutation.isPending || unfollowMutation.isPending + + // LinkedIn/Facebook style button + if (compact) { + return ( + + ) + } + + // Full button with count + return ( +
+ + + {showCount && ( + + {followerCount.toLocaleString()} {followerCount === 1 ? 'follower' : 'followers'} + + )} +
+ ) +} diff --git a/frontend/src/components/JurisdictionDiscovery.tsx b/frontend/src/components/JurisdictionDiscovery.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1d23454dc134702552b77cd589a8c365f9782bc4 --- /dev/null +++ b/frontend/src/components/JurisdictionDiscovery.tsx @@ -0,0 +1,268 @@ +import { useState } from 'react' +import { + ChevronDownIcon, + ChevronUpIcon, + CheckCircleIcon, + GlobeAltIcon, + VideoCameraIcon, + DocumentTextIcon, + ShareIcon +} from '@heroicons/react/24/outline' + +interface JurisdictionDiscoveryProps { + jurisdiction: { + name: string + state: string + website?: string + youtube_channels?: string[] + facebook?: string + twitter?: string + agenda_portal?: string + meeting_platform?: string + completeness: number + } +} + +export default function JurisdictionDiscovery({ jurisdiction }: JurisdictionDiscoveryProps) { + const [isExpanded, setIsExpanded] = useState(false) + + const hasData = jurisdiction.website || jurisdiction.youtube_channels?.length || jurisdiction.facebook + + return ( +
+ {/* Header - Always Visible */} +
+
+
+
+ +

+ {jurisdiction.name}, {jurisdiction.state} - DISCOVERY COMPLETE! +

+
+ + {/* Summary Stats */} +
+ {jurisdiction.website && ( + + + Website + + )} + {jurisdiction.youtube_channels && jurisdiction.youtube_channels.length > 0 && ( + + + {jurisdiction.youtube_channels.length} YouTube Channel{jurisdiction.youtube_channels.length > 1 ? 's' : ''} + + )} + {jurisdiction.agenda_portal && ( + + + Agenda Portal + + )} + {(jurisdiction.facebook || jurisdiction.twitter) && ( + + + Social Media + + )} +
+ + {/* Completeness Bar */} + {hasData && ( +
+
+
+
+
+ + {jurisdiction.completeness}% + +
+

+ Completeness: ~{Math.round(jurisdiction.completeness)}% - { + jurisdiction.completeness >= 75 ? 'Good' : + jurisdiction.completeness >= 50 ? 'Fair' : + 'Limited' + } digital infrastructure! +

+
+ )} +
+ + {/* Expand/Collapse Button */} + {hasData && ( + + )} +
+
+ + {/* Expandable Details */} + {isExpanded && hasData && ( +
+

+ 🎯 {jurisdiction.name.toUpperCase()}, {jurisdiction.state} FINDINGS +

+ +
+ {/* Website */} + {jurisdiction.website && ( +
+
🌐 Official Website:
+ + ✅ {jurisdiction.website} + +
+ )} + + {/* Agenda Portal */} + {jurisdiction.agenda_portal && ( +
+
📄 Meeting/Agenda Portal:
+ + ✅ {jurisdiction.agenda_portal} + +
+ )} + + {/* YouTube Channels */} + {jurisdiction.youtube_channels && jurisdiction.youtube_channels.length > 0 && ( +
+
📺 YouTube Channels:
+ {jurisdiction.youtube_channels.map((channel, idx) => ( +
+ ✅ @{channel} +
+ ))} +
+ )} + + {/* Social Media */} + {(jurisdiction.facebook || jurisdiction.twitter) && ( +
+
📱 Social Media:
+
+ {jurisdiction.facebook && ( +
+ ✅ Facebook: {jurisdiction.facebook} +
+ )} + {jurisdiction.twitter && ( +
+ ✅ Twitter: {jurisdiction.twitter} +
+ )} +
+
+ )} + + {/* Meeting Platform */} + {jurisdiction.meeting_platform && ( +
+
🏛️ Meeting Platform:
+
+ {jurisdiction.meeting_platform} +
+
+ )} + + {/* Summary Table */} +
+
📊 {jurisdiction.name.toUpperCase()} SUMMARY
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryFoundDetails
Website{jurisdiction.website ? '✅' : '❌'} + {jurisdiction.website ? new URL(jurisdiction.website).hostname : 'Not found'} +
YouTube{jurisdiction.youtube_channels?.length ? '✅' : '❌'} + {jurisdiction.youtube_channels?.length || 0} channel{jurisdiction.youtube_channels?.length !== 1 ? 's' : ''} +
Agendas{jurisdiction.agenda_portal ? '✅' : '❌'} + {jurisdiction.agenda_portal ? 'Portal found' : 'Not available'} +
Social{jurisdiction.facebook || jurisdiction.twitter ? '✅' : '❌'} + {[jurisdiction.facebook && 'Facebook', jurisdiction.twitter && 'Twitter'].filter(Boolean).join(', ') || 'None'} +
Platform{jurisdiction.meeting_platform ? '✅' : '❌'} + {jurisdiction.meeting_platform || 'Unknown'} +
+
+ + {/* Key Takeaway */} +
+
💡 KEY TAKEAWAY
+

+ The automation successfully discovered: +

+
    + {jurisdiction.website &&
  • ✅ Official website (automatic)
  • } + {(jurisdiction.youtube_channels?.length ?? 0) > 0 &&
  • ✅ YouTube channels (automatic)
  • } + {jurisdiction.agenda_portal &&
  • ✅ Agenda portal (found via link scanning)
  • } + {(jurisdiction.facebook || jurisdiction.twitter) &&
  • ✅ Social media (automatic)
  • } +
+
+
+
+ )} + + {/* No Data Message */} + {!hasData && ( +
+

No discovery data available yet. Run discovery pipeline to populate.

+
+ )} +
+ ) +} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1971a094acc51b0238b2984740f587fa1709ab90 --- /dev/null +++ b/frontend/src/components/Layout.tsx @@ -0,0 +1,498 @@ +import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom' +import { useState, Fragment } from 'react' +import { Menu, Transition } from '@headlessui/react' +import { + HomeIcon, + MapIcon, + DocumentTextIcon, + BellAlertIcon, + BuildingLibraryIcon, + Cog6ToothIcon, + ChartBarIcon, + MagnifyingGlassIcon, + BookOpenIcon, + UserGroupIcon, + AcademicCapIcon, + Bars3Icon, + XMarkIcon, + UserCircleIcon, + ArrowRightOnRectangleIcon, + ChevronDownIcon, + MapPinIcon, + HeartIcon, + CodeBracketIcon, +} from '@heroicons/react/24/outline' +import { useAuth } from '../contexts/AuthContext' +import { useLocation as useLocationContext } from '../contexts/LocationContext' + +const navigation = [ + { name: 'Home', href: '/', icon: HomeIcon }, + { name: 'Explore Data', href: '/explore', icon: MagnifyingGlassIcon }, + { name: 'Search', href: '/search', icon: MagnifyingGlassIcon }, + { name: 'Jurisdictions', href: '/jurisdictions', icon: MapPinIcon }, + { + section: 'Families & Individuals', + items: [ + { name: 'Community Events', href: '/events', icon: BookOpenIcon }, + { name: 'Services & Resources', href: '/services', icon: HeartIcon }, + ] + }, + { + section: 'Policy & Government', + items: [ + { name: 'Policy Decisions', href: '/documents', icon: DocumentTextIcon }, + { name: 'Budget Analysis', href: '/analytics', icon: ChartBarIcon }, + { name: 'Elected Officials', href: '/people', icon: UserGroupIcon }, + { name: 'Policy Map', href: '/policy-map', icon: MapIcon }, + ] + }, + { + section: 'Community & Advocacy', + items: [ + { name: 'Nonprofits', href: '/nonprofits', icon: BuildingLibraryIcon }, + { name: 'Advocacy Topics', href: '/advocacy-topics', icon: BellAlertIcon }, + { name: 'Fact-Checking', href: '/fact-checking', icon: AcademicCapIcon }, + ] + }, + { + section: 'Developers', + items: [ + { name: 'Open Source', href: '/opensource', icon: CodeBracketIcon }, + { name: 'Hackathons', href: '/hackathons', icon: AcademicCapIcon }, + ] + }, + { name: 'Settings', href: '/settings', icon: Cog6ToothIcon }, +] + +export default function Layout() { + const location = useLocation() + const navigate = useNavigate() + const [searchQuery, setSearchQuery] = useState('') + const [mobileMenuOpen, setMobileMenuOpen] = useState(false) + const [showLoginMenu, setShowLoginMenu] = useState(false) + const { user, isAuthenticated, login, logout, isLoading } = useAuth() + const { location: userLocation, hasLocation } = useLocationContext() + + // Environment-aware URLs + const docsUrl = import.meta.env.PROD ? 'https://www.communityone.com/docs/intro' : 'http://localhost:3000/docs/intro' + const apiDocsUrl = import.meta.env.PROD ? 'https://www.communityone.com/api/docs' : 'http://localhost:8000/docs' + + const handleSearch = (e: React.FormEvent) => { + e.preventDefault() + if (searchQuery.trim()) { + navigate(`/search?q=${encodeURIComponent(searchQuery)}`) + } + } + + return ( +
+ {/* Top Header Bar */} +
+
+
+ {/* Mobile menu button */} + + + + CommunityOne Logo +

+ Open Navigator +

+ +
+ + {/* Global Search - Hidden on home page and mobile */} + {location.pathname !== '/' && ( +
+
+ setSearchQuery(e.target.value)} + className="w-full px-4 py-2 pl-10 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent" + /> + +
+
+ )} + + {/* Header Actions */} +
+ {/* Location Banner - Compact */} + {hasLocation && userLocation && ( +
+ +
+
+ {userLocation.city}, {userLocation.state} +
+ {userLocation.county && ( +
{userLocation.county}
+ )} +
+ +
+ )} + {/* Authentication */} + {isLoading ? ( +
+
+
+ ) : isAuthenticated && user ? ( + + + {user.avatar_url ? ( + {user.full_name { + // If image fails to load, hide it and show fallback + e.currentTarget.style.display = 'none'; + const fallback = e.currentTarget.nextElementSibling as HTMLElement | null; + if (fallback) fallback.style.display = 'flex'; + }} + /> + ) : null} +
+ {(user.full_name || user.username || user.email).charAt(0).toUpperCase()} +
+ + {user.full_name || user.username || user.email.split('@')[0]} + + +
+ + + +
+
+ {user.avatar_url ? ( + {user.full_name { + // If image fails to load, hide it and show fallback + e.currentTarget.style.display = 'none'; + const fallback = e.currentTarget.nextElementSibling as HTMLElement | null; + if (fallback) fallback.style.display = 'flex'; + }} + /> + ) : null} +
+ {(user.full_name || user.username || user.email).charAt(0).toUpperCase()} +
+
+

+ {user.full_name || user.username || user.email.split('@')[0]} +

+

+ {user.email} +

+
+
+ {user.oauth_provider && ( +
+ Signed in via + {user.oauth_provider} +
+ )} +
+
+ + {({ active }) => ( + + )} + + + {({ active }) => ( + + )} + + + {({ active }) => ( + + )} + +
+
+
+
+ ) : ( +
+ + + {showLoginMenu && ( +
+
+

Sign in with:

+
+ + + +
+ +
+ )} +
+ )} + + + + Docs + + e.currentTarget.style.backgroundColor = '#2e4346'} + onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#354F52'} + > + API + +
+
+
+ + {/* Sidebar */} +
+ + + {/* Sidebar Footer */} +
+
+
Open Data Sources
+
+ • 925 Jurisdictions
+ • 43,726 Nonprofits
+ • 6,913 Meeting Pages
+ • 362 Officials +
+
+ + 📍 Request Jurisdiction Coverage + +
+
+
+
+ + {/* Mobile menu overlay */} + {mobileMenuOpen && ( +
setMobileMenuOpen(false)} + /> + )} + + {/* Main content */} +
+
+ +
+
+
+ ) +} diff --git a/frontend/src/components/RegistrationModal.tsx b/frontend/src/components/RegistrationModal.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5fa88a9afbb501b63baf67fa6e3cfaa8c3369211 --- /dev/null +++ b/frontend/src/components/RegistrationModal.tsx @@ -0,0 +1,216 @@ +import { Fragment, useState } from 'react' +import { Dialog, Transition } from '@headlessui/react' +import { XMarkIcon, MapPinIcon } from '@heroicons/react/24/outline' + +interface RegistrationModalProps { + isOpen: boolean + onClose: () => void + onComplete: (data: LocationData) => void + initialData?: { + state?: string + county?: string + city?: string + school_board?: string + } +} + +interface LocationData { + state: string + county: string + city: string + school_board: string +} + +export default function RegistrationModal({ isOpen, onClose, onComplete, initialData }: RegistrationModalProps) { + const [formData, setFormData] = useState({ + state: initialData?.state || '', + county: initialData?.county || '', + city: initialData?.city || '', + school_board: initialData?.school_board || '', + }) + + const [errors, setErrors] = useState>({}) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + + // Validate required fields + const newErrors: Record = {} + if (!formData.state.trim()) newErrors.state = 'State is required' + if (!formData.county.trim()) newErrors.county = 'County is required' + if (!formData.city.trim()) newErrors.city = 'City is required' + + if (Object.keys(newErrors).length > 0) { + setErrors(newErrors) + return + } + + onComplete(formData) + } + + const handleChange = (field: keyof LocationData, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })) + // Clear error for this field + if (errors[field]) { + setErrors(prev => { + const newErrors = { ...prev } + delete newErrors[field] + return newErrors + }) + } + } + + return ( + + + +
+ + +
+
+ + +
+
+ + + Complete Your Profile + +
+ +
+ +

+ Help us personalize your experience by telling us where you're located. + You can update this information anytime in Settings. +

+ +
+ {/* State */} +
+ + handleChange('state', e.target.value)} + placeholder="e.g., California, Texas, New York" + className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${ + errors.state ? 'border-red-500' : 'border-gray-300' + }`} + /> + {errors.state && ( +

{errors.state}

+ )} +
+ + {/* County */} +
+ + handleChange('county', e.target.value)} + placeholder="e.g., Los Angeles County, Harris County" + className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${ + errors.county ? 'border-red-500' : 'border-gray-300' + }`} + /> + {errors.county && ( +

{errors.county}

+ )} +
+ + {/* City */} +
+ + handleChange('city', e.target.value)} + placeholder="e.g., Los Angeles, Houston, New York" + className={`w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 ${ + errors.city ? 'border-red-500' : 'border-gray-300' + }`} + /> + {errors.city && ( +

{errors.city}

+ )} +
+ + {/* School Board */} +
+ + handleChange('school_board', e.target.value)} + placeholder="e.g., LAUSD, Houston ISD" + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500" + /> +
+ + {/* Submit Button */} +
+ + +
+
+
+
+
+
+
+
+ ) +} diff --git a/frontend/src/components/ScrollToTop.tsx b/frontend/src/components/ScrollToTop.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9e3aec0633b794b09b859c5ee4375ef975dca7fc --- /dev/null +++ b/frontend/src/components/ScrollToTop.tsx @@ -0,0 +1,16 @@ +import { useEffect } from 'react' +import { useLocation } from 'react-router-dom' + +/** + * ScrollToTop component that automatically scrolls to the top of the page + * when the route changes or the page is refreshed. + */ +export default function ScrollToTop() { + const { pathname } = useLocation() + + useEffect(() => { + window.scrollTo(0, 0) + }, [pathname]) + + return null +} diff --git a/frontend/src/components/SocialStats.tsx b/frontend/src/components/SocialStats.tsx new file mode 100644 index 0000000000000000000000000000000000000000..ca2ac2e14b78f84d6a90ac0437b0b3e49112658f --- /dev/null +++ b/frontend/src/components/SocialStats.tsx @@ -0,0 +1,121 @@ +import { useQuery } from '@tanstack/react-query' +import api from '../lib/api' +import { UserGroupIcon, UserPlusIcon } from '@heroicons/react/24/outline' +import { Link } from 'react-router-dom' + +interface SocialStatsProps { + userId?: number + showBreakdown?: boolean + clickable?: boolean +} + +interface FollowerStats { + followers: number + following: number + following_users: number + following_leaders: number + following_organizations: number + following_causes: number +} + +export default function SocialStats({ + userId, + showBreakdown = false, + clickable = true +}: SocialStatsProps) { + const { data: stats, isLoading } = useQuery({ + queryKey: ['social', 'stats', userId], + queryFn: async () => { + const params = userId ? `?user_id=${userId}` : '' + const response = await api.get(`/social/stats${params}`) + return response.data + } + }) + + if (isLoading) { + return ( +
+
+
+
+ ) + } + + if (!stats) return null + + const StatsContent = () => ( + <> + {/* LinkedIn-style stat display */} +
+
+ + + {stats.followers.toLocaleString()} + + + {stats.followers === 1 ? 'Follower' : 'Followers'} + +
+ +
+ + + {stats.following.toLocaleString()} + + Following +
+
+ + {/* Breakdown of what they're following */} + {showBreakdown && stats.following > 0 && ( +
+ {stats.following_leaders > 0 && ( +
+
+ {stats.following_leaders} +
+
Leaders
+
+ )} + + {stats.following_organizations > 0 && ( +
+
+ {stats.following_organizations} +
+
Charities
+
+ )} + + {stats.following_causes > 0 && ( +
+
+ {stats.following_causes} +
+
Causes
+
+ )} + + {stats.following_users > 0 && ( +
+
+ {stats.following_users} +
+
People
+
+ )} +
+ )} + + ) + + if (clickable) { + return ( + + + + ) + } + + return +} diff --git a/frontend/src/components/USMap.tsx b/frontend/src/components/USMap.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d201b0dd53d20d451b4a226a3e1880ab23ddd400 --- /dev/null +++ b/frontend/src/components/USMap.tsx @@ -0,0 +1,543 @@ +// @ts-nocheck +import { useState, useRef, useEffect } from 'react' +import { Link } from 'react-router-dom' +import { ComposableMap, Geographies, Geography } from 'react-simple-maps' + +const geoUrl = 'https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json' + +interface BillSample { + bill_number: string + title: string + status: string + type: string + action: string + date?: string // Format: "Jan 2026" + state: string +} + +interface StateData { + state: string + total_bills: number + type_counts: { + ban: number + restriction: number + protection: number + other: number + } + status_counts: { + enacted: number + failed: number + pending: number + } + primary_type: string + primary_status: string + map_category: string + sample_bills?: BillSample[] +} + +interface USMapProps { + stateData: Record + onStateClick?: (stateCode: string) => void + legend?: { + types: Record + statuses: Record + } +} + +// State code to FIPS mapping +const STATE_FIPS: Record = { + 'AL': '01', 'AK': '02', 'AZ': '04', 'AR': '05', 'CA': '06', + 'CO': '08', 'CT': '09', 'DE': '10', 'FL': '12', 'GA': '13', + 'HI': '15', 'ID': '16', 'IL': '17', 'IN': '18', 'IA': '19', + 'KS': '20', 'KY': '21', 'LA': '22', 'ME': '23', 'MD': '24', + 'MA': '25', 'MI': '26', 'MN': '27', 'MS': '28', 'MO': '29', + 'MT': '30', 'NE': '31', 'NV': '32', 'NH': '33', 'NJ': '34', + 'NM': '35', 'NY': '36', 'NC': '37', 'ND': '38', 'OH': '39', + 'OK': '40', 'OR': '41', 'PA': '42', 'RI': '44', 'SC': '45', + 'SD': '46', 'TN': '47', 'TX': '48', 'UT': '49', 'VT': '50', + 'VA': '51', 'WA': '53', 'WV': '54', 'WI': '55', 'WY': '56', + 'DC': '11', 'PR': '72' +} + +const FIPS_STATE: Record = Object.fromEntries( + Object.entries(STATE_FIPS).map(([k, v]) => [v, k]) +) + +// Flexible color palette for different legislation types +const TYPE_COLOR_PALETTE: Record = { + // Fluoridation categories + 'mandate': '#4CAF50', // Green - Mandate + 'removal': '#F44336', // Red - Removal + 'study': '#9C27B0', // Purple - Study + + // Dental/Oral Health categories + 'coverage_expansion': '#4CAF50', // Green - Expansion + 'screening': '#FF9800', // Orange - Screening + 'provider_access': '#2196F3', // Blue - Provider Access + + // Medicaid categories + 'expansion': '#4CAF50', // Green - Expansion + 'coverage': '#2196F3', // Blue - Coverage + 'reimbursement': '#FF9800', // Orange - Reimbursement + 'eligibility': '#9C27B0', // Purple - Eligibility + + // Education categories + 'requirement': '#FF9800', // Orange - Requirement + 'curriculum': '#2196F3', // Blue - Curriculum + 'reform': '#9C27B0', // Purple - Reform + + // Health/General categories + 'protection': '#4CAF50', // Green - Protection + 'restriction': '#F44336', // Red - Restriction + + // Generic categories + 'support': '#4CAF50', // Green - Support + 'oppose': '#F44336', // Red - Oppose + 'regulate': '#FF9800', // Orange - Regulate + + // Shared + 'funding': '#2196F3', // Blue - Funding (appears in multiple) + 'other': '#9E9E9E', // Gray - Other +} + +// Get color for any category with fallback +const getColorForCategory = (category: string): string => { + return TYPE_COLOR_PALETTE[category] || '#9E9E9E' // Default to gray +} + +// Color scheme based on the user's description +const getStateColor = (stateCode: string, stateData: Record): string => { + const data = stateData[stateCode] + + if (!data || data.total_bills === 0) { + return '#E3F2FD' // Light blue - no legislation + } + + const { primary_type, primary_status } = data + + // Get base color for this type + let baseColor = getColorForCategory(primary_type) + + // Adjust shade based on status + if (primary_status === 'enacted') { + // Slightly darker for enacted (reduce lightness) + return adjustColorBrightness(baseColor, -20) + } else if (primary_status === 'failed') { + // Lighter for failed (increase lightness) + return adjustColorBrightness(baseColor, 40) + } + + return baseColor +} + +// Helper to adjust color brightness +const adjustColorBrightness = (hex: string, percent: number): string => { + // Simple brightness adjustment + const num = parseInt(hex.replace('#', ''), 16) + const amt = Math.round(2.55 * percent) + const R = (num >> 16) + amt + const G = (num >> 8 & 0x00FF) + amt + const B = (num & 0x0000FF) + amt + + return '#' + ( + 0x1000000 + + (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 + + (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 + + (B < 255 ? (B < 1 ? 0 : B) : 255) + ).toString(16).slice(1).toUpperCase() +} + +const getPatternForState = (stateCode: string, stateData: Record): string | null => { + const data = stateData[stateCode] + + if (!data || data.total_bills === 0) { + return null + } + + const { primary_status } = data + + if (primary_status === 'failed') { + return 'crosshatch' + } else if (primary_status === 'enacted') { + return 'diagonal' + } + + return null +} + +export default function USMap({ stateData, onStateClick, legend }: USMapProps) { + const [hoveredState, setHoveredState] = useState(null) + const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }) + const hoverTimeoutRef = useRef(null) + const hoveredStateElementRef = useRef(null) + const containerRef = useRef(null) + + // Get unique types from actual state data if legend not provided + const legislationTypes = legend?.types || {} + const legislationStatuses = legend?.statuses || { + 'enacted': 'Enacted', + 'failed': 'Failed', + 'pending': 'Pending' + } + + // Helper to calculate position relative to container + const calculateRelativePosition = (element: any) => { + if (!element || !containerRef.current) return { x: 0, y: 0 } + + const stateBounds = element.getBoundingClientRect() + const containerBounds = containerRef.current.getBoundingClientRect() + + return { + x: stateBounds.left - containerBounds.left + stateBounds.width / 2, + y: stateBounds.top - containerBounds.top + } + } + + // Update tooltip position on scroll + useEffect(() => { + const updateTooltipPosition = () => { + if (hoveredState && hoveredStateElementRef.current) { + setTooltipPosition(calculateRelativePosition(hoveredStateElementRef.current)) + } + } + + if (hoveredState) { + window.addEventListener('scroll', updateTooltipPosition, true) + return () => window.removeEventListener('scroll', updateTooltipPosition, true) + } + }, [hoveredState]) + + const handleMouseEnter = (event: any, stateCode: string) => { + // Clear any pending state change + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current) + hoverTimeoutRef.current = null + } + + // Store the element reference for scroll updates + hoveredStateElementRef.current = event.target + + // If no tooltip showing, show immediately + if (!hoveredState) { + setHoveredState(stateCode) + setTooltipPosition(calculateRelativePosition(event.target)) + } + // If tooltip already showing for different state, delay switch + // This prevents accidental switches when moving mouse to tooltip + else if (hoveredState !== stateCode) { + hoverTimeoutRef.current = setTimeout(() => { + setHoveredState(stateCode) + setTooltipPosition(calculateRelativePosition(event.target)) + }, 200) // 200ms delay prevents accidental switches + } + } + + const handleMouseLeave = () => { + // Clear any pending state change when leaving + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current) + hoverTimeoutRef.current = null + } + } + + // Close button handler + const handleCloseTooltip = () => { + setHoveredState(null) + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current) + hoverTimeoutRef.current = null + } + } + + const hoveredData = hoveredState ? stateData[hoveredState] : null + + return ( +
+ {/* SVG Patterns for overlays */} + + + {/* Crosshatch pattern for failed */} + + + + + + {/* Diagonal stripes for enacted */} + + + + + + + + + {({ geographies }) => + geographies.map((geo) => { + const fips = geo.id + const stateCode = FIPS_STATE[fips] || fips + const data = stateData[stateCode] + const pattern = getPatternForState(stateCode, stateData) + + return ( + onStateClick?.(stateCode)} + onMouseEnter={(event) => handleMouseEnter(event, stateCode)} + onMouseLeave={handleMouseLeave} + /> + ) + }) + } + + + + {/* Tooltip - Stays visible until hover another state or click close */} + {hoveredState && hoveredData && ( +
+
+ {/* Close button */} + +
+
{hoveredState}
+
+ {hoveredData.total_bills.toLocaleString()} bill{hoveredData.total_bills !== 1 ? 's' : ''} +
+
+ + {hoveredData.total_bills > 0 && ( + <> + {/* Primary Type and Status - Prominent */} +
+
+
+
+
+
+ {legislationTypes[hoveredData.primary_type] || hoveredData.primary_type} +
+
Primary Type
+
+
+
+ {hoveredData.primary_status === 'enacted' ? '✓ Enacted' : + hoveredData.primary_status === 'failed' ? '✗ Failed' : + '⏳ Pending'} +
+
+
+ + {/* Sample Bills Grouped by Type */} + {hoveredData.sample_bills && hoveredData.sample_bills.length > 0 && ( +
+
Recent Bills by Type:
+
+ {(() => { + // Group bills by type + const billsByType = hoveredData.sample_bills.reduce((acc, bill) => { + if (!acc[bill.type]) acc[bill.type] = [] + acc[bill.type].push(bill) + return acc + }, {} as Record) + + return Object.entries(billsByType).map(([type, bills]) => ( +
+
+
+
+ {legislationTypes[type] || type} ({bills.length}) +
+
+
+ {bills.map((bill, idx) => ( + +
+ + {bill.bill_number} + + + {bill.status === 'enacted' ? '✓ Enacted' : + bill.status === 'failed' ? '✗ Failed' : '⏳ Pending'} + +
+
+ {bill.date && 📅 {bill.date}} + {bill.date && bill.action && } + {bill.action || 'Click for details'} +
+ + ))} +
+
+ )) + })()} +
+
+ )} + + {/* Status Summary */} +
+
+
{hoveredData.status_counts.enacted}
+
Enacted
+
+
+
{hoveredData.status_counts.pending}
+
Pending
+
+
+
{hoveredData.status_counts.failed}
+
Failed
+
+
+ + {/* Drill Down Button */} + + + )} + + {hoveredData.total_bills === 0 && ( +
No legislation found
+ )} + + {/* Tooltip arrow */} +
+
+
+ )} + + {/* Legend */} +
+
Legend
+ + {/* Type of Legislation - Show actual types from data */} + {(() => { + // Get unique types from actual state data + const uniqueTypes = new Set() + Object.values(stateData).forEach(state => { + if (state.primary_type) uniqueTypes.add(state.primary_type) + }) + + return uniqueTypes.size > 0 && ( +
+
Type of Legislation
+
+ {Array.from(uniqueTypes).sort().map(type => ( +
+
+ {legislationTypes[type] || type.replace(/_/g, ' ')} +
+ ))} +
+
+ No Legislation +
+
+
+ ) + })()} + + {/* Status of Legislation */} +
+
Status
+
+
+
+ Enacted (darker) +
+
+
+ Pending (normal) +
+
+
+ Failed (lighter) +
+
+
+
+
+ ) +} diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b16119f913098a1b804bd27554f2758b0ff3d7b9 --- /dev/null +++ b/frontend/src/contexts/AuthContext.tsx @@ -0,0 +1,124 @@ +import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; + +interface User { + id: number; + email: string; + username?: string; + full_name?: string; + avatar_url?: string; + oauth_provider?: string; + state?: string; + county?: string; + city?: string; + school_board?: string; + profile_completed?: boolean; +} + +interface AuthContextType { + user: User | null; + token: string | null; + login: (provider: string) => void; + logout: () => void; + isAuthenticated: boolean; + isLoading: boolean; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [user, setUser] = useState(null); + const [token, setToken] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const API_URL = import.meta.env.PROD + ? '/api' + : 'http://localhost:8000'; + + // Load user from localStorage on mount + useEffect(() => { + // Check for token in URL FIRST (OAuth callback) + const urlParams = new URLSearchParams(window.location.search); + const urlToken = urlParams.get('token'); + + if (urlToken) { + localStorage.setItem('auth_token', urlToken); + setToken(urlToken); + fetchUser(urlToken); + + // Clean URL (remove token from address bar) + window.history.replaceState({}, document.title, window.location.pathname); + return; // Exit early, fetchUser will handle loading state + } + + // Check for stored token + const storedToken = localStorage.getItem('auth_token'); + + if (storedToken) { + setToken(storedToken); + fetchUser(storedToken); + } else { + setIsLoading(false); + } + }, []); + + const fetchUser = async (authToken: string) => { + try { + const response = await fetch(`${API_URL}/auth/me`, { + headers: { + 'Authorization': `Bearer ${authToken}`, + }, + }); + + if (response.ok) { + const userData = await response.json(); + setUser(userData); + } else { + // Token is invalid + localStorage.removeItem('auth_token'); + setToken(null); + } + } catch (error) { + console.error('Error fetching user:', error); + localStorage.removeItem('auth_token'); + setToken(null); + } finally { + setIsLoading(false); + } + }; + + const login = (provider: string) => { + // Redirect to OAuth endpoint + const redirectUri = encodeURIComponent(window.location.origin); + const authUrl = `${API_URL}/auth/login/${provider}?redirect_uri=${redirectUri}`; + window.location.href = authUrl; + }; + + const logout = () => { + localStorage.removeItem('auth_token'); + setToken(null); + setUser(null); + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/frontend/src/contexts/LocationContext.tsx b/frontend/src/contexts/LocationContext.tsx new file mode 100644 index 0000000000000000000000000000000000000000..23b583219488667f3b00f6a9be3a0a09e403468e --- /dev/null +++ b/frontend/src/contexts/LocationContext.tsx @@ -0,0 +1,95 @@ +import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react' +import { useAuth } from './AuthContext' +import { stateNameToCode } from '../utils/stateMapping' + +interface LocationData { + address?: string + state: string + county: string + city: string + school_board?: string + latitude?: number + longitude?: number +} + +interface LocationContextType { + location: LocationData | null + setLocation: (location: LocationData) => void + clearLocation: () => void + hasLocation: boolean +} + +const LocationContext = createContext(undefined) + +export const LocationProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const { user, isAuthenticated } = useAuth() + const [location, setLocationState] = useState(null) + + // Load location from user profile or localStorage + useEffect(() => { + if (isAuthenticated && user) { + // Use location from user profile if available + if (user.state && user.city) { + // Migrate full state names to state codes + const stateCode = stateNameToCode(user.state) + + setLocationState({ + state: stateCode, + county: user.county || '', + city: user.city, + school_board: user.school_board, + }) + } + } else { + // Load from localStorage for anonymous users + const savedLocation = localStorage.getItem('user_location') + if (savedLocation) { + try { + const parsed = JSON.parse(savedLocation) + + // Migrate full state names to state codes + if (parsed.state) { + parsed.state = stateNameToCode(parsed.state) + } + + setLocationState(parsed) + + // Save back the migrated version + localStorage.setItem('user_location', JSON.stringify(parsed)) + } catch (e) { + console.error('Failed to parse saved location:', e) + } + } + } + }, [user, isAuthenticated]) + + const setLocation = (newLocation: LocationData) => { + setLocationState(newLocation) + + // Save to localStorage for anonymous users + if (!isAuthenticated) { + localStorage.setItem('user_location', JSON.stringify(newLocation)) + } + } + + const clearLocation = () => { + setLocationState(null) + localStorage.removeItem('user_location') + } + + const hasLocation = location !== null && !!location.state && !!location.city + + return ( + + {children} + + ) +} + +export const useLocation = () => { + const context = useContext(LocationContext) + if (context === undefined) { + throw new Error('useLocation must be used within a LocationProvider') + } + return context +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000000000000000000000000000000000000..8bac7379563f5ad88550bab0b0ed02a8700679fe --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,58 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #F1F5F9; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + min-height: 100vh; +} + +@layer base { + h1 { + @apply text-4xl font-bold; + } + h2 { + @apply text-3xl font-semibold; + } + h3 { + @apply text-2xl font-semibold; + } +} + +@layer components { + .card { + @apply bg-white rounded-lg shadow-md p-6; + } + + .btn-primary { + @apply bg-neutral-600 hover:bg-neutral-700 text-white font-medium py-2 px-4 rounded-lg transition-colors; + } + + .btn-secondary { + @apply bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded-lg transition-colors; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000000000000000000000000000000000000..b5662844647574ec5a72e68088700879692ab78a --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,164 @@ +// Native fetch-based API client - No axios dependency! +// Handles relative URLs correctly without HTTP/HTTPS conversion issues + +// Environment-aware API base URL with NUCLEAR OPTION for production +let API_BASE_URL: string + +if (import.meta.env.PROD) { + // 🚨 NUCLEAR OPTION: HARDCODE /api in production - IGNORE ALL ENVIRONMENT VARIABLES + // This prevents HuggingFace build secrets from injecting http:// URLs + API_BASE_URL = '/api' + console.log('🌐 [API] Production mode: HARDCODED relative path:', API_BASE_URL) + console.log('🚨 [API] Ignoring all environment variables (nuclear option enabled)') + + // SAFETY CHECK: If somehow an http:// URL got through, log a warning + if (typeof import.meta.env.VITE_API_URL === 'string' && import.meta.env.VITE_API_URL.startsWith('http://')) { + console.warn('⚠️ [API] BLOCKED http:// URL from environment:', import.meta.env.VITE_API_URL) + console.warn('⚠️ [API] Using hardcoded /api instead') + } +} else { + // Development: Use environment variable or default to localhost + API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000/api' + console.log('🔧 [API] Development mode:', API_BASE_URL) +} + +console.log('📡 [API] Final base URL:', API_BASE_URL) +console.log('🔒 [API] Page protocol:', typeof window !== 'undefined' ? window.location.protocol : 'N/A') + +// Response type that matches axios structure +interface APIResponse { + data: T + status: number + statusText: string +} + +// Fetch wrapper that mimics axios interface +class APIClient { + private baseURL: string + + constructor(baseURL: string) { + this.baseURL = baseURL + } + + private async request( + url: string, + options: RequestInit = {} + ): Promise> { + // Build full URL + const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}` + + // 🚨 PRODUCTION SAFETY CHECK: Block any http:// URLs + if (import.meta.env.PROD && fullUrl.startsWith('http://')) { + const httpsUrl = fullUrl.replace('http://', 'https://') + console.error('❌ [API] BLOCKED insecure HTTP request in production:', fullUrl) + console.error('❌ [API] This would cause Mixed Content errors') + console.error('❌ [API] Upgrading to HTTPS:', httpsUrl) + throw new Error(`BLOCKED: Attempted to make insecure HTTP request in production: ${fullUrl}`) + } + + console.log('🔍 [FETCH] Request URL:', fullUrl) + console.log('🔍 [FETCH] Method:', options.method || 'GET') + + // Add auth token if available + const token = localStorage.getItem('auth_token') + const headers: Record = { + 'Content-Type': 'application/json', + ...(options.headers as Record), + } + + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + + try { + const response = await fetch(fullUrl, { + ...options, + headers, + }) + + // Handle 401 unauthorized + if (response.status === 401) { + localStorage.removeItem('auth_token') + } + + // Parse response + let data: T + const contentType = response.headers.get('content-type') + if (contentType && contentType.includes('application/json')) { + data = await response.json() + } else { + data = (await response.text()) as unknown as T + } + + if (!response.ok) { + throw { + response: { + data, + status: response.status, + statusText: response.statusText, + }, + message: `HTTP ${response.status}: ${response.statusText}`, + } + } + + console.log('✅ [FETCH] Success:', response.status) + return { + data, + status: response.status, + statusText: response.statusText, + } + } catch (error) { + console.error('❌ [FETCH] Error:', error) + throw error + } + } + + async get(url: string, config?: { params?: Record }): Promise> { + // Build query string + let fullUrl = url + if (config?.params) { + const params = new URLSearchParams() + Object.entries(config.params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + params.append(key, String(value)) + } + }) + const queryString = params.toString() + if (queryString) { + fullUrl = `${url}?${queryString}` + } + } + + return this.request(fullUrl, { method: 'GET' }) + } + + async post(url: string, data?: any): Promise> { + return this.request(url, { + method: 'POST', + body: data ? JSON.stringify(data) : undefined, + }) + } + + async put(url: string, data?: any): Promise> { + return this.request(url, { + method: 'PUT', + body: data ? JSON.stringify(data) : undefined, + }) + } + + async delete(url: string): Promise> { + return this.request(url, { method: 'DELETE' }) + } + + async patch(url: string, data?: any): Promise> { + return this.request(url, { + method: 'PATCH', + body: data ? JSON.stringify(data) : undefined, + }) + } +} + +// Create and export the API client instance +const api = new APIClient(API_BASE_URL) + +export default api diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8ae669357972a6faca139acc817ec941432b1f09 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { AuthProvider } from './contexts/AuthContext' +import { LocationProvider } from './contexts/LocationContext' +import ScrollToTop from './components/ScrollToTop' +import App from './App' +import './index.css' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: 1, + staleTime: 5 * 60 * 1000, // 5 minutes + }, + }, +}) + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + + + + + + , +) diff --git a/frontend/src/pages/AdvocacyTopics.tsx b/frontend/src/pages/AdvocacyTopics.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9b203dffc901790e3c6cc4684dfba4fafda31d74 --- /dev/null +++ b/frontend/src/pages/AdvocacyTopics.tsx @@ -0,0 +1,229 @@ +import { + BellAlertIcon, + MegaphoneIcon, + HeartIcon, + AcademicCapIcon, + ShieldCheckIcon, + HomeModernIcon, + SparklesIcon, + UserGroupIcon, +} from '@heroicons/react/24/outline' +import { Link } from 'react-router-dom' + +export default function AdvocacyTopics() { + const topics = [ + { + icon: HeartIcon, + title: 'Oral Health', + description: 'Track dental programs, fluoridation policies, and oral health initiatives in your community.', + count: '5,200+ related meetings', + color: '#DC143C', + keywords: ['dental', 'fluoride', 'oral health', 'teeth'], + }, + { + icon: AcademicCapIcon, + title: 'Education', + description: 'Monitor school budgets, curriculum changes, and educational programs across districts.', + count: '15,000+ related meetings', + color: '#354F52', + keywords: ['schools', 'education', 'curriculum', 'budget'], + }, + { + icon: HomeModernIcon, + title: 'Housing & Development', + description: 'Follow zoning decisions, affordable housing initiatives, and development projects.', + count: '8,500+ related meetings', + color: '#52796F', + keywords: ['housing', 'zoning', 'development', 'affordable'], + }, + { + icon: ShieldCheckIcon, + title: 'Public Safety', + description: 'Stay informed on police budgets, fire department resources, and emergency services.', + count: '12,000+ related meetings', + color: '#84A98C', + keywords: ['police', 'fire', 'safety', 'emergency'], + }, + { + icon: UserGroupIcon, + title: 'Social Services', + description: 'Track programs for seniors, families, mental health, and community support.', + count: '7,800+ related meetings', + color: '#CAD2C5', + keywords: ['social services', 'mental health', 'seniors', 'families'], + }, + { + icon: SparklesIcon, + title: 'Environment & Sustainability', + description: 'Monitor climate initiatives, recycling programs, and environmental policies.', + count: '6,400+ related meetings', + color: '#52796F', + keywords: ['environment', 'climate', 'sustainability', 'recycling'], + }, + ] + + const howToAdvocate = [ + { + step: '1', + title: 'Find Your Topic', + description: 'Browse advocacy topics or search for specific issues affecting your community.', + }, + { + step: '2', + title: 'Track Meetings', + description: 'Monitor upcoming government meetings where your topic will be discussed.', + }, + { + step: '3', + title: 'Prepare Your Message', + description: 'Use our talking points and data to craft compelling testimony.', + }, + { + step: '4', + title: 'Make Your Voice Heard', + description: 'Attend meetings, submit comments, or contact officials directly.', + }, + ] + + return ( +
+
+ {/* Header */} +
+
+ +

+ Advocacy Topics +

+
+

+ Track what your community is discussing. Find advocacy opportunities and get involved in local decision-making. +

+
+ + {/* Search Banner */} +
+ +

+ Find Advocacy Opportunities in Your Area +

+

+ Search meeting minutes for topics that matter to you. Get alerts when your issues are being discussed. +

+
+ + Search Meeting Minutes + + + View All Opportunities + +
+
+ + {/* Topics Grid */} +
+

Popular Advocacy Topics

+
+ {topics.map((topic) => { + const Icon = topic.icon + return ( + +
+
+ +
+ + {topic.count} + +
+

+ {topic.title} +

+

+ {topic.description} +

+
+ {topic.keywords.slice(0, 3).map((keyword) => ( + + {keyword} + + ))} +
+
+ Search meetings → +
+ + ) + })} +
+
+ + {/* How to Advocate */} +
+

How to Advocate Effectively

+
+ {howToAdvocate.map((item) => ( +
+
+ {item.step} +
+

{item.title}

+

{item.description}

+
+ ))} +
+
+ + {/* Get Started CTA */} +
+

+ Ready to Make a Difference? +

+

+ Start by searching for your community and exploring what local government is discussing. + Your voice matters in local decision-making. +

+
+ + Find Your Community + + + View Upcoming Meetings + +
+
+
+
+ ) +} diff --git a/frontend/src/pages/Analytics.tsx b/frontend/src/pages/Analytics.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0fe6ebbca1858618c3a03c2ad991899ab6a05b2c --- /dev/null +++ b/frontend/src/pages/Analytics.tsx @@ -0,0 +1,249 @@ +import { useState } from 'react' +import { + ChartBarIcon, + CurrencyDollarIcon, + ArrowTrendingUpIcon, + DocumentChartBarIcon, +} from '@heroicons/react/24/outline' + +// Commented out for now - will be used when API is ready +// import { useQuery } from '@tanstack/react-query' +// import axios from 'axios' + +// interface BudgetData { +// jurisdiction: string +// state: string +// fiscal_year: string +// total_budget: number +// budget_change: number +// categories: { +// education: number +// infrastructure: number +// public_safety: number +// health: number +// other: number +// } +// } + +export default function Analytics() { + const [selectedState, setSelectedState] = useState('all') + const [fiscalYear, setFiscalYear] = useState('2024') + + // Commented out for now - will be implemented when API is ready + // const { data, isLoading } = useQuery({ + // queryKey: ['budget-analytics', selectedState, fiscalYear], + // queryFn: async () => { + // const response = await axios.get('/api/budgets', { + // params: { state: selectedState, year: fiscalYear }, + // }) + // return response.data.budgets || [] + // }, + // }) + + return ( +
+
+ {/* Header */} +
+
+ +

+ Budget Analysis +

+
+

+ Explore city, county, and school budgets with budget-to-minutes delta analysis +

+
+ + {/* Filter Controls */} +
+
+
+ + +
+
+ + +
+
+
+ + {/* Stats Overview */} +
+
+
+
+

+ Jurisdictions Tracked +

+

+ 90,000+ +

+
+ +
+
+ +
+
+
+

+ Budget Records +

+

+ 15,000+ +

+
+ +
+
+ +
+
+
+

+ Avg Budget Increase +

+

+ +3.2% + +

+
+
+
+ +
+
+
+

+ States Covered +

+

+ 50 +

+
+
+
+
+ + {/* Features Section */} +
+
+

+ Budget-to-Minutes Delta Analysis +

+

+ Compare what governments say in meeting minutes versus what they actually allocate in budgets. +

+
    +
  • + + Track rhetoric vs. reality in budget decisions +
  • +
  • + + Identify funding priorities and gaps +
  • +
  • + + Monitor year-over-year changes +
  • +
+
+ +
+

+ Budget Categories Tracked +

+
+
+
+ Education & Schools + 35% +
+
+
+
+
+
+
+ Public Safety + 25% +
+
+
+
+
+
+
+ Infrastructure + 20% +
+
+
+
+
+
+
+ Health & Human Services + 15% +
+
+
+
+
+
+
+
+ + {/* Coming Soon / Data Integration Notice */} +
+ +

+ Budget Data Integration In Progress +

+

+ We're currently integrating budget data from cities, counties, and school districts across all 50 states. + Check back soon for interactive budget comparisons, trend analysis, and meeting-to-budget correlation insights. +

+ +
+
+
+ ) +} diff --git a/frontend/src/pages/BillDetail.tsx b/frontend/src/pages/BillDetail.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a79d35662d7f673806a248748c2997dce448846d --- /dev/null +++ b/frontend/src/pages/BillDetail.tsx @@ -0,0 +1,254 @@ +import { useParams, Link } from 'react-router-dom' +import { useQuery } from '@tanstack/react-query' +import api from '../lib/api' +import { ArrowLeftIcon, CalendarIcon, DocumentTextIcon, BuildingLibraryIcon } from '@heroicons/react/24/outline' + +interface BillDetail { + bill_id: string + bill_number: string + title: string + classification: string[] + session: string + session_name: string + first_action_date: string + latest_action_date: string + latest_action: string + jurisdiction: string + state: string + openstates_url?: string + sponsors?: Array<{ + name: string + primary: boolean + classification?: string + }> + actions?: Array<{ + date: string + description: string + classification: string[] + }> + sources?: Array<{ + url: string + note: string + }> +} + +export default function BillDetail() { + const { billId } = useParams<{ billId: string }>() + + const { data: bill, isLoading, error } = useQuery({ + queryKey: ['bill', billId], + queryFn: async () => { + const response = await api.get(`/bills/${billId}`) + return response.data + }, + enabled: !!billId, + }) + + if (isLoading) { + return ( +
+
+
+
+
+

Loading bill details...

+
+
+
+
+ ) + } + + if (error || !bill) { + const errorMessage = error instanceof Error + ? error.message + : (error as any)?.response?.data?.detail + ? (error as any).response.data.detail + : (error as any)?.message + ? (error as any).message + : 'Unable to load bill details' + + return ( +
+
+
+
⚠️
+

Bill not found

+

{errorMessage}

+ + + Back to Policy Map + +
+
+
+ ) + } + + const statusColor = bill.latest_action?.toLowerCase().includes('enact') ? 'green' : + bill.latest_action?.toLowerCase().includes('fail') ? 'red' : + bill.latest_action?.toLowerCase().includes('veto') ? 'red' : 'yellow' + + return ( +
+
+ {/* Back Button */} +
+ + + Back to Policy Map + +
+ + {/* Bill Header */} +
+
+
+
+ + {bill.bill_number} + + + {bill.state} + + + {bill.latest_action || 'Status Unknown'} + +
+

+ {bill.title} +

+
+
+ + {bill.session_name} +
+ {bill.classification && bill.classification.length > 0 && ( + + )} + {bill.classification && bill.classification.length > 0 && ( + {bill.classification.join(', ')} + )} +
+
+
+
+ + {/* Timeline */} +
+

+ + Timeline +

+
+ {bill.first_action_date && ( +
+
+ {new Date(bill.first_action_date).toLocaleDateString()} +
+
+
First Action
+
+
+ )} + {bill.latest_action_date && ( +
+
+ {new Date(bill.latest_action_date).toLocaleDateString()} +
+
+
Latest Action
+
{bill.latest_action}
+
+
+ )} +
+
+ + {/* Sponsors */} + {bill.sponsors && bill.sponsors.length > 0 && ( +
+

Sponsors

+
+ {bill.sponsors.map((sponsor, idx) => ( +
+ + {sponsor.primary ? 'Primary' : 'Co-sponsor'} + + {sponsor.name} +
+ ))} +
+
+ )} + + {/* Actions History */} + {bill.actions && bill.actions.length > 0 && ( +
+

+ + Legislative Actions +

+
+ {bill.actions.map((action, idx) => ( +
+
+ {new Date(action.date).toLocaleDateString()} +
+
+
{action.description}
+ {action.classification && action.classification.length > 0 && ( +
+ {action.classification.join(', ')} +
+ )} +
+
+ ))} +
+
+ )} + + {/* Source Links */} +
+

Official Sources

+
+ {bill.openstates_url && ( + + View on OpenStates → + + )} + {bill.sources && bill.sources.map((source: any, idx: number) => ( + + {source.note || 'View on Legislature Website'} → + + ))} +
+
+
+
+ ) +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000000000000000000000000000000000000..56b3847e5f6ab92ef413ae56d3bee6fa0d451612 --- /dev/null +++ b/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,201 @@ +import { useQuery } from '@tanstack/react-query' +import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts' +import api from '../lib/api' + +const COLORS = ['#0ea5e9', '#f59e0b', '#10b981', '#ef4444', '#8b5cf6'] + +interface DashboardStats { + total_documents: number + total_opportunities: number + states_monitored: number + topics: Record + recent_opportunities: Array<{ + state: string + municipality: string + topic: string + urgency: string + date: string + }> +} + +export default function Dashboard() { + const { data, isLoading } = useQuery({ + queryKey: ['dashboard'], + queryFn: async () => { + const response = await api.get('/dashboard') + return response.data + }, + }) + + if (isLoading) { + return ( +
+
Loading analytics...
+
+ ) + } + + const topicData = Object.entries(data?.topics || {}).map(([name, value]) => ({ + name: name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), + value, + })) + + return ( +
+
+ {/* Header */} +
+

Data & Trends

+

+ Statistics and insights across communities - see what's happening in local government +

+
+ + {/* Stats Grid */} +
+
+

Total Documents

+

+ {data?.total_documents?.toLocaleString() || '0'} +

+

Meeting minutes & budgets

+
+ +
+

Opportunities Found

+

+ {data?.total_opportunities?.toLocaleString() || '0'} +

+

Advocacy windows identified

+
+ +
+

States Monitored

+

+ {data?.states_monitored || '0'} +

+

Across the nation

+
+
+ + {/* Charts */} +
+ {/* Topics Bar Chart */} +
+

Policy Topics

+ {topicData.length > 0 ? ( + + + + + + + + + + ) : ( +
+ No topic data available +
+ )} +
+ + {/* Topics Pie Chart */} +
+

Topic Distribution

+ {topicData.length > 0 ? ( + + + entry.name} + outerRadius={80} + fill="#8884d8" + dataKey="value" + > + {topicData.map((_entry, index) => ( + + ))} + + + + + ) : ( +
+ No topic data available +
+ )} +
+
+ + {/* Recent Opportunities */} +
+

Recent Causes

+ {data?.recent_opportunities && data.recent_opportunities.length > 0 ? ( +
+ + + + + + + + + + + {data.recent_opportunities.map((opp, idx) => ( + + + + + + + ))} + +
+ Location + + Topic + + Urgency + + Date +
+
{opp.municipality}
+
{opp.state}
+
+ {opp.topic.replace(/_/g, ' ')} + + + {opp.urgency} + + + {new Date(opp.date).toLocaleDateString()} +
+
+ ) : ( +
+

No opportunities found yet

+

+ Run the data ingestion pipeline to analyze meetings and identify advocacy opportunities +

+
+ )} +
+
+
+ ) +} diff --git a/frontend/src/pages/DebateGrader.tsx b/frontend/src/pages/DebateGrader.tsx new file mode 100644 index 0000000000000000000000000000000000000000..14e478b571f28cecb7f084f35ee44c0a42b79257 --- /dev/null +++ b/frontend/src/pages/DebateGrader.tsx @@ -0,0 +1,274 @@ +import { useState } from 'react' +import { CheckCircleIcon, XCircleIcon, QuestionMarkCircleIcon } from '@heroicons/react/24/outline' + +interface DebateGrade { + dimensions: { + harms: DimensionScore + solvency: DimensionScore + topicality: DimensionScore + } + overall: { + score: number + grade: string + summary: string + } + timestamp: string +} + +interface DimensionScore { + score: number + grade: string + explanation: string + layperson_label: string + layperson_question: string +} + +export default function DebateFinder() { + const [text, setText] = useState('') + const [title, setTitle] = useState('') + const [grade, setGrade] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + const gradeDecision = async () => { + if (!text) { + setError('Please enter some text to grade') + return + } + + setLoading(true) + setError('') + + try { + const params = new URLSearchParams() + params.set('text', text) + if (title) params.set('title', title) + + const response = await fetch(`/api/debate-grade?${params.toString()}`, { + method: 'POST' + }) + + if (!response.ok) { + throw new Error('Failed to grade decision') + } + + const data = await response.json() + setGrade(data.debate_grade) + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred') + } finally { + setLoading(false) + } + } + + const getGradeColor = (grade: string) => { + switch (grade) { + case 'excellent': + return 'text-green-600' + case 'good': + return 'text-blue-600' + case 'fair': + return 'text-yellow-600' + case 'weak': + return 'text-orange-600' + case 'missing': + return 'text-red-600' + default: + return 'text-gray-600' + } + } + + const getGradeIcon = (grade: string) => { + switch (grade) { + case 'excellent': + case 'good': + return + case 'fair': + return + case 'weak': + case 'missing': + return + default: + return null + } + } + + return ( +
+
+
+

+ Debate Finder +

+

+ Evaluate government decisions using debate framework: Harms, Solvency, and Topicality +

+
+ + {/* Input Form */} +
+
+
+ + setTitle(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500" + placeholder="e.g., City Council approves dental screening program" + /> +
+ +
+ +