Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>Personalized SDG Explorer</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- React & ReactDOM --> | |
| <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script> | |
| <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> | |
| <!-- @babel/standalone --> | |
| <script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script> | |
| <!-- Chart.js for visualizations --> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <!-- Font Awesome for icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Country List Data --> | |
| <script> | |
| const countries = [ | |
| { code: "AF", name: "Afghanistan" }, { code: "AL", name: "Albania" }, { code: "DZ", name: "Algeria" }, | |
| { code: "AD", name: "Andorra" }, { code: "AO", name: "Angola" }, { code: "AG", name: "Antigua and Barbuda" }, | |
| { code: "AR", name: "Argentina" }, { code: "AM", name: "Armenia" }, { code: "AU", name: "Australia" }, | |
| { code: "AT", name: "Austria" }, { code: "AZ", name: "Azerbaijan" }, { code: "BS", name: "Bahamas" }, | |
| { code: "BH", name: "Bahrain" }, { code: "BD", name: "Bangladesh" }, { code: "BB", name: "Barbados" }, | |
| { code: "BY", name: "Belarus" }, { code: "BE", name: "Belgium" }, { code: "BZ", name: "Belize" }, | |
| { code: "BJ", name: "Benin" }, { code: "BT", name: "Bhutan" }, { code: "BO", name: "Bolivia" }, | |
| { code: "BA", name: "Bosnia and Herzegovina" }, { code: "BW", name: "Botswana" }, { code: "BR", name: "Brazil" }, | |
| { code: "BN", name: "Brunei" }, { code: "BG", name: "Bulgaria" }, { code: "BF", name: "Burkina Faso" }, | |
| { code: "BI", name: "Burundi" }, { code: "CV", name: "Cabo Verde" }, { code: "KH", name: "Cambodia" }, | |
| { code: "CM", name: "Cameroon" }, { code: "CA", name: "Canada" }, { code: "CF", name: "Central African Republic" }, | |
| { code: "TD", name: "Chad" }, { code: "CL", name: "Chile" }, { code: "CN", name: "China" }, | |
| { code: "CO", name: "Colombia" }, { code: "KM", name: "Comoros" }, { code: "CG", name: "Congo, Republic of the" }, | |
| { code: "CD", name: "Congo, Democratic Republic of the" }, { code: "CR", name: "Costa Rica" }, { code: "CI", name: "Cote d'Ivoire" }, | |
| { code: "HR", name: "Croatia" }, { code: "CU", name: "Cuba" }, { code: "CY", name: "Cyprus" }, | |
| { code: "CZ", name: "Czech Republic" }, { code: "DK", name: "Denmark" }, { code: "DJ", name: "Djibouti" }, | |
| { code: "DM", name: "Dominica" }, { code: "DO", name: "Dominican Republic" }, { code: "EC", name: "Ecuador" }, | |
| { code: "EG", name: "Egypt" }, { code: "SV", name: "El Salvador" }, { code: "GQ", name: "Equatorial Guinea" }, | |
| { code: "ER", name: "Eritrea" }, { code: "EE", name: "Estonia" }, { code: "SZ", name: "Eswatini" }, | |
| { code: "ET", name: "Ethiopia" }, { code: "FJ", name: "Fiji" }, { code: "FI", name: "Finland" }, | |
| { code: "FR", name: "France" }, { code: "GA", name: "Gabon" }, { code: "GM", name: "Gambia" }, | |
| { code: "GE", name: "Georgia" }, { code: "DE", name: "Germany" }, { code: "GH", name: "Ghana" }, | |
| { code: "GR", name: "Greece" }, { code: "GD", name: "Grenada" }, { code: "GT", name: "Guatemala" }, | |
| { code: "GN", name: "Guinea" }, { code: "GW", name: "Guinea-Bissau" }, { code: "GY", name: "Guyana" }, | |
| { code: "HT", name: "Haiti" }, { code: "HN", name: "Honduras" }, { code: "HU", name: "Hungary" }, | |
| { code: "IS", name: "Iceland" }, { code: "IN", name: "India" }, { code: "ID", name: "Indonesia" }, | |
| { code: "IR", name: "Iran" }, { code: "IQ", name: "Iraq" }, { code: "IE", name: "Ireland" }, | |
| { code: "IL", name: "Israel" }, { code: "IT", name: "Italy" }, { code: "JM", name: "Jamaica" }, | |
| { code: "JP", name: "Japan" }, { code: "JO", name: "Jordan" }, { code: "KZ", name: "Kazakhstan" }, | |
| { code: "KE", name: "Kenya" }, { code: "KI", name: "Kiribati" }, { code: "KP", name: "Korea, North" }, | |
| { code: "KR", name: "Korea, South" }, { code: "KW", name: "Kuwait" }, { code: "KG", name: "Kyrgyzstan" }, | |
| { code: "LA", name: "Laos" }, { code: "LV", name: "Latvia" }, { code: "LB", name: "Lebanon" }, | |
| { code: "LS", name: "Lesotho" }, { code: "LR", name: "Liberia" }, { code: "LY", name: "Libya" }, | |
| { code: "LI", name: "Liechtenstein" }, { code: "LT", name: "Lithuania" }, { code: "LU", name: "Luxembourg" }, | |
| { code: "MG", name: "Madagascar" }, { code: "MW", name: "Malawi" }, { code: "MY", name: "Malaysia" }, | |
| { code: "MV", name: "Maldives" }, { code: "ML", name: "Mali" }, { code: "MT", name: "Malta" }, | |
| { code: "MH", name: "Marshall Islands" }, { code: "MR", name: "Mauritania" }, { code: "MU", name: "Mauritius" }, | |
| { code: "MX", name: "Mexico" }, { code: "FM", name: "Micronesia" }, { code: "MD", name: "Moldova" }, | |
| { code: "MC", name: "Monaco" }, { code: "MN", name: "Mongolia" }, { code: "ME", name: "Montenegro" }, | |
| { code: "MA", name: "Morocco" }, { code: "MZ", name: "Mozambique" }, { code: "MM", name: "Myanmar (Burma)" }, | |
| { code: "NA", name: "Namibia" }, { code: "NR", name: "Nauru" }, { code: "NP", name: "Nepal" }, | |
| { code: "NL", name: "Netherlands" }, { code: "NZ", name: "New Zealand" }, { code: "NI", name: "Nicaragua" }, | |
| { code: "NE", name: "Niger" }, { code: "NG", name: "Nigeria" }, { code: "MK", name: "North Macedonia" }, | |
| { code: "NO", name: "Norway" }, { code: "OM", name: "Oman" }, { code: "PK", name: "Pakistan" }, | |
| { code: "PW", name: "Palau" }, { code: "PS", name: "Palestine" }, { code: "PA", name: "Panama" }, | |
| { code: "PG", name: "Papua New Guinea" }, { code: "PY", name: "Paraguay" }, { code: "PE", name: "Peru" }, | |
| { code: "PH", name: "Philippines" }, { code: "PL", name: "Poland" }, { code: "PT", name: "Portugal" }, | |
| { code: "QA", name: "Qatar" }, { code: "RO", name: "Romania" }, { code: "RU", name: "Russia" }, | |
| { code: "RW", name: "Rwanda" }, { code: "KN", name: "Saint Kitts and Nevis" }, { code: "LC", name: "Saint Lucia" }, | |
| { code: "VC", name: "Saint Vincent and the Grenadines" }, { code: "WS", name: "Samoa" }, { code: "SM", name: "San Marino" }, | |
| { code: "ST", name: "Sao Tome and Principe" }, { code: "SA", name: "Saudi Arabia" }, { code: "SN", name: "Senegal" }, | |
| { code: "RS", name: "Serbia" }, { code: "SC", name: "Seychelles" }, { code: "SL", name: "Sierra Leone" }, | |
| { code: "SG", name: "Singapore" }, { code: "SK", name: "Slovakia" }, { code: "SI", name: "Slovenia" }, | |
| { code: "SB", name: "Solomon Islands" }, { code: "SO", name: "Somalia" }, { code: "ZA", name: "South Africa" }, | |
| { code: "SS", name: "South Sudan" }, { code: "ES", name: "Spain" }, { code: "LK", name: "Sri Lanka" }, | |
| { code: "SD", name: "Sudan" }, { code: "SR", name: "Suriname" }, { code: "SE", name: "Sweden" }, | |
| { code: "CH", name: "Switzerland" }, { code: "SY", name: "Syria" }, { code: "TW", name: "Taiwan" }, | |
| { code: "TJ", name: "Tajikistan" }, { code: "TZ", name: "Tanzania" }, { code: "TH", name: "Thailand" }, | |
| { code: "TL", name: "Timor-Leste" }, { code: "TG", name: "Togo" }, { code: "TO", name: "Tonga" }, | |
| { code: "TT", name: "Trinidad and Tobago" }, { code: "TN", name: "Tunisia" }, { code: "TR", name: "Turkey" }, | |
| { code: "TM", name: "Turkmenistan" }, { code: "TV", name: "Tuvalu" }, { code: "UG", name: "Uganda" }, | |
| { code: "UA", name: "Ukraine" }, { code: "AE", name: "United Arab Emirates" }, { code: "GB", name: "United Kingdom" }, | |
| { code: "US", name: "United States" }, { code: "UY", name: "Uruguay" }, { code: "UZ", name: "Uzbekistan" }, | |
| { code: "VU", name: "Vanuatu" }, { code: "VA", name: "Vatican City" }, { code: "VE", name: "Venezuela" }, | |
| { code: "VN", name: "Vietnam" }, { code: "YE", name: "Yemen" }, { code: "ZM", name: "Zambia" }, | |
| { code: "ZW", name: "Zimbabwe" } | |
| ]; | |
| </script> | |
| <style> | |
| html, body { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100%; | |
| margin: 0; | |
| font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif; | |
| } | |
| .glass { | |
| background: rgba(255, 255, 255, 0.9); | |
| backdrop-filter: blur(15px); | |
| border-radius: 1.5rem; | |
| box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37); | |
| border: 1px solid rgba(255, 255, 255, 0.18); | |
| } | |
| .sdg-card { | |
| background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); | |
| border-radius: 1rem; | |
| padding: 1.5rem; | |
| color: white; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| border: 2px solid transparent; | |
| text-align: center; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .sdg-card:hover { | |
| transform: scale(1.05); | |
| border-color: rgba(255,255,255,0.5); | |
| box-shadow: 0 8px 25px rgba(0,0,0,0.2); | |
| } | |
| .sdg-card.selected { | |
| border-color: #fbbf24; | |
| box-shadow: 0 0 20px rgba(251, 191, 36, 0.6); | |
| transform: scale(1.02); | |
| } | |
| .sdg-card::before { | |
| content: | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: linear-gradient(45deg, transparent 30%, rgba(255,255,255,0.1) 50%, transparent 70%); | |
| transform: translateX(-100%); | |
| transition: transform 0.6s; | |
| } | |
| .sdg-card:hover::before { | |
| transform: translateX(100%); | |
| } | |
| /* SDG-specific colors */ | |
| .sdg-1 { background: linear-gradient(135deg, #e5243b 0%, #c5192d 100%); } | |
| .sdg-2 { background: linear-gradient(135deg, #dda63a 0%, #c09525 100%); } | |
| .sdg-3 { background: linear-gradient(135deg, #4c9f38 0%, #3d7f2a 100%); } | |
| .sdg-4 { background: linear-gradient(135deg, #c5192d 0%, #a01525 100%); } | |
| .sdg-5 { background: linear-gradient(135deg, #ff3a21 0%, #e6331d 100%); } | |
| .sdg-6 { background: linear-gradient(135deg, #26bde2 0%, #1da1c4 100%); } | |
| .sdg-7 { background: linear-gradient(135deg, #fcc30b 0%, #e3af0a 100%); } | |
| .sdg-8 { background: linear-gradient(135deg, #a21942 0%, #8a1538 100%); } | |
| .sdg-9 { background: linear-gradient(135deg, #fd6925 0%, #e45a21 100%); } | |
| .sdg-10 { background: linear-gradient(135deg, #dd1367 0%, #c41159 100%); } | |
| .sdg-11 { background: linear-gradient(135deg, #fd9d24 0%, #e48920 100%); } | |
| .sdg-12 { background: linear-gradient(135deg, #bf8b2e 0%, #a67a28 100%); } | |
| .sdg-13 { background: linear-gradient(135deg, #3f7e44 0%, #356b3a 100%); } | |
| .sdg-14 { background: linear-gradient(135deg, #0a97d9 0%, #0985c2 100%); } | |
| .sdg-15 { background: linear-gradient(135deg, #56c02b 0%, #4aa625 100%); } | |
| .sdg-16 { background: linear-gradient(135deg, #00689d 0%, #005a87 100%); } | |
| .sdg-17 { background: linear-gradient(135deg, #19486a 0%, #153d5a 100%); } | |
| .phase-indicator { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 0.5rem 1rem; | |
| border-radius: 1rem; | |
| font-weight: 600; | |
| display: inline-block; | |
| margin-bottom: 1rem; | |
| } | |
| .ai-insight-container { | |
| background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); | |
| border-radius: 1rem; | |
| padding: 1.5rem; | |
| margin: 1rem 0; | |
| border-left: 4px solid #10b981; | |
| } | |
| .ai-feedback-container { | |
| background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%); | |
| border-radius: 1rem; | |
| padding: 1.5rem; | |
| margin: 1rem 0; | |
| border-left: 4px solid #f59e0b; | |
| } | |
| .action-card { | |
| background: rgba(255, 255, 255, 0.95); | |
| border-radius: 1rem; | |
| padding: 1rem; | |
| margin: 0.5rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| border: 2px solid transparent; | |
| } | |
| .action-card:hover { | |
| border-color: #3b82f6; | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3); | |
| } | |
| .action-card.selected { | |
| border-color: #10b981; | |
| background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%); | |
| } | |
| .simulation-result { | |
| background: rgba(255, 255, 255, 0.95); | |
| border-radius: 1rem; | |
| padding: 1rem; | |
| margin: 0.5rem 0; | |
| border-left: 4px solid #3b82f6; | |
| } | |
| .progress-bar { | |
| background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); | |
| height: 8px; | |
| border-radius: 4px; | |
| transition: width 0.5s ease; | |
| } | |
| .badge { | |
| display: inline-block; | |
| padding: 0.5rem 1rem; | |
| border-radius: 1rem; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| margin: 0.25rem; | |
| } | |
| .badge.explorer { | |
| background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); | |
| color: #333; | |
| } | |
| .badge.planner { | |
| background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%); | |
| color: white; | |
| } | |
| .badge.simulator { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| } | |
| .badge.reflector { | |
| background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); | |
| color: white; | |
| } | |
| .typing-indicator { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| .typing-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: #667eea; | |
| animation: typing 1.4s infinite ease-in-out; | |
| } | |
| .typing-dot:nth-child(1) { animation-delay: -0.32s; } | |
| .typing-dot:nth-child(2) { animation-delay: -0.16s; } | |
| @keyframes typing { | |
| 0%, 80%, 100% { transform: scale(0); opacity: 0.5; } | |
| 40% { transform: scale(1); opacity: 1; } | |
| } | |
| .fade-in { | |
| animation: fadeIn 0.6s ease-in; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(20px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .slide-in { | |
| animation: slideIn 0.5s ease-out; | |
| } | |
| @keyframes slideIn { | |
| from { opacity: 0; transform: translateX(-30px); } | |
| to { opacity: 1; transform: translateX(0); } | |
| } | |
| .pulse { | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.05); } | |
| } | |
| .celebration { | |
| animation: celebration 0.6s ease-out; | |
| } | |
| @keyframes celebration { | |
| 0% { transform: scale(1); } | |
| 50% { transform: scale(1.2) rotate(5deg); } | |
| 100% { transform: scale(1); } | |
| } | |
| .impact-meter { | |
| background: linear-gradient(90deg, #ef4444 0%, #f59e0b 50%, #10b981 100%); | |
| height: 20px; | |
| border-radius: 10px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .impact-indicator { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| height: 100%; | |
| background: rgba(255, 255, 255, 0.8); | |
| border-radius: 10px; | |
| transition: width 0.8s ease; | |
| } | |
| /* Responsive design */ | |
| @media (max-width: 768px) { | |
| .sdg-grid { | |
| grid-template-columns: repeat(2, 1fr); | |
| } | |
| } | |
| @media (max-width: 640px) { | |
| .sdg-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| /* Accessibility improvements */ | |
| .sr-only { | |
| position: absolute; | |
| width: 1px; | |
| height: 1px; | |
| padding: 0; | |
| margin: -1px; | |
| overflow: hidden; | |
| clip: rect(0, 0, 0, 0); | |
| white-space: nowrap; | |
| border: 0; | |
| } | |
| button:focus, input:focus, textarea:focus, select:focus { | |
| outline: 3px solid #fbbf24; | |
| outline-offset: 2px; | |
| } | |
| .sdg-card:focus { | |
| outline: 3px solid #fbbf24; | |
| outline-offset: 2px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="root"></div> | |
| <script type="text/babel"> | |
| const { useState, useEffect, useCallback, useRef } = React; | |
| // SDG Data (Same as before) | |
| const SDG_DATA = { | |
| 1: { | |
| title: "No Poverty", | |
| description: "End poverty in all its forms everywhere", | |
| icon: "π ", | |
| color: "#e5243b", | |
| targets: ["Eradicate extreme poverty", "Reduce poverty by half", "Implement social protection systems"] | |
| }, | |
| 2: { | |
| title: "Zero Hunger", | |
| description: "End hunger, achieve food security and improved nutrition", | |
| icon: "πΎ", | |
| color: "#dda63a", | |
| targets: ["End hunger", "End malnutrition", "Double agricultural productivity"] | |
| }, | |
| 3: { | |
| title: "Good Health and Well-being", | |
| description: "Ensure healthy lives and promote well-being for all", | |
| icon: "β€οΈ", | |
| color: "#4c9f38", | |
| targets: ["Reduce maternal mortality", "End preventable deaths", "Combat diseases"] | |
| }, | |
| 4: { | |
| title: "Quality Education", | |
| description: "Ensure inclusive and equitable quality education", | |
| icon: "π", | |
| color: "#c5192d", | |
| targets: ["Free primary and secondary education", "Equal access to education", "Increase literacy rates"] | |
| }, | |
| 5: { | |
| title: "Gender Equality", | |
| description: "Achieve gender equality and empower all women and girls", | |
| icon: "βοΈ", | |
| color: "#ff3a21", | |
| targets: ["End discrimination", "Eliminate violence", "Ensure equal participation"] | |
| }, | |
| 6: { | |
| title: "Clean Water and Sanitation", | |
| description: "Ensure availability and sustainable management of water", | |
| icon: "π§", | |
| color: "#26bde2", | |
| targets: ["Universal access to water", "Adequate sanitation", "Improve water quality"] | |
| }, | |
| 7: { | |
| title: "Affordable and Clean Energy", | |
| description: "Ensure access to affordable, reliable, sustainable energy", | |
| icon: "β‘", | |
| color: "#fcc30b", | |
| targets: ["Universal access to energy", "Increase renewable energy", "Improve energy efficiency"] | |
| }, | |
| 8: { | |
| title: "Decent Work and Economic Growth", | |
| description: "Promote sustained, inclusive economic growth", | |
| icon: "πΌ", | |
| color: "#a21942", | |
| targets: ["Sustain economic growth", "Achieve full employment", "Protect labor rights"] | |
| }, | |
| 9: { | |
| title: "Industry, Innovation and Infrastructure", | |
| description: "Build resilient infrastructure, promote innovation", | |
| icon: "π", | |
| color: "#fd6925", | |
| targets: ["Develop infrastructure", "Promote innovation", "Increase access to technology"] | |
| }, | |
| 10: { | |
| title: "Reduced Inequalities", | |
| description: "Reduce inequality within and among countries", | |
| icon: "βοΈ", | |
| color: "#dd1367", | |
| targets: ["Reduce income inequality", "Ensure equal opportunity", "Promote inclusion"] | |
| }, | |
| 11: { | |
| title: "Sustainable Cities and Communities", | |
| description: "Make cities and human settlements inclusive and sustainable", | |
| icon: "ποΈ", | |
| color: "#fd9d24", | |
| targets: ["Ensure access to housing", "Provide sustainable transport", "Enhance urbanization"] | |
| }, | |
| 12: { | |
| title: "Responsible Consumption and Production", | |
| description: "Ensure sustainable consumption and production patterns", | |
| icon: "β»οΈ", | |
| color: "#bf8b2e", | |
| targets: ["Achieve sustainable management", "Reduce waste generation", "Promote sustainable practices"] | |
| }, | |
| 13: { | |
| title: "Climate Action", | |
| description: "Take urgent action to combat climate change", | |
| icon: "π", | |
| color: "#3f7e44", | |
| targets: ["Strengthen resilience", "Integrate climate measures", "Improve education on climate"] | |
| }, | |
| 14: { | |
| title: "Life Below Water", | |
| description: "Conserve and sustainably use the oceans and marine resources", | |
| icon: "π", | |
| color: "#0a97d9", | |
| targets: ["Reduce marine pollution", "Protect marine ecosystems", "Regulate fishing"] | |
| }, | |
| 15: { | |
| title: "Life on Land", | |
| description: "Protect, restore and promote sustainable use of ecosystems", | |
| icon: "π³", | |
| color: "#56c02b", | |
| targets: ["Conserve forest ecosystems", "Combat desertification", "Halt biodiversity loss"] | |
| }, | |
| 16: { | |
| title: "Peace, Justice and Strong Institutions", | |
| description: "Promote peaceful and inclusive societies", | |
| icon: "βοΈ", | |
| color: "#00689d", | |
| targets: ["Reduce violence", "Promote rule of law", "Build effective institutions"] | |
| }, | |
| 17: { | |
| title: "Partnerships for the Goals", | |
| description: "Strengthen means of implementation and global partnership", | |
| icon: "π€", | |
| color: "#19486a", | |
| targets: ["Strengthen partnerships", "Enhance global cooperation", "Promote knowledge sharing"] | |
| } | |
| }; | |
| // Action types (Same as before) | |
| const ACTION_TYPES = { | |
| awareness: { | |
| name: "Awareness Campaign", | |
| description: "Educate community about the issue", | |
| icon: "π’", | |
| difficulty: "easy", | |
| impact_range: [10, 30] | |
| }, | |
| community: { | |
| name: "Community Project", | |
| description: "Organize local community initiatives", | |
| icon: "π₯", | |
| difficulty: "medium", | |
| impact_range: [20, 50] | |
| }, | |
| policy: { | |
| name: "Policy Advocacy", | |
| description: "Advocate for policy changes", | |
| icon: "π", | |
| difficulty: "hard", | |
| impact_range: [30, 70] | |
| }, | |
| technology: { | |
| name: "Technology Solution", | |
| description: "Implement technological solutions", | |
| icon: "π»", | |
| difficulty: "medium", | |
| impact_range: [25, 60] | |
| }, | |
| education: { | |
| name: "Educational Program", | |
| description: "Create educational initiatives", | |
| icon: "π", | |
| difficulty: "medium", | |
| impact_range: [15, 45] | |
| }, | |
| partnership: { | |
| name: "Partnership Building", | |
| description: "Build partnerships with organizations", | |
| icon: "π€", | |
| difficulty: "hard", | |
| impact_range: [35, 80] | |
| } | |
| }; | |
| // Badge system (Same as before) | |
| const BADGES = { | |
| "sdg-explorer": { name: "SDG Explorer", icon: "π", color: "explorer" }, | |
| "context-analyzer": { name: "Context Analyzer", icon: "π", color: "planner" }, | |
| "action-planner": { name: "Action Planner", icon: "π", color: "planner" }, | |
| "impact-simulator": { name: "Impact Simulator", icon: "π", color: "simulator" }, | |
| "thoughtful-reflector": { name: "Thoughtful Reflector", icon: "π", color: "reflector" }, | |
| "global-citizen": { name: "Global Citizen", icon: "π", color: "explorer" }, | |
| "change-maker": { name: "Change Maker", icon: "β‘", color: "simulator" }, | |
| "sustainability-champion": { name: "Sustainability Champion", icon: "π", color: "explorer" } | |
| }; | |
| // Utility function to safely render content (Same as before) | |
| const safeRender = (content) => { | |
| if (typeof content === 'string') { | |
| return content; | |
| } | |
| if (typeof content === 'object' && content !== null) { | |
| return JSON.stringify(content, null, 2); | |
| } | |
| return String(content); | |
| }; | |
| // Component to safely render AI content (Same as before) | |
| const SafeAIContent = ({ content, title }) => { | |
| if (typeof content === 'string') { | |
| return <div className="text-green-700 whitespace-pre-line">{content}</div>; | |
| } | |
| if (Array.isArray(content)) { | |
| return ( | |
| <div className="space-y-2"> | |
| {content.map((item, index) => ( | |
| <div key={index} className="bg-white bg-opacity-50 rounded-lg p-3"> | |
| <SafeAIContent content={item} /> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| if (typeof content === 'object' && content !== null) { | |
| return ( | |
| <div className="bg-white bg-opacity-50 rounded-lg p-3"> | |
| {Object.entries(content).map(([key, value]) => ( | |
| <div key={key} className="mb-2"> | |
| <strong className="text-blue-800 capitalize">{key.replace(/_/g, ' ')}:</strong> | |
| <p className="text-blue-700 mt-1">{safeRender(value)}</p> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| return <div className="text-green-700">{safeRender(content)}</div>; | |
| }; | |
| // Error Boundary Component (Same as before) | |
| class ErrorBoundary extends React.Component { | |
| constructor(props) { | |
| super(props); | |
| this.state = { hasError: false, error: null, errorInfo: null }; | |
| } | |
| static getDerivedStateFromError(error) { | |
| return { hasError: true }; | |
| } | |
| componentDidCatch(error, errorInfo) { | |
| this.setState({ error, errorInfo }); | |
| console.error("Uncaught error:", error, errorInfo); | |
| } | |
| render() { | |
| if (this.state.hasError) { | |
| return ( | |
| <div className="glass p-6 m-4 text-center"> | |
| <h2 className="text-2xl font-bold text-red-600 mb-4">π¨ Application Error</h2> | |
| <p className="text-gray-700 mb-4"> | |
| Something went wrong. Please try refreshing the page or starting a new exploration. | |
| </p> | |
| <details className="text-left text-sm text-gray-600 bg-gray-100 p-3 rounded"> | |
| <summary>Error Details</summary> | |
| <pre className="mt-2 whitespace-pre-wrap break-words"> | |
| {this.state.error && this.state.error.toString()} | |
| <br /> | |
| {this.state.errorInfo && this.state.errorInfo.componentStack} | |
| </pre> | |
| </details> | |
| </div> | |
| ); | |
| } | |
| return this.props.children; | |
| } | |
| } | |
| function PersonalizedSDGExplorer() { | |
| // Core state | |
| const [phase, setPhase] = useState(1); | |
| const [selectedSDG, setSelectedSDG] = useState(null); | |
| const [apiKey, setApiKey] = useState(""); | |
| const [showApiKeyModal, setShowApiKeyModal] = useState(false); | |
| // NEW: Personalization state | |
| const [userCountry, setUserCountry] = useState(""); | |
| const [localSituation, setLocalSituation] = useState(""); | |
| const [personalExperience, setPersonalExperience] = useState(""); | |
| const [userActionIdea, setUserActionIdea] = useState(""); | |
| // Phase 2: Contextualization state | |
| const [aiContext, setAiContext] = useState(""); | |
| const [isGeneratingContext, setIsGeneratingContext] = useState(false); | |
| const [localContext, setLocalContext] = useState(""); | |
| const [globalContext, setGlobalContext] = useState(""); | |
| // Phase 3: Action Planning state | |
| const [selectedActions, setSelectedActions] = useState([]); | |
| const [actionDetails, setActionDetails] = useState({}); | |
| const [successIndicators, setSuccessIndicators] = useState(""); | |
| // Phase 4: Simulation state | |
| const [simulationResults, setSimulationResults] = useState(null); | |
| const [isSimulating, setIsSimulating] = useState(false); | |
| const [impactScore, setImpactScore] = useState(0); | |
| const [challenges, setChallenges] = useState([]); | |
| const [mitigationStrategies, setMitigationStrategies] = useState([]); | |
| // Phase 5: Reflection state | |
| const [reflections, setReflections] = useState({ | |
| surprising_results: "", | |
| influencing_factors: "", | |
| realism_assessment: "", | |
| personal_connection: "" // NEW: Reflection on personal connection | |
| }); | |
| const [aiFeedback, setAiFeedback] = useState(""); | |
| const [isGeneratingFeedback, setIsGeneratingFeedback] = useState(false); | |
| // Progress and gamification state | |
| const [badges, setBadges] = useState([]); | |
| const [explorationHistory, setExplorationHistory] = useState([]); | |
| const [aiError, setAiError] = useState(""); | |
| // Load data from localStorage (Same as before) | |
| useEffect(() => { | |
| try { | |
| const savedApiKey = localStorage.getItem("sdg-explorer-api-key"); | |
| const savedHistory = localStorage.getItem("sdg-explorer-history"); | |
| const savedBadges = localStorage.getItem("sdg-explorer-badges"); | |
| if (savedApiKey) { | |
| setApiKey(savedApiKey); | |
| } | |
| if (savedHistory) { | |
| setExplorationHistory(JSON.parse(savedHistory)); | |
| } | |
| if (savedBadges) { | |
| setBadges(JSON.parse(savedBadges)); | |
| } | |
| } catch (error) { | |
| console.error("Error loading saved data:", error); | |
| } | |
| }, []); | |
| // Save data to localStorage (Same as before) | |
| useEffect(() => { | |
| try { | |
| if (apiKey) { | |
| localStorage.setItem("sdg-explorer-api-key", apiKey); | |
| } | |
| } catch (error) { | |
| console.error("Error saving API key:", error); | |
| } | |
| }, [apiKey]); | |
| useEffect(() => { | |
| try { | |
| localStorage.setItem("sdg-explorer-history", JSON.stringify(explorationHistory)); | |
| } catch (error) { | |
| console.error("Error saving history:", error); | |
| } | |
| }, [explorationHistory]); | |
| useEffect(() => { | |
| try { | |
| localStorage.setItem("sdg-explorer-badges", JSON.stringify(badges)); | |
| } catch (error) { | |
| console.error("Error saving badges:", error); | |
| } | |
| }, [badges]); | |
| const selectSDG = (sdgNumber) => { | |
| setSelectedSDG(sdgNumber); | |
| setBadges(prev => [...new Set([...prev, "sdg-explorer"])]); | |
| }; | |
| const completeSDGSelection = () => { | |
| if (!selectedSDG) { | |
| alert("Please select an SDG to explore"); | |
| return; | |
| } | |
| setPhase(2); | |
| }; | |
| const generateAIContext = async () => { | |
| if (!apiKey) { | |
| setShowApiKeyModal(true); | |
| return; | |
| } | |
| if (!userCountry) { | |
| alert("Please select your country"); | |
| return; | |
| } | |
| setIsGeneratingContext(true); | |
| setAiError(""); | |
| try { | |
| const prompt = createContextPrompt(); | |
| const response = await callOpenAI(prompt); | |
| const context = parseAIContext(response); | |
| setAiContext(response); | |
| setLocalContext(context.local); | |
| setGlobalContext(context.global); | |
| setBadges(prev => [...new Set([...prev, "context-analyzer"])]); | |
| } catch (error) { | |
| console.error("Error generating AI context:", error); | |
| setAiError(`Failed to generate context: ${error.message}. Please check your API key and try again.`); | |
| } finally { | |
| setIsGeneratingContext(false); | |
| } | |
| }; | |
| // UPDATED: AI Prompt includes personalization | |
| const createContextPrompt = () => { | |
| const sdg = SDG_DATA[selectedSDG]; | |
| return `You are an educational assistant specialized in providing detailed, engaging, and informative local and global examples connected to the UN's Sustainable Development Goals, personalized for the user's context. | |
| **Selected SDG:** SDG ${selectedSDG} - ${sdg.title} | |
| **User's Country:** ${userCountry} | |
| **User's Local Situation Description:** ${localSituation || "Not provided"} | |
| Please provide comprehensive context in the following JSON format: | |
| { | |
| "global": "Detailed global context including worldwide challenges, statistics, successful initiatives, and international efforts related to this SDG. (as a single comprehensive string)", | |
| "local": "Local context examples specifically relevant to **${userCountry}** and the user's described situation (${localSituation || 'general local context'}). Include challenges and solutions that students in **${userCountry}** might encounter. Provide actionable examples. (as a single comprehensive string)", | |
| "connections": "How this SDG connects to other SDGs and broader sustainability themes, potentially highlighting relevance to **${userCountry}**. (as a single string)", | |
| "youth_opportunities": "Specific opportunities for young people in **${userCountry}** to contribute to this SDG, considering the local situation. (as a single string)" | |
| } | |
| CRITICAL: All fields must be comprehensive strings. Personalize the 'local' and 'youth_opportunities' sections based on the user's country and situation.`; | |
| }; | |
| // Parse AI Context (Same as before, but fields are now personalized) | |
| const parseAIContext = (response) => { | |
| try { | |
| const jsonMatch = response.match(/\{[\s\S]*\}/); | |
| if (!jsonMatch) { | |
| throw new Error("No JSON found in AI response"); | |
| } | |
| const data = JSON.parse(jsonMatch[0]); | |
| return { | |
| global: typeof data.global === 'string' ? data.global : "Global context will be provided.", | |
| local: typeof data.local === 'string' ? data.local : "Local context will be provided.", | |
| connections: typeof data.connections === 'string' ? data.connections : "SDG connections will be explained.", | |
| youth_opportunities: typeof data.youth_opportunities === 'string' ? data.youth_opportunities : "Youth opportunities will be outlined." | |
| }; | |
| } catch (error) { | |
| console.error("Error parsing AI context:", error); | |
| return { | |
| global: "Error processing global context. Please try again.", | |
| local: "Error processing local context. Please try again.", | |
| connections: "Error processing connections. Please try again.", | |
| youth_opportunities: "Error processing opportunities. Please try again." | |
| }; | |
| } | |
| }; | |
| // callOpenAI (Same as before) | |
| const callOpenAI = async (prompt) => { | |
| if (!apiKey) { | |
| throw new Error("API key not provided"); | |
| } | |
| try { | |
| const response = await fetch("https://api.openai.com/v1/chat/completions", { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/json", | |
| "Authorization": `Bearer ${apiKey.trim()}` | |
| }, | |
| body: JSON.stringify({ | |
| model: "gpt-4o-mini", | |
| messages: [ | |
| { | |
| role: "system", | |
| content: "You are an educational assistant specialized in providing detailed, engaging, and informative content about the UN's Sustainable Development Goals. Always respond with valid JSON matching the requested format." | |
| }, | |
| { role: "user", content: prompt } | |
| ], | |
| max_tokens: 1500, | |
| temperature: 0.7, | |
| response_format: { type: "json_object" } | |
| }) | |
| }); | |
| if (!response.ok) { | |
| let errorData = {}; | |
| try { | |
| errorData = await response.json(); | |
| } catch (e) { | |
| // Ignore if response body is not JSON | |
| } | |
| throw new Error(`OpenAI API error: ${response.status} ${response.statusText}${errorData.error ? ` - ${errorData.error.message}` : ""}`); | |
| } | |
| const data = await response.json(); | |
| if (!data.choices || !data.choices[0] || !data.choices[0].message || !data.choices[0].message.content) { | |
| throw new Error("Invalid response format from OpenAI API"); | |
| } | |
| return data.choices[0].message.content; | |
| } catch (error) { | |
| console.error("Error calling OpenAI API:", error); | |
| throw error; | |
| } | |
| }; | |
| const completeContextualization = () => { | |
| if (!aiContext && !localContext && !globalContext) { | |
| alert("Please generate AI context first"); | |
| return; | |
| } | |
| setPhase(3); | |
| }; | |
| const toggleAction = (actionType) => { | |
| setSelectedActions(prev => { | |
| if (prev.includes(actionType)) { | |
| return prev.filter(a => a !== actionType); | |
| } else { | |
| return [...prev, actionType]; | |
| } | |
| }); | |
| }; | |
| const updateActionDetails = (actionType, details) => { | |
| setActionDetails(prev => ({ | |
| ...prev, | |
| [actionType]: details | |
| })); | |
| }; | |
| const completeActionPlanning = () => { | |
| if (selectedActions.length === 0 && !userActionIdea.trim()) { | |
| alert("Please select at least one action or suggest your own idea"); | |
| return; | |
| } | |
| if (!successIndicators.trim()) { | |
| alert("Please define success indicators"); | |
| return; | |
| } | |
| setBadges(prev => [...new Set([...prev, "action-planner"])]); | |
| setPhase(4); | |
| }; | |
| const runSimulation = async () => { | |
| setIsSimulating(true); | |
| setAiError(""); | |
| try { | |
| // Simulate impact calculation (including user idea if provided) | |
| let totalImpact = 0; | |
| const simulatedChallenges = []; | |
| const simulatedMitigations = []; | |
| selectedActions.forEach(actionType => { | |
| const action = ACTION_TYPES[actionType]; | |
| const randomImpact = Math.floor(Math.random() * (action.impact_range[1] - action.impact_range[0] + 1)) + action.impact_range[0]; | |
| totalImpact += randomImpact; | |
| if (action.difficulty === "hard") { | |
| simulatedChallenges.push(`${action.name}: Requires significant resources and stakeholder buy-in`); | |
| simulatedMitigations.push(`Build partnerships early and secure funding commitments`); | |
| } else if (action.difficulty === "medium") { | |
| simulatedChallenges.push(`${action.name}: May face community resistance or coordination issues`); | |
| simulatedMitigations.push(`Engage community leaders and create clear communication plan`); | |
| } | |
| }); | |
| // Add impact for user idea (simple estimation) | |
| if (userActionIdea.trim()) { | |
| totalImpact += Math.floor(Math.random() * 30) + 10; // Add 10-40% impact for user idea | |
| simulatedChallenges.push(`User Idea (${userActionIdea.substring(0, 20)}...): Requires careful planning and validation`); | |
| simulatedMitigations.push(`Develop a detailed plan and seek expert advice for your idea`); | |
| } | |
| // Simulate AI-enhanced predictions if API key available | |
| if (apiKey) { | |
| try { | |
| const simulationPrompt = createSimulationPrompt(); | |
| const aiResponse = await callOpenAI(simulationPrompt); | |
| const aiSimulation = parseSimulationResponse(aiResponse); | |
| setSimulationResults({ | |
| impact: Math.min(totalImpact, 100), | |
| timeline: aiSimulation.timeline || "6-12 months", | |
| challenges: aiSimulation.challenges || simulatedChallenges, | |
| mitigations: aiSimulation.mitigations || simulatedMitigations, | |
| success_factors: aiSimulation.success_factors || ["Community engagement", "Resource availability", "Stakeholder support"] | |
| }); | |
| } catch (error) { | |
| console.error("AI simulation failed, using basic simulation:", error); | |
| setSimulationResults({ | |
| impact: Math.min(totalImpact, 100), | |
| timeline: "6-12 months", | |
| challenges: simulatedChallenges, | |
| mitigations: simulatedMitigations, | |
| success_factors: ["Community engagement", "Resource availability", "Stakeholder support"] | |
| }); | |
| } | |
| } else { | |
| setSimulationResults({ | |
| impact: Math.min(totalImpact, 100), | |
| timeline: "6-12 months", | |
| challenges: simulatedChallenges, | |
| mitigations: simulatedMitigations, | |
| success_factors: ["Community engagement", "Resource availability", "Stakeholder support"] | |
| }); | |
| } | |
| setImpactScore(Math.min(totalImpact, 100)); | |
| setBadges(prev => [...new Set([...prev, "impact-simulator"])]); | |
| } catch (error) { | |
| console.error("Error running simulation:", error); | |
| setAiError(`Simulation error: ${error.message}`); | |
| } finally { | |
| setIsSimulating(false); | |
| } | |
| }; | |
| // UPDATED: Simulation prompt includes personalization | |
| const createSimulationPrompt = () => { | |
| const sdg = SDG_DATA[selectedSDG]; | |
| const actionsText = selectedActions.map(a => ACTION_TYPES[a].name).join(", "); | |
| const fullActionList = userActionIdea.trim() ? `${actionsText}, User Idea: ${userActionIdea}` : actionsText; | |
| return `You are an expert in sustainable development and impact assessment. Provide realistic simulation results for SDG actions, personalized for the user's context. | |
| **SDG Context:** SDG ${selectedSDG} - ${sdg.title} | |
| **User's Country:** ${userCountry} | |
| **User's Local Situation:** ${localSituation || "General context"} | |
| **Selected Actions:** ${fullActionList} | |
| **Success Indicators:** ${successIndicators} | |
| Provide simulation results in JSON format, considering the user's country and local situation: | |
| { | |
| "timeline": "Realistic timeline for seeing results in **${userCountry}** (as a string)", | |
| "challenges": ["List of 3-5 realistic challenges specific to **${userCountry}** and the local situation"], | |
| "mitigations": ["Corresponding mitigation strategies relevant to **${userCountry}**"], | |
| "success_factors": ["Key factors for success in **${userCountry}**"], | |
| "impact_description": "Detailed description of expected impact in **${userCountry}**, considering local context (as a string)" | |
| } | |
| Focus on realistic, evidence-based predictions relevant to the user's specified location and situation.`; | |
| }; | |
| // Parse Simulation Response (Same as before) | |
| const parseSimulationResponse = (response) => { | |
| try { | |
| const jsonMatch = response.match(/\{[\s\S]*\}/); | |
| if (!jsonMatch) { | |
| throw new Error("No JSON found in simulation response"); | |
| } | |
| const data = JSON.parse(jsonMatch[0]); | |
| return { | |
| timeline: data.timeline || "6-12 months", | |
| challenges: Array.isArray(data.challenges) ? data.challenges : ["Resource constraints", "Stakeholder coordination"], | |
| mitigations: Array.isArray(data.mitigations) ? data.mitigations : ["Seek partnerships", "Create detailed plans"], | |
| success_factors: Array.isArray(data.success_factors) ? data.success_factors : ["Community support", "Adequate resources"], | |
| impact_description: data.impact_description || "Positive impact expected with proper implementation" | |
| }; | |
| } catch (error) { | |
| console.error("Error parsing simulation response:", error); | |
| return { | |
| timeline: "6-12 months", | |
| challenges: ["Resource constraints", "Stakeholder coordination"], | |
| mitigations: ["Seek partnerships", "Create detailed plans"], | |
| success_factors: ["Community support", "Adequate resources"], | |
| impact_description: "Positive impact expected with proper implementation" | |
| }; | |
| } | |
| }; | |
| const completeSimulation = () => { | |
| if (!simulationResults) { | |
| alert("Please run the simulation first"); | |
| return; | |
| } | |
| setPhase(5); | |
| }; | |
| const generateReflectionFeedback = async () => { | |
| if (!apiKey) { | |
| setShowApiKeyModal(true); | |
| return; | |
| } | |
| setIsGeneratingFeedback(true); | |
| setAiError(""); | |
| try { | |
| const prompt = createReflectionPrompt(); | |
| const response = await callOpenAI(prompt); | |
| let feedbackText = response; | |
| try { | |
| const jsonData = JSON.parse(response); | |
| feedbackText = jsonData.feedback || jsonData.text || response; | |
| } catch (e) { | |
| feedbackText = response; | |
| } | |
| setAiFeedback(feedbackText); | |
| setBadges(prev => [...new Set([...prev, "thoughtful-reflector", "global-citizen"])]); | |
| } catch (error) { | |
| console.error("Error generating reflection feedback:", error); | |
| setAiError(`Failed to generate feedback: ${error.message}`); | |
| } finally { | |
| setIsGeneratingFeedback(false); | |
| } | |
| }; | |
| // UPDATED: Reflection prompt includes personalization | |
| const createReflectionPrompt = () => { | |
| const sdg = SDG_DATA[selectedSDG]; | |
| const reflectionText = Object.entries(reflections) | |
| .map(([key, value]) => `${key.replace(/_/g, " ")}: ${value}`) | |
| .join("\n"); | |
| return `You are an educational assistant providing supportive feedback on student reflections about SDG action simulations, personalized for their context. | |
| **Student's SDG Exploration:** | |
| SDG: ${selectedSDG} - ${sdg.title} | |
| Country: ${userCountry} | |
| Local Situation: ${localSituation || "Not specified"} | |
| Actions Planned: ${selectedActions.map(a => ACTION_TYPES[a].name).join(", ")}${userActionIdea ? `, User Idea: ${userActionIdea}` : ''} | |
| Impact Score: ${impactScore}% | |
| Badges Earned: ${badges.map(id => BADGES[id]?.name).join(", ")} | |
| **Student Reflections:** | |
| ${reflectionText} | |
| Provide encouraging, educational feedback in JSON format, considering the student's country, local situation, and personal connection: | |
| { | |
| "feedback": "Your complete feedback as a single string. Acknowledge their SDG exploration, country context, and personal reflections. Highlight insights related to their specific situation. Offer suggestions for real-world application relevant to **${userCountry}**. Connect to global citizenship from their perspective. Encourage continued engagement. Use an encouraging, educational tone." | |
| } | |
| CRITICAL: Respond ONLY with valid JSON containing a single 'feedback' field.`; | |
| }; | |
| const completeExploration = () => { | |
| const exploration = { | |
| id: Date.now(), | |
| timestamp: new Date().toISOString(), | |
| sdg: selectedSDG, | |
| country: userCountry, | |
| localSituation: localSituation, | |
| actions: selectedActions, | |
| userActionIdea: userActionIdea, | |
| impactScore: impactScore, | |
| reflections: reflections, | |
| badgesEarned: [...new Set(badges)], | |
| simulationResults: simulationResults | |
| }; | |
| setExplorationHistory(prev => [...prev, exploration]); | |
| setBadges(prev => [...new Set([...prev, "change-maker", "sustainability-champion"])]); | |
| // Reset for new exploration | |
| setPhase(1); | |
| setSelectedSDG(null); | |
| setUserCountry(""); | |
| setLocalSituation(""); | |
| setPersonalExperience(""); | |
| setUserActionIdea(""); | |
| setAiContext(""); | |
| setLocalContext(""); | |
| setGlobalContext(""); | |
| setSelectedActions([]); | |
| setActionDetails({}); | |
| setSuccessIndicators(""); | |
| setSimulationResults(null); | |
| setImpactScore(0); | |
| setReflections({ surprising_results: "", influencing_factors: "", realism_assessment: "", personal_connection: "" }); | |
| setAiFeedback(""); | |
| setAiError(""); | |
| }; | |
| // validateApiKey (Same as before) | |
| const validateApiKey = async (key) => { | |
| try { | |
| const response = await fetch("https://api.openai.com/v1/models", { | |
| headers: { | |
| "Authorization": `Bearer ${key.trim()}` | |
| } | |
| }); | |
| if (!response.ok) { | |
| throw new Error("Invalid API key"); | |
| } | |
| return true; | |
| } catch (error) { | |
| throw new Error("Invalid API key. Please check and try again."); | |
| } | |
| }; | |
| // handleApiKeySubmit (Same as before) | |
| const handleApiKeySubmit = async (key) => { | |
| setAiError(""); | |
| try { | |
| await validateApiKey(key); | |
| setApiKey(key); | |
| setShowApiKeyModal(false); | |
| } catch (error) { | |
| setAiError(error.message); | |
| throw error; | |
| } | |
| }; | |
| const getPhaseProgress = () => { | |
| return (phase / 5) * 100; | |
| }; | |
| // API Key Modal Component (Same as before) | |
| const ApiKeyModal = () => { | |
| const [keyInput, setKeyInput] = useState(""); | |
| const [isValidating, setIsValidating] = useState(false); | |
| const [modalError, setModalError] = useState(""); | |
| const handleSubmit = async (e) => { | |
| e.preventDefault(); | |
| if (!keyInput.trim()) return; | |
| setIsValidating(true); | |
| setModalError(""); | |
| try { | |
| await handleApiKeySubmit(keyInput); | |
| } catch (error) { | |
| setModalError(error.message); | |
| } finally { | |
| setIsValidating(false); | |
| } | |
| }; | |
| return ( | |
| <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> | |
| <div className="glass p-6 max-w-md w-full"> | |
| <h3 className="text-xl font-bold text-gray-800 mb-4">π OpenAI API Key Required</h3> | |
| <p className="text-gray-600 mb-4"> | |
| This SDG Explorer uses AI to provide rich contextual insights and personalized feedback. | |
| Your API key will be stored locally and used only for this application. | |
| </p> | |
| <form onSubmit={handleSubmit}> | |
| <input | |
| type="password" | |
| value={keyInput} | |
| onChange={(e) => setKeyInput(e.target.value)} | |
| placeholder="sk-..." | |
| className="w-full px-3 py-2 border rounded-lg mb-4" | |
| autoFocus | |
| /> | |
| {modalError && ( | |
| <div className="mb-3 text-red-600 text-sm">{modalError}</div> | |
| )} | |
| <div className="flex gap-2"> | |
| <button | |
| type="submit" | |
| disabled={!keyInput.trim() || isValidating} | |
| className="flex-1 bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition disabled:opacity-50" | |
| > | |
| {isValidating ? "Validating..." : "Continue"} | |
| </button> | |
| <button | |
| type="button" | |
| onClick={() => setShowApiKeyModal(false)} | |
| className="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400 transition" | |
| > | |
| Cancel | |
| </button> | |
| </div> | |
| </form> | |
| <p className="text-xs text-gray-500 mt-3"> | |
| Get your API key at <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" className="text-blue-600 underline">OpenAI</a> | |
| </p> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| return ( | |
| <ErrorBoundary> | |
| <div className="min-h-screen py-6"> | |
| <div className="max-w-6xl mx-auto px-4"> | |
| {/* Header (Same as before) */} | |
| <div className="glass p-6 mb-6 text-center"> | |
| <h1 className="text-4xl font-bold text-gray-800 mb-2"> | |
| π Personalized SDG Explorer | |
| </h1> | |
| <p className="text-gray-600 mb-4"> | |
| Explore SDGs, connect to your context, plan actions, simulate outcomes, and reflect on your impact. By Shift Mind AI Labs | |
| </p> | |
| {/* Progress and Badges Display (Same as before) */} | |
| <div className="flex flex-wrap justify-center items-center gap-4 mt-4"> | |
| <div className="phase-indicator"> | |
| Phase {phase}/5: { | |
| phase === 1 ? "SDG Exploration" : | |
| phase === 2 ? "Personalized Context" : | |
| phase === 3 ? "Action Planning" : | |
| phase === 4 ? "Outcome Simulation" : | |
| "Reflection & Feedback" | |
| } | |
| </div> | |
| {badges.length > 0 && ( | |
| <div className="flex flex-wrap justify-center gap-2"> | |
| {[...new Set(badges)].slice(-3).map((badgeId, index) => { | |
| const badge = BADGES[badgeId]; | |
| return ( | |
| <div key={index} className={`badge ${badge.color} celebration`}> | |
| {badge.icon} {badge.name} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| {/* Overall Progress Bar (Same as before) */} | |
| <div className="max-w-md mx-auto mt-4"> | |
| <div className="flex justify-between text-xs text-gray-500 mb-2"> | |
| <span>Overall Progress</span> | |
| <span>{Math.round(getPhaseProgress())}%</span> | |
| </div> | |
| <div className="w-full bg-gray-200 rounded-full h-2"> | |
| <div | |
| className="progress-bar rounded-full h-2" | |
| style={{ width: `${getPhaseProgress()}%` }} | |
| ></div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Phase 1: SDG Exploration and Selection (Same as before) */} | |
| {phase === 1 && ( | |
| <div className="fade-in"> | |
| <div className="glass p-6 mb-6"> | |
| <h2 className="text-2xl font-bold text-gray-800 mb-6 text-center"> | |
| π Phase 1: SDG Exploration and Selection | |
| </h2> | |
| <p className="text-gray-600 text-center mb-8"> | |
| Explore the 17 Sustainable Development Goals and select one for deeper investigation. | |
| Click on any SDG to learn more about its objectives and global importance. | |
| </p> | |
| {/* SDG Grid (Same as before) */} | |
| <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 sdg-grid mb-8"> | |
| {Object.entries(SDG_DATA).map(([number, sdg]) => ( | |
| <div | |
| key={number} | |
| className={`sdg-card sdg-${number} ${selectedSDG === parseInt(number) ? "selected" : ""}`} | |
| onClick={() => selectSDG(parseInt(number))} | |
| tabIndex={0} | |
| role="button" | |
| aria-pressed={selectedSDG === parseInt(number)} | |
| > | |
| <div className="text-3xl mb-2">{sdg.icon}</div> | |
| <div className="text-lg font-bold mb-1">SDG {number}</div> | |
| <h4 className="font-semibold text-sm mb-2">{sdg.title}</h4> | |
| <p className="text-xs opacity-90">{sdg.description}</p> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Selected SDG Details (Same as before) */} | |
| {selectedSDG && ( | |
| <div className="slide-in mb-8"> | |
| <div className="bg-blue-50 rounded-lg p-6"> | |
| <h3 className="text-xl font-bold text-blue-800 mb-3"> | |
| {SDG_DATA[selectedSDG].icon} SDG {selectedSDG}: {SDG_DATA[selectedSDG].title} | |
| </h3> | |
| <p className="text-blue-700 mb-4">{SDG_DATA[selectedSDG].description}</p> | |
| <div> | |
| <h4 className="font-semibold text-blue-800 mb-2">Key Targets:</h4> | |
| <ul className="list-disc list-inside text-blue-700 space-y-1"> | |
| {SDG_DATA[selectedSDG].targets.map((target, index) => ( | |
| <li key={index}>{target}</li> | |
| ))} | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Continue Button (Same as before) */} | |
| {selectedSDG && ( | |
| <div className="text-center"> | |
| <button | |
| onClick={completeSDGSelection} | |
| className="bg-gradient-to-r from-blue-600 to-purple-600 text-white px-8 py-3 rounded-lg font-semibold hover:from-blue-700 hover:to-purple-700 transition pulse" | |
| > | |
| Continue to Personalized Context π | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {/* Phase 2: Personalized Contextualization */} | |
| {phase === 2 && ( | |
| <div className="fade-in"> | |
| <div className="glass p-6 mb-6"> | |
| <h2 className="text-2xl font-bold text-gray-800 mb-6 text-center"> | |
| π Phase 2: Personalized Contextualization | |
| </h2> | |
| {/* Selected SDG Summary (Same as before) */} | |
| <div className="bg-blue-50 rounded-lg p-4 mb-6"> | |
| <h3 className="font-semibold text-blue-800 mb-2">Your Selected SDG:</h3> | |
| <p className="text-blue-700"> | |
| <strong>{SDG_DATA[selectedSDG].icon} SDG {selectedSDG}: {SDG_DATA[selectedSDG].title}</strong> | |
| </p> | |
| <p className="text-blue-600 mt-2">{SDG_DATA[selectedSDG].description}</p> | |
| </div> | |
| {/* NEW: Personalization Inputs */} | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8"> | |
| <div> | |
| <label htmlFor="countrySelect" className="block text-gray-700 font-medium mb-2"> | |
| Select Your Country: | |
| </label> | |
| <select | |
| id="countrySelect" | |
| value={userCountry} | |
| onChange={(e) => setUserCountry(e.target.value)} | |
| className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| > | |
| <option value="">-- Select Country --</option> | |
| {countries.map(country => ( | |
| <option key={country.code} value={country.name}>{country.name}</option> | |
| ))} | |
| </select> | |
| </div> | |
| <div> | |
| <label htmlFor="localSituation" className="block text-gray-700 font-medium mb-2"> | |
| Describe Your Local Situation (Optional): | |
| </label> | |
| <textarea | |
| id="localSituation" | |
| value={localSituation} | |
| onChange={(e) => setLocalSituation(e.target.value)} | |
| placeholder={`How does SDG ${selectedSDG} relate to your community? (e.g., challenges, initiatives, personal observations)`} | |
| className="w-full px-4 py-3 border border-gray-300 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| rows={3} | |
| /> | |
| </div> | |
| </div> | |
| {/* Generate AI Context Button */} | |
| {!aiContext && !isGeneratingContext && !aiError && ( | |
| <div className="text-center mb-8"> | |
| <button | |
| onClick={generateAIContext} | |
| disabled={!userCountry} | |
| className="bg-gradient-to-r from-green-600 to-teal-600 text-white px-8 py-3 rounded-lg font-semibold hover:from-green-700 hover:to-teal-700 transition pulse disabled:opacity-50" | |
| > | |
| Generate Personalized AI Context π€ | |
| </button> | |
| <p className="text-gray-600 mt-2"> | |
| AI will provide context tailored to your country and local situation | |
| </p> | |
| </div> | |
| )} | |
| {/* AI Context Generation Loading (Same as before) */} | |
| {isGeneratingContext && ( | |
| <div className="ai-insight-container text-center"> | |
| <h3 className="text-lg font-semibold text-green-800 mb-4"> | |
| π€ AI is Analyzing Personalized Context... | |
| </h3> | |
| <div className="typing-indicator justify-center mb-4"> | |
| <span className="text-green-700 mr-3">Generating examples for {userCountry}</span> | |
| <div className="typing-dot"></div> | |
| <div className="typing-dot"></div> | |
| <div className="typing-dot"></div> | |
| </div> | |
| </div> | |
| )} | |
| {/* AI Context Display (Same as before) */} | |
| {aiContext && !isGeneratingContext && ( | |
| <div className="space-y-6"> | |
| <div className="ai-insight-container"> | |
| <h3 className="text-lg font-semibold text-green-800 mb-3">π AI-Generated Personalized Context</h3> | |
| <SafeAIContent content={aiContext} /> | |
| </div> | |
| {/* Continue to Action Planning */} | |
| <div className="text-center"> | |
| <button | |
| onClick={completeContextualization} | |
| className="bg-gradient-to-r from-purple-600 to-pink-600 text-white px-8 py-3 rounded-lg font-semibold hover:from-purple-700 hover:to-pink-700 transition pulse" | |
| > | |
| Continue to Action Planning π | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Error Display (Same as before) */} | |
| {aiError && !isGeneratingContext && ( | |
| <div className="bg-red-50 border border-red-200 rounded-lg p-4 mt-6"> | |
| <h4 className="font-semibold text-red-800 mb-2">π¨ Error Generating Context</h4> | |
| <p className="text-red-700 mb-3">{aiError}</p> | |
| <button | |
| onClick={generateAIContext} | |
| disabled={!userCountry} | |
| className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition disabled:opacity-50" | |
| > | |
| Retry AI Analysis | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {/* Phase 3: Interactive Action Planning */} | |
| {phase === 3 && ( | |
| <div className="fade-in"> | |
| <div className="glass p-6 mb-6"> | |
| <h2 className="text-2xl font-bold text-gray-800 mb-6 text-center"> | |
| π Phase 3: Interactive Action Planning | |
| </h2> | |
| {/* Action Type Selection (Same as before) */} | |
| <div className="mb-8"> | |
| <h3 className="text-xl font-semibold text-gray-700 mb-4">Select Action Strategies</h3> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
| {Object.entries(ACTION_TYPES).map(([key, action]) => ( | |
| <div | |
| key={key} | |
| className={`action-card ${selectedActions.includes(key) ? "selected" : ""}`} | |
| onClick={() => toggleAction(key)} | |
| > | |
| <div className="flex items-center justify-between mb-2"> | |
| <div className="text-2xl">{action.icon}</div> | |
| <span className={`px-2 py-1 rounded text-xs font-medium ${ | |
| action.difficulty === "easy" ? "bg-green-100 text-green-800" : | |
| action.difficulty === "medium" ? "bg-yellow-100 text-yellow-800" : | |
| "bg-red-100 text-red-800" | |
| }`}> | |
| {action.difficulty} | |
| </span> | |
| </div> | |
| <h4 className="font-semibold text-gray-800 mb-2">{action.name}</h4> | |
| <p className="text-gray-600 text-sm mb-2">{action.description}</p> | |
| <div className="text-xs text-gray-500"> | |
| Impact Range: {action.impact_range[0]}-{action.impact_range[1]}% | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* NEW: User Action Idea Input */} | |
| <div className="mb-8 slide-in"> | |
| <h3 className="text-xl font-semibold text-gray-700 mb-4">π‘ Suggest Your Own Action Idea (Optional)</h3> | |
| <textarea | |
| value={userActionIdea} | |
| onChange={(e) => setUserActionIdea(e.target.value)} | |
| placeholder={`Do you have a unique idea to address SDG ${selectedSDG} in ${userCountry}? Describe it here...`} | |
| className="w-full px-4 py-3 border border-gray-300 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| rows={3} | |
| /> | |
| </div> | |
| {/* Action Details (Same as before) */} | |
| {selectedActions.length > 0 && ( | |
| <div className="mb-8 slide-in"> | |
| <h3 className="text-xl font-semibold text-gray-700 mb-4">Define Action Details</h3> | |
| {selectedActions.map(actionType => ( | |
| <div key={actionType} className="mb-4 p-4 bg-gray-50 rounded-lg"> | |
| <h4 className="font-semibold text-gray-800 mb-2"> | |
| {ACTION_TYPES[actionType].icon} {ACTION_TYPES[actionType].name} | |
| </h4> | |
| <textarea | |
| value={actionDetails[actionType] || ""} | |
| onChange={(e) => updateActionDetails(actionType, e.target.value)} | |
| placeholder={`Describe how you would implement ${ACTION_TYPES[actionType].name.toLowerCase()} for SDG ${selectedSDG} in ${userCountry}...`} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| rows={3} | |
| /> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {/* Success Indicators (Same as before) */} | |
| {(selectedActions.length > 0 || userActionIdea.trim()) && ( | |
| <div className="mb-8 slide-in"> | |
| <h3 className="text-xl font-semibold text-gray-700 mb-4">Define Success Indicators</h3> | |
| <textarea | |
| value={successIndicators} | |
| onChange={(e) => setSuccessIndicators(e.target.value)} | |
| placeholder="How will you measure the success of your actions? (e.g., participation rates, learning outcomes, behavior changes...)" | |
| className="w-full px-4 py-3 border border-gray-300 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| rows={4} | |
| /> | |
| </div> | |
| )} | |
| {/* Continue Button (Updated condition) */} | |
| {(selectedActions.length > 0 || userActionIdea.trim()) && successIndicators && ( | |
| <div className="text-center"> | |
| <button | |
| onClick={completeActionPlanning} | |
| className="bg-gradient-to-r from-orange-600 to-red-600 text-white px-8 py-3 rounded-lg font-semibold hover:from-orange-700 hover:to-red-700 transition pulse" | |
| > | |
| Continue to Outcome Simulation π | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {/* Phase 4: Outcome Simulation (Same as before, but AI prompt is personalized) */} | |
| {phase === 4 && ( | |
| <div className="fade-in"> | |
| <div className="glass p-6 mb-6"> | |
| <h2 className="text-2xl font-bold text-gray-800 mb-6 text-center"> | |
| π Phase 4: Outcome Simulation | |
| </h2> | |
| {/* Action Summary (Updated to include user idea) */} | |
| <div className="bg-blue-50 rounded-lg p-4 mb-6"> | |
| <h3 className="font-semibold text-blue-800 mb-3">Your Action Plan Summary:</h3> | |
| <div className="space-y-2"> | |
| {selectedActions.map(actionType => ( | |
| <div key={actionType} className="flex items-center gap-2"> | |
| <span className="text-lg">{ACTION_TYPES[actionType].icon}</span> | |
| <span className="text-blue-700">{ACTION_TYPES[actionType].name}</span> | |
| </div> | |
| ))} | |
| {userActionIdea.trim() && ( | |
| <div className="flex items-center gap-2"> | |
| <span className="text-lg">π‘</span> | |
| <span className="text-blue-700">Your Idea: {userActionIdea.substring(0, 50)}...</span> | |
| </div> | |
| )} | |
| </div> | |
| <div className="mt-3"> | |
| <strong className="text-blue-800">Success Indicators:</strong> | |
| <p className="text-blue-700 mt-1">{successIndicators}</p> | |
| </div> | |
| </div> | |
| {/* Run Simulation Button (Same as before) */} | |
| {!simulationResults && !isSimulating && ( | |
| <div className="text-center mb-8"> | |
| <button | |
| onClick={runSimulation} | |
| className="bg-gradient-to-r from-purple-600 to-blue-600 text-white px-8 py-3 rounded-lg font-semibold hover:from-purple-700 hover:to-blue-700 transition pulse" | |
| > | |
| Run Personalized Impact Simulation π | |
| </button> | |
| <p className="text-gray-600 mt-2"> | |
| Simulate outcomes based on your actions and context in {userCountry} | |
| </p> | |
| </div> | |
| )} | |
| {/* Simulation Loading (Same as before) */} | |
| {isSimulating && ( | |
| <div className="ai-insight-container text-center"> | |
| <h3 className="text-lg font-semibold text-green-800 mb-4"> | |
| π Running Personalized Simulation... | |
| </h3> | |
| <div className="typing-indicator justify-center mb-4"> | |
| <span className="text-green-700 mr-3">Calculating potential outcomes for {userCountry}</span> | |
| <div className="typing-dot"></div> | |
| <div className="typing-dot"></div> | |
| <div className="typing-dot"></div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Simulation Results (Same as before) */} | |
| {simulationResults && !isSimulating && ( | |
| <div className="space-y-6"> | |
| {/* Impact Score */} | |
| <div className="simulation-result"> | |
| <h3 className="text-lg font-semibold text-blue-800 mb-3">π― Predicted Impact Score (in {userCountry})</h3> | |
| <div className="flex items-center gap-4 mb-2"> | |
| <div className="flex-1 impact-meter"> | |
| <div | |
| className="impact-indicator" | |
| style={{ width: `${impactScore}%` }} | |
| ></div> | |
| </div> | |
| <span className="text-2xl font-bold text-blue-800">{impactScore}%</span> | |
| </div> | |
| <p className="text-blue-700"> | |
| Based on your actions and local context, the simulation predicts a {impactScore}% positive impact on SDG {selectedSDG} in {userCountry}. | |
| </p> | |
| </div> | |
| {/* Timeline */} | |
| <div className="simulation-result"> | |
| <h3 className="text-lg font-semibold text-blue-800 mb-3">β° Expected Timeline</h3> | |
| <p className="text-blue-700">{simulationResults.timeline}</p> | |
| </div> | |
| {/* Challenges */} | |
| {simulationResults.challenges && simulationResults.challenges.length > 0 && ( | |
| <div className="simulation-result"> | |
| <h3 className="text-lg font-semibold text-blue-800 mb-3">β οΈ Potential Challenges (in {userCountry})</h3> | |
| <ul className="list-disc list-inside text-blue-700 space-y-1"> | |
| {simulationResults.challenges.map((challenge, index) => ( | |
| <li key={index}>{challenge}</li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| {/* Mitigation Strategies */} | |
| {simulationResults.mitigations && simulationResults.mitigations.length > 0 && ( | |
| <div className="simulation-result"> | |
| <h3 className="text-lg font-semibold text-blue-800 mb-3">π‘ Mitigation Strategies (for {userCountry})</h3> | |
| <ul className="list-disc list-inside text-blue-700 space-y-1"> | |
| {simulationResults.mitigations.map((mitigation, index) => ( | |
| <li key={index}>{mitigation}</li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| {/* Success Factors */} | |
| {simulationResults.success_factors && simulationResults.success_factors.length > 0 && ( | |
| <div className="simulation-result"> | |
| <h3 className="text-lg font-semibold text-blue-800 mb-3">π Key Success Factors (in {userCountry})</h3> | |
| <ul className="list-disc list-inside text-blue-700 space-y-1"> | |
| {simulationResults.success_factors.map((factor, index) => ( | |
| <li key={index}>{factor}</li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| {/* Continue to Reflection */} | |
| <div className="text-center"> | |
| <button | |
| onClick={completeSimulation} | |
| className="bg-gradient-to-r from-green-600 to-teal-600 text-white px-8 py-3 rounded-lg font-semibold hover:from-green-700 hover:to-teal-700 transition pulse" | |
| > | |
| Continue to Reflection & Feedback π | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {/* Error Display (Same as before) */} | |
| {aiError && !isSimulating && ( | |
| <div className="bg-red-50 border border-red-200 rounded-lg p-4 mt-6"> | |
| <h4 className="font-semibold text-red-800 mb-2">π¨ Simulation Error</h4> | |
| <p className="text-red-700 mb-3">{aiError}</p> | |
| <button | |
| onClick={runSimulation} | |
| className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition" | |
| > | |
| Retry Simulation | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {/* Phase 5: Reflection and AI-driven Feedback */} | |
| {phase === 5 && ( | |
| <div className="fade-in"> | |
| <div className="glass p-6 mb-6"> | |
| <h2 className="text-2xl font-bold text-gray-800 mb-6 text-center"> | |
| π Phase 5: Reflection and Personalized Feedback | |
| </h2> | |
| {/* Exploration Summary (Same as before) */} | |
| <div className="bg-blue-50 rounded-lg p-4 mb-8"> | |
| <h3 className="font-semibold text-blue-800 mb-3">π― Your SDG Exploration Summary</h3> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm"> | |
| <div> | |
| <strong className="text-blue-700">SDG:</strong> | |
| <p className="text-blue-600">{SDG_DATA[selectedSDG].icon} SDG {selectedSDG}: {SDG_DATA[selectedSDG].title}</p> | |
| </div> | |
| <div> | |
| <strong className="text-blue-700">Country:</strong> | |
| <p className="text-blue-600">{userCountry}</p> | |
| </div> | |
| <div> | |
| <strong className="text-blue-700">Impact Score:</strong> | |
| <p className="text-blue-600">{impactScore}% predicted impact</p> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Reflection Questions (Updated with personal connection) */} | |
| <div className="mb-8"> | |
| <h3 className="text-xl font-semibold text-gray-700 mb-4">π Reflection Questions</h3> | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="block text-gray-700 font-medium mb-2"> | |
| What were the most surprising simulation results for {userCountry}? | |
| </label> | |
| <textarea | |
| value={reflections.surprising_results} | |
| onChange={(e) => setReflections(prev => ({ ...prev, surprising_results: e.target.value }))} | |
| className="w-full px-4 py-3 border border-gray-300 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| rows={3} | |
| placeholder="Reflect on unexpected outcomes or insights from the simulation..." | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-gray-700 font-medium mb-2"> | |
| What factors most influenced the outcomes in your local context? | |
| </label> | |
| <textarea | |
| value={reflections.influencing_factors} | |
| onChange={(e) => setReflections(prev => ({ ...prev, influencing_factors: e.target.value }))} | |
| className="w-full px-4 py-3 border border-gray-300 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| rows={3} | |
| placeholder="Analyze which elements had the greatest impact on your results..." | |
| /> | |
| </div> | |
| <div> | |
| <label className="block text-gray-700 font-medium mb-2"> | |
| How realistic do you think these outcomes are for {userCountry}, and why? | |
| </label> | |
| <textarea | |
| value={reflections.realism_assessment} | |
| onChange={(e) => setReflections(prev => ({ ...prev, realism_assessment: e.target.value }))} | |
| className="w-full px-4 py-3 border border-gray-300 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| rows={3} | |
| placeholder="Evaluate the realism of the simulation and consider real-world factors..." | |
| /> | |
| </div> | |
| {/* NEW: Personal Connection Reflection */} | |
| <div> | |
| <label className="block text-gray-700 font-medium mb-2"> | |
| How does this SDG connect to your personal experiences or observations in {userCountry}? | |
| </label> | |
| <textarea | |
| value={reflections.personal_connection} | |
| onChange={(e) => setReflections(prev => ({ ...prev, personal_connection: e.target.value }))} | |
| className="w-full px-4 py-3 border border-gray-300 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| rows={3} | |
| placeholder="Share any personal connections, stories, or observations related to this SDG..." | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Generate AI Feedback Button (Updated condition) */} | |
| {!aiFeedback && !isGeneratingFeedback && !aiError && ( | |
| <div className="text-center mb-8"> | |
| <button | |
| onClick={generateReflectionFeedback} | |
| disabled={!reflections.surprising_results || !reflections.influencing_factors || !reflections.realism_assessment || !reflections.personal_connection || isGeneratingFeedback} | |
| className="bg-gradient-to-r from-purple-600 to-pink-600 text-white px-8 py-3 rounded-lg font-semibold hover:from-purple-700 hover:to-pink-700 transition disabled:opacity-50" | |
| > | |
| Get Personalized AI Feedback π€ | |
| </button> | |
| </div> | |
| )} | |
| {/* AI Feedback Generation Loading (Same as before) */} | |
| {isGeneratingFeedback && ( | |
| <div className="ai-feedback-container text-center"> | |
| <h3 className="text-lg font-semibold text-orange-800 mb-4"> | |
| π€ AI is Analyzing Your Personalized Learning Journey... | |
| </h3> | |
| <div className="typing-indicator justify-center"> | |
| <span className="text-orange-700 mr-3">Providing personalized feedback and insights</span> | |
| <div className="typing-dot"></div> | |
| <div className="typing-dot"></div> | |
| <div className="typing-dot"></div> | |
| </div> | |
| </div> | |
| )} | |
| {/* AI Feedback Display (Same as before) */} | |
| {aiFeedback && !isGeneratingFeedback && ( | |
| <div className="ai-feedback-container mb-8"> | |
| <h3 className="text-lg font-semibold text-orange-800 mb-3">π AI Feedback on Your Personalized SDG Exploration</h3> | |
| <SafeAIContent content={aiFeedback} /> | |
| </div> | |
| )} | |
| {/* Error Display for Feedback (Same as before) */} | |
| {aiError && !isGeneratingFeedback && ( | |
| <div className="bg-red-50 border border-red-200 rounded-lg p-4 mt-6"> | |
| <h4 className="font-semibold text-red-800 mb-2">π¨ Error Generating AI Feedback</h4> | |
| <p className="text-red-700 mb-3">{aiError}</p> | |
| <button | |
| onClick={generateReflectionFeedback} | |
| className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition" | |
| > | |
| Retry AI Feedback | |
| </button> | |
| </div> | |
| )} | |
| {/* Complete Exploration Button (Same as before) */} | |
| {aiFeedback && !isGeneratingFeedback && ( | |
| <div className="text-center"> | |
| <button | |
| onClick={completeExploration} | |
| className="bg-gradient-to-r from-green-600 to-teal-600 text-white px-8 py-3 rounded-lg font-semibold hover:from-green-700 hover:to-teal-700 transition celebration" | |
| > | |
| Complete Exploration & Start New One π | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {/* Exploration History (Updated to show country) */} | |
| {explorationHistory.length > 0 && ( | |
| <div className="glass p-6 mt-6"> | |
| <h3 className="text-xl font-bold text-gray-800 mb-4"> | |
| π Your Personalized SDG Exploration History | |
| </h3> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
| {explorationHistory.slice(-6).map(exploration => ( | |
| <div key={exploration.id} className="bg-white rounded-lg p-4 border border-gray-200"> | |
| <div className="flex items-center justify-between mb-2"> | |
| <h4 className="font-semibold text-gray-800"> | |
| {SDG_DATA[exploration.sdg].icon} SDG {exploration.sdg} | |
| </h4> | |
| <span className="text-xs text-gray-500"> | |
| {new Date(exploration.timestamp).toLocaleDateString()} | |
| </span> | |
| </div> | |
| <p className="text-sm text-gray-700 mb-1"> | |
| {SDG_DATA[exploration.sdg].title} | |
| </p> | |
| <p className="text-xs text-gray-500 mb-2">Country: {exploration.country}</p> | |
| <div className="flex justify-between text-xs text-gray-600 mb-2"> | |
| <span>Actions: {exploration.actions.length + (exploration.userActionIdea ? 1 : 0)}</span> | |
| <span>Impact: {exploration.impactScore}%</span> | |
| </div> | |
| <div className="flex flex-wrap gap-1"> | |
| {exploration.badgesEarned.slice(-2).map((badgeId, index) => { | |
| const badge = BADGES[badgeId]; | |
| return ( | |
| <span key={index} className="text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded"> | |
| {badge?.icon} {badge?.name} | |
| </span> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| <p className="text-center text-gray-500 mt-4"> | |
| Created by Shift Mind AI Labs | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| {/* API Key Modal (Same as before) */} | |
| {showApiKeyModal && <ApiKeyModal />} | |
| </div> | |
| </ErrorBoundary> | |
| ); | |
| } | |
| ReactDOM.createRoot(document.getElementById("root")).render(<PersonalizedSDGExplorer />); | |
| </script> | |
| </body> | |
| </html> | |