Spaces:
Sleeping
Sleeping
initial commit
Browse files- .dockerignore +23 -0
- .gitignore +27 -0
- Dockerfile +45 -0
- add_column.py +17 -0
- backend/app/__init__.py +1 -0
- backend/app/data/uploads/emotions/50ebcab1-5059-4dde-8dec-55a7e7b1c61e.avif +0 -0
- backend/app/data/uploads/emotions/a4fdb987-a0ec-4753-a92e-2658dfc5a2b0.avif +0 -0
- backend/app/database.py +33 -0
- backend/app/main.py +81 -0
- backend/app/models.py +74 -0
- backend/app/models/custom_ai/custom_weights.pth +0 -0
- backend/app/models/custom_ai/labels.json +1 -0
- backend/app/routers/__init__.py +0 -0
- backend/app/routers/activities.py +33 -0
- backend/app/routers/auth.py +67 -0
- backend/app/routers/dashboard.py +74 -0
- backend/app/routers/diary.py +43 -0
- backend/app/routers/emotion.py +234 -0
- backend/app/utils/auth_utils.py +25 -0
- backend/app/utils/storage.py +76 -0
- backend/requirements.txt +21 -0
- check_data.py +17 -0
- check_db.py +15 -0
- check_ids.py +23 -0
- check_logs.py +15 -0
- check_root_db.py +23 -0
- frontend/static/css/style.css +340 -0
- frontend/static/js/script.js +0 -0
- frontend/templates/activities.html +290 -0
- frontend/templates/children.html +91 -0
- frontend/templates/dashboard.html +77 -0
- frontend/templates/diary.html +51 -0
- frontend/templates/emotion.html +257 -0
- frontend/templates/game.html +123 -0
- frontend/templates/index.html +47 -0
- frontend/templates/login.html +39 -0
- frontend/templates/quiz.html +139 -0
- link_child.py +48 -0
- run.py +34 -0
- seed_db.py +114 -0
- test_cloud_db.py +33 -0
- test_fer.py +8 -0
- test_fer_simple.py +23 -0
- train_custom_model.py +126 -0
- update_db_schema.py +26 -0
.dockerignore
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
.env
|
| 6 |
+
.venv
|
| 7 |
+
venv/
|
| 8 |
+
ENV/
|
| 9 |
+
|
| 10 |
+
# Databases
|
| 11 |
+
*.db
|
| 12 |
+
*.sqlite3
|
| 13 |
+
|
| 14 |
+
# Local Data
|
| 15 |
+
backend/data/uploads/
|
| 16 |
+
backend/data/reports/
|
| 17 |
+
data/
|
| 18 |
+
|
| 19 |
+
# OS/IDE
|
| 20 |
+
.vscode/
|
| 21 |
+
.idea/
|
| 22 |
+
.DS_Store
|
| 23 |
+
Thumbs.db
|
.gitignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
.venv
|
| 6 |
+
venv/
|
| 7 |
+
ENV/
|
| 8 |
+
|
| 9 |
+
# Environment / Secrets (CRITICAL)
|
| 10 |
+
.env
|
| 11 |
+
backend/.env
|
| 12 |
+
|
| 13 |
+
# Databases
|
| 14 |
+
*.db
|
| 15 |
+
*.sqlite3
|
| 16 |
+
backend/data/*.db
|
| 17 |
+
|
| 18 |
+
# Local Uploads (S3 will handle this in cloud)
|
| 19 |
+
backend/data/uploads/*
|
| 20 |
+
!backend/data/uploads/.gitkeep
|
| 21 |
+
backend/data/reports/*
|
| 22 |
+
!backend/data/reports/.gitkeep
|
| 23 |
+
|
| 24 |
+
# OS/IDE
|
| 25 |
+
.vscode/
|
| 26 |
+
.idea/
|
| 27 |
+
.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use an official Python runtime as a parent image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Set environment variables
|
| 5 |
+
ENV PYTHONUNBUFFERED=1
|
| 6 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 7 |
+
ENV PYTHONPATH=/app/backend
|
| 8 |
+
|
| 9 |
+
# Set the working directory in the container
|
| 10 |
+
WORKDIR /app
|
| 11 |
+
|
| 12 |
+
# Install system dependencies for OpenCV and other libraries
|
| 13 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 14 |
+
libgl1-mesa-glx \
|
| 15 |
+
libglib2.0-0 \
|
| 16 |
+
libsm6 \
|
| 17 |
+
libxext6 \
|
| 18 |
+
libxrender-dev \
|
| 19 |
+
gcc \
|
| 20 |
+
python3-dev \
|
| 21 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 22 |
+
|
| 23 |
+
# Copy only the requirements first to leverage Docker cache
|
| 24 |
+
COPY backend/requirements.txt .
|
| 25 |
+
|
| 26 |
+
# Install dependencies
|
| 27 |
+
# Note: This will install CPU versions of torch to keep the image size manageable
|
| 28 |
+
# since most standard cloud servers don't have GPUs.
|
| 29 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 30 |
+
|
| 31 |
+
# Copy the backend and frontend directories into the container
|
| 32 |
+
COPY backend /app/backend
|
| 33 |
+
COPY frontend /app/frontend
|
| 34 |
+
|
| 35 |
+
# Create necessary directories for runtime data (even if we move to S3 later)
|
| 36 |
+
RUN mkdir -p /app/backend/data/uploads/emotions \
|
| 37 |
+
/app/backend/data/uploads/diary \
|
| 38 |
+
/app/backend/data/reports
|
| 39 |
+
|
| 40 |
+
# Expose the port the app runs on
|
| 41 |
+
EXPOSE 8000
|
| 42 |
+
|
| 43 |
+
# Run the application using uvicorn
|
| 44 |
+
# We use 0.0.0.0 to allow external connections from the cloud
|
| 45 |
+
CMD ["uvicorn", "backend.app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
add_column.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
db_path = os.path.join("backend", "data", "neurosense.db")
|
| 5 |
+
if os.path.exists(db_path):
|
| 6 |
+
try:
|
| 7 |
+
conn = sqlite3.connect(db_path)
|
| 8 |
+
cursor = conn.cursor()
|
| 9 |
+
cursor.execute("ALTER TABLE emotion_logs ADD COLUMN image_hash VARCHAR;")
|
| 10 |
+
conn.commit()
|
| 11 |
+
print("Success! Added image_hash column to emotion_logs table.")
|
| 12 |
+
except Exception as e:
|
| 13 |
+
print(f"Error (maybe column already exists): {e}")
|
| 14 |
+
finally:
|
| 15 |
+
conn.close()
|
| 16 |
+
else:
|
| 17 |
+
print(f"Database not found at {db_path}")
|
backend/app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Mark app as a package
|
backend/app/data/uploads/emotions/50ebcab1-5059-4dde-8dec-55a7e7b1c61e.avif
ADDED
|
backend/app/data/uploads/emotions/a4fdb987-a0ec-4753-a92e-2658dfc5a2b0.avif
ADDED
|
backend/app/database.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from sqlalchemy import create_engine
|
| 3 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 4 |
+
from sqlalchemy.orm import sessionmaker
|
| 5 |
+
|
| 6 |
+
# Get the absolute path to the directory where this file is located (backend/app)
|
| 7 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 8 |
+
# Default local SQLite path
|
| 9 |
+
DEFAULT_DB_PATH = os.path.join(os.path.dirname(BASE_DIR), "data", "neurosense.db")
|
| 10 |
+
|
| 11 |
+
# Use DATABASE_URL from environment variable (for Cloud), or fallback to local SQLite
|
| 12 |
+
SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL", f"sqlite:///{DEFAULT_DB_PATH}")
|
| 13 |
+
|
| 14 |
+
# Fix for some cloud providers (like Heroku) that use 'postgres://' instead of 'postgresql://'
|
| 15 |
+
if SQLALCHEMY_DATABASE_URL.startswith("postgres://"):
|
| 16 |
+
SQLALCHEMY_DATABASE_URL = SQLALCHEMY_DATABASE_URL.replace("postgres://", "postgresql://", 1)
|
| 17 |
+
|
| 18 |
+
# SQLite requires 'check_same_thread: False', PostgreSQL does not
|
| 19 |
+
engine_args = {}
|
| 20 |
+
if SQLALCHEMY_DATABASE_URL.startswith("sqlite"):
|
| 21 |
+
engine_args["connect_args"] = {"check_same_thread": False}
|
| 22 |
+
|
| 23 |
+
engine = create_engine(SQLALCHEMY_DATABASE_URL, **engine_args)
|
| 24 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 25 |
+
|
| 26 |
+
Base = declarative_base()
|
| 27 |
+
|
| 28 |
+
def get_db():
|
| 29 |
+
db = SessionLocal()
|
| 30 |
+
try:
|
| 31 |
+
yield db
|
| 32 |
+
finally:
|
| 33 |
+
db.close()
|
backend/app/main.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Request
|
| 2 |
+
from fastapi.responses import RedirectResponse
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
from fastapi.templating import Jinja2Templates
|
| 5 |
+
from app.database import engine, Base
|
| 6 |
+
from app.routers import auth, dashboard, emotion, activities, diary
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
# Create Database Tables
|
| 10 |
+
Base.metadata.create_all(bind=engine)
|
| 11 |
+
|
| 12 |
+
app = FastAPI(title="NeuroSense")
|
| 13 |
+
|
| 14 |
+
# Get base directory (backend root)
|
| 15 |
+
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 16 |
+
FRONTEND_DIR = os.path.join(os.path.dirname(BASE_DIR), "frontend")
|
| 17 |
+
|
| 18 |
+
# Ensure necessary directories exist in backend/data
|
| 19 |
+
os.makedirs(os.path.join(BASE_DIR, "data/uploads/emotions"), exist_ok=True)
|
| 20 |
+
os.makedirs(os.path.join(BASE_DIR, "data/uploads/diary"), exist_ok=True)
|
| 21 |
+
os.makedirs(os.path.join(BASE_DIR, "data/reports"), exist_ok=True)
|
| 22 |
+
|
| 23 |
+
# Mount Static Files (CSS, JS) from frontend folder
|
| 24 |
+
app.mount("/static", StaticFiles(directory=os.path.join(FRONTEND_DIR, "static")), name="static")
|
| 25 |
+
|
| 26 |
+
# Mount Data Folders for viewing (from backend/data)
|
| 27 |
+
app.mount("/uploads", StaticFiles(directory=os.path.join(BASE_DIR, "data/uploads")), name="uploads")
|
| 28 |
+
app.mount("/reports", StaticFiles(directory=os.path.join(BASE_DIR, "data/reports")), name="reports")
|
| 29 |
+
|
| 30 |
+
# Templates from frontend/templates
|
| 31 |
+
templates = Jinja2Templates(directory=os.path.join(FRONTEND_DIR, "templates"))
|
| 32 |
+
|
| 33 |
+
# Include Routers
|
| 34 |
+
app.include_router(auth.router)
|
| 35 |
+
app.include_router(dashboard.router)
|
| 36 |
+
app.include_router(emotion.router)
|
| 37 |
+
app.include_router(activities.router)
|
| 38 |
+
app.include_router(diary.router)
|
| 39 |
+
|
| 40 |
+
# Page Routes
|
| 41 |
+
|
| 42 |
+
@app.get("/")
|
| 43 |
+
def home_page(request: Request):
|
| 44 |
+
# Root is Dashboard
|
| 45 |
+
return templates.TemplateResponse("dashboard.html", {"request": request})
|
| 46 |
+
|
| 47 |
+
@app.get("/login")
|
| 48 |
+
def login_page(request: Request):
|
| 49 |
+
return templates.TemplateResponse("login.html", {"request": request})
|
| 50 |
+
|
| 51 |
+
@app.get("/children")
|
| 52 |
+
def children_page(request: Request):
|
| 53 |
+
return templates.TemplateResponse("children.html", {"request": request})
|
| 54 |
+
|
| 55 |
+
@app.get("/emotion-learning")
|
| 56 |
+
def emotion_page(request: Request):
|
| 57 |
+
return templates.TemplateResponse("emotion.html", {"request": request})
|
| 58 |
+
|
| 59 |
+
@app.get("/activities")
|
| 60 |
+
def activities_page(request: Request):
|
| 61 |
+
return templates.TemplateResponse("activities.html", {"request": request})
|
| 62 |
+
|
| 63 |
+
@app.get("/game/{game_name}")
|
| 64 |
+
def game_page(request: Request, game_name: str):
|
| 65 |
+
return templates.TemplateResponse("game.html", {"request": request, "game_name": game_name})
|
| 66 |
+
|
| 67 |
+
@app.get("/dashboard")
|
| 68 |
+
def dashboard_redirect(request: Request):
|
| 69 |
+
return RedirectResponse(url="/")
|
| 70 |
+
|
| 71 |
+
@app.get("/diary")
|
| 72 |
+
def diary_page(request: Request):
|
| 73 |
+
return templates.TemplateResponse("diary.html", {"request": request})
|
| 74 |
+
|
| 75 |
+
@app.get("/quiz")
|
| 76 |
+
def quiz_page(request: Request):
|
| 77 |
+
return templates.TemplateResponse("quiz.html", {"request": request})
|
| 78 |
+
|
| 79 |
+
if __name__ == "__main__":
|
| 80 |
+
import uvicorn
|
| 81 |
+
uvicorn.run(app, host="127.0.0.1", port=8000)
|
backend/app/models.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Float
|
| 2 |
+
from sqlalchemy.orm import relationship
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from app.database import Base
|
| 5 |
+
|
| 6 |
+
class User(Base):
|
| 7 |
+
__tablename__ = "users"
|
| 8 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 9 |
+
username = Column(String, unique=True, index=True)
|
| 10 |
+
hashed_password = Column(String)
|
| 11 |
+
children = relationship("Child", back_populates="parent")
|
| 12 |
+
diary_entries = relationship("DiaryEntry", back_populates="parent")
|
| 13 |
+
|
| 14 |
+
class Child(Base):
|
| 15 |
+
__tablename__ = "children"
|
| 16 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 17 |
+
name = Column(String)
|
| 18 |
+
age = Column(Integer)
|
| 19 |
+
parent_id = Column(Integer, ForeignKey("users.id"))
|
| 20 |
+
autism_inheritance = Column(String, default="") # Family history/traits
|
| 21 |
+
sensory_level = Column(String, default="standard") # 'low', 'standard', 'high'
|
| 22 |
+
parent = relationship("User", back_populates="children")
|
| 23 |
+
|
| 24 |
+
emotion_logs = relationship("EmotionLog", back_populates="child")
|
| 25 |
+
activity_logs = relationship("ActivityLog", back_populates="child")
|
| 26 |
+
quiz_results = relationship("QuizResult", back_populates="child")
|
| 27 |
+
|
| 28 |
+
class EmotionLog(Base):
|
| 29 |
+
__tablename__ = "emotion_logs"
|
| 30 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 31 |
+
child_id = Column(Integer, ForeignKey("children.id"))
|
| 32 |
+
image_path = Column(String)
|
| 33 |
+
image_hash = Column(String, index=True) # New column
|
| 34 |
+
predicted_emotion = Column(String)
|
| 35 |
+
corrected_emotion = Column(String, nullable=True)
|
| 36 |
+
confirmed = Column(Boolean, default=False)
|
| 37 |
+
timestamp = Column(DateTime, default=datetime.utcnow)
|
| 38 |
+
|
| 39 |
+
child = relationship("Child", back_populates="emotion_logs")
|
| 40 |
+
|
| 41 |
+
class ActivityLog(Base):
|
| 42 |
+
__tablename__ = "activity_logs"
|
| 43 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 44 |
+
child_id = Column(Integer, ForeignKey("children.id"))
|
| 45 |
+
activity_name = Column(String) # e.g., "Emotion Match", "Color Learn"
|
| 46 |
+
score = Column(Integer) # e.g., 100
|
| 47 |
+
duration_seconds = Column(Integer)
|
| 48 |
+
timestamp = Column(DateTime, default=datetime.utcnow)
|
| 49 |
+
|
| 50 |
+
child = relationship("Child", back_populates="activity_logs")
|
| 51 |
+
|
| 52 |
+
class QuizResult(Base):
|
| 53 |
+
__tablename__ = "quiz_results"
|
| 54 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 55 |
+
child_id = Column(Integer, ForeignKey("children.id"))
|
| 56 |
+
quiz_name = Column(String)
|
| 57 |
+
score = Column(Integer)
|
| 58 |
+
total_questions = Column(Integer)
|
| 59 |
+
timestamp = Column(DateTime, default=datetime.utcnow)
|
| 60 |
+
|
| 61 |
+
child = relationship("Child", back_populates="quiz_results")
|
| 62 |
+
|
| 63 |
+
class DiaryEntry(Base):
|
| 64 |
+
__tablename__ = "diary_entries"
|
| 65 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 66 |
+
parent_id = Column(Integer, ForeignKey("users.id"))
|
| 67 |
+
child_name = Column(String) # Or link directly to child, but keeping simple for now
|
| 68 |
+
title = Column(String)
|
| 69 |
+
message = Column(String)
|
| 70 |
+
image_path = Column(String, nullable=True)
|
| 71 |
+
video_path = Column(String, nullable=True)
|
| 72 |
+
timestamp = Column(DateTime, default=datetime.utcnow)
|
| 73 |
+
|
| 74 |
+
parent = relationship("User", back_populates="diary_entries")
|
backend/app/models/custom_ai/custom_weights.pth
ADDED
|
Binary file (7.14 kB). View file
|
|
|
backend/app/models/custom_ai/labels.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"0": "angry", "1": "disgust", "2": "fear", "3": "happy", "4": "love", "5": "sad", "6": "shock", "7": "silly", "8": "surprise", "9": "tired"}
|
backend/app/routers/__init__.py
ADDED
|
File without changes
|
backend/app/routers/activities.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends
|
| 2 |
+
from sqlalchemy.orm import Session
|
| 3 |
+
from app.database import get_db
|
| 4 |
+
from app.models import ActivityLog, QuizResult
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
|
| 7 |
+
router = APIRouter(prefix="/activities", tags=["activities"])
|
| 8 |
+
|
| 9 |
+
class ActivityCreate(BaseModel):
|
| 10 |
+
child_id: int
|
| 11 |
+
activity_name: str
|
| 12 |
+
score: int
|
| 13 |
+
duration_seconds: int
|
| 14 |
+
|
| 15 |
+
class QuizCreate(BaseModel):
|
| 16 |
+
child_id: int
|
| 17 |
+
quiz_name: str
|
| 18 |
+
score: int
|
| 19 |
+
total_questions: int
|
| 20 |
+
|
| 21 |
+
@router.post("/log-activity")
|
| 22 |
+
def log_activity(act: ActivityCreate, db: Session = Depends(get_db)):
|
| 23 |
+
new_log = ActivityLog(**act.dict())
|
| 24 |
+
db.add(new_log)
|
| 25 |
+
db.commit()
|
| 26 |
+
return {"message": "Activity logged"}
|
| 27 |
+
|
| 28 |
+
@router.post("/log-quiz")
|
| 29 |
+
def log_quiz(quiz: QuizCreate, db: Session = Depends(get_db)):
|
| 30 |
+
new_log = QuizResult(**quiz.dict())
|
| 31 |
+
db.add(new_log)
|
| 32 |
+
db.commit()
|
| 33 |
+
return {"message": "Quiz logged"}
|
backend/app/routers/auth.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException, status, Response
|
| 2 |
+
from sqlalchemy.orm import Session
|
| 3 |
+
from app.database import get_db
|
| 4 |
+
from app.models import User, Child
|
| 5 |
+
from app.utils.auth_utils import get_password_hash, verify_password, create_access_token
|
| 6 |
+
from pydantic import BaseModel
|
| 7 |
+
from typing import List
|
| 8 |
+
|
| 9 |
+
router = APIRouter(prefix="/auth", tags=["auth"])
|
| 10 |
+
|
| 11 |
+
class UserCreate(BaseModel):
|
| 12 |
+
username: str
|
| 13 |
+
password: str
|
| 14 |
+
|
| 15 |
+
class ChildCreate(BaseModel):
|
| 16 |
+
name: str
|
| 17 |
+
age: int
|
| 18 |
+
parent_id: int
|
| 19 |
+
autism_inheritance: str = ""
|
| 20 |
+
sensory_level: str = "standard"
|
| 21 |
+
|
| 22 |
+
@router.post("/register")
|
| 23 |
+
def register(user: UserCreate, db: Session = Depends(get_db)):
|
| 24 |
+
db_user = db.query(User).filter(User.username == user.username).first()
|
| 25 |
+
if db_user:
|
| 26 |
+
raise HTTPException(status_code=400, detail="Username already registered")
|
| 27 |
+
hashed_pwd = get_password_hash(user.password)
|
| 28 |
+
new_user = User(username=user.username, hashed_password=hashed_pwd)
|
| 29 |
+
db.add(new_user)
|
| 30 |
+
db.commit()
|
| 31 |
+
db.refresh(new_user)
|
| 32 |
+
return {"message": "User created successfully"}
|
| 33 |
+
|
| 34 |
+
@router.post("/login")
|
| 35 |
+
def login(user: UserCreate, db: Session = Depends(get_db)):
|
| 36 |
+
db_user = db.query(User).filter(User.username == user.username).first()
|
| 37 |
+
if not db_user or not verify_password(user.password, db_user.hashed_password):
|
| 38 |
+
raise HTTPException(status_code=401, detail="Invalid credentials")
|
| 39 |
+
access_token = create_access_token(data={"sub": db_user.username, "id": db_user.id})
|
| 40 |
+
return {"access_token": access_token, "token_type": "bearer", "user_id": db_user.id}
|
| 41 |
+
|
| 42 |
+
@router.post("/add-child")
|
| 43 |
+
def add_child(child: ChildCreate, db: Session = Depends(get_db)):
|
| 44 |
+
new_child = Child(
|
| 45 |
+
name=child.name,
|
| 46 |
+
age=child.age,
|
| 47 |
+
parent_id=child.parent_id,
|
| 48 |
+
autism_inheritance=child.autism_inheritance,
|
| 49 |
+
sensory_level=child.sensory_level
|
| 50 |
+
)
|
| 51 |
+
db.add(new_child)
|
| 52 |
+
db.commit()
|
| 53 |
+
db.refresh(new_child)
|
| 54 |
+
return {"message": "Child added", "child_id": new_child.id}
|
| 55 |
+
|
| 56 |
+
@router.get("/children/{parent_id}")
|
| 57 |
+
def get_children(parent_id: int, db: Session = Depends(get_db)):
|
| 58 |
+
return db.query(Child).filter(Child.parent_id == parent_id).all()
|
| 59 |
+
|
| 60 |
+
@router.delete("/delete-child/{child_id}")
|
| 61 |
+
def delete_child(child_id: int, db: Session = Depends(get_db)):
|
| 62 |
+
db_child = db.query(Child).filter(Child.id == child_id).first()
|
| 63 |
+
if not db_child:
|
| 64 |
+
raise HTTPException(status_code=404, detail="Child not found")
|
| 65 |
+
db.delete(db_child)
|
| 66 |
+
db.commit()
|
| 67 |
+
return {"message": "Child deleted successfully"}
|
backend/app/routers/dashboard.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 2 |
+
from sqlalchemy.orm import Session
|
| 3 |
+
from sqlalchemy import func
|
| 4 |
+
from app.database import get_db
|
| 5 |
+
from app.models import Child, EmotionLog, ActivityLog, QuizResult
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
from reportlab.lib.pagesizes import letter
|
| 8 |
+
from reportlab.pdfgen import canvas
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
| 12 |
+
|
| 13 |
+
@router.get("/progress/{child_id}")
|
| 14 |
+
def get_progress(child_id: int, db: Session = Depends(get_db)):
|
| 15 |
+
# Activity Stats
|
| 16 |
+
activity_stats = db.query(
|
| 17 |
+
ActivityLog.activity_name,
|
| 18 |
+
func.avg(ActivityLog.score).label('avg_score'),
|
| 19 |
+
func.count(ActivityLog.id).label('sessions')
|
| 20 |
+
).filter(ActivityLog.child_id == child_id).group_by(ActivityLog.activity_name).all()
|
| 21 |
+
|
| 22 |
+
# Quiz Stats
|
| 23 |
+
quiz_stats = db.query(
|
| 24 |
+
QuizResult.quiz_name,
|
| 25 |
+
func.avg(QuizResult.score).label('avg_score'),
|
| 26 |
+
func.count(QuizResult.id).label('attempts')
|
| 27 |
+
).filter(QuizResult.child_id == child_id).group_by(QuizResult.quiz_name).all()
|
| 28 |
+
|
| 29 |
+
# Emotion Stats (Last 30 days)
|
| 30 |
+
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
|
| 31 |
+
emotion_stats = db.query(
|
| 32 |
+
EmotionLog.predicted_emotion,
|
| 33 |
+
func.count(EmotionLog.id).label('count')
|
| 34 |
+
).filter(
|
| 35 |
+
EmotionLog.child_id == child_id,
|
| 36 |
+
EmotionLog.timestamp >= thirty_days_ago
|
| 37 |
+
).group_by(EmotionLog.predicted_emotion).all()
|
| 38 |
+
|
| 39 |
+
return {
|
| 40 |
+
"activities": [{"name": s[0], "avg_score": s[1], "sessions": s[2]} for s in activity_stats],
|
| 41 |
+
"quizzes": [{"name": s[0], "avg_score": s[1], "attempts": s[2]} for s in quiz_stats],
|
| 42 |
+
"emotions": [{"emotion": s[0], "count": s[1]} for s in emotion_stats]
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
@router.get("/generate-report/{child_id}")
|
| 46 |
+
def generate_report(child_id: int, db: Session = Depends(get_db)):
|
| 47 |
+
child = db.query(Child).filter(Child.id == child_id).first()
|
| 48 |
+
if not child:
|
| 49 |
+
raise HTTPException(status_code=404, detail="Child not found")
|
| 50 |
+
|
| 51 |
+
report_dir = "data/reports"
|
| 52 |
+
os.makedirs(report_dir, exist_ok=True)
|
| 53 |
+
filename = f"report_{child_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf"
|
| 54 |
+
file_path = os.path.join(report_dir, filename)
|
| 55 |
+
|
| 56 |
+
# Simple PDF generation with ReportLab
|
| 57 |
+
c = canvas.Canvas(file_path, pagesize=letter)
|
| 58 |
+
c.drawString(100, 750, f"NeuroSense - 15 Day Progress Report")
|
| 59 |
+
c.drawString(100, 730, f"Child Name: {child.name}")
|
| 60 |
+
c.drawString(100, 710, f"Age: {child.age}")
|
| 61 |
+
c.drawString(100, 690, f"Date: {datetime.now().strftime('%Y-%m-%d')}")
|
| 62 |
+
|
| 63 |
+
# Add activity summary
|
| 64 |
+
y = 650
|
| 65 |
+
c.drawString(100, y, "Activity Participation:")
|
| 66 |
+
y -= 20
|
| 67 |
+
activities = db.query(ActivityLog).filter(ActivityLog.child_id == child_id).all()
|
| 68 |
+
for act in activities[-5:]: # Last 5
|
| 69 |
+
c.drawString(120, y, f"- {act.activity_name}: Score {act.score} ({act.timestamp.strftime('%Y-%m-%d')})")
|
| 70 |
+
y -= 20
|
| 71 |
+
|
| 72 |
+
c.save()
|
| 73 |
+
|
| 74 |
+
return {"report_url": f"/reports/{filename}"}
|
backend/app/routers/diary.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, UploadFile, File, Form
|
| 2 |
+
from sqlalchemy.orm import Session
|
| 3 |
+
from app.database import get_db
|
| 4 |
+
from app.models import DiaryEntry
|
| 5 |
+
import os
|
| 6 |
+
import shutil
|
| 7 |
+
import uuid
|
| 8 |
+
|
| 9 |
+
router = APIRouter(prefix="/diary", tags=["diary"])
|
| 10 |
+
|
| 11 |
+
UPLOAD_DIR = "data/uploads/diary"
|
| 12 |
+
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 13 |
+
|
| 14 |
+
from app.utils.storage import save_file
|
| 15 |
+
|
| 16 |
+
@router.post("/add")
|
| 17 |
+
async def add_diary(
|
| 18 |
+
parent_id: int = Form(...),
|
| 19 |
+
child_name: str = Form(...),
|
| 20 |
+
title: str = Form(...),
|
| 21 |
+
message: str = Form(...),
|
| 22 |
+
file: UploadFile = File(None),
|
| 23 |
+
db: Session = Depends(get_db)
|
| 24 |
+
):
|
| 25 |
+
image_path_db = None
|
| 26 |
+
if file:
|
| 27 |
+
image_path_db = save_file(file, "diary")
|
| 28 |
+
|
| 29 |
+
entry = DiaryEntry(
|
| 30 |
+
parent_id=parent_id,
|
| 31 |
+
child_name=child_name,
|
| 32 |
+
title=title,
|
| 33 |
+
message=message,
|
| 34 |
+
image_path=image_path_db
|
| 35 |
+
)
|
| 36 |
+
db.add(entry)
|
| 37 |
+
db.commit()
|
| 38 |
+
db.refresh(entry)
|
| 39 |
+
return {"message": "Diary entry added", "id": entry.id}
|
| 40 |
+
|
| 41 |
+
@router.get("/{parent_id}")
|
| 42 |
+
def get_diary(parent_id: int, db: Session = Depends(get_db)):
|
| 43 |
+
return db.query(DiaryEntry).filter(DiaryEntry.parent_id == parent_id).order_by(DiaryEntry.timestamp.desc()).all()
|
backend/app/routers/emotion.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
# Ensure Keras backend is torch if not already set (important for Python 3.14)
|
| 3 |
+
if "KERAS_BACKEND" not in os.environ:
|
| 4 |
+
os.environ["KERAS_BACKEND"] = "torch"
|
| 5 |
+
|
| 6 |
+
from fastapi import APIRouter, Depends, UploadFile, File, Form, HTTPException
|
| 7 |
+
from sqlalchemy.orm import Session
|
| 8 |
+
from app.database import get_db
|
| 9 |
+
from app.models import EmotionLog
|
| 10 |
+
import uuid
|
| 11 |
+
import shutil
|
| 12 |
+
from fer import FER
|
| 13 |
+
import cv2
|
| 14 |
+
import hashlib
|
| 15 |
+
from PIL import Image
|
| 16 |
+
import numpy as np
|
| 17 |
+
import torch
|
| 18 |
+
import torch.nn as nn
|
| 19 |
+
import json
|
| 20 |
+
|
| 21 |
+
router = APIRouter(prefix="/emotion", tags=["emotion"])
|
| 22 |
+
detector = FER(mtcnn=False)
|
| 23 |
+
|
| 24 |
+
# --- Custom Model Support ---
|
| 25 |
+
class EmotionClassifier(nn.Module):
|
| 26 |
+
def __init__(self, input_size, num_classes):
|
| 27 |
+
super(EmotionClassifier, self).__init__()
|
| 28 |
+
self.fc1 = nn.Linear(input_size, 64)
|
| 29 |
+
self.relu = nn.ReLU()
|
| 30 |
+
self.fc2 = nn.Linear(64, num_classes)
|
| 31 |
+
def forward(self, x):
|
| 32 |
+
return self.fc2(self.relu(self.fc1(x)))
|
| 33 |
+
|
| 34 |
+
# Get backend root directory
|
| 35 |
+
ROUTER_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 36 |
+
APP_DIR = os.path.dirname(ROUTER_DIR)
|
| 37 |
+
BASE_DIR = os.path.dirname(APP_DIR)
|
| 38 |
+
UPLOAD_DIR = os.path.join(BASE_DIR, "data/uploads/emotions")
|
| 39 |
+
MODEL_PATH = os.path.join(APP_DIR, "models/custom_ai")
|
| 40 |
+
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 41 |
+
|
| 42 |
+
def get_custom_emotion(base_emotions_dict):
|
| 43 |
+
"""If a custom model exists, use it to refine the FER output."""
|
| 44 |
+
weights_path = os.path.join(MODEL_PATH, "custom_weights.pth")
|
| 45 |
+
labels_path = os.path.join(MODEL_PATH, "labels.json")
|
| 46 |
+
|
| 47 |
+
# Get base FER's best guess
|
| 48 |
+
top_emo = max(base_emotions_dict, key=base_emotions_dict.get)
|
| 49 |
+
top_score = base_emotions_dict[top_emo]
|
| 50 |
+
|
| 51 |
+
# If standard FER is VERY confident (> 0.95), trust it.
|
| 52 |
+
if top_score > 0.95:
|
| 53 |
+
return top_emo
|
| 54 |
+
|
| 55 |
+
# 1. Fallback to standard FER logic if no custom model exists
|
| 56 |
+
if not os.path.exists(weights_path) or not os.path.exists(labels_path):
|
| 57 |
+
if top_score < 0.35:
|
| 58 |
+
return "neutral"
|
| 59 |
+
|
| 60 |
+
if top_emo == "happy":
|
| 61 |
+
neutral_score = base_emotions_dict.get("neutral", 0)
|
| 62 |
+
if neutral_score > (top_score * 0.5):
|
| 63 |
+
return "neutral"
|
| 64 |
+
|
| 65 |
+
return top_emo
|
| 66 |
+
|
| 67 |
+
try:
|
| 68 |
+
with open(labels_path, "r") as f:
|
| 69 |
+
idx_to_emotion = json.load(f)
|
| 70 |
+
|
| 71 |
+
num_classes = len(idx_to_emotion)
|
| 72 |
+
model = EmotionClassifier(input_size=7, num_classes=num_classes)
|
| 73 |
+
model.load_state_dict(torch.load(weights_path, weights_only=True))
|
| 74 |
+
model.eval()
|
| 75 |
+
|
| 76 |
+
# Convert FER dict to tensor features (standard 7 emotions)
|
| 77 |
+
# Sort keys to ensure consistent feature order matching training
|
| 78 |
+
features_list = [base_emotions_dict[k] for k in sorted(base_emotions_dict.keys())]
|
| 79 |
+
features = torch.tensor(features_list, dtype=torch.float32).unsqueeze(0)
|
| 80 |
+
|
| 81 |
+
with torch.no_grad():
|
| 82 |
+
outputs = model(features)
|
| 83 |
+
probs = torch.softmax(outputs, dim=1)
|
| 84 |
+
conf, predicted = torch.max(probs, 1)
|
| 85 |
+
|
| 86 |
+
custom_emo = idx_to_emotion[str(predicted.item())]
|
| 87 |
+
|
| 88 |
+
# Use custom model if it has reasonable confidence (> 0.4)
|
| 89 |
+
# or if the base FER is not very confident (< 0.6)
|
| 90 |
+
if conf.item() > 0.4 or top_score < 0.6:
|
| 91 |
+
return custom_emo
|
| 92 |
+
|
| 93 |
+
return top_emo # Stick with base FER
|
| 94 |
+
except Exception as e:
|
| 95 |
+
print(f"DEBUG: Custom model inference error: {e}")
|
| 96 |
+
return top_emo
|
| 97 |
+
|
| 98 |
+
from app.utils.storage import save_file
|
| 99 |
+
import hashlib
|
| 100 |
+
from PIL import Image
|
| 101 |
+
import io
|
| 102 |
+
|
| 103 |
+
@router.post("/upload")
|
| 104 |
+
async def upload_emotion(
|
| 105 |
+
child_id: int = Form(...),
|
| 106 |
+
file: UploadFile = File(...),
|
| 107 |
+
db: Session = Depends(get_db)
|
| 108 |
+
):
|
| 109 |
+
content = await file.read()
|
| 110 |
+
image_hash = hashlib.md5(content).hexdigest()
|
| 111 |
+
|
| 112 |
+
existing_entry = db.query(EmotionLog).filter(
|
| 113 |
+
EmotionLog.image_hash == image_hash,
|
| 114 |
+
EmotionLog.corrected_emotion != None
|
| 115 |
+
).order_by(EmotionLog.timestamp.desc()).first()
|
| 116 |
+
|
| 117 |
+
# Reset file pointer for save_file
|
| 118 |
+
file.file.seek(0)
|
| 119 |
+
# Save file using utility (returns S3 URL or local path)
|
| 120 |
+
saved_path = save_file(file, "emotions")
|
| 121 |
+
|
| 122 |
+
if existing_entry:
|
| 123 |
+
predicted_emotion = existing_entry.corrected_emotion
|
| 124 |
+
else:
|
| 125 |
+
try:
|
| 126 |
+
# For FER detection, we still need a local copy or numpy array
|
| 127 |
+
# Using io.BytesIO to avoid saving to disk twice
|
| 128 |
+
pil_img = Image.open(io.BytesIO(content)).convert("RGB")
|
| 129 |
+
img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
|
| 130 |
+
if img is None:
|
| 131 |
+
predicted_emotion = "unknown"
|
| 132 |
+
else:
|
| 133 |
+
emotions = detector.detect_emotions(img)
|
| 134 |
+
if emotions:
|
| 135 |
+
predicted_emotion = get_custom_emotion(emotions[0]["emotions"])
|
| 136 |
+
else:
|
| 137 |
+
predicted_emotion = "neutral"
|
| 138 |
+
except Exception as e:
|
| 139 |
+
predicted_emotion = "error"
|
| 140 |
+
print(f"DEBUG: Image processing error: {e}")
|
| 141 |
+
|
| 142 |
+
log_entry = EmotionLog(
|
| 143 |
+
child_id=child_id,
|
| 144 |
+
image_path=saved_path,
|
| 145 |
+
image_hash=image_hash,
|
| 146 |
+
predicted_emotion=predicted_emotion,
|
| 147 |
+
confirmed=False
|
| 148 |
+
)
|
| 149 |
+
db.add(log_entry)
|
| 150 |
+
db.commit()
|
| 151 |
+
db.refresh(log_entry)
|
| 152 |
+
|
| 153 |
+
return {
|
| 154 |
+
"id": log_entry.id,
|
| 155 |
+
"predicted_emotion": predicted_emotion,
|
| 156 |
+
"image_url": saved_path
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
@router.post("/process-frame")
|
| 160 |
+
async def process_frame(
|
| 161 |
+
child_id: int = Form(...),
|
| 162 |
+
frame_data: str = Form(...),
|
| 163 |
+
db: Session = Depends(get_db)
|
| 164 |
+
):
|
| 165 |
+
import base64
|
| 166 |
+
try:
|
| 167 |
+
header, encoded = frame_data.split(",", 1)
|
| 168 |
+
data = base64.b64decode(encoded)
|
| 169 |
+
nparr = np.frombuffer(data, np.uint8)
|
| 170 |
+
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
| 171 |
+
if img is None:
|
| 172 |
+
return {"emotion": "unknown", "error": "Failed to decode image"}
|
| 173 |
+
|
| 174 |
+
emotions = detector.detect_emotions(img)
|
| 175 |
+
if emotions:
|
| 176 |
+
scores = emotions[0]["emotions"]
|
| 177 |
+
# Use custom model logic for real-time camera too!
|
| 178 |
+
predicted_emotion = get_custom_emotion(scores)
|
| 179 |
+
else:
|
| 180 |
+
predicted_emotion = "neutral"
|
| 181 |
+
|
| 182 |
+
return {"emotion": predicted_emotion}
|
| 183 |
+
except Exception as e:
|
| 184 |
+
return {"emotion": "unknown", "error": str(e)}
|
| 185 |
+
|
| 186 |
+
@router.post("/confirm")
|
| 187 |
+
def confirm_emotion(
|
| 188 |
+
log_id: int = Form(...),
|
| 189 |
+
corrected_emotion: str = Form(None),
|
| 190 |
+
confirmed: bool = Form(...),
|
| 191 |
+
db: Session = Depends(get_db)
|
| 192 |
+
):
|
| 193 |
+
log_entry = db.query(EmotionLog).filter(EmotionLog.id == log_id).first()
|
| 194 |
+
if not log_entry:
|
| 195 |
+
raise HTTPException(status_code=404, detail="Log entry not found")
|
| 196 |
+
|
| 197 |
+
log_entry.confirmed = confirmed
|
| 198 |
+
if corrected_emotion:
|
| 199 |
+
log_entry.corrected_emotion = corrected_emotion
|
| 200 |
+
db.query(EmotionLog).filter(EmotionLog.image_hash == log_entry.image_hash).update({
|
| 201 |
+
"corrected_emotion": corrected_emotion,
|
| 202 |
+
"confirmed": True
|
| 203 |
+
})
|
| 204 |
+
|
| 205 |
+
db.commit()
|
| 206 |
+
return {"message": "Log updated successfully"}
|
| 207 |
+
|
| 208 |
+
@router.get("/unique-emotions")
|
| 209 |
+
def get_unique_emotions(db: Session = Depends(get_db)):
|
| 210 |
+
standard = {"happy", "sad", "angry", "fear", "surprise", "neutral"}
|
| 211 |
+
custom = db.query(EmotionLog.corrected_emotion).filter(EmotionLog.corrected_emotion != None).distinct().all()
|
| 212 |
+
custom_set = {c[0] for c in custom if c[0]}
|
| 213 |
+
return sorted(list(standard.union(custom_set)))
|
| 214 |
+
|
| 215 |
+
@router.post("/train")
|
| 216 |
+
async def train_model():
|
| 217 |
+
import subprocess
|
| 218 |
+
import sys
|
| 219 |
+
# Run the train_custom_model.py script as a subprocess
|
| 220 |
+
try:
|
| 221 |
+
# Need to find the absolute path to train_custom_model.py
|
| 222 |
+
# It is in the root directory (one level up from backend)
|
| 223 |
+
root_dir = os.path.dirname(BASE_DIR)
|
| 224 |
+
train_script = os.path.join(root_dir, "train_custom_model.py")
|
| 225 |
+
|
| 226 |
+
result = subprocess.run([sys.executable, train_script], capture_output=True, text=True)
|
| 227 |
+
if result.returncode == 0:
|
| 228 |
+
return {"message": "Success! AI brain retrained.", "output": result.stdout}
|
| 229 |
+
else:
|
| 230 |
+
# Combine stdout and stderr to catch the "FAILED:" message
|
| 231 |
+
error_msg = result.stdout + "\n" + result.stderr
|
| 232 |
+
return {"message": "Training failed", "error": error_msg.strip()}
|
| 233 |
+
except Exception as e:
|
| 234 |
+
return {"message": "Error starting training", "error": str(e)}
|
backend/app/utils/auth_utils.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from passlib.context import CryptContext
|
| 2 |
+
from datetime import datetime, timedelta
|
| 3 |
+
from jose import jwt
|
| 4 |
+
|
| 5 |
+
SECRET_KEY = "supersecretkey" # In production, use environment variable
|
| 6 |
+
ALGORITHM = "HS256"
|
| 7 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
| 8 |
+
|
| 9 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 10 |
+
|
| 11 |
+
def verify_password(plain_password, hashed_password):
|
| 12 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 13 |
+
|
| 14 |
+
def get_password_hash(password):
|
| 15 |
+
return pwd_context.hash(password)
|
| 16 |
+
|
| 17 |
+
def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
| 18 |
+
to_encode = data.copy()
|
| 19 |
+
if expires_delta:
|
| 20 |
+
expire = datetime.utcnow() + expires_delta
|
| 21 |
+
else:
|
| 22 |
+
expire = datetime.utcnow() + timedelta(minutes=15)
|
| 23 |
+
to_encode.update({"exp": expire})
|
| 24 |
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 25 |
+
return encoded_jwt
|
backend/app/utils/storage.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import boto3
|
| 3 |
+
from botocore.exceptions import NoCredentialsError
|
| 4 |
+
from fastapi import UploadFile
|
| 5 |
+
import shutil
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
# S3 Configuration from Environment Variables
|
| 9 |
+
S3_BUCKET = os.getenv("S3_BUCKET_NAME")
|
| 10 |
+
AWS_ACCESS_KEY = os.getenv("AWS_ACCESS_KEY_ID")
|
| 11 |
+
AWS_SECRET_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
|
| 12 |
+
AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
|
| 13 |
+
|
| 14 |
+
# Initialize S3 Client
|
| 15 |
+
s3_client = None
|
| 16 |
+
if S3_BUCKET and AWS_ACCESS_KEY and AWS_SECRET_KEY:
|
| 17 |
+
s3_client = boto3.client(
|
| 18 |
+
"s3",
|
| 19 |
+
aws_access_key_id=AWS_ACCESS_KEY,
|
| 20 |
+
aws_secret_access_key=AWS_SECRET_KEY,
|
| 21 |
+
region_name=AWS_REGION
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
def save_file(file: UploadFile, folder: str) -> str:
|
| 25 |
+
"""
|
| 26 |
+
Saves a file to S3 if configured, otherwise saves locally.
|
| 27 |
+
Returns the public URL (for S3) or the local relative path.
|
| 28 |
+
"""
|
| 29 |
+
file_name = f"{os.urandom(8).hex()}_{file.filename}"
|
| 30 |
+
|
| 31 |
+
# --- Option A: Upload to S3 ---
|
| 32 |
+
if s3_client:
|
| 33 |
+
try:
|
| 34 |
+
s3_client.upload_fileobj(
|
| 35 |
+
file.file,
|
| 36 |
+
S3_BUCKET,
|
| 37 |
+
f"{folder}/{file_name}",
|
| 38 |
+
ExtraArgs={"ACL": "public-read"} # Make publicly accessible
|
| 39 |
+
)
|
| 40 |
+
# Return the S3 URL
|
| 41 |
+
return f"https://{S3_BUCKET}.s3.{AWS_REGION}.amazonaws.com/{folder}/{file_name}"
|
| 42 |
+
except NoCredentialsError:
|
| 43 |
+
print("AWS credentials not found. Falling back to local storage.")
|
| 44 |
+
except Exception as e:
|
| 45 |
+
print(f"S3 Upload failed: {e}. Falling back to local storage.")
|
| 46 |
+
|
| 47 |
+
# --- Option B: Local Fallback ---
|
| 48 |
+
# Determine base directory (backend root)
|
| 49 |
+
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
| 50 |
+
local_path = BASE_DIR / "data" / "uploads" / folder
|
| 51 |
+
local_path.mkdir(parents=True, exist_ok=True)
|
| 52 |
+
|
| 53 |
+
full_path = local_path / file_name
|
| 54 |
+
with open(full_path, "wb") as buffer:
|
| 55 |
+
shutil.copyfileobj(file.file, buffer)
|
| 56 |
+
|
| 57 |
+
# Return local relative path (as currently used in your DB)
|
| 58 |
+
return f"/uploads/{folder}/{file_name}"
|
| 59 |
+
|
| 60 |
+
def delete_file(file_path: str):
|
| 61 |
+
"""
|
| 62 |
+
Optional: Delete a file from S3 or local disk.
|
| 63 |
+
"""
|
| 64 |
+
if s3_client and "amazonaws.com" in file_path:
|
| 65 |
+
# Extract key from URL
|
| 66 |
+
key = file_path.split(".com/")[-1]
|
| 67 |
+
try:
|
| 68 |
+
s3_client.delete_object(Bucket=S3_BUCKET, Key=key)
|
| 69 |
+
except Exception as e:
|
| 70 |
+
print(f"S3 Delete failed: {e}")
|
| 71 |
+
elif file_path.startswith("/uploads/"):
|
| 72 |
+
# Local delete
|
| 73 |
+
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
| 74 |
+
full_path = BASE_DIR / "data" / file_path.lstrip("/")
|
| 75 |
+
if os.path.exists(full_path):
|
| 76 |
+
os.remove(full_path)
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn
|
| 3 |
+
sqlalchemy
|
| 4 |
+
python-multipart
|
| 5 |
+
jinja2
|
| 6 |
+
python-jose[cryptography]
|
| 7 |
+
passlib[bcrypt]
|
| 8 |
+
bcrypt==4.0.1
|
| 9 |
+
fer
|
| 10 |
+
opencv-python
|
| 11 |
+
reportlab
|
| 12 |
+
python-dotenv
|
| 13 |
+
aiofiles
|
| 14 |
+
Pillow
|
| 15 |
+
torch
|
| 16 |
+
torchvision
|
| 17 |
+
torchaudio
|
| 18 |
+
numpy
|
| 19 |
+
moviepy<2.0.0
|
| 20 |
+
psycopg2-binary
|
| 21 |
+
boto3
|
check_data.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
db_path = os.path.join("neurosense", "data", "neurosense.db")
|
| 5 |
+
if os.path.exists(db_path):
|
| 6 |
+
conn = sqlite3.connect(db_path)
|
| 7 |
+
cursor = conn.cursor()
|
| 8 |
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
| 9 |
+
tables = cursor.fetchall()
|
| 10 |
+
for table in tables:
|
| 11 |
+
name = table[0]
|
| 12 |
+
cursor.execute(f"SELECT count(*) FROM {name};")
|
| 13 |
+
count = cursor.fetchone()[0]
|
| 14 |
+
print(f"Table {name}: {count} rows")
|
| 15 |
+
conn.close()
|
| 16 |
+
else:
|
| 17 |
+
print(f"Database file not found at: {db_path}")
|
check_db.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
db_path = os.path.join("neurosense", "data", "neurosense.db")
|
| 5 |
+
if os.path.exists(db_path):
|
| 6 |
+
conn = sqlite3.connect(db_path)
|
| 7 |
+
cursor = conn.cursor()
|
| 8 |
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
|
| 9 |
+
tables = cursor.fetchall()
|
| 10 |
+
print("Tables in database:")
|
| 11 |
+
for table in tables:
|
| 12 |
+
print(f"- {table[0]}")
|
| 13 |
+
conn.close()
|
| 14 |
+
else:
|
| 15 |
+
print(f"Database file not found at: {db_path}")
|
check_ids.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
db_path = os.path.join("neurosense", "data", "neurosense.db")
|
| 5 |
+
if os.path.exists(db_path):
|
| 6 |
+
conn = sqlite3.connect(db_path)
|
| 7 |
+
cursor = conn.cursor()
|
| 8 |
+
|
| 9 |
+
print("--- Users ---")
|
| 10 |
+
cursor.execute("SELECT id, username FROM users")
|
| 11 |
+
users = cursor.fetchall()
|
| 12 |
+
for u in users:
|
| 13 |
+
print(f"ID: {u[0]}, Username: {u[1]}")
|
| 14 |
+
|
| 15 |
+
print("\n--- Children ---")
|
| 16 |
+
cursor.execute("SELECT id, name, parent_id FROM children")
|
| 17 |
+
children = cursor.fetchall()
|
| 18 |
+
for c in children:
|
| 19 |
+
print(f"ID: {c[0]}, Name: {c[1]}, Parent ID: {c[2]}")
|
| 20 |
+
|
| 21 |
+
conn.close()
|
| 22 |
+
else:
|
| 23 |
+
print(f"Database file not found at: {db_path}")
|
check_logs.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
db_path = os.path.join("backend", "data", "neurosense.db")
|
| 5 |
+
if os.path.exists(db_path):
|
| 6 |
+
conn = sqlite3.connect(db_path)
|
| 7 |
+
cursor = conn.cursor()
|
| 8 |
+
cursor.execute("SELECT id, predicted_emotion, corrected_emotion, image_hash FROM emotion_logs ORDER BY id DESC LIMIT 5")
|
| 9 |
+
rows = cursor.fetchall()
|
| 10 |
+
print("Recent Emotion Logs:")
|
| 11 |
+
for r in rows:
|
| 12 |
+
print(f"ID: {r[0]}, Predicted: {r[1]}, Corrected: {r[2]}, Hash: {r[3]}")
|
| 13 |
+
conn.close()
|
| 14 |
+
else:
|
| 15 |
+
print("DB not found")
|
check_root_db.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
db_path = os.path.join("data", "neurosense.db")
|
| 5 |
+
if os.path.exists(db_path):
|
| 6 |
+
conn = sqlite3.connect(db_path)
|
| 7 |
+
cursor = conn.cursor()
|
| 8 |
+
|
| 9 |
+
print("--- Users ---")
|
| 10 |
+
cursor.execute("SELECT id, username FROM users")
|
| 11 |
+
users = cursor.fetchall()
|
| 12 |
+
for u in users:
|
| 13 |
+
print(f"ID: {u[0]}, Username: {u[1]}")
|
| 14 |
+
|
| 15 |
+
print("\n--- Children ---")
|
| 16 |
+
cursor.execute("SELECT id, name, parent_id FROM children")
|
| 17 |
+
children = cursor.fetchall()
|
| 18 |
+
for c in children:
|
| 19 |
+
print(f"ID: {c[0]}, Name: {c[1]}, Parent ID: {c[2]}")
|
| 20 |
+
|
| 21 |
+
conn.close()
|
| 22 |
+
else:
|
| 23 |
+
print(f"Database file not found at: {db_path}")
|
frontend/static/css/style.css
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap');
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--primary: #8d6e63;
|
| 5 |
+
--secondary: #d6c6a2;
|
| 6 |
+
--success: #6d8764;
|
| 7 |
+
--bg: #f4eee0;
|
| 8 |
+
--text: #4e342e;
|
| 9 |
+
|
| 10 |
+
--earth-tan: #eaddcf;
|
| 11 |
+
--earth-sand: #d6c6a2;
|
| 12 |
+
--earth-clay: #d7ccc8;
|
| 13 |
+
--earth-cream: #f5f5f5;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
body {
|
| 17 |
+
font-family: 'Nunito', sans-serif;
|
| 18 |
+
background-color: var(--bg);
|
| 19 |
+
color: var(--text);
|
| 20 |
+
margin: 0;
|
| 21 |
+
line-height: 1.6;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
header {
|
| 25 |
+
background: var(--secondary);
|
| 26 |
+
color: var(--text);
|
| 27 |
+
padding: 3.5rem 2rem;
|
| 28 |
+
text-align: center;
|
| 29 |
+
border-bottom: 8px solid #bdae8a;
|
| 30 |
+
box-shadow: 0 5px 20px rgba(0,0,0,0.05);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
header h1 {
|
| 34 |
+
font-size: 3.5rem;
|
| 35 |
+
margin: 0;
|
| 36 |
+
font-weight: 800;
|
| 37 |
+
letter-spacing: -1px;
|
| 38 |
+
text-shadow: 2px 2px 0px rgba(255,255,255,0.3);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
header p {
|
| 42 |
+
font-size: 1.3rem;
|
| 43 |
+
opacity: 0.8;
|
| 44 |
+
font-weight: 700;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
nav {
|
| 48 |
+
display: flex;
|
| 49 |
+
justify-content: center;
|
| 50 |
+
gap: 10px;
|
| 51 |
+
padding: 12px;
|
| 52 |
+
background: white;
|
| 53 |
+
width: fit-content;
|
| 54 |
+
margin: -25px auto 40px;
|
| 55 |
+
border-radius: 15px;
|
| 56 |
+
box-shadow: 0 10px 25px rgba(78,52,46,0.1);
|
| 57 |
+
border: 3px solid var(--secondary);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
nav a {
|
| 61 |
+
text-decoration: none;
|
| 62 |
+
color: var(--text);
|
| 63 |
+
font-weight: 800;
|
| 64 |
+
padding: 12px 24px;
|
| 65 |
+
border-radius: 10px;
|
| 66 |
+
transition: all 0.2s;
|
| 67 |
+
text-transform: uppercase;
|
| 68 |
+
font-size: 0.9rem;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
nav a:hover {
|
| 72 |
+
background: var(--secondary);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.container {
|
| 76 |
+
max-width: 1100px;
|
| 77 |
+
margin: 0 auto 5rem;
|
| 78 |
+
padding: 0 1.5rem;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.grid {
|
| 82 |
+
display: grid;
|
| 83 |
+
grid-template-columns: repeat(auto-fit, minmax(300px,1fr));
|
| 84 |
+
gap: 2.5rem;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.card {
|
| 88 |
+
background: white;
|
| 89 |
+
padding: 2.5rem;
|
| 90 |
+
border-radius: 20px;
|
| 91 |
+
box-shadow: 0 10px 30px rgba(78,52,46,0.05);
|
| 92 |
+
border: 2px solid #e0d7c6;
|
| 93 |
+
transition: transform 0.3s;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.card:hover {
|
| 97 |
+
transform: translateY(-8px);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.card.theme-blue { background-color: #efebe9; }
|
| 101 |
+
.card.theme-green { background-color: #f5f5f5; }
|
| 102 |
+
.card.theme-purple { background-color: var(--earth-tan); }
|
| 103 |
+
.card.theme-orange { background-color: var(--earth-sand); }
|
| 104 |
+
.card.theme-pink { background-color: var(--earth-clay); }
|
| 105 |
+
|
| 106 |
+
button {
|
| 107 |
+
background: var(--text);
|
| 108 |
+
color: white;
|
| 109 |
+
border: none;
|
| 110 |
+
padding: 16px 32px;
|
| 111 |
+
border-radius: 12px;
|
| 112 |
+
font-weight: 800;
|
| 113 |
+
cursor: pointer;
|
| 114 |
+
box-shadow: 0 6px 0 #2d1b18;
|
| 115 |
+
transition: all 0.1s;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
button:active {
|
| 119 |
+
transform: translateY(4px);
|
| 120 |
+
box-shadow: 0 2px 0 #2d1b18;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
button:hover {
|
| 124 |
+
filter: brightness(1.2);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.btn-play {
|
| 128 |
+
background: white !important;
|
| 129 |
+
color: var(--text) !important;
|
| 130 |
+
box-shadow: 0 6px 0 #d6c6a2 !important;
|
| 131 |
+
border: 2px solid #d6c6a2 !important;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
input, select, textarea {
|
| 135 |
+
width: 100%;
|
| 136 |
+
padding: 16px;
|
| 137 |
+
margin: 10px 0;
|
| 138 |
+
border: 2px solid #d6c6a2;
|
| 139 |
+
border-radius: 12px;
|
| 140 |
+
background: white;
|
| 141 |
+
font-family: inherit;
|
| 142 |
+
font-weight: 700;
|
| 143 |
+
color: var(--text);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.hidden {
|
| 147 |
+
display: none !important;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/* Sensory Themes - Re-unified to Beige/Brown */
|
| 151 |
+
body.calm-theme {
|
| 152 |
+
--primary: #a1887f;
|
| 153 |
+
--secondary: #d7ccc8;
|
| 154 |
+
--bg: #efebe9;
|
| 155 |
+
--text: #3e2723;
|
| 156 |
+
}
|
| 157 |
+
body.calm-theme .card, body.calm-theme .kids-card {
|
| 158 |
+
background: #fdfbf9;
|
| 159 |
+
border-color: #d7ccc8;
|
| 160 |
+
}
|
| 161 |
+
body.calm-theme header {
|
| 162 |
+
background: #d7ccc8;
|
| 163 |
+
border-bottom-color: #bcaaa4;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/* ========================= */
|
| 167 |
+
/* GAME UI IMPROVEMENTS */
|
| 168 |
+
/* ========================= */
|
| 169 |
+
|
| 170 |
+
.control-box {
|
| 171 |
+
display: flex;
|
| 172 |
+
justify-content: center;
|
| 173 |
+
gap: 20px;
|
| 174 |
+
margin-top: 30px;
|
| 175 |
+
flex-wrap: wrap;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.cute-btn {
|
| 179 |
+
padding: 12px 24px;
|
| 180 |
+
border-radius: 50px;
|
| 181 |
+
font-size: 1rem;
|
| 182 |
+
display: flex;
|
| 183 |
+
align-items: center;
|
| 184 |
+
gap: 8px;
|
| 185 |
+
border: 3px solid white;
|
| 186 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
/* All buttons now shades of brown/beige */
|
| 190 |
+
.btn-pink, .btn-blue, .btn-orange, .btn-green {
|
| 191 |
+
background: var(--primary);
|
| 192 |
+
color: white;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
/* ========================= */
|
| 196 |
+
/* BIG EMOJI GAME BUTTONS */
|
| 197 |
+
/* ========================= */
|
| 198 |
+
|
| 199 |
+
.emotion-options {
|
| 200 |
+
display:flex;
|
| 201 |
+
justify-content:center;
|
| 202 |
+
gap:40px;
|
| 203 |
+
flex-wrap:wrap;
|
| 204 |
+
margin-top:30px;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/* UPDATED SIZE FOR VISIBILITY */
|
| 208 |
+
.game-shape {
|
| 209 |
+
font-size: 150px;
|
| 210 |
+
width: 250px;
|
| 211 |
+
height: 250px;
|
| 212 |
+
cursor: pointer;
|
| 213 |
+
transition: transform 0.2s ease, background-color 0.2s;
|
| 214 |
+
background: white;
|
| 215 |
+
border-radius: 40px;
|
| 216 |
+
border: 6px solid var(--secondary);
|
| 217 |
+
box-shadow: 0 12px 0 var(--secondary);
|
| 218 |
+
display: flex;
|
| 219 |
+
justify-content: center;
|
| 220 |
+
align-items: center;
|
| 221 |
+
line-height: 1;
|
| 222 |
+
margin: 10px;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.game-shape:hover {
|
| 226 |
+
transform: scale(1.05) translateY(-5px);
|
| 227 |
+
background-color: #fffdf5;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.game-shape:active {
|
| 231 |
+
transform: translateY(6px);
|
| 232 |
+
box-shadow: 0 4px 0 var(--secondary);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
/* color circles */
|
| 236 |
+
|
| 237 |
+
.color-swatch {
|
| 238 |
+
border-radius:50%;
|
| 239 |
+
cursor:pointer;
|
| 240 |
+
transition: transform 0.2s;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.color-swatch:hover {
|
| 244 |
+
transform: scale(1.15);
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
/* memory game */
|
| 248 |
+
|
| 249 |
+
.memory-card .content.hidden {
|
| 250 |
+
visibility:hidden;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/* ========================= */
|
| 254 |
+
/* KIDS THEME CARDS & PANELS */
|
| 255 |
+
/* ========================= */
|
| 256 |
+
|
| 257 |
+
.kids-card {
|
| 258 |
+
background: #fffdf5;
|
| 259 |
+
border: 4px solid #4e342e;
|
| 260 |
+
border-radius: 30px;
|
| 261 |
+
overflow: hidden;
|
| 262 |
+
box-shadow: 10px 10px 0px rgba(78, 52, 46, 0.1);
|
| 263 |
+
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 264 |
+
cursor: pointer;
|
| 265 |
+
text-align: center;
|
| 266 |
+
padding: 20px;
|
| 267 |
+
display: flex;
|
| 268 |
+
flex-direction: column;
|
| 269 |
+
align-items: center;
|
| 270 |
+
justify-content: space-between;
|
| 271 |
+
min-height: 280px;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.kids-card:hover {
|
| 275 |
+
transform: translateY(-8px) scale(1.02);
|
| 276 |
+
box-shadow: 12px 12px 0px rgba(78, 52, 46, 0.15);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.kids-card .icon {
|
| 280 |
+
font-size: 60px;
|
| 281 |
+
margin-bottom: 15px;
|
| 282 |
+
display: block;
|
| 283 |
+
filter: drop-shadow(0 5px 0 rgba(0,0,0,0.05));
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.kids-card h3 {
|
| 287 |
+
margin: 8px 0;
|
| 288 |
+
font-size: 1.4rem;
|
| 289 |
+
color: #4e342e;
|
| 290 |
+
font-weight: 800;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.kids-card p {
|
| 294 |
+
font-size: 0.9rem;
|
| 295 |
+
color: #6d4c41;
|
| 296 |
+
font-weight: 700;
|
| 297 |
+
margin-bottom: 15px;
|
| 298 |
+
line-height: 1.3;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.btn-kids {
|
| 302 |
+
background: white !important;
|
| 303 |
+
color: #4e342e !important;
|
| 304 |
+
border: 3px solid #d6c6a2 !important;
|
| 305 |
+
box-shadow: 0 5px 0 #d6c6a2 !important;
|
| 306 |
+
padding: 10px 20px;
|
| 307 |
+
border-radius: 12px;
|
| 308 |
+
font-weight: 800;
|
| 309 |
+
text-transform: uppercase;
|
| 310 |
+
font-size: 0.8rem;
|
| 311 |
+
width: 100%;
|
| 312 |
+
transition: all 0.1s;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.kids-card:hover .btn-kids {
|
| 316 |
+
background: var(--secondary) !important;
|
| 317 |
+
border-color: #4e342e !important;
|
| 318 |
+
box-shadow: 0 5px 0 #4e342e !important;
|
| 319 |
+
color: #4e342e !important;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
.grid-kids {
|
| 323 |
+
display: grid;
|
| 324 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 325 |
+
gap: 30px;
|
| 326 |
+
margin-top: 20px;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
@media (min-width: 1100px) {
|
| 330 |
+
.grid-kids {
|
| 331 |
+
grid-template-columns: repeat(3, 1fr);
|
| 332 |
+
}
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
/* Reverting all theme classes to beige/brown variations */
|
| 336 |
+
.card.theme-blue, .kids-card.theme-blue { background-color: #efebe9; }
|
| 337 |
+
.card.theme-green, .kids-card.theme-green { background-color: #f5f5f5; }
|
| 338 |
+
.card.theme-purple, .kids-card.theme-purple { background-color: var(--earth-tan); }
|
| 339 |
+
.card.theme-orange, .kids-card.theme-orange { background-color: var(--earth-sand); }
|
| 340 |
+
.card.theme-pink, .kids-card.theme-pink { background-color: var(--earth-clay); }
|
frontend/static/js/script.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/templates/activities.html
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Activities - NeuroSense</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/css/style.css?v=1.2">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<header>
|
| 11 |
+
<h1 id="page-title">Loading Activities...</h1>
|
| 12 |
+
<p id="age-level-indicator" style="font-weight: bold; color: var(--primary); font-size: 1.4rem; margin-top: -10px;"></p>
|
| 13 |
+
<p id="level-description"></p>
|
| 14 |
+
</header>
|
| 15 |
+
<nav>
|
| 16 |
+
<a href="/">Dashboard</a>
|
| 17 |
+
<a href="/emotion-learning">Emotion Learning</a>
|
| 18 |
+
<a href="/activities">Activities</a>
|
| 19 |
+
<a href="/quiz">Quiz</a>
|
| 20 |
+
<a href="/diary">Memory Diary</a>
|
| 21 |
+
<a href="/children">Child</a>
|
| 22 |
+
<a href="/login" id="nav-login">Login</a>
|
| 23 |
+
<a href="#" id="nav-logout" class="hidden" onclick="logout()">Logout</a>
|
| 24 |
+
</nav>
|
| 25 |
+
|
| 26 |
+
<div class="container" style="max-width: 1200px;">
|
| 27 |
+
<!-- The container starts empty to prevent clashing -->
|
| 28 |
+
<div id="game-selection-container" class="grid-kids">
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
<script src="/static/js/script.js?v=1.7"></script>
|
| 33 |
+
<script>
|
| 34 |
+
const GAME_TEMPLATES = {
|
| 35 |
+
'color-match': `
|
| 36 |
+
<div class="kids-card theme-orange" onclick="goToGame('Color Match')">
|
| 37 |
+
<span class="icon">🎨</span>
|
| 38 |
+
<h3>Learn Colors</h3>
|
| 39 |
+
<p>Basic color concepts and matching!</p>
|
| 40 |
+
<button class="btn-kids">Play Now</button>
|
| 41 |
+
</div>`,
|
| 42 |
+
'coloring-book': `
|
| 43 |
+
<div class="kids-card theme-pink" onclick="goToGame('Coloring Book')">
|
| 44 |
+
<span class="icon">🖌️</span>
|
| 45 |
+
<h3>Online Coloring</h3>
|
| 46 |
+
<p>Creativity and motor skills!</p>
|
| 47 |
+
<button class="btn-kids">Play Now</button>
|
| 48 |
+
</div>`,
|
| 49 |
+
'animal-coloring': `
|
| 50 |
+
<div class="kids-card theme-green" onclick="goToGame('Animal Coloring')">
|
| 51 |
+
<span class="icon">🐶</span>
|
| 52 |
+
<h3>Animal Coloring</h3>
|
| 53 |
+
<p>Fine motor and color recognition!</p>
|
| 54 |
+
<button class="btn-kids">Play Now</button>
|
| 55 |
+
</div>`,
|
| 56 |
+
'memory-game': `
|
| 57 |
+
<div class="kids-card theme-blue" onclick="goToGame('Memory Game')">
|
| 58 |
+
<span class="icon">🧠</span>
|
| 59 |
+
<h3>Memory Game</h3>
|
| 60 |
+
<p>Find the pairs and the magic star!</p>
|
| 61 |
+
<button class="btn-kids">Play Now</button>
|
| 62 |
+
</div>`,
|
| 63 |
+
'pattern-match': `
|
| 64 |
+
<div class="kids-card theme-blue" onclick="goToGame('Pattern Match')">
|
| 65 |
+
<span class="icon">🧩</span>
|
| 66 |
+
<h3>Pattern Match</h3>
|
| 67 |
+
<p>Logical pattern challenges!</p>
|
| 68 |
+
<button class="btn-kids">Play Now</button>
|
| 69 |
+
</div>`,
|
| 70 |
+
'mood-matcher': `
|
| 71 |
+
<div class="kids-card theme-purple" onclick="goToGame('Mood Matcher')">
|
| 72 |
+
<span class="icon">🎭</span>
|
| 73 |
+
<h3>Mood Matcher</h3>
|
| 74 |
+
<p>Advanced emotion recognition!</p>
|
| 75 |
+
<button class="btn-kids">Play Now</button>
|
| 76 |
+
</div>`,
|
| 77 |
+
'emotion-basic': `
|
| 78 |
+
<div class="kids-card theme-purple" onclick="goToGame('Learn Emotions')">
|
| 79 |
+
<span class="icon">😊</span>
|
| 80 |
+
<h3>Learn Emotions</h3>
|
| 81 |
+
<p>Happy, sad, angry, and silly!</p>
|
| 82 |
+
<button class="btn-kids">Play Now</button>
|
| 83 |
+
</div>`,
|
| 84 |
+
'shape-match': `
|
| 85 |
+
<div class="kids-card theme-orange" onclick="goToGame('Learn Shapes')">
|
| 86 |
+
<span class="icon">📐</span>
|
| 87 |
+
<h3>Learn Shapes</h3>
|
| 88 |
+
<p>Circle, square, triangle, and star!</p>
|
| 89 |
+
<button class="btn-kids">Play Now</button>
|
| 90 |
+
</div>`,
|
| 91 |
+
'alphabet-trace': `
|
| 92 |
+
<div class="kids-card theme-blue" onclick="goToGame('Alphabet Trace')">
|
| 93 |
+
<span class="icon">📝</span>
|
| 94 |
+
<h3>Alphabet Writing</h3>
|
| 95 |
+
<p>Trace letters A to Z!</p>
|
| 96 |
+
<button class="btn-kids">Play Now</button>
|
| 97 |
+
</div>`,
|
| 98 |
+
'number-write': `
|
| 99 |
+
<div class="kids-card theme-green" onclick="goToGame('Number Write')">
|
| 100 |
+
<span class="icon">🔢</span>
|
| 101 |
+
<h3>Number Writing</h3>
|
| 102 |
+
<p>Write numbers 1 to 10!</p>
|
| 103 |
+
<button class="btn-kids">Play Now</button>
|
| 104 |
+
</div>`,
|
| 105 |
+
'alphabet-memory': `
|
| 106 |
+
<div class="kids-card theme-orange" onclick="goToGame('Alphabet Memory')">
|
| 107 |
+
<span class="icon">🔤</span>
|
| 108 |
+
<h3>Alphabet Match</h3>
|
| 109 |
+
<p>Match Upper and Lower case!</p>
|
| 110 |
+
<button class="btn-kids">Play Now</button>
|
| 111 |
+
</div>`,
|
| 112 |
+
'word-match': `
|
| 113 |
+
<div class="kids-card theme-purple" onclick="goToGame('Word Match')">
|
| 114 |
+
<span class="icon">🔊</span>
|
| 115 |
+
<h3>Word Matching</h3>
|
| 116 |
+
<p>Audio flashcard memory!</p>
|
| 117 |
+
<button class="btn-kids">Play Now</button>
|
| 118 |
+
</div>`,
|
| 119 |
+
'halves-match': `
|
| 120 |
+
<div class="kids-card theme-pink" onclick="goToGame('Halves Match')">
|
| 121 |
+
<span class="icon">🌓</span>
|
| 122 |
+
<h3>Joining Halves</h3>
|
| 123 |
+
<p>Complete the pictures!</p>
|
| 124 |
+
<button class="btn-kids">Play Now</button>
|
| 125 |
+
</div>`,
|
| 126 |
+
'abc-sort': `
|
| 127 |
+
<div class="kids-card theme-blue" onclick="goToGame('Alphabet Sort')">
|
| 128 |
+
<span class="icon">🔠</span>
|
| 129 |
+
<h3>Alphabet Sort</h3>
|
| 130 |
+
<p>Upper and lower case sorting!</p>
|
| 131 |
+
<button class="btn-kids">Play Now</button>
|
| 132 |
+
</div>`,
|
| 133 |
+
'alphabet-order': `
|
| 134 |
+
<div class="kids-card theme-orange" onclick="goToGame('Alphabet Order')">
|
| 135 |
+
<span class="icon">🔡</span>
|
| 136 |
+
<h3>Alphabet Order</h3>
|
| 137 |
+
<p>Put letters in A-Z order!</p>
|
| 138 |
+
<button class="btn-kids">Play Now</button>
|
| 139 |
+
</div>`,
|
| 140 |
+
'missing-letter': `
|
| 141 |
+
<div class="kids-card theme-green" onclick="goToGame('Missing Letter')">
|
| 142 |
+
<span class="icon">❓</span>
|
| 143 |
+
<h3>Missing Letter</h3>
|
| 144 |
+
<p>Spelling and phonics challenge!</p>
|
| 145 |
+
<button class="btn-kids">Play Now</button>
|
| 146 |
+
</div>`,
|
| 147 |
+
'word-picture': `
|
| 148 |
+
<div class="kids-card theme-orange" onclick="goToGame('Word Picture Match')">
|
| 149 |
+
<span class="icon">🖼️</span>
|
| 150 |
+
<h3>Words & Pictures</h3>
|
| 151 |
+
<p>Match complex words to icons!</p>
|
| 152 |
+
<button class="btn-kids">Play Now</button>
|
| 153 |
+
</div>`,
|
| 154 |
+
'object-search': `
|
| 155 |
+
<div class="kids-card theme-purple" onclick="goToGame('Object Search')">
|
| 156 |
+
<span class="icon">🔍</span>
|
| 157 |
+
<h3>Object Search</h3>
|
| 158 |
+
<p>Find colors and objects!</p>
|
| 159 |
+
<button class="btn-kids">Play Now</button>
|
| 160 |
+
</div>`,
|
| 161 |
+
'jigsaw-puzzle': `
|
| 162 |
+
<div class="kids-card theme-pink" onclick="goToGame('Jigsaw Puzzle')">
|
| 163 |
+
<span class="icon">🧩</span>
|
| 164 |
+
<h3>Jigsaw Puzzle</h3>
|
| 165 |
+
<p>Complete the picture pieces!</p>
|
| 166 |
+
<button class="btn-kids">Play Now</button>
|
| 167 |
+
</div>`,
|
| 168 |
+
'spatial-puzzle': `
|
| 169 |
+
<div class="kids-card theme-pink" onclick="goToGame('Spatial Puzzle')">
|
| 170 |
+
<span class="icon">📐</span>
|
| 171 |
+
<h3>Spatial Reasoning</h3>
|
| 172 |
+
<p>Match the missing pieces!</p>
|
| 173 |
+
<button class="btn-kids">Play Now</button>
|
| 174 |
+
</div>`,
|
| 175 |
+
'advanced-patterns': `
|
| 176 |
+
<div class="kids-card theme-blue" onclick="goToGame('Advanced Patterns')">
|
| 177 |
+
<span class="icon">📈</span>
|
| 178 |
+
<h3>Pattern Logic</h3>
|
| 179 |
+
<p>Complex pattern completion!</p>
|
| 180 |
+
<button class="btn-kids">Play Now</button>
|
| 181 |
+
</div>`
|
| 182 |
+
};
|
| 183 |
+
|
| 184 |
+
document.addEventListener('DOMContentLoaded', async () => {
|
| 185 |
+
console.log("Activities page loaded");
|
| 186 |
+
|
| 187 |
+
// Check if token exists (global from script.js)
|
| 188 |
+
const activeToken = localStorage.getItem('token');
|
| 189 |
+
if (!activeToken) {
|
| 190 |
+
console.log("No token found, redirecting to login");
|
| 191 |
+
window.location.href = '/login';
|
| 192 |
+
return;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
// Try to sync child data, but don't block forever
|
| 196 |
+
try {
|
| 197 |
+
if (typeof syncChildAge === 'function') {
|
| 198 |
+
await syncChildAge();
|
| 199 |
+
} else {
|
| 200 |
+
console.warn("syncChildAge function not found");
|
| 201 |
+
}
|
| 202 |
+
} catch (e) {
|
| 203 |
+
console.error("Error syncing child age:", e);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
renderLevelSpecificContent();
|
| 207 |
+
});
|
| 208 |
+
|
| 209 |
+
function renderLevelSpecificContent() {
|
| 210 |
+
console.log("Rendering activities...");
|
| 211 |
+
const ageStr = localStorage.getItem('selectedChildAge');
|
| 212 |
+
const age = (ageStr && ageStr !== 'undefined') ? parseInt(ageStr) : 0;
|
| 213 |
+
const container = document.getElementById('game-selection-container');
|
| 214 |
+
const title = document.getElementById('page-title');
|
| 215 |
+
const indicator = document.getElementById('age-level-indicator');
|
| 216 |
+
const desc = document.getElementById('level-description');
|
| 217 |
+
|
| 218 |
+
if (!container) {
|
| 219 |
+
console.error("Game selection container not found!");
|
| 220 |
+
return;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// Clear container
|
| 224 |
+
container.innerHTML = '';
|
| 225 |
+
|
| 226 |
+
// Fallback for missing level info function
|
| 227 |
+
let levelInfo = { name: "All Levels", desc: "Fun activities for everyone!", level: 1 };
|
| 228 |
+
if (typeof getLevelInfo === 'function') {
|
| 229 |
+
levelInfo = getLevelInfo(age);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
title.innerText = "Your Activities";
|
| 233 |
+
|
| 234 |
+
if (age === 0) {
|
| 235 |
+
indicator.innerText = "All Activities";
|
| 236 |
+
desc.innerText = "Try any game below to start learning!";
|
| 237 |
+
|
| 238 |
+
// Show ALL games
|
| 239 |
+
Object.values(GAME_TEMPLATES).forEach(template => {
|
| 240 |
+
container.innerHTML += template;
|
| 241 |
+
});
|
| 242 |
+
return;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
indicator.innerText = levelInfo.name;
|
| 246 |
+
desc.innerText = levelInfo.desc;
|
| 247 |
+
|
| 248 |
+
// Show recommended + other games based on age
|
| 249 |
+
if (age >= 3 && age <= 6) {
|
| 250 |
+
container.innerHTML += GAME_TEMPLATES['animal-coloring'];
|
| 251 |
+
container.innerHTML += GAME_TEMPLATES['color-match'];
|
| 252 |
+
container.innerHTML += GAME_TEMPLATES['shape-match'];
|
| 253 |
+
container.innerHTML += GAME_TEMPLATES['emotion-basic'];
|
| 254 |
+
container.innerHTML += GAME_TEMPLATES['coloring-book'];
|
| 255 |
+
}
|
| 256 |
+
else if (age >= 7 && age <= 9) {
|
| 257 |
+
container.innerHTML += GAME_TEMPLATES['alphabet-trace'];
|
| 258 |
+
container.innerHTML += GAME_TEMPLATES['number-write'];
|
| 259 |
+
container.innerHTML += GAME_TEMPLATES['alphabet-memory'];
|
| 260 |
+
container.innerHTML += GAME_TEMPLATES['word-match'];
|
| 261 |
+
container.innerHTML += GAME_TEMPLATES['halves-match'];
|
| 262 |
+
container.innerHTML += GAME_TEMPLATES['pattern-match'];
|
| 263 |
+
container.innerHTML += GAME_TEMPLATES['memory-game'];
|
| 264 |
+
}
|
| 265 |
+
else {
|
| 266 |
+
container.innerHTML += GAME_TEMPLATES['abc-sort'];
|
| 267 |
+
container.innerHTML += GAME_TEMPLATES['alphabet-order'];
|
| 268 |
+
container.innerHTML += GAME_TEMPLATES['missing-letter'];
|
| 269 |
+
container.innerHTML += GAME_TEMPLATES['word-picture'];
|
| 270 |
+
container.innerHTML += GAME_TEMPLATES['object-search'];
|
| 271 |
+
container.innerHTML += GAME_TEMPLATES['jigsaw-puzzle'];
|
| 272 |
+
container.innerHTML += GAME_TEMPLATES['spatial-puzzle'];
|
| 273 |
+
container.innerHTML += GAME_TEMPLATES['advanced-patterns'];
|
| 274 |
+
container.innerHTML += GAME_TEMPLATES['mood-matcher'];
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
if (container.innerHTML === '') {
|
| 278 |
+
console.warn("No activities rendered for age:", age);
|
| 279 |
+
container.innerHTML = "<h3>No specific activities found for this age. Try the games above!</h3>";
|
| 280 |
+
Object.values(GAME_TEMPLATES).forEach(template => {
|
| 281 |
+
container.innerHTML += template;
|
| 282 |
+
});
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
function goToGame(name) {
|
| 286 |
+
window.location.href = `/game/${encodeURIComponent(name)}`;
|
| 287 |
+
}
|
| 288 |
+
</script>
|
| 289 |
+
</body>
|
| 290 |
+
</html>
|
frontend/templates/children.html
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Manage Child - NeuroSense</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/css/style.css?v=1.1">
|
| 8 |
+
<style>
|
| 9 |
+
.child-card {
|
| 10 |
+
display: flex;
|
| 11 |
+
justify-content: space-between;
|
| 12 |
+
align-items: center;
|
| 13 |
+
padding: 1.5rem;
|
| 14 |
+
margin-bottom: 1rem;
|
| 15 |
+
}
|
| 16 |
+
.child-info h3 { margin: 0; color: var(--primary); }
|
| 17 |
+
.child-actions { display: flex; gap: 10px; }
|
| 18 |
+
.btn-delete {
|
| 19 |
+
background: #e74c3c;
|
| 20 |
+
box-shadow: 0 4px 0 #c0392b;
|
| 21 |
+
}
|
| 22 |
+
.btn-select {
|
| 23 |
+
background: #2ecc71;
|
| 24 |
+
box-shadow: 0 4px 0 #27ae60;
|
| 25 |
+
}
|
| 26 |
+
</style>
|
| 27 |
+
</head>
|
| 28 |
+
<body>
|
| 29 |
+
<header>
|
| 30 |
+
<h1>Manage Children</h1>
|
| 31 |
+
<p>Add, select, or remove child profiles</p>
|
| 32 |
+
</header>
|
| 33 |
+
<nav>
|
| 34 |
+
<a href="/">Dashboard</a>
|
| 35 |
+
<a href="/emotion-learning">Emotion Learning</a>
|
| 36 |
+
<a href="/activities">Activities</a>
|
| 37 |
+
<a href="/quiz">Quiz</a>
|
| 38 |
+
<a href="/diary">Memory Diary</a>
|
| 39 |
+
<a href="/children">Child</a>
|
| 40 |
+
<a href="/login" id="nav-login">Login</a>
|
| 41 |
+
<a href="#" id="nav-logout" class="hidden" onclick="logout()">Logout</a>
|
| 42 |
+
</nav>
|
| 43 |
+
|
| 44 |
+
<div class="container">
|
| 45 |
+
<div class="grid">
|
| 46 |
+
<div class="card theme-orange">
|
| 47 |
+
<h3>➕ Add New Child</h3>
|
| 48 |
+
<input type="text" id="child-name" placeholder="Child's Name" style="background: white;">
|
| 49 |
+
<input type="number" id="child-age" placeholder="Child's Age" style="background: white;">
|
| 50 |
+
|
| 51 |
+
<div style="margin-top: 15px; text-align: left;">
|
| 52 |
+
<label style="font-weight: bold; font-size: 0.9rem;">Autism Traits/History in Family?</label>
|
| 53 |
+
<select id="child-autism-inheritance" style="background: white; margin-top: 5px;">
|
| 54 |
+
<option value="none">No known history</option>
|
| 55 |
+
<option value="immediate">Immediate family (parents/siblings)</option>
|
| 56 |
+
<option value="extended">Extended family (cousins/grandparents)</option>
|
| 57 |
+
<option value="suspected">Suspected traits (not diagnosed)</option>
|
| 58 |
+
</select>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<div style="margin-top: 15px; text-align: left;">
|
| 62 |
+
<label style="font-weight: bold; font-size: 0.9rem;">Sensory Sensitivity Level?</label>
|
| 63 |
+
<select id="child-sensory-level" style="background: white; margin-top: 5px;">
|
| 64 |
+
<option value="low">Low (Needs more stimulation)</option>
|
| 65 |
+
<option value="standard" selected>Standard (Balanced)</option>
|
| 66 |
+
<option value="high">High (Needs calm environment)</option>
|
| 67 |
+
</select>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<button onclick="addChild()" style="width:100%; margin-top: 20px;">Add Profile</button>
|
| 71 |
+
</div>
|
| 72 |
+
|
| 73 |
+
<div class="card theme-blue">
|
| 74 |
+
<h3>👶 Your Children</h3>
|
| 75 |
+
<div id="child-list">
|
| 76 |
+
<!-- Loaded via JS -->
|
| 77 |
+
<p>Loading children...</p>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<script src="/static/js/script.js"></script>
|
| 84 |
+
<script>
|
| 85 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 86 |
+
if (!token) window.location.href = '/login';
|
| 87 |
+
else loadChildren();
|
| 88 |
+
});
|
| 89 |
+
</script>
|
| 90 |
+
</body>
|
| 91 |
+
</html>
|
frontend/templates/dashboard.html
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Dashboard - NeuroSense</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/css/style.css?v=1.1">
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<header>
|
| 12 |
+
<h1>Dashboard</h1>
|
| 13 |
+
<p>Track progress and celebrate achievements! 🏆</p>
|
| 14 |
+
</header>
|
| 15 |
+
<nav>
|
| 16 |
+
<a href="/">Dashboard</a>
|
| 17 |
+
<a href="/emotion-learning">Emotion Learning</a>
|
| 18 |
+
<a href="/activities">Activities</a>
|
| 19 |
+
<a href="/quiz">Quiz</a>
|
| 20 |
+
<a href="/diary">Memory Diary</a>
|
| 21 |
+
<a href="/children">Child</a>
|
| 22 |
+
<a href="/login" id="nav-login">Login</a>
|
| 23 |
+
<a href="#" id="nav-logout" class="hidden" onclick="logout()">Logout</a>
|
| 24 |
+
</nav>
|
| 25 |
+
|
| 26 |
+
<div class="container">
|
| 27 |
+
<div class="card theme-orange" style="margin-bottom: 40px;">
|
| 28 |
+
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 20px;">
|
| 29 |
+
<div style="flex: 1; min-width: 300px;">
|
| 30 |
+
<h2 style="margin-bottom: 10px;">👋 Welcome Back!</h2>
|
| 31 |
+
<p style="font-weight: 700;">Select a child to view their amazing progress.</p>
|
| 32 |
+
<div style="display: flex; gap: 10px; margin-top: 15px;">
|
| 33 |
+
<select id="dashboard-child-select" onchange="loadProgress()" style="max-width: 250px; margin: 0; background: white;">
|
| 34 |
+
<option value="">Choose a Profile</option>
|
| 35 |
+
</select>
|
| 36 |
+
<button onclick="generateReport()" class="btn-play" style="width: auto; margin: 0;">PDF Report</button>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
<div>
|
| 40 |
+
<a href="/children" class="button" style="text-decoration: none; background: white; color: var(--primary); padding: 14px 28px; border-radius: 20px; font-weight: 800; box-shadow: 0 6px 0 #ddd;">Manage Profiles</a>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
<div id="report-link" style="margin-top: 15px;"></div>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<!-- New: Learning Level Display -->
|
| 47 |
+
<div id="dashboard-level-display"></div>
|
| 48 |
+
|
| 49 |
+
<div class="grid">
|
| 50 |
+
<div class="card" style="background: white;">
|
| 51 |
+
<h3>📊 Activity Performance</h3>
|
| 52 |
+
<p style="font-size: 0.9rem; color: #888; margin-bottom: 15px;">Scores across different learning games</p>
|
| 53 |
+
<canvas id="activitiesChart"></canvas>
|
| 54 |
+
</div>
|
| 55 |
+
<div class="card" style="background: white;">
|
| 56 |
+
<h3>🌈 Emotion Distribution</h3>
|
| 57 |
+
<p style="font-size: 0.9rem; color: #888; margin-bottom: 15px;">Emotions detected during learning sessions</p>
|
| 58 |
+
<canvas id="emotionChart"></canvas>
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
|
| 62 |
+
<div class="card" style="margin-top: 40px;">
|
| 63 |
+
<h3>✨ Recent Achievements</h3>
|
| 64 |
+
<div id="progress-highlights" style="margin-top: 20px;">
|
| 65 |
+
<p style="color: #999; font-style: italic;">Select a child to see highlights...</p>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
<script src="/static/js/script.js"></script>
|
| 70 |
+
<script>
|
| 71 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 72 |
+
if (!token) window.location.href = '/login';
|
| 73 |
+
loadChildrenForDashboard();
|
| 74 |
+
});
|
| 75 |
+
</script>
|
| 76 |
+
</body>
|
| 77 |
+
</html>
|
frontend/templates/diary.html
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Memory Diary - NeuroSense</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/css/style.css?v=1.1">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<header><h1>Memory Diary</h1></header>
|
| 11 |
+
<nav>
|
| 12 |
+
<a href="/">Dashboard</a>
|
| 13 |
+
<a href="/emotion-learning">Emotion Learning</a>
|
| 14 |
+
<a href="/activities">Activities</a>
|
| 15 |
+
<a href="/quiz">Quiz</a>
|
| 16 |
+
<a href="/diary">Memory Diary</a>
|
| 17 |
+
<a href="/children">Child</a>
|
| 18 |
+
<a href="/login" id="nav-login">Login</a>
|
| 19 |
+
<a href="#" id="nav-logout" class="hidden" onclick="logout()">Logout</a>
|
| 20 |
+
</nav>
|
| 21 |
+
|
| 22 |
+
<div class="container" style="max-width: 800px;">
|
| 23 |
+
<div class="kids-card theme-pink" style="cursor: default; text-align: left; align-items: flex-start;">
|
| 24 |
+
<div style="display: flex; align-items: center; gap: 15px; margin-bottom: 20px;">
|
| 25 |
+
<span class="icon" style="font-size: 50px; margin: 0;">📖</span>
|
| 26 |
+
<h2 style="margin: 0; font-size: 2rem;">New Memory</h2>
|
| 27 |
+
</div>
|
| 28 |
+
<p style="text-align: left; margin-bottom: 20px;">Write down a happy memory or something fun you did today!</p>
|
| 29 |
+
<input type="text" id="diary-title" placeholder="Memory Title (e.g., Fun at the Park!)" style="margin-bottom: 15px;">
|
| 30 |
+
<textarea id="diary-message" placeholder="What happened today? Tell your story..." style="min-height: 120px; margin-bottom: 15px;"></textarea>
|
| 31 |
+
<div style="width: 100%; margin-bottom: 20px;">
|
| 32 |
+
<label style="font-weight: 800; display: block; margin-bottom: 5px; color: #4e342e;">Add a Photo (optional):</label>
|
| 33 |
+
<input type="file" id="diary-upload" accept="image/*" style="padding: 10px; border-style: dashed;">
|
| 34 |
+
</div>
|
| 35 |
+
<button onclick="saveDiary()" class="btn-kids">✨ Save to My Timeline ✨</button>
|
| 36 |
+
</div>
|
| 37 |
+
<div style="margin: 50px 0 30px; text-align: center;">
|
| 38 |
+
<h2 style="font-size: 2.5rem; color: #4e342e; text-shadow: 2px 2px 0 white;">🌈 My Timeline</h2>
|
| 39 |
+
<div style="width: 100px; height: 6px; background: var(--secondary); margin: 10px auto; border-radius: 3px;"></div>
|
| 40 |
+
</div>
|
| 41 |
+
<div id="diary-timeline"></div>
|
| 42 |
+
</div>
|
| 43 |
+
<script src="/static/js/script.js"></script>
|
| 44 |
+
<script>
|
| 45 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 46 |
+
if (!token) window.location.href = '/login';
|
| 47 |
+
loadDiary();
|
| 48 |
+
});
|
| 49 |
+
</script>
|
| 50 |
+
</body>
|
| 51 |
+
</html>
|
frontend/templates/emotion.html
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Emotion Learning - NeuroSense</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/css/style.css?v=1.1">
|
| 8 |
+
<style>
|
| 9 |
+
.learning-grid {
|
| 10 |
+
display: grid;
|
| 11 |
+
grid-template-columns: 1fr 1fr;
|
| 12 |
+
gap: 30px;
|
| 13 |
+
align-items: start;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.feature-panel {
|
| 17 |
+
background: #fffdf5;
|
| 18 |
+
border: 4px solid #4e342e;
|
| 19 |
+
border-radius: 30px;
|
| 20 |
+
overflow: hidden;
|
| 21 |
+
box-shadow: 10px 10px 0px rgba(78, 52, 46, 0.1);
|
| 22 |
+
transition: transform 0.2s;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
.panel-header {
|
| 26 |
+
background: var(--secondary);
|
| 27 |
+
padding: 20px;
|
| 28 |
+
border-bottom: 4px solid #4e342e;
|
| 29 |
+
display: flex;
|
| 30 |
+
align-items: center;
|
| 31 |
+
gap: 15px;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.panel-header h2 {
|
| 35 |
+
margin: 0;
|
| 36 |
+
font-size: 1.8rem;
|
| 37 |
+
color: #4e342e;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.panel-content {
|
| 41 |
+
padding: 25px;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.instruction-tag {
|
| 45 |
+
display: inline-block;
|
| 46 |
+
background: #fdfae6;
|
| 47 |
+
color: #8d6e63;
|
| 48 |
+
padding: 5px 15px;
|
| 49 |
+
border-radius: 10px;
|
| 50 |
+
font-weight: 800;
|
| 51 |
+
font-size: 0.8rem;
|
| 52 |
+
margin-bottom: 15px;
|
| 53 |
+
border: 2px solid #eaddcf;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.camera-container-v2 {
|
| 57 |
+
background: #fdfaf0;
|
| 58 |
+
border-radius: 20px;
|
| 59 |
+
overflow: hidden;
|
| 60 |
+
position: relative;
|
| 61 |
+
border: 4px solid #d6c6a2;
|
| 62 |
+
min-height: 300px;
|
| 63 |
+
display: flex;
|
| 64 |
+
align-items: center;
|
| 65 |
+
justify-content: center;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.camera-overlay {
|
| 69 |
+
font-family: 'Nunito', sans-serif;
|
| 70 |
+
font-weight: 900;
|
| 71 |
+
padding: 10px 25px;
|
| 72 |
+
border: 3px solid #d6c6a2 !important;
|
| 73 |
+
border-radius: 40px;
|
| 74 |
+
background: white;
|
| 75 |
+
color: #4e342e;
|
| 76 |
+
font-size: 1.2rem;
|
| 77 |
+
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.camera-controls-v3 {
|
| 81 |
+
display: flex;
|
| 82 |
+
gap: 12px;
|
| 83 |
+
justify-content: center;
|
| 84 |
+
flex-wrap: wrap;
|
| 85 |
+
margin-top: 25px;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.btn-light-action {
|
| 89 |
+
background: white !important;
|
| 90 |
+
color: #4e342e !important;
|
| 91 |
+
border: 3px solid #d6c6a2 !important;
|
| 92 |
+
box-shadow: 0 5px 0 #d6c6a2 !important;
|
| 93 |
+
min-width: 130px;
|
| 94 |
+
padding: 12px 20px;
|
| 95 |
+
font-size: 0.9rem;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.btn-light-action:hover {
|
| 99 |
+
background: #fdfaf0 !important;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.btn-light-action:active {
|
| 103 |
+
transform: translateY(3px);
|
| 104 |
+
box-shadow: 0 2px 0 #d6c6a2 !important;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.upload-zone {
|
| 108 |
+
border: 3px dashed #d6c6a2;
|
| 109 |
+
padding: 30px;
|
| 110 |
+
text-align: center;
|
| 111 |
+
border-radius: 20px;
|
| 112 |
+
background: #fffcf5;
|
| 113 |
+
cursor: pointer;
|
| 114 |
+
transition: all 0.2s;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.upload-zone:hover {
|
| 118 |
+
background: #fdf8eb;
|
| 119 |
+
border-color: #8d6e63;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.training-section {
|
| 123 |
+
grid-column: span 2;
|
| 124 |
+
margin-top: 20px;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
@media (max-width: 950px) {
|
| 128 |
+
.learning-grid { grid-template-columns: 1fr; }
|
| 129 |
+
.training-section { grid-column: span 1; }
|
| 130 |
+
}
|
| 131 |
+
</style>
|
| 132 |
+
</head>
|
| 133 |
+
<body>
|
| 134 |
+
<header>
|
| 135 |
+
<h1>Emotion Learning</h1>
|
| 136 |
+
<p>Explore and recognize emotions with AI assistance! 🌟</p>
|
| 137 |
+
</header>
|
| 138 |
+
<nav>
|
| 139 |
+
<a href="/">Dashboard</a>
|
| 140 |
+
<a href="/emotion-learning">Emotion Learning</a>
|
| 141 |
+
<a href="/activities">Activities</a>
|
| 142 |
+
<a href="/quiz">Quiz</a>
|
| 143 |
+
<a href="/diary">Memory Diary</a>
|
| 144 |
+
<a href="/children">Child</a>
|
| 145 |
+
<a href="/login" id="nav-login">Login</a>
|
| 146 |
+
<a href="#" id="nav-logout" class="hidden" onclick="logout()">Logout</a>
|
| 147 |
+
</nav>
|
| 148 |
+
|
| 149 |
+
<div class="container" style="max-width: 1200px;">
|
| 150 |
+
<div class="learning-grid">
|
| 151 |
+
|
| 152 |
+
<!-- Left Panel: Live Scan -->
|
| 153 |
+
<div class="feature-panel">
|
| 154 |
+
<div class="panel-header">
|
| 155 |
+
<span style="font-size: 2.5rem;">📸</span>
|
| 156 |
+
<h2>Live Detection</h2>
|
| 157 |
+
</div>
|
| 158 |
+
<div class="panel-content">
|
| 159 |
+
<span class="instruction-tag">STEP 1: SEE EMOTIONS LIVE</span>
|
| 160 |
+
<p style="font-size: 0.95rem; margin-bottom: 20px; font-weight: 600;">Look into the lens! The AI will identify your emotions in real-time.</p>
|
| 161 |
+
|
| 162 |
+
<div class="camera-container-v2">
|
| 163 |
+
<video id="webcam" autoplay playsinline style="width: 100%; display: block;"></video>
|
| 164 |
+
<div id="live-overlay" class="camera-overlay" style="position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); width: 80%; text-align: center;">Ready...</div>
|
| 165 |
+
</div>
|
| 166 |
+
|
| 167 |
+
<div class="camera-controls-v3">
|
| 168 |
+
<button onclick="startCamera()" class="btn-light-action">Activate Lens</button>
|
| 169 |
+
<button onclick="capturePhoto()" class="btn-play" style="width: auto; margin: 0; min-width: 130px;">Snapshot</button>
|
| 170 |
+
<button onclick="stopCamera()" class="btn-light-action">Turn Off</button>
|
| 171 |
+
</div>
|
| 172 |
+
<p id="live-result" style="text-align: center; font-weight: 800; margin-top: 15px; color: var(--text);"></p>
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
<!-- Right Panel: Photo Analysis -->
|
| 177 |
+
<div class="feature-panel">
|
| 178 |
+
<div class="panel-header">
|
| 179 |
+
<span style="font-size: 2.5rem;">📂</span>
|
| 180 |
+
<h2>Photo Library</h2>
|
| 181 |
+
</div>
|
| 182 |
+
<div class="panel-content">
|
| 183 |
+
<span class="instruction-tag">STEP 2: ANALYZE SAVED PHOTOS</span>
|
| 184 |
+
<p style="font-size: 0.95rem; margin-bottom: 20px; font-weight: 600;">Choose a file from your device to perform a deep emotion scan.</p>
|
| 185 |
+
|
| 186 |
+
<div class="upload-zone" onclick="document.getElementById('emotion-upload').click()">
|
| 187 |
+
<div style="font-size: 3rem; margin-bottom: 10px;">☁️</div>
|
| 188 |
+
<p style="margin: 0; font-weight: 800;">Click to Upload Image</p>
|
| 189 |
+
<input type="file" id="emotion-upload" accept="image/*" style="display: none;" onchange="handleFileSelected(this)">
|
| 190 |
+
<p id="file-name-display" style="font-size: 0.8rem; color: var(--primary); margin-top: 10px;"></p>
|
| 191 |
+
</div>
|
| 192 |
+
<button onclick="uploadEmotion()" style="width: 100%; margin-top: 15px;" class="btn-play">Run Analysis</button>
|
| 193 |
+
|
| 194 |
+
<div id="emotion-result" class="ai-feedback-card hidden" style="margin-top: 25px; border: 3px solid var(--secondary); background: #fffcf5;">
|
| 195 |
+
<h3 style="margin-bottom: 15px; font-size: 1.4rem;">Scan Result: <span id="predicted-emotion" style="color: var(--primary);"></span></h3>
|
| 196 |
+
<img id="result-img" style="max-width: 100%; border-radius: 15px; border: 4px solid white; box-shadow: 0 5px 15px rgba(0,0,0,0.1);">
|
| 197 |
+
|
| 198 |
+
<div style="margin-top: 10px; padding-top: 15px; border-top: 2px dashed #d6c6a2; position: relative; top: -10px;">
|
| 199 |
+
<p style="font-weight: 800; margin-bottom: 10px;">Verify Result:</p>
|
| 200 |
+
<div style="display: flex; gap: 15px; justify-content: center; align-items: center; width: 100%;">
|
| 201 |
+
<button onclick="confirmEmotion(true)" class="btn-select" style="flex: 1; min-width: 120px; padding: 12px 10px; font-size: 0.9rem; margin: 0;">Correct ✅</button>
|
| 202 |
+
<button onclick="confirmEmotion(false)" class="btn-delete" style="flex: 1; min-width: 120px; padding: 12px 10px; font-size: 0.9rem; margin: 0;">Wrong ❌</button>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<div id="correction-area" class="hidden" style="margin-top: 20px; text-align: left;">
|
| 206 |
+
<label style="font-weight: 800; font-size: 0.9rem;">Select Correct Emotion:</label>
|
| 207 |
+
<select id="corrected-emotion" onchange="toggleCustomEmotion()" style="margin-top: 5px; border-width: 3px;">
|
| 208 |
+
<option value="happy">Happy</option>
|
| 209 |
+
<option value="sad">Sad</option>
|
| 210 |
+
<option value="angry">Angry</option>
|
| 211 |
+
<option value="fear">Fear</option>
|
| 212 |
+
<option value="surprise">Surprise</option>
|
| 213 |
+
<option value="neutral">Neutral</option>
|
| 214 |
+
<option value="other">Other (Type new...)</option>
|
| 215 |
+
</select>
|
| 216 |
+
<input type="text" id="custom-emotion-name" class="hidden" placeholder="Enter emotion name">
|
| 217 |
+
<button onclick="saveCorrection()" class="btn-play" style="margin-top: 10px; width: 100%;">Update AI Knowledge</button>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
|
| 224 |
+
<!-- Bottom Section: Training Hub -->
|
| 225 |
+
<div class="training-section kids-card theme-orange">
|
| 226 |
+
<div style="display: flex; align-items: center; gap: 15px; justify-content: center;">
|
| 227 |
+
<span class="icon">🧠</span>
|
| 228 |
+
<h2 style="margin: 0; font-size: 2.2rem;">AI Brain Center</h2>
|
| 229 |
+
</div>
|
| 230 |
+
<p style="text-align: center; max-width: 700px; margin: 20px auto; font-weight: 700; font-size: 1.1rem; color: #4e342e;">
|
| 231 |
+
Finalize your session! Click below to permanently integrate your corrections into the AI's memory.
|
| 232 |
+
</p>
|
| 233 |
+
<div style="text-align: center; margin-bottom: 10px; width: 100%;">
|
| 234 |
+
<button onclick="trainAI()" class="btn-kids" style="width: auto; padding: 25px 60px; font-size: 1.4rem; background: #fff !important;">✨ SYNC AI BRAIN ✨</button>
|
| 235 |
+
<p id="train-status" style="font-size: 1rem; margin-top: 20px; color: #4e342e; font-weight: 800;"></p>
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
+
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
<script src="/static/js/script.js?v=1.8"></script>
|
| 243 |
+
<script>
|
| 244 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 245 |
+
if (!token) window.location.href = '/login';
|
| 246 |
+
loadCustomEmotions();
|
| 247 |
+
});
|
| 248 |
+
|
| 249 |
+
function handleFileSelected(input) {
|
| 250 |
+
const display = document.getElementById('file-name-display');
|
| 251 |
+
if (input.files && input.files[0]) {
|
| 252 |
+
display.innerText = "Selected: " + input.files[0].name;
|
| 253 |
+
}
|
| 254 |
+
}
|
| 255 |
+
</script>
|
| 256 |
+
</body>
|
| 257 |
+
</html>
|
frontend/templates/game.html
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>{{ game_name }} - NeuroSense</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/css/style.css?v=1.2">
|
| 8 |
+
<style>
|
| 9 |
+
.memory-card {
|
| 10 |
+
aspect-ratio: 1/1;
|
| 11 |
+
min-width: 100px;
|
| 12 |
+
min-height: 100px;
|
| 13 |
+
border-radius: 25px !important;
|
| 14 |
+
box-shadow: 0 10px 20px rgba(0,0,0,0.1) !important;
|
| 15 |
+
font-size: 50px !important;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
#game-area {
|
| 19 |
+
border-top: 15px solid var(--primary);
|
| 20 |
+
text-align: center;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
#current-game-title {
|
| 24 |
+
font-size: 2.5rem;
|
| 25 |
+
margin-bottom: 30px;
|
| 26 |
+
text-shadow: 2px 2px 0px rgba(0,0,0,0.05);
|
| 27 |
+
}
|
| 28 |
+
</style>
|
| 29 |
+
</head>
|
| 30 |
+
<body>
|
| 31 |
+
<header>
|
| 32 |
+
<h1 class="bounce">{{ game_name }}</h1>
|
| 33 |
+
<p>You're doing great! Keep going! 🌟</p>
|
| 34 |
+
</header>
|
| 35 |
+
<nav>
|
| 36 |
+
<a href="/">Dashboard</a>
|
| 37 |
+
<a href="/emotion-learning">Emotion Learning</a>
|
| 38 |
+
<a href="/activities">Activities</a>
|
| 39 |
+
<a href="/quiz">Quiz</a>
|
| 40 |
+
<a href="/diary">Memory Diary</a>
|
| 41 |
+
<a href="/children">Child</a>
|
| 42 |
+
<a href="/login" id="nav-login">Login</a>
|
| 43 |
+
<a href="#" id="nav-logout" class="hidden" onclick="logout()">Logout</a>
|
| 44 |
+
</nav>
|
| 45 |
+
|
| 46 |
+
<div class="container">
|
| 47 |
+
<div id="game-area" class="card">
|
| 48 |
+
<h2 id="current-game-title">{{ game_name }}</h2>
|
| 49 |
+
<div id="game-container" style="min-height: 450px; padding: 20px; width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
| 50 |
+
<div id="loading-fallback" style="text-align:center;">
|
| 51 |
+
<p>Loading your fun game...</p>
|
| 52 |
+
<button onclick="forceStart()" class="btn-blue" style="margin-top:10px;">Click here if it stays blank</button>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
<div id="game-controls" class="hidden" style="margin-top: 30px;">
|
| 56 |
+
<div style="background: var(--bg); padding: 15px 30px; border-radius: 40px; display: inline-block; margin-bottom: 20px; border: 3px solid white; box-shadow: 0 5px 15px rgba(0,0,0,0.05);">
|
| 57 |
+
<span style="font-size: 1.5rem; font-weight: 800;">Score: <span id="game-score" style="color: var(--primary);">0</span></span>
|
| 58 |
+
</div>
|
| 59 |
+
<br>
|
| 60 |
+
<button onclick="restartGame()" class="btn-blue">Restart</button>
|
| 61 |
+
<button onclick="finishGame()" class="btn-green">Finish & Save</button>
|
| 62 |
+
</div>
|
| 63 |
+
<div style="text-align: center; margin-top: 30px;">
|
| 64 |
+
<button onclick="closeGame()" class="cute-btn btn-pink" id="close-game-btn" style="display:inline-flex; margin: 0 auto; cursor:pointer;"><span>🔙</span> Back to Activities</button>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
<script src="/static/js/script.js?v=1.7"></script>
|
| 69 |
+
<script>
|
| 70 |
+
const RAW_GAME_NAME = "{{ game_name }}";
|
| 71 |
+
|
| 72 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 73 |
+
if (!token) {
|
| 74 |
+
window.location.href = '/login';
|
| 75 |
+
return;
|
| 76 |
+
}
|
| 77 |
+
if (!selectedChildId) {
|
| 78 |
+
alert("Please select a child profile first!");
|
| 79 |
+
window.location.href = '/children';
|
| 80 |
+
return;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
forceStart();
|
| 84 |
+
});
|
| 85 |
+
|
| 86 |
+
function forceStart() {
|
| 87 |
+
const decoded = decodeURIComponent(RAW_GAME_NAME);
|
| 88 |
+
console.log("Forcing start for:", decoded);
|
| 89 |
+
if (typeof startGame === 'function') {
|
| 90 |
+
startGame(decoded);
|
| 91 |
+
} else {
|
| 92 |
+
console.error("startGame function not found!");
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
window.closeGame = function() {
|
| 97 |
+
window.location.href = '/activities';
|
| 98 |
+
};
|
| 99 |
+
|
| 100 |
+
// Standardized checkShape for the new UI
|
| 101 |
+
window.checkShape = function(selected, target) {
|
| 102 |
+
const feedback = document.getElementById('feedback');
|
| 103 |
+
if (selected === target) {
|
| 104 |
+
if (feedback) {
|
| 105 |
+
feedback.innerText = "Fantastic! 🌟";
|
| 106 |
+
feedback.style.color = "var(--success)";
|
| 107 |
+
}
|
| 108 |
+
gameScore += 10;
|
| 109 |
+
document.getElementById('game-score').innerText = gameScore;
|
| 110 |
+
setTimeout(() => {
|
| 111 |
+
const container = document.getElementById('game-container');
|
| 112 |
+
if (container) startShapeMatch(container);
|
| 113 |
+
}, 1000);
|
| 114 |
+
} else {
|
| 115 |
+
if (feedback) {
|
| 116 |
+
feedback.innerText = "Not that one. Try again! 🤔";
|
| 117 |
+
feedback.style.color = "var(--danger)";
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
};
|
| 121 |
+
</script>
|
| 122 |
+
</body>
|
| 123 |
+
</html>
|
frontend/templates/index.html
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>NeuroSense - AI Assisted Learning</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/css/style.css?v=1.1">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<header>
|
| 11 |
+
<h1>NeuroSense</h1>
|
| 12 |
+
<p>AI Assisted Learning Platform for Children with Autism</p>
|
| 13 |
+
</header>
|
| 14 |
+
<nav>
|
| 15 |
+
<a href="/">Home</a>
|
| 16 |
+
<a href="/login" id="nav-login">Login/Register</a>
|
| 17 |
+
<a href="/emotion-learning">Emotion Learning</a>
|
| 18 |
+
<a href="/activities">Activities</a>
|
| 19 |
+
<a href="/quiz">Quiz</a>
|
| 20 |
+
<a href="/dashboard">Dashboard</a>
|
| 21 |
+
<a href="/diary">Memory Diary</a>
|
| 22 |
+
<a href="/children">Switch Child</a>
|
| 23 |
+
<a href="#" onclick="logout()" id="nav-logout" class="hidden">Logout</a>
|
| 24 |
+
</nav>
|
| 25 |
+
<div class="container">
|
| 26 |
+
<div class="card">
|
| 27 |
+
<h2>Welcome to NeuroSense</h2>
|
| 28 |
+
<p>Our platform helps children with ASD improve their emotional and cognitive skills through interactive games and AI-powered learning.</p>
|
| 29 |
+
<div class="grid">
|
| 30 |
+
<div class="card">
|
| 31 |
+
<h3>😊 Emotion Learning</h3>
|
| 32 |
+
<p>Learn to recognize and express emotions using AI.</p>
|
| 33 |
+
</div>
|
| 34 |
+
<div class="card">
|
| 35 |
+
<h3>🎮 Fun Activities</h3>
|
| 36 |
+
<p>Color matching, memory games, and more.</p>
|
| 37 |
+
</div>
|
| 38 |
+
<div class="card">
|
| 39 |
+
<h3>📊 Parent Dashboard</h3>
|
| 40 |
+
<p>Track progress and download reports.</p>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
<script src="/static/js/script.js"></script>
|
| 46 |
+
</body>
|
| 47 |
+
</html>
|
frontend/templates/login.html
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Login - NeuroSense</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/css/style.css?v=1.1">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<header>
|
| 11 |
+
<h1>NeuroSense</h1>
|
| 12 |
+
<p>Your child's learning journey starts here! ✨</p>
|
| 13 |
+
</header>
|
| 14 |
+
|
| 15 |
+
<div class="container" style="max-width: 500px;">
|
| 16 |
+
<div id="login-section" class="card">
|
| 17 |
+
<h2 style="text-align: center; margin-bottom: 30px;">Welcome Back! 👋</h2>
|
| 18 |
+
<input type="text" id="login-username" placeholder="Username">
|
| 19 |
+
<input type="password" id="login-password" placeholder="Password">
|
| 20 |
+
<button onclick="login()" style="width: 100%; margin-top: 20px;">Login to Dashboard</button>
|
| 21 |
+
<p style="text-align: center; margin-top: 20px; font-weight: 600;">
|
| 22 |
+
New here? <a href="#" onclick="toggleAuth(true)" style="color: var(--primary);">Create an account</a>
|
| 23 |
+
</p>
|
| 24 |
+
</div>
|
| 25 |
+
|
| 26 |
+
<div id="register-section" class="card hidden">
|
| 27 |
+
<h2 style="text-align: center; margin-bottom: 30px;">Create Account 🚀</h2>
|
| 28 |
+
<input type="text" id="reg-username" placeholder="Choose Username">
|
| 29 |
+
<input type="password" id="reg-password" placeholder="Choose Password">
|
| 30 |
+
<button onclick="register()" style="width: 100%; margin-top: 20px;">Join NeuroSense</button>
|
| 31 |
+
<p style="text-align: center; margin-top: 20px; font-weight: 600;">
|
| 32 |
+
Already have an account? <a href="#" onclick="toggleAuth(false)" style="color: var(--primary);">Login here</a>
|
| 33 |
+
</p>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
|
| 37 |
+
<script src="/static/js/script.js"></script>
|
| 38 |
+
</body>
|
| 39 |
+
</html>
|
frontend/templates/quiz.html
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Quiz - NeuroSense</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/css/style.css?v=1.1">
|
| 8 |
+
<style>
|
| 9 |
+
.quiz-option {
|
| 10 |
+
font-size: 1.2rem;
|
| 11 |
+
padding: 20px;
|
| 12 |
+
cursor: pointer;
|
| 13 |
+
transition: all 0.2s;
|
| 14 |
+
display: flex;
|
| 15 |
+
flex-direction: column;
|
| 16 |
+
align-items: center;
|
| 17 |
+
gap: 10px;
|
| 18 |
+
}
|
| 19 |
+
.quiz-option:hover {
|
| 20 |
+
transform: scale(1.05);
|
| 21 |
+
background: #f8f9fa;
|
| 22 |
+
}
|
| 23 |
+
.quiz-option span {
|
| 24 |
+
font-size: 50px;
|
| 25 |
+
}
|
| 26 |
+
.progress-bar {
|
| 27 |
+
width: 100%;
|
| 28 |
+
height: 10px;
|
| 29 |
+
background: #eee;
|
| 30 |
+
border-radius: 5px;
|
| 31 |
+
margin-bottom: 20px;
|
| 32 |
+
overflow: hidden;
|
| 33 |
+
}
|
| 34 |
+
#progress-fill {
|
| 35 |
+
height: 100%;
|
| 36 |
+
background: var(--success);
|
| 37 |
+
width: 0%;
|
| 38 |
+
transition: width 0.3s;
|
| 39 |
+
}
|
| 40 |
+
</style>
|
| 41 |
+
</head>
|
| 42 |
+
<body>
|
| 43 |
+
<header>
|
| 44 |
+
<h1>Quiz Time!</h1>
|
| 45 |
+
<p id="age-level-indicator" style="font-weight: bold; color: var(--primary); text-align: center; margin-top: -10px; font-size: 1.2rem;"></p>
|
| 46 |
+
</header>
|
| 47 |
+
<nav>
|
| 48 |
+
<a href="/">Dashboard</a>
|
| 49 |
+
<a href="/emotion-learning">Emotion Learning</a>
|
| 50 |
+
<a href="/activities">Activities</a>
|
| 51 |
+
<a href="/quiz">Quiz</a>
|
| 52 |
+
<a href="/diary">Memory Diary</a>
|
| 53 |
+
<a href="/children">Child</a>
|
| 54 |
+
<a href="/login" id="nav-login">Login</a>
|
| 55 |
+
<a href="#" id="nav-logout" class="hidden" onclick="logout()">Logout</a>
|
| 56 |
+
</nav>
|
| 57 |
+
<div class="container" style="max-width: 1200px;">
|
| 58 |
+
<div id="quiz-selection" class="grid-kids">
|
| 59 |
+
<!-- Level 1 Quizzes (Visible to all) -->
|
| 60 |
+
<div class="kids-card theme-green age-level-1 age-level-2 age-level-3" onclick="startNewQuiz('Daily Routine')">
|
| 61 |
+
<span class="icon">☀️</span>
|
| 62 |
+
<h3>Daily Routine</h3>
|
| 63 |
+
<p>What comes next in our daily schedule?</p>
|
| 64 |
+
<button class="btn-kids">Start Quiz</button>
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
+
<!-- Level 2 Quizzes (Visible to Level 2 & 3) -->
|
| 68 |
+
<div class="kids-card theme-blue age-level-2 age-level-3" onclick="startNewQuiz('Social Skills')">
|
| 69 |
+
<span class="icon">🤝</span>
|
| 70 |
+
<h3>Social Skills</h3>
|
| 71 |
+
<p>What should we do in different social situations?</p>
|
| 72 |
+
<button class="btn-kids">Start Quiz</button>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
<div class="kids-card theme-orange age-level-2 age-level-3" onclick="startNewQuiz('Safety & Help')">
|
| 76 |
+
<span class="icon">🆘</span>
|
| 77 |
+
<h3>Safety & Help</h3>
|
| 78 |
+
<p>Learn who to ask for help and stay safe.</p>
|
| 79 |
+
<button class="btn-kids">Start Quiz</button>
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
<!-- Level 3 Quizzes (Visible to Level 3) -->
|
| 83 |
+
<div class="kids-card theme-purple age-level-3" onclick="startNewQuiz('Advanced Social Skills')">
|
| 84 |
+
<span class="icon">🧠</span>
|
| 85 |
+
<h3>Advanced Logic</h3>
|
| 86 |
+
<p>Understand complex emotions and social cues.</p>
|
| 87 |
+
<button class="btn-kids">Start Quiz</button>
|
| 88 |
+
</div>
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
+
<div id="quiz-area" class="card hidden">
|
| 92 |
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
| 93 |
+
<h2 id="current-quiz-name"></h2>
|
| 94 |
+
<span id="question-counter" style="font-weight: bold; color: var(--primary);"></span>
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
<div class="progress-bar"><div id="progress-fill"></div></div>
|
| 98 |
+
|
| 99 |
+
<div id="quiz-content">
|
| 100 |
+
<p id="quiz-question-text" style="font-size: 24px; text-align: center; margin-bottom: 30px; font-weight: 500;"></p>
|
| 101 |
+
<div id="quiz-options-grid" class="grid" style="grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));">
|
| 102 |
+
<!-- Options injected via JS -->
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<div id="quiz-results" class="hidden" style="text-align: center; padding: 20px;">
|
| 107 |
+
<h2 style="font-size: 40px;">Great Job! 🎉</h2>
|
| 108 |
+
<p id="final-score-text" style="font-size: 24px; margin: 20px 0;"></p>
|
| 109 |
+
<button onclick="location.reload()" class="btn-blue">Back to Quizzes</button>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
<script src="/static/js/script.js"></script>
|
| 114 |
+
<script>
|
| 115 |
+
document.addEventListener('DOMContentLoaded', async () => {
|
| 116 |
+
if (!token) window.location.href = '/login';
|
| 117 |
+
await syncChildAge();
|
| 118 |
+
filterQuizzesByAge();
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
function filterQuizzesByAge() {
|
| 122 |
+
const ageStr = localStorage.getItem('selectedChildAge');
|
| 123 |
+
const age = ageStr ? parseInt(ageStr) : 0;
|
| 124 |
+
const indicator = document.getElementById('age-level-indicator');
|
| 125 |
+
|
| 126 |
+
const levelInfo = getLevelInfo(age);
|
| 127 |
+
if (indicator) indicator.innerText = levelInfo.name;
|
| 128 |
+
|
| 129 |
+
document.querySelectorAll('.kids-card').forEach(card => {
|
| 130 |
+
if (card.classList.contains(`age-level-${levelInfo.level}`)) {
|
| 131 |
+
card.style.display = 'flex';
|
| 132 |
+
} else {
|
| 133 |
+
card.style.display = 'none';
|
| 134 |
+
}
|
| 135 |
+
});
|
| 136 |
+
}
|
| 137 |
+
</script>
|
| 138 |
+
</body>
|
| 139 |
+
</html>
|
link_child.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# Add backend to sys.path
|
| 5 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 6 |
+
backend_dir = os.path.join(base_dir, "backend")
|
| 7 |
+
sys.path.insert(0, backend_dir)
|
| 8 |
+
|
| 9 |
+
from app.database import SessionLocal
|
| 10 |
+
from app.models import User, Child, DiaryEntry
|
| 11 |
+
|
| 12 |
+
def link_to_nehal():
|
| 13 |
+
db = SessionLocal()
|
| 14 |
+
try:
|
| 15 |
+
# Find nehal user
|
| 16 |
+
nehal = db.query(User).filter(User.username == "nehal").first()
|
| 17 |
+
if not nehal:
|
| 18 |
+
print("User 'nehal' not found. Please register it in the app first!")
|
| 19 |
+
return
|
| 20 |
+
|
| 21 |
+
# Find Alex child
|
| 22 |
+
alex = db.query(Child).filter(Child.name == "Alex").first()
|
| 23 |
+
if not alex:
|
| 24 |
+
print("Child 'Alex' not found. Seeding new data for nehal...")
|
| 25 |
+
from seed_db import seed
|
| 26 |
+
seed() # This will ensure Alex exists
|
| 27 |
+
alex = db.query(Child).filter(Child.name == "Alex").first()
|
| 28 |
+
|
| 29 |
+
# Update Alex's parent_id to nehal's ID
|
| 30 |
+
alex.parent_id = nehal.id
|
| 31 |
+
|
| 32 |
+
# Also update diary entries
|
| 33 |
+
diary_entries = db.query(DiaryEntry).all()
|
| 34 |
+
for entry in diary_entries:
|
| 35 |
+
entry.parent_id = nehal.id
|
| 36 |
+
|
| 37 |
+
db.commit()
|
| 38 |
+
print(f"Success! Alex (and diary entries) are now linked to user '{nehal.username}' (ID: {nehal.id}).")
|
| 39 |
+
print("You can now login as 'nehal' and you will see Alex.")
|
| 40 |
+
|
| 41 |
+
except Exception as e:
|
| 42 |
+
print(f"Error: {e}")
|
| 43 |
+
db.rollback()
|
| 44 |
+
finally:
|
| 45 |
+
db.close()
|
| 46 |
+
|
| 47 |
+
if __name__ == "__main__":
|
| 48 |
+
link_to_nehal()
|
run.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uvicorn
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
# Set Keras backend to torch for Python 3.14 compatibility
|
| 6 |
+
os.environ["KERAS_BACKEND"] = "torch"
|
| 7 |
+
|
| 8 |
+
if __name__ == "__main__":
|
| 9 |
+
# Get absolute path to the backend directory
|
| 10 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 11 |
+
backend_dir = os.path.join(base_dir, "backend")
|
| 12 |
+
|
| 13 |
+
# Add backend_dir to sys.path so 'app' can be found
|
| 14 |
+
sys.path.insert(0, backend_dir)
|
| 15 |
+
|
| 16 |
+
# Move into the backend directory so relative paths for DB/Uploads work correctly
|
| 17 |
+
os.chdir(backend_dir)
|
| 18 |
+
|
| 19 |
+
# Ensure data directories exist inside backend/data
|
| 20 |
+
dirs = [
|
| 21 |
+
"data/uploads/emotions",
|
| 22 |
+
"data/uploads/diary",
|
| 23 |
+
"data/reports"
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
for d in dirs:
|
| 27 |
+
os.makedirs(d, exist_ok=True)
|
| 28 |
+
|
| 29 |
+
print(f"Starting NeuroSense Backend from: {backend_dir}")
|
| 30 |
+
print("Frontend is being served from: ../frontend")
|
| 31 |
+
print("Open http://127.0.0.1:8000 in your browser.")
|
| 32 |
+
|
| 33 |
+
# Run the app
|
| 34 |
+
uvicorn.run("app.main:app", host="127.0.0.1", port=8000, reload=True)
|
seed_db.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from passlib.context import CryptContext
|
| 5 |
+
|
| 6 |
+
# Add backend to sys.path
|
| 7 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 8 |
+
backend_dir = os.path.join(base_dir, "backend")
|
| 9 |
+
sys.path.insert(0, backend_dir)
|
| 10 |
+
|
| 11 |
+
from app.database import SessionLocal, engine, Base
|
| 12 |
+
from app.models import User, Child, EmotionLog, DiaryEntry, ActivityLog, QuizResult
|
| 13 |
+
|
| 14 |
+
# Password hashing
|
| 15 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 16 |
+
|
| 17 |
+
def seed():
|
| 18 |
+
db = SessionLocal()
|
| 19 |
+
try:
|
| 20 |
+
# Create tables if they don't exist
|
| 21 |
+
Base.metadata.create_all(bind=engine)
|
| 22 |
+
|
| 23 |
+
# 1. Create User
|
| 24 |
+
user = db.query(User).filter(User.username == "parent1").first()
|
| 25 |
+
if not user:
|
| 26 |
+
user = User(
|
| 27 |
+
username="parent1",
|
| 28 |
+
hashed_password=pwd_context.hash("password123")
|
| 29 |
+
)
|
| 30 |
+
db.add(user)
|
| 31 |
+
db.commit()
|
| 32 |
+
db.refresh(user)
|
| 33 |
+
print("User 'parent1' created.")
|
| 34 |
+
else:
|
| 35 |
+
print("User 'parent1' already exists.")
|
| 36 |
+
|
| 37 |
+
# 2. Create Child
|
| 38 |
+
child = db.query(Child).filter(Child.parent_id == user.id).first()
|
| 39 |
+
if not child:
|
| 40 |
+
child = Child(
|
| 41 |
+
name="Alex",
|
| 42 |
+
age=6,
|
| 43 |
+
parent_id=user.id
|
| 44 |
+
)
|
| 45 |
+
db.add(child)
|
| 46 |
+
db.commit()
|
| 47 |
+
db.refresh(child)
|
| 48 |
+
print("Child 'Alex' created.")
|
| 49 |
+
else:
|
| 50 |
+
print("Child 'Alex' already exists.")
|
| 51 |
+
|
| 52 |
+
# 3. Add Emotion Logs (Past 3 days)
|
| 53 |
+
if db.query(EmotionLog).count() == 0:
|
| 54 |
+
emotions = ["happy", "sad", "angry", "surprise", "neutral", "happy", "fear"]
|
| 55 |
+
for i, emotion in enumerate(emotions):
|
| 56 |
+
log = EmotionLog(
|
| 57 |
+
child_id=child.id,
|
| 58 |
+
predicted_emotion=emotion,
|
| 59 |
+
image_path=f"uploads/emotions/sample_{i}.jpg",
|
| 60 |
+
timestamp=datetime.utcnow() - timedelta(days=i/2),
|
| 61 |
+
confirmed=True if i % 2 == 0 else False
|
| 62 |
+
)
|
| 63 |
+
db.add(log)
|
| 64 |
+
print(f"Added {len(emotions)} emotion logs.")
|
| 65 |
+
|
| 66 |
+
# 4. Add Diary Entries
|
| 67 |
+
if db.query(DiaryEntry).count() == 0:
|
| 68 |
+
entries = [
|
| 69 |
+
{"title": "Great Progress!", "message": "Alex was very happy during the emotion matching game today."},
|
| 70 |
+
{"title": "Rough Morning", "message": "Had a bit of trouble focusing this morning, but improved by noon."}
|
| 71 |
+
]
|
| 72 |
+
for entry_data in entries:
|
| 73 |
+
entry = DiaryEntry(
|
| 74 |
+
parent_id=user.id,
|
| 75 |
+
child_name="Alex",
|
| 76 |
+
title=entry_data["title"],
|
| 77 |
+
message=entry_data["message"],
|
| 78 |
+
timestamp=datetime.utcnow() - timedelta(days=1)
|
| 79 |
+
)
|
| 80 |
+
db.add(entry)
|
| 81 |
+
print(f"Added {len(entries)} diary entries.")
|
| 82 |
+
|
| 83 |
+
# 5. Add Activity Logs
|
| 84 |
+
if db.query(ActivityLog).count() == 0:
|
| 85 |
+
activities = [
|
| 86 |
+
{"name": "Emotion Match", "score": 90, "duration": 120},
|
| 87 |
+
{"name": "Color Learn", "score": 100, "duration": 80}
|
| 88 |
+
]
|
| 89 |
+
for act in activities:
|
| 90 |
+
log = ActivityLog(
|
| 91 |
+
child_id=child.id,
|
| 92 |
+
activity_name=act["name"],
|
| 93 |
+
score=act["score"],
|
| 94 |
+
duration_seconds=act["duration"]
|
| 95 |
+
)
|
| 96 |
+
db.add(log)
|
| 97 |
+
print(f"Added {len(activities)} activity logs.")
|
| 98 |
+
|
| 99 |
+
db.commit()
|
| 100 |
+
print("\nDatabase seeded successfully!")
|
| 101 |
+
print("-" * 30)
|
| 102 |
+
print("Login Credentials:")
|
| 103 |
+
print("Username: parent1")
|
| 104 |
+
print("Password: password123")
|
| 105 |
+
print("-" * 30)
|
| 106 |
+
|
| 107 |
+
except Exception as e:
|
| 108 |
+
print(f"Error seeding database: {e}")
|
| 109 |
+
db.rollback()
|
| 110 |
+
finally:
|
| 111 |
+
db.close()
|
| 112 |
+
|
| 113 |
+
if __name__ == "__main__":
|
| 114 |
+
seed()
|
test_cloud_db.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
# Load .env first
|
| 6 |
+
load_dotenv(os.path.join(os.getcwd(), "backend", ".env"))
|
| 7 |
+
|
| 8 |
+
# Add the backend directory to the Python path
|
| 9 |
+
sys.path.append(os.path.join(os.getcwd(), "backend"))
|
| 10 |
+
|
| 11 |
+
from app.database import engine, Base
|
| 12 |
+
from sqlalchemy import text
|
| 13 |
+
|
| 14 |
+
def test_connection():
|
| 15 |
+
print("Testing connection to Neon Cloud PostgreSQL...")
|
| 16 |
+
try:
|
| 17 |
+
# Try to connect
|
| 18 |
+
with engine.connect() as connection:
|
| 19 |
+
result = connection.execute(text("SELECT version();"))
|
| 20 |
+
version = result.fetchone()
|
| 21 |
+
print(f"✅ Success! Connected to: {version[0]}")
|
| 22 |
+
|
| 23 |
+
# Try to create tables
|
| 24 |
+
print("Creating tables in cloud database...")
|
| 25 |
+
from app.models import User, Child, EmotionLog, ActivityLog, QuizResult, DiaryEntry
|
| 26 |
+
Base.metadata.create_all(bind=engine)
|
| 27 |
+
print("✅ Success! All tables created.")
|
| 28 |
+
|
| 29 |
+
except Exception as e:
|
| 30 |
+
print(f"❌ Connection failed: {e}")
|
| 31 |
+
|
| 32 |
+
if __name__ == "__main__":
|
| 33 |
+
test_connection()
|
test_fer.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
os.environ["KERAS_BACKEND"] = "torch"
|
| 3 |
+
try:
|
| 4 |
+
from fer import FER
|
| 5 |
+
detector = FER()
|
| 6 |
+
print("FER imported and detector created with torch backend.")
|
| 7 |
+
except Exception as e:
|
| 8 |
+
print(f"Error: {e}")
|
test_fer_simple.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
os.environ["KERAS_BACKEND"] = "torch"
|
| 3 |
+
from fer import FER
|
| 4 |
+
import cv2
|
| 5 |
+
import numpy as np
|
| 6 |
+
|
| 7 |
+
def test_fer():
|
| 8 |
+
try:
|
| 9 |
+
detector = FER(mtcnn=False)
|
| 10 |
+
# Create a blank image
|
| 11 |
+
img = np.zeros((100, 100, 3), dtype=np.uint8)
|
| 12 |
+
# Add a white circle (to represent a face-ish thing, though FER might not find a face)
|
| 13 |
+
cv2.circle(img, (50, 50), 30, (255, 255, 255), -1)
|
| 14 |
+
|
| 15 |
+
print("Testing FER detector...")
|
| 16 |
+
emotions = detector.detect_emotions(img)
|
| 17 |
+
print(f"Result: {emotions}")
|
| 18 |
+
print("FER is working (even if no face found in blank image)")
|
| 19 |
+
except Exception as e:
|
| 20 |
+
print(f"FER error: {e}")
|
| 21 |
+
|
| 22 |
+
if __name__ == "__main__":
|
| 23 |
+
test_fer()
|
train_custom_model.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# Set Keras backend to torch for Python 3.14 compatibility
|
| 5 |
+
os.environ["KERAS_BACKEND"] = "torch"
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import torch.nn as nn
|
| 9 |
+
import torch.optim as optim
|
| 10 |
+
from torch.utils.data import Dataset, DataLoader
|
| 11 |
+
import cv2
|
| 12 |
+
import numpy as np
|
| 13 |
+
from PIL import Image
|
| 14 |
+
|
| 15 |
+
# Add backend to sys.path
|
| 16 |
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
| 17 |
+
backend_dir = os.path.join(base_dir, "backend")
|
| 18 |
+
sys.path.insert(0, backend_dir)
|
| 19 |
+
|
| 20 |
+
from app.database import SessionLocal
|
| 21 |
+
from app.models import EmotionLog
|
| 22 |
+
from fer import FER
|
| 23 |
+
|
| 24 |
+
# 1. Simple Neural Network to "learn" new emotions
|
| 25 |
+
class EmotionClassifier(nn.Module):
|
| 26 |
+
def __init__(self, input_size, num_classes):
|
| 27 |
+
super(EmotionClassifier, self).__init__()
|
| 28 |
+
self.fc1 = nn.Linear(input_size, 64)
|
| 29 |
+
self.relu = nn.ReLU()
|
| 30 |
+
self.fc2 = nn.Linear(64, num_classes)
|
| 31 |
+
|
| 32 |
+
def forward(self, x):
|
| 33 |
+
return self.fc2(self.relu(self.fc1(x)))
|
| 34 |
+
|
| 35 |
+
def train():
|
| 36 |
+
db = SessionLocal()
|
| 37 |
+
detector = FER(mtcnn=False)
|
| 38 |
+
|
| 39 |
+
# Get all logs that have a corrected emotion
|
| 40 |
+
logs = db.query(EmotionLog).filter(EmotionLog.corrected_emotion != None).all()
|
| 41 |
+
|
| 42 |
+
# Check unique emotions count
|
| 43 |
+
all_emotions = sorted(list(set([log.corrected_emotion for log in logs])))
|
| 44 |
+
|
| 45 |
+
if len(all_emotions) < 2:
|
| 46 |
+
print(f"FAILED: Not enough variety! You only have one corrected emotion: {all_emotions}. Correct at least two different images with different emotions to train the custom brain.")
|
| 47 |
+
sys.exit(1)
|
| 48 |
+
|
| 49 |
+
if len(logs) < 3:
|
| 50 |
+
print(f"FAILED: Not enough data! You only have {len(logs)} corrections. Please correct at least 3 images (from different photos) to help the AI learn.")
|
| 51 |
+
sys.exit(1)
|
| 52 |
+
|
| 53 |
+
# Prepare labels and mappings
|
| 54 |
+
emotion_to_idx = {emo: i for i, emo in enumerate(all_emotions)}
|
| 55 |
+
idx_to_emotion = {i: emo for emo, i in emotion_to_idx.items()}
|
| 56 |
+
|
| 57 |
+
X = []
|
| 58 |
+
y = []
|
| 59 |
+
|
| 60 |
+
print(f"Preparing data for emotions: {all_emotions}")
|
| 61 |
+
|
| 62 |
+
for log in logs:
|
| 63 |
+
# Load the image
|
| 64 |
+
img_path = os.path.join(backend_dir, "data", log.image_path)
|
| 65 |
+
if not os.path.exists(img_path):
|
| 66 |
+
print(f"Skipping missing image: {img_path}")
|
| 67 |
+
continue
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
img = cv2.imread(img_path)
|
| 71 |
+
if img is None:
|
| 72 |
+
print(f"Skipping unreadable image: {img_path}")
|
| 73 |
+
continue
|
| 74 |
+
# Use FER to detect face and get the emotion probabilities (features)
|
| 75 |
+
results = detector.detect_emotions(img)
|
| 76 |
+
if results:
|
| 77 |
+
# We take the 7 base emotion probabilities as features
|
| 78 |
+
# Sort keys to ensure consistent feature order matching inference
|
| 79 |
+
emo_dict = results[0]["emotions"]
|
| 80 |
+
features = [emo_dict[k] for k in sorted(emo_dict.keys())]
|
| 81 |
+
X.append(features)
|
| 82 |
+
y.append(emotion_to_idx[log.corrected_emotion])
|
| 83 |
+
else:
|
| 84 |
+
# If FER fails on this specific image, we use neutral features
|
| 85 |
+
print(f"Warning: FER could not find face in {img_path}, skipping.")
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print(f"Error processing {img_path}: {e}")
|
| 88 |
+
continue
|
| 89 |
+
|
| 90 |
+
if not X:
|
| 91 |
+
print("FAILED: Could not extract features from any of your corrected images.")
|
| 92 |
+
sys.exit(1)
|
| 93 |
+
|
| 94 |
+
X = torch.tensor(X, dtype=torch.float32)
|
| 95 |
+
y = torch.tensor(y, dtype=torch.long)
|
| 96 |
+
|
| 97 |
+
# 2. Train the model
|
| 98 |
+
model = EmotionClassifier(input_size=7, num_classes=len(all_emotions))
|
| 99 |
+
criterion = nn.CrossEntropyLoss()
|
| 100 |
+
optimizer = optim.Adam(model.parameters(), lr=0.01)
|
| 101 |
+
|
| 102 |
+
print("Training the custom brain...")
|
| 103 |
+
# Increase epochs for small dataset to ensure convergence
|
| 104 |
+
for epoch in range(200):
|
| 105 |
+
optimizer.zero_grad()
|
| 106 |
+
outputs = model.forward(X)
|
| 107 |
+
loss = criterion(outputs, y)
|
| 108 |
+
loss.backward()
|
| 109 |
+
optimizer.step()
|
| 110 |
+
if (epoch+1) % 50 == 0:
|
| 111 |
+
print(f"Epoch [{epoch+1}/200], Loss: {loss.item():.4f}")
|
| 112 |
+
|
| 113 |
+
# 3. Save the custom model and the label mapping
|
| 114 |
+
save_path = os.path.join(backend_dir, "app/models/custom_ai")
|
| 115 |
+
os.makedirs(save_path, exist_ok=True)
|
| 116 |
+
|
| 117 |
+
torch.save(model.state_dict(), os.path.join(save_path, "custom_weights.pth"))
|
| 118 |
+
|
| 119 |
+
import json
|
| 120 |
+
with open(os.path.join(save_path, "labels.json"), "w") as f:
|
| 121 |
+
json.dump(idx_to_emotion, f)
|
| 122 |
+
|
| 123 |
+
print(f"SUCCESS: Custom brain trained and saved. It now knows: {all_emotions}")
|
| 124 |
+
|
| 125 |
+
if __name__ == "__main__":
|
| 126 |
+
train()
|
update_db_schema.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
db_path = 'backend/data/neurosense.db'
|
| 5 |
+
|
| 6 |
+
if not os.path.exists(db_path):
|
| 7 |
+
print(f"Error: Database not found at {db_path}")
|
| 8 |
+
else:
|
| 9 |
+
conn = sqlite3.connect(db_path)
|
| 10 |
+
cursor = conn.cursor()
|
| 11 |
+
|
| 12 |
+
# Check current columns
|
| 13 |
+
cursor.execute('PRAGMA table_info(children)')
|
| 14 |
+
cols = [c[1] for c in cursor.fetchall()]
|
| 15 |
+
|
| 16 |
+
if 'autism_inheritance' not in cols:
|
| 17 |
+
print("Adding autism_inheritance...")
|
| 18 |
+
cursor.execute("ALTER TABLE children ADD COLUMN autism_inheritance VARCHAR DEFAULT ''")
|
| 19 |
+
|
| 20 |
+
if 'sensory_level' not in cols:
|
| 21 |
+
print("Adding sensory_level...")
|
| 22 |
+
cursor.execute("ALTER TABLE children ADD COLUMN sensory_level VARCHAR DEFAULT 'standard'")
|
| 23 |
+
|
| 24 |
+
conn.commit()
|
| 25 |
+
conn.close()
|
| 26 |
+
print("Database updated successfully.")
|