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)
}
}}
/>
-
- Let Compass help (Suggest ideas)
-
@@ -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