// Global State
let token = localStorage.getItem('token');
let userId = localStorage.getItem('userId');
let selectedChildId = localStorage.getItem('selectedChildId');
let currentEmotionLogId = null;
// --- Data Templates ---
const animalTemplates = {
'Dog': `
`,
'Cat': `
`,
'Lion': `
`,
'Bunny': `
`,
'Fish': `
`
};
const coloringTemplates = {
'House': `
`,
'Flower': `
`,
'Car': `
`
};
const quizData = {
'Social Skills': [
{
question: "A friend is feeling sad and crying. What should you do?",
options: [
{ text: "Ask if they are okay ๐ซ", correct: true },
{ text: "Laugh at them ๐", correct: false },
{ text: "Run away ๐", correct: false }
]
},
{
question: "You want to play with a toy someone else has. What do you say?",
options: [
{ text: "Can I have a turn please? ๐งธ", correct: true },
{ text: "Grab it quickly! ๐๏ธ", correct: false },
{ text: "Yell at them ๐ข", correct: false }
]
},
{
question: "Someone says 'Hello' to you. What is a nice way to respond?",
options: [
{ text: "Smile and say 'Hi'! ๐", correct: true },
{ text: "Look at the floor โฌ๏ธ", correct: false },
{ text: "Walk away ๐ถ", correct: false }
]
},
{
question: "You accidentally bumped into a friend. What do you say?",
options: [
{ text: "I'm sorry! ๐", correct: true },
{ text: "It was your fault! ๐ ", correct: false },
{ text: "Say nothing ๐ค", correct: false }
]
},
{
question: "A friend is talking, but you have something to say too. What should you do?",
options: [
{ text: "Wait for them to finish, then speak ๐", correct: true },
{ text: "Interrupt them immediately ๐ข", correct: false },
{ text: "Start talking louder ๐ฃ๏ธ", correct: false }
]
}
],
'Daily Routine': [
{
question: "You just finished eating dinner. What comes next?",
options: [
{ text: "Brush your teeth ๐ชฅ", correct: true },
{ text: "Go to school ๐ซ", correct: false },
{ text: "Eat breakfast ๐ฅฃ", correct: false }
]
},
{
question: "What should you do right after you wake up in the morning?",
options: [
{ text: "Wash your face and get dressed โ๏ธ", correct: true },
{ text: "Go to sleep ๐ด", correct: false },
{ text: "Eat dinner ๐ฝ๏ธ", correct: false }
]
},
{
question: "What is the first thing you do before eating your lunch?",
options: [
{ text: "Wash your hands ๐งผ", correct: true },
{ text: "Run outside ๐ณ", correct: false },
{ text: "Start singing ๐ค", correct: false }
]
},
{
question: "Where do your toys go when you are finished playing?",
options: [
{ text: "In the toy box ๐ฆ", correct: true },
{ text: "On the floor ๐งน", correct: false },
{ text: "In the fridge ๐ง", correct: false }
]
},
{
question: "What do we do before going to bed at night?",
options: [
{ text: "Put on pajamas and read a story ๐", correct: true },
{ text: "Go for a swim ๐", correct: false },
{ text: "Eat a big meal ๐", correct: false }
]
}
],
'Safety & Help': [
{
question: "If you feel lost in a big store, who should you look for?",
options: [
{ text: "A store worker in a uniform ๐ฎ", correct: true },
{ text: "A stranger ๐ค", correct: false },
{ text: "Run outside ๐", correct: false }
]
},
{
question: "What should you do if you see something hot on the stove?",
options: [
{ text: "Stay away and tell a grown-up ๐ซ", correct: true },
{ text: "Touch it ๐๏ธ", correct: false },
{ text: "Blow on it ๐ฌ๏ธ", correct: false }
]
},
{
question: "If you get a small scrape on your knee, what should you do?",
options: [
{ text: "Tell a teacher or parent ๐ฉน", correct: true },
{ text: "Keep running ๐", correct: false },
{ text: "Cry all day ๐ญ", correct: false }
]
},
{
question: "A stranger asks you to go with them. What do you do?",
options: [
{ text: "Say 'NO' and run to a safe adult ๐", correct: true },
{ text: "Go with them ๐ถ", correct: false },
{ text: "Take the candy they offer ๐ฌ", correct: false }
]
},
{
question: "What number should you know for emergencies?",
options: [
{ text: "911 (or your local emergency number) โ๏ธ", correct: true },
{ text: "123 ๐ข", correct: false },
{ text: "555 ๐", correct: false }
]
}
],
'Advanced Social Skills': [
{
question: "Your friend is building a tower and it keeps falling. How might they feel?",
options: [
{ text: "Frustrated or annoyed ๐ ", correct: true },
{ text: "Excited ๐คฉ", correct: false },
{ text: "Sleepy ๐ด", correct: false }
]
},
{
question: "A friend is talking about their favorite cat. What is a good thing to do?",
options: [
{ text: "Listen and ask 'What is your cat's name?' ๐ฑ", correct: true },
{ text: "Start talking about your dog ๐ถ", correct: false },
{ text: "Walk away ๐ถ", correct: false }
]
},
{
question: "You want to play a game with a group. How can you join in?",
options: [
{ text: "Wait for a break and ask 'Can I play too?' ๐ค", correct: true },
{ text: "Jump into the middle of the game ๐", correct: false },
{ text: "Take the ball away โฝ", correct: false }
]
},
{
question: "You see someone sitting alone at recess. What could you do?",
options: [
{ text: "Ask if they want to play with you ๐ค", correct: true },
{ text: "Ignore them ๐", correct: false },
{ text: "Point and laugh โ๏ธ", correct: false }
]
},
{
question: "If your friend wins a game and you lose, what should you say?",
options: [
{ text: "Good game! You did well! ๐", correct: true },
{ text: "I hate this game! ๐ ", correct: false },
{ text: "You cheated! ๐ค", correct: false }
]
}
]
};
// UI Initial Checks
document.addEventListener('DOMContentLoaded', () => {
console.log("DOM Loaded. Path:", window.location.pathname);
updateNav();
applySensoryUI();
applyLevelUI();
if (window.location.pathname === '/login') {
if (token) {
const loginSection = document.getElementById('login-section');
const childSection = document.getElementById('child-section');
if (loginSection) loginSection.classList.add('hidden');
if (childSection) {
childSection.classList.remove('hidden');
loadChildren();
} else {
window.location.href = '/children';
}
}
}
if (window.location.pathname === '/dashboard' || window.location.pathname === '/') {
loadChildrenForDashboard();
}
if (window.location.pathname === '/diary') {
loadDiary();
}
if (window.location.pathname === '/emotion-learning') {
loadCustomEmotions();
}
});
function updateNav() {
if (token) {
const loginNav = document.getElementById('nav-login');
const logoutNav = document.getElementById('nav-logout');
if (loginNav) loginNav.classList.add('hidden');
if (logoutNav) logoutNav.classList.remove('hidden');
}
}
function logout() {
localStorage.clear();
window.location.href = '/login';
}
function toggleAuth(showRegister) {
const loginSection = document.getElementById('login-section');
const regSection = document.getElementById('register-section');
if (showRegister) {
if (loginSection) loginSection.classList.add('hidden');
if (regSection) regSection.classList.remove('hidden');
} else {
if (loginSection) loginSection.classList.remove('hidden');
if (regSection) regSection.classList.add('hidden');
}
}
// Auth API Calls
async function register() {
const username = document.getElementById('reg-username').value;
const password = document.getElementById('reg-password').value;
const res = await fetch('/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (res.ok) {
alert("Registered! Please login.");
toggleAuth(false);
} else alert(data.detail);
}
async function login() {
const usernameInput = document.getElementById('login-username');
const passwordInput = document.getElementById('login-password');
if (!usernameInput || !passwordInput) return;
const username = usernameInput.value;
const password = passwordInput.value;
const res = await fetch('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (res.ok) {
localStorage.setItem('token', data.access_token);
localStorage.setItem('userId', data.user_id);
token = data.access_token;
userId = data.user_id;
window.location.href = '/children';
} else alert(data.detail);
}
async function addChild() {
const nameInput = document.getElementById('child-name');
const ageInput = document.getElementById('child-age');
const inheritanceInput = document.getElementById('child-autism-inheritance');
const sensoryInput = document.getElementById('child-sensory-level');
if (!nameInput || !ageInput) return;
const name = nameInput.value;
const age = parseInt(ageInput.value);
const parent_id = parseInt(userId || localStorage.getItem('userId'));
if (!parent_id) {
alert("Please log in again.");
window.location.href = '/login';
return;
}
const autism_inheritance = inheritanceInput ? inheritanceInput.value : "";
const sensory_level = sensoryInput ? sensoryInput.value : "standard";
console.log("Adding child with data:", { name, age, parent_id, autism_inheritance, sensory_level });
try {
const res = await fetch('/auth/add-child', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
age,
parent_id,
autism_inheritance,
sensory_level
})
});
const data = await res.json();
if (res.ok) {
loadChildren();
alert("Child added!");
nameInput.value = '';
ageInput.value = '';
} else {
console.error("Server error:", data);
alert(data.detail || "Error adding child profile.");
}
} catch (err) {
console.error("Network error:", err);
alert("Could not connect to server.");
}
}
async function loadChildren() {
const res = await fetch(`/auth/children/${userId}`);
const children = await res.json();
const list = document.getElementById('child-list');
if (!list) return;
list.innerHTML = '';
if (children.length === 0) {
list.innerHTML = '
No child profiles found. Add one to get started!
';
return;
}
children.forEach(c => {
const div = document.createElement('div');
div.className = 'card child-card';
div.innerHTML = `
${c.name}
Age: ${c.age} | Level: ${c.level || 1} | Sensory: ${c.sensory_level}
Select
Delete
`;
list.appendChild(div);
});
}
async function deleteChild(id) {
if (!confirm("Are you sure you want to delete this child profile? All progress will be lost.")) return;
const res = await fetch(`/auth/delete-child/${id}`, { method: 'DELETE' });
if (res.ok) {
alert("Child profile deleted.");
if (selectedChildId == id) {
localStorage.removeItem('selectedChildId');
localStorage.removeItem('selectedChildAge');
localStorage.removeItem('selectedChildSensory');
localStorage.removeItem('selectedChildLevel');
selectedChildId = null;
}
loadChildren();
} else {
const data = await res.json();
alert(data.detail || "Error deleting child.");
}
}
function selectChild(id, name, age, sensory, level) {
localStorage.setItem('selectedChildId', id);
localStorage.setItem('selectedChildAge', age);
localStorage.setItem('selectedChildSensory', sensory);
localStorage.setItem('selectedChildLevel', level || 1);
selectedChildId = id;
alert(`Child profile selected: ${name} (Age: ${age}, Level: ${level || 1})`);
applySensoryUI(sensory);
applyLevelUI(age); // Note: age still used for some legacy UI logic if any
window.location.href = '/dashboard';
}
function applySensoryUI(level) {
if (!level) level = localStorage.getItem('selectedChildSensory') || 'standard';
console.log("Applying UI for sensory level:", level);
if (level === 'high') {
document.body.classList.add('calm-theme');
} else {
document.body.classList.remove('calm-theme');
}
}
function applyLevelUI(age) {
// Reverted to normal UI for all levels as requested.
// No specific theme classes added here.
document.body.classList.remove('level-2-theme', 'level-3-theme');
}
// Real-time Emotion Tracker Logic
let videoStream = null;
let captureInterval = null;
let emotionHistory = []; // Buffer for smoothing results
const SMOOTHING_WINDOW = 5; // Average over last 5 frames
async function startCamera() {
const display = document.getElementById('live-result');
const video = document.getElementById('webcam');
console.log("Attempting to start camera...");
console.log("Hostname:", window.location.hostname);
console.log("Secure Context:", window.isSecureContext);
if (!selectedChildId) {
alert("Please select a child profile first on the Child page!");
return;
}
// 1. Check for Secure Context (HTTPS or localhost)
if (!window.isSecureContext && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
const errorMsg = `โ Camera BLOCKED. Your browser requires HTTPS for camera access unless you are using 'localhost'. (Current: ${window.location.hostname})`;
console.error(errorMsg);
if (display) display.innerHTML = `${errorMsg} `;
alert(errorMsg);
return;
}
// 2. Check for MediaDevices support
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
const errorMsg = "โ Camera API not supported in this browser. Try Chrome or Edge.";
console.error(errorMsg);
if (display) display.innerHTML = `${errorMsg} `;
alert(errorMsg);
return;
}
// Stop any existing camera session
stopCamera();
emotionHistory = [];
if (!video) {
console.error("Video element 'webcam' not found in DOM.");
return;
}
if (display) display.innerText = "โ Requesting camera permission...";
try {
console.log("Calling getUserMedia...");
const constraints = {
video: {
facingMode: "user",
width: { ideal: 640 },
height: { ideal: 480 }
}
};
videoStream = await navigator.mediaDevices.getUserMedia(constraints);
console.log("Camera stream obtained.");
video.srcObject = videoStream;
// Ensure video plays
video.onloadedmetadata = () => {
console.log("Video metadata loaded, playing...");
video.play()
.then(() => {
console.log("Video playing successfully.");
if (display) display.innerHTML = 'โ
Camera Active! ';
})
.catch(e => {
console.error("Error playing video:", e);
if (display) display.innerHTML = `Error playing video: ${e.message} `;
});
};
// Start capturing frames every 1.5 seconds
captureInterval = setInterval(captureFrame, 1500);
} catch (err) {
console.error("Camera start error:", err);
let msg = "โ Camera Error.";
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError')
msg = "โ Permission Denied. Please allow camera access in your browser settings.";
else if (err.name === 'NotFoundError' || err.name === 'DevicesNotFoundError')
msg = "โ No camera found on this device.";
else if (err.name === 'NotReadableError' || err.name === 'TrackStartError')
msg = "โ Camera is already in use by another app.";
else
msg = `โ Error: ${err.name} - ${err.message}`;
if (display) display.innerHTML = `${msg} `;
alert(msg);
}
}
function stopCamera() {
console.log("Stopping camera...");
if (captureInterval) {
clearInterval(captureInterval);
captureInterval = null;
}
if (videoStream) {
videoStream.getTracks().forEach(track => {
track.stop();
});
videoStream = null;
}
const video = document.getElementById('webcam');
if (video) {
video.srcObject = null;
}
const display = document.getElementById('live-result');
if (display) display.innerText = "Camera Stopped";
const overlay = document.getElementById('live-overlay');
if (overlay) overlay.innerText = "Ready...";
emotionHistory = [];
}
// Automatically stop camera if user leaves the page
window.addEventListener('beforeunload', stopCamera);
window.addEventListener('popstate', stopCamera);
async function captureFrame() {
const video = document.getElementById('webcam');
if (!video || !video.srcObject || !videoStream) {
return;
}
if (video.paused || video.ended) return;
console.log("Capturing frame for analysis...");
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
if (canvas.width === 0 || canvas.height === 0) return;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const frameData = canvas.toDataURL('image/jpeg', 0.6); // Lower quality for speed
const formData = new FormData();
formData.append('child_id', selectedChildId);
formData.append('frame_data', frameData);
try {
const res = await fetch('/emotion/process-frame', {
method: 'POST',
body: formData
});
if (!res.ok) {
console.error("Frame processing failed on server:", res.status);
return;
}
const data = await res.json();
if (data.emotion) {
console.log("Detected emotion:", data.emotion);
// Add to history for smoothing
emotionHistory.push(data.emotion);
if (emotionHistory.length > SMOOTHING_WINDOW) {
emotionHistory.shift();
}
// Get the most frequent emotion in the window
const counts = {};
emotionHistory.forEach(e => counts[e] = (counts[e] || 0) + 1);
const stableEmotion = Object.keys(counts).reduce((a, b) => counts[a] > counts[b] ? a : b);
const overlay = document.getElementById('live-overlay');
const display = document.getElementById('live-result');
const emotion = stableEmotion.toUpperCase();
if (overlay) {
overlay.innerText = emotion;
const colors = {
'HAPPY': '#4CAF50', 'SAD': '#2196F3', 'ANGRY': '#F44336',
'SURPRISE': '#FFEB3B', 'NEUTRAL': '#9E9E9E', 'FEAR': '#9C27B0', 'DISGUST': '#795548'
};
overlay.style.borderColor = colors[emotion] || '#4a90e2';
overlay.style.background = (emotion === 'SURPRISE') ? 'rgba(255,235,59,0.9)' : 'rgba(0,0,0,0.7)';
overlay.style.color = (emotion === 'SURPRISE') ? '#000' : '#fff';
}
if (display) {
display.innerHTML = `LIVE: ${emotion} `;
}
}
} catch (err) {
console.error("Frame processing error:", err);
}
}
async function capturePhoto() {
console.log("Capturing photo...");
if (!selectedChildId) return alert("Select child first!");
const video = document.getElementById('webcam');
if (!video || !video.srcObject) return alert("Start camera first!");
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/jpeg'));
const formData = new FormData();
formData.append('file', blob, 'capture.jpg');
formData.append('child_id', selectedChildId);
try {
const res = await fetch('/emotion/upload', {
method: 'POST',
body: formData
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.detail || "Upload failed");
}
const data = await res.json();
currentEmotionLogId = data.id;
document.getElementById('predicted-emotion').innerText = data.predicted_emotion.toUpperCase();
document.getElementById('result-img').src = data.image_url;
document.getElementById('emotion-result').classList.remove('hidden');
} catch (err) {
console.error("Capture upload error:", err);
alert("Upload error: " + err.message);
}
}
// Emotion Module (Legacy Upload)
async function loadCustomEmotions() {
try {
const res = await fetch('/emotion/unique-emotions');
if (!res.ok) return;
const emotions = await res.json();
const select = document.getElementById('corrected-emotion');
if (!select) return;
// Save current "other" option
const otherOption = select.querySelector('option[value="other"]');
select.innerHTML = '';
emotions.forEach(emo => {
const opt = document.createElement('option');
opt.value = emo;
opt.innerText = emo.charAt(0).toUpperCase() + emo.slice(1);
select.appendChild(opt);
});
if (otherOption) select.appendChild(otherOption);
} catch (err) {
console.error("Failed to load custom emotions:", err);
}
}
async function uploadEmotion() {
console.log("Uploading emotion image...");
if (!selectedChildId) return alert("Please select a child profile first!");
const fileInput = document.getElementById('emotion-upload');
if (!fileInput || fileInput.files.length === 0) return alert("Select an image first.");
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('child_id', selectedChildId);
try {
const res = await fetch('/emotion/upload', {
method: 'POST',
body: formData
});
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.detail || "Upload failed");
}
const data = await res.json();
currentEmotionLogId = data.id;
document.getElementById('predicted-emotion').innerText = data.predicted_emotion.toUpperCase();
document.getElementById('result-img').src = data.image_url;
document.getElementById('emotion-result').classList.remove('hidden');
} catch (err) {
console.error("Upload error:", err);
alert("Upload error: " + err.message);
}
}
async function confirmEmotion(confirmed) {
if (confirmed) {
await fetch('/emotion/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `log_id=${currentEmotionLogId}&confirmed=true`
});
alert("Confirmed! Thank you.");
const resDiv = document.getElementById('emotion-result');
if (resDiv) resDiv.classList.add('hidden');
} else {
const corrArea = document.getElementById('correction-area');
if (corrArea) corrArea.classList.remove('hidden');
}
}
function toggleCustomEmotion() {
const select = document.getElementById('corrected-emotion');
const customInput = document.getElementById('custom-emotion-name');
if (select.value === 'other') {
customInput.classList.remove('hidden');
} else {
customInput.classList.add('hidden');
}
}
async function saveCorrection() {
const select = document.getElementById('corrected-emotion');
const customInput = document.getElementById('custom-emotion-name');
let corrected = select.value;
if (corrected === 'other') {
corrected = customInput.value.toLowerCase().trim();
if (!corrected) return alert("Please type a new emotion name.");
}
await fetch('/emotion/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `log_id=${currentEmotionLogId}&confirmed=true&corrected_emotion=${corrected}`
});
alert(`Learned! This image is now marked as: ${corrected}`);
const resDiv = document.getElementById('emotion-result');
if (resDiv) resDiv.classList.add('hidden');
// Refresh the list to include the new emotion
loadCustomEmotions();
if (customInput) customInput.value = '';
if (select) select.value = 'happy';
toggleCustomEmotion();
}
async function trainAI() {
const status = document.getElementById('train-status');
if (status) status.innerText = "Training in progress... please wait.";
try {
const res = await fetch('/emotion/train', { method: 'POST' });
const data = await res.json();
if (res.ok) {
alert(data.message);
if (status) status.innerText = "Last training: " + data.message;
} else {
alert("Training failed: " + data.error);
if (status) status.innerText = "Training failed.";
}
} catch (err) {
console.error("Training error:", err);
alert("Error connecting to server.");
}
}
// Activities and Games Logic
let activeGame = "";
let gameScore = 0;
let startTime = 0;
function startGame(name) {
if (!selectedChildId) return alert("Select child first.");
name = (name || "").trim();
console.log("Starting game:", name);
activeGame = name;
gameScore = 0;
startTime = Date.now();
const gameArea = document.getElementById('game-area');
if (gameArea) gameArea.classList.remove('hidden');
const title = document.getElementById('current-game-title');
if (title) {
title.innerText = name;
title.style.display = 'block';
}
const controls = document.getElementById('game-controls');
if (controls) controls.classList.remove('hidden');
const scoreDisplay = document.getElementById('game-score');
if (scoreDisplay) scoreDisplay.innerText = gameScore;
const closeBtn = document.getElementById('close-game-btn');
if (closeBtn) closeBtn.classList.add('hidden');
const container = document.getElementById('game-container');
if (!container) return console.error("Game container not found!");
container.innerHTML = '';
const lowerName = name.toLowerCase();
if (lowerName === 'color match' || lowerName === 'learn colors' || lowerName === 'learn about colors') startColorMatch(container);
else if (lowerName === 'memory game') startMemoryGame(container);
else if (lowerName === 'coloring book' || lowerName === 'online coloring' || lowerName === 'online coloring game') startColoringBook(container);
else if (lowerName === 'animal coloring' || lowerName === 'animal coloring game') startAnimalColoring(container);
else if (lowerName === 'pattern match' || lowerName === 'patternmatch') startPatternMatch(container);
else if (lowerName === 'mood matcher' || lowerName === 'moodmatcher') startMoodMatch(container);
else if (lowerName === 'learn emotions' || lowerName === 'learn about emotions') startLearnEmotions(container);
else if (lowerName === 'shape match' || lowerName === 'learn shapes' || lowerName === 'learn about shapes') startShapeMatch(container);
else if (lowerName === 'alphabet trace') startAlphabetTrace(container);
else if (lowerName === 'number write') startNumberWrite(container);
else if (lowerName === 'alphabet memory') startAlphabetMemory(container);
else if (lowerName === 'word match') startWordMatch(container);
else if (lowerName === 'halves match') startHalvesMatch(container);
else if (lowerName === 'alphabet sort') startAbcSort(container);
else if (lowerName === 'alphabet order') startAlphabetOrder(container);
else if (lowerName === 'missing letter') startMissingLetter(container);
else if (lowerName === 'word picture match') startWordPictureMatch(container);
else if (lowerName === 'object search') startObjectSearch(container);
else if (lowerName === 'spatial puzzle') startSpatialPuzzle(container);
else if (lowerName === 'jigsaw puzzle') startJigsawPuzzle(container);
else if (lowerName === 'advanced patterns') startAdvancedPatterns(container);
else {
console.error("Unknown game:", name);
container.innerHTML = `Oops! Game "${name}" not found. Reload Page `;
}
}
window.startGame = startGame;
// --- New Level 2 Game Functions ---
function startAlphabetTrace(container) {
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
let currentIdx = 0;
window.renderTraceLetter = (idx) => {
const char = letters[idx];
container.innerHTML = `
Trace the Letter: ${char}
๐ Reset
โก๏ธ Next Letter
โ
Done
`;
initTracing();
};
window.nextTraceLetter = () => {
gameScore += 10;
document.getElementById('game-score').innerText = gameScore;
currentIdx = (currentIdx + 1) % letters.length;
renderTraceLetter(currentIdx);
};
function initTracing() {
const canvas = document.getElementById('trace-canvas');
const ctx = canvas.getContext('2d');
let drawing = false;
ctx.strokeStyle = '#8d6e63';
ctx.lineWidth = 15;
ctx.lineCap = 'round';
ctx.font = '250px Nunito';
ctx.fillStyle = '#f0f0f0';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(letters[currentIdx], 150, 160);
const startDraw = (e) => { drawing = true; draw(e); };
const endDraw = () => { drawing = false; ctx.beginPath(); };
const draw = (e) => {
if (!drawing) return;
const rect = canvas.getBoundingClientRect();
const x = (e.clientX || e.touches[0].clientX) - rect.left;
const y = (e.clientY || e.touches[0].clientY) - rect.top;
ctx.lineTo(x, y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x, y);
};
canvas.addEventListener('mousedown', startDraw);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', endDraw);
canvas.addEventListener('touchstart', startDraw);
canvas.addEventListener('touchmove', draw);
canvas.addEventListener('touchend', endDraw);
}
renderTraceLetter(currentIdx);
}
function startNumberWrite(container) {
const numbers = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];
let currentIdx = 0;
window.renderTraceNumber = (idx) => {
const num = numbers[idx];
container.innerHTML = `
Write the Number: ${num}
๐ Reset
โก๏ธ Next Number
โ
Done
`;
initTracing();
};
window.nextTraceNumber = () => {
gameScore += 10;
document.getElementById('game-score').innerText = gameScore;
currentIdx = (currentIdx + 1) % numbers.length;
renderTraceNumber(currentIdx);
};
function initTracing() {
const canvas = document.getElementById('trace-canvas');
const ctx = canvas.getContext('2d');
let drawing = false;
ctx.strokeStyle = '#8d6e63';
ctx.lineWidth = 15;
ctx.lineCap = 'round';
ctx.font = '250px Nunito';
ctx.fillStyle = '#f0f0f0';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(numbers[currentIdx], 150, 160);
const startDraw = (e) => { drawing = true; draw(e); };
const endDraw = () => { drawing = false; ctx.beginPath(); };
const draw = (e) => {
if (!drawing) return;
const rect = canvas.getBoundingClientRect();
const x = (e.clientX || e.touches[0].clientX) - rect.left;
const y = (e.clientY || e.touches[0].clientY) - rect.top;
ctx.lineTo(x, y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x, y);
};
canvas.addEventListener('mousedown', startDraw);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', endDraw);
canvas.addEventListener('touchstart', startDraw);
canvas.addEventListener('touchmove', draw);
canvas.addEventListener('touchend', endDraw);
}
renderTraceNumber(currentIdx);
}
function startAlphabetMemory(container) {
const letters = [
{ u: 'A', l: 'a' }, { u: 'B', l: 'b' }, { u: 'C', l: 'c' }, { u: 'D', l: 'd' },
{ u: 'E', l: 'e' }, { u: 'F', l: 'f' }
];
let cards = [];
letters.forEach(pair => {
cards.push({ val: pair.u, match: pair.l, type: 'upper' });
cards.push({ val: pair.l, match: pair.u, type: 'lower' });
});
cards.sort(() => Math.random() - 0.5);
let firstCard = null;
let secondCard = null;
let lockBoard = false;
container.innerHTML = `
Match Upper & Lower Case!
${cards.map((card, i) => `
${card.val}
`).join('')}
๐ Reset
โ
Done
`;
window.flipAbcCard = (i) => {
if (lockBoard) return;
const cardEl = document.getElementById(`abc-card-${i}`);
if (cardEl.classList.contains('matched') || cardEl === firstCard) return;
cardEl.style.color = 'var(--text)';
cardEl.style.background = 'white';
if (!firstCard) {
firstCard = cardEl;
return;
}
secondCard = cardEl;
lockBoard = true;
const val1 = firstCard.innerText.trim();
const val2 = secondCard.innerText.trim();
const isMatch = (val1.toUpperCase() === val2.toUpperCase() && val1 !== val2);
if (isMatch) {
firstCard.classList.add('matched');
secondCard.classList.add('matched');
firstCard.style.background = '#d7ccc8';
secondCard.style.background = '#d7ccc8';
gameScore += 20;
document.getElementById('game-score').innerText = gameScore;
resetBoard();
} else {
setTimeout(() => {
firstCard.style.color = 'transparent';
firstCard.style.background = 'var(--secondary)';
secondCard.style.color = 'transparent';
secondCard.style.background = 'var(--secondary)';
resetBoard();
}, 1000);
}
};
function resetBoard() {
firstCard = null;
secondCard = null;
lockBoard = false;
}
}
function startWordMatch(container) {
const wordPairs = [
{ word: 'Apple', icon: '๐' },
{ word: 'Dog', icon: '๐ถ' },
{ word: 'Sun', icon: 'โ๏ธ' },
{ word: 'Book', icon: '๐' }
];
let currentPair = wordPairs[Math.floor(Math.random() * wordPairs.length)];
window.renderWordMatch = () => {
currentPair = wordPairs[Math.floor(Math.random() * wordPairs.length)];
const options = [...wordPairs].sort(() => Math.random() - 0.5);
container.innerHTML = `
Which picture matches the word?
๐ ${currentPair.word.toUpperCase()}
${options.map(opt => `
${opt.icon}
`).join('')}
๐ Next
โ
Done
`;
};
window.speakWord = (word) => {
const msg = new SpeechSynthesisUtterance();
msg.text = word;
window.speechSynthesis.speak(msg);
};
window.checkWordMatch = (selected, target) => {
const feedback = document.getElementById('feedback');
if (selected === target) {
feedback.innerText = "Correct! ๐";
feedback.style.color = "var(--success)";
gameScore += 15;
document.getElementById('game-score').innerText = gameScore;
setTimeout(renderWordMatch, 1500);
} else {
feedback.innerText = "Try again! ๐";
feedback.style.color = "#e74c3c";
}
};
renderWordMatch();
}
function startHalvesMatch(container) {
const objects = [
{ icon: '๐', left: '๐', right: '๐' }, // In real app, these would be split images
{ icon: '๐ถ', left: '๐ถ', right: '๐ถ' },
{ icon: '๐', left: '๐', right: '๐' },
{ icon: '๐ ', left: '๐ ', right: '๐ ' }
];
window.renderHalves = () => {
const target = objects[Math.floor(Math.random() * objects.length)];
const options = [...objects].sort(() => Math.random() - 0.5);
container.innerHTML = `
Find the matching half!
${options.map(opt => `
${opt.icon}
`).join('')}
๐ Next
โ
Done
`;
};
window.checkHalvesMatch = (selected, target) => {
const feedback = document.getElementById('feedback');
if (selected === target) {
feedback.innerText = "You completed it! ๐";
feedback.style.color = "var(--success)";
gameScore += 15;
document.getElementById('game-score').innerText = gameScore;
setTimeout(renderHalves, 1500);
} else {
feedback.innerText = "Keep looking! ๐";
feedback.style.color = "#e74c3c";
}
};
renderHalves();
}
// --- Game Functions ---
function startAnimalColoring(container) {
console.log("Starting Animal Coloring selection...");
if (!animalTemplates || Object.keys(animalTemplates).length === 0) {
console.error("No animal templates found!");
container.innerHTML = "Sorry, no animals found! Please refresh the page. ";
return;
}
let animalCards = Object.keys(animalTemplates).map(name => {
let icon = '๐พ';
if (name === 'Dog') icon = '๐ถ';
else if (name === 'Cat') icon = '๐ฑ';
else if (name === 'Lion') icon = '๐ฆ';
else if (name === 'Bunny') icon = '๐ฐ';
else if (name === 'Fish') icon = '๐ ';
return `
`;
}).join('');
container.innerHTML = `
Pick an animal friend to color!
${animalCards}
Go Back
`;
}
function selectAnimalTemplate(name) {
const container = document.getElementById('game-container');
const templateSvg = animalTemplates[name];
const referenceSvg = templateSvg.replace(/data-ref-fill="([^"]+)"/g, 'fill="$1"');
const drawingSvg = templateSvg.replace(/data-ref-fill="([^"]+)"/g, (match, p1) => {
if (p1 === 'none') return 'fill="none"';
return 'fill="#fff" class="block-to-color" onclick="window.colorBlock(this)"';
});
const palette = [
'#FF5252', '#FF4081', '#E040FB', '#7C4DFF',
'#536DFE', '#448AFF', '#40C4FF', '#18FFFF',
'#64FFDA', '#69F0AE', '#B2FF59', '#EEFF41',
'#FFFF00', '#FFD740', '#FFAB40', '#FF6E40',
'#8D6E63', '#9E9E9E', '#CFD8DC', '#000000', '#FFFFFF'
];
container.innerHTML = `
Look at this!
${referenceSvg}
${drawingSvg}
Step 1: Click a color โฌ๏ธ | Step 2: Click the animal ๐จ
${palette.map(c => `
`).join('')}
๐ Different Animal
๐ Start Over
โ
I'm Finished!
`;
const firstSwatch = document.querySelector('.color-swatch');
if (firstSwatch) window.selectPaletteColor(palette[0], firstSwatch);
}
function startLearnEmotions(container) {
const basicEmotions = [
{ name: 'Happy', emoji: '๐', color: '#f1c40f' },
{ name: 'Sad', emoji: '๐ข', color: '#3498db' },
{ name: 'Angry', emoji: '๐ ', color: '#e74c3c' },
{ name: 'Silly', emoji: '๐', color: '#9b59b6' }
];
const target = basicEmotions[Math.floor(Math.random() * basicEmotions.length)];
const options = [...basicEmotions].sort(() => 0.5 - Math.random());
container.innerHTML = `
Can you find the ${target.name.toUpperCase()} face?
${target.emoji}
${options.map(m => `
${m.name}
`).join('')}
๐ Back
๐ Next
โ
I'm Done!
`;
}
window.checkBasicEmotion = function(selected, target) {
const feedback = document.getElementById('feedback');
if (selected === target) {
feedback.innerText = "Yay! You got it! ๐";
feedback.style.color = "#2ecc71";
gameScore += 10;
document.getElementById('game-score').innerText = gameScore;
setTimeout(() => startLearnEmotions(document.getElementById('game-container')), 1500);
} else {
feedback.innerText = "Not quite, try again! ๐";
feedback.style.color = "#e74c3c";
}
};
function startShapeMatch(container) {
const shapes = [
{ name: 'Circle', icon: '๐ด' },
{ name: 'Square', icon: '๐ง' },
{ name: 'Triangle', icon: '๐บ' },
{ name: 'Star', icon: 'โญ' },
{ name: 'Heart', icon: 'โค๏ธ' },
{ name: 'Moon', icon: '๐' }
];
const target = shapes[Math.floor(Math.random() * shapes.length)];
const options = [...shapes].sort(() => Math.random() - 0.5);
container.innerHTML = `
Can you find the ${target.name.toUpperCase()} ?
${options.map(s => `
${s.icon}
`).join('')}
๐ Back
๐ Shuffle
โ
All Done!
`;
}
window.checkShape = function(selected, target) {
const feedback = document.getElementById('feedback');
if (selected === target) {
feedback.innerText = "Correct! You found the " + target + "! ๐";
feedback.style.color = "#2ecc71";
gameScore += 10;
document.getElementById('game-score').innerText = gameScore;
setTimeout(() => startShapeMatch(document.getElementById('game-container')), 1200);
} else {
feedback.innerText = "Not that one. Try again! ๐ค";
feedback.style.color = "#e74c3c";
}
};
function startPatternMatch(container) {
const emojiSets = [
['๐', '๐'], ['๐ถ', '๐ฑ'], ['โ๏ธ', '๐'], ['๐', '๐ฒ'], ['๐', '๐'], ['๐ฆ', '๐ฐ']
];
const set = emojiSets[Math.floor(Math.random() * emojiSets.length)];
const pattern = [set[0], set[1], set[0], set[1]];
const target = set[0];
const options = [...set].sort(() => Math.random() - 0.5);
container.innerHTML = `
What comes next in the pattern?
${pattern[0]}
โก๏ธ
${pattern[1]}
โก๏ธ
${pattern[2]}
โก๏ธ
${pattern[3]}
โก๏ธ
?
${options.map(emoji => `
${emoji}
`).join('')}
๐ Back
๐ New Pattern
โ
I'm Done!
`;
}
window.checkPattern = function(selected, target) {
const feedback = document.getElementById('feedback');
if (selected === target) {
feedback.innerText = "Wow! You're so smart! ๐";
feedback.style.color = "#2ecc71";
gameScore += 10;
document.getElementById('game-score').innerText = gameScore;
const placeholders = document.querySelectorAll('#game-container div');
placeholders.forEach(div => {
if (div.innerText === '?') {
div.innerText = target;
div.style.color = '#000';
div.style.borderColor = '#2ecc71';
}
});
setTimeout(() => startPatternMatch(document.getElementById('game-container')), 1500);
} else {
feedback.innerText = "Not quite, try looking at the sequence again! ๐";
feedback.style.color = "#e74c3c";
}
};
function startColorMatch(container) {
const colors = [
{ name: 'Red', color: '#e74c3c' }, { name: 'Blue', color: '#3498db' }, { name: 'Green', color: '#2ecc71' },
{ name: 'Yellow', color: '#f1c40f' }, { name: 'Purple', color: '#9b59b6' }, { name: 'Orange', color: '#e67e22' }
];
const target = colors[Math.floor(Math.random() * colors.length)];
container.innerHTML = `
Pick the ${target.name.toUpperCase()} block!
${colors.map(c => `
`).join('')}
๐ Back
๐ Shuffle
โ
All Done!
`;
}
window.checkColor = function(selected, target) {
const feedback = document.getElementById('feedback');
if (selected === target) {
feedback.innerText = "Correct! You're a Color Master! ๐จ";
feedback.style.color = "#2ecc71";
gameScore += 10;
document.getElementById('game-score').innerText = gameScore;
setTimeout(() => startColorMatch(document.getElementById('game-container')), 1200);
} else {
feedback.innerText = "Oops! Not that one. Try again!";
feedback.style.color = "#e74c3c";
}
};
let flippedCards = [];
function startMemoryGame(container) {
const items = ['๐', '๐ข', '๐ถ', '๐ฑ'];
const bonus = 'โญ';
let deck = [...items, ...items, bonus];
deck.sort(() => Math.random() - 0.5);
const cardGradients = [
'linear-gradient(135deg, #ff7eb3, #ff758c)', 'linear-gradient(135deg, #4facfe, #00f2fe)',
'linear-gradient(135deg, #43e97b, #38f9d7)', 'linear-gradient(135deg, #fa709a, #fee140)',
'linear-gradient(135deg, #667eea, #764ba2)', 'linear-gradient(135deg, #f093fb, #f5576c)',
'linear-gradient(135deg, #5ee7df, #b490ca)', 'linear-gradient(135deg, #c3cfe2, #c3cfe2)',
'linear-gradient(135deg, #f6d365, #fda085)'
];
flippedCards = [];
container.innerHTML = `
Find the 4 pairs + The Magic Star!
${deck.map((item, index) => `
${item}
`).join('')}
๐ Back
๐ Shuffle
โ
All Done!
`;
}
window.flipCard = function(index, item) {
const card = document.getElementById(`card-${index}`);
const content = card.querySelector('.content');
const feedback = document.getElementById('feedback');
if (card.classList.contains('matched') || !content.classList.contains('hidden') || flippedCards.length >= 2) return;
if (item === 'โญ') {
content.classList.remove('hidden');
card.style.background = 'linear-gradient(135deg, #fff95b, #ff930f)';
card.classList.add('matched');
feedback.innerText = "You found the Magic Star! +50 Points! ๐";
feedback.style.color = "#f39c12";
gameScore += 50;
document.getElementById('game-score').innerText = gameScore;
return;
}
content.classList.remove('hidden');
card.style.background = '#ffffff';
flippedCards.push({ index, item });
if (flippedCards.length === 2) setTimeout(checkMatch, 800);
};
function checkMatch() {
const [c1, c2] = flippedCards;
const card1 = document.getElementById(`card-${c1.index}`);
const card2 = document.getElementById(`card-${c2.index}`);
const feedback = document.getElementById('feedback');
if (c1.item === c2.item) {
card1.classList.add('matched');
card2.classList.add('matched');
card1.style.background = 'linear-gradient(135deg, #43e97b, #38f9d7)';
card2.style.background = 'linear-gradient(135deg, #43e97b, #38f9d7)';
feedback.innerText = "Match found! ๐";
feedback.style.color = "#2ecc71";
gameScore += 20;
} else {
card1.querySelector('.content').classList.add('hidden');
card2.querySelector('.content').classList.add('hidden');
card1.style.background = '';
card2.style.background = '';
feedback.innerText = "Not a match, try again!";
feedback.style.color = "#e74c3c";
}
document.getElementById('game-score').innerText = gameScore;
flippedCards = [];
}
let selectedBrushColor = '#f1c40f';
function startColoringBook(container) {
container.innerHTML = `
Choose a picture to color!
${Object.keys(coloringTemplates).map(name => `
${name === 'House' ? '๐ ' : name === 'Flower' ? '๐ธ' : '๐'}
${name}
`).join('')}
`;
}
window.selectTemplate = function(name) {
const container = document.getElementById('game-container');
const templateSvg = coloringTemplates[name];
const referenceSvg = templateSvg.replace(/data-ref-fill="([^"]+)"/g, 'fill="$1"');
const drawingSvg = templateSvg.replace(/data-ref-fill="([^"]+)"/g, 'fill="#fff" class="block-to-color" onclick="window.colorBlock(this)"');
container.innerHTML = `
Reference
${referenceSvg}
${drawingSvg}
${['#f1c40f', '#f39c12', '#e74c3c', '#34495e', '#95a5a6', '#2ecc71', '#3498db', '#ecf0f1', '#333'].map(c => `
`).join('')}
๐ Back
๐ Reset
โก๏ธ Next
โ
Done
`;
const firstSwatch = document.querySelector('.color-swatch');
if (firstSwatch) window.selectPaletteColor('#f1c40f', firstSwatch);
};
window.selectPaletteColor = function(color, element) {
selectedBrushColor = color;
document.querySelectorAll('.color-swatch').forEach(s => s.style.border = 'none');
element.style.border = '3px solid #333';
};
window.colorBlock = function(element) {
element.setAttribute('fill', selectedBrushColor);
gameScore += 5;
document.getElementById('game-score').innerText = gameScore;
};
window.nextImage = function(currentName) {
const keys = Object.keys(coloringTemplates);
const currentIndex = keys.indexOf(currentName);
const nextIndex = (currentIndex + 1) % keys.length;
window.selectTemplate(keys[nextIndex]);
};
function closeGame() {
window.location.href = '/activities';
}
function restartGame() {
startGame(activeGame);
}
async function finishGame() {
const duration = Math.floor((Date.now() - startTime) / 1000);
const res = await fetch('/activities/log-activity', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
child_id: selectedChildId,
activity_name: activeGame,
score: gameScore,
duration_seconds: duration
})
});
if (res.ok) {
alert(`Great Job! You scored ${gameScore} points! ๐`);
window.location.href = '/activities';
}
}
let currentQuizType = "";
let currentQuestionIndex = 0;
let quizScore = 0;
function startNewQuiz(type) {
if (!selectedChildId) return alert("Please select a child profile first.");
currentQuizType = type;
currentQuestionIndex = 0;
quizScore = 0;
document.getElementById('quiz-selection').classList.add('hidden');
document.getElementById('quiz-area').classList.remove('hidden');
document.getElementById('quiz-content').classList.remove('hidden');
document.getElementById('quiz-results').classList.add('hidden');
document.getElementById('current-quiz-name').innerText = type;
loadQuizQuestion();
}
function loadQuizQuestion() {
const questions = quizData[currentQuizType];
const q = questions[currentQuestionIndex];
document.getElementById('question-counter').innerText = `Question ${currentQuestionIndex + 1} of ${questions.length}`;
document.getElementById('quiz-question-text').innerText = q.question;
const progress = (currentQuestionIndex / questions.length) * 100;
document.getElementById('progress-fill').style.width = `${progress}%`;
// Reset feedback
const feedback = document.getElementById('quiz-feedback');
if (feedback) feedback.innerText = "";
const optionsGrid = document.getElementById('quiz-options-grid');
optionsGrid.innerHTML = '';
q.options.forEach(opt => {
const div = document.createElement('div');
div.className = 'card quiz-option';
div.innerHTML = `${opt.text.split(' ').pop()} ${opt.text.split(' ').slice(0, -1).join(' ')}
`;
div.onclick = () => window.selectQuizOption(opt.correct, div);
optionsGrid.appendChild(div);
});
}
window.selectQuizOption = function(isCorrect, element) {
const feedback = document.getElementById('quiz-feedback');
if (isCorrect) {
quizScore++;
if (feedback) {
feedback.innerText = "Correct! Great thinking! ๐";
feedback.style.color = "var(--success)";
}
element.style.borderColor = "var(--success)";
element.style.background = "#e8f5e9";
// Disable all options during the transition
document.querySelectorAll('.quiz-option').forEach(opt => opt.onclick = null);
setTimeout(() => {
currentQuestionIndex++;
if (currentQuestionIndex < quizData[currentQuizType].length) {
loadQuizQuestion();
} else {
finishQuiz();
}
}, 1500);
} else {
if (feedback) {
feedback.innerText = "Not quite. Let's try again! ๐";
feedback.style.color = "#e74c3c";
}
element.style.borderColor = "#e74c3c";
element.style.opacity = "0.7";
// Do NOT increment index, allowing retry
}
};
async function finishQuiz() {
const total = quizData[currentQuizType].length;
// Every question was eventually answered correctly
const finalScore = total;
await fetch('/activities/log-quiz', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
child_id: selectedChildId,
quiz_name: currentQuizType,
score: finalScore,
total_questions: total
})
});
document.getElementById('progress-fill').style.width = `100%`;
document.getElementById('quiz-content').classList.add('hidden');
document.getElementById('quiz-results').classList.remove('hidden');
document.getElementById('final-score-text').innerText = `Amazing! You completed all ${total} questions! ๐`;
}
async function loadChildrenForDashboard() {
if (!userId) return;
const res = await fetch(`/auth/children/${userId}`);
const children = await res.json();
const select = document.getElementById('dashboard-child-select');
if (!select) return;
select.innerHTML = 'Select Child ';
children.forEach(c => {
const isSelected = selectedChildId == c.id ? 'selected' : '';
select.innerHTML += `${c.name} `;
});
if (selectedChildId) loadProgress();
}
function getLevelInfo(child) {
console.log("getLevelInfo called with:", child);
let age = 0;
if (typeof child === 'number') {
age = child;
} else if (child && typeof child === 'object') {
age = parseInt(child.age) || 0;
} else {
// Try getting from localStorage as fallback
age = parseInt(localStorage.getItem('selectedChildAge')) || 0;
}
console.log("Determined age for level check:", age);
let level = 1;
if (age >= 10) level = 3;
else if (age >= 7) level = 2;
else level = 1;
console.log("Calculated level:", level);
if (level === 3) return { level: 3, name: "Level 3: Advanced (10+)", desc: "Complex emotions and mood analysis.", theme: "purple" };
if (level === 2) return { level: 2, name: "Level 2: Intermediate (7-9)", desc: "Patterns and alphabetical logic.", theme: "sand" };
return { level: 1, name: "Level 1: Basic (3-6)", desc: "Colors, shapes, and basic emotions.", theme: "orange" };
}
async function syncChildAge() {
const childId = localStorage.getItem('selectedChildId');
if (!childId || !userId) return;
try {
const res = await fetch(`/auth/children/${userId}`);
const children = await res.json();
const child = children.find(c => c.id == childId);
if (child) {
localStorage.setItem('selectedChildAge', child.age);
localStorage.setItem('selectedChildSensory', child.sensory_level);
applySensoryUI(child.sensory_level);
}
} catch (err) { console.error("Failed to sync child age:", err); }
}
async function loadProgress() {
const select = document.getElementById('dashboard-child-select');
if (!select) return;
const childId = select.value;
if (!childId) return;
localStorage.setItem('selectedChildId', childId);
const resChild = await fetch(`/auth/children/${userId}`);
const children = await resChild.json();
const child = children.find(c => c.id == childId);
if (child) {
localStorage.setItem('selectedChildAge', child.age);
localStorage.setItem('selectedChildLevel', child.level || 1);
const levelInfo = getLevelInfo(child);
const levelDisplay = document.getElementById('dashboard-level-display');
if (levelDisplay) {
levelDisplay.innerHTML = `
๐ Current Level: ${levelInfo.name}
${levelInfo.desc}
`;
}
}
const resAct = await fetch(`/dashboard/progress/${childId}`);
const data = await resAct.json();
const ctxAct = document.getElementById('activitiesChart');
if (ctxAct) {
new Chart(ctxAct.getContext('2d'), {
type: 'bar',
data: {
labels: data.activities.map(a => a.name),
datasets: [{ label: 'Avg Score', data: data.activities.map(a => a.avg_score), backgroundColor: '#8d6e63' }]
}
});
}
const ctxEmo = document.getElementById('emotionChart');
if (ctxEmo) {
new Chart(ctxEmo.getContext('2d'), {
type: 'pie',
data: {
labels: data.emotions.map(e => e.emotion),
datasets: [{ data: data.emotions.map(e => e.count), backgroundColor: ['#8d6e63', '#d6c6a2', '#a1887f', '#d7ccc8', '#bcaaa4', '#efebe9'] }]
}
});
}
}
async function generateReport() {
const select = document.getElementById('dashboard-child-select');
if (!select) return;
const childId = select.value;
if (!childId) return alert("Select child.");
const res = await fetch(`/dashboard/generate-report/${childId}`);
const data = await res.json();
const reportLink = document.getElementById('report-link');
if (reportLink) reportLink.innerHTML = `View Report PDF `;
}
async function saveDiary() {
if (!userId) return alert("Login first.");
const titleVal = document.getElementById('diary-title');
const msgVal = document.getElementById('diary-message');
if (!titleVal || !msgVal) return;
const title = titleVal.value;
const message = msgVal.value;
const fileInput = document.getElementById('diary-upload');
const file = fileInput ? fileInput.files[0] : null;
const formData = new FormData();
formData.append('parent_id', userId);
formData.append('child_name', "Child");
formData.append('title', title);
formData.append('message', message);
if (file) formData.append('file', file);
const res = await fetch('/diary/add', { method: 'POST', body: formData });
if (res.ok) {
alert("Memory Saved!");
loadDiary();
titleVal.value = '';
msgVal.value = '';
}
}
async function loadDiary() {
if (!userId) return;
const res = await fetch(`/diary/${userId}`);
const diary = await res.json();
const list = document.getElementById('diary-timeline');
if (!list) return;
list.innerHTML = '';
diary.forEach(e => {
const div = document.createElement('div');
div.className = 'card';
div.innerHTML = `${e.title} ${e.message}
${e.timestamp} `;
if (e.image_path) div.innerHTML += ` `;
list.appendChild(div);
});
}
function startMoodMatch(container) {
const moods = [
{ name: 'Happy', emoji: '๐', color: '#f1c40f' }, { name: 'Sad', emoji: '๐ข', color: '#3498db' },
{ name: 'Angry', emoji: '๐ ', color: '#e74c3c' }, { name: 'Surprised', emoji: '๐ฒ', color: '#9b59b6' },
{ name: 'Scared', emoji: '๐จ', color: '#2ecc71' }, { name: 'Sleepy', emoji: '๐ด', color: '#95a5a6' }
];
const shuffled = [...moods].sort(() => 0.5 - Math.random());
const options = shuffled.slice(0, 3);
const target = options[Math.floor(Math.random() * options.length)];
container.innerHTML = `
How is the friend feeling?
${options.map(m => `
${m.name} `).join('')}
๐ Back
๐ Next One
โ
I'm Done!
`;
}
window.checkMood = function(selected, target) {
const feedback = document.getElementById('feedback');
if (selected === target) {
feedback.innerText = "Yes! That's exactly right! ๐";
feedback.style.color = "#2ecc71";
gameScore += 15;
document.getElementById('game-score').innerText = gameScore;
setTimeout(() => startMoodMatch(document.getElementById('game-container')), 1500);
} else {
feedback.innerText = "Not quite. Look closely at the face! ๐";
feedback.style.color = "#e74c3c";
}
};
// --- Level 3 Advanced Game Functions ---
function startAbcSort(container) {
const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
const lowercase = "abcdefghijklmnopqrstuvwxyz".split("");
let currentSet = [];
window.renderAbcSort = () => {
// Pick 4 random pairs
const indices = [];
while(indices.length < 4) {
const r = Math.floor(Math.random() * 26);
if(!indices.includes(r)) indices.push(r);
}
const items = [];
indices.forEach(i => {
items.push({ val: uppercase[i], type: 'upper' });
items.push({ val: lowercase[i], type: 'lower' });
});
items.sort(() => Math.random() - 0.5);
container.innerHTML = `
Sort into Upper and Lower Case
${items.map((item, i) => `
${item.val}
`).join('')}
๐ New Letters
โ
Done
`;
};
window.allowDrop = (ev) => ev.preventDefault();
window.drag = (ev) => {
ev.dataTransfer.setData("text", ev.target.id);
ev.dataTransfer.setData("type", ev.target.getAttribute("data-type"));
};
window.drop = (ev, binType) => {
ev.preventDefault();
const data = ev.dataTransfer.getData("text");
const itemType = ev.dataTransfer.getData("type");
const draggedEl = document.getElementById(data);
const feedback = document.getElementById('feedback');
if (itemType === binType) {
let bin = ev.target;
if (!bin.classList.contains('bin-content')) {
bin = bin.querySelector('.bin-content') || bin.closest('div').querySelector('.bin-content');
}
bin.appendChild(draggedEl);
draggedEl.setAttribute("draggable", "false");
draggedEl.style.cursor = "default";
gameScore += 10;
const scoreEl = document.getElementById('game-score');
if (scoreEl) scoreEl.innerText = gameScore;
const sourceBin = document.getElementById('source-bin');
if (sourceBin && sourceBin.children.length === 0) {
feedback.innerText = "Great sorting! ๐";
feedback.style.color = "var(--success)";
setTimeout(window.renderAbcSort, 1500);
}
} else {
feedback.innerText = "Try the other bin! ๐";
feedback.style.color = "var(--danger)";
setTimeout(() => feedback.innerText = "", 1500);
}
};
window.renderAbcSort();
}
function startMissingLetter(container) {
const words = [
{ word: 'APPLE', missing: 0, icon: '๐' },
{ word: 'BREAD', missing: 2, icon: '๐' },
{ word: 'CANDY', missing: 1, icon: '๐ฌ' },
{ word: 'DRESS', missing: 3, icon: '๐' },
{ word: 'EARTH', missing: 4, icon: '๐' },
{ word: 'FROGS', missing: 2, icon: '๐ธ' }
];
window.renderMissingLetter = () => {
const target = words[Math.floor(Math.random() * words.length)];
const wordArr = target.word.split("");
const correctLetter = wordArr[target.missing];
wordArr[target.missing] = "_";
// Generate options including correct one
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
const options = [correctLetter];
while(options.length < 4) {
const r = alphabet[Math.floor(Math.random() * 26)];
if(!options.includes(r)) options.push(r);
}
options.sort(() => Math.random() - 0.5);
container.innerHTML = `
Fill in the missing letter!
${target.icon}
${wordArr.join("")}
${options.map(letter => `
${letter}
`).join('')}
๐ Next Word
โ
Done
`;
};
window.checkMissingLetter = (selected, target) => {
const feedback = document.getElementById('feedback');
if (selected === target) {
feedback.innerText = "Excellent spelling! ๐";
feedback.style.color = "var(--success)";
gameScore += 20;
const scoreEl = document.getElementById('game-score');
if (scoreEl) scoreEl.innerText = gameScore;
setTimeout(window.renderMissingLetter, 1500);
} else {
feedback.innerText = "Try another letter! ๐";
feedback.style.color = "var(--danger)";
}
};
window.renderMissingLetter();
}
function startWordPictureMatch(container) {
const items = [
{ word: 'Elephant', icon: '๐' }, { word: 'Rocket', icon: '๐' },
{ word: 'Guitar', icon: '๐ธ' }, { word: 'Pizza', icon: '๐' },
{ word: 'Rainbow', icon: '๐' }, { word: 'Bicycle', icon: '๐ฒ' }
];
window.renderWordPicture = () => {
const target = items[Math.floor(Math.random() * items.length)];
const options = [...items].sort(() => Math.random() - 0.5).slice(0, 4);
if (!options.find(o => o.word === target.word)) {
options[Math.floor(Math.random() * 4)] = target;
}
container.innerHTML = `
Match the picture to the word!
${target.word.toUpperCase()}
${options.map(opt => `
${opt.icon}
`).join('')}
๐ Next
โ
Done
`;
};
window.checkWordPicture = (selected, target) => {
const feedback = document.getElementById('feedback');
if (selected === target) {
feedback.innerText = "Perfect Match! ๐";
feedback.style.color = "var(--success)";
gameScore += 15;
const scoreEl = document.getElementById('game-score');
if (scoreEl) scoreEl.innerText = gameScore;
setTimeout(window.renderWordPicture, 1500);
} else {
feedback.innerText = "That's a different one! ๐";
feedback.style.color = "var(--danger)";
}
};
window.renderWordPicture();
}
function startObjectSearch(container) {
const objects = [
{ name: 'Red Ball', icon: '๐ด', color: 'red' }, { name: 'Blue Square', icon: '๐ฆ', color: 'blue' },
{ name: 'Yellow Star', icon: 'โญ', color: 'yellow' }, { name: 'Green Apple', icon: '๐', color: 'green' },
{ name: 'Purple Heart', icon: '๐', color: 'purple' }, { name: 'Orange Orange', icon: '๐', color: 'orange' }
];
window.renderObjectSearch = () => {
const target = objects[Math.floor(Math.random() * objects.length)];
// Create a grid of 12 items
const gridItems = [];
for(let i=0; i<11; i++) {
gridItems.push(objects[Math.floor(Math.random() * objects.length)]);
}
gridItems.push(target);
gridItems.sort(() => Math.random() - 0.5);
container.innerHTML = `
Find the ${target.name} !
${gridItems.map((item, i) => `
${item.icon}
`).join('')}
๐ New Grid
โ
Done
`;
};
window.checkObjectSearch = (selected, target, el) => {
const feedback = document.getElementById('feedback');
if (selected === target) {
el.style.background = "#e8f5e9";
el.style.transform = "scale(1.1)";
feedback.innerText = "Found it! Well done! ๐";
feedback.style.color = "var(--success)";
gameScore += 10;
const scoreEl = document.getElementById('game-score');
if (scoreEl) scoreEl.innerText = gameScore;
setTimeout(window.renderObjectSearch, 1500);
} else {
el.style.opacity = "0.3";
feedback.innerText = "Keep looking... ๐";
feedback.style.color = "var(--text)";
}
};
window.renderObjectSearch();
}
function startSpatialPuzzle(container) {
const puzzles = [
{ full: '๐งฉ', parts: ['๐ฆ', '๐ฉ', '๐จ'] },
{ full: '๐ ', parts: ['๐', '๐งฑ', '๐ช'] },
{ full: '๐ธ', parts: ['๐', '๐', '๐'] }
];
window.renderSpatialPuzzle = () => {
const target = puzzles[Math.floor(Math.random() * puzzles.length)];
container.innerHTML = `
Spatial Reasoning: Build the Object
?
${target.parts.map(part => `
${part}
`).join('')}
๐ New Puzzle
โ
Done
`;
let piecesPlaced = 0;
window.addSpatialPiece = (el, fullIcon, totalPieces) => {
if (el.style.opacity === "0.5") return;
el.style.opacity = "0.5";
piecesPlaced++;
gameScore += 5;
const scoreEl = document.getElementById('game-score');
if (scoreEl) scoreEl.innerText = gameScore;
if (piecesPlaced >= totalPieces) {
const feedback = document.getElementById('feedback');
const puzzleTarget = document.getElementById('puzzle-target');
if (feedback) {
feedback.innerText = "Puzzle Complete! ๐";
feedback.style.color = "var(--success)";
}
if (puzzleTarget) {
puzzleTarget.innerText = fullIcon;
puzzleTarget.style.background = "#fff";
puzzleTarget.style.borderColor = "var(--success)";
}
setTimeout(window.renderSpatialPuzzle, 2000);
}
};
};
window.renderSpatialPuzzle();
}
function startAlphabetOrder(container) {
const letters = "ABCDEFG".split("");
window.renderAlphabetOrder = () => {
const items = [...letters].sort(() => Math.random() - 0.5);
container.innerHTML = `
Put the letters in order! (A to G)
${letters.map((_, i) => `
${i+1}
`).join('')}
${items.map((item, i) => `
${item}
`).join('')}
๐ Reset
โ
Done
`;
};
window.dropOrder = (ev, targetIdx) => {
ev.preventDefault();
const data = ev.dataTransfer.getData("text");
const draggedEl = document.getElementById(data);
if (!draggedEl) return;
const val = draggedEl.getAttribute("data-val");
const feedback = document.getElementById('feedback');
if (val === letters[targetIdx]) {
ev.target.innerText = val;
ev.target.style.background = "#e8f5e9";
ev.target.style.borderColor = "#4caf50";
ev.target.style.color = "#2e7d32";
ev.target.style.borderStyle = "solid";
draggedEl.remove();
gameScore += 15;
const scoreEl = document.getElementById('game-score');
if (scoreEl) scoreEl.innerText = gameScore;
const sourceRow = document.getElementById('source-row');
if (sourceRow && sourceRow.children.length === 0) {
feedback.innerText = "Alphabet Master! ๐";
feedback.style.color = "var(--success)";
setTimeout(window.renderAlphabetOrder, 2000);
}
} else {
feedback.innerText = "That's not where " + val + " goes! ๐";
feedback.style.color = "var(--danger)";
setTimeout(() => feedback.innerText = "", 1500);
}
};
window.renderAlphabetOrder();
}
function startJigsawPuzzle(container) {
const images = [
{ icon: '๐ถ', name: 'Puppy' }, { icon: '๐ฑ', name: 'Kitty' },
{ icon: '๐ฆ', name: 'Lion' }, { icon: '๐', name: 'Elephant' }
];
window.renderJigsaw = () => {
const target = images[Math.floor(Math.random() * images.length)];
const parts = [1, 2, 3, 4];
const shuffledParts = [...parts].sort(() => Math.random() - 0.5);
container.innerHTML = `
Complete the Jigsaw!
${parts.map(p => `
${p}
`).join('')}
${shuffledParts.map(p => `
${target.icon}
`).join('')}
๐ New Puzzle
โ
Done
`;
};
window.dropJigsaw = (ev, targetPart) => {
ev.preventDefault();
const data = ev.dataTransfer.getData("text");
const draggedEl = document.getElementById(data);
if (!draggedEl) return;
const part = draggedEl.getAttribute("data-part");
const feedback = document.getElementById('feedback');
if (parseInt(part) === targetPart) {
ev.target.innerHTML = draggedEl.innerHTML;
ev.target.style.background = "var(--secondary)";
ev.target.style.borderColor = "var(--primary)";
ev.target.style.borderStyle = "solid";
draggedEl.remove();
gameScore += 20;
const scoreEl = document.getElementById('game-score');
if (scoreEl) scoreEl.innerText = gameScore;
const pieces = document.getElementById('jigsaw-pieces');
if (pieces && pieces.children.length === 0) {
feedback.innerText = "Puzzle Solved! ๐";
feedback.style.color = "var(--success)";
setTimeout(window.renderJigsaw, 2000);
}
} else {
feedback.innerText = "Wrong spot! ๐";
feedback.style.color = "var(--danger)";
setTimeout(() => feedback.innerText = "", 1500);
}
};
window.renderJigsaw();
}
function startAdvancedPatterns(container) {
const sequences = [
{ pattern: ['๐ด', '๐ต', '๐ข', '๐ด', '๐ต'], next: '๐ข', options: ['๐ข', '๐ก', '๐ '] },
{ pattern: ['1', '2', '4', '8'], next: '16', options: ['10', '16', '12'] },
{ pattern: ['โฌ๏ธ', 'โก๏ธ', 'โฌ๏ธ', 'โฌ
๏ธ', 'โฌ๏ธ'], next: 'โก๏ธ', options: ['โฌ๏ธ', 'โก๏ธ', 'โฌ๏ธ'] }
];
window.renderAdvancedPattern = () => {
const target = sequences[Math.floor(Math.random() * sequences.length)];
container.innerHTML = `
Complete the Logic Pattern
${target.pattern.map(item => `
${item}
`).join('
โข
')}
โข
?
${target.options.map(opt => `
${opt}
`).join('')}
๐ New Challenge
โ
Done
`;
};
window.checkAdvancedPattern = (selected, target) => {
const feedback = document.getElementById('feedback');
const patternTarget = document.getElementById('pattern-target');
if (selected === target) {
if (feedback) {
feedback.innerText = "Brilliant Logic! ๐";
feedback.style.color = "var(--success)";
}
gameScore += 25;
const scoreEl = document.getElementById('game-score');
if (scoreEl) scoreEl.innerText = gameScore;
if (patternTarget) {
patternTarget.innerText = target;
patternTarget.style.color = "#000";
patternTarget.style.borderColor = "var(--success)";
}
setTimeout(window.renderAdvancedPattern, 2000);
} else {
if (feedback) {
feedback.innerText = "Look closer at the sequence! ๐";
feedback.style.color = "var(--danger)";
}
}
};
window.renderAdvancedPattern();
}
// Final Window Exports
window.startCamera = startCamera;
window.stopCamera = stopCamera;
window.capturePhoto = capturePhoto;
window.uploadEmotion = uploadEmotion;
window.confirmEmotion = confirmEmotion;
window.toggleCustomEmotion = toggleCustomEmotion;
window.saveCorrection = saveCorrection;
window.trainAI = trainAI;
window.startGame = startGame;
window.startNewQuiz = startNewQuiz;
window.saveDiary = saveDiary;
window.logout = logout;
window.register = register;
window.login = login;
window.addChild = addChild;
window.selectChild = selectChild;
window.deleteChild = deleteChild;
window.toggleAuth = toggleAuth;
window.syncChildAge = syncChildAge;
window.loadProgress = loadProgress;
window.generateReport = generateReport;
window.startAnimalColoring = startAnimalColoring;
window.selectAnimalTemplate = selectAnimalTemplate;
window.startLearnEmotions = startLearnEmotions;
window.startShapeMatch = startShapeMatch;
window.startPatternMatch = startPatternMatch;
window.startColorMatch = startColorMatch;
window.startMemoryGame = startMemoryGame;
window.startColoringBook = startColoringBook;
window.selectTemplate = selectTemplate;
window.selectPaletteColor = selectPaletteColor;
window.colorBlock = colorBlock;
window.nextImage = nextImage;
window.closeGame = closeGame;
window.restartGame = restartGame;
window.finishGame = finishGame;
window.startMoodMatch = startMoodMatch;
window.startAbcSort = startAbcSort;
window.startAlphabetOrder = startAlphabetOrder;
window.startMissingLetter = startMissingLetter;
window.startWordPictureMatch = startWordPictureMatch;
window.startObjectSearch = startObjectSearch;
window.startSpatialPuzzle = startSpatialPuzzle;
window.startJigsawPuzzle = startJigsawPuzzle;
window.startAdvancedPatterns = startAdvancedPatterns;
window.getLevelInfo = getLevelInfo;