diff --git a/.gitignore b/.gitignore index 2289e2a..cadb2e5 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,11 @@ media/ # Ignore target directories for Polkadot contracts polkadot_contracts/target/ polkadot_contracts/*/target/ -polkadot-react-app/node_modules/ \ No newline at end of file +polkadot-react-app/node_modules/ + +# Polkadot API build artifacts (generated during CI/CD) +# Keep config files but ignore build outputs +eunoia_web/.papi/descriptors/dist/ +eunoia_web/.papi/descriptors/src/ +# Note: .papi/descriptors/.gitignore, .papi/descriptors/package.json, +# .papi/metadata/dot.scale, and .papi/polkadot-api.json are kept in git \ No newline at end of file diff --git a/AI_FEATURES_EUNOIA.md b/AI_FEATURES_EUNOIA.md new file mode 100644 index 0000000..d25bec8 --- /dev/null +++ b/AI_FEATURES_EUNOIA.md @@ -0,0 +1,187 @@ +# AI Features - Eunoia + +## Charity Registration + +We aim to make the charity registration process as seamless as possible. Our AI-powered system automatically extracts information from charity websites, categorizes charities, and discovers active movements/campaigns. + +### Automated AI Processing + +When a charity is registered **via the API or web form**, the following happens automatically: + +1. **Basic AI Extraction** (via `process_charity_website()` signal) + - Fetches and parses the charity's website + - Uses GPT-5 to extract: + - **Description**: 2-3 paragraph summary of mission and activities + - **Tagline**: Short, catchy phrase (max 15 words) + - **Category**: Automatically assigns from 9 categories (ENV, EDU, HEA, ANI, ART, HUM, COM, DIS, OTH) + - **Keywords**: 5-7 relevant keywords describing the charity's focus + - Generates semantic embedding for search/matching + - Runs synchronously (completes in seconds) + +2. **Comprehensive Research** (via `launch_charity_research_in_background()`) + - Crawls charity website (up to 6 pages) + - Uses GPT-4.1 to analyze: + - **Charity Profile**: Detailed profile extraction + - **Movements**: Discovers up to 5 active campaigns/initiatives per charity + - Creates `Movement` objects with: + - Title, summary, category, geography + - Source URLs, confidence scores + - Embeddings for semantic matching + - Runs asynchronously in background (takes minutes) + +### Test Charity Setup + +For development and testing, we provide scripts to quickly set up test charities: + +#### 1. Register Test Charities + +By running: + +```bash +cd backend/eunoia_backend +python register_test_charities.py +``` + +This script: +- Creates 10 test charities with diverse categories +- Pre-populates basic fields (name, description, wallet addresses, etc.) +- Triggers basic AI extraction (description, tagline, category, keywords) +- **Note**: Does NOT automatically fetch movements (see below) + +**Output:** +``` +Creating test charities... +================================================== +✅ Created: Global Water Initiative + Category: Health & Medicine + Location: Global + Status: ✅ VERIFIED + Contact: Sarah Chen +-------------------------------------------------- +... +✅ Successfully created 10 test charities! +📊 Total charities in database: 10 +``` + +#### 2. Fetch Movements for Test Charities + +Since test charities are created via direct ORM (bypassing API hooks), movements must be fetched manually. + +**Option A: Async Mode (Recommended)** +```bash +python trigger_movement_research.py +``` + +This will: +- Trigger research for all 10 test charities +- Wait for all background threads to complete +- Show progress as each charity finishes +- Display number of movements found per charity + +**Option B: Sync Mode (One at a time)** +```bash +python trigger_movement_research.py --wait +``` + +This runs synchronously, showing results immediately for each charity. + +**Option C: Django Management Command** +```bash +python manage.py research_existing_charity --test-only --sync +``` + +**Output:** +``` +🔄 Launching research for: Global Water Initiative (ID: 55) + Website: https://www.charitywater.org +------------------------------------------------------------ +⏳ Waiting for background research to complete... + [1/10] Thread completed + ✅ Global Water Initiative: 3 movements found + [2/10] Thread completed + ✅ Education For All Foundation: 2 movements found +... +``` + +### Complete Setup Workflow + +For a complete test environment setup: + +```bash +# 1. Register test charities +python register_test_charities.py + +# 2. Fetch movements for all test charities +python trigger_movement_research.py + +# 3. Verify movements were created +python manage.py shell +``` + +```python +# In Django shell +from main.models import Charity, Movement + +# Check movements for a charity +charity = Charity.objects.get(name="Global Water Initiative") +print(f"Movements for {charity.name}: {charity.movements.count()}") +for movement in charity.movements.all(): + print(f" - {movement.title}: {movement.summary[:50]}...") +``` + +### What Happens Automatically vs Manually + +| Action | When Created Via | What Runs Automatically | +|--------|-----------------|------------------------| +| **API** (`POST /api/charities/`) | REST API | ✅ Basic AI extraction
✅ Movement research | +| **Web Form** (`CharityRegistrationForm`) | Django form | ✅ Basic AI extraction
✅ Movement research | +| **Test Script** (`register_test_charities.py`) | Direct ORM | ✅ Basic AI extraction
❌ **NO** movement research* | + +*Movements must be fetched manually using `trigger_movement_research.py` + +### AI Models Used + +| Feature | Model | Purpose | +|---------|-------|---------| +| Basic extraction | GPT-5 | Description, tagline, category, keywords | +| Embeddings | text-embedding-3-small | Semantic search vectors | +| Charity profile | GPT-4.1 | Detailed profile analysis | +| Movement discovery | GPT-4.1 | Finding active campaigns/initiatives | + +### API Endpoints + +- **Register Charity**: `POST /api/charities/` + - Automatically triggers AI processing + - Returns charity object with AI-populated fields + +- **Get Charities**: `GET /api/charities/` + - Returns all charities with AI-extracted data + +- **Semantic Search**: `GET /api/charity-semantic-search/?query=` + - Uses embeddings for intelligent matching + - Returns charities ranked by relevance + +### Troubleshooting + +**Problem**: Movements not appearing after registration +- **Solution**: Use `trigger_movement_research.py` if charity was created via test script +- **Check**: Verify charity has `website_url` set + +**Problem**: AI extraction failed +- **Check**: Website URL is accessible and returns HTML +- **Check**: OpenAI API key is set in environment +- **Check**: Logs for specific error messages + +**Problem**: Research taking too long +- **Normal**: Each charity can take 2-5 minutes +- **Tip**: Use `--sync` mode to see progress in real-time +- **Check**: Network connectivity and API rate limits + +### Best Practices + +1. **Always provide website_url**: AI features require a website to extract information +2. **Verify extraction**: Check that `description`, `tagline`, `category`, and `keywords` are populated +3. **Wait for movements**: Allow background research to complete (use `--wait` flag for sync mode) +4. **Monitor logs**: Check Django logs for research progress and errors +5. **Test first**: Use test charities to verify AI features before production use + diff --git a/backend/eunoia_backend/delete_test_charities.py b/backend/eunoia_backend/delete_test_charities.py new file mode 100644 index 0000000..9e97e2f --- /dev/null +++ b/backend/eunoia_backend/delete_test_charities.py @@ -0,0 +1,43 @@ +import os +import django + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eunoia_backend.settings') +django.setup() + +from main.models import Charity + +# List of test charity names from register_test_charities.py +test_charity_names = [ + 'Global Water Initiative', + 'Education For All Foundation', + 'Wildlife Conservation Network', + 'Green Earth Climate Action', + 'Community Health Partners', + 'Arts & Culture Preservation Society', + 'Human Rights Advocacy Network', + 'Disaster Relief Response Team', + 'Rural Development Initiative', + 'Tech For Good Collective' +] + +print("Deleting test charities...") +print("=" * 50) + +deleted_count = 0 +for name in test_charity_names: + try: + charity = Charity.objects.filter(name=name).first() + if charity: + charity.delete() + print(f"✅ Deleted: {name}") + deleted_count += 1 + else: + print(f"⚠️ Charity '{name}' not found, skipping...") + except Exception as e: + print(f"❌ Error deleting charity '{name}': {e}") + +print("=" * 50) +print(f"✅ Successfully deleted {deleted_count} test charities!") +print(f"📊 Remaining charities in database: {Charity.objects.count()}") + + diff --git a/backend/eunoia_backend/eunoia_backend/settings.py b/backend/eunoia_backend/eunoia_backend/settings.py index 9238016..a9f19a1 100644 --- a/backend/eunoia_backend/eunoia_backend/settings.py +++ b/backend/eunoia_backend/eunoia_backend/settings.py @@ -58,12 +58,21 @@ # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-#da0k)&*ea)01gqr)3a_7$g#166x#&q2!$-2if+3zr#a6m(jy6' +SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-#da0k)&*ea)01gqr)3a_7$g#166x#&q2!$-2if+3zr#a6m(jy6') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = ['eunoia.work', 'www.eunoia.work', 'localhost', '127.0.0.1', '52.37.227.112', 'ec2-52-37-227-112.us-west-2.compute.amazonaws.com', 'eunoia-api-eya2hhfdfzcchyc2.canadacentral-01.azurewebsites.net'] +DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' + +ALLOWED_HOSTS = [ + 'eunoia-backend-bebv.onrender.com', + 'eunoia.work', + 'www.eunoia.work', + 'localhost', + '127.0.0.1', + '52.37.227.112', + 'ec2-52-37-227-112.us-west-2.compute.amazonaws.com', + 'eunoia-api-eya2hhfdfzcchyc2.canadacentral-01.azurewebsites.net' +] # OpenAI API Key OPENAI_API_KEY = os.getenv('OPENAI_API_KEY') @@ -120,10 +129,20 @@ # Database # https://docs.djangoproject.com/en/5.2/ref/settings/#databases +# Use persistent disk path on Render, fallback to BASE_DIR for local development +# Render persistent disk should be mounted at a non-reserved path like /opt/render/project/data +if os.getenv('RENDER'): + # On Render, use persistent disk path + # The persistent disk should be mounted at /opt/render/project/data + db_path = '/opt/render/project/data/db.sqlite3' +else: + # Local development + db_path = BASE_DIR / 'db.sqlite3' + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + 'NAME': db_path, } } diff --git a/backend/eunoia_backend/main/management/commands/research_existing_charity.py b/backend/eunoia_backend/main/management/commands/research_existing_charity.py new file mode 100644 index 0000000..88ca92e --- /dev/null +++ b/backend/eunoia_backend/main/management/commands/research_existing_charity.py @@ -0,0 +1,182 @@ +from django.core.management.base import BaseCommand +from main.models import Charity +from agents_sdk import launch_charity_research_in_background, research_charity_sync + + +class Command(BaseCommand): + help = "Trigger movement research for existing charities" + + def add_arguments(self, parser): + parser.add_argument( + '--charity-id', + type=int, + help='ID of the charity to research' + ) + parser.add_argument( + '--charity-name', + type=str, + help='Name of the charity to research' + ) + parser.add_argument( + '--all', + action='store_true', + help='Research all charities with website URLs' + ) + parser.add_argument( + '--test-only', + action='store_true', + help='Research only test charities (from register_test_charities.py)' + ) + parser.add_argument( + '--sync', + action='store_true', + help='Run research synchronously (waits for completion)' + ) + parser.add_argument( + '--max-pages', + type=int, + default=6, + help='Maximum pages to crawl (default: 6)' + ) + + def handle(self, *args, **options): + charity_id = options.get('charity_id') + charity_name = options.get('charity_name') + all_charities = options.get('all') + test_only = options.get('test_only') + sync_mode = options.get('sync') + max_pages = options.get('max_pages') + + test_charity_names = [ + 'Global Water Initiative', + 'Education For All Foundation', + 'Wildlife Conservation Network', + 'Green Earth Climate Action', + 'Community Health Partners', + 'Arts & Culture Preservation Society', + 'Human Rights Advocacy Network', + 'Disaster Relief Response Team', + 'Rural Development Initiative', + 'Tech For Good Collective' + ] + + if sync_mode: + research_func = research_charity_sync + self.stdout.write(self.style.WARNING("Running in SYNC mode (will wait for completion)...")) + else: + research_func = launch_charity_research_in_background + self.stdout.write(self.style.SUCCESS("Running in ASYNC mode (background threads)...")) + + # Research specific charity by ID + if charity_id: + try: + charity = Charity.objects.get(id=charity_id) + self._research_charity(charity, research_func, max_pages, sync_mode) + except Charity.DoesNotExist: + self.stdout.write(self.style.ERROR(f"Charity with ID {charity_id} not found!")) + return + + # Research specific charity by name + if charity_name: + charity = Charity.objects.filter(name=charity_name).first() + if not charity: + self.stdout.write(self.style.ERROR(f"Charity '{charity_name}' not found!")) + return + self._research_charity(charity, research_func, max_pages, sync_mode) + return + + # Research test charities only + if test_only: + self.stdout.write(self.style.SUCCESS("Researching test charities...")) + charities = Charity.objects.filter(name__in=test_charity_names) + self._research_multiple(charities, research_func, max_pages, sync_mode) + return + + # Research all charities + if all_charities: + self.stdout.write(self.style.SUCCESS("Researching ALL charities with website URLs...")) + charities = Charity.objects.exclude(website_url__isnull=True).exclude(website_url='') + self._research_multiple(charities, research_func, max_pages, sync_mode) + return + + # Default: show help + self.stdout.write(self.style.WARNING("\nPlease specify what to research:")) + self.stdout.write(" --charity-id Research specific charity by ID") + self.stdout.write(" --charity-name Research specific charity by name") + self.stdout.write(" --test-only Research only test charities") + self.stdout.write(" --all Research all charities with websites") + self.stdout.write("\nOptions:") + self.stdout.write(" --sync Run synchronously (wait for completion)") + self.stdout.write(" --max-pages Max pages to crawl (default: 6)") + + def _research_charity(self, charity: Charity, research_func, max_pages: int, sync_mode: bool): + """Research a single charity.""" + if not charity.website_url: + self.stdout.write(self.style.ERROR( + f"Charity '{charity.name}' (ID: {charity.id}) has no website URL!" + )) + return + + self.stdout.write(f"🔍 Researching: {charity.name} (ID: {charity.id})") + self.stdout.write(f" Website: {charity.website_url}") + + if sync_mode: + result = research_func(charity.id, max_pages=max_pages) + if result.get('success'): + self.stdout.write(self.style.SUCCESS( + f"✅ Completed! Pages crawled: {result.get('pages_crawled')}, " + f"Movements found: {result.get('movements_found')}" + )) + else: + self.stdout.write(self.style.ERROR( + f"❌ Failed: {result.get('error')}" + )) + else: + research_func(charity.id, max_pages=max_pages) + self.stdout.write(self.style.SUCCESS("✅ Research triggered in background!")) + + def _research_multiple(self, charities, research_func, max_pages: int, sync_mode: bool): + """Research multiple charities.""" + total = charities.count() + self.stdout.write(f"Found {total} charities to research\n") + + success_count = 0 + error_count = 0 + + for charity in charities: + if not charity.website_url: + self.stdout.write(self.style.WARNING( + f"⚠️ Skipping '{charity.name}': No website URL" + )) + continue + + try: + if sync_mode: + result = research_func(charity.id, max_pages=max_pages) + if result.get('success'): + success_count += 1 + self.stdout.write(self.style.SUCCESS( + f"✅ {charity.name}: {result.get('movements_found')} movements found" + )) + else: + error_count += 1 + self.stdout.write(self.style.ERROR( + f"❌ {charity.name}: {result.get('error')}" + )) + else: + research_func(charity.id, max_pages=max_pages) + success_count += 1 + self.stdout.write(f"🔄 {charity.name}: Research triggered") + except Exception as e: + error_count += 1 + self.stdout.write(self.style.ERROR( + f"❌ {charity.name}: {str(e)}" + )) + + self.stdout.write("\n" + "=" * 60) + self.stdout.write(self.style.SUCCESS(f"✅ Triggered research for {success_count} charities")) + if error_count > 0: + self.stdout.write(self.style.ERROR(f"❌ Failed for {error_count} charities")) + if not sync_mode: + self.stdout.write("\n💡 Research is running in background. Check logs for progress.") + diff --git a/backend/eunoia_backend/main/utils.py b/backend/eunoia_backend/main/utils.py index 5b9ebe2..2e5c36b 100644 --- a/backend/eunoia_backend/main/utils.py +++ b/backend/eunoia_backend/main/utils.py @@ -100,11 +100,9 @@ def process_charity_website(charity: Charity) -> None: charity_info_tool = { "type": "function", - "function": { - "name": "extract_charity_information", - "description": "Extracts and structures information about a charity including summary, keywords, category, and tagline from website text.", - "parameters": CharityInfo.model_json_schema() - } + "name": "extract_charity_information", + "description": "Extracts and structures information about a charity including summary, keywords, category, and tagline from website text.", + "parameters": CharityInfo.model_json_schema() } prompt_text = ( @@ -118,20 +116,45 @@ def process_charity_website(charity: Charity) -> None: ) print(f"Sending text to OpenAI for structured extraction (length: {len(prompt_text)} chars)...") - completion = client.chat.completions.create( - model="gpt-4o", - messages=[ - {"role": "system", "content": "You are an expert assistant skilled in analyzing charity websites and extracting structured information according to the provided tool. Provide all requested fields: summary, tagline, category, and keywords."}, - {"role": "user", "content": prompt_text} - ], + response = client.responses.create( + model="gpt-5", + instructions="You are an expert assistant skilled in analyzing charity websites and extracting structured information according to the provided tool. Provide all requested fields: summary, tagline, category, and keywords.", + input=prompt_text, tools=[charity_info_tool], - tool_choice={"type": "function", "function": {"name": "extract_charity_information"}}, - temperature=0.3, # Slightly increased for more creative tagline/summary if needed + tool_choice={"type": "function", "name": "extract_charity_information"}, ) - tool_calls = completion.choices[0].message.tool_calls - if tool_calls and tool_calls[0].function.name == "extract_charity_information": - arguments_json = tool_calls[0].function.arguments + # Find the function call in the response output + # Items can be objects (with attributes) or dicts + function_call_item = None + for item in response.output: + # Handle both object attributes and dict keys + if isinstance(item, dict): + item_type = item.get('type') + item_name = item.get('name') + else: + item_type = getattr(item, 'type', None) + item_name = getattr(item, 'name', None) + + if item_type == 'function_call' and item_name == 'extract_charity_information': + function_call_item = item + break + + if function_call_item: + # Get arguments - handle both object and dict + if hasattr(function_call_item, 'arguments'): + arguments_raw = function_call_item.arguments + elif isinstance(function_call_item, dict): + arguments_raw = function_call_item.get('arguments', {}) + else: + arguments_raw = {} + + # Convert to JSON string if it's a dict/object + if isinstance(arguments_raw, (dict, str)): + arguments_json = json.dumps(arguments_raw) if isinstance(arguments_raw, dict) else arguments_raw + else: + arguments_json = json.dumps(arguments_raw) if arguments_raw else '{}' + print(f"Received arguments from OpenAI: {arguments_json}") try: charity_info = CharityInfo.model_validate_json(arguments_json) @@ -227,29 +250,52 @@ def generate_combined_mission_statement(user_query: str, charities_data: List[di combined_mission_tool = { "type": "function", - "function": { - "name": "extract_combined_charity_mission", - "description": "Extracts a combined, resonating mission statement for a list of charities based on user query.", - "parameters": CombinedCharityMission.model_json_schema() - } + "name": "extract_combined_charity_mission", + "description": "Extracts a combined, resonating mission statement for a list of charities based on user query.", + "parameters": CombinedCharityMission.model_json_schema() } try: print(f"Sending prompt to OpenAI for combined mission statement (query: '{user_query}', num_charities: {len(charities_data)})...") - completion = client.chat.completions.create( - model="gpt-4o-mini", # Or your preferred model, gpt-3.5-turbo might also work for this - messages=[ - {"role": "system", "content": "You are an expert assistant skilled in synthesizing information about multiple charities and a user query into a single, impactful mission statement. Respond using the provided tool."}, - {"role": "user", "content": prompt_text} - ], + response = client.responses.create( + model="gpt-5", + instructions="You are an expert assistant skilled in synthesizing information about multiple charities and a user query into a single, impactful mission statement. Respond using the provided tool.", + input=prompt_text, tools=[combined_mission_tool], - tool_choice={"type": "function", "function": {"name": "extract_combined_charity_mission"}}, - temperature=0.5, # Allow for some creativity + tool_choice={"type": "function", "name": "extract_combined_charity_mission"}, ) - tool_calls = completion.choices[0].message.tool_calls - if tool_calls and tool_calls[0].function.name == "extract_combined_charity_mission": - arguments_json = tool_calls[0].function.arguments + # Find the function call in the response output + # Items can be objects (with attributes) or dicts + function_call_item = None + for item in response.output: + # Handle both object attributes and dict keys + if isinstance(item, dict): + item_type = item.get('type') + item_name = item.get('name') + else: + item_type = getattr(item, 'type', None) + item_name = getattr(item, 'name', None) + + if item_type == 'function_call' and item_name == 'extract_combined_charity_mission': + function_call_item = item + break + + if function_call_item: + # Get arguments - handle both object and dict + if hasattr(function_call_item, 'arguments'): + arguments_raw = function_call_item.arguments + elif isinstance(function_call_item, dict): + arguments_raw = function_call_item.get('arguments', {}) + else: + arguments_raw = {} + + # Convert to JSON string if it's a dict/object + if isinstance(arguments_raw, (dict, str)): + arguments_json = json.dumps(arguments_raw) if isinstance(arguments_raw, dict) else arguments_raw + else: + arguments_json = json.dumps(arguments_raw) if arguments_raw else '{}' + print(f"Received combined mission arguments from OpenAI: {arguments_json}") try: combined_mission_info = CombinedCharityMission.model_validate_json(arguments_json) @@ -301,29 +347,53 @@ def fallback_charity_search(q: str, limit: int = 6) -> List[Charity]: try: enhanced_query_tool = { "type": "function", - "function": { - "name": "enhance_user_query", - "description": "Enhances a user query for better semantic search in a charity database.", - "parameters": EnhancedQuery.model_json_schema() # Pydantic v2 method - } + "name": "enhance_user_query", + "description": "Enhances a user query for better semantic search in a charity database.", + "parameters": EnhancedQuery.model_json_schema() # Pydantic v2 method } prompt = f"Enhance the following user query to make it more effective for semantic search in a database of charities. Focus on keywords and the underlying intent. Return the enhanced query.\n\nUser Query: '{user_query}'" - enhanced_query_completion = client.chat.completions.create( - model="gpt-4o", - messages=[ - {"role": "system", "content": "You are an expert query enhancer. Your goal is to refine user queries for better semantic search against a charity database. Respond using the provided tool."}, - {"role": "user", "content": prompt} - ], + response = client.responses.create( + model="gpt-5", + instructions="You are an expert query enhancer. Your goal is to refine user queries for better semantic search against a charity database. Respond using the provided tool.", + input=prompt, tools=[enhanced_query_tool], - tool_choice={"type": "function", "function": {"name": "enhance_user_query"}}, - temperature=0.1, + tool_choice={"type": "function", "name": "enhance_user_query"}, ) search_query_text = user_query # Default to original query - tool_calls = enhanced_query_completion.choices[0].message.tool_calls - if tool_calls and tool_calls[0].function.name == "enhance_user_query": - arguments_json = tool_calls[0].function.arguments + + # Find the function call in the response output + # Items can be objects (with attributes) or dicts + function_call_item = None + for item in response.output: + # Handle both object attributes and dict keys + if isinstance(item, dict): + item_type = item.get('type') + item_name = item.get('name') + else: + item_type = getattr(item, 'type', None) + item_name = getattr(item, 'name', None) + + if item_type == 'function_call' and item_name == 'enhance_user_query': + function_call_item = item + break + + if function_call_item: + # Get arguments - handle both object and dict + if hasattr(function_call_item, 'arguments'): + arguments_raw = function_call_item.arguments + elif isinstance(function_call_item, dict): + arguments_raw = function_call_item.get('arguments', {}) + else: + arguments_raw = {} + + # Convert to JSON string if it's a dict/object + if isinstance(arguments_raw, (dict, str)): + arguments_json = json.dumps(arguments_raw) if isinstance(arguments_raw, dict) else arguments_raw + else: + arguments_json = json.dumps(arguments_raw) if arguments_raw else '{}' + try: enhanced_query_data = EnhancedQuery.model_validate_json(arguments_json) # Pydantic v2 method search_query_text = enhanced_query_data.enhanced_query diff --git a/backend/eunoia_backend/register_test_charities.py b/backend/eunoia_backend/register_test_charities.py index d6f62d3..00ac01d 100644 --- a/backend/eunoia_backend/register_test_charities.py +++ b/backend/eunoia_backend/register_test_charities.py @@ -5,6 +5,7 @@ django.setup() from main.models import Charity +from agents_sdk import research_charity_sync # Clear existing test charities (optional - uncomment if you want to start fresh) # Charity.objects.all().delete() @@ -163,12 +164,46 @@ # Check if charity already exists existing = Charity.objects.filter(name=charity_data['name']).first() if existing: - print(f"⚠️ Charity '{charity_data['name']}' already exists, skipping...") + print(f"⚠️ Charity '{charity_data['name']}' already exists, checking movements...") + charity = existing + + # Check if charity has movements, if not, research them + from main.models import Movement + movement_count = Movement.objects.filter(charity=charity).count() + if movement_count == 0 and charity.website_url: + print(f"🔄 No movements found, researching movements for {charity.name}...") + try: + result = research_charity_sync(charity.id, max_pages=6) + if result.get('success'): + movements_count = result.get('movements_found', 0) + print(f" ✅ Found {movements_count} movements") + else: + print(f" ⚠️ Research failed: {result.get('error', 'Unknown error')}") + except Exception as e: + print(f"⚠️ Could not research movements for {charity.name}: {e}") + elif movement_count > 0: + print(f" ✅ Already has {movement_count} movements, skipping research") + else: + print(f" ⚠️ No website URL, skipping movement research") continue charity = Charity.objects.create(**charity_data) created_charities.append(charity) + # Trigger movement research synchronously if website URL exists + # This ensures movements are populated before deployment completes + if charity.website_url: + try: + print(f"🔄 Researching movements for {charity.name}...") + result = research_charity_sync(charity.id, max_pages=6) + if result.get('success'): + movements_count = result.get('movements_found', 0) + print(f" ✅ Found {movements_count} movements") + else: + print(f" ⚠️ Research failed: {result.get('error', 'Unknown error')}") + except Exception as e: + print(f"⚠️ Could not research movements for {charity.name}: {e}") + status = "✅ VERIFIED" if charity.is_verified else "⏳ PENDING" print(f"✅ Created: {charity.name}") print(f" Category: {charity.get_category_display()}") @@ -205,3 +240,5 @@ print(f" {charity.name}: {charity.aptos_wallet_address}") print("\n✨ Test charities are ready for blockchain donations!") +print("\n💡 Movements have been researched and populated for charities with website URLs.") +print(" To check movements: GET /api/movements/ or GET /api/charities//movements/") diff --git a/backend/eunoia_backend/trigger_movement_research.py b/backend/eunoia_backend/trigger_movement_research.py new file mode 100644 index 0000000..a44bd32 --- /dev/null +++ b/backend/eunoia_backend/trigger_movement_research.py @@ -0,0 +1,213 @@ +import os +import django +import threading + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eunoia_backend.settings') +django.setup() + +from main.models import Charity +from agents_sdk import launch_charity_research_in_background, research_charity_sync + +# List of test charity names from register_test_charities.py +test_charity_names = [ + 'Global Water Initiative', + 'Education For All Foundation', + 'Wildlife Conservation Network', + 'Green Earth Climate Action', + 'Community Health Partners', + 'Arts & Culture Preservation Society', + 'Human Rights Advocacy Network', + 'Disaster Relief Response Team', + 'Rural Development Initiative', + 'Tech For Good Collective' +] + +def trigger_research_for_test_charities(wait_for_completion=False): + """Trigger movement research for all test charities.""" + print("=" * 60) + print("Triggering movement research for test charities...") + if wait_for_completion: + print("⏳ Running synchronously (will wait for completion)...") + else: + print("🔄 Running asynchronously (background threads)...") + print("=" * 60) + + triggered = 0 + skipped = 0 + threads = [] + + for name in test_charity_names: + charity = Charity.objects.filter(name=name).first() + if not charity: + print(f"⚠️ '{name}' not found in database, skipping...") + skipped += 1 + continue + + if not charity.website_url: + print(f"⚠️ '{name}' has no website URL, skipping...") + skipped += 1 + continue + + print(f"🔄 {'Researching' if wait_for_completion else 'Launching research for'}: {name} (ID: {charity.id})") + print(f" Website: {charity.website_url}") + + if wait_for_completion: + # Run synchronously + try: + result = research_charity_sync(charity.id, max_pages=6) + if result.get('success'): + print(f" ✅ Completed: {result.get('movements_found', 0)} movements found") + else: + print(f" ❌ Failed: {result.get('error')}") + except Exception as e: + print(f" ❌ Error: {str(e)}") + else: + # Run asynchronously but track threads + def _research_with_tracking(charity_id, charity_name): + try: + result = research_charity_sync(charity_id, max_pages=6) + if result.get('success'): + print(f" ✅ {charity_name}: {result.get('movements_found', 0)} movements found") + else: + print(f" ❌ {charity_name}: {result.get('error')}") + except Exception as e: + print(f" ❌ {charity_name}: Error - {str(e)}") + + thread = threading.Thread( + target=_research_with_tracking, + args=(charity.id, name), + daemon=False # Non-daemon so it keeps running + ) + thread.start() + threads.append(thread) + + triggered += 1 + print("-" * 60) + + # Wait for all threads to complete if running async + if not wait_for_completion and threads: + print("\n⏳ Waiting for background research to complete...") + print(" (This may take several minutes per charity)") + for i, thread in enumerate(threads, 1): + thread.join() + print(f" [{i}/{len(threads)}] Thread completed") + + print("=" * 60) + print(f"✅ Successfully {'completed' if wait_for_completion else 'triggered'} research for {triggered} charities!") + if skipped > 0: + print(f"⚠️ Skipped {skipped} charities (not found or no website)") + + if wait_for_completion: + print("\n✨ All research completed! Check the database for movements.") + else: + print("\n✨ Research completed! Check the database for movements.") + +def trigger_research_for_all_charities(): + """Trigger movement research for ALL charities with website URLs.""" + print("=" * 60) + print("Triggering movement research for ALL charities with websites...") + print("=" * 60) + + charities = Charity.objects.exclude(website_url__isnull=True).exclude(website_url='') + total = charities.count() + + print(f"Found {total} charities with website URLs\n") + + triggered = 0 + for charity in charities: + print(f"🔄 Launching research for: {charity.name} (ID: {charity.id})") + print(f" Website: {charity.website_url}") + launch_charity_research_in_background(charity.id, max_pages=6) + triggered += 1 + + print("=" * 60) + print(f"✅ Successfully triggered research for {triggered} charities!") + print("\n💡 Note: Research runs in background threads. Check logs for progress.") + +def trigger_research_for_charity_by_id(charity_id: int): + """Trigger movement research for a specific charity by ID.""" + try: + charity = Charity.objects.get(id=charity_id) + + if not charity.website_url: + print(f"❌ Charity '{charity.name}' (ID: {charity_id}) has no website URL!") + return + + print(f"🔄 Launching research for: {charity.name} (ID: {charity_id})") + print(f" Website: {charity.website_url}") + launch_charity_research_in_background(charity_id, max_pages=6) + print(f"✅ Research triggered! Check logs for progress.") + + except Charity.DoesNotExist: + print(f"❌ Charity with ID {charity_id} not found!") + +def trigger_research_for_charity_by_name(name: str): + """Trigger movement research for a specific charity by name.""" + charity = Charity.objects.filter(name=name).first() + + if not charity: + print(f"❌ Charity '{name}' not found!") + return + + if not charity.website_url: + print(f"❌ Charity '{name}' has no website URL!") + return + + print(f"🔄 Launching research for: {charity.name} (ID: {charity.id})") + print(f" Website: {charity.website_url}") + launch_charity_research_in_background(charity.id, max_pages=6) + print(f"✅ Research triggered! Check logs for progress.") + +if __name__ == "__main__": + import sys + + # Check for --wait flag + wait_for_completion = "--wait" in sys.argv or "--sync" in sys.argv + if wait_for_completion: + sys.argv = [arg for arg in sys.argv if arg not in ["--wait", "--sync"]] + + if len(sys.argv) > 1: + arg = sys.argv[1].lower() + + if arg == "--all": + trigger_research_for_all_charities() + elif arg == "--test": + trigger_research_for_test_charities(wait_for_completion=wait_for_completion) + elif arg.isdigit(): + # Assume it's a charity ID + if wait_for_completion: + charity = Charity.objects.get(id=int(arg)) + result = research_charity_sync(int(arg), max_pages=6) + if result.get('success'): + print(f"✅ Completed: {result.get('movements_found', 0)} movements found") + else: + print(f"❌ Failed: {result.get('error')}") + else: + trigger_research_for_charity_by_id(int(arg)) + else: + # Assume it's a charity name + if wait_for_completion: + charity = Charity.objects.filter(name=sys.argv[1]).first() + if charity: + result = research_charity_sync(charity.id, max_pages=6) + if result.get('success'): + print(f"✅ Completed: {result.get('movements_found', 0)} movements found") + else: + print(f"❌ Failed: {result.get('error')}") + else: + print(f"❌ Charity '{sys.argv[1]}' not found!") + else: + trigger_research_for_charity_by_name(sys.argv[1]) + else: + # Default: trigger for test charities + print("Usage:") + print(" python trigger_movement_research.py # Test charities (async)") + print(" python trigger_movement_research.py --wait # Test charities (sync, wait)") + print(" python trigger_movement_research.py --test # Test charities (async)") + print(" python trigger_movement_research.py --all # All charities") + print(" python trigger_movement_research.py # Specific charity by ID") + print(" python trigger_movement_research.py '' # Specific charity by name") + print("\nAdd --wait or --sync to wait for completion (synchronous mode)") + print() + trigger_research_for_test_charities(wait_for_completion=wait_for_completion) + diff --git a/eunoia_web/package-lock.json b/eunoia_web/package-lock.json index dcd1e1e..9896c7c 100644 --- a/eunoia_web/package-lock.json +++ b/eunoia_web/package-lock.json @@ -16,22 +16,29 @@ "@mui/icons-material": "^7.1.0", "@mui/material": "^7.1.0", "@polkadot-api/descriptors": "file:.papi\\descriptors", + "@polkadot/api": "^10.12.2", + "@polkadot/extension-dapp": "^0.46.6", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", + "leaflet": "^1.9.4", "petra-plugin-wallet-adapter": "^0.4.5", "polkadot-api": "^1.17.1", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-leaflet": "^5.0.0", "react-router-dom": "^7.6.0", "react-scripts": "^5.0.1", "web-vitals": "^2.1.4" + }, + "engines": { + "node": ">=20.0.0" } }, ".papi/descriptors": { "name": "@polkadot-api/descriptors", - "version": "0.1.0-autogenerated.6554241485927049051", + "version": "0.1.0-autogenerated.12133326621938921434", "peerDependencies": { "polkadot-api": ">=1.11.2" } @@ -4760,6 +4767,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@polkadot-api/client": { + "version": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/client/-/client-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz", + "integrity": "sha512-0fqK6pUKcGHSG2pBvY+gfSS+1mMdjd/qRygAcKI5d05tKsnZLRnmhb9laDguKmGEIB0Bz9vQqNK3gIN/cfvVwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@polkadot-api/metadata-builders": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "@polkadot-api/substrate-bindings": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "@polkadot-api/substrate-client": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "@polkadot-api/utils": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" + }, + "peerDependencies": { + "rxjs": ">=7.8.0" + } + }, "node_modules/@polkadot-api/codegen": { "version": "0.18.3", "resolved": "https://registry.npmjs.org/@polkadot-api/codegen/-/codegen-0.18.3.tgz", @@ -4874,6 +4897,20 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz", + "integrity": "sha512-EaUS9Fc3wsiUr6ZS43PQqaRScW7kM6DYbuM/ou0aYjm8N9MBqgDbGm2oL6RE1vAVmOfEuHcXZuZkhzWtyvQUtA==", + "license": "MIT", + "optional": true + }, + "node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz", + "integrity": "sha512-0hZ8vtjcsyCX8AyqP2sqUHa1TFFfxGWmlXJkit0Nqp9b32MwZqn5eaUAiV2rNuEpoglKOdKnkGtUF8t5MoodKw==", + "license": "MIT", + "optional": true + }, "node_modules/@polkadot-api/known-chains": { "version": "0.9.8", "resolved": "https://registry.npmjs.org/@polkadot-api/known-chains/-/known-chains-0.9.8.tgz", @@ -5000,6 +5037,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@polkadot-api/metadata-builders": { + "version": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz", + "integrity": "sha512-BD7rruxChL1VXt0icC2gD45OtT9ofJlql0qIllHSRYgama1CR2Owt+ApInQxB+lWqM+xNOznZRpj8CXNDvKIMg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@polkadot-api/substrate-bindings": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "@polkadot-api/utils": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0" + } + }, "node_modules/@polkadot-api/metadata-compatibility": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-compatibility/-/metadata-compatibility-0.3.5.tgz", @@ -5302,6 +5350,33 @@ } } }, + "node_modules/@polkadot-api/substrate-bindings": { + "version": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz", + "integrity": "sha512-N4vdrZopbsw8k57uG58ofO7nLXM4Ai7835XqakN27MkjXMp5H830A1KJE0L9sGQR7ukOCDEIHHcwXVrzmJ/PBg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@noble/hashes": "^1.3.1", + "@polkadot-api/utils": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "@scure/base": "^1.1.1", + "scale-ts": "^1.6.0" + } + }, + "node_modules/@polkadot-api/substrate-client": { + "version": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-client/-/substrate-client-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz", + "integrity": "sha512-lcdvd2ssUmB1CPzF8s2dnNOqbrDa+nxaaGbuts+Vo8yjgSKwds2Lo7Oq+imZN4VKW7t9+uaVcKFLMF7PdH0RWw==", + "license": "MIT", + "optional": true + }, + "node_modules/@polkadot-api/utils": { + "version": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0.tgz", + "integrity": "sha512-0CYaCjfLQJTCRCiYvZ81OncHXEKPzAexCMoVloR+v2nl/O2JRya/361MtPkeNLC6XBoaEgLAG9pWQpH3WePzsw==", + "license": "MIT", + "optional": true + }, "node_modules/@polkadot-api/wasm-executor": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@polkadot-api/wasm-executor/-/wasm-executor-0.2.1.tgz", @@ -5347,6 +5422,607 @@ } } }, + "node_modules/@polkadot/api": { + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@polkadot/api/-/api-10.13.1.tgz", + "integrity": "sha512-YrKWR4TQR5CDyGkF0mloEUo7OsUA+bdtENpJGOtNavzOQUDEbxFE0PVzokzZfVfHhHX2CojPVmtzmmLxztyJkg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api-augment": "10.13.1", + "@polkadot/api-base": "10.13.1", + "@polkadot/api-derive": "10.13.1", + "@polkadot/keyring": "^12.6.2", + "@polkadot/rpc-augment": "10.13.1", + "@polkadot/rpc-core": "10.13.1", + "@polkadot/rpc-provider": "10.13.1", + "@polkadot/types": "10.13.1", + "@polkadot/types-augment": "10.13.1", + "@polkadot/types-codec": "10.13.1", + "@polkadot/types-create": "10.13.1", + "@polkadot/types-known": "10.13.1", + "@polkadot/util": "^12.6.2", + "@polkadot/util-crypto": "^12.6.2", + "eventemitter3": "^5.0.1", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-augment": { + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@polkadot/api-augment/-/api-augment-10.13.1.tgz", + "integrity": "sha512-IAKaCp19QxgOG4HKk9RAgUgC/VNVqymZ2GXfMNOZWImZhxRIbrK+raH5vN2MbWwtVHpjxyXvGsd1RRhnohI33A==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api-base": "10.13.1", + "@polkadot/rpc-augment": "10.13.1", + "@polkadot/types": "10.13.1", + "@polkadot/types-augment": "10.13.1", + "@polkadot/types-codec": "10.13.1", + "@polkadot/util": "^12.6.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-base": { + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@polkadot/api-base/-/api-base-10.13.1.tgz", + "integrity": "sha512-Okrw5hjtEjqSMOG08J6qqEwlUQujTVClvY1/eZkzKwNzPelWrtV6vqfyJklB7zVhenlxfxqhZKKcY7zWSW/q5Q==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/rpc-core": "10.13.1", + "@polkadot/types": "10.13.1", + "@polkadot/util": "^12.6.2", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-derive": { + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@polkadot/api-derive/-/api-derive-10.13.1.tgz", + "integrity": "sha512-ef0H0GeCZ4q5Om+c61eLLLL29UxFC2/u/k8V1K2JOIU+2wD5LF7sjAoV09CBMKKHfkLenRckVk2ukm4rBqFRpg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api": "10.13.1", + "@polkadot/api-augment": "10.13.1", + "@polkadot/api-base": "10.13.1", + "@polkadot/rpc-core": "10.13.1", + "@polkadot/types": "10.13.1", + "@polkadot/types-codec": "10.13.1", + "@polkadot/util": "^12.6.2", + "@polkadot/util-crypto": "^12.6.2", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/@polkadot/extension-dapp": { + "version": "0.46.9", + "resolved": "https://registry.npmjs.org/@polkadot/extension-dapp/-/extension-dapp-0.46.9.tgz", + "integrity": "sha512-y5udSeQ/X9MEoyjlpTcCn0UAEjZ2jjy6U3V/jiVFQo5vBKhdqAhN1oN8X5c4yWurmhYM/7oibImxAjEoXuwH+Q==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/extension-inject": "0.46.9", + "@polkadot/util": "^12.6.2", + "@polkadot/util-crypto": "^12.6.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/api": "*", + "@polkadot/util": "*", + "@polkadot/util-crypto": "*" + } + }, + "node_modules/@polkadot/extension-inject": { + "version": "0.46.9", + "resolved": "https://registry.npmjs.org/@polkadot/extension-inject/-/extension-inject-0.46.9.tgz", + "integrity": "sha512-m0jnrs9+jEOpMH6OUNl7nHpz9SFFWK9LzuqB8T3htEE3RUYPL//SLCPyEKxAAgHu7F8dgkUHssAWQfANofALCQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api": "^10.12.4", + "@polkadot/rpc-provider": "^10.12.4", + "@polkadot/types": "^10.12.4", + "@polkadot/util": "^12.6.2", + "@polkadot/util-crypto": "^12.6.2", + "@polkadot/x-global": "^12.6.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/api": "*", + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/keyring": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/@polkadot/keyring/-/keyring-12.6.2.tgz", + "integrity": "sha512-O3Q7GVmRYm8q7HuB3S0+Yf/q/EB2egKRRU3fv9b3B7V+A52tKzA+vIwEmNVaD1g5FKW9oB97rmpggs0zaKFqHw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "12.6.2", + "@polkadot/util-crypto": "12.6.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "12.6.2", + "@polkadot/util-crypto": "12.6.2" + } + }, + "node_modules/@polkadot/networks": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/@polkadot/networks/-/networks-12.6.2.tgz", + "integrity": "sha512-1oWtZm1IvPWqvMrldVH6NI2gBoCndl5GEwx7lAuQWGr7eNL+6Bdc5K3Z9T0MzFvDGoi2/CBqjX9dRKo39pDC/w==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "12.6.2", + "@substrate/ss58-registry": "^1.44.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-augment": { + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-augment/-/rpc-augment-10.13.1.tgz", + "integrity": "sha512-iLsWUW4Jcx3DOdVrSHtN0biwxlHuTs4QN2hjJV0gd0jo7W08SXhWabZIf9mDmvUJIbR7Vk+9amzvegjRyIf5+A==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/rpc-core": "10.13.1", + "@polkadot/types": "10.13.1", + "@polkadot/types-codec": "10.13.1", + "@polkadot/util": "^12.6.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-core": { + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-core/-/rpc-core-10.13.1.tgz", + "integrity": "sha512-eoejSHa+/tzHm0vwic62/aptTGbph8vaBpbvLIK7gd00+rT813ROz5ckB1CqQBFB23nHRLuzzX/toY8ID3xrKw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/rpc-augment": "10.13.1", + "@polkadot/rpc-provider": "10.13.1", + "@polkadot/types": "10.13.1", + "@polkadot/util": "^12.6.2", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-provider": { + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-provider/-/rpc-provider-10.13.1.tgz", + "integrity": "sha512-oJ7tatVXYJ0L7NpNiGd69D558HG5y5ZDmH2Bp9Dd4kFTQIiV8A39SlWwWUPCjSsen9lqSvvprNLnG/VHTpenbw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/keyring": "^12.6.2", + "@polkadot/types": "10.13.1", + "@polkadot/types-support": "10.13.1", + "@polkadot/util": "^12.6.2", + "@polkadot/util-crypto": "^12.6.2", + "@polkadot/x-fetch": "^12.6.2", + "@polkadot/x-global": "^12.6.2", + "@polkadot/x-ws": "^12.6.2", + "eventemitter3": "^5.0.1", + "mock-socket": "^9.3.1", + "nock": "^13.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@substrate/connect": "0.8.8" + } + }, + "node_modules/@polkadot/rpc-provider/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/@polkadot/types": { + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@polkadot/types/-/types-10.13.1.tgz", + "integrity": "sha512-Hfvg1ZgJlYyzGSAVrDIpp3vullgxrjOlh/CSThd/PI4TTN1qHoPSFm2hs77k3mKkOzg+LrWsLE0P/LP2XddYcw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/keyring": "^12.6.2", + "@polkadot/types-augment": "10.13.1", + "@polkadot/types-codec": "10.13.1", + "@polkadot/types-create": "10.13.1", + "@polkadot/util": "^12.6.2", + "@polkadot/util-crypto": "^12.6.2", + "rxjs": "^7.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-augment": { + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@polkadot/types-augment/-/types-augment-10.13.1.tgz", + "integrity": "sha512-TcrLhf95FNFin61qmVgOgayzQB/RqVsSg9thAso1Fh6pX4HSbvI35aGPBAn3SkA6R+9/TmtECirpSNLtIGFn0g==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/types": "10.13.1", + "@polkadot/types-codec": "10.13.1", + "@polkadot/util": "^12.6.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-codec": { + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@polkadot/types-codec/-/types-codec-10.13.1.tgz", + "integrity": "sha512-AiQ2Vv2lbZVxEdRCN8XSERiWlOWa2cTDLnpAId78EnCtx4HLKYQSd+Jk9Y4BgO35R79mchK4iG+w6gZ+ukG2bg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "^12.6.2", + "@polkadot/x-bigint": "^12.6.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-create": { + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@polkadot/types-create/-/types-create-10.13.1.tgz", + "integrity": "sha512-Usn1jqrz35SXgCDAqSXy7mnD6j4RvB4wyzTAZipFA6DGmhwyxxIgOzlWQWDb+1PtPKo9vtMzen5IJ+7w5chIeA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/types-codec": "10.13.1", + "@polkadot/util": "^12.6.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-known": { + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@polkadot/types-known/-/types-known-10.13.1.tgz", + "integrity": "sha512-uHjDW05EavOT5JeU8RbiFWTgPilZ+odsCcuEYIJGmK+es3lk/Qsdns9Zb7U7NJl7eJ6OWmRtyrWsLs+bU+jjIQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/networks": "^12.6.2", + "@polkadot/types": "10.13.1", + "@polkadot/types-codec": "10.13.1", + "@polkadot/types-create": "10.13.1", + "@polkadot/util": "^12.6.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-support": { + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@polkadot/types-support/-/types-support-10.13.1.tgz", + "integrity": "sha512-4gEPfz36XRQIY7inKq0HXNVVhR6HvXtm7yrEmuBuhM86LE0lQQBkISUSgR358bdn2OFSLMxMoRNoh3kcDvdGDQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "^12.6.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-12.6.2.tgz", + "integrity": "sha512-l8TubR7CLEY47240uki0TQzFvtnxFIO7uI/0GoWzpYD/O62EIAMRsuY01N4DuwgKq2ZWD59WhzsLYmA5K6ksdw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-bigint": "12.6.2", + "@polkadot/x-global": "12.6.2", + "@polkadot/x-textdecoder": "12.6.2", + "@polkadot/x-textencoder": "12.6.2", + "@types/bn.js": "^5.1.5", + "bn.js": "^5.2.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util-crypto": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-12.6.2.tgz", + "integrity": "sha512-FEWI/dJ7wDMNN1WOzZAjQoIcCP/3vz3wvAp5QQm+lOrzOLj0iDmaIGIcBkz8HVm3ErfSe/uKP0KS4jgV/ib+Mg==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@polkadot/networks": "12.6.2", + "@polkadot/util": "12.6.2", + "@polkadot/wasm-crypto": "^7.3.2", + "@polkadot/wasm-util": "^7.3.2", + "@polkadot/x-bigint": "12.6.2", + "@polkadot/x-randomvalues": "12.6.2", + "@scure/base": "^1.1.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "12.6.2" + } + }, + "node_modules/@polkadot/wasm-bridge": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-bridge/-/wasm-bridge-7.5.1.tgz", + "integrity": "sha512-E+N3CSnX3YaXpAmfIQ+4bTyiAqJQKvVcMaXjkuL8Tp2zYffClWLG5e+RY15Uh+EWfUl9If4y6cLZi3D5NcpAGQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-util": "7.5.1", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto/-/wasm-crypto-7.5.1.tgz", + "integrity": "sha512-acjt4VJ3w19v7b/SIPsV/5k9s6JsragHKPnwoZ0KTfBvAFXwzz80jUzVGxA06SKHacfCUe7vBRlz7M5oRby1Pw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-bridge": "7.5.1", + "@polkadot/wasm-crypto-asmjs": "7.5.1", + "@polkadot/wasm-crypto-init": "7.5.1", + "@polkadot/wasm-crypto-wasm": "7.5.1", + "@polkadot/wasm-util": "7.5.1", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-asmjs": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.5.1.tgz", + "integrity": "sha512-jAg7Uusk+xeHQ+QHEH4c/N3b1kEGBqZb51cWe+yM61kKpQwVGZhNdlWetW6U23t/BMyZArIWMsZqmK/Ij0PHog==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-init": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.5.1.tgz", + "integrity": "sha512-Obu4ZEo5jYO6sN31eqCNOXo88rPVkP9TrUOyynuFCnXnXr8V/HlmY/YkAd9F87chZnkTJRlzak17kIWr+i7w3A==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-bridge": "7.5.1", + "@polkadot/wasm-crypto-asmjs": "7.5.1", + "@polkadot/wasm-crypto-wasm": "7.5.1", + "@polkadot/wasm-util": "7.5.1", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-wasm": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.5.1.tgz", + "integrity": "sha512-S2yQSGbOGTcaV6UdipFVyEGanJvG6uD6Tg7XubxpiGbNAblsyYKeFcxyH1qCosk/4qf+GIUwlOL4ydhosZflqg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-util": "7.5.1", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/wasm-util": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-util/-/wasm-util-7.5.1.tgz", + "integrity": "sha512-sbvu71isFhPXpvMVX+EkRnUg/+54Tx7Sf9BEMqxxoPj7cG1I/MKeDEwbQz6MaU4gm7xJqvEWCAemLFcXfHQ/2A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/x-bigint": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-bigint/-/x-bigint-12.6.2.tgz", + "integrity": "sha512-HSIk60uFPX4GOFZSnIF7VYJz7WZA7tpFJsne7SzxOooRwMTWEtw3fUpFy5cYYOeLh17/kHH1Y7SVcuxzVLc74Q==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "12.6.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-fetch": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-fetch/-/x-fetch-12.6.2.tgz", + "integrity": "sha512-8wM/Z9JJPWN1pzSpU7XxTI1ldj/AfC8hKioBlUahZ8gUiJaOF7K9XEFCrCDLis/A1BoOu7Ne6WMx/vsJJIbDWw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "12.6.2", + "node-fetch": "^3.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-fetch/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@polkadot/x-global": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-global/-/x-global-12.6.2.tgz", + "integrity": "sha512-a8d6m+PW98jmsYDtAWp88qS4dl8DyqUBsd0S+WgyfSMtpEXu6v9nXDgPZgwF5xdDvXhm+P0ZfVkVTnIGrScb5g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-randomvalues": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-randomvalues/-/x-randomvalues-12.6.2.tgz", + "integrity": "sha512-Vr8uG7rH2IcNJwtyf5ebdODMcr0XjoCpUbI91Zv6AlKVYOGKZlKLYJHIwpTaKKB+7KPWyQrk4Mlym/rS7v9feg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "12.6.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "12.6.2", + "@polkadot/wasm-util": "*" + } + }, + "node_modules/@polkadot/x-textdecoder": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-textdecoder/-/x-textdecoder-12.6.2.tgz", + "integrity": "sha512-M1Bir7tYvNappfpFWXOJcnxUhBUFWkUFIdJSyH0zs5LmFtFdbKAeiDXxSp2Swp5ddOZdZgPac294/o2TnQKN1w==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "12.6.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-textencoder": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-textencoder/-/x-textencoder-12.6.2.tgz", + "integrity": "sha512-4N+3UVCpI489tUJ6cv3uf0PjOHvgGp9Dl+SZRLgFGt9mvxnvpW/7+XBADRMtlG4xi5gaRK7bgl5bmY6OMDsNdw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "12.6.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-ws": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/@polkadot/x-ws/-/x-ws-12.6.2.tgz", + "integrity": "sha512-cGZWo7K5eRRQCRl2LrcyCYsrc3lRbTlixZh3AzgU8uX4wASVGRlNWi/Hf4TtHNe1ExCDmxabJzdIsABIfrr7xw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "12.6.2", + "tslib": "^2.6.2", + "ws": "^8.15.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-ws/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -5387,6 +6063,17 @@ } } }, + "node_modules/@react-leaflet/core": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz", + "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -6036,6 +6723,59 @@ "node": ">=14.17" } }, + "node_modules/@substrate/connect": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@substrate/connect/-/connect-0.8.8.tgz", + "integrity": "sha512-zwaxuNEVI9bGt0rT8PEJiXOyebLIo6QN1SyiAHRPBOl6g3Sy0KKdSN8Jmyn++oXhVRD8aIe75/V8ZkS81T+BPQ==", + "deprecated": "versions below 1.x are no longer maintained", + "license": "GPL-3.0-only", + "optional": true, + "dependencies": { + "@substrate/connect-extension-protocol": "^2.0.0", + "@substrate/connect-known-chains": "^1.1.1", + "@substrate/light-client-extension-helpers": "^0.0.4", + "smoldot": "2.0.22" + } + }, + "node_modules/@substrate/connect-extension-protocol": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@substrate/connect-extension-protocol/-/connect-extension-protocol-2.2.2.tgz", + "integrity": "sha512-t66jwrXA0s5Goq82ZtjagLNd7DPGCNjHeehRlE/gcJmJ+G56C0W+2plqOMRicJ8XGR1/YFnUSEqUFiSNbjGrAA==", + "license": "GPL-3.0-only", + "optional": true + }, + "node_modules/@substrate/connect-known-chains": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@substrate/connect-known-chains/-/connect-known-chains-1.10.3.tgz", + "integrity": "sha512-OJEZO1Pagtb6bNE3wCikc2wrmvEU5x7GxFFLqqbz1AJYYxSlrPCGu4N2og5YTExo4IcloNMQYFRkBGue0BKZ4w==", + "license": "GPL-3.0-only", + "optional": true + }, + "node_modules/@substrate/light-client-extension-helpers": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@substrate/light-client-extension-helpers/-/light-client-extension-helpers-0.0.4.tgz", + "integrity": "sha512-vfKcigzL0SpiK+u9sX6dq2lQSDtuFLOxIJx2CKPouPEHIs8C+fpsufn52r19GQn+qDhU8POMPHOVoqLktj8UEA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@polkadot-api/client": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "@polkadot-api/json-rpc-provider": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "@polkadot-api/json-rpc-provider-proxy": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "@polkadot-api/substrate-client": "0.0.1-492c132563ea6b40ae1fc5470dec4cd18768d182.1.0", + "@substrate/connect-extension-protocol": "^2.0.0", + "@substrate/connect-known-chains": "^1.1.1", + "rxjs": "^7.8.1" + }, + "peerDependencies": { + "smoldot": "2.x" + } + }, + "node_modules/@substrate/ss58-registry": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/@substrate/ss58-registry/-/ss58-registry-1.51.0.tgz", + "integrity": "sha512-TWDurLiPxndFgKjVavCniytBIw+t4ViOi7TYp9h/D0NMmkEc9klFTo+827eyEJ0lELpqO207Ey7uGxUa+BS1jQ==", + "license": "Apache-2.0" + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -6471,6 +7211,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -8293,8 +9042,7 @@ "node_modules/bn.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", - "peer": true + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==" }, "node_modules/body-parser": { "version": "1.20.3", @@ -9556,6 +10304,15 @@ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", @@ -11351,6 +12108,29 @@ "bser": "2.1.1" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/figures": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", @@ -11761,6 +12541,18 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -14462,8 +15254,7 @@ "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "peer": true + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, "node_modules/json5": { "version": "2.2.3", @@ -14596,6 +15387,12 @@ "shell-quote": "^1.8.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -15030,6 +15827,15 @@ "ufo": "^1.6.1" } }, + "node_modules/mock-socket": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", + "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -15106,6 +15912,40 @@ "tslib": "^2.0.3" } }, + "node_modules/nock": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz", + "integrity": "sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -17365,6 +18205,15 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -17671,6 +18520,20 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-leaflet": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz", + "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^3.0.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -18910,6 +19773,38 @@ "node": ">=8" } }, + "node_modules/smoldot": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/smoldot/-/smoldot-2.0.22.tgz", + "integrity": "sha512-B50vRgTY6v3baYH6uCgL15tfaag5tcS2o/P5q1OiXcKGv1axZDfz2dzzMuIkVpyMR2ug11F6EAtQlmYBQd292g==", + "license": "GPL-3.0-or-later WITH Classpath-exception-2.0", + "optional": true, + "dependencies": { + "ws": "^8.8.1" + } + }, + "node_modules/smoldot/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -20837,6 +21732,15 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/web-vitals": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", diff --git a/eunoia_web/package.json b/eunoia_web/package.json index 53c107e..e5395ea 100644 --- a/eunoia_web/package.json +++ b/eunoia_web/package.json @@ -17,10 +17,12 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^13.5.0", + "leaflet": "^1.9.4", "petra-plugin-wallet-adapter": "^0.4.5", "polkadot-api": "^1.17.1", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-leaflet": "^5.0.0", "react-router-dom": "^7.6.0", "react-scripts": "^5.0.1", "web-vitals": "^2.1.4" diff --git a/eunoia_web/src/components/CharityResultCard.js b/eunoia_web/src/components/CharityResultCard.js index 13940d5..9842e3d 100644 --- a/eunoia_web/src/components/CharityResultCard.js +++ b/eunoia_web/src/components/CharityResultCard.js @@ -25,22 +25,24 @@ import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline'; const StyledCard = styled(Card)(({ theme, selected }) => ({ - height: '100%', + minHeight: '320px', + maxHeight: '420px', display: 'flex', flexDirection: 'column', borderRadius: '16px', - transition: 'transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out', - border: selected ? `3px solid ${theme.palette.primary.main}` : `1px solid ${theme.palette.divider}`, + transition: 'all 0.3s ease-in-out', + border: selected ? `2px solid ${theme.palette.primary.main}` : `1px solid ${theme.palette.divider}`, boxShadow: selected - ? `0 10px 25px ${alpha(theme.palette.primary.main, 0.35)}` - : theme.shadows[3], + ? `0 8px 20px ${alpha(theme.palette.primary.main, 0.3)}` + : theme.shadows[2], cursor: 'pointer', position: 'relative', + backgroundColor: selected ? alpha(theme.palette.primary.main, 0.02) : theme.palette.background.paper, '&:hover': { - transform: 'translateY(-5px)', + transform: 'translateY(-3px)', boxShadow: selected - ? `0 14px 30px ${alpha(theme.palette.primary.main, 0.4)}` - : theme.shadows[6], + ? `0 10px 24px ${alpha(theme.palette.primary.main, 0.35)}` + : theme.shadows[4], }, })); @@ -109,8 +111,8 @@ const CharityResultCard = ({ }} /> )} - - + + + {charity?.name || 'Charity'} diff --git a/eunoia_web/src/components/ImpactMap.js b/eunoia_web/src/components/ImpactMap.js index c0c6d15..f9c2834 100644 --- a/eunoia_web/src/components/ImpactMap.js +++ b/eunoia_web/src/components/ImpactMap.js @@ -1,123 +1,248 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { Box, Typography, Paper, Chip } from '@mui/material'; import { styled, alpha } from '@mui/material/styles'; +import { MapContainer, TileLayer, Marker, Popup, Circle } from 'react-leaflet'; +import L from 'leaflet'; +import 'leaflet/dist/leaflet.css'; +// Fix for default marker icons in react-leaflet +delete L.Icon.Default.prototype._getIconUrl; +L.Icon.Default.mergeOptions({ + iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'), + iconUrl: require('leaflet/dist/images/marker-icon.png'), + shadowUrl: require('leaflet/dist/images/marker-shadow.png'), +}); -// Styled map container -const MapContainer = styled(Paper)(({ theme }) => ({ +// Styled map container wrapper +const MapWrapper = styled(Box)(({ theme }) => ({ position: 'relative', - height: '300px', + height: '350px', width: '100%', - borderRadius: '8px', + borderRadius: '12px', overflow: 'hidden', - backgroundImage: 'url("https://images.unsplash.com/photo-1530056046039-5488bdcc0de6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=600&q=80")', - backgroundSize: 'cover', - backgroundPosition: 'center', - display: 'flex', - flexDirection: 'column', - justifyContent: 'flex-end', - '&::before': { - content: '""', - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: alpha(theme.palette.common.black, 0.4), - zIndex: 1 + '& .leaflet-container': { + height: '100%', + width: '100%', + borderRadius: '12px', + zIndex: 0 } })); -// Location marker component -const ImpactMarker = styled(Paper)(({ theme, color = 'primary.main' }) => ({ - position: 'absolute', - width: '12px', - height: '12px', - borderRadius: '50%', - backgroundColor: theme.palette[color.split('.')[0]][color.split('.')[1] || 'main'], - border: `2px solid ${theme.palette.common.white}`, - transform: 'translate(-50%, -50%)', - zIndex: 2, - cursor: 'pointer', - boxShadow: theme.shadows[2], - '&:hover': { - transform: 'translate(-50%, -50%) scale(1.5)', - transition: 'transform 0.2s ease-in-out', - }, -})); - // Legend overlay -const MapLegend = styled(Box)(({ theme }) => ({ - position: 'relative', - zIndex: 2, +const MapLegend = styled(Paper)(({ theme }) => ({ + position: 'absolute', + bottom: '10px', + left: '10px', + zIndex: 1000, padding: theme.spacing(1.5), - backgroundColor: alpha(theme.palette.background.paper, 0.85), - backdropFilter: 'blur(4px)', - borderTop: `1px solid ${alpha(theme.palette.divider, 0.6)}`, + backgroundColor: alpha(theme.palette.background.paper, 0.95), + backdropFilter: 'blur(8px)', + borderRadius: '8px', + maxWidth: '250px', + boxShadow: theme.shadows[3], })); -// Mock geocoding function - Simplified coordinates for visual placement only -const MOCK_GEOCODES = { - 'Hope Uganda Initiative': { x: 62, y: 42, name: 'Kampala, Uganda' }, - "African Children's Fund": { x: 65, y: 48, name: 'Nairobi, Kenya' }, - 'Faithful Scholars Africa': { x: 67, y: 52, name: 'Mombasa, Kenya' }, - 'default': { x: 55, y: 45 } +// Geocoding database - Maps locations to coordinates +const LOCATION_COORDINATES = { + // Countries + 'Uganda': { lat: 1.3733, lng: 32.2903, name: 'Uganda' }, + 'Kenya': { lat: -0.0236, lng: 37.9062, name: 'Kenya' }, + 'Tanzania': { lat: -6.3690, lng: 34.8888, name: 'Tanzania' }, + 'Rwanda': { lat: -1.9403, lng: 29.8739, name: 'Rwanda' }, + 'Ethiopia': { lat: 9.1450, lng: 40.4897, name: 'Ethiopia' }, + 'Ghana': { lat: 7.9465, lng: -1.0232, name: 'Ghana' }, + 'Nigeria': { lat: 9.0820, lng: 8.6753, name: 'Nigeria' }, + 'South Africa': { lat: -30.5595, lng: 22.9375, name: 'South Africa' }, + 'India': { lat: 20.5937, lng: 78.9629, name: 'India' }, + 'Bangladesh': { lat: 23.6850, lng: 90.3563, name: 'Bangladesh' }, + 'Nepal': { lat: 28.3949, lng: 84.1240, name: 'Nepal' }, + 'Philippines': { lat: 12.8797, lng: 121.7740, name: 'Philippines' }, + 'Indonesia': { lat: -0.7893, lng: 113.9213, name: 'Indonesia' }, + 'Brazil': { lat: -14.2350, lng: -51.9253, name: 'Brazil' }, + 'Peru': { lat: -9.1900, lng: -75.0152, name: 'Peru' }, + 'Colombia': { lat: 4.5709, lng: -74.2973, name: 'Colombia' }, + 'Mexico': { lat: 23.6345, lng: -102.5528, name: 'Mexico' }, + 'Haiti': { lat: 18.9712, lng: -72.2852, name: 'Haiti' }, + + // Cities + 'Kampala': { lat: 0.3476, lng: 32.5825, name: 'Kampala, Uganda' }, + 'Nairobi': { lat: -1.2864, lng: 36.8172, name: 'Nairobi, Kenya' }, + 'Mombasa': { lat: -4.0435, lng: 39.6682, name: 'Mombasa, Kenya' }, + 'Dar es Salaam': { lat: -6.7924, lng: 39.2083, name: 'Dar es Salaam, Tanzania' }, + 'Kigali': { lat: -1.9706, lng: 30.1044, name: 'Kigali, Rwanda' }, + 'Addis Ababa': { lat: 9.0320, lng: 38.7469, name: 'Addis Ababa, Ethiopia' }, + 'Accra': { lat: 5.6037, lng: -0.1870, name: 'Accra, Ghana' }, + 'Lagos': { lat: 6.5244, lng: 3.3792, name: 'Lagos, Nigeria' }, + 'Mumbai': { lat: 19.0760, lng: 72.8777, name: 'Mumbai, India' }, + 'Dhaka': { lat: 23.8103, lng: 90.4125, name: 'Dhaka, Bangladesh' }, + 'Manila': { lat: 14.5995, lng: 120.9842, name: 'Manila, Philippines' }, + + // Regions + 'East Africa': { lat: -1.2921, lng: 36.8219, name: 'East Africa' }, + 'West Africa': { lat: 7.5400, lng: -5.5471, name: 'West Africa' }, + 'Southern Africa': { lat: -25.7461, lng: 28.1881, name: 'Southern Africa' }, + 'South Asia': { lat: 23.0000, lng: 80.0000, name: 'South Asia' }, + 'Southeast Asia': { lat: 15.0000, lng: 105.0000, name: 'Southeast Asia' }, + 'Latin America': { lat: -8.7832, lng: -55.4915, name: 'Latin America' }, + 'Central America': { lat: 15.0000, lng: -90.0000, name: 'Central America' }, + + // Default fallback + 'Global': { lat: 20.0, lng: 0.0, name: 'Global' }, }; -const getGeocode = (charityName, charityDescription) => { - if (MOCK_GEOCODES[charityName]) { - return MOCK_GEOCODES[charityName]; +// Function to extract location from charity data +const extractLocation = (charity) => { + const text = `${charity.name} ${charity.description}`.toLowerCase(); + + // Try to find specific location matches + for (const [key, value] of Object.entries(LOCATION_COORDINATES)) { + if (text.includes(key.toLowerCase())) { + return { ...value, charityName: charity.name, charityId: charity.id }; + } } - if (charityDescription && charityDescription.toLowerCase().includes('uganda')) { - return MOCK_GEOCODES['Hope Uganda Initiative']; + + // Check for Africa mentions + if (text.includes('africa')) { + return { ...LOCATION_COORDINATES['East Africa'], charityName: charity.name, charityId: charity.id }; } - return MOCK_GEOCODES['default']; + + // Default to Global + return { ...LOCATION_COORDINATES['Global'], charityName: charity.name, charityId: charity.id }; }; const ImpactMap = ({ charities }) => { + const locations = useMemo(() => { + if (!charities || charities.length === 0) return []; + return charities.map(charity => extractLocation(charity)); + }, [charities]); + + // Calculate center and zoom based on locations + const { center, zoom } = useMemo(() => { + if (locations.length === 0) { + return { center: [20, 0], zoom: 2 }; + } + + const avgLat = locations.reduce((sum, loc) => sum + loc.lat, 0) / locations.length; + const avgLng = locations.reduce((sum, loc) => sum + loc.lng, 0) / locations.length; + + // Calculate bounds to determine zoom + const lats = locations.map(l => l.lat); + const lngs = locations.map(l => l.lng); + const latRange = Math.max(...lats) - Math.min(...lats); + const lngRange = Math.max(...lngs) - Math.min(...lngs); + const maxRange = Math.max(latRange, lngRange); + + let calculatedZoom = 2; + if (maxRange < 5) calculatedZoom = 6; + else if (maxRange < 15) calculatedZoom = 5; + else if (maxRange < 30) calculatedZoom = 4; + else if (maxRange < 60) calculatedZoom = 3; + + return { center: [avgLat, avgLng], zoom: calculatedZoom }; + }, [locations]); + + // Group locations by coordinates to avoid overlapping markers + const groupedLocations = useMemo(() => { + const groups = new Map(); + locations.forEach(loc => { + const key = `${loc.lat.toFixed(2)},${loc.lng.toFixed(2)}`; + if (!groups.has(key)) { + groups.set(key, { ...loc, charities: [loc.charityName] }); + } else { + groups.get(key).charities.push(loc.charityName); + } + }); + return Array.from(groups.values()); + }, [locations]); + if (!charities || charities.length === 0) { - return No locations to display on map.; + return ( + + + No locations to display on map. + + + ); } return ( - - {/* Place markers */} - {charities.map((charity, index) => { - const location = getGeocode(charity.name, charity.description); - return ( - + + + - ); - })} - - {/* Legend */} - - - Your Impact Regions - - - {charities.map((charity, index) => { - const location = getGeocode(charity.name, charity.description); - return ( + + {groupedLocations.map((location, index) => ( + + {/* Highlight circle for impact area */} + + + {/* Marker for the location */} + + + + + {location.name} + + + Charities: + + {location.charities.map((charityName, idx) => ( + + ))} + + + + + ))} + + + + + Impact Regions + + + {groupedLocations.slice(0, 3).map((location, index) => ( - ); - })} - - - + ))} + {groupedLocations.length > 3 && ( + + +{groupedLocations.length - 3} more regions + + )} + + + + ); }; -export default ImpactMap; \ No newline at end of file +export default ImpactMap; diff --git a/eunoia_web/src/components/donate/AiProcessingView.jsx b/eunoia_web/src/components/donate/AiProcessingView.jsx index 50f59b4..4405f0d 100644 --- a/eunoia_web/src/components/donate/AiProcessingView.jsx +++ b/eunoia_web/src/components/donate/AiProcessingView.jsx @@ -1,10 +1,9 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { Box, Typography, CircularProgress, - LinearProgress, - Chip + LinearProgress } from '@mui/material'; import { styled } from '@mui/material/styles'; import axios from 'axios'; @@ -32,9 +31,16 @@ const AiProcessingView = ({ setGroupedMatches }) => { console.log('AiProcessingView render'); + const isSearchingRef = useRef(false); useEffect(() => { + // Prevent double execution (especially in React StrictMode) + if (isSearchingRef.current) { + return; + } + const performSemanticSearch = async () => { + isSearchingRef.current = true; if (!visionPrompt.trim()) { setSemanticSearchError("Please enter your vision before searching."); setCurrentStage('visionPrompt'); @@ -43,8 +49,7 @@ const AiProcessingView = ({ setSemanticSearchLoading(true); setSemanticSearchError(null); - setAiMatchedCharities([]); - setAiSuggestedAllocations({}); + // Don't clear charities here - let the new data replace it, avoiding double renders // // Artificial delay for testing animation visibility // await new Promise(resolve => setTimeout(resolve, 5000)); // 5-second delay @@ -179,14 +184,12 @@ const AiProcessingView = ({ setCurrentStage('charityResults'); } finally { setSemanticSearchLoading(false); + isSearchingRef.current = false; } }; performSemanticSearch(); - }, [visionPrompt, totalDonationAmount, setCurrentStage, setAiMatchedCharities, setAiSuggestedAllocations, setSemanticSearchLoading, setSemanticSearchError, setCombinedMissionStatement]); - - const keywords = visionPrompt.split(' ').filter(k => k.length > 3); - if(keywords.length === 0) keywords.push(...['Impact', 'Faith', 'Children', 'Education', 'Africa']); + }, [visionPrompt, totalDonationAmount, setCurrentStage, setAiMatchedCharities, setAiSuggestedAllocations, setSemanticSearchLoading, setSemanticSearchError, setCombinedMissionStatement, setCompassRecommendations, setGroupedMatches]); if (semanticSearchLoading) { return ( @@ -197,9 +200,6 @@ const AiProcessingView = ({ Finding the causes that truly fit you… - - {keywords.slice(0,5).map(kw => )} - Consulting the Eunoia Compass... diff --git a/eunoia_web/src/components/donate/CharityResultsView.jsx b/eunoia_web/src/components/donate/CharityResultsView.jsx index 5e30c1a..995b005 100644 --- a/eunoia_web/src/components/donate/CharityResultsView.jsx +++ b/eunoia_web/src/components/donate/CharityResultsView.jsx @@ -9,7 +9,8 @@ import { alpha, Divider, FormControlLabel, - Switch + Switch, + Chip } from '@mui/material'; import { Link } from 'react-router-dom'; import { styled } from '@mui/material/styles'; @@ -76,21 +77,6 @@ const CharityResultsView = ({ console.log('Selected IDs:', selectedCharityIds); console.log('Individual Amounts:', individualDonationAmounts); - const extractUserInputs = () => { - const missionKeywords = visionPrompt.toLowerCase().match(/\b(empower|support|education|girls|africa|children|communities|health|environment|innovation|faith|art)\b/g) || []; - const uniqueMissionKeywords = [...new Set(missionKeywords)]; - const valueKeywords = uniqueMissionKeywords.slice(0, 2); - return { - mission: visionPrompt || 'Not specified', - values: valueKeywords.length > 0 ? valueKeywords.join(', ') : 'General Impact', - region: visionPrompt.toLowerCase().includes('africa') ? 'Africa' : visionPrompt.toLowerCase().includes('uganda') ? 'Uganda' : 'Global/Not specified', - givingStyle: 'One-time (recurring can be an option)' - }; - }; - - console.log('Individual Amounts:', aiSuggestedAllocations); // This was an old log, might be individualDonationAmounts now - console.log('CharityResultsView - received combinedMissionStatement prop:', combinedMissionStatement); - return ( @@ -102,10 +88,21 @@ const CharityResultsView = ({ + {/* Explanation of Movement vs Charity */} + + + Understanding Your Results: Compass matches you with movements (specific initiatives or programs) from various charities (organizations). + When you select a charity below, your donation goes to the organization, supporting all their movements. + + + {/* Top 3 movement recommendations */} {Array.isArray(compassRecommendations) && compassRecommendations.length > 0 && ( - - Top Picks For You + + + Top Movement Picks For You + + {compassRecommendations.slice(0,3).map((rec, idx) => { const group = Object.values(groupedMatches || {}).find(g => g.charity_name === rec.charity_name); @@ -114,20 +111,50 @@ const CharityResultsView = ({ const summary = movement?.summary || ''; return ( - - {rec.charity_name} - {rec.movement_title} - - {summary ? `${summary.substring(0, 180)}${summary.length > 180 ? '…' : ''}` : 'No summary available.'} + + + + From: {rec.charity_name} + + {rec.movement_title} + + {summary ? `${summary.substring(0, 150)}${summary.length > 150 ? '…' : ''}` : 'No summary available.'} {rec.reason && ( - - {rec.reason} - + + + Why: {rec.reason} + + )} {charityId && ( - )} @@ -141,6 +168,9 @@ const CharityResultsView = ({ {/* Charity Results Feed - now a nested grid for 2 columns */} + + All Matched Charities + {/* Nested grid for cards */} {semanticSearchLoading && ( @@ -239,6 +269,23 @@ const CharityResultsView = ({ + {/* Warning for zero donation */} + {totalDonationAmount === 0 && ( + + + + + + Donation Amount is 0 + + + Please go back to set your donation amount, or connect your wallet to use your balance. + + + + + )} + {/* Donation Summary and Action */} Total Donation: - + {typeof totalDonationAmount === 'number' ? totalDonationAmount.toFixed(2) : '0.00'} {selectedCrypto} diff --git a/eunoia_web/src/components/donate/VisionPromptView.jsx b/eunoia_web/src/components/donate/VisionPromptView.jsx index b88da4c..927862d 100644 --- a/eunoia_web/src/components/donate/VisionPromptView.jsx +++ b/eunoia_web/src/components/donate/VisionPromptView.jsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useRef, useEffect } from 'react'; import { Box, Typography, @@ -10,10 +10,7 @@ import { InputAdornment, IconButton, Tooltip, - Slider, MenuItem, - Chip, - Stack, Switch, FormControlLabel } from '@mui/material'; @@ -21,9 +18,6 @@ import { styled } from '@mui/material/styles'; import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet'; import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline'; -import TwitterIcon from '@mui/icons-material/Twitter'; -import InstagramIcon from '@mui/icons-material/Instagram'; -import LinkedInIcon from '@mui/icons-material/LinkedIn'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; const StepContent = styled(Box)(({ theme }) => ({ @@ -95,9 +89,12 @@ const VisionPromptView = ({ // Select the appropriate options based on the active chain const cryptoOptions = activeChain === 'polkadot' ? polkadotCryptoOptions : aptosCryptoOptions; - const handleSocialChange = (platform, value) => { - setSocialHandles(prev => ({ ...prev, [platform]: value })); - }; + // Ensure donation amount never exceeds wallet balance + useEffect(() => { + if (walletBalance > 0 && totalDonationAmount > walletBalance) { + setTotalDonationAmount(walletBalance); + } + }, [walletBalance, totalDonationAmount, setTotalDonationAmount]); const isNextDisabled = !visionPrompt.trim() || @@ -126,16 +123,12 @@ const VisionPromptView = ({ value={visionPrompt} onChange={(e) => setVisionPrompt(e.target.value)} sx={{ - mb: 1, '& .MuiOutlinedInput-root': { borderRadius: '12px', backgroundColor: alpha(theme.palette.common.white, 0.5) } }} /> - @@ -183,15 +176,36 @@ const VisionPromptView = ({ onChange={(e) => { const value = Number(e.target.value); if (!isNaN(value) && value >= 0) { - setTotalDonationAmount(value); + // Ensure amount never exceeds wallet balance + const maxAmount = walletBalance > 0 ? walletBalance : 0; + const clampedValue = Math.min(value, maxAmount); + setTotalDonationAmount(clampedValue); } }} + inputProps={{ + min: 0, + max: walletBalance > 0 ? walletBalance : 0, + }} InputProps={{ endAdornment: - setTotalDonationAmount(Math.max(1, totalDonationAmount - 1))}> + { + const newAmount = Math.max(0, totalDonationAmount - 1); + setTotalDonationAmount(newAmount); + }} + > - setTotalDonationAmount(totalDonationAmount + 1)}> + { + const maxAmount = walletBalance > 0 ? walletBalance : 0; + const newAmount = Math.min(totalDonationAmount + 1, maxAmount); + setTotalDonationAmount(newAmount); + }} + disabled={walletBalance > 0 && totalDonationAmount >= walletBalance} + > @@ -235,65 +249,6 @@ const VisionPromptView = ({ ))} - - 0 ? walletBalance : 1} // Using wallet balance as max value - step={1} - onChange={(e, newValue) => { - const value = Number(newValue); - if (!isNaN(value) && value >= 0) { - setTotalDonationAmount(value); - } - }} - aria-labelledby="donation-amount-slider" - sx={{color: 'primary.main'}} - valueLabelDisplay="auto" - /> - - 1 {cryptoOptions[0].value} - {(walletBalance > 0 ? walletBalance : 1).toFixed(2)} {cryptoOptions[0].value} - - - - - - - Want smarter matches? Share your socials. - - - We'll never post or share anything. This helps our AI understand your interests better. - - - handleSocialChange('twitter', e.target.value)} - InputProps={{ startAdornment: }} - sx={{backgroundColor: alpha(theme.palette.common.white, 0.5), borderRadius: '8px'}} - /> - handleSocialChange('instagram', e.target.value)} - InputProps={{ startAdornment: }} - sx={{backgroundColor: alpha(theme.palette.common.white, 0.5), borderRadius: '8px'}} - /> - handleSocialChange('linkedin', e.target.value)} - InputProps={{ startAdornment: }} - sx={{backgroundColor: alpha(theme.palette.common.white, 0.5), borderRadius: '8px'}} - /> - diff --git a/eunoia_web/src/config.js b/eunoia_web/src/config.js index be1f976..a4b9f4b 100644 --- a/eunoia_web/src/config.js +++ b/eunoia_web/src/config.js @@ -1,4 +1,6 @@ -// export const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'http://127.0.0.1:8080/api'; -export const API_BASE_URL = 'https://eunoia-api-eya2hhfdfzcchyc2.canadacentral-01.azurewebsites.net/api'; +// export const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'http://127.0.0.1:8000/api'; +// export const API_BASE_URL = 'https://eunoia-api-eya2hhfdfzcchyc2.canadacentral-01.azurewebsites.net/api'; +export const API_BASE_URL = 'https://eunoia-backend-bebv.onrender.com/api'; + // export const POLKADOT_NODE_URL = process.env.REACT_APP_POLKADOT_NODE_URL || 'wss://testnet-passet-hub.polkadot.io'; -export const POLKADOT_NODE_URL = process.env.REACT_APP_POLKADOT_NODE_URL || 'wss://polkadot-rpc.publicnode.com'; +export const POLKADOT_NODE_URL = process.env.REACT_APP_POLKADOT_NODE_URL || 'wss://polkadot-rpc.publicnode.com'; \ No newline at end of file