Personalized_SDG / index.html
Hma47's picture
Update index.html
62d0409 verified
<!DOCTYPE html>
<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>